Commit Graph

249 Commits

Author SHA1 Message Date
Sylpheed RE agent
e72f0f14be re: the effect->package map, 103 of 137 - and eff_f0002 was a substring artefact
Enumerating eff_* names per .xpr across all 166 packages and matching the bound
names EXACTLY gives a real home for 103 of the 137, up from the 71 ptc_pack
alone accounted for. Only 36 packages carry an effect name at all, and two
dominate:

  Base.xpr                  53
  ptc_pack.xpr              46
  Stage_S28.xpr              2
  five rou_f001_wep_NN.xpr   1 each

So there are TWO shared effect libraries, not one - and ptc_pack.xpr is the only
*_pack bundle on the disc, so no third shared library is hiding. By digit-width:
3-digit 80 resolved of 110, 4-digit 23 of 27. The previous iteration's split
survives and sharpens - the four-digit series really does live outside ptc_pack
(that zero stands), and now we can say where: Base.xpr.

Correction to the previous commit. It reported eff_f0002 and eff_f0002_barn as
present in Base.xpr. Both were SUBSTRING artefacts: what the file actually holds
is eff_f0002_barnhaze, one longer resource name that grep -l eff_f0002 and
grep -l eff_f0002_barn each match inside. Neither bound name is there. This is
the corpus's own paid-for prefix lesson arriving from the other direction - last
time it was rot_n001 vs rot_n001_break with the stored name longer; here the
BOUND name was the prefix. The new map is exact-keyed and does not have this
failure mode, so the earlier positive is withdrawn.

34 names remain unlocated, dominated by a family the last pass did not single
out: eff_l### with 17 of the 34, then h 4, s 4, j 2, m 2, t 1, and four
four-digit names - eff_e0044, eff_f0002, eff_f0002_barn, eff_n0071.

Scope note worth keeping: the j 22 / t 14 clustering reported last time was the
residual against ptc_pack ALONE; against all packages those families are largely
accounted for and l is what is left. Both numbers are right for their own
population, which is exactly why a residual has to say what it was measured
against.

New artefact with its regenerator: tools/re-capture/effect_homes.py ->
docs/re/data/effect-homes.txt, which lists all 34 by name. All sixteen existing
artefacts byte-identical.
2026-08-28 09:12:15 +00:00
Sylpheed RE agent
fa8a538c33 re: the disc ships the unit datasheet's own SCHEMA (and 文字列 was not a placeholder)
This corrects the previous commit. 文字列 is not a developer's leftover: it is
one member of a six-word Shift-JIS TYPE vocabulary, and the records carrying it
are a machine-readable schema for the unit datasheet.

The whole non-ASCII population on the disc is 6 distinct values out of 99328 -
0 of 3496 record names and 0 of 12173 field names - and all six are type words:

  文字列              string                366 uses
  NS_"文字列"         NS_ string             12
  整数 / 整数値        integer            66 / 6
  浮動小数値           floating-point value  504
  浮動小数値[0〜1]     float in [0,1]         36

990 type-valued fields. So the reader defect noted last time is real but bounded
to these six strings, and name_hash re-encodes Latin-1 byte-for-byte, so hashing
was never affected.

They sit in 15 records x 6 GP_MAIN_GAME_* paks = 90 instances, i.e. 15 records
with ONE user. The names are exactly the unit substructure family, and six carry
a literal wildcard: Turret_???, Hatch_???, Bridge_???, Thruster_???,
ShieldGenerator_???, Versatile_???, and NS_*. ??? is the numeric-suffix wildcard
at record AND field level - Turret_??? is the schema for Turret_000..00N, and
inside it CannonFrame_??? / MuzzleFrame_??? stand for the numbered slots.

Where a field's type is an enumeration the schema holds an EXAMPLE value instead
of a type name: Yes for the five booleans, Vessel for Generic.Type (the 43
Craft + 71 Vessel split), Ship_ for the ID prefix convention. Maneuver is the
one fully-typed record, 34 of 34. Every ResistanceTo* and every Color_* channel
is declared FLOAT[0..1] - normalised by declaration, matching the sampled values
in unit-datasheet-static. Generic.NozzleSpec_??? has its own type NS_"文字列"
and NS_* is a record, so the nozzle spec is a nested sub-schema.

Control separates schema from data cleanly: the _??? records and NS_* exist ONLY
as schema, 6 of 6 instances typed, while the eight real substructure names are
typed in 6 instances and untyped in the rest - Generic 6 of 3651, the others
6 of 684 each.

Turret_??? carries the game's own typo NomalModel beside DamagedModel.

This gives the port an authoritative field-type table: types the disc declares,
rather than types inferred from sampled values.

New artefact with its regenerator: tools/re-capture/datasheet_schema.py ->
docs/re/data/datasheet-schema.txt. All fifteen existing artefacts byte-identical.
2026-08-28 08:22:36 +00:00
Sylpheed RE agent
105ccab038 re: the base-solver's confidence axis was inverted
The remaining named false-positive mode - "107 rows solve to a 64K-boundary
base, a bare addis with no addi of its own, so any scatter of displacements
votes for it" - is refuted by its own measurement.

New positive test in the tool: simulate lis/addis rD,r0,HI + addi rD,rA,N +
or rD,rA,rA forward through each row's function and ask whether the solved
base lands in the solved register.

  64K-boundary bases ("low confidence") : 107 / 107 confirmed
  non-zero low half ("trustworthy")     :   8 / 154 confirmed

A round base is the case where the compiler needed no second instruction, so
`addis r11, r0, 0x820B` stands in the code in full. A miss on the other class
is silence (base built in the caller or loaded from memory), not refutation.

