Commit Graph

905 Commits

Author SHA1 Message Date
Sylpheed RE agent
bb37b16847 re: the in-game HUD config -- 16 records, 419 asset paths, a per-stage ResourceTable
Checked first: no docs/re file mentions ArmsStatus, RangeFinder, Radar,
Sight, Wing, NamePlate or ResourceTable.  Only HudResource had been
opened; the other fifteen records had not.

The six IDXD entries of GP_MAIN_GAME_E2D.pak: two carry the 16-record
HUD config, two the 13-record ObjectiveMarker_*/TutorialMarker_* set,
one Face (52 portrait sprites), one ResourceTable.  Between them they
name 419 distinct asset paths -- the whole flight HUD -- with new
subdirectory prefixes throughout (ArmsSt, ActvArm, RangeF, Marker,
Manuva, Map, Speed, Radar, Sight, Wing, Hitmark, Lockon, Info).

The decisive control uses the config's own exact path strings, so no
guessing is left in the loop: 419 distinct .prt/.t32/.tbl values, ZERO
resolve as a pak entry under 10 prefixes, and the four config filenames
resolve to nothing either.  The .t32 sprites certainly exist -- 574 T8aD
in that pak.  So the 2D pak is not addressed by name_hash of the name
its config uses.

This supersedes the earlier framing: the 28 'dangling' .prt names were
never a missing-asset story; they are 28 of a set where none of the 419
resolves.

ResourceTable is 58 positional fields = 29 pairs, alternating
HudResource.tbl / HudMarkerResource.tbl, identical in all six language
paks, with exactly one override at pair index 25 -- HudResource_S26.tbl.
Reading: indexed by stage number minus 1, so index 25 is S26, the one
stage with its own HUD config.  Arithmetic exact, indexing unproven, not
adopted.

New structure doc, artefact and regenerator; the other seven regenerate
byte-identical.
2026-08-27 14:53:49 +00:00
Sylpheed RE agent
dcf4945db5 re: the variant reading is refuted; HudResource is the in-game screen config
Censused the 32 bundle-less in-game .prt names with the 68 as control:
4 of 32 appear as an element of some 2D bundle, against 58 of 68.
Being an element is normal for a part that exists; these are not
elements either.  Last iteration's 'not adopted' reading is refuted.
28 names are neither a bundle nor an element.

They are not unreferenced: a HudResource record inside the 2D paks' own
IDXD entries names them -- the in-game screen config, parallel to
tables.pak for menus.  GP_MAIN_GAME_E2D.pak has 6 IDXD entries; two
carry HudResource (24 named fields) beside ArmsStatus, Map, ArmsItem,
RangeFinder, Marker, Manuva, Number; two more carry the
ObjectiveMarker_*/TutorialMarker_* set.

Its values carry subdirectory prefixes -- Hitmark\, Lockon\ -- which my
hand-written prefix list never had.  So I rebuilt the negative the right
way round: harvested every IDXD field value containing a backslash, kept
the 60 commonest directories, and re-swept.  241/376 with the hand list;
241 with hand + harvested -- zero new resolutions.  After the language\
miss, this is the closure that counts: the prefix list came from the
disc, not from me.

Still open: whether the 28 are cut features or assembled at runtime from
sprites.  Nothing static separates those two.

Artefact +45 lines / 0 deletions; the other six regenerate
byte-identical.
2026-08-27 14:42:20 +00:00
Sylpheed RE agent
1132d95227 re: an embedded .prt part is a top-level RATC bundle, addressed by its element prefix
Read first: ui-rat-layout.md OWNS the RATC stack and already documents
the 60-byte element declaration table and 'one bundle = one
(context x language) build of that screen'.  What it does not say is how
a part is addressed when it is NOT a pak entry.

The elements of one bundle share a common name prefix, and that prefix
is the part name.  GP_MAIN_GAME_E2D.pak has 130 bundles with 114
distinct element prefixes -- pgmenu_btn00, pghud_wing, pgface,
pghud_range, pgmanuva_eff0 -- exactly the in-game part names.

The 100 in-game-only .prt names match a 2D bundle prefix 68 times; the
control, the 241 shipped parts, matches ZERO.  The two families are
disjoint on the test.  13 shipped parts match a bundle prefix in their
own screen pak, which is the expected shape.

Four of the five mission banners land here: pgmsg_start.prt is E2D
bundle 0x89fac252, a one-element bundle declaring pgmsg_start_sub.rat;
likewise _end_, _failed_, _restart_.  pgmsg_update has no bundle at all,
consistent with having no _sub.rat.

32 in-game names still have no 2D bundle.  They cluster into families
whose base name is a bundle, reading as a variant declared inside a
parent bundle -- a reading, not adopted.

Artefact +38 lines / 0 deletions; the other six regenerate
byte-identical.
2026-08-27 14:27:38 +00:00
Sylpheed RE agent
e4000d9cb5 re: who references a .prt decides whether it ships -- menu named, in-game embedded
Censused the whole 135 unresolved set instead of chasing the five.
Cross-tab over all 376 .prt names: referenced by tables.pak AND
resolves 241; neither 100; referenced but absent 35; resolves WITHOUT
being referenced by tables.pak -- ZERO.

So resolving implies a tables.pak reference, 0 counterexamples of 376.
tables.pak holds the menu screen configs and every part it names ships
as <lang3>\<name>.prt.  The 100 names that appear only inside the
GP_MAIN_GAME_*2D.pak bundles never ship as entries -- the in-flight HUD
is authored into the bundles, not loaded by name.  The five pgmsg_*.prt
were never a special case; the split is menu = named + shipped,
in-game = embedded.

It is not a prefix rule: 6 of the 135 pg* names do resolve (pgloading,
pgloading2, pgmsg_scr, pgpause, pgpause_ttrl, pgpbase), because
tables.pak names them.

The 35-name residual is one coherent family -- pgmenu_btn*/item*/pad,
pgfacewin*, pgtextwin, phinfo1-3, 02d, 02d_scr, psview_release: the
in-game pause menu and squadron-order overlay, named by a menu config
but drawn from the in-game bundles.  Consistent with the split, not
separately proved.

Artefact +44 lines / 0 deletions; the other six regenerate
byte-identical.
2026-08-27 14:13:41 +00:00
Sylpheed RE agent
94065d40ec re: where .prt screen parts live; the five mission banners are a real exception
Read first, as the rule now requires: ui-prm-primitives.md,
ui-rat-layout.md, ui-screen-runtime.md -- they own the .prt/RATC/T8aD
stack but describe element names inside bundles, not where parts live.

A .prt IS a pak entry, under a LANGUAGE directory.  Of 376 distinct
.prt names referenced on the disc, 241 resolve as archive entries --
220 under eng\ and jpn\, 38 under each of the other four.  prbase.prt
resolves under all six; palogo1.prt under two.

All five pgmsg_*.prt resolve under nothing, across 15 prefixes that
include every language dir, so this is not the missing-prefix mistake
again.

A lead, refuted by its own control: four of the five have a same-stem
_sub.rat sub-bundle inside every per-language 2D pak, and pgmsg_update
has none -- but only 4 of 376 .prt stems have a _sub.rat, and
prbase.prt, which does ship as an entry, has none.  Not the container
convention, not adopted.

An intermediate pass nearly reported 'the five appear only in the
manifest'; that was wrong because the token census was truncated by a
slice and the RATC hits were pgmsg_start_sub.rat, a longer token.

New artefact and regenerator; the other six regenerate byte-identical.
2026-08-27 14:01:24 +00:00
Sylpheed RE agent
9d25a3a582 re: correction -- the dialogue table was already in the corpus, and one negative was false
The previous commit announced message\MissionDialogMessage.tbl as a
find.  structures/ixud-localised-text.md OWNS it and already says
S02_P1_OBJECTIVE and friends are record names in an IDXD map,
message\MissionDialogMessage.tbl, whose positional fields list the
lowercase per-line IXUD names -- and that *_GRAPH is the odd one, a
single named field holding a texture.  I grepped the manifest doc and
not the text doc.  Third overclaim in four days; the rule is now to grep
the doc that owns the DATA, not only the doc that owns the FILE.

Worse, the headline negative was wrong.  MissionDialog_local_string.tbl
DOES resolve -- as language\MissionDialog_local_string.tbl, IXUD, in all
six GP_MAIN_GAME_* paks.  My 33 prefixes omitted language\, which is
precisely the convention ixud-localised-text.md records for language
paks.  A prefix sweep is only as good as its prefix list, and the list
should come from the corpus.

'LOSE has exactly one entry' was wrong too.  Per kind: HINT_PAUSE 4,
HINT 3, LOSE 4, OBJECTIVE 4 positional fields, and GRAPH 1 NAMED field
holding the .t32.  The single-field bucket was GRAPH, not LOSE.

What survives as new: the 11 non-MISSION field values read as a block;
the GP_TEST and TEXTS sibling records; the per-stage phase census; and
the five pgmsg_*.prt resolving nowhere under 34 prefixes x 41 archives,
now with two working controls in the same sweep.

Artefact 8 insertions / 3 deletions, every deleted line replaced by its
corrected form; the other five regenerate byte-identical.
2026-08-27 13:45:35 +00:00
Sylpheed RE agent
acbcd8d75c re: Stage\script.tbl's 11 non-MISSION fields, and the mission dialogue table
structures/mission-script-ssb.md owns this manifest and names its 40
fields but never read the 11 that are not MISSION<n> = StageNN.ssb.
They are DIALOG_MESSAGE, DIALOG_LOCAL_STRING, FONT (+size), TEXT_POS,
TEXT_LINES and the five pgmsg_*.prt.  Two sibling records were also
unread: GP_TEST (PATH = dat\GP_TEST\, a debug archive not on the disc)
and TEXTS (a second text style).

