Commit Graph

19 Commits

Author SHA1 Message Date
Sylpheed RE agent
3dc3fbadb8 re: built-in 15's write side verified; its consumer NOT found, filters recorded
Read directly at 0x8232C7CC, the unit message pump switches on [msg+36]:

  mode 1  [unit+196] = [unit+192]     restore the unit's stored default
  mode 2  [unit+196] = [r27+13912]    a global constant
  mode 3  [unit+196] = [msg+40]       the value the script passed

So the field has a per-unit default at +192 and a distinguished global value.
The shipped scripts only ever use mode 3 -- slot 8 is the double tag in
1961/1961 call sites -- so the two defaulting modes are dead in this build.

The consumer I did not find, and the failed approaches are recorded so the
next attempt does not repeat them:

* searching the flight/AI range for 196(rN) gives 170 hits, almost all on
  unrelated structs -- the N(rM) trap this file already warns about;
* narrowing to functions touching BOTH +192 and +196 still leaves 50;
* unit_definition_layout.txt names offsets 192/196 as AV_PitchMinus_Max/Min,
  but that is the unit DEFINITION object (vtable 0x820af844), not the spawned
  entity built-in 15 writes to. It does not apply, and taking it would have
  been an easy wrong turn -- angular-velocity names would have "explained"
  the turret anomaly while being about a different struct entirely.

What would settle it is pinning the spawned-entity struct first, then reading
+196's consumers within it.

Built-in 15 stays unnamed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 01:03:06 +00:00
Sylpheed RE agent
dd3c27f288 re: built-in 15 takes a craft-class scalar — but "set_speed" does not survive
15 is the biggest unnamed built-in: 1360 sites across 27 of 28 stages, with
an invariant (unit, double) operand shape. Joining every call site's unit to
its craft type through stage\UnitGroup_S<NN>.tbl resolves 1360 of 1360, none
unknown, and the values stratify hard:

  capital hulls (Destroyer, Carrier, CruiserEX, Cruiser, Acropolis,
                 BattleshipEX, ASFrigate)                        0-100
  mobile craft  (Elan 500, ArrowHead 400, DeltaSaber_T 400,
                 DeltaSaber_W 600, Attacker_S 250-400,
                 Phantom 300)                                  250-600
  asteroid      (mn040_Asteroid_Big, 74 sites)                  0, only 0

Capital ships crawl, fighters run, the player's craft is fastest at 600, and
a rock never moves. That is a speed's signature.

Except turrets break it. UN_e007_ADAN_Turret carries 400 x89 / 280 x17 /
380 x4 / 250 x2 and UN_e008_ADAN_TurretPlus 450 x62 / 500 x2 -- 176 of the
1360 sites, 13%, at fighter-class values on something that does not move.

The asteroid is what makes this sharp rather than dismissible: a genuinely
immobile object gets 0 every single time, so "immobile things get junk" does
not explain the turrets.

So the operand is a craft-class-dependent scalar with speed's range and
ordering, but set_speed is not a sound reading of it. Left UNNAMED. The
handler writes [unit+196], initialised from [unit+192] at spawn; reading its
consumer is what would settle it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 00:56:28 +00:00
Sylpheed RE agent
fa20db3db8 re: four ISL built-ins are tutorial-only, and 96 encodes the tutorial index
96, 97, 98 and 104 were unnamed. Measured over all 28 stages they form a
family that appears nowhere outside the six tutorials:

  96    8 sites   S18-S23 only
  97   38 sites   S18-S23 only, followed by start_coroutine 27/38
  98    0 sites   never called anywhere
  104  39 sites   S18-S23 only, followed by wait_s 39/39,
                  preceded by end_coroutine 37/39

104's adjacency is a textbook poll loop with no exceptions: coroutine starts,
tests the predicate, waits, repeats.

96's operand is the giveaway. Its eight payloads in stage order are
S18 -> 101, 102, 103; S19 -> 201; S20 -> 301; S21 -> 401; S22 -> 501;
S23 -> 601, i.e. (stage - 17) * 100 + section. Tutorial 1 has three sections
and the other five have one each, and the tutorial index is encoded in the
argument.

Names are deliberately NOT applied. tutorial_begin / tutorial_end /
tutorial_message_pending fit the shape, and [phase+340]/[phase+344] are said
to carry the state and payload, but I have not read that handler myself and
this file has already had to withdraw five names guessed from shape. The
distribution and the argument encoding are established and are what a port
needs; the labels can wait for someone to read the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 00:50:24 +00:00
Sylpheed RE agent
cb830e233e re: name the five mission-banner built-ins from call-site structure
77, 78, 81, 82 and 135 were unnamed. The engine has five contiguous strings
-- MISSION_START_PRT at 0x820A83F0, then _END_, _UPDATE_, _FAILED_,
_RESTART_ -- and five sequential ScriptPhase fields at +388/+392/+396/+400/
+404, stored in ascending order by one constructor region. Five names, five
fields, five unnamed built-ins.

