We consume 18-22 generator words per turn and model none of them as a count.
All of it is inside StrategyServer::ProcessTurn; OnAllCombatDone_Tail costs 0
on every turn observed; the residual outside the two drivers is exactly 0. The
generator does not move between turns at all, so the interval a standalone has
to reproduce is closed at both ends.
The instrument reads generator STATE, not calls, and that choice paid: the
image has four draw entry points, not three (NextUInt 0x004f7670 is in no
lane's primitive set) plus inlined draws in twelve functions, two reachable
from the turn roots. A primitive-counting hook would have undercounted
silently.
Checked against the save files independently: the turn-6 autosave pair gives
18 words read from the two Sim.RNG blobs, and with twists == 0 that number
never passes through a twist implementation -- so the two instruments do not
share the hidden assumption they could have.
Corrections to combat-done-tail.md, in place:
* the node-line 0x20000-fleet check runs AFTER the Chance(0.5f) call and
cannot gate the draw; the expiry test is NodePath::RemainingLife 0x006e2130
and is now a formula rather than a description
* StrategyHost::Autosave is ret 8 and returns the std::string* in EAX
* SNMAllCombatDone IS delivered every End Turn (8 of 8) -- lane K's inference
was right; the stronger no-encounter reading is narrowed, not closed
* S+0x8 advances 12-14 times per turn, not twice
Node-line decay still has not fired. The hook reports the distance instead of
the absence: 51 of 53 lines are permanent, the mortal ones are dug ~1/turn by
the Zuul, each ~40 turns from expiry. It stays a labelled hypothesis.
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""Independent check of the live ledger: pull the RNG blob out of two save files and
|
|
compute the word delta from the FILES, with no reference to the shim's numbers."""
|
|
import sys, os
|
|
sys.path.insert(0, os.path.expanduser('~/sots-re/verify/save-reader'))
|
|
import save_reader as sr
|
|
|
|
N, M = 624, 397
|
|
def twist(mt):
|
|
mt = list(mt)
|
|
for kk in range(N - M):
|
|
y = (mt[kk] & 0x80000000) | (mt[kk+1] & 0x7fffffff)
|
|
mt[kk] = mt[kk+M] ^ (y >> 1) ^ (0x9908b0df if y & 1 else 0)
|
|
for kk in range(N - M, N - 1):
|
|
y = (mt[kk] & 0x80000000) | (mt[kk+1] & 0x7fffffff)
|
|
mt[kk] = mt[kk+(M-N)] ^ (y >> 1) ^ (0x9908b0df if y & 1 else 0)
|
|
y = (mt[N-1] & 0x80000000) | (mt[0] & 0x7fffffff)
|
|
mt[N-1] = mt[M-1] ^ (y >> 1) ^ (0x9908b0df if y & 1 else 0)
|
|
return mt
|
|
|
|
def find_rng(node, out):
|
|
name = getattr(node, 'name', None)
|
|
if name == 'RNG':
|
|
out.append(node)
|
|
for c in getattr(node, 'children', []) or []:
|
|
find_rng(c, out)
|
|
|
|
def rng_of(path):
|
|
res = sr.read_save(path)
|
|
hits = []
|
|
find_rng(res.tree, hits)
|
|
if not hits:
|
|
raise SystemExit(f"no RNG frame in {path}")
|
|
n = hits[0]
|
|
raw = n.raw
|
|
if not raw and n.children:
|
|
raw = b"".join(c.raw for c in n.children)
|
|
if not raw:
|
|
raise SystemExit(f"RNG frame in {path} carries no bytes")
|
|
return raw
|
|
|
|
def parse(raw):
|
|
# the blob is mt[624] then left, little-endian; tolerate a leading/trailing frame byte
|
|
for off in range(0, len(raw) - 2500 + 1):
|
|
if len(raw) - off < 2500: continue
|
|
mt = [int.from_bytes(raw[off+4*i:off+4*i+4], 'little') for i in range(N)]
|
|
left = int.from_bytes(raw[off+2496:off+2500], 'little', signed=True)
|
|
if 0 <= left <= N:
|
|
return mt, left, off, len(raw)
|
|
raise SystemExit(f"cannot parse RNG blob of {len(raw)} bytes")
|
|
|
|
a, b = sys.argv[1], sys.argv[2]
|
|
ra, rb = rng_of(a), rng_of(b)
|
|
ma, la, oa, na = parse(ra)
|
|
mb, lb, ob, nb = parse(rb)
|
|
print(f"{os.path.basename(a)}: blob {na} B, offset {oa}, left={la}")
|
|
print(f"{os.path.basename(b)}: blob {nb} B, offset {ob}, left={lb}")
|
|
cur, tw = ma, 0
|
|
while tw <= 64 and cur != mb:
|
|
cur = twist(cur); tw += 1
|
|
if cur != mb:
|
|
raise SystemExit("second block is not on the first block's chain within 64 twists")
|
|
words = 624*tw + (la - lb)
|
|
print(f"twists={tw} words consumed between the two files = {words}")
|