Probed 7 values x 33 prefixes x 41 archives.  One resolves:
message\MissionDialogMessage.tbl, in all six GP_MAIN_GAME_* paks --
200 records, 25280 bytes, every name S<NN>_P<n>_<KIND> with five kinds
40 each (HINT_PAUSE, HINT, OBJECTIVE, GRAPH, LOSE), fields positional
and tagged 0..3, each value a message key.  An index from (stage,
phase, kind) to the localised strings, on the same S<NN>_P<n> keying
the ISL corpus already uses.

Control: the 40 stage-phases span stages 1-16 and 24-29 -- a subset of
the 28 shipped, and the six with no hints are exactly 18-23, the
tutorials.  A fifth independent route to the story/tutorial split, and
it gives the phase count per stage.

The other six do not resolve, with the control in the same sweep:
MissionDialog_local_string.tbl and all five pgmsg_*.prt are not a pak
entry under any of the 33 prefixes, while
message\MissionDialogMessage.tbl and Stage\script.tbl both resolve in
6 archives.

name_hash is CASE-INSENSITIVE (message\ == Message\); tag_hash is not.

New structure doc, artefact and regenerator; the other five artefacts
regenerate byte-identical.
2026-08-27 13:37:31 +00:00
Sylpheed RE agent
ccdc226022 re: the stage numbering is consistent everywhere; the manifest still declares the cut S17
Correction first: the previous commit claimed the corpus 'did not know
the stage NUMBERS'.  It did -- structures/mission-script-ssb.md has
recorded S01-S16, S18-S29, S17 absent by a three-way convergence (hash
lookup, the table sweep, the loader's != 16 guard) since before that
pass.  What was actually new there is the player craft per stage and
ResourceID's ordinal tags; the numbering is a fourth independent route,
not a discovery.

The question this item asked: do the ISL StageNN.ssb files and the
UnitGroup_SNN.tbl tables use the same numbering, or is something
silently mis-labelled?  They are identical.  Stage\StageNN.ssb,
stage\UnitGroup_SNN.tbl and stage\Route_SNN.tbl all resolve for the
same 28 N with an EMPTY symmetric difference; AIParams is the same minus
18-23.  Nothing in the corpus is mis-keyed.  Formation_SNN and
Message_SNN resolve 0/40 -- those are field names, not files, and the
four families above are the controls that make that negative real.

New: Stage\script.tbl's SCRIPTS record lists 29 MISSION*.ssb fields --
MISSION1..MISSION29, including MISSION17 = Stage17.ssb -- and only 28 of
29 name a file that ships.  Control: MISSIONn -> Stage<n>.ssb with the
same n, 0/29 mismatched.  So S17 is a cut mission the shipping manifest
still names, which is why the loader carries an explicit != 16 guard
rather than just running 0..27.

Artefact +14 lines / 0 deletions; the other four regenerate
byte-identical.
2026-08-27 13:27:19 +00:00
Sylpheed RE agent
28a59d839f re: the disc's stage numbers, and the player's craft per stage
The corpus knew '28 stages' and 'six tutorials with no AIParams'.  It
did not know the NUMBERS, and they are not 1..28.  Hashing
stage\UnitGroup_S%02d.tbl for N=0..39 against GP_MAIN_GAME_E.pak:
1-16 and 18-29 ship, 17 does not -- 28 files -- and the six with no
AIParams_SNN.tbl are exactly 18-23.  So S01-S16 story, S17 absent,
S18-S23 tutorials, S24-S29 story: 22 + 6.

eng\GP_HANGAR_ARSENAL_3D.tbl's ResourceID record keys a player-craft
mesh by stage: Unit_St1_6 -> rou_f001 (DeltaSaber T), Unit_St7_16 ->
rou_f002 (W), plus six further fields tagged with the raw numbers
24,25,26,27,28,29 -> rou_f002 x4, rou_f004 (DeltaSaber A) at 28,
rou_f002.  The bare tags are the last six story-stage numbers.  Control
in the same record: tag_hash(name) == tag for 11/11 named fields, so
those six genuinely carry no name.

Cross-check from a different file: grouping the Arsenal pak's 168
stage-scoped entries by which _Player craft their loadout mounts gives
f001 = 6, f002 = 15, f004 = 1, tutorials 5+1 = 6, and every number
closes against ResourceID -- 6 = Unit_St1_6; 15 = Unit_St7_16 (10) plus
tags 24,25,26,27,29 (5); 1 = tag 28; 6+15+1+6 = 28.

So the player flies the DeltaSaber A in exactly one mission, S28, and
the DeltaSaber T only for the first six.

Not settled: the Arsenal entry filenames, so which of the 168 is S24 vs
S25 is constrained but not pinned.

New structure doc, artefact and regenerator; the other five artefacts
regenerate byte-identical.
2026-08-27 13:18:42 +00:00
Sylpheed RE agent
2b9709eaa0 re: who selects a loadout -- the pak entry does; the Arsenal pak is stage-scoped
Settled by elimination with two controls.  The 15 loadout names appear
as a field VALUE nowhere on the disc -- 0 occurrences across every pak
-- while the control Arbalest_155KG, which is referenced, appears 150
times as a value in the same pak.  They occur only as the field names of
the UNITS record.  They are not in the executable either; control:
WEAPONS (a section key) is, Arbalest_155KG is not.

So nothing references a loadout by name.  The selection is which pak
entry the Hangar loads.  GP_HANGAR_ARSENAL.pak has 180 IDXD entries in
10 shapes; 168 carry a UNITS roster = 28 stages x 6 languages, and the
other 12 = 2 x 6.  Each entry is one stage's whole Hangar config, and
its UNITS roster is the flight for that stage -- 5 distinct rosters over
15 / 5 / 5 / 2 / 1 stages, including one where Rhino2-Katana flies
alone.  Every count is a multiple of 6 and they sum to 28, the disc's
stage count by a fourth independent route.

Corrects the previous commit: it read the pak with setdefault, so
'15 loadouts / 24 allow-lists / STANDARD_ARM1 has 11 entries' is the
union of first-seen records, not one table.  Contents vary per entry --
PlayerSET_ARM1 has 5 distinct contents, STANDARD_ARM1 3, ExSET_NOSE 4.
The chain and both controls (60/60, 70/88) are per-record and
unaffected; only the per-list sizes were over-generalised.

Not settled: the entry filenames.  name_hash probing over 8 templates x
40 indices x 6 languages resolved 0, while the same probe's controls
<lang>\weapon.tbl and <lang>\strings.tbl resolved 12/12.

Artefact +40 lines / 1 changed heading; the other four regenerate
byte-identical.
2026-08-27 13:09:07 +00:00
Sylpheed RE agent
2b2d984f0c re: the Hangar loadout system -- loadout, allow-list, arsenal item
15 loadout records, one per flight position x pilot (Bird1-Sandra ..
Rhino4-Yoji), each with Arm1/Arm2/Arm3/Nose + UnitID.

The same trap as PlayerWeapon, one level up: Arm1/Arm2/Arm3/Nose do NOT
name items.  They name a per-slot ALLOW-LIST record -- one of 24 whose
only named field is Type (the slot kind) -- and the candidate items are
that record's positional, unnamed fields, in order.  Four hops:

  Rhino4-Yoji.Arm1 -> STANDARD_ARM1 -> [Falcon_9AM, Condor_105AM, ...]
  -> item.PlayerWeapon = Turret_NNN -> slot.WeaponID -> Weapon.ID

Controls: Arm1/2/3/Nose -> allow-list record 60/60; allow-list
positional entries -> arsenal item 70/88, and every one of the 18
misses is the single sentinel No_Equipment -- one of the four
WEAPONS-roster values with no item record, i.e. the empty-slot marker.

UnitID is two ID spaces at once: 5 rows name a unit Generic.ID (the
three -Katana rows are the player -- a _Player craft plus an extra,
empty PlayerUnit field), 8 name a character, resolving as Character +
the value into the 64-record character table.

Two values resolve to nothing, both single rows against 13 that do:
Rhino2-Ellen.UnitID = UN_f001_TCAF_DeltaSaber_W exists nowhere (checked
as a Generic.ID across every pak and as a record name, 0 hits) while
UN_f002_TCAF_DeltaSaber_W does -- consistent with a shipped typo,
reported not diagnosed -- and Rhino4-Brandon.UnitID = BRANDON has no
CharacterBRANDON among the 64.

New structure doc, artefact and regenerator; the other four artefacts
regenerate byte-identical.
2026-08-27 12:58:53 +00:00
Sylpheed RE agent
21e710a531 re: the 59-of-131 arsenal question is closed -- an item names a hardpoint
An Arsenal item does not reference a Weapon record.  It references a
Turret_NNN HARDPOINT SLOT on the player craft's own unit table, and the
slot is what carries the WeaponID.  Three hops:

  Arbalest_155KG.PlayerWeapon -> Turret_050  (a slot on
  UN_f001_TCAF_DeltaSaber_T_Player) -> .WeaponID ->
  Weapon_DSaber_P_wep_50_Cannon

Controls, both in the same loop: 0/59 distinct PlayerWeapon values are a
Weapon.ID; 59/59 are a Turret_NNN slot id; the full chain lands on a
Weapon.ID 59/59.  WingmanWeapon resolves identically.  The WEAPONS
roster's 59 = 55 item names + 4 empty-slot sentinels.

Wingmen fly a cheaper gun: following the same 59 slots across craft
variants, the _Player tables give each item its own weapon record (59
distinct) while the AI tables collapse all 59 onto 10 generic classes.
That is most of the 131.

Upgrades yesterday's 'hardpoint catalogue' reading from 21 to adopted,
proved from an independent file, and corrects its '10 distinct WeaponID'
figure -- that was the AI variant, not the player's.

Also adds an __main__ guard to unit_substructures.py so importing
pak_entries from it does not run its report; its artefact is unchanged
and still byte-identical.
2026-08-27 12:49:22 +00:00
Sylpheed RE agent
4a5e5d55e6 re: the destructible-subsystem model -- a unit's sub-records
The corpus has named these since unit-struct-runtime.md but never
opened them.  Per unit table: Turret_NNN 835 records (max 63 on one
unit), ShieldGenerator_NNN 46, Thruster_NNN 38, Hatch_NNN 26,
Bridge_NNN 25, plus one each of Shield/Mass/SE/Explosion/
StructureCount and NS_Body on 68 of 114.

Turret/ShieldGenerator/Thruster/Hatch/Bridge are ONE record shape: a
shared 19-field destructible-part base (ID, Name, ParentStructureID,
Frame = a mesh NODE name, NomalModel, CollisionModel, Radius, HP, the
four Is* flags, SpreadDamage, damaged/destroy motion + time, and the
three Effect_*), with per-kind extras.  Turrets add WeaponID,
AngularVelocity, YawLimit, PitchLimit_Elevation/_Depression, CoverArea,
IsAuto, HasBarrel and up to 80 CannonModel_NNN/CannonFrame_NNN.  Shield
generators, thrusters and bridges add PowerRatio.  Hatches add
SquadronID, LoadedCount, MaxAvailableCount, TakeoffInterval -- a
carrier's launch bay.

Control 1: StructureCount.<Kind>Count == #<Kind>_NNN records, over 684
comparisons -- 612 equal, 55 "0 declared, one blank placeholder"
(55/55 blank in Name AND NomalModel AND Frame), 11 differ, 6 kind
absent.  All 11 exceptions are Turret and all are declared < records.

Control 2: 835/835 Turret_NNN.WeaponID resolve to an ID in the
131-record Weapon datasheet, zero unresolved; 26 weapons are never
mounted on a turret.

Refuted in the same pass: "the DeltaSaber's 59 non-NULL hardpoints are
the 59-name WEAPONS arsenal roster".  The counts match exactly and the
sets overlap in 0 values -- two namespaces, one coincidence.

New structure doc, artefact and regenerator; other artefacts unchanged.
2026-08-27 12:39:50 +00:00
Sylpheed RE agent
39b3a4de7d re: correction -- Generic is a per-FILE header record, not a table
Yesterday's page said "Generic (394 per pak) is the unit datasheet".
Only 114 of the 394 are.  Every IDXD file carries exactly one Generic
record and its schema is set by what kind of file it is.  Partitioned
by field set, identically in all six GP_MAIN_GAME_*.pak:

  114  has HP                        -- a unit datasheet
  204  {Count} only                  -- a dialogue file
   64  {ID, Name, SideID, Unique}    -- a character (36 TCAF + 28 ADAN)
   10  {EnumAsteroidGroup}           -- an asteroid group
    2  degenerate

204+114+64+10+2 = 394, and 178 distinct Generic.ID = 114 unit + 64
character, the only two shapes carrying an ID.  That settles the
"178 IDs vs 394 records" question the previous entry left open.

Positive control in the same loop: for the 204 dialogue headers Count
equals the number of Message_NNN siblings, 204/204, zero mismatches.

Cross-check from the other side: Maneuver = 114, Effect = 114, the
carrying-entry sets are identical, every unit Generic has a Maneuver
sibling, and Generic.Type splits 43 Craft + 71 Vessel -- the same
43/71/114 unit-struct-runtime.md reached from live guest memory.

New tool generic_partition.py + artefact; ISL artefacts byte-identical.
2026-08-27 12:29:30 +00:00
Sylpheed RE agent
69e6f6bcd8 re: the static unit datasheet and the AI flight model
Same technique as the weapon tables -- field names are literal, so a
field-shape search over 190782 records finds the carriers.

Generic (394 per pak, 110 field names, 178 distinct IDs) is the unit
datasheet: HP, ShieldRatio, DefencePoint, AttackCraft/VesselPoint, the
five Resistance* terms, Size_X/Y/Z + radius, RadarRange 10000,
FCSRange 8500, MountedFCS, MountedShieldGenerator, Score/Damage/Mass,
NozzleCount with per-nozzle FX, and Model rou_e006 -- which ties the
sheet to the mesh names the corpus already decodes.

Maneuver (114) is the AI flight model: MaximumVelocity 1200,
CruisingVelocity 700, Acceleration 600, Deceleration 400,
SideThrustAcceleration 1000, Turn_AngularVelocity 180,
MaximumBank_Normal 60, per-axis DragFactor 3.0, afterburner and
reverse-thrust factors, plus nine named manoeuvres each with its own
timing/ratio/length bounds and AA_/AV_ rate pairs.  This is the static
source for what flight-speed-law.md measured at runtime.

Corrects a guess in the same pass: HP_CLASS/HP_ID are not ship stats --
they sit on records named for wingmen and ship classes alongside
HPGauge/RadarCursorType, i.e. a HUD gauge binding table.

New structure doc + artefact; ISL artefacts byte-identical.
2026-08-27 12:20:03 +00:00
Sylpheed RE agent
f2cacce59d re: the static weapon datasheet -- Weapon, Shell, AssortMissileParam
The corpus's weapon numbers came from a runtime capture; the static
tables are now read with no emulator.  The move: IDXD field names are
literal strings in the pool, so the stat-shaped keys already harvested
from the disassembly (MegaTons, GuidanceType, SpiralType, ...) can be
searched directly.  8648 records carry at least one, clustering on
three record names in every per-language GP_MAIN_GAME_*.pak:

  Weapon              131 records, 21 fields -- the launcher
  Shell               131 records, 37 fields -- the projectile
  AssortMissileParam    9 records, 20 fields -- missile guidance

131, not the 59 of the WEAPONS roster: that roster is the arsenal
menu's list, not the full set.

Controlled negative in the same pass: stageNN_settings.tbl does not
exist.  AUTO_SETTINGS names 28; four path prefixes give 0/116 while
both controls in the same loop are found.

Also decoded: EnumUnit (54 unit ids tying eNNN/fNNN mesh prefixes to
faction/class names), ArmsItemFile (59 weapon -> HUD icon mappings),
SETTINGS (sound config), Parameters (HUD markers).

New structure doc + artefact; ISL artefacts byte-identical.
2026-08-27 12:11:03 +00:00
Sylpheed RE agent
2a1236074b re: decode the config records -- render/input constants, and a correction
parse() + named() on each named record (data/config-records.txt).

The find, directly portable: Rendering carries the tone-mapping and
bloom numbers (Brightness 1.4, Contrast 1.0, ExposureKeyValue 0.18,
BrightPassThreshold 0.6, LuminanceMin/Max 0.15/3.6, BloomScale 0.5,
AfterimageScale 0.05, StarScale 1.20, GlareType 8), ControlTweak the
input constants (CameraSpeed 0.50, TargetSpeed 0.50, TargetMovingRange
-10..10, mov_trigger_play 20, eye/mov_stick_play 6000), and Camera
NoseCameraFOV 0.92 / ChangeTime 0.20.

Corrects my own last entry: WEAPONS is a 59-entry NAME ROSTER, not a
stat sheet, and UNITS is not units at all -- seven craft-slot/pilot
pairs, the wingman roster.  IGNORE is a 13-name blocklist over the
weapon enumeration.

AUTO_SETTINGS lists exactly 28 stageNN_settings.tbl files -- a third
independent confirmation of the 28-stage count.

The objective-shaped records are UI: SUB_OBJECTIVE is a HUD layout (two
variants ship), SQUADRON_ORDER_OBJECT is 29 .prt sprite names, MISSIONS
holds the challenge categories, STAGES has zero fields.

Docs + one new artefact; ISL artefacts byte-identical.
2026-08-27 12:02:13 +00:00
Sylpheed RE agent
bc48fa576d re: the config sections are real disc data -- 64/65, with a clean 0/54 control
Testing every literal key from data/config-keys.txt against the 3496
distinct IDXD record keys with tag_hash:

  section names (arg to sub_82448AA0) -> 64 / 65 present
  field  keys  (arg to sub_824482D0) ->  0 / 54 present

The two classes are each other's control and the dissociation is
total, confirming the model: sub_82448AA0 finds a RECORD keyed by
tag_hash(name); sub_824482D0 reads a FIELD whose name is a literal
string in the pool, so none of them can be a record key.

The single absent section is LANGUAGE -- the one real section in the
disc-root config.ini.  The same API serves both stores.

This deflates my own earlier framing: I had listed "64 sections whose
hardcoded fallbacks are worth pinning".  They are not fallbacks; all
but one are real data.  STAGENN_UNIT_MAX is the genuine exception,
because its name is built at runtime from the stage index.

The artefact now names the pak each record lives in -- notably
GP_HANGAR_ARSENAL.pak holds WEAPONS, UNITS, Camera, ControlTweak,
Rendering, AUTO_SETTINGS and IGNORE.

Docs + regenerated artefact; ISL artefacts byte-identical.
2026-08-27 11:54:36 +00:00
Sylpheed RE agent
49b5892df5 re: Stage30-33 do not exist; AIParams is missing for exactly the tutorials
Probing pak entry names with name_hash across all 41 paks:
UnitGroup_SNN.tbl exists for exactly S01-S16 and S18-S29 = 28 stages.
S17 and S30-S33 are absent, so the 33-entry STAGENN_UNIT_MAX table is
headroom over a 28-stage game.  Control is built in: 28 hits from 33
probes shows the loop resolves entry names.

Corroboration that fell out of it: AIParams_SNN.tbl exists for S01-S16
and S24-S29 but is missing for exactly S18-S23 -- the six stages the
corpus identified as tutorials by a completely different route (their
lone guard is request_next() != 1).  Two unrelated methods, same six.

Stage_SNN.tbl probes find 0, which is expected: stagetbl.py documents
that the per-stage record is not name-addressed.

Drops the [phase+356]/[phase+360] consumer question: 243 non-stack lwz
reads from those offsets image-wide, and the only ISL-region hits are
the two writers plus two vtable calls.  Not a cheap static question.

Docs only; artefacts byte-identical.
2026-08-27 11:40:29 +00:00
Sylpheed RE agent
731c3cd849 re: the section reader is a generic IDXD accessor; UNIT_MAX absence re-tested
Resolving the r4 string at every sub_82448AA0 / sub_824482D0 call site
gives 65 section names and 54 int keys (data/config-keys.txt) -- far
more than the 400-byte config.ini holds -- and BASE_INFO, SYSTEM,
MISSIONS and FONTS are all present as IDXD record keys on the disc.  So
it is a generic named-section accessor over the .tbl containers, not
"the INI reader" as I labelled it yesterday.

My first re-test was invalid: IDXD record keys are tag_hash, not
name_hash.  Scanning with name_hash returned 0 for STAGENN_UNIT_MAX and
0 for every control name too -- the control is what exposed it.

Redone with tag_hash: 7750/7750 entries parsed, 190782 records, 3496
distinct keys, controls BASE_INFO/SYSTEM/MISSIONS/FONTS found, and
STAGE01..33_UNIT_MAX -> 0 records.  With the literal search and
config.ini printed in full, the section exists nowhere on the disc, so
PLANE=200 / VESSEL=20 for every stage now rests on a controlled test.

Docs + one new artefact; ISL artefacts byte-identical.
2026-08-27 11:33:30 +00:00
Sylpheed RE agent
e46055ffae re: the config is an INI that ships nearly empty -- every stage takes 200/20
Corrects my own label: sub_824480D0/sub_82448AA0/sub_824482D0 is not an
XML reader.  The 40 <?xml pak entries are XPR2 resource manifests
(XBGMesh/Texture declarations).  The real config is a loose config.ini
at the disc root, header comment "application / game-part initial-
settings table", and every key this code touches is an INI section or
key name.

The shipped file is 400 bytes: [SYSTEM] (empty) and [LANGUAGE].  No
[STAGENN_UNIT_MAX] anywhere, and UNIT_MAX/VESSEL appear in 0 pak
entries -- so the lookup always misses and every stage runs on the
defaults PLANE=200, VESSEL=20.

The absence is controlled: the same scan decompressed 26443 entries
across 41 paks and found MSG_DEMO 192x, mapmesh_box_500km 162x and
<?xml exactly 40x, the census number.  Last iteration's null was
worthless -- it used s[8:] instead of s[10:] for the Z1 header and
searched the index rather than the .pNN data.  Using the corpus's own
reader was the whole difference.

Docs only; artefacts byte-identical.
2026-08-27 11:24:01 +00:00
Sylpheed RE agent
ad5270cfdd re: STAGENN_UNIT_MAX holds two per-stage caps, PLANE and VESSEL
Reading sub_82261F70 past the table lookup finishes the mechanism the
stage index feeds: the section name is built from the 0-based stage
index, looked up, and two ints land in the phase --

  [phase+356] = PLANE   (default 200)
  [phase+360] = VESSEL  (default 20)

with the defaults taken when a stage has no entry.  Key strings are
0x820A8B14 "PLANE" and 0x820A8B1C "VESSEL"; section and both keys are
the game's own names.  Surrounding reads (GP_SCRIPT, SCRIPTS,
Resource2D, TABLE) plus the pak census's 40 <?xm entries make this an
XML config reader.