Control: every row the corpus independently validated against the disc has a
64K-boundary base - debriefing, career, save, leaderboard, the 205-name PG*
HUD roster, material slots, the S16 boss collision/frames/motions and its
loader. 13 rows over 10 functions. The dense-short-string false positives the
corpus did name (r31 = 0x8202xxxx) all sit in the "trustworthy" class.

The 0x820B0000 cluster is DUPLICATION, not error: 60 of its 82 rows are one
function emitted 60 times, exactly 491 instructions each, two instructions
differing (both global data pointers), identical 41-address string sequences.
40 resource names written into a per-copy global via sub_8217FA08 at 24-byte
strides. 38 of the 40 are disc GameResourceID values (480 distinct); rot_n001
and rou_e202 are not, and no disc GameResourceID uses the rot_ prefix.

Artefact diff 13/4, confined to the replaced section; the 261-row table and
the 64K histogram untouched; byte-identical on a second run. Fourteen other
artefacts byte-identical.
2026-08-28 00:42:46 +00:00
Sylpheed RE agent
bb6fb9024b re: r0 is not a base register - the last shortlisted row withdrawn
sub_825F2CF0 / sub_825F2F88 (r0, 30 names each, 97% disc names) are not
name-block readers at all: they are the compiler's VMX save/restore helper
pair. 145 instructions each, 72 differ, every difference stvx<->lvx over
v14..v127.

Root cause: in `addi rD, rA, N` the rA slot reads as literal zero when it
names r0, so `addi r11, r0, -N` is `li r11, -N` - vector spill offsets, not
displacements. The solver excluded r1 only; the bogus base landed inside the
stage-settings name block, dense enough to "resolve" 30 real field names.

name_block_bases.py now skips r0 as well. Rows 277 -> 261, functions 190 ->
176, non-64K bases 170 -> 154, data-table rows 53 -> 50; 154+107=261 and
277-16=261. Artefact diff -100/+3, every removed row line carries r0, control
sub_82341A20 r30 = 0x82088F94 217/226 untouched.

Nothing lost: all 30 names are a strict subset of sub_8230D1F8 r29 (129
names), the stage-settings loader already in the corpus. String-xref join
agrees - FinalPassBG, FogMinDistance, ScreenColorR, ExposureKey_BeginValue
each exist once with exactly one xref, from sub_8230D1F8.

Fourteen other artefacts byte-identical; name-block-bases.txt byte-identical
on a second run.
2026-08-28 00:21:26 +00:00
Claude (auto)
ced73c5488 re: all 277 base-solver rows classified objectively; 53 are data-table schemas
name_block_bases.py extended with a per-row data-table test; artefact +57/-0,
byte-identical across two runs (now ~2 min 12 s -- it adds a disc-wide pak scan).

The test is objective, not by eye: a row is a data-table schema if its names are
IDXD record/field names on the disc (13450 such names disc-wide).  53 of 277 rows
are >=50 % disc names with >=8 names; the other 224 are engine/XDK vocabulary,
compiled key lists, or noise.

The two axes are independent: against base confidence, solved bases split 34
table / 136 not, round bases 16 / 91.  "Round base" and "not a table" are
different questions.

The 53 contain every loader already known -- that is the control.  Five rows in
the 53 are unowned, each noun grepped and appearing in no docs/re/ file:
sub_823BDAA8 r11 (33) = the S16 boss's muzzle/attach frames (GN_MainGun_*_Muz*);
sub_823BDAA8 r10 (25) = motion names (Motion_stand, Motion_attackA_start), the
EnumMotions family DefTables declares; sub_82315AE8 r11 (20) = the Guardian
record's own fields, i.e. the S16 boss loader; sub_8219E560 r11 (18) = the
leaderboard screen keys; sub_825F2CF0 + sub_825F2F88 r0 (30 each, same base) =
post-processing (FinalPassBG, FogMin/MaxDistance).

Four rows that look new are not, and their disc-overlap says so -- 53-70 % rather
than ~100 %, because they mix arsenal fields the corpus owns (ConditionToDevelop,
WeaponDesc, SilhouetteModel) with literal screen coordinates as strings.

Not settled: none of the five was opened -- this iteration produced the shortlist,
not the findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 23:09:57 +00:00
Claude (auto)
9e2524d028 re: every AIParams object mapped to its stage, zero residual
StageResource.EnumerateAIParams names the table, and every name resolves.

29 StageResource records; 28 carry EnumerateAIParams (the one without it is the
_Test template); 23 distinct table names declared; 23/23 hash to an AIParams
object key under the prefix "stage\"; 0 objects left unnamed; 0 declared names
with no object.

AIParams_S01..S16 (16) + AIParams_S24..S29 (6) + AIParams_Tutorial.tbl shared by
six (UnitGroup_S18..S23, the tutorials) = 23 tables over 16 + 6 + 6 = 28 records.
The arithmetic closes both ways and matches the 23-object count found last
iteration.

Same sharing shape as the settings family (stage-settings-table.md: 24 objects,
StageParameter_Tutorial shared by six tutorials).  Two independent families agree
on how the tutorials are handled -- n=2, a pattern rather than a rule.

