From 009bca880e2bcd3cdece2b77e4771d455c261d72 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 22:51:07 +0000 Subject: [PATCH] =?UTF-8?q?re:=20an=20ISL=20symbol=20operand=20is=20a=20(t?= =?UTF-8?q?ag,=20index)=20pair=20=E2=80=94=20and=20two=20more=20names=20wi?= =?UTF-8?q?thdrawn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified rather than adopted: a subagent proposed that every even operand slot is a type tag. Measured, the strong form is false and a precise form is true. TRUE: a SYMBOL operand is two words, a tag holding the constant 1 followed by the index. Slot 0 is the integer 1 in 19899/19899 calls whose slot 4 is a unit; slot 8 is tag-shaped in 100% of calls for every built-in taking a second unit; slot 16 is 1 in 152/152 for built-in 128, the only one taking a third. The 24 built-ins whose slot 0 is NOT the constant are exactly those taking no symbol there. This explains the unit slots 4/12/20 rather than replacing them. FALSE as stated: slot 8 is a bare double for built-ins 4, 20, 24, 26, 28, 29, 90, 106 and 127, and built-in 75 carries five bare indices at 0/4/8/12/16 with no tags at all. Each built-in has a fixed signature and is 100% self-consistent; none of the 34 with >=20 sites mixes the two. Symbol table 1 has three types -- 1 routes (1362), 6 messages (2247), 7 effects (81) -- and its operand slots are type-pure, measured the same way. Resolving them makes listings say what the script means: `request_script_message(MSG_VOICE_D_257, ...)`, a fourth independent confirmation of that name. Slots 24@4, 46@12 and 114@4 resolve 100% but MIX types 6 and 1, so they are left unresolved rather than guessed. Two more names withdrawn, neither replaced: * 88 `camera_at` -- ZERO call sites in all 28 stages; never testable. * 90 `camera_at_route` -- 8 sites, all Stage 02 phase 3, first operand is symtab-1 type 7 `eff_n0071`, an EFFECT name, in 8/8, with a per-missile Route_ADT301..308_p3M at slot 20. Not aimed at a camera. Left unnamed on purpose: replacing a guessed name with another guess is how the three names corrected earlier today went wrong. Also flagged: 115 `named_event`'s only symbol operand is an eff_* name in 84/84 sites, so that name is suspect too. Not renamed pending a handler read. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/data/isl-stage02.txt | 5 +-- docs/re/structures/isl-builtins.md | 61 ++++++++++++++++++++++++++++ tools/re-capture/isl.py | 65 +++++++++++++++++++++++++++--- tools/re-capture/isl_report.py | 4 +- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/docs/re/data/isl-stage02.txt b/docs/re/data/isl-stage02.txt index 3e192778..2aff4ba7 100644 --- a/docs/re/data/isl-stage02.txt +++ b/docs/re/data/isl-stage02.txt @@ -110,7 +110,6 @@ Generated by `tools/re-capture/isl_report.py calls`. 115 named_event 9 48 squadron_escort 8 39 MARK_LAST_PHASE 8 - 90 camera_at_route 8 17 wait_frames 6 10 random 6 123 timer_resume 5 @@ -140,7 +139,7 @@ resync from 0x5058 0050CC: 02030C00 set.i len=12 k=02,03 00000010 00000000 local[16] = special[0] 0050D8: 01021001 set.f len=16 k=01,02 00000000 BFF00000 00000000 special[0] = imm -1 0050E8: 02030C01 set.f len=12 k=02,03 00000018 00000000 local[24] = special[0] -0050F4: 00000C13 call len=12 k=00,00 00000040 00000245 request_script_message(0x42, 0x2, 0x1, 0x9, 0x1, -1) +0050F4: 00000C13 call len=12 k=00,00 00000040 00000245 request_script_message(MSG_VOICE_D_257, 0x2, 0x1, 0x9, 0x1, -1) 005100: 0000080C jmp len=8 k=00,00 0000507C -> code+0x507C (file 0x50A0) 005108: 0000080C jmp len=8 k=00,00 0000507C -> code+0x507C (file 0x50A0) 005110: 01020C0A op10 len=12 k=01,02 00000000 00000000 @@ -167,7 +166,7 @@ resync from 0x5058 005208: 01030C00 set.i len=12 k=01,03 00000004 00000056 local[4] = imm 0x56 005214: 01021001 set.f len=16 k=01,02 00000000 00000000 00000000 special[0] = imm 0 005224: 02030C01 set.f len=12 k=02,03 00000008 00000000 local[8] = special[0] -005230: 00000C13 call len=12 k=00,00 00000014 00000252 hp_pct_test(0x1, TCN004, 0) +005230: 00000C13 call len=12 k=00,00 00000014 00000252 hp_pct_test(TCN004, 0) 00523C: 01020C0A op10 len=12 k=01,02 00000000 00000001 005248: 0000080E op14 len=8 k=00,00 0000575C 005250: 01020C00 set.i len=12 k=01,02 00000000 00000000 special[0] = imm 0x0 diff --git a/docs/re/structures/isl-builtins.md b/docs/re/structures/isl-builtins.md index 6de7f955..e0acc9b1 100644 --- a/docs/re/structures/isl-builtins.md +++ b/docs/re/structures/isl-builtins.md @@ -140,6 +140,67 @@ and spread checks but its maximum *exceeds* the table — flag indices run 0..31 against symbol tables as small as 40 — so it is excluded, and the disassembler now declines to resolve it rather than printing an invented name. +### ✅ A symbol operand is a two-word pair: a **tag**, then the index + +This is why the unit indices sit at slots 4/12/20 and never at 0/8/16 — the even +slot in front of each is a tag word holding the constant **1**, and it is not an +argument. Measured over all 28 stages: + +* slot 0 is the integer **1 in 19 899 / 19 899** calls whose slot 4 is a unit; +* slot 8 is tag-shaped in **100 %** of calls for every built-in taking a second + unit, and slot 16 is the constant 1 in **152/152** for built-in 128, the only + one taking a third; +* **24 built-ins have a slot 0 that is not the constant** — and every one of them + takes no symbol there (`start_coroutine` a code offset, `wait_s` a double, + `set_flag` an index). The tag appears exactly where a symbol does. + +⚠️ It does **not** generalise to "every even slot is a tag". Slot 8 is a *bare +double* for built-ins 4, 20, 24, 26, 28, 29, 90, 106 and 127, and built-in 75 +carries five bare symbol indices at 0/4/8/12/16 with no tags at all. Each +built-in has a fixed signature and is **100 % consistent with itself** — not one +of the 34 built-ins with ≥20 sites mixes the two shapes. The disassembler now +drops the tag word, so `hp_pct_test(0x1, TCN004, 0)` reads `hp_pct_test(TCN004, 0)`. + +### ✅ Symbol table 1 has three types, and its slots are type-pure + +| type | entries (28 stages) | what | +|---|---|---| +| 1 | 1362 | `Route_*` names | +| 6 | 2247 | message / objective names | +| 7 | 81 | `eff_*` effect names | + +Measured the same way as the unit slots — every observed value resolves, ≥5 +distinct values, resolved type pure: + +* **type 1** — 2@12, 3@12, 7@12, 16@12, 19@12, 25@12, 48@24, 90@20, 108@12, + 128@28, 136@4, 143@12 +* **type 6** — 64@0 (2683 sites), 75@0/4/8/12/16 +* **type 7** — 115@0 (84/84) + +Built-ins **24@4, 46@12 and 114@4** resolve 100 % but **mix type 6 and type 1**, +so the slot does not mean one thing; they are deliberately left unresolved. + +Resolving these makes the listings say what the script means: +`request_script_message(MSG_VOICE_D_257, …)` — which is a fourth, independent +confirmation of that name, since its first operand is literally a `MSG_VOICE_*`. + +🟡 **`115 named_event` is now suspect.** Its only symbol operand is an `eff_*` +**effect** name in 84/84 sites. The name is left alone pending a handler read, +but "named event" is probably not what it does. + +### ❌ `camera_at` and `camera_at_route` WITHDRAWN + +`isl.py` named built-ins 88 and 90. Both names are unsupported: + +* **88 has zero call sites** in all 28 stages, so the name was never testable. +* **90 has exactly 8**, all in Stage 02 phase 3 — the nine-cruise-missile act — + and its first operand resolves to symbol-table-1 **type 7, `eff_n0071`, an + effect name**, in 8/8, with a per-missile `Route_ADT301..308_p3M` at slot 20. + +Whatever 90 does, it is not aimed at a camera. Both are left **unnamed** rather +than renamed: replacing one guessed name with another is how the three names +above got wrong in the first place. + ❔ **New, unexplained:** symbol table 2 holds **two types**, 2 (1160 entries disc-wide) and 8 (249), and they are not interchangeable. Built-ins **95** and **128** take a type-2 unit at slot 4 and, at slot 12, an operand that is type 8 diff --git a/tools/re-capture/isl.py b/tools/re-capture/isl.py index d97d2b0d..a2811c7f 100755 --- a/tools/re-capture/isl.py +++ b/tools/re-capture/isl.py @@ -88,7 +88,13 @@ BUILTIN = { 48: 'squadron_escort', 52: 'play_stream', 53: 'sound_busy', 54: 'stop_sound', 56: 'unit_relation', 59: 'fade_sound', 62: 'FORCE_END_PHASE', 64: 'request_script_message', 69: 'unit_state', 70: 'unit_alive', 72: 'group_ratio_pct', 73: 'timer_start', - 74: 'timer_limit', 88: 'camera_at', 90: 'camera_at_route', + 74: 'timer_limit', + # ❌ 88 'camera_at' and 90 'camera_at_route' WITHDRAWN. 88 has ZERO call + # sites in all 28 stages, so its name was never testable. 90 has exactly 8, + # all in Stage 02 phase 3 (the cruise-missile act), and its first operand + # resolves to symbol-table-1 type 7 -- `eff_n0071`, an EFFECT name -- in + # 8/8, with a per-missile `Route_ADT30N_p3M` at slot 20. Whatever it does, + # it is not aimed at a camera. Left unnamed rather than renamed on a guess. 93: 'clear_flag', 94: 'is_engaged', 95: 'unit_hp_pct', 100: 'reset_phase_threads', 102: 'prompt_yes_no', 109: 'set_unit_flags', 115: 'named_event', 120: 'wait_cmds_drained', 123: 'timer_resume', 124: 'timer_stop', @@ -154,6 +160,46 @@ UNIT_ARG2 = {2, 18, 47, 48, 56, 79, 95, 128} # a SECOND unit index at blob[12 UNIT_ARG3 = {128} # and a third at blob[20] UNIT_SLOTS = {4: UNIT_ARG, 12: UNIT_ARG2, 20: UNIT_ARG3} +# WHY the unit indices sit at 4/12/20 and never at 0/8/16: a **symbol operand is +# a two-word pair** -- a tag word holding the constant 1, then the index. The +# tag is not data, so printing it puts a meaningless leading `0x1` in front of +# every unit predicate. +# +# Measured over all 28 stages: +# * slot 0 is the integer 1 in 19899 / 19899 calls whose slot 4 is a unit; +# * slot 8 is tag-shaped in 100% of calls for every built-in taking a second +# unit, and slot 16 is the constant 1 in 152/152 for built-in 128, the only +# one taking a third; +# * 24 built-ins have a slot 0 that is NOT the constant -- and every one of +# them takes no symbol at slot 4 (`start_coroutine` a code offset, `wait_s` +# a double, `set_flag` an index). +# +# The tag does NOT generalise to "every even slot is a tag": slot 8 is a bare +# double for built-ins 4, 20, 24, 26, 28, 29, 90, 106 and 127, and built-in 75 +# carries five bare symbol indices at 0/4/8/12/16 with no tags at all. Each +# built-in has a fixed signature and is 100% consistent with itself; none mixes. +TAG_SLOTS = {slot - 4 for slot in UNIT_SLOTS} + +# Symbol table 1 holds three types, and its slots were measured the same way as +# the unit slots (every observed value resolves, >=5 distinct values, and the +# resolved type is pure): +# type 1 (1362 entries) `Route_*` names +# type 6 (2247) message / objective names +# type 7 (81) `eff_*` effect names +SYM1_SLOTS = { + 0: {64, 75, 115}, # 64 & 75 type 6; 115 type 7 + 4: {75, 136}, + 8: {75}, + 12: {2, 3, 7, 16, 19, 25, 75, 108, 143}, + 16: {75}, + 20: {90}, + 24: {48}, + 28: {128}, +} +# Deliberately NOT listed: built-ins 24@4, 46@12 and 114@4 resolve 100% but mix +# type 6 and type 1, so the slot's meaning is not one thing. Recorded rather +# than guessed at. + # Symbol table 2 holds TWO entity types: type 2 (1160 entries disc-wide) and # type 8 (249). They are not interchangeable -- built-ins 95 and 128 take a # type-2 unit at slot 4 and, at slot 12, an operand that is type 8 in 100% of @@ -161,7 +207,7 @@ UNIT_SLOTS = {4: UNIT_ARG, 12: UNIT_ARG2, 20: UNIT_ARG3} # not yet established. -def dis(b, off, count=40, code_base=0x24, args=True, sym2=None): +def dis(b, off, count=40, code_base=0x24, args=True, sym2=None, sym1=None): out = [] staged = {} # local[] slot -> last value staged into it pending = None # value most recently put in special[0] @@ -221,15 +267,22 @@ def dis(b, off, count=40, code_base=0x24, args=True, sym2=None): extra = ' %s' % BUILTIN.get(words[0], 'builtin%d' % words[0]) if args and staged: parts = [] + # The tag word in front of a symbol operand is not an argument. + tags = {slot - 4 for slot, ids in UNIT_SLOTS.items() + if words[0] in ids and slot in staged} for slot, v in sorted(staged.items()): + if slot in tags and v == 1: + continue txt = ('0x%X' % v) if isinstance(v, int) else v - # Resolve only a slot that is declared a unit index AND - # whose value really is one -- a resolver that invents a - # name for a non-index is worse than one that prints the - # raw number. + # Resolve only a slot that is declared an index AND whose + # value really is one -- a resolver that invents a name for + # a non-index is worse than one that prints the raw number. if (sym2 and words[0] in UNIT_SLOTS.get(slot, ()) and isinstance(v, int) and v in sym2): txt = sym2[v][1] + elif (sym1 and words[0] in SYM1_SLOTS.get(slot, ()) + and isinstance(v, int) and v in sym1): + txt = sym1[v][1] parts.append(txt) extra += '(' + ', '.join(parts) + ')' staged = {} diff --git a/tools/re-capture/isl_report.py b/tools/re-capture/isl_report.py index 8bafdc14..b1c60c9e 100644 --- a/tools/re-capture/isl_report.py +++ b/tools/re-capture/isl_report.py @@ -27,7 +27,7 @@ def census(b): def emit_calls(b, path): cs, h = census(b) - s2 = isl.symbols(b, 2) + s1, s2 = isl.symbols(b, 1), isl.symbols(b, 2) print('# %s — ISL disassembly artefacts' % path) print() print('Generated by `tools/re-capture/isl_report.py calls`.') @@ -57,7 +57,7 @@ def emit_calls(b, path): target = [off for off, bid, _ in cs if bid == 6][0] start = isl.resync(b, target) print('resync from 0x%X' % start) - for line in isl.dis(b, start, 64, code_base=0x24, sym2=s2): + for line in isl.dis(b, start, 64, code_base=0x24, sym2=s2, sym1=s1): print(line)