Register discipline mattered twice: in sub_82261F70 r21 is the phase
and r31 is the STACK frame, and 0x82273400 lwz r11, 360(r11) in the
built-in switch is a vtable slot 90 call, not a phase read.

Not settled: the values (a crude zlib scan found no UNIT_MAX, but it
did not use the corpus's pak reader, so that is not evidence of
absence), and no consumer of the two fields was identified.

Docs only; artefacts byte-identical.
2026-08-27 11:14:17 +00:00
Sylpheed RE agent
17d1d2c999 re: X+12 is a 0-based STAGE INDEX -- proved by the game's own string table
Three iterations circled this.  Following the value rather than the
filename settles it in two hops:

  1. SilphScriptPhase's ctor sub_8225FEF8 never touches r7 (zero
     mentions).  The BASE ctor sub_822700C0 keeps it:
       or  r26, r7, r7   ->   stw r26, 152(r30)
  2. sub_82261F70 indexes a stack table of string pointers with that
     field.  The table is contiguous at 0x820A8880, 20 bytes/entry:
       index 0 = STAGE01_UNIT_MAX ... index 32 = STAGE33_UNIT_MAX,
       then PLANE at 33.

So X+12 = [phase+152] = a 0-based stage index, N -> STAGE(N+1), and the
gate is explained rather than described: <= 32 is the array bound (33
entries) and != 16 is STAGE17, the one stage number with no .ssb.