aiparams_census.py extended with the stage mapping; artefact +7/-0, byte-identical
across two runs; the other fourteen verify unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 22:12:58 +00:00
Claude (auto)
2456d28f1a re: AIParams disc-wide, and a correction to my own last entry
CORRECTION FIRST.  Last iteration I recorded sub_8233C368 as unblocking a
NEEDS-HUMAN item -- "the corpus carries the AI tail of Maneuver as
NEEDS-HUMAN/runtime; it is statically reachable after all."  That was wrong.
stage-mission-tables.md already documents AIParams_S02.tbl as exact original
values obtained by static RE, directly portable, listing all 20 field names and
both shapes.  I grepped FiringLength and saw the file but did not read the
section.  Finding the owning doc is not reading it.  The only genuinely new part
was the loader's name.

What is new: the census generalises Stage 02 to the disc.  23 AIParams objects,
identical in all six GP_MAIN_GAME_* paks, sharing ONE declared-name set of 34
profiles; 782 profile records = 23 x 34; 0 declared names without a record in
their own object.  So "34 AI profiles" is not a Stage-02 fact -- every stage
carries the same 34 and only the values move.  The roster is declared by an
Enumerate_AIs record whose field names are the profile names, the same
declaration-table mechanism that closed DefTables.

Type predicts the field count with exactly two exceptions: Fleet -> 6 fields is
253/253 zero partials; Squad -> 20 fields is 483/529.  The 46-record residual in
full: AI_Test and AI_CraftSquadron_Test, both Type = Squad with only the six base
fields, in all 23 objects.  No profile's shape varies between objects.

New regenerator aiparams_census.py, 45-line artefact, byte-identical across two
runs; the other fourteen verify unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 21:57:24 +00:00
Claude (auto)
17b4a791eb re: mining the base-solver index -- a false-positive mode named, and the AI-table reader found
277 rows over 190 distinct functions (a function can read several blocks).
name_block_bases.py extended with a confidence split.

The tool's false-positive mode, measured and named: 107 of 277 rows solve to a
base on a 64K boundary -- a bare "addis rX, r0, 0xHHHH" with no addi, so any
scatter of displacements votes for it.  82 are 0x820B0000: about 60
near-identical functions in 0x8281xxxx-0x8284xxxx all "naming" the same rou_e0NN
list.  The 170 rows with a non-zero low half are the trustworthy set.  A round
base is not automatically wrong -- sub_822215D0 sits on 0x820A0000 and resolves
205/206 -- so read the ratio, not the base.

The index re-derives every loader we already knew (unit 217, stage settings 129,
PlayerParams 90, hangar 81, squadron orders, missile guidance, shell movement,
substructures, six camera/fog readers) -- that is the control.

The find: sub_8233C368 reads the AI behaviour table.  r28, base 0x8208583C, 20
names -- Enumerate_AIs, FiringLength, GuardLength, AutoGuardLength, CounterLength,
MusterLength.  stage-mission-tables.md owns those field names on the data side,
but Enumerate_AIs appears in no document and no reader was known; the corpus
carries the AI tail of Maneuver as NEEDS-HUMAN/runtime.  It is statically
reachable after all.  The same base also serves sub_82338EE0 (97 names, Weapon
TargetType SpecialWeaponType ReticleType IsCharging ...) -- the weapon datasheet
loader, also not previously named.

Five unowned blocks surfaced and NOT opened: PGHUD_*/PGREMAIN_NUM HUD part names
(205/206), STAGE_RESULT/stage_num_shoot_down_aircrafts/EX_OVERVIEW,
g_mWorldViewProjection/NormalMap/GlossinessMap engine material slots,
Boss16Collision* (cross-links the S16 Guardian object), and roh_n001_menu1_cam_pos
menu camera tags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 21:41:03 +00:00
Claude (auto)
ae37e15d7d re: the base-solver -- 277 name-block loaders indexed, and the analog block is SOLVED
New reusable tool, tools/re-capture/name_block_bases.py -> docs/re/data/name-block-bases.txt
(2880 lines, ~65 s, byte-identical across two runs).

A loader that reads a table by field name keeps one base pointer and emits
"addi rX, rBASE, -N" per name, so no static xref sees the strings.  Solve the base
from the DISPLACEMENT SET alone: every (string address, displacement) pair implies
a candidate base, and the true base collects a vote from every name it explains,
so it wins outright.  My first cut took candidates from ONE displacement and
scored the unit loader at 52/226 against the right answer's 217/226 -- vote over
the whole set, not a probe.

Control passes with no prior knowledge: the tool recovers sub_82341A20 -> r30 =
0x82088F94 at 217/226, and independently recovers sub_8230D1F8 (129/132),
sub_822F9498 (90/91) and sub_822AE628 (81/108).  277 name-block-reading functions
image-wide, with the schema each names.

The analog block is SOLVED: sub_821A6CF0, r29 = 0x820A1630, 22/24.  In code order
it names ControlTweakName, YawMagForNormal, the 12 Tweak fields, the 8
AnalogRevice_* curves and GP_MAIN_GAME -- the whole schema in the object's own
order plus its pak.  r29 is built at 0x821A6D34 as addi r29, r11, 5680 =
0x820A0000 + 5680, matching the solved base exactly.  It is the same function that
reads PlayerParams.

Two of my own verdicts withdrawn: "referenced by nothing" and the softened "not
found by these routes".  The measurements behind them were right; the conclusions
were wrong.  The base was solvable from the data the whole time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 21:22:50 +00:00
Claude (auto)
c171a194d9 re: the GP_MAIN_GAME unnamed block -- 333 of 337 already owned, 4 genuinely new
All six language copies carry the identical 337 unnamed IDXD hashes.  Partitioned
by record-name shape (53 shapes), 333 map onto families the corpus already
documents: 131 weapon datasheets, 114 unit datasheets, 64 unit Faces tables, 10
unit message sets, 8 chatter rule tables, 5 enumerations/formations/placement.

