From 81e9267e31ec518b05b56a96ceab98630613daf7 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 12:42:50 +0000 Subject: [PATCH] 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. --- docs/re/data/isl-stage02-conditions.txt | 345 ++++++++++++++++++++++++ docs/re/structures/isl-builtins.md | 35 +++ tools/re-capture/isl.py | 51 +++- 3 files changed, 426 insertions(+), 5 deletions(-) create mode 100644 docs/re/data/isl-stage02-conditions.txt diff --git a/docs/re/data/isl-stage02-conditions.txt b/docs/re/data/isl-stage02-conditions.txt new file mode 100644 index 00000000..f0065f79 --- /dev/null +++ b/docs/re/data/isl-stage02-conditions.txt @@ -0,0 +1,345 @@ +# Stage02.ssb -- condition sites, symbols resolved +# unit args are symbol-table-2 indices; resolved to names. + +== set_flag: 12 sites + -- 0x2BE8 + 002A50: builtin91(0x1, 0x6F, 0x1) + 002A8C: builtin91(0x1, 0x0, 0x1) + 002AB0: start_coroutine(0x4C2C) + 002AD4: start_coroutine(0x64C0) + 002AF8: start_coroutine(0x6198) + 002B7C: builtin75(0x39, 0x3A, 0x3B, 0x3C, 0x3D) + 002BA0: play_stream(0x3ED) + 002BC4: builtin38(0x1) + 002BE8: set_flag(0x5) + -- 0x2DCC + 002A8C: builtin91(0x1, 0x0, 0x1) + 002AB0: start_coroutine(0x4C2C) + 002AD4: start_coroutine(0x64C0) + 002AF8: start_coroutine(0x6198) + 002B7C: builtin75(0x39, 0x3A, 0x3B, 0x3C, 0x3D) + 002BA0: play_stream(0x3ED) + 002BC4: builtin38(0x1) + 002BE8: set_flag(0x5) + 002BF4: yield + -- 0xF7C0 + 00F4E4: objective_marker(0x1, 0x2D, 0x0, 0x8, 0x0) + 00F524: unit_state(0x1, ADN110) + 00F5A8: objective_marker(0x1, 0x1, 0x0, 0x8, 0x0) + 00F5E8: unit_state(0x1, ADN111) + 00F66C: objective_marker(0x1, 0x2, 0x0, 0x8, 0x0) + 00F6AC: unit_state(0x1, ADN112) + 00F730: objective_marker(0x1, 0x5, 0x0, 0x8, 0x0) + 00F79C: objective_marker(0x1, 0x3A, 0x1, 0x8, 0x0) + 00F7C0: set_flag(0x8) + -- 0x183B8 + 018220: start_coroutine(0x77D0) + 018244: start_coroutine(0x8718) + 018268: start_coroutine(0x91DC) + 0182A4: builtin79(0x1, 0x6, 0x1, 0x75) + 0182C8: start_coroutine(0x9264) + 01834C: builtin75(0xBC, 0xBD, 0xBE, 0xBF, 0xC0) + 018370: play_stream(0x3EB) + 018394: builtin38(0x1) + 0183B8: set_flag(0x5) + -- 0x18504 + 018244: start_coroutine(0x8718) + 018268: start_coroutine(0x91DC) + 0182A4: builtin79(0x1, 0x6, 0x1, 0x75) + 0182C8: start_coroutine(0x9264) + 01834C: builtin75(0xBC, 0xBD, 0xBE, 0xBF, 0xC0) + 018370: play_stream(0x3EB) + 018394: builtin38(0x1) + 0183B8: set_flag(0x5) + 0183C4: yield + -- 0x1A8B8 + 01A56C: unit_alive(0x1, ADN227) + 01A5C8: unit_alive(0x1, ADN228) + 01A624: unit_alive(0x1, ADN229) + 01A680: unit_alive(0x1, ADN230) + 01A6DC: unit_alive(0x1, ADN231) + 01A738: unit_alive(0x1, ADN232) + 01A824: builtin64(0xC3, 0x2, 0x1, 0x9, 0x1, -1) + 01A87C: named_event(0xC4) + 01A8B8: set_flag(0xE) + -- 0x1A980 + 01A680: unit_alive(0x1, ADN230) + 01A6DC: unit_alive(0x1, ADN231) + 01A738: unit_alive(0x1, ADN232) + 01A824: builtin64(0xC3, 0x2, 0x1, 0x9, 0x1, -1) + 01A87C: named_event(0xC4) + 01A8B8: set_flag(0xE) + 01A914: hp_pct_test(0x1, TCT206, 89.9) + 01A950: named_event(0xC5) + 01A980: set_flag(0xF) + -- 0x23750 + 023474: objective_marker(0x1, 0x1B, 0x0, 0x8, 0x0) + 0234B4: unit_state(0x1, ADN206) + 023538: objective_marker(0x1, 0x22, 0x0, 0x8, 0x0) + 023578: unit_state(0x1, ADN208) + 0235FC: objective_marker(0x1, 0x2F, 0x0, 0x8, 0x0) + 02363C: unit_state(0x1, ADN209) + 0236C0: objective_marker(0x1, 0x35, 0x0, 0x8, 0x0) + 02372C: objective_marker(0x1, 0x40, 0x1, 0x8, 0x0) + 023750: set_flag(0x8) + -- 0x289D0 + 028808: builtin91(0x1, 0x56, 0x1) + 028844: builtin91(0x1, 0x74, 0x1) + 028880: builtin91(0x1, 0x78, 0x1) + 0288BC: builtin91(0x1, 0x5D, 0x1) + 0288E0: start_coroutine(0x694C) + 028964: builtin75(0x124, 0x125, 0x126, 0x127, 0x128) + 028988: play_stream(0x3EE) + 0289AC: builtin38(0x1) + 0289D0: set_flag(0x5) + -- 0x28B1C + 028844: builtin91(0x1, 0x74, 0x1) + 028880: builtin91(0x1, 0x78, 0x1) + 0288BC: builtin91(0x1, 0x5D, 0x1) + 0288E0: start_coroutine(0x694C) + 028964: builtin75(0x124, 0x125, 0x126, 0x127, 0x128) + 028988: play_stream(0x3EE) + 0289AC: builtin38(0x1) + 0289D0: set_flag(0x5) + 0289DC: yield + -- 0x2CB60 + 02C900: hp_pct_test(0x1, ADN327, 0) + 02C954: hp_pct_test(0x1, ADN328, 0) + 02C9A8: hp_pct_test(0x1, ADN329, 0) + 02C9FC: hp_pct_test(0x1, ADN330, 0) + 02CA50: hp_pct_test(0x1, ADN331, 0) + 02CAA4: hp_pct_test(0x1, ADN332, 0) + 02CAF8: hp_pct_test(0x1, ADS351, 0) + 02CB30: named_event(0x132) + 02CB60: set_flag(0xE) + -- 0x2CC68 + 02C9FC: hp_pct_test(0x1, ADN330, 0) + 02CA50: hp_pct_test(0x1, ADN331, 0) + 02CAA4: hp_pct_test(0x1, ADN332, 0) + 02CAF8: hp_pct_test(0x1, ADS351, 0) + 02CB30: named_event(0x132) + 02CB60: set_flag(0xE) + 02CC00: builtin64(0x133, 0x2, 0x1, 0x9, 0x1, -1) + 02CC38: named_event(0x134) + 02CC68: set_flag(0xF) + +== END_PHASE: 12 sites + -- 0x51E4 + 004EFC: builtin64(0x3F, 0x2, 0x1, 0x9, 0x1, -1) + 004FA4: builtin64(0x40, 0x2, 0x1, 0x9, 0x1, -1) + 00504C: builtin64(0x41, 0x2, 0x1, 0x9, 0x1, -1) + 0050F4: builtin64(0x42, 0x2, 0x1, 0x9, 0x1, -1) + 005160: wait_cmds_drained + 005188: fade_sound(3) + 0051B0: builtin85(3) + 0051D8: wait_s(3) + 0051E4: END_PHASE + -- 0x5828 + 005518: builtin64(0x1, 0x9, 0x1, -1) + 0055C0: builtin64(0x46, 0x2, 0x1, 0x9, 0x1, -1) + 005668: builtin64(0x47, 0x2, 0x1, 0x9, 0x1, -1) + 005710: builtin64(0x48, 0x2, 0x1, 0x9, 0x1, -1) + 0057A4: wait_cmds_drained + 0057CC: fade_sound(3) + 0057F4: builtin85(3) + 00581C: wait_s(3) + 005828: END_PHASE + -- 0x6010 + 005E24: hp_pct_test(0x1, ADN112, 0) + 005E78: hp_pct_test(0x1, ADN114, 0) + 005ECC: hp_pct_test(0x1, ADN115, 0) + 005F80: builtin64(0x4A, 0x2, 0x1, 0x9, 0x1, -1) + 005F8C: wait_cmds_drained + 005FB4: fade_sound(3) + 005FDC: builtin85(3) + 006004: wait_s(3) + 006010: END_PHASE + -- 0x6260 + 0060FC: MARK_LAST_PHASE + 006108: builtin82 + 006130: wait_s(4) + 0061D0: builtin64(0x4B, 0x2, 0x1, 0x9, 0x1, -1) + 0061DC: wait_cmds_drained + 006204: fade_sound(3) + 00622C: builtin85(3) + 006254: wait_s(3) + 006260: END_PHASE + -- 0x19640 + 019358: builtin64(0x3F, 0x2, 0x1, 0x9, 0x1, -1) + 019400: builtin64(0x40, 0x2, 0x1, 0x9, 0x1, -1) + 0194A8: builtin64(0x41, 0x2, 0x1, 0x9, 0x1, -1) + 019550: builtin64(0x42, 0x2, 0x1, 0x9, 0x1, -1) + 0195BC: wait_cmds_drained + 0195E4: fade_sound(3) + 01960C: builtin85(3) + 019634: wait_s(3) + 019640: END_PHASE + -- 0x19934 + 019758: wait_s(4) + 0197F8: builtin64(0xC1, 0x2, 0x1, 0x9, 0x1, -1) + 019804: wait_cmds_drained + 0198A4: builtin64(0xC2, 0x2, 0x1, 0x9, 0x1, -1) + 0198B0: wait_cmds_drained + 0198D8: fade_sound(3) + 019900: builtin85(3) + 019928: wait_s(3) + 019934: END_PHASE + -- 0x1AC44 + 01AAE0: builtin64(0xC7, 0x2, 0x1, 0x9, 0x1, -1) + 01AAEC: wait_cmds_drained + 01AB44: read_freg(5) + 01AB80: read_freg(5) + 01ABC0: wait_s(5) + 01ABE8: fade_sound(3) + 01AC10: builtin85(3) + 01AC38: wait_s(3) + 01AC44: END_PHASE + -- 0x2B96C + 02B684: builtin64(0x3F, 0x2, 0x1, 0x9, 0x1, -1) + 02B72C: builtin64(0x40, 0x2, 0x1, 0x9, 0x1, -1) + 02B7D4: builtin64(0x41, 0x2, 0x1, 0x9, 0x1, -1) + 02B87C: builtin64(0x42, 0x2, 0x1, 0x9, 0x1, -1) + 02B8E8: wait_cmds_drained + 02B910: fade_sound(3) + 02B938: builtin85(3) + 02B960: wait_s(3) + 02B96C: END_PHASE + -- 0x2BFB0 + 02BCA0: builtin64(0x1, 0x9, 0x1, -1) + 02BD48: builtin64(0x46, 0x2, 0x1, 0x9, 0x1, -1) + 02BDF0: builtin64(0x47, 0x2, 0x1, 0x9, 0x1, -1) + 02BE98: builtin64(0x48, 0x2, 0x1, 0x9, 0x1, -1) + 02BF2C: wait_cmds_drained + 02BF54: fade_sound(3) + 02BF7C: builtin85(3) + 02BFA4: wait_s(3) + 02BFB0: END_PHASE + -- 0x2C1E0 + 02C060: builtin82 + 02C088: wait_s(4) + 02C0B0: wait_s(5) + 02C150: builtin64(0x131, 0x2, 0x1, 0x9, 0x1, -1) + 02C15C: wait_cmds_drained + 02C184: fade_sound(3) + 02C1AC: builtin85(3) + 02C1D4: wait_s(3) + 02C1E0: END_PHASE + -- 0x2CF74 + 02CDBC: wait_cmds_drained + 02CE14: read_freg(5) + 02CE50: read_freg(5) + 02CE90: wait_s(5) + 02CECC: builtin99 + 02CF18: fade_sound(3) + 02CF40: builtin85(3) + 02CF68: wait_s(3) + 02CF74: END_PHASE + -- 0x2D1DC + 02D06C: mark_not_last + 02D078: builtin78 + 02D0A0: wait_s(4) + 02D140: builtin64(0x136, 0x2, 0x1, 0x9, 0x1, -1) + 02D14C: wait_cmds_drained + 02D180: fade_sound(3) + 02D1A8: builtin85(3) + 02D1D0: wait_s(3) + 02D1DC: END_PHASE + +== FORCE_END_PHASE: 3 sites + -- 0x1482C + 0144B0: builtin64 + -- 0x249F0 + 0246B0: unit_state(0x1, ADN208) + 0246EC: start_coroutine(0xD5A8) + 0246F8: yield + 02474C: unit_state(0x1, ADN209) + 024788: start_coroutine(0xD5A8) + 024794: yield + -- 0x34A10 + 034868: unit_state(0x1, ADT305) + 03488C: yield + 0348E0: unit_state(0x1, ADT306) + 034904: yield + 034958: unit_state(0x1, ADT307) + 03497C: yield + 0349D0: unit_state(0x1, ADT308) + 0349F4: yield + 034A00: yield + +== MARK_LAST_PHASE: 8 sites + -- 0x4DFC + 004A94: start_coroutine(0x7290) + 004AB8: start_coroutine(0x8A68) + 004ADC: start_coroutine(0x93D8) + 004AE8: yield + -- 0x52C8 + 0051F0: yield + 005230: hp_pct_test(0x1, TCN004, 0) + 005268: builtin116(0x0) + 005274: builtin101 + 005280: push_trigger + 00528C: timer_stop + 0052B0: clear_flag(0xFFFFFFFF) + 0052BC: builtin118 + 0052C8: MARK_LAST_PHASE + -- 0x60FC + 00601C: yield + 006060: read_freg(0x0) + 00609C: builtin116(0x0) + 0060A8: builtin101 + 0060B4: push_trigger + 0060C0: timer_stop + 0060E4: clear_flag(0xFFFFFFFF) + 0060F0: builtin118 + 0060FC: MARK_LAST_PHASE + -- 0x19258 + 018F04: builtin58(0x1, 0x35, 0x1) + 018F5C: builtin15(0x1, 0x35, 0x2, 380) + 018FB0: builtin47(0x1, 0x35, 0x1, 0x75, 0x0) + 018FF0: hp_pct_test(0x1, ADN208, 0) + 019028: builtin63(0x1, 0x4E) + 01907C: builtin47(0x1, 0x4E, 0x1, 0x35, 0x1) + 0190D8: builtin47(0x1, 0x4E, 0x1, 0x35, 0x0) + 0190FC: start_coroutine(0xC48C) + 019108: yield + -- 0x19724 + 01964C: yield + 01968C: hp_pct_test(0x1, TCT206, 0) + 0196C4: builtin116(0x0) + 0196D0: builtin101 + 0196DC: push_trigger + 0196E8: timer_stop + 01970C: clear_flag(0xFFFFFFFF) + 019718: builtin118 + 019724: MARK_LAST_PHASE + -- 0x2B584 + 02B304: builtin47(0x1, 0x51, 0x1, 0x15, 0x0) + 02B328: start_coroutine(0x9680) + 02B34C: start_coroutine(0x9708) + 02B370: start_coroutine(0x9790) + 02B394: start_coroutine(0xA2D8) + 02B3B8: start_coroutine(0xA4F8) + 02B3DC: start_coroutine(0xAA10) + 02B47C: builtin64(0x130, 0x2, 0x0, 0x8, 0x1, -1) + 02B488: yield + -- 0x2BA50 + 02B978: yield + 02B9B8: hp_pct_test(0x1, TCN004, 0) + 02B9F0: builtin116(0x0) + 02B9FC: builtin101 + 02BA08: push_trigger + 02BA14: timer_stop + 02BA38: clear_flag(0xFFFFFFFF) + 02BA44: builtin118 + 02BA50: MARK_LAST_PHASE + -- 0x2C054 + 02BFB0: END_PHASE + 02BFBC: yield + 02BFF4: builtin116(0x0) + 02C000: builtin101 + 02C00C: push_trigger + 02C018: timer_stop + 02C03C: clear_flag(0xFFFFFFFF) + 02C048: builtin118 + 02C054: MARK_LAST_PHASE + diff --git a/docs/re/structures/isl-builtins.md b/docs/re/structures/isl-builtins.md index 0dbaec86..452cdbd1 100644 --- a/docs/re/structures/isl-builtins.md +++ b/docs/re/structures/isl-builtins.md @@ -110,6 +110,41 @@ is not. ⚠️ Scoped to Stage 02. Other stages may well use `squad_survival_pct` — the counting is per-file and cheap to repeat. +## ✅ A real Stage 02 condition, read end to end + +With the symbol tables resolved (unit arguments are symbol-table-2 indices), the +bytecode reads as mission logic. From `Stage02.ssb` at `0xF524` +(`data/isl-stage02-conditions.txt`): + +``` +unit_state(1, ADN110) objective_marker(1, 0x01, 0, 8, 0) +unit_state(1, ADN111) objective_marker(1, 0x02, 0, 8, 0) +unit_state(1, ADN112) objective_marker(1, 0x05, 0, 8, 0) + objective_marker(1, 0x3A, 1, 8, 0) +set_flag(8) +``` + +Three **named ADAN squadrons** are polled for lifecycle state, each with its +objective marker updated, and then **flag 8** is latched. That is the shape +[mission-phase-advance](../mission-phase-advance.md) predicted from the +disassembly alone — trigger → predicate → `set_flag` → (later) `END_PHASE` — now +seen in the mission's own code with the squadron names the roster tables already +gave us. + +The 12 `END_PHASE` sites are, by contrast, **outro sequences**: +`wait_cmds_drained` → `fade_sound(3)` → `builtin85(3)` → `wait_s(3)` → +`END_PHASE` → `yield`. The *decision* is not there; the terminator is. + +### ⚠️ A decode bug that hid every argument + +The first version of the argument tracker only followed +`local[i] = special[0]`. But the common form is +**`set.i k=01,03` — an immediate written straight into `local[i]`** — and +missing it meant every unit predicate printed with **no arguments at all** +(`unit_state` rather than `unit_state(1, ADN110)`). The disassembly looked +complete and was silently empty where it mattered most. Both staging forms are +now handled. + ## 🟡 Not settled * **Three handlers resisted**: id 55 (`vt35`, 411 instructions, returns a float), diff --git a/tools/re-capture/isl.py b/tools/re-capture/isl.py index c1140b78..2e322896 100755 --- a/tools/re-capture/isl.py +++ b/tools/re-capture/isl.py @@ -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]