This retracts my own "not adopted": the stage-index reading was 1-of-1
but coincidence-shaped two iterations ago; 0-based is now PROVED by
index 5 -> STAGE06.

Caught a false positive: 0x82272D88 lwz r11, 152(r11) in the built-in
switch is a virtual call to slot 38 -- r11 is the vptr, not the phase.

Also names a third class: sub_822700C0 stamps 0x820A8E44 =
SilphScriptPhaseBase, so the hierarchy is Base <- ScriptPhase and
Base <- Demo.

Docs only; artefacts byte-identical.
2026-08-27 11:05:50 +00:00
Sylpheed RE agent
4feab6c579 re: a script load builds TWO phase objects, main at Y+4 and demo at Y+8
sub_8225FEF8's only caller is sub_82260568, which looks like a derived
ctor calling its base -- i.e. every phase is a Demo, which would make
the built-in dispatch inert.  Reading the constructor refutes it: it
allocates 10216 bytes and runs SilphScriptPhase's ctor on object A,
then initialises a second object B and stamps the Demo vptr on it.
They land at Y+4 and Y+8, which is why the load path resolves the
ScriptPhase through Y+4.

The Demo class's 109 overrides all point at three no-op stubs; only
slot 15, slot 111 (Update = sub_82275800), slot 112 and the destructor
survive.  DEMO is the corpus's established cutscene text family, so the
cutscene-runner reading is recorded as a reading, not a result.

Also notes a new handle for the stalled X+12 question: the kind is
passed straight into sub_82260568 as its third argument.

Docs only; artefacts byte-identical.
2026-08-27 10:14:00 +00:00
Sylpheed RE agent
044b98dcc4 re: the ScriptPhase vtable is 113 slots, and there is a SilphScriptPhaseDemo
RTTI at vtable[-1] names the class .?AVSilphScriptPhase@silph@@ and the
next class's COL at 0x820A8680 terminates the table at 113 entries.
The earlier ">=200 slots" reading used a bad terminator -- scanning for
a non-code word runs straight into the next vtable.  Cross-check: the
147 built-in stubs use 109 distinct slots, min 0 max 110.

The next vtable is .?AVSilphScriptPhaseDemo@silph@@, also 113 slots,
overriding 109 of them with mostly one shared stub -- a cut-down phase.
That explains why sub_822710D0 has two callers: sub_82263408 and
sub_82275800 are the two classes' Update, both slot 111.

Both "unread spawner callers" are placed: sub_82264058 is slot 0 of
SilphScriptPhase and sub_82273910 is slot 0 of the Demo class -- the two
destructors.

Also records a negative: the Stage%02d construction site I proposed last
iteration does not exist (no stage format string among the 43 short %d
strings; no precomputed name hash anywhere), so the stage-index lead
cannot be settled that way.

Docs only; artefacts byte-identical.
2026-08-27 10:05:01 +00:00
Sylpheed RE agent
6213825a2f re: the GamePart_MainGame vtable names the code region; a stage-index lead
sub_821A6CF0 is reached only by a tail b from a run of adjustor thunks
whose vtable is at 0x820A319C.  Slot 1 is 'addi r3, r0, 17; blr' -- the
factory id -- so RegisterToFactory<17, class silph::GamePart_MainGame>
names the class, and every thunk's lwz r3, 8(r3) makes slots 4-10
GamePart_MainGame::Impl methods (9 = the per-frame Update
sub_821AA1B0).  Several iterations' worth of inference is now a read.

Lead, not adopted: the disc has 28 scripts numbered Stage01-16 and
Stage18-29 -- exactly one gap, Stage17 -- and the gate excludes exactly
one value, 16, which lines up 0-based.  Control comparison: the refuted
GamePart-id reading had 4 missing values (1-of-4, worthless), this is
1-of-1.  Tighter but still coincidence-shaped.  The Stage%02d
construction site would settle it; not found this pass.

Docs only; artefacts byte-identical.
2026-08-27 09:56:34 +00:00
Sylpheed RE agent
2c5c8b11d9 re: the (16,32] gate is != 16 && <= 32, a validity check on a load parameter
Corrects this corpus's own notation: there is no lower bound, values
below 16 pass.

sub_8225EC78(X, kind, ...) -- the function carrying "script load
cancel" -- stores its second argument into X+12 and applies the same
two tests to it immediately, bailing out of the load.  So the gate is
the object's invariant, re-checked at every later touch, not a
selector.  42 sites image-wide have the shape, 34 of them in one code
region plus 3 in sub_8225EC78.

Object chain: X = [GamePart+104] is the current script instance
(installed at 0x821A78EC, previous one torn down via sub_8225EB60);
X+4 -> Y; Y+4 -> the ScriptPhase; Y+72 and [ScriptPhase+10152] get
the answer.