114 is exactly the corpus's unit count (43 Craft + 71 Vessel) -- these are the
tables the corpus has always worked with, reached by SHAPE because they have no
names.  Naming them adds nothing.

The Enumerate object in each GP_MAIN_GAME_* is EMPTY, zero fields, which is why
route 2 named 1283 entries in DefTables and 0 here.

The 4 unclassified objects are new -> docs/re/structures/player-tuning-tables.md:
the analog stick response curves (8 axes, 11 samples + a named Count = 11, tested
8/8; yaw/roll/throttle are the identity ramp, the shaping is all on pitch and the
camera axes; adv_yaw is non-monotone and unexplained), the player craft's flight
envelope (Booster, 50 fields -- the player side of the AA_/AV_ pair documented
for NPCs -- plus TacticalManeuver, SpecialAttack + three gauge bands,
SpecialWeapon, Misc), the Stage 16 boss (identified by Shell_S16Boss_* ids;
Guardian HP 65000, Core 42000), and one unidentified Generic naming eff_n0071.

New regenerator main_game_unnamed.py, 112-line artefact, byte-identical across
two runs; the other twelve artefacts verify unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 20:17:10 +00:00
Claude (auto)
53084a11ca re: the Enumerate declaration tables close DefTables (130 -> 1413 of 1425 IDXD)
The string harvest could never name DefTables because those names are not
spelled out as "something.tbl" anywhere on the disc -- they are declared.

An IDXD object whose single record is named Enumerate is a DECLARATION TABLE:
its field NAMES are the names of other objects, each resolving as
name_hash("<field name>.tbl").  EnumLODSet_test.tbl declares 676,
EnumGameModel_test.tbl 360; the disc holds 144 such objects (138 in DefTables,
one in each GP_MAIN_GAME_*) declaring 1298 distinct names.

Route 2 names +1283 entries route 1 could not.  130 + 1283 + 12 = 1425, no
overlap, 99.2 % coverage.  Zero partials: of 5 suffixes x 6 prefixes, ('', .tbl)
scored 1036/1036 and every other combination scored 0.  Residual in full: 8
declaration tables nothing declares, 2 LOD sets (Model rou_e004 / rou_e013), 2
motion sets; 15 declared names have no pak entry at all.

REFUTED alongside it: the 40 XPR2 manifests are not the naming source -- their
82 Name= values and 82 DataFile/Source paths resolve 0 entries under any of the
5 suffixes.  They share the MODEL namespace only: 40 of the 82 appear as the
Model field value inside the tables.

Artefact +17/-18, every line paired, byte-identical across two runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 19:59:20 +00:00
Claude (auto)
bcc23cba4a re: why each archive is low-coverage -- two different reasons, cleanly separated
Splitting every unnamed entry by magic turns the coverage percentages into an
explanation.

The three low-coverage UI archives have ZERO unnamed IDXD.  GP_HANGAR_ARSENAL
is 180 IDXD, 180 named, 0 unnamed -- its 1191 unnamed entries are 1149 T8aD/RATC
plus 42 LSTA, i.e. sprites.  GP_MISSION_SELECT and GP_DEBRIEFING_PILOTLOG hold
no IDXD objects at all.  So "22.6 % named" is misleading: every data table in
that pak is named, and these three are the same artwork-naming phenomenon as the
blocked 2D and READY_ROOM archives.

DefTables is the only genuine data gap: 1425 IDXD, 130 named, 1295 unnamed, in
17 record-name shapes -- Generic + Level_0..Level_3 (807, LOD sets) and Default +
EnumMotions + Generic + ReferenceFrames +/- Motion_break/dead/down (463, motion
sets).  One LOD and one motion table per model.  Level_0, EnumMotions and
ReferenceFrames appear in no document.

Control: the 100 % archives have no unnamed entry of any kind, and GP_MAIN_GAME_E
is 667/337 IDXD with zero unnamed artwork -- two failure modes, not a gradient.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 19:41:29 +00:00
Claude (auto)
a543b8da96 re: correction -- the naming sweep already covered the 24; it just never said so
archive_naming.py already harvests 6027 candidate names under 16 prefixes, and
testing its candidate set directly shows it names all 24 StageParameter_S<NN>
objects, 24/24.  The previous entry presented that naming as new -- it is not.
What was new was the identification (which object is which stage, the shared
_Tutorial table, IsBoss16Enable = S16), not the method.

The real gap, now closed: the sweep reported only per-archive percentages and
never emitted WHICH entry got which name, which is exactly why nobody could say
the settings objects were StageParameter_*.  It now prints the resolved name
families per archive -- 6573 named entries, 1631 families disc-wide.

Determinism caught again by the verify loop: the resolved map was built by
iterating a set, so collided hashes picked a different winner each run.  Now
iterated sorted().  Second time in two iterations -- any map built from a set
needs a sort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 19:25:30 +00:00
Claude (auto)
589d09f2b5 re: all 24 settings objects named -- stage\StageParameter_S<NN>.tbl
The queued item (the 29-object resource manifest) turned out to be already
settled by challenge-mission-gate.md and stage-mission-tables.md, so the
iteration moved to the open question those docs make answerable.

Hashing EVERY string that appears in any IDXD object on the disc resolves all
24: StageParameter_S01..S16 (story), S24..S29 (challenge), StageParameter_
Tutorial shared by all six tutorials, and StageParameter_Test.  16+6+1+1 = 24,
which is exactly why the settings family is 24 against 29 stage records.

