diff --git a/docs/re/mission-objective-vocabulary.md b/docs/re/mission-objective-vocabulary.md new file mode 100644 index 0000000..af49708 --- /dev/null +++ b/docs/re/mission-objective-vocabulary.md @@ -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, )` | +| 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`, `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. diff --git a/tools/re-capture/isl.py b/tools/re-capture/isl.py index 269358b..1fed8c8 100755 --- a/tools/re-capture/isl.py +++ b/tools/re-capture/isl.py @@ -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):