All re-read twice — the handler, and the thing it calls — because each had been named from its shape rather than its effect. * id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ; stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from the active list and returns it to the free list. It destroys the thread. 2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was. * id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the thread parked at the target pc, or itself if the target is its own pc. It waits for nothing. * id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger container and then frees every thread whose pc differs from the caller's — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage 02 sites all sit in the phase terminator, next to timer_stop, clear_flag(-1) and MARK_LAST_PHASE. One name recovered from the game's own text: opcode 992 prints "RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message (2683 sites). Return codes documented properly: 1 = restart the coroutine from its entry (previously not recorded at all), 3 = terminate. And the blocking set was wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block; its handler ends `b 0x822724F8`, so it always returns 0. Unit-operand resolution settled from DATA over all 28 stages rather than by reading 147 handlers: a slot qualifies only if every value is a valid symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most of the table — that last clause is what discriminates, since every small integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the table, and the resolver now declines rather than inventing a name. New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and 128 take type 8 at slot 12 in 100% of their sites. A downstream inference is withdrawn with it: the note reading the live trigger counter attributed it to "the script arming watches as it goes" via built-in 100. The measurement stands; the attribution does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
118 lines
6.3 KiB
Markdown
118 lines
6.3 KiB
Markdown
# What the missions actually test — all 28 scripts surveyed
|
||
|
||
Status: ✅ corpus-wide counts, verified independently; ✅ two idioms identified
|
||
that change how the numbers read; 🟡 one built-in's semantics only partly read.
|
||
|
||
All 28 `Stage\StageNN.ssb` scripts decoded with `tools/re-capture/isl.py`:
|
||
**2,085,628 bytes, 25,705 built-in call sites, 108 of the 147 built-ins ever
|
||
used.** Nothing failed to parse. Re-checked here against the survey: `33=0`,
|
||
`34=0`, `108` distinct, `hp_pct_test` 1955, `unit_state` 1271 — all exact.
|
||
|
||
## ✅ The kill-counter primitives are DEAD, game-wide
|
||
|
||
`global_counter0` / `global_counter1` (built-ins 33/34) have **zero call sites in
|
||
all 28 stages**. Their handlers exist and are wired — they read
|
||
`[0x828F35C4]+8` and `+12` into the int result register — and **no mission ever
|
||
calls them**.
|
||
|
||
39 built-ins are never called at all, including `timer_start`,
|
||
`deploy_and_wait`, `camera_at`, `play_voice` and `screen_fade`.
|
||
|
||
### 🔴 CORRECTION: "no mission counts kills" was too strong
|
||
|
||
I wrote that here, and it is wrong as stated. What is true is narrower: **no
|
||
mission reads the two GLOBAL counters.** The game does track kills, and missions
|
||
do test them — through *attribution*, per unit and per squadron pair:
|
||
|
||
| built-in | what it is | sites |
|
||
|---|---|---|
|
||
| **105 `killed_by_player(unit)`** | reads `[squadron+600]`, which the unit-death handler sets from the damage message's `_BY_PLAYER` flag — "was the most recent death in this squadron the player's doing" | **117 across 14 stages** (S02: 38, S01: 32) |
|
||
| **72** | numerator is a per-`(killer, victim)` **squadron kill counter** incremented in the unit-death handler | 13, **S01 only** |
|
||
|
||
`killed_by_player` is the 17th-most-used built-in in Stage 02. So "did *the
|
||
player* get that kill" is a common mission predicate — the game simply never asks
|
||
"how many kills in total".
|
||
|
||
The evidence for 105 is the game's own strings: the death handler branches on
|
||
that flag into `"KILLED_BY_PLAYER"` versus `"KILLED"`, alongside
|
||
`LOST_TURRET_BY_PLAYER`, `SHIELD_DOWNED_BY_PLAYER`,
|
||
`DAMAGED_BY_PLAYER_GREATER_50` and the rest of the family.
|
||
|
||
## ✅ `hp_pct_test(unit, 0.0)` *is* a destruction test
|
||
|
||
**1786 of 1955 `hp_pct_test` calls (91 %) pass threshold `0.0`** — 107 of
|
||
Stage 02's 109 resolvable sites. Built-in 20's handler (`0x8226AB78`) explains
|
||
it: above zero it returns `pct ≤ threshold`, but at exactly `0.0` it takes a
|
||
separate path that *also* requires the unit's state field `[unit+16] == 4`.
|
||
|
||
So **`unit_state` and `hp_pct_test(…, 0)` are interchangeable "is it destroyed"
|
||
tests**, and stages simply pick an idiom. Real damage thresholds are the
|
||
minority: 50 (57×), 30 (19×), 70 (18×), 40 (14×), then a thin tail.
|
||
|
||
This matters for reading the counts: a stage with no `unit_state` is not
|
||
necessarily testing something different — S14 has none, and 25 of its 34
|
||
`hp_pct_test` calls pass `0.0`.
|
||
|
||
## ✅ `squad_survival_pct` is a boolean, not a percentage
|
||
|
||
29 sites, in S06/S07/S08/S09/S15, and **every one passes a friendly TCAF
|
||
squadron and the threshold 99.9**. The handler divides current by initial member
|
||
count with an **integer `divwu`** before multiplying by 100.0, so the value can
|
||
only be **100 or 0**.
|
||
|
||
At a 99.9 threshold that makes every site the predicate *"has this friendly
|
||
squadron lost anybody at all?"* — an escort-casualty test. **The game has no
|
||
"destroy N% of a squadron" objective.**
|
||
|
||
## ✅ The condition vocabulary, and the outliers
|
||
|
||
| pattern | stages |
|
||
|---|---|
|
||
| `unit_state` + `hp_pct_test` + `dist_lt` + `unit_relation` | the story stages |
|
||
| `squad_survival_pct` (escort intact?) | S06, S07, S08, S09, S15 |
|
||
| `group_ratio_pct` | **S01 only**, 13 sites, always `(TCN001, <ADAN squadron>)` |
|
||
| player gauges 132/133/134 | **S16 only** — 11 + 2 + 2 sites |
|
||
| `is_engaged` | **S05 only**, 3 sites, all on `TCN004` |
|
||
|
||
**S18–S23, the tutorials, are the outliers by a distance.** No flag/trigger
|
||
machinery at all — zero `read_freg`, `clear_flag`, `reset_phase_threads`
|
||
(built-in 100, formerly mis-named `push_trigger`),
|
||
`named_event`, `MARK_LAST_PHASE`, `FORCE_END_PHASE`, and none of the timer
|
||
family. One `set_flag` and one `END_PHASE` each. They are linear scripted
|
||
lessons, not missions.
|
||
|
||
**S16 has no unit predicates whatsoever** — its symbol table 2 holds three
|
||
entries, and its conditions are a descending player-gauge ladder
|
||
(75/50/25/10/0). A player-state set piece.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* ✅ **`group_ratio_pct` (72) — read to the bottom, and the old label was wrong
|
||
in the numerator.** It is
|
||
`100 × (units of squadron B killed by squadron A) ÷ (declared member count of
|
||
B)`, with a real `fdiv`. The numerator is `e[+12]` of a 16-byte entry in the
|
||
table at `0x828F363C`, created by an interpreter command (max 32 entries) and
|
||
incremented inside the unit-death handler as
|
||
`(killer squadron id, victim squadron id)`. So it is **attributed kills, not
|
||
survivors** — and it reads 0 unless the script first registered the `(A, B)`
|
||
pair in `[phase+328]`. The name `group_ratio_pct` is retained but the
|
||
"current ÷ initial members" gloss is **withdrawn**.
|
||
* ✅ **Built-in 55 is effectively dead**: **0 call sites corpus-wide**. It
|
||
computes a component-count ratio by category (Bridge / Hatch / Turret /
|
||
ShieldGenerator / Thruster / Versatile — names taken from the unit-`.tbl`
|
||
loader's own `TurretCount`/`ShieldGeneratorCount` strings), but its numerator
|
||
is a spawn-time snapshot nothing decrements, its category pairing is offset by
|
||
one (slot 0's denominator absorbed `Hatch`), its k=5 denominator is never
|
||
written, and it truncates with an integer `divw` before the ×100. Whether the
|
||
misalignment is a shipped bug is **unknown**.
|
||
* ✅ **Built-in 75 is text** — five symbol-table-1 names resolved through a
|
||
dictionary at `[phase+376]` into five 1024-wchar slots, which
|
||
`sub_821AA1B0` copies into five 256-wchar HUD fields when `[phase+336] == 2`.
|
||
Mechanism confirmed; **which** text (objectives, briefing, subtitles) is not.
|
||
39 sites, 3 in Stage 02.
|
||
* `isl.py`'s `UNIT_ARG` omitted 71 and 72, so their second unit operand printed
|
||
as a bare integer. Noted in the tool; 56 may be the same shape, unverified.
|
||
* The timer names in `isl.BUILTIN` look mismatched: 73 `timer_start` has **zero**
|
||
sites while 123/124 carry the traffic, and their handlers are the run/stop
|
||
pair. Flagged, not renamed.
|