Which is which is decided by call-site structure, measured over all 28
stages, and it is exact:

  39 MARK_LAST_PHASE   89 sites  -> 82 in 89/89
  82 banner_mission_failed        <- 39 in 89/89, then wait_s 89/89
  40 mark_not_last     50 sites  -> 78 (27) + 81 (17) + END_PHASE (6) = 50
  78 banner_mission_complete      <- 40 in 27/27
  81 banner_objective_update      <- 40 in 17/17
  77 banner_mission_start         22 sites in 22 stages, one per stage,
                                  after play_bgm
  135 banner_mission_restart      16 sites, after play_bgm, phase >= 2

39 -> 82 is a perfect pairing and 40's sites partition exactly three ways.

Stated as inferred rather than read: the string-to-field pairing itself comes
from both sequences ascending in the same order; my operand tracker did not
catch the string loads in that constructor. The ROLES above do not depend on
it.

76 is left unnamed on purpose. It has 38 sites = 22 + 16, exactly 77's count
plus 135's, and precedes them; its body sets [phase+332] = 1 and nothing in
the image reads that field. Suggestive arithmetic is not a name.

Flagged as a consequence: MARK_LAST_PHASE is followed by the FAILED banner in
89 of 89 sites and mark_not_last by END or UPDATE. So [phase+300] = 2 reads
less like "this is the last phase" than "end the mission now,
unsuccessfully" -- the existing names for that pair may be mis-framing it.

Artifact regenerated: docs/re/data/isl-stage02.txt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 00:44:38 +00:00
Sylpheed RE agent
75f0664bfe re: an ISL symbol operand is a (tag, index) pair — and two more names withdrawn
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-25 22:51:07 +00:00
Sylpheed RE agent
501298de81 re: the trigger container's "push" is a clear — and the live test is blocked
Third independent line for yesterday's built-in 100 rename, from the callee
this time. sub_8226E3B8 was labelled "push", which is what made built-in 100
look like push_trigger. It reads the element count, returns immediately when
the container is EMPTY, and otherwise walks the node list splicing nodes out
until it is empty. A push links one node; this unlinks all of them. It is
clear(). The append is sub_8226E160, reached from built-ins 19 and 25.

So the rename now rests on the handler, the usage (all 12 Stage 02 sites sit
in the phase terminator next to timer_stop / clear_flag(-1) /
MARK_LAST_PHASE), and the callee.

The dynamic half did NOT run, and the write-up says so. phase_watch.py now
samples [phase+272+20] (triggers queued) and [phase+216+8] (coroutines
alive) so a phase terminator's effect on the VM is visible in one line —
written here, never yet exercised against a live guest.

Boot-nav could not reach the title in 381 s. Diagnosed rather than retried:
skip_intro.sh only runs the title test on a static frame, gated at
rmse <= 1500, and this run measured 1503 at 104 s and 1549 at 139 s — just
above the cut — so is_title.py was never called and the one allowed press
was never spent. Recorded in BACKLOG with the explicit instruction NOT to
raise the constant: the first step is to log rmse and the glyph count
through a whole boot and look at the two distributions, because tuning a
threshold to make one run pass is fitting to a single sample.

Also reaped a stale lock: a gdb orphaned 2h14m earlier was holding
/tmp/xenia-canary.lock with an already-defunct emulator child.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-25 22:25:10 +00:00
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
Sylpheed RE agent
ac4f53b8a1 re: interpreter command table recovered; withdraw my 'constructors' reading
sub_822FE040 is a fully unrolled registration: 1023 slots filled with a default,
then 57 explicit writes, of which 48 are real handlers and nine are a shared
accept-and-discard stub. Return convention is nonzero = consumed, 0 = retry,
which is how the interpreter waits for a named unit to exist.

Opcode 995 is the ONLY handler touching the phase mirror [*(0x828F35F8)+236] --
the sole read and sole write in the table -- independently confirming why polling
that mirror saw nothing during phase 1. And no handler spawns or despawns a unit:
256 is the strongest deploy candidate but is unconfirmed because the message ids
are write-only in this image.