IsBoss16Enable confirmed independently: that object is StageParameter_S16.
GravityFactor is non-zero in S10 (700), S11 (400), S03 (250), S27 (250); the
three unscored objects are S24, S27, S28; the one without SplinterCell is _Test.

Refuted on the way: TOC order is not stage order (the TOC is hash-sorted and the
Boss16 object is 17th, not 16th), and neither the StageResource template nor a
real 19-field record names the settings table.  The AUTO_SETTINGS filenames are
a different scheme -- none of the 28 hashes to any of these 24.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 19:05:28 +00:00
Claude (auto)
5f764458cd re: the Phase_1/2/3 block of the settings family -- mostly per stage, not per phase
72 records, 94 distinct field names, 31 in every one (post-processing core, five
Fog*, three ScreenColor*, SpaceSize, five Supply*, UnderCommandSquadron, the
first BGOperate slot).  Optional families sit in clean tiers: Nebura_* and
ColorLayer* in 69/72, DOF_*/UnsharpMask_*/ExposureKey_* in 15/72, FinalPass* in
3/72.

REFUTED: BGOperateFrameCount is NOT the number of BGOperateFrameName_i slots --
36 of 72.  What holds is Count <= slots, 72/72: a fixed slot array with a live
count, the same shape as MessageCount under the 32-slot clamp.

68 of the fields common to all three phases NEVER differ in any of the 24
objects.  The phase block is a per-stage environment block copied three times;
what a phase change is actually for is the backdrop animation (BGOperate*) and
the supply/command squadron assignment.

MapPath is not in this family -- its 87 records per pack are 29 x 3, the
resource manifest's Phase_N.

Also fixed a non-determinism the verify loop caught: most_common() over a set
iteration ordered ties differently per run; now sorted by (-count, name).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 18:50:27 +00:00
Claude (auto)
26b7728ceb re: the rest of the stage-settings object -- cameras, player limits, difficulty
53 objects in GP_MAIN_GAME_E carry a Phase_1 record, in TWO families: 29 are
the resource manifest the corpus already owns (Phase_N = 4 fields) and 24 are
the settings table (Phase_N = 31-90 fields).  That answers the 24-vs-28 puzzle
left open by the scoring entry -- the 29 is a different table, not the settings.

Camera: three chase rigs in metres, 13 of 14 fields identical in every stage --
Nose (0, 4.5, 7), Near (0, 10, 40), Far (0, 15, 80), FOV 0.92; only CameraFar
varies, once.  Player: BulletLimit 512 / HomingLimit 256 / LaserLimit 32 and
the three 0.30 axis adjustments are constant, GravityFactor is non-zero in 4 of
24 stages, and IsBoss16Enable appears in exactly ONE object -- the first
per-stage handle for a family whose filenames do not resolve.

Difficulty_Easy/Normal/Hard is a SECOND difficulty record (8 damage and
guidance multipliers), separate from Score_*.

New doc structures/stage-settings-table.md; mission_scoring.py extended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 18:35:05 +00:00
Claude (auto)
192d3bf67f re: mission scoring and the S/A/B/C/D rank thresholds
sub_8230D1F8 -- the loader that contaminated the AA_/AV_ offset search -- is the
stage-settings loader.  Reading its 122 field NAMES instead of its offsets found
the scoring block, which nothing in docs/re owned.

24 IDXD objects per language pack x 6 = 144, each holding Score_Easy /
Score_Normal / Score_Hard: 72 records per pack on one 22-field schema, no
variants.  Difficulty moves 10 of the 22 fields and never the five RankScore_*
thresholds -- the rank bar is per stage, difficulty scales the earning rate
(x0.5 / x1.0 / x2.0) and the penalties.  23 of 24 objects differ from the
commonest Normal record; 9 of 72 records zero the scoring entirely.

Not settled: which object is which stage.  None of AUTO_SETTINGS's 28 filenames
resolves to any of the 24 under 19 prefixes, and 24 vs 28 is unexplained.

New doc structures/mission-scoring.md, regenerator mission_scoring.py, artefact
data/mission-scoring.txt.  Twelve artefacts now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 18:22:36 +00:00
Claude (auto)
36f97c6322 re: the player-facing chatter partition; correcting the wingman claim
The 13 tables that voice every player-facing event are all
PresetMessage_Katana_*, the PLAYER's own sets -- unit-group-table.md already
names the link (DeltaSaber_T_Player carries msg=MessageSet_Katana).  Exact
partition: 13 voice all 14 events, 131 voice none, 0 voice some, and no
non-Katana table voices any of them.  CharacterKATANA is the only speaker
exclusive to the set.

Corrects preset-message-rules.md, which called those the wingman tables.  The
wingman roster is owned by isl-condition-builtins.md (UNITS: Bird1-Sandra ...
Rhino2-Katana, Rhino3-Ellen) and needed no experiment.

Residual: Katana_09_S10-1 and Katana_14_S16-2 voice none of the 14.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 17:42:28 +00:00
Claude (auto)
6ee041c1bf re: the chatter phase merge is not additive; 224 authored variants never play
sub_82215A58 reads CrewCount + PresetMessage_Phase1/2/3 and reaches the loader
from ONE call site, so the three phase tables fold into one map keyed by the
event record name.  Every merge collides; sub_82213840 reconciles on the
message list plus +16/+17/+18/+20/+24/+28 (NOT the +32 Yes mask), and the
incumbent always wins.  Measured: 26432 collisions, 26208 identical, 224
different (189 differ only in the message list), 0 mask-only differences.

