re: survey all 28 mission scripts -- kill counters are dead game-wide

All 28 StageNN.ssb decoded: 2,085,628 bytes, 25,705 call sites, 108 of 147
built-ins used. Verified the survey independently -- 33=0, 34=0, 108 distinct,
hp_pct_test 1955, unit_state 1271, all exact.

global_counter0/1 have ZERO call sites in all 28 stages. The handlers exist and
are wired; no mission calls them. So 'does a wave start after N kills?' is
answered for the whole game, not just Stage 02: no mission counts kills.

Two idioms change how the counts read. hp_pct_test(unit, 0.0) IS a destruction
test -- 1786 of 1955 calls (91%) pass 0.0, and the handler's zero path
additionally requires state == 4 -- so unit_state and hp_pct_test(...,0) are
interchangeable and stages just pick one. And squad_survival_pct is a boolean,
not a percentage: an integer divwu before the x100 means it can only be 0 or
100, and all 29 sites pass a friendly TCAF squadron with threshold 99.9, making
it 'has this escort lost anybody'. The game has no destroy-N%-of-a-squadron
objective.

Outliers: S18-S23 (tutorials) have no flag/trigger machinery at all -- linear
lessons; S16 has no unit predicates, only a descending player-gauge ladder.

Not settled: group_ratio_pct takes two unit indices (blob+4 and blob+12) and its
numerator lookup was not read to the bottom, so it is not being labelled
'percent killed by the player'. isl.py's UNIT_ARG omitted 71/72.
This commit is contained in:
Sylpheed RE agent
2026-08-25 18:53:56 +00:00
parent da2d02db77
commit c50ab7275f
2 changed files with 83 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# 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**.
That settles the "does a wave start after N kills?" question far beyond Stage 02:
**no mission in the game counts kills.** 39 built-ins are never called at all,
including `timer_start`, `deploy_and_wait`, `camera_at`, `play_voice` and
`screen_fade`.
## ✅ `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` |
**S18S23, the tutorials, are the outliers by a distance.** No flag/trigger
machinery at all — zero `read_freg`, `clear_flag`, `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)** takes **two** unit indices — blob+4 and blob+12 —
and its numerator comes from a pair-keyed lookup through `[phase+328]` that
was not read to the bottom. The natural reading (a per-squadron ratio credited
to the player's flight) fits S01's usage, but is **not** established, so it is
not being called "percent killed by the player".
* `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.

View File

@@ -135,6 +135,9 @@ def symbols(b, which):
UNIT_ARG = {18, 20, 24, 26, 56, 69, 70, 94, 95, 105, 109} # unit idx at blob[4]
# Built-ins 71 and 72 carry a SECOND unit index at blob[12] (handler 0x8226E568
# indexes [phase+324] with both), so their second operand printed as a bare
# integer until this was noticed. 56 may be the same shape -- unverified.
def dis(b, off, count=40, code_base=0x24, args=True, sym2=None):