WITHDRAWN, verified wrong: I had recorded the writes to '+20' in sub_8226E7D8 /
sub_8226E930 as block initialisations by container constructors. At
0x8226E86C-0x8226E8E0 they do li r3,28 / bl 0x8230C160 then
lis r10,0xAB03 / ori r7,r10,0xE4BA / stw r7,4(r3): they build an INTERPRETER
COMMAND RECORD for opcode 996 and push it, i.e. AddSelector and RemoveSelector,
with a 32-entry cap. The stw to 20(r3) is the command record's +20, a different
object. Wrong twice: not constructors, and not that container.

Also flags that sub_8230C398 -- gated on *(0x82899CE0) == 16 at both call sites
-- looks like Stage 16's script compiled in C++, which 'debug defaults' does not
survive given the .ssb loader explicitly refuses mission 16.
2026-08-25 20:17:30 +00:00
Sylpheed RE agent
13ec75465b re: sub_8226E458 is a splice, but not shown to touch the trigger queue
Chasing which of sub_8226EAB8's 16 callers grows the trigger count: sub_8226E458
decrements a count at 8(r30) then calls the insert helper -- a splice -- and has
exactly one caller, inside sub_8226D740, the per-frame engine->script drain. That
suggests the engine moves records into the phase queue each frame.

But the call site refutes the neat reading: at 0x8226D780 the argument is
lwz r4, 324(r29), the UNIT ARRAY, not the trigger container. So I have not shown
sub_8226E458 touches the trigger queue at all, and 'the engine feeds triggers
each frame' is my inference rather than the disassembly's.

Recorded unresolved. Taking a function's shape for its purpose is what produced
the 'push' mislabel on sub_8226E3B8 and the ADN110-for-null pretty-print, each of
which cost an iteration.

Unaffected and solid: sub_8226EAB8 increments a count at +8 of its container, the
trigger container embeds its list at +12, so the watched word at phase+272+20 is
that inner list's count, and the guest was inside sub_8226EAB8 at the write.

Next: instead of guessing among 16 callers, re-run the watchpoint and read the
guest LR from the context -- the technique that named the writer will name its
caller.
2026-08-25 19:44:16 +00:00
Sylpheed RE agent
33013a9b84 re: the trigger-queue appender found -- sub_8226EAB8, count at inner+8
The watchpoint plus Canary's source settle it. At the write, the guest context
(rsi, per x64_emitter.cc:881) holds 0x8226EAE0, inside sub_8226EAB8. That
function is a generic list-node insert: it reads the count at 8(r30), guards
against 0x3FFFFFFF overflow, does addi r11,r11,1 / stw r11,8(r30), then links the
node. It has 16 callers, so it is a shared container helper.

That explains why two static searches missed it. The trigger container at
phase+272 EMBEDS an inner list object at +12 -- which is why the push does
'addi r31, r30, 12' -- and the inner object keeps its count at its own +8. So
272 + 12 + 8 = 292 = the watched word, and the instruction is stw r11, 8(r30)
with r30 = phase+284. Searching for 'stw rN, 20(rM)' could never have found it.

Also resolves the earlier open item on +12: it is the embedded list object, not
a list head pointer, which is why it read 0x000A0009 instead of an address.

Method note kept: the static hunt assumed the field's offset in the OUTER object
would appear in the writing instruction. A watchpoint is indifferent to the
addressing form, which is why it was the right tool after two failed offset
searches.
2026-08-25 19:34:15 +00:00
Sylpheed RE agent
da2d02db77 re: the trigger-count watchpoint fires, but the writer is JIT guest code
trigger_watch.sh + host_addr.py translate the guest VA to a host address
(0xBE14DEA4 -> host 0x1BE14DEA4) and watch it. It fires: Thread 50 'Main
XThread', old 0, new 16777216 -- which is big-endian 1 read little-endian, so
the count going 0 -> 1, independently confirming the field. The write happens on
the guest's own main thread, not an emulator worker.

But the writer cannot be named from the host stack: the faulting PC is
0xa0c65f23 with no symbol, executing 'mov 0x110(%rsi),%rbx', i.e. Xenia's
JIT-compiled guest code, and the frames above it are not host-unwindable. So the
watchpoint answers when and which thread, not which guest function.

Recorded as a ceiling of the method rather than retried blindly. The way past it
is that the JIT holds the guest context in a register (%rsi here), so the guest
PC is recoverable from the context block -- which needs Xenia's context layout
from the xenia-rs sources on this box, a separate tractable piece of work.
2026-08-25 18:52:35 +00:00
Sylpheed RE agent
415dd75a8b re: what appends a trigger node -- not found, recorded as such
Three approaches, none successful. sub_8226E160, earlier flagged as 'enqueue a
pending trigger', has exactly one caller and is a specific operation rather than
the general append. Writes to the count at +20 inside the container code number
only four, and all four are part of a block initialisation (stw to 0/8/12/16/20/
24 in consecutive instructions) in sub_8226E7D8 and sub_8226E930 -- constructors,
called from 0x8226E560 and from ScriptMission's own constructor at 0x822608A0.