Refuted handle: intersecting functions by the object's offsets finds dozens of
unrelated layouts -- offset shape is not an identifier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 16:50:42 +00:00
Claude (auto)
c4fd90baa9 re: the seven chatter fields, read off sub_82213980
The one function referencing all seven field-name strings is the rule table's
loader.  Interval / IntervalFluctuation / EffectiveTime are SECONDS, emitted
as *60 frame counts; Probability is a percentage and zero skips the record;
Pattern is a 4-arm enum of which only Sound and Window ship; the Yes/No pair
element is a u32 bitmask, which is why MessageCount is clamped to 32 (max on
disc is 26).  40-byte object layout recorded.  13 dead records characterised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 16:35:56 +00:00
Claude (auto)
6a81268cf1 re: all 144 chatter rule tables named; the undeclared 8 are NOT tutorial
Three more naming routes: strip _msg from a message table (137/144, superset
of route 1, zero non-rule hits), predict the name from the Sperkers roster
(6/6, control 0/4), and sweep the naming grammar (1/144).  Union 144/144,
and every rule table has its _msg companion.

Refutes the reading left by 2c2af7a: the undeclared tables are two story-stage
tables and six TCAF fleet/ship tables, no tutorial content at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 16:22:19 +00:00
Claude (auto)
2c2af7ad78 re: the reactive-chatter rule table behind ORDOR_SQUADRON_EXTENDED
864 records = 144 rule tables per language pack x 6.  One schema for all
9216 event records; MessageCount*2 == positional count with zero mismatches.
Named 136/144 by two independent routes that agree as sets.  2388/2405
message ids join the settled sound-cue table.

Corrects squadron-orders.md: the executable misspells all four SQUADRON
entries of the 0x820AEEB0 enum as ORDOR_, and the disc data matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 16:07:17 +00:00
Sylpheed RE agent
d1368c12db re: the objective/tutorial marker records; S24-S29 are CHALLENGE, not story
Correction first.  stage-numbering-and-player-craft.md called S24-S29
'story' and reported 22 story stages.  challenge-mission-gate.md OWNS
that split and already had it: S01-S16 story, S18-S23 tutorials,
S24-S29 challenge.  A third confirmation fell out of this iteration:
AUTO_SETTINGS names stage01..16_settings.tbl, tutorial01..06 and
challenge01..06, and its field tags are 1-16, 18-23, 24-29 -- the
shipped stage numbers exactly.  The campaign is 16 story missions, not
22.  AUTO_SETTINGS and IGNORE are likewise already owned by
isl-condition-builtins.md, so nothing there was new either.

What is new: the 11 marker records hud-config.md listed as unread.  12
named fields each, portable HUD data.  Colour is ARGB, three per marker
-- objectives red 0xFFFF0000, guard/waypoint green 0xFF20FF20,
Acropolis/tanker cyan 0xFF00FFFF, sub-objective yellow 0xFFFFFF00, and
every TutorialMarker_* amber 0xFFFFA010.  Each carries a Normal sprite
and an Emphasis part with optional Sub companions; HPGauge 0/1/2;
RadarCursorType Circle/Rectangle/blank; BlinkCycle 0x800 on all eleven.
TutorialTarget is Yes on exactly the five TutorialMarker_* and No on the
six ObjectiveMarker_*.

Parameters binds the roles: TargetMarker -> ObjectiveMarker_Target,
HelpMarker -> ObjectiveMarker_SubObjective, TutoTargetMarker ->
TutorialMarker_Target, plus ReloadDispTime 0.1.
Enumerate_ObjectiveMarkers is the 11-name roster.

Artefact +13 lines / 0 deletions; the other nine regenerate
byte-identical.
2026-08-27 15:46:31 +00:00
Sylpheed RE agent
15c9e13c12 re: CoverArea is a 6-bit MOUNT mask; the three other turret leftovers closed
Six bits, never more.  Over all 835 turrets: 41 distinct values, max
0x3f, bits 6/7 set on none; per-bit 709/117/274/274/260/237.  0x00 (34)
= the empty Weapon_NULL hardpoints with YawLimit 0; 0x01 alone (439) =
craft hardpoints with YawLimit 2.5/1.0/0; 2-6 bits (362) = warship
mounts with YawLimit 45-180.

It tracks the MOUNT, not the weapon.  UN_e107_ADAN_AAFrigate has eight
identical AAFrigate_AAGun turrets: GN_GunXS_01..04 are 0x1d and 05..08
are 0x2d -- same gun, same YawLimit 120, different mask.  The Battleship
spreads five masks over GN_TGunL_01..05 / GN_TGunM_02..03, all firing
the same CAF_Ship_ASGun.

Which sector each bit denotes is NOT determined; six bits and the name
invite +-X/+-Y/+-Z or six hull faces, but nothing static fixes the
convention.  Not adopted.  Refuted on the way: bits 2 and 3 are not a
mutually-exclusive pair -- 188 turrets set both.

The 26 weapons no turret mounts are a coherent set: 11 _P player
variants, the nose/twin mounts, two _Child sub-munitions, the three
S16Boss_*, three Weapon_Test_*, and ADAN_Attacker_S_GunTurret.

Versatile_NNN ships nowhere -- 0 populated records across all 41
archives, only the ??? template row.

The two units one turret short: both extras are missile mounts with no
Frame.  Elan_EX4 = NoseGun + Missile, both mask 0x01, both frameless;
AAFrigate_EX4 = eight framed guns plus one Ship_AAMissile at 0x0c with
no Frame.  n=2, stated as the observed pattern, not a rule.

