re: a Stage 02 clear condition read end to end, with squadron names

Resolving symbol-table-2 indices turns the bytecode into mission logic. At
0xF524 Stage02.ssb polls unit_state on ADN110, ADN111 and ADN112, updates each
one's objective marker, then latches set_flag(8) -- exactly the
trigger/predicate/set_flag/END_PHASE shape predicted from the disassembly, now
observed in the mission's own code with names the roster tables already gave.

The 12 END_PHASE sites are outro sequences (wait_cmds_drained / fade_sound(3) /
builtin85(3) / wait_s(3) / END_PHASE / yield) -- the terminator, not the
decision.

Fixes a decode bug that hid every argument: the tracker only followed
local[i] = special[0], but the common form is an immediate written straight into
local[i] (k=01,03), so every unit predicate printed with NO arguments. The
disassembly looked complete while being empty exactly where it mattered.

Also records the live probe result: the phase mirror at [*(0x828F35F8)+236]
stayed 0 for ~530s of actively-hunting flight, no advance observed -- which is
what the static analysis predicts for phase 1, since ChangePhase only posts once
the ordinal exceeds 1.
This commit is contained in:
Sylpheed RE agent
2026-08-25 12:42:50 +00:00
parent 70cff9ca21
commit dcf37bcf93
3 changed files with 426 additions and 5 deletions

View File

@@ -105,7 +105,33 @@ def load(path):
return open(path, 'rb').read()
def dis(b, off, count=40, code_base=0x24, args=True):
def symbols(b, which):
"""Parse a .ssb symbol table -> {index: (type, name)}.
Built-in argument blobs carry INDICES into these: fields that index
`[phase+244]` are symtab-1 (routes, messages, subobjectives) and fields that
index `[phase+324]` are symtab-2 (the unit ids). Resolving them is what turns
`unit_state(0x2b)` into `unit_state(ADN201)`.
"""
off = struct.unpack_from('>I', b, 0x0C if which == 1 else 0x10)[0]
cnt = struct.unpack_from('>I', b, off)[0]
base = off + 4
out = {}
for i in range(cnt):
o = struct.unpack_from('>I', b, base + 4 * i)[0]
if o == 0:
continue
rp = base + o
typ = struct.unpack_from('>I', b, rp)[0]
e = b.index(b'\0', rp + 4)
out[i] = (typ, b[rp + 4:e].decode('latin-1'))
return out
UNIT_ARG = {18, 20, 24, 26, 56, 69, 70, 94, 95, 105, 109} # unit idx at blob[4]
def dis(b, off, count=40, code_base=0x24, args=True, sym2=None):
out = []
staged = {} # local[] slot -> last value staged into it
pending = None # value most recently put in special[0]
@@ -152,12 +178,26 @@ def dis(b, off, count=40, code_base=0x24, args=True):
pending = words[1]
elif k0 == 3 and k1 == 2 and pending is not None:
staged[words[0]] = pending
elif k0 == 3 and k1 == 1:
# local[i] = immediate, DIRECTLY -- the common form. Missing this
# made every unit predicate print with no arguments at all.
if op == 1:
lo = words[2] if len(words) > 2 else 0
staged[words[0]] = '%.6g' % struct.unpack(
'>d', struct.pack('>II', words[1], lo))[0]
else:
staged[words[0]] = words[1]
if op == 19 and words:
extra = ' %s' % BUILTIN.get(words[0], 'builtin%d' % words[0])
if args and staged:
extra += '(' + ', '.join(
'%s' % (('0x%X' % v) if isinstance(v, int) else v)
for _, v in sorted(staged.items())) + ')'
parts = []
for slot, v in sorted(staged.items()):
txt = ('0x%X' % v) if isinstance(v, int) else v
if (sym2 and slot == 4 and words[0] in UNIT_ARG
and isinstance(v, int) and v in sym2):
txt = sym2[v][1]
parts.append(txt)
extra += '(' + ', '.join(parts) + ')'
staged = {}
elif op == 12 and words:
extra = ' -> code+0x%X (file 0x%X)' % (words[0], code_base + words[0])
@@ -228,7 +268,8 @@ if __name__ == '__main__':
if st is None:
print('could not resync into 0x%X' % t); sys.exit(1)
print('resync from 0x%X' % st)
print('\n'.join(dis(b, st, int(sys.argv[4], 0) if len(sys.argv) > 4 else 40)))
print('\n'.join(dis(b, st, int(sys.argv[4], 0) if len(sys.argv) > 4 else 40,
sym2=symbols(b, 2))))
sys.exit(0)
code_base = struct.unpack_from('>I', b, CODE_BASE_FIELD)[0]
a = sys.argv[2]