So the increment that takes the count 0 -> 1 -> 2, which is measured live, does
not appear as a plain stw to 20(rM) anywhere in the container's code. It is
inlined, uses another addressing form, or lives somewhere I have not looked.

Recorded as not found rather than guessed: inferring from the shape of nearby
functions is exactly what produced the 'push' mislabel last iteration.

Names the approach that would settle it: a gdb watchpoint on
ScriptPhase+272+20 during a live mission. The address is known at runtime, the
count demonstrably changes within ~2 minutes, and a watchpoint reports the
writing instruction directly instead of inferring it from static shape.
2026-08-25 18:24:21 +00:00
Sylpheed RE agent
25a98fb204 re: trigger queue walks live -- structure confirmed, record contents refuted
Walked the container's list from +16 for 200s. The structure holds: +20 tracks
the node count, nodes chain through their first word, and entries appear as the
mission runs (0 -> 1 -> 2, then stable).

The record layout does not. I expected node+8 to hold small symbol indices,
which the pop's out-parameters made natural. Every field is a guest heap pointer
(0xBC..), so trigger records reference objects rather than table indices, and
those objects are unidentified.

Also records a false resolution I introduced: a line printed 'f4=0(ADN101)'
because the raw value is 0 and my formatter mapped index 0 to symbol-table-2's
first entry. ADN101 is not in that record -- the pretty-printer invented a name
for a null. A resolver must refuse values that were never indices.

And corrects the previous section: sub_8226E3B8 is a CLEAR, not a push. Its tail
decrements a counter, calls an erase helper, and loops while [+20] != 0. So
built-in 100 clears the queue then rebuilds the thread list, matching the
built-in table's own wording; 'push' was my label, not the disassembly's. Two
callers: vt2 (script) and 0x8226D420 (an engine site). What appends a node is
still unidentified.
2026-08-25 18:18:19 +00:00
Sylpheed RE agent
eeff706134 re: the trigger-queue pending count verified live -- 0 -> 1 -> 2
Read from a running Stage 02 mission: the container at ScriptPhase+272 has +20
moving 0 -> 1 -> 2 over the first two minutes while the phase ordinal stays 1.
So +20 is a real count of currently registered triggers -- the script arming
watches as it goes, and Stage 02 has 12 push_trigger sites -- and it is readable
from /dev/shm with no debugger. First direct view of what the script is waiting
for.

Corrects one field from the static reading: +12 is not (only) a list head. It
reads 0x000A0009, which is not a pointer, even though the push's
'addi r31, r30, 12' made list-head the obvious interpretation. Recorded as
unresolved rather than kept quietly.
2026-08-25 18:03:50 +00:00
Sylpheed RE agent
63d3ce089f re: trigger queue layout at phase+272, and a readable pending count
Chasing what makes the phase-1 condition re-evaluate, since the polls do not run
continuously.

Two method corrections: searching the VM range for '272(rN)' mostly returns
VTABLE slot offsets -- 0x82273174 lwz r11,272(r11) is followed by mtctr/bctrl,
a virtual call through slot 68, not an access to the phase field. And
[phase+272] is not a pointer to a queue but an EMBEDDED container: vt2
(sub_82265DD0) is 'addi r3,r3,272 ; b 0x8226E3B8', passing phase+272 as this.

Layout from the push/pop pair (sub_8226E3B8 from built-in 100, sub_8226E220
called every frame from sub_8226D740): +12 list head, +16 current node, +20
element count (zero = empty, tested first by the pop), +24 scratch. The pop
returns the record through out-parameters read from node+8: three u32s, a
double at +16, another u32 at +24 -- matching the six pointers sub_8226D740
passes in.

The actionable part is [phase+272+20], a live pending-trigger count readable
from /dev/shm. Watching it alongside [ScriptMission+40] should show when the
engine hands the script an event, which is when condition coroutines start --
the thing every phase experiment so far has been blind to.

Layout is from disassembly only; not yet verified live.
2026-08-25 17:54:34 +00:00
Sylpheed RE agent
b07b678ef0 re: unit_state reads rec+4 and rec+104, never rec+16
Disassembling built-in 69's handler at 0x8226ADF0 instead of trusting the
summary: it loads rec = base[idx], tests rec+4 against zero (early exit =
absent), passes rec+4 to sub_82301240 for the lifecycle lookup, and compares the
byte at rec+104. rec+16 is never touched on that path.