Refutes my own lead in the same pass: "X+12 is a GamePart id and 16 is
the unregistered one" fails its control -- 1, 2 and 18 are also absent
from the RegisterToFactory list, so 16 is one of four gaps.

Docs only; artefacts byte-identical.
2026-08-27 09:48:36 +00:00
Sylpheed RE agent
237dbce155 re: [phase+10152]/[phase+10156] are an answer and a request state
Built-ins 102/103/130 are one trio over two phase fields, read off
four consecutive vtable stubs (slots 70/71/72/97).  102's body:

  state==1 -> return 2                     (pending)
  state==2 -> special[0] = (answer==1); state=0; return 0
  else     -> state=1; answer=0; return 2  (issue)

2 and 0 are the dispatcher's thread codes, so 102 blocks the coroutine
until an answer arrives; 103 is the non-blocking form and 130 clears
the answer.  The answer is published by sub_821A9DC8 under the same
(16,32] gate that request_next's writer uses.

Flags an inherited name I could not verify: prompt_yes_no (102) has no
derivation recorded anywhere in the corpus, so 103 and 130 are left
unnamed rather than named off it.

Docs only; artefacts byte-identical.
2026-08-27 09:39:52 +00:00
Sylpheed RE agent
af61c162f3 isl: built-in 104 is request_next -- named from the game's own log string
Followed the writer, not the reader.  [phase+10160]'s only writer in
the image is one site in the mission frame loop sub_821AA1B0: it does
obj->get() on an object fetched from a registry by id 0x20FFFF02,
stores it to the phase, then clears the object -- read, publish, clear,
every frame.

The id namespace has exactly three members (0x20FFFF00/01/02), each
built at exactly 4 sites, and two of those are in sub_821D5178, which
gets 0x20FFFF01 and 0x20FFFF02 and logs both:

  GamePart_ReadyRoom::Impl::OnCommand - Wait() command is requested.
  Check flow control valiables. WAIT_MODE : %d, REQUEST_NEXT : %d

Argument order gives 0x20FFFF01 = WAIT_MODE, 0x20FFFF02 = REQUEST_NEXT.
PrepareScript corroborates: it sets WAIT_MODE=1, REQUEST_NEXT=0 before
an ISL script runs.  So the six tutorial stages' lone dominating
condition request_next() != 1 is the script waiting on the game part's
proceed flag.

6 artefact lines changed, all 6 pair exactly.
2026-08-27 09:31:01 +00:00
Sylpheed RE agent
b86c8c82ba isl: rename built-ins 8/9/93 to stopwatch_start/_elapsed/_stop
They are start / read / stop of one of 32 per-phase stopwatches, not
flag operations.  123-127 keep timer_* -- that is the mission timer,
five scalars at [phase+304..320], a different clock.

Artefact check: 84 lines changed across 5 files and all 84 pair
exactly with their old-name partners once column padding is
normalised (0 removed lines without an old name, 0 added lines
without a new one).  data/isl-timers.txt reproduces the same
675/675, 11.2 % control, 82/1 and identical histograms, which is
what shows the rename is cosmetic.

Also withdraws a label from the previous commit: sub_8230C398 is NOT
the message pump.  It runs every frame but drains nothing -- a state
machine on [0x828E1F8C] that only allocates, builds strings, looks up
and PUSHES.  And bus+8216 is weak evidence: sub_82254A08 is a generic
map find with ~120 sites, and the key looked up is a pointer, not a
tag.  The open handle is now the ring buffer at bus+4, not bus+8216.
2026-08-27 09:21:04 +00:00
Sylpheed RE agent
bf17f19f67 re: the timer message's consumer is a runtime registry, not a tag compare
The move I flagged last iteration -- "0xAB03E5BA is built once, so
nothing consumes it" -- is refuted by its own control: 25 of the 40
distinct 0xAB03xxxx tags in the image are built exactly once (62.5 %),
and no 4-aligned word anywhere in the image begins 0xAB03, so tags are
never stored statically.

What does stand:
  * sub_82175C20(bus+4, &msg) is a ring-buffer PUSH, not a dispatch.
  * The message class has a one-method vtable {scalar deleting dtor}
    with an MSVC RTTI locator at vtable[-1] -- no handler of its own.
  * The bus at [0x828F35DC] has a map at +8216, looked up via
    sub_82254A08 from sub_823001E8, whose only caller is the per-frame
    pump sub_8230C398.

So the item is NOT settled: the insert path into bus+8216 is unread,
and whether running out of time ends the mission is still unknown.

Docs only; ISL artefacts regenerate byte-identical.
2026-08-27 09:12:36 +00:00
Sylpheed RE agent
d79f59d24e re: timer_set's second argument is a SECOND countdown, not a warning threshold
mission-phase-timers.md left 180 open as "a limit and a warning
threshold is the obvious reading, but not established".  Reading
sub_822639B8 -- ScriptPhase::Update 0x82263528, same dt as the
stopwatch bank -- settles it the other way:

    if running: [+304] += dt
    if armed:   [+308] -= dt   while [+308] > 0
                else [+312] -= dt, clamped at 0

[+312] is never compared with [+308]; it is decremented, and only in
the A<=0 arm.  Two sequential countdowns.  Disc-wide the second
argument is 180 in all 29 timer_set sites while the first varies
(600 x19, 1200 x8, 900, 1800).

Built-ins 123-127 are vtable slots 90-94 on five scalars at
[phase+304..320].  125 and 126 have ZERO call sites in all 28 scripts:
the script arms, starts and stops this clock but never reads it.

Corrects mission-phase-timers.md, which merged this clock with
stopwatch 0 -- timer_resume starts [+304], set_flag(0) one instruction
later starts the stopwatch the timeline's kind=0 reads.

Docs only; all seven ISL artefacts regenerate byte-identical.
2026-08-27 09:03:18 +00:00
Sylpheed RE agent
3cad5d063f re: read_freg counts SECONDS -- the 32 "flags" are a stopwatch bank
The open question was the unit, not the array.  Following the writers of
[phase+88] settles it:

  * sub_822710D0(phase, dt), called from ScriptPhase::Update, does
    prev[i] = cur[i] then, only while [phase+120][i] == 1, cur[i] += dt --
    so +88/+104/+120 are current / previous / running, 32 entries each.
  * dt is seconds by a non-circular round trip: frames * (1/60) * 10000
    -> clamp 3200 -> * 1e-4, in the timing singleton at [0x828F35B4].
    The clamp is 0.32 s, a frame ceiling.
  * 675/675 timeline kinds are indices their own phase starts (control
    11.2 %), which is why kind is only ever 0 or 5.

Corrects isl-builtins.md twice: set_flag writes 0.0 not 1.0, and
clear_flag clears the running flag rather than the value.  Confirms its
grouping of 8/9/93 as one family.  Docs only -- all seven ISL artefacts
regenerate byte-identical.
2026-08-27 08:48:49 +00:00
Sylpheed RE agent
20b8412a38 re: read_freg reads the timeline's clock array -- "six clocks" was my mislabel
Built-in 9 is inline in its stub, with no vtable slot.  It bounds-checks
0 <= local[0] < 32 and then:

    822725E8  lwz    r10, 88(r31)          ; [phase+88]
    822725EC  rlwinm r11, r11, 2, 0, 29    ; i * 4
    822725F0  lfsx   f0, r11, r10          ; a FLOAT

isl-builtins.md already recorded read_freg(i) -> [phase+88][i].  What is new is that
this is exactly the array, and exactly the indexing, that the TIMELINE walker uses for
its `kind` field.  So kind is not a selector over six private clocks -- it is an index
into the same 32-entry float register file the script reads with read_freg(i).  A
clear condition like `read_freg(0) < 1200` and a timeline entry with kind = 0 read the
same register.  My "six clocks" framing came from the data only ever using indices 0
and 5, and it is corrected in isl-schedule.md.

Recorded as a nuance rather than a correction, because it is half-read: clear_flag
(93) clears a DIFFERENT 32-entry array, [phase+120], looping to 128 in steps of 4 --
not [phase+88].  set_flag (8) does bounds-check against [phase+88], but only its first
13 instructions were read, so the corpus's grouping of 8/9/93 as one "32-entry file"
family is neither confirmed nor overturned here.

This does NOT settle what the item actually asked: read_freg returns a float out of a
register file, and that the unit is seconds remains INFERRED from the 210/300/600/1200
gate values, exactly as it was before.  Also still open: what writes [phase+88], which
has 73 writers in the ISL region alone.

All artefacts regenerate byte-identical; documentation only.
2026-08-27 08:28:26 +00:00
Sylpheed RE agent
b63e6cc8af re: A/B in the trigger test -- searched, not settled, and the matrix reading excluded
Ran the "who writes this" move on the two 3-double vectors the trigger condition pulls
out of the unit record.  It did not settle them, and it is worth recording why.

99 sites across 40 functions write +32/+40/+48; 16 sites across 13 write +64/+72/+80;
six functions write both.  The most inviting is sub_821AD218, which is a float-to-double
widening copy of exactly 16 floats -- lfs from r4+0..60, stfd to r3+0..120.  Sixteen
floats is a 4x4 matrix, under which +32/+40/+48 is row 1 and +64/+72/+80 is row 2,
making A and B basis vectors rather than positions.  That is a tidy story and it is
wrong for this record.

The disproof was already in hand: sub_8226DAF8 reads `lwz r10, 100(r11)`, a WORD at
+100, which falls inside where a 16-double matrix's row 3 (+96..+120) would sit.  A
second writer, sub_8226A348 -- same built-in shape, [phase+324] indexed by local[4]
with the rec+4 liveness check -- confirms a mixed layout: words at +0/+4/+8/+12/+16 and
at +80, doubles only at +32/+40/+48.

So these offsets recur at the same positions in several unrelated object layouts, and
nothing found here ties a specific writer to the object the trigger actually reads.

A and B stay unnamed.  That is the second tidy reading rejected for them, after
previous/current-position.

All artefacts regenerate byte-identical; documentation only.
2026-08-27 08:21:58 +00:00
Sylpheed RE agent
c9d479f083 re: the kind-1 trigger condition is the same test with a pre-filter -- mechanism read end to end
sub_8226DC80 turns out to be sub_8226DAF8's twin.  Diffing them instruction by
instruction, 33 of the first 86 differ, and every difference before index 65 is a
register rename or a branch target.  Both:

  * resolve the route name (sub_8217FA08 -> sub_823012D8), leaving the route point in
    [r31+112/120/128];
  * index [phase+324] by payload+0 and require rec+100 != 0;
  * copy the two 3-double vectors out of the unit record.

They part at index 65.  Kind 0 calls vtable slot 60 immediately.  Kind 1 instead
computes an inline point-to-point distance first -- three fsub against the route
point, fmul plus two fmadd, fsqrt -- and rejects when it exceeds f31, the same radius:

    8226DDC4  fsqrt f0, f0
    8226DDC8  fcmpu cr6, f0, f31
    8226DDCC  bc    4, gt, return-0
    8226DDE8  lwz   r11, 60(r11)      ; then the SAME slot 60
    8226DDF0  bcctrl

So both trigger kinds run the identical point-to-segment test; kind 1 only adds a
cheap early-out against the same radius, and per the drain it does not spawn.

Diffing the two functions rather than reading the second one cost one query and made
the relationship obvious -- the same move that resolved the six ISL branch handlers.

All artefacts regenerate byte-identical; documentation only.

What remains on triggers is peripheral: which unit-record fields A and B are, slot 60
past the t < 0 rejection, payload+8, and the kind-1 path in the drain.
2026-08-27 08:14:24 +00:00
Sylpheed RE agent
950db19f1b re: the kind-0 trigger condition is a point-to-segment proximity test
sub_8226DAF8 past the route-name lookup: it resolves the name through sub_823012D8,
requires the unit record's rec+100 != 0, pulls TWO three-double vectors out of that
record -- rec+32/+40/+48 and rec+64/+72/+80 -- and then makes a virtual call:

    8226DC1C  lwz  r11, 0(r30)     ; the ScriptPhase vptr
    8226DC28  lwz  r11, 60(r11)    ; vtable slot 60
    8226DC30  bcctrl               ; test(phase, A, B, P = the route, f1 = node+16)

Slot 60 resolves to sub_82268068, which no built-in uses, so it is engine-internal.
Its arithmetic leaves no room for interpretation: per-axis fsub, fmul plus two fmadd,
fsqrt, giving |B - A|; reject if that is under 0.1 (the constant is literally 0.1, a
degenerate-segment guard); reject if |P - A| > f1 or |P - B| > f1; then compute
t = (P-A).(B-A) / |B-A|^2 and reject if t < 0.

That is a point-to-segment distance test, and it makes the node's double -- built-in
19's local[24] -- a RADIUS.  So a kind-0 trigger fires when a route point lies within
that radius of the segment between two vectors from the unit's record.

Recorded as unproven: WHAT A and B are.  A previous/current position pair would make
this the standard frame-rate-robust waypoint test, and it would be easy to write that
down -- which is exactly why it is not written down.  rec+64/72/80 and rec+32/40/48
are two positions and nothing here says which.

All artefacts regenerate byte-identical; documentation only.

Still unread: sub_8226DC80, the kind-1 condition, entirely; and slot 60 past the
t < 0 rejection, where a second constant at 0x820B0000+25192 is loaded.
2026-08-27 08:08:01 +00:00
Sylpheed RE agent
5670716566 re: both trigger kinds watch a ROUTE -- the unit-reaches-route mechanism
The kind-0 condition tester sub_8226DAF8 resolves a symbol as well, but from
payload+4 rather than +28:

    8226DB14  rlwinm r10, r5, 2, 0, 29   ; r5 = payload+4, x4
    8226DB24  lwz    r11, 244(r30)       ; [phase+244] = symbol table 1
    8226DB3C  lwzx   r10, r10, r11
    8226DB50  addi   r4, r11, 4          ; -> the name string

Both appenders write local[12] into whichever slot their own path reads -- built-in 19
into +4, built-in 25 into +4 and +28 -- so the same operand is the symbol either way,
and it resolves cleanly:

    built-in 19   local[12] -> symtab 1   79 / 79   all symbol type 1
    built-in 25   local[12] -> symtab 1   25 / 25   all symbol type 1

isl-builtins.md records symtab-1 type 1 as the Route_* names, and the samples agree:
Route_ADN106_p1F, Route_ADN1xxe1_p1F, Route_ADS101_p1F, Route_TCN004_p1S.

So both built-ins register a trigger on a UNIT reaching a ROUTE -- payload+0 is the
unit, local[12] the route.  They differ only in the payoff: built-in 19 additionally
carries a handler routine (local[32] -> payload+28, kind 0 -> spawn) while built-in 25
carries none (kind 1 -> no spawn).  That is a third independent agreement with
isl.py's SYM1_SLOTS, which lists slot 12 for both and was derived from operand ranges
alone.

It also joins up with the closed REMAINING OB work, where the counter was measured to
rise at a squadron's route arrival time -- route triggers are the mechanism that would
do that.  Recorded as a connection, not a demonstration: nothing here traces a trigger
to that counter.

All artefacts regenerate byte-identical; documentation only.

Still unread: what the conditions actually COMPARE -- only sub_8226DAF8's first ~34
instructions are read, up to the name lookup, and sub_8226DC80 not at all -- and
payload+8, a computed value passed to both testers.  A waypoint index would fit it,
which is exactly why it is not being called one.
2026-08-27 08:00:22 +00:00
Sylpheed RE agent
3a56cc6998 re: +24 is the trigger kind tag -- built-in 19 registers a routine, built-in 25 a route
Read straight off the drain, immediately after the pop:

    8226D7AC  lwz   r11, 92(r31)        ; payload+24
    8226D7B8  cmpli cr6, 0, r11, 0x1
    8226D7C4  bc    12, lt, 0x8226D89C  ; +24 == 0 -> the SPAWN path
    8226D7C8  bc     4, eq, 0x8226DA44  ; +24  > 1 -> bail
                                        ; +24 == 1 -> fall through, a NON-spawn path

So the field I recorded last iteration as "kind-tag shaped, but the drain was not
shown branching on it" does branch on it, three ways.

  +24 = 0, written by built-in 19: condition sub_8226DAF8, then bl 0x822737C8 --
      +28 is a code offset, matching the 79/79-vs-27.8%-control measurement.
  +24 = 1, written by built-in 25: condition sub_8226DC80, no spawn, and the path
      resolves +28 through [phase+244] -- symbol table 1:
          8226D834  lwz  r10, 112(r31)     ; payload+28
          8226D838  lwz  r11, 244(r29)     ; symtab 1
          8226D848  lwzx r10, r10, r11

And that resolution checks out: over all 25 call sites disc-wide, built-in 25's
local[12] resolves to a symbol-table-1 entry 25 times out of 25, and every name is a
route -- Route_ADS101_p1F, Route_ADT107_p1F, Route_TCN004_p1S, Route_ADT202A_p2S.
The 1-12 values that looked like a puzzle two iterations ago are simply small symbol
indices.  This independently confirms isl.py's SYM1_SLOTS, which lists slot 12 for
built-in 25 and was derived from operand ranges alone.

So there are two kinds of trigger: built-in 19 registers one whose payload is a
HANDLER ROUTINE, built-in 25 one whose payload is a ROUTE.

All artefacts regenerate byte-identical; documentation only.

Unread: both condition testers (sub_8226DAF8, sub_8226DC80), what the kind-1 path
does with the route past sub_8217FA08 / sub_8218CC88 / sub_8225FEA0, and the drain's
second spawn site at 0x8226DA10.
2026-08-27 07:53:26 +00:00
Sylpheed RE agent
4f1f5ac0a2 re: the unreached code -- five mechanisms eliminated, nothing in the file names it
Inverting the question -- read what an unreached routine DOES, rather than testing
another candidate mechanism -- sharpened this a lot without solving it.

The metric was partly inflated.  Of 24901 unreached instructions in 564 runs, 458 runs
are length 1, and 111 of those are a lone `ret` stranded after an end_coroutine, with
the next routine being a properly seeded entry.  Stage 02's first two "unreached
routines" are exactly that.  But those are only 458 instructions: the real gap is
24443 in 106 runs, the largest 1526 (Stage 26), 1496 and 1481 (Stage 29), 1069, 904.

And it is live mission logic.  Stage 26's 1526-instruction run stages arguments and
calls builtin57(ADN110, 1) and set_group_speed(ADN110, 2, 0) -- named units, named
built-ins.

New this iteration: kill_coroutine (built-in 5) is confirmed as a code-offset carrier,
which is what the corpus said -- its local[0] resolves 150/150 onto the instruction
stream against a 34.0% control, values 13740..221048.  And, like the others, it points
at zero unreached run-starts.

That is now five mechanisms.  Across 2757 code references -- start_coroutine 1765, the
phase timeline 675, built-in 19's trigger handlers 79, kill_coroutine 150, and the
0x1883 records 88 -- not ONE lands on an unreached run-start.  A categorical absence,
not a near miss.

Not a tool artefact: 1765 of 1765 start_coroutine sites resolve with 0 missed, 0
entries land outside their phase region, and 0 are discarded by the on-stream filter.

Combined with the earlier result that an unreached routine's offset appears nowhere in
the file as a word in any encoding, these routines are referenced by nothing inside the
.ssb.  Either they are unused in this build, or they are entered from outside it.  A
1526-instruction routine driving a named squadron sits badly with "unused", but that
discomfort is not evidence and the two readings are recorded without choosing.

No static test I have devised can decide it; a runtime probe logging sub_822737C8's
third argument through a mission would.

All artefacts regenerate byte-identical; documentation only.
2026-08-27 07:46:56 +00:00
Sylpheed RE agent
6402a34d9b re: two trigger appenders -- built-in 19's node carries the handler routine
Resolves the puzzle the previous commit left open: payload+28 is a 1-12 index, yet
the drain hands it to a spawner that wants a code offset.  The cause is that built-in
25 is not the only thing that appends a trigger node.

Searching for functions that write a double at +16 AND a word at +28 of the same
register finds exactly two: sub_8226A0D8 (built-in 25, vtable slot 28) which fills the
payload inline, and sub_8226E160, a free-list recycling variant whose sole caller
sub_82269ED0 is vtable slot 68 = BUILT-IN 19.

Built-in 19 passes local[4] -> node+0 (the unit), local[12] -> +4, local[24] -> +16 as
a double, and local[32] -> +28.  That last one is the field the drain spawns from, and
it is a genuine code offset:

  over all 79 call sites disc-wide
    land on the instruction stream    79 / 79 = 100.0%
    control, random 4-aligned offsets           27.8%
    values 12164 .. 111080, 73 distinct

Large, distinct, and every one resolves against a control that resolves barely a
quarter of the time.  So built-in 19 registers a trigger whose handler is a routine,
and the drain's `base + payload+28` spawn is that handler.  The two builders simply
put different things in the same slot.

Observed alongside: built-in 25 writes +24 = 1 and built-in 19 writes +24 = 0, which
is kind-tag shaped -- but the drain was not shown branching on it, so that is recorded
as an observation and not a reading.

All artefacts regenerate byte-identical; documentation only.

Still open: whether +24 selects between the node kinds, and this STILL does not
explain the unreached code -- 0 of the 79 handler offsets are unreached run-starts.
2026-08-27 07:38:24 +00:00
Sylpheed RE agent
9ee32f7940 re: decode the trigger node -- and withdraw two of my own claims about it
The reader is sub_8226E220, called by the per-frame drain sub_8226D740 as
(phase+272, six out-params).  It pops one node and copies seven payload fields out.
Built-in 25, the appender, writes exactly those seven offsets:

    +0   local[4]  -- the unit          +24  the constant 1
    +4   local[12]                      +28  local[12] again
    +8   computed                       +32  --
    +16  a DOUBLE from local[16]

Neither side was derived from the other, so the agreement is the check.  Container:
+16 head, +20 pending count -- which matches the count isl-builtins.md watched live at
phase+272+20 from a completely different direction -- and +24 a cursor.

WITHDRAWN, from the previous commit: "the drain spawns from [node+112]".  r31 = r1 -
256, the stack frame, so +112 is an output slot and not a node field.

REFUTED, the follow-up hypothesis that the trigger carries the routine offset and so
names the code nothing else starts: payload+28 is local[12], and across all 25 call
sites disc-wide those values are the small integers 1 through 12 -- 1 of 25 (4.0%)
land on the instruction stream against a 16.0% control, and none are unreached
run-starts.  Below chance.  My first version of that test used n=2, Stage 02 only; it
happened to agree, but two samples could not have supported it either way.

One correction in the other direction, to the corpus: isl-builtins.md withdrew
sub_8226E458's link to the trigger queue on the grounds that "the argument is
lwz r4, 324(r29), the unit array, not the trigger container".  That is the SECOND
argument; the first is r26 = phase + 272, set twelve instructions earlier.  The drain
does operate on the container.  What sub_8226E458 does to it remains unread, so only
the argument is corrected, not the conclusion.

All artefacts regenerate byte-identical; documentation only.

Still open: what local[12] indexes; what the drain actually spawns, since [stack+112]
is filled from payload+28 yet the spawner wants a code offset, so some step in that
chain is not what I read; and what starts the ~15% of unreached code.
2026-08-27 07:30:41 +00:00
Sylpheed RE agent
a3cb3339bc re: sub_822737C8 is the coroutine spawner; the record's third word is the MAIN ENTRY
Asking who calls the function the timeline calls enumerates every way an ISL routine
can begin.  sub_822737C8(phase, base, offset) computes base + offset early on, and has
seven real call sites: the phase initialiser sub_82270DF8, the built-in stub region
(start_coroutine), the timeline walker sub_822748D0, TWICE inside sub_8226D740 -- the
per-frame engine->script drain -- and two unread, sub_82273910 and sub_82264058.

CORRECTION to my own write-up: the 0x1883 record's third word is the phase's MAIN
ENTRY, not a "size".  The initialiser hands it straight to the spawner:

    8227101C  or  r5, r22, r22     ; the record's third word
    82271020  or  r4, r26, r26     ; the code base
    82271030  bl  0x822737C8

44 of 44 records land on the instruction stream (100%) against a 25.0% control, and
all three Stage-02 targets open with the identical prologue
`special[0]=0 ; local[0]=0 ; call builtin116(0)` -- a routine entry, not a length.
So the record is 0x1883, base, MAIN_ENTRY, 0, code_end, force_end_handler.

Seeding the main entries moves no coverage number: every one was already among the
CFG's entry points by another route.  This corrects a field's meaning, not the graph.

Lead recorded rather than claimed: both of the drain's spawns take their offset from
[node+112], the first field of a drained node to be located, and the best remaining
angle on the ~15% of code nothing appears to start.  It is NOT shown that those nodes
come from the trigger queue at phase+272 -- that is precisely the over-reach
isl-builtins.md already made and withdrew, so it is not asserted here.

All artefacts regenerate byte-identical; this is documentation only.
2026-08-27 07:22:51 +00:00
Sylpheed RE agent
4e948d0317 re: the timeline's consumer confirms the decode, and kind is a clock index
The schedule decode previously rested on structural checks alone.  The engine's own
reader is now found, statically.

The phase initialiser sub_82270DF8 takes the 0x1883 record's six words as arguments
and stores the table pointer:

    82270FDC  add  r10, r26, r21      ; code_base + entry_a
    82270FEC  stw  r10, 240(r30)      ; [phase+240]

[phase+240] has exactly two readers in the ISL region, and one -- sub_822748D0 -- is
called from sub_82263408, ScriptPhase::Update.  It walks the table and confirms every
field independently of my inference: N at table+4, kind at group+20, float t at
group+12, offset at group+4, and a 24-byte stride (three 8-byte records).

kind is NOT a flag.  `rlwinm r11, r11, 2, 0, 29` then `lfsx` -- it is multiplied by 4
and used to index float arrays at [phase+104] and [phase+88], so it selects WHICH
TIMER the entry is scheduled against; 0 and 5 are two of at least six.  An entry fires
only when its t lies between the two readings, the classic "which events did this
frame cross" test.  Which array is previous and which is current follows from the two
comparison directions and is stated as a reading of those branches, not measured.

Both arrays are fields of the phase object, so the clock is PER PHASE -- which is why
every phase's table restarts at t = 0.  That was an inference from the layout in the
previous commit; it is now a read.

All five artefacts regenerate byte-identical: this changes documentation only.

Still open and said so: what the six clocks are (only indices 0 and 5 appear in the
data, and neither array was traced to a source); sub_822737C8, which actually starts
the routine, is unread; and this is still not what starts the unreachable code, since
0 of the 675 targets are unreached run-starts.
2026-08-27 07:15:25 +00:00
Sylpheed RE agent
2463748a71 re: the trailing data table is a per-phase TIMELINE of scheduled routines
Decodes the table found at the end of every phase region.  Layout:

    int  N
    N x [ int offset ; float t ; int kind ]      -- 8-byte typed records,
                                                    tag 0x19 int, 0x1A float

1 + 3N matches the record count in every phase measured (Stage 02: 76/40/55
records for N = 25/13/18).

Checks, all independent of each other:
  schedule entries disc-wide                675
  0x1A float records disc-wide              675   (counted by a different route)
  offsets landing on the instruction stream 675/675 = 100.0%
  control, random 4-aligned offsets                  33.3%

The floats are seconds -- 0, 0.5, 1, 4, 5, 30, 50, 60, 90, 120, 150, 170, 180, 210,
240, 270, 300, 330, 360, 420, 570, 1020, 1080, 1140, 1170 -- and the targets are small
one-shot coroutines that set arguments, call one built-in and end_coroutine.  kind is
0 (556) or 5 (119) and is not identified.

Runtime cross-check, recorded as consistency rather than confirmation: the closed
REMAINING OB work measured Stage 02's squadron arrivals at t = 0, 120 and 210 s over
n=5 emulator runs, and all three appear in phase 1's static schedule, with 120 and 210
each appearing TWICE.  These are round numbers and phase 1 has ~22 distinct times over
0-1170, so presence alone is not unlikely; the doubling is the sharper detail and was
not predicted in advance.

New artefacts data/isl-stage02-schedule.txt and data/isl-schedule-all.txt with a
committed generator (isl_report.py schedule).  calls, phase-ends, conditions and
phase-guards all regenerate byte-identical.

Not settled and said so: kind is unread; the consumer is unread, so the decode rests
on the structural checks above; whether the clock is per-phase or per-mission is an
inference from the layout; and this is NOT what starts the unreachable code -- 0 of
the 675 targets are unreached run-starts, so that ~15% gap stands.
2026-08-27 07:07:35 +00:00
Sylpheed RE agent
02d3c9c82b re: entry_a is a code/data boundary, not an entry -- and the decoder was reading data as code
Disassembling the three Stage-02 entry_a targets shows opcodes 0x19 and 0x1A, and the
ISL dispatcher's table has 25 entries (cmplwi 0x18).  They are not instructions.  Each
phase region ENDS with a trailing data table of 8-byte typed records -- tag 0x19 = int,
tag 0x1A = IEEE float (0.0, 0.5, 1.0, 4.0) -- and entry_a is where it starts.

Confirmed across the disc: in 44 of 44 phases the first offset whose opcode exceeds
0x18 is exactly that phase's entry_a, with zero exceptions, and only two tags ever
appear (1394 x 0x19, 675 x 0x1A).  So the record is

    0x1883, base, size, 0, code_end, force_end_handler

one boundary and one entry, not two entries as the previous commit said.

That also retires this thread's own "82 of 88 land on a valid instruction = 93.2% vs a
38.6% control" as TOO WEAK a test: a data record has length 8 and passes "nonzero,
even".  The entry_b result stands on different evidence -- those targets were matched
against isl.call_sites(), an independent enumeration.

isl.linear_offsets was decoding all 2069 data records as instructions, 1.23% of the
stream.  Now each phase's walk stops at its boundary:

  decoded instructions   168251 -> 166182  (= 168251 - 2069, as predicted)
  opcode > 0x18               2069 -> 0
  call sites covered     25705/25705 -> 25705/25705
  exits unreachable                0 -> 0
  conditions unknown             400 -> 400

Recorded because the first attempt at the fix was worse than the bug: it destroyed 36%
of the stream (168251 -> 107596, exits 0 -> 74) because linear_offsets is ONE global
walk from the first phase base, so stopping at phase 1's table lost every later phase.
It has to skip the region and resume at the next base.  A count moving hard in the
wrong direction is the same signal as one that will not move.

Still open: the table's contents are undecoded -- its int values land on the
instruction stream 46/51 against a 29.5% chance rate, but 0 of them are unreached
run-starts, so this is not what starts the unreachable code either.
2026-08-27 06:59:58 +00:00
Sylpheed RE agent
5c4ae3ad7b re: the 0x1883 record carries entry points -- every phase exit is now reachable
data/isl-phase-guards-all.txt goes from 5 of 177 unreachable exits to 0.

The cheap first step failed, usefully.  An unreached routine's entry offset does NOT
appear as a word anywhere in the file, in any encoding: phase-relative 6.6% against an
11.5% control on reached offsets, absolute 1.6% vs 3.3%, and the /4 forms 0-1.6% vs
6.6-8.2%.  Every variant is at or below its control, which rules out the whole family
of "some instruction operand points at them".  It also rules out dead code: Stage 02's
3069 unreached instructions contain 485 calls, including start_coroutine x75,
squadron_attack x59, set_group_speed x42 and objective_marker x13.

The answer is the mission-level stream that isl-bytecode.md already partly read.  Each
0x1883 record is

    0x1883, base_delta, size, 0, entry_a, entry_b     ; entries PHASE-RELATIVE

Measured over all 28 stages, 82 of those 88 values land on a valid instruction --
93.2%, against a 38.6% chance rate for a random 4-aligned offset.  In Stage 02
entry_b is the phase's force-end handler: 0x1482C, 0x249F0 and 0x34A10, two of which
were exactly the unreachable exits, and the third being already reachable is the
consistency check.

Seeding them: exits unreachable 5 -> 0.  Those exits now report 0 necessary
conditions, which is what an engine-entered abort handler should look like.

Recorded because it is the same mistake twice: the first seeding attempt moved NOTHING
(reach 85.0% -> 85.0%, exits 5 -> 5).  dominating_conditions() builds its own entry set
and did not use the one I had patched -- fix-the-instance-not-the-class again, caught
only because an unchanged count is by now a standing signal.

Not settled and stated: reach went only 85.0% -> 85.2%, so what starts the other ~15%
of code is still unknown, and the negative above says it is not an operand in the file;
entry_a is unidentified; 6 of the 88 values do not land on an instruction.
2026-08-27 06:51:16 +00:00
Sylpheed RE agent
da6eb236ef re: the trigger-queue appender is built-in 25 -- the script appends, not the engine
isl-builtins.md carried the trigger queue at phase+272 a long way -- layout, live
count at +272+20, the generic appender sub_8226EAB8 -- and stopped at "what actually
appends a node is still unidentified", proposing a gdb watchpoint on a running
emulator.  It is a static question after all.

sub_8226EAB8 is a shared helper with 16 callers, so it cannot be identified by name.
But the corpus records that the trigger container EMBEDS its inner list at +12, so the
appender must hand it container+12.  Only two of the sixteen callers do, and one sets
its base 34 instructions earlier:

  8226A22C  addi r29, r24, 272     ; r24 = the ScriptPhase
  8226A2B4  addi r3,  r29, 12      ; -> phase + 272 + 12
            bl   0x8226EAB8

phase+272+12 is exactly the inner list whose count the corpus watched at +272+20.

sub_8226A0D8 is ScriptPhase vtable SLOT 28, which is what BUILT-IN 25's stub
tail-calls, and there are ZERO real `bl 0x8226A0D8` instructions in the image -- it is
reached only through the vtable.  So the queue is populated by the SCRIPT, retiring
the residual "the engine moves records into the phase's queue each frame" reading that
isl-builtins.md had already flagged as unsupported by its own call site.  Built-in 25's
arguments agree with two independently measured tables: UNIT_ARG (a unit at local[4])
and SYM1_SLOTS[12] (a symtab-1 name at local[12]).

REFUTED in the same pass, with a control: built-in 25 does NOT carry the coroutine
entry that a trigger fires, which would have seeded the 389 routines reachable from no
static entry.  Taking each staged operand as a code offset from the phase base, 0 of 8
land on an instruction boundary, against 38.7% for an arbitrary 4-aligned offset --
below chance, not above it.  And the scale is wrong regardless: built-in 25 has only
2 call sites in Stage 02.

So what starts the unreachable routines is still open, and it is not this.  A trigger
node's own layout and its consumer are unread, and built-in 25 stays unnamed.
2026-08-27 06:42:47 +00:00
Sylpheed RE agent
a2c9486b20 re: the sufficient side -- each phase exit now names the condition that FIRES it
Dominance said a phase cannot end unless X.  A port also needs "once X holds, it
must end", and that is a must-reach set: nodes from which END_PHASE is unavoidable,
as a least fixpoint where n qualifies when it has successors and ALL of them qualify.

The conservatism is deliberate and is the honest answer: a loop never enters the set,
because a poll loop reaches its exit only if the polled predicate eventually becomes
true, which is a liveness property rather than a graph one.

A dominating condition is a TRIGGER when the successor it takes on being satisfied
lies in that set.  Over all 28 stages: 732 dominating conditions, 234 triggers
(31.97%).  isl_report.py phase-guards now tags every line precond / TRIGGER.

The split lands where it should.  Stage 02's phase-1 objective exit is six
preconditions -- player alive, TCN004 destroyed, t <= 210, ADT102/ADT107/ADT113
destroyed -- and exactly ONE trigger: hp_pct_test(ADN101, 0) != 1.  Destroying ADN101
is what fires the phase.  That is a sentence a port can implement.

Per-exit distribution over 172 reachable exits: 89 have exactly one trigger, 42 have
none, 41 have several.  The 42 with none are not a failure -- they are the exits no
branch fires; Stage 02's 0x006260 ends on read_freg(0) < 1200, a timeout, and time
passing is not a property of the graph, so declining to call it a trigger is correct.

Recorded as a heuristic rather than a rule: "the first trigger is the point of no
return" holds for 33 of the 41 multi-trigger exits, with 8 counterexamples where a
precondition appears after a trigger.  The likely cause is that the listing is
ordered by file offset, which is not execution order -- coroutines and jumps let a
lower offset run later.  Not asserted.

calls, phase-ends and conditions all regenerate byte-identical; the two phase-guards
artefacts change only by gaining the tags.
2026-08-27 06:34:39 +00:00
Sylpheed RE agent
7bd6342061 re: read the three built-ins that appear inside clear conditions -- none named
isl-phase-guards produced the per-phase clear conditions, but three of the built-ins
in them were unread, leaving the conditions half-readable.  All three are read now
via the vtable at 0x820A84BC (control: 69 unit_state -> slot 184 -> 0x8226ADF0).
NONE is named: the corpus has withdrawn two names taken from usage shape, and what I
read does not support naming these.

builtin104 (slot 288, 0x8226BFE0) is a THREE-INSTRUCTION PURE GETTER:
    lwz r11, 10160(r3) ; stw r11, 164(r3) ; blr
So all six tutorial stages S18-S23 end on the value of a single engine-written word,
which is why their exits each have exactly one dominating condition and why
isl-builtins.md only ever saw 104 inside a poll loop.  That word has EXACTLY ONE
writer in the image -- sub_821AA1B0 at 0x821AAD9C, gated on a kind field being in
(16, 32] -- and r29 there is the return of a preceding call, so the meaning of the
value and of the gate are both unestablished.

builtin7 (slot 40) indexes [phase+324] by local[4], bails when rec+16 (the documented
unit state) is 1, 3 or 4, and resolves local[12] through [phase+244] -- symbol table 1.
isl.py's SYM1_SLOTS already listed slot 12 for built-in 7, derived from operand ranges
alone; reading the implementation shows the mechanism behind that.  Two independent
methods agreeing.  What it asks ABOUT the route is still unread.

builtin141 (slot 428) has the same unit-array entry and returns 0 when the unit is
gone; everything past 0x8226CA0C is unread.  Stage 16 calls it twice with arguments
differing in one position (0 vs -4000), which looks like a coordinate -- and looking
like one is exactly the evidence this corpus does not accept.

No artefact changes: the listings already printed builtin104/7/141 and still do.
All four Stage 02 artefacts regenerate byte-identical.
2026-08-27 06:27:00 +00:00