This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/mission-objective-vocabulary.md
Sylpheed RE agent 9a9921b759 re: three ISL built-in names were wrong, including the most-used one
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
2026-08-25 22:09:05 +00:00

118 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.