That explains the null poke result exactly -- I wrote into a field the condition
does not consult. +16 remains a faithful OBSERVABLE (the arrival and death
transitions were real) but it is a readout, not the input. It also explains why
rec+4 holds small consecutive integers: it is a handle that sub_82301240
resolves, not a pointer.

The corrected simulation of 'this squadron is gone' is rec+4 = 0, taking the
documented early exit.

Flags a general caution: this built-in table came from a subagent's reading of
handler behaviour, and this is the second per-offset field description that did
not survive the disassembly. The identifications of which built-in does what
have held up; the offset meanings are leads to verify.
2026-08-25 16:58:49 +00:00
Sylpheed RE agent
9f2692eadd re: correction -- the script reads its own flags via built-in 9
Last iteration ended with 'what reads the flag file is unknown' after an offset
search failed. The framing was wrong: I was hunting an engine-side reader, but
the consumer is the script itself, through built-in 9 (read_freg), which loads
[phase+88][i] into the double result register.

Stage02.ssb calls read_freg 12 times -- the same count as set_flag (12) and
clear_flag (12) -- so the latch is symmetric and entirely inside the VM. That
closes the middle of the set_flag -> ... -> END_PHASE chain: one coroutine
latches, another reads it back and branches.

Also records an unconfirmed observation: op10 + op13 pairs with consecutive
small immediates and their own code offsets look like a case/branch dispatch.
Flagged as a pattern, not named -- neither handler has been read.
2026-08-25 13:47:56 +00:00
Sylpheed RE agent
dcf37bcf93 re: a Stage 02 clear condition read end to end, with squadron names
Resolving symbol-table-2 indices turns the bytecode into mission logic. At
0xF524 Stage02.ssb polls unit_state on ADN110, ADN111 and ADN112, updates each
one's objective marker, then latches set_flag(8) -- exactly the
trigger/predicate/set_flag/END_PHASE shape predicted from the disassembly, now
observed in the mission's own code with names the roster tables already gave.

The 12 END_PHASE sites are outro sequences (wait_cmds_drained / fade_sound(3) /
builtin85(3) / wait_s(3) / END_PHASE / yield) -- the terminator, not the
decision.

Fixes a decode bug that hid every argument: the tracker only followed
local[i] = special[0], but the common form is an immediate written straight into
local[i] (k=01,03), so every unit predicate printed with NO arguments. The
disassembly looked complete while being empty exactly where it mattered.

Also records the live probe result: the phase mirror at [*(0x828F35F8)+236]
stayed 0 for ~530s of actively-hunting flight, no advance observed -- which is
what the static analysis predicts for phase 1, since ChangePhase only posts once
the ordinal exceeds 1.
2026-08-25 12:42:50 +00:00
Sylpheed RE agent
70cff9ca21 re: the 147 ISL built-ins characterised; Stage 02 gates on units, not counts
Table at 0x8227226C is 147 big-endian absolute VAs (verified structurally: it
ends exactly where the first handler begins, all targets inside sub_82272220).
Arguments are not in the instruction -- every handler does c_str() on
[phase+20], a packed blob, which is what the local[] staging fills. Return 2 =
yield; five built-ins block by skipping the pc advance.

Recovered the ScriptPhase state layout: 32-entry float and flag register files,
int/double result registers, the timer block, and the runtime unit array at
+324 indexed by symbol-table-2 index -- a direct hook from bytecode call sites
to the two .ssb symbol tables.

Spot-checked two claims against the disassembly rather than trusting them: id 4
loads a DOUBLE into the thread countdown and returns 2 (wait_s), and id 24 reads
current/initial squadron member counts (squad_survival_pct). Both exact.

Counting Stage02.ssb: unit_state 255, hp_pct_test 167, dist_lt 92, unit_alive
71, unit_relation 52 -- and squad_survival_pct, group_ratio_pct and the two
global counters are NOT called at all. So Stage 02's phases are gated on named
units (destroyed / HP / proximity), never on an aggregate count, even though the
kill-counter primitives exist in the VM. That answers the standing 'next wave
after N kills or after an event?' question for this stage: specific units, not a
number.

isl.py now names the built-ins, so the run-up to the first END_PHASE reads
wait_cmds_drained / fade_sound(3) / builtin85(3) / wait_s(3) / END_PHASE.

Not settled: 3 handlers unresolved (55, 75, 105); the 1024-slot interpreter
command table is only partly recovered.
2026-08-25 12:36:02 +00:00