New artefact and regenerator; the other nine regenerate byte-identical.
2026-08-27 15:35:09 +00:00
Sylpheed RE agent
27602a3032 re: BLOCKED -- the 2D / READY_ROOM TOC names, three routes closed with controls
1. A different hash family.  The corpus knows three (idxd-tag-hash.md):
name_hash, tag_hash, ixud_hash.  Scoring all 5977 harvested names x 6
prefixes: GP_TITLE 8/16 and GP_PAUSE_MENU 6/11 under name_hash (the
positive controls), and tag_hash and ixud_hash explain NOTHING anywhere
-- including the paks name_hash does explain.  So they are not the TOC
function, and the unnameable pair is not keyed by a different one.
GP_MAIN_GAME_E2D stays at 0/711 under all three.

2. The executable.  sylpheed.db's strings table holds 7140 rows, of
which exactly two look like asset paths -- Data\gmicon002_2.t32 and
Data\gmicon006_2.t32, in a Data\ directory nothing else on the disc
uses -- and neither resolves in any archive.  The binary is not the name
source; it holds two strays and no table.

3. Name transformations -- 13 of them on the 419 config paths, all 0.

The container runs out here.  Those TOC keys hash names that exist on
neither the disc nor the executable in readable form.  The only lever
left is a dictionary attack using name_hash's shape (top byte = the
character-sum checksum), and that needs a plausible name corpus this
disc does not contain.  Noted as blocked rather than improvised around.

The port does not need these names: sprites and bundles are readable by
content (T8aD, RATC), and the config records already say which asset
each HUD element uses.  Only the archive-key to name mapping is missing.

Artefact +22 lines / 0 deletions; the other eight regenerate
byte-identical.
2026-08-27 15:22:13 +00:00
Sylpheed RE agent
15b97af170 re: which archives the disc can name -- 100% for menu paks, 0.0% for the 2D paks and READY_ROOM
Chasing more prefixes for the 419 HUD config paths would have been the
same mistake twice, so this censuses the whole disc: harvest every
plausible asset-name string from every archive (6027), hash each under
the 16 known path prefixes, and ask per archive what fraction of its TOC
that explains.  idxd-container.md and idxd-tag-hash.md own the hash;
neither says which archives are reachable by it.

The result is bimodal.  GP_TITLE 16/16, GP_PAUSE_MENU 11/11,
GP_STAGE_CLEAR 44/44, GP_CHALLENGE 151/151, GP_MOVIE_THEATER 56/56,
MiscBin 40/40, GP_GAMEOVER, GP_BUNK, GP_SYSTEM, GP_TUTORIAL and fonts
are at 100%; tables.pak 78/79, GP_DIALOG 139/140, the six language paks
115/117; GP_MAIN_GAME_* 751/1119.  Then the cliff: the six
GP_MAIN_GAME_*2D.pak at 0 of 711 each, and GP_READY_ROOM at 6 of 1106 --
the largest UI pak on the disc, not previously noted anywhere.

Eleven paks at 100% in the same run is the control that makes 0.0% a
finding rather than a failed guess.

So the 419 HUD paths are not missing assets: nothing in the 2D paks is
reachable by name from the disc's own strings at all.  Those TOC keys
hash names that are not written anywhere readable.

Also refuted first: the 419 values under 13 name transformations, every
one scoring 0 against the E2D 711 and against all 16630 entries.

Not settled: what those names are.  The lever left is the hash's shape
-- the top byte is the character-sum checksum -- but that needs a name
corpus the disc does not contain.

New structure doc, artefact and regenerator; the other eight regenerate
byte-identical.
2026-08-27 15:08:24 +00:00
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
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
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
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
b636ac9d4e re: phase-guards for all 28 stages, and a 6/6 cross-check from an unrelated method
isl_report.py now accepts a directory, so the dominance analysis runs over the whole
disc: data/isl-phase-guards-all.txt, 177 phase exits, of which only 5 (2.8%) are
reachable from no static entry.  CFG reach ranges 69.5% (S26) to 95.8% (S25), median
about 4 dominating conditions per exit.

The lopsided number in the per-stage table was the six TUTORIAL stages, S18-S23, each
with exactly ONE exit and exactly ONE dominating condition.  That could have been a
degenerate result, so I looked: it is the same condition in all six,

    END_PHASE  <-  builtin104() != 1

and isl-builtins.md reached built-in 104 from call-site USAGE alone -- "S18-S23 only,
followed by wait_s 39/39, preceded by end_coroutine 37/39, a textbook poll loop".
Usage said 104 is the tutorial's polled test; dominance says it is the tutorial's
clear condition.  Two unrelated methods, six for six.

Stage 16 -- the corpus outlier whose script may be compiled C++ -- resolves as well:
read_freg(0) < 600, player_gauge0_test, player_gauge1_test, and two builtin141 calls
differing in a single argument (0 vs -4000), which is the shape of a position or zone
test.  builtin141 is unread, so it is not named.

Stage 02's separate artefact regenerates byte-identical.

Also added: an RLIMIT_AS cap in isl_report's entry point.  The dominator pass
OOM-killed a run earlier on this 15 GB box; a bad input should now fail the process
rather than the machine.

Still not settled and stated in the doc: dominance gives necessary, not sufficient,
conditions; the 5 unreachable exits need the trigger queue at phase+272; builtin104,
builtin141 and builtin7 all appear in clear conditions and are unread.
2026-08-27 06:19:44 +00:00
Sylpheed RE agent
4f95b98813 re: the per-phase clear conditions, by dominance over the ISL CFG
Closes the backlog's "which condition guards each END_PHASE".  With the CFG from the
previous commit this is a graph query, not new machinery.

The obvious query is WRONG for this language, and I implemented it first: "one
successor reaches END_PHASE and the other does not" finds 1/62/1 guards across Stage
02's three phases, and the 1s are both the same read_freg(0) < 1200 timeout -- every
objective test missed.  The cause is the dominant idiom: a POLL LOOP's loop-back
branch also reaches the exit, one iteration later, so neither successor discriminates.
The asymmetric 1/62/1 is what exposed it; a uniform number would have read as
plausible.

Dominance has no such blind spot: a condition dominates an exit when every path from
an entry passes through it, so it is NECESSARY for the phase to end that way, and a
poll loop's test dominates its own exit by construction.  Iterative dominators
converge in 3 passes over 15670/18739 instructions (83.6%).

Result for Stage 02 -- every exit in all three phases is dominated by
unit_hp_pct(TCN001, Character_Player_Test) != 0, the player's ship being alive, which
falls out rather than being assumed.  Beyond that, phase 1's objective exit requires
hp_pct_test on ADT102, ADT107 and ADT113; phase 3's requires ADT301 and ADT302;
read_freg(0) gates at 210 / 300 and times out at 1200; random(3) and random(5)
dominate only the exits that pick one of several closing lines.

Two of the 15 exits are reachable from NO static entry, both FORCE_END_PHASE.  That
agrees with the independently measured 389 unreachable routines: they are started from
the trigger queue at phase+272, by data rather than code.

Practical note recorded: the first dominator run was OOM-killed -- 6743 nodes each
holding a Python set of up to 6743 elements.  Integer bitmasks run in seconds.

Not settled, and said so: dominance gives necessary, not sufficient, conditions; only
Stage 02's artefact is committed; one listed condition is still an unresolved
<unknown>; read_freg's units are inferred from the gate values, not read.

calls, phase-ends and conditions all regenerate byte-identical.
2026-08-27 06:12:41 +00:00
Sylpheed RE agent
5ea9e38b35 re: recover ISL conditions by CFG dataflow instead of a linear walk
The linear walk's 10% unknown was a floor imposed by the method: a block entered only
by a branch has a well-defined state, just not one a straight-line pass can see.
tools/re-capture/isl_cfg.py replaces it with a worklist fixpoint that joins each
block's state over its ACTUAL predecessors -- a value survives only if every
predecessor agrees.

Over all 28 stages:
  instructions reached by the CFG          85.0%
  condition sites, unknown LHS             756 (10.00%) -> 402 (5.32%)
  of those, never reached at all           389
  joined away (predecessors disagree)       13
  both resolve but DISAGREE                161   <- linear walk was wrong here

Those 161 are on top of the 889 the previous jmp fix caught.

Two zero-results on the way, both my own bug, both caught because the number looked
wrong rather than because a test failed:

  * The first CFG run reached only 36% of instructions and made things WORSE (35%
    unknown).  Cause: the phase bases reach almost nothing.  Most routines are
    COROUTINES the engine starts from its trigger queue, with no static predecessor,
    so every start_coroutine target has to be seeded as an entry.
  * That seeding then found ZERO entries in a file with 216 start_coroutine calls,
    because the target is staged in TWO steps -- special[0] = imm, then
    local[0] = special[0] -- and I matched only the direct-immediate form.

Reachability went 36% -> 64% -> 85% as each was fixed.

The 389 still unreached are an honest limit rather than a gap: nothing in the bytecode
starts them; they are entered from the trigger queue at phase+272, by data rather than
code, so no purely static analysis reaches them.

isl_report.py conditions now uses isl_cfg; calls and phase-ends regenerate
byte-identical.  Stage 02 unknowns drop from 71 to 25.
2026-08-27 06:02:37 +00:00
Sylpheed RE agent
58b404aef4 re: builtin80 is a command -- and finding that exposed an 11.75% bug in my tracker
Reading builtin80's body (0x82268460) to name it: it is NOT a predicate.  It
allocates a 20-byte object, stamps vtable 0x820A8CB0, magic 0xAB0311BA and the
unit's live object into it, pushes it onto a queue via the same helper push.i uses,
and returns 1 -- or 0 when the unit is absent.  A command.

That made the conditions listing impossible: it showed a six-way switch
`if builtin80(TCT206) == 0 … == 5` on a function returning 1 or 0.  Disassembling the
site shows two unconditional `jmp`s between the call and the compare, so 0x1B6C0 is
reached ONLY by a branch and its special[0] has nothing to do with builtin80.

op12 is unconditional -- the next instruction is never reached by fall-through -- and
the tracker walked through it exactly as it had walked through end_coroutine.  Last
iteration I fixed the instance and not the class, leaving 22x more bad sites in place
than the fix removed.

A/B over all 28 stages, 7563 sites, resetting at jmp as well:
  sites whose operands change              889  (11.75%)
  LHS unresolved, before -> after     34 (0.45%) -> 756 (10.00%)

So the previous commit's headline "0.0% unresolved" was a MISSING CHECK, not a strong
result: the linear walk always had some value to report, and reporting it was the bug.
10% is the honest figure and the other 90% is trustworthy for a reason.

Also corrected: isl-unit-args.md illustrated its diff with 0x1B6C0, which is one of
the bogus sites.  The UNIT_ARG result itself stands -- it came from reading
implementations, not from this listing -- but the example was picked from bad output.

Not done, and said so: recovering the 756 needs a dataflow join over each block's
actual predecessors, a CFG fixpoint rather than a linear pass.  The branch targets are
all known so the CFG is available; the analysis is not written.

calls and phase-ends regenerate byte-identical; conditions changes on 187 lines.
2026-08-27 05:53:20 +00:00