Commit Graph

814 Commits

Author SHA1 Message Date
Sylpheed RE agent
c760cdc509 re: the boot sequence is not data-driven -- closing Q6 with the negative
rather than leaving it amber

Q6's second half asked what the game READS to decide the boot order. The
answer is nothing, and the value here is the reach rather than a find.

Four places checked, the order in none of them: config.ini's [SYSTEM] is
empty and it is the disc's only config; the movie manifest carries the
boot-side assets but no transitions; the requested GamePart id was
already shown to exist only as a stack argument in flight, with no
persistent field and no literal store; and the string GP_ADVERTISE_DEMO
has zero xrefs of any kind, so nothing in the code reads the attract
entry of the id table. A transition is a call with an id argument.

Traced as far as it goes cheaply: the RegisterToFactory<0,
GamePart_Title> string is referenced from exactly one site,
sub_8280E148, which also takes the address of sub_821C7D98 -- where a
factory template puts its creator. Marked amber, because that is position
and convention rather than proof, and sub_821C7D98 has zero .rdata
references, which fits a new+ctor thunk and not a state machine. The
substantial function in that neighbourhood is sub_821C6458 and I did not
read it.

So Q6 closes as answered with the driver classified as code rather than
data, which means the port AUTHORS the sequence -- and that is fine,
because the sequence itself is measured end to end and the handoff now
carries it in one line.
2026-08-28 18:53:14 +00:00
Sylpheed RE agent
c8e8dc0427 re: what the game actually reads at boot -- config.ini, and which
GameParts exist at all

Q6's second half, advanced but not closed, and the negative is the point.

config.ini is the disc's ONLY config file -- one find over the whole
extract -- and its own Shift-JIS header calls it the "Application /
GamePart initial settings table". Its [SYSTEM] section, which that same
comment says holds what the game and every game part share, is EMPTY. So
the boot order is not in disc-side configuration at all, and that search
space is now closed rather than merely unexplored.

What the file DOES carry is the language: XC_LANGUAGE_* -> eng/jpn/deu/
fra/esp/ita, defaulting to eng. That is the mechanism behind the EN/JP
build pairs in GP_TITLE and the <lang>.pak families -- a question the
corpus had described but never traced to its input.

Then the registry. Pulling every RegisterToFactory diagnostic string
binds 24 of the 29 GamePart ids to a C++ class, and five ids have no
registration site: 1, 2, 16, 18, 28. Id 1 is GP_ADVERTISE_DEMO, which
agrees with what I measured two iterations ago -- the attract loop is the
TITLE replaying ADV.wmv, not a transition into an advertise part. Marked
amber, not green: it is an argument from an error message, not from code.

Two things fall out for Q4: ids 3 and 4 are the same class
(GamePart_SaveLoad, one part with two ids), and the menu buttons' ids now
match the executable's own class names rather than a list of table names.
Still a name match, one level closer to the code.

What is still missing is the transitions themselves, and I say so: the
manifest gives the boot-side assets, config.ini the language, the
registry which parts exist. What decides to advance is in
GamePart_Title's code and that dig has not been started.
2026-08-28 18:49:19 +00:00
Sylpheed RE agent
935f7ecab8 re: one A skips the intro, and NEW GAME was never a hang
Two corpus claims died in one boot, and both were costing us.

Q9's last row: a movie IS skippable with a single A. One tap ~45s into
the boot put the title up at ~57s, against a ~193s no-input baseline
measured over three boots, and Canary's own keystroke counter went 3->4
so exactly one press was delivered. The skipped-to title is fully
functional -- it draws the PRESS A plate and a second A opens the main
menu. What actually breaks the boot is hammering: the 88-press run in the
traps doc. The scripts' "tapping breaks the title" comment is too broad
and costs every scripted boot two and a half minutes.

Q4's last row: A on NEW GAME does not hang. It opens DIFFICULTY
(EASY/NORMAL/HARD/BACK, focus on NORMAL), then SELECT DATA, and only then
does the guest throw -- at PC 0x82307128, which is inside sub_823070B0,
the cache-manager STL erase this corpus already documents and which has
nothing to do with the menu path. The screen sat unchanged for 90s
because it was a menu waiting for input from a loop that never pressed
anything. That is now a METHOD line: a screen that never changes is not
necessarily hung, and the fix is to look at it and press something.

Also METHOD: never run ps -ef in this container -- all three long-lived
processes carry the entire loop prompt as argv.
2026-08-28 18:45:59 +00:00
Sylpheed RE agent
1f822f8b30 re: the menu's sound events are named on the disc; the binding to them is
not

Q8. The cheapest thing nobody had tried was to look at the SE cue names,
and they are semantic: SE_UI_CURSOR (2), SE_UI_DECIDE (3), SE_UI_CANSEL
(4), SE_UI_IMPOSI (5, the error buzz), SE_UI_SUB_WIN_OPN/CLS, and
SE_UI_SPLASH_IN/OUT. That is exactly the move/confirm/back/error
vocabulary the question asks for, named by the authors after the EVENT
rather than the sound. 322 SE cues committed as reference data.

They all live in one bank: BANK_SE is a single field reading Static.slb,
and the disc-wide check agrees -- 0 of the 322 has an entry in FILES, the
5135-path list that names every voice, briefing and BGM bank.

I am calling the binding a NAME MATCH, not a measurement, and the page
says so in the same breath as it gives the table. It is a stronger name
match than Q4's GamePart ids -- these are the authors' own event names --
but nobody has watched the game emit cue 2 on a d-pad press, and this
container is muted against a dummy device so there is no audio path to
watch either.

One new negative, with its reach. Static.slb's 8 353 472 readable bytes
contain zero RIFF, zero seek and zero WAVE, scanned over the whole
buffer. The boundary marker that works for all 7 620 other banks is
absent, so the cue is named, the bank is named, and the wave inside it is
not locatable. The UI sound effects cannot be exported yet, and that is a
gap in the assets rather than in the naming. The named next step is
Pj_Silph.xgs -- the XACT project SETTINGS points at, which IS in
sound.pak at TOC 9454, though its 533 bytes carry no XGSF magic and its
region's phase says they are probably the previous bank's tail.
2026-08-28 18:33:58 +00:00
Sylpheed RE agent
e81dcad632 re: the boot intro and the attract loop are the same video, and the
new-game intro has a name

Q9, answered twice over -- once from the disc and once from frames I had
already captured and not used.

The movie manifest's first eight slots are the whole boot-side flow, and
the slot key is the role: LOGO1-4 -> logo1-4.wmv (not on the disc, which
is why the splash is a screen), ADVERTISE_MOVIE -> ADV.wmv, STAFF_ROLL ->
the credits reel, MS00A -> S00A.wmv, MS01A -> S01A.wmv. So the new-game
intro is S00A.wmv, decoded -- which is the half I could not test at
runtime, because A on NEW GAME hangs the emulator.

And there is no separate boot-intro slot: ADV.wmv IS the advertise movie,
the boot just plays it first. Confirmed independently by matching 19
attract frames against five candidates -- 15 hit ADV with a playhead that
advances monotonically at the sampling rate and ends at 137 s, its full
length, with the title back on the next sample. So the attract movie
plays to its end; nothing cuts it short.

That corrects me. Two iterations ago I recorded the attract movie as
"~85 s, so probably not ADV.wmv" -- arithmetic on a start I never
observed, since sampling began 39 s in. REFUTED and METHOD both take it,
along with the matcher's real failure mode: a near-black frame has no
signature, and its runner-up is not evidence.

Skippability I did NOT settle, and the corpus contradicts itself: one
page says A skips a movie every time, while the boot harness deliberately
never taps during one because it breaks the title. Named the one-boot
test rather than picking a side.
2026-08-28 18:29:39 +00:00
Sylpheed RE agent
e64965b3f9 re: S1 -- the Ready Room probe is a no-go, and not for the reason the
question expected

One iteration, as the mission gates it, and no Ready Room work started.

The two criteria it named both pass. It is NOT 3D: the corpus already
holds a capture of the running screen and it is a flat six-item menu in
the same visual language as the title. And unrecoverable paths do not
mean unrenderable screens -- content enumeration finds 60 builds in the
1106 entries.

What kills it is something the question did not ask. GP_READY_ROOM.pak
does not appear to hold the Ready Room's own menu at all. Two signals
agree: the four largest builds composite to briefing panels -- a planet
readout, unit markers, direction arrows -- and, much harder, the 902
bundles declare 317 distinct element names of which NONE matches any of
the six labels on the screen (takeoff, brief, hangar, pilot, option,
mission, ready all return nothing). The pak is the briefing /
tactical-map content behind the BRIEFINGS item. If this is ever picked
up, the first job is finding where the menu lives -- GP_BUNK.pak is the
obvious place -- not naming this pak's keys.

The probe also reaches back into something already delivered: kind ==
0x3002 finds ZERO buttons across all 902 bundles here, while GP_TITLE has
16. 0x3002 is one member of a 0x3000 family with sub-bits, and this pak
uses 0x3000/0x3004/0x300c/0x3008. Nothing in the milestone changes --
every screen in scope is GP_TITLE -- but the handoff now says not to ship
that equality as a general button test.
2026-08-28 18:24:48 +00:00
Sylpheed RE agent
b3c8632006 re: a music bank is two stems that play together -- not intro+loop, and
not something to concatenate

Q10, and it starts by withdrawing the question's own premise. BGM_001 is
not three sub-waves of 10 KB / 4.47 MB / 4.67 MB: the 10 KB is the bank
header. A bank is exactly TWO waves, and across all 32 BGM banks on the
disc the two always have the SAME duration -- equal to 0.01 s over
lengths from 37 s to 277 s. That alone kills intro+loop and kills two
halves of one piece, both of which require unequal lengths.

Four banks appear to break the rule and do not: BGM_106-109 are the known
leading-region straddle, and the giveaway is that the entry named
BGM_107.slb contains BANK id=1108. The seek packet counts pin each join
exactly, so they realign to the same two-equal-waves shape rather than
being dropped as noise. That trap goes in METHOD.

Then the roles, by decoding both waves to PCM. They are
sample-synchronous: transient-envelope correlation searched over +/-5 s
peaks at lag +0.00 s, and both waves stop at the same millisecond,
167.663 s. Two stems of one performance, meant to sound at once. Wave 1
is quieter, has almost no bass and is far more L/R-decorrelated, which
reads as a surround-rear pair or a second intensity layer -- I cannot
separate those two from the file, and say so: ChannelMask is 0x0002 on
both, and this game's channel metadata is already documented as
meaningless.

Two things the port needs that are NOT on the disc, both marked as
authored: the track is not a seamless loop (BGM_001 fades out and is
followed by 6.15 s of silence, no loop-point field found), and nothing
names which bank the menu plays -- all 32 BGM cues are numeric.
2026-08-28 18:13:10 +00:00
Sylpheed RE agent
7fef19b3a0 re: the transition between screens is a fade through black, and most of
its timing is on the disc

Q7. Every title-side screen carries a full-screen black .prm quad that
paints last, and its keyframe group IS the transition: black at T0, clear
by T1, clear until T2, then back to black on exit. Read with the corpus's
start-of-a-ramp rule and Q1's time unit that gives 0.87s for EXTRAS,
0.97s for the main menu, 4.08s for the title -- from the file, not from a
stopwatch.

The disc-wide check is per-pak all-or-nothing rather than the 41% the
headline count suggests, and GP_TITLE's 6 of 12 is the useful row: the
six builds carrying a fade quad are exactly the six SCREENS, and the six
without are exactly the six overlays. GP_DIALOG is 0 of 133. That is
independent corroboration of the overlay finding from two iterations ago.

One piece is NOT on the disc and says so: the fade-OUT length. The fourth
keyframe has no time slot, because a group's last block stops four bytes
short. Measured instead, at 30fps, ~0.4s and the same both directions.

And a warning I earned: the luminance rise after a transition is NOT the
quad's ramp. The incoming screen's own elements animate in after the quad
has cleared -- 1.47s observed against a declared 0.97s. Time the fade
from where the frame is pure black.

Rig: screenshot samples at 0.5 Hz and cannot see a 0.4s fade at all,
which is why an earlier burst called this an instant cut. ffmpeg x11grab
at 30fps instead; both go in METHOD.
2026-08-28 18:05:19 +00:00
Sylpheed RE agent
4e745c8177 re: the title menu wraps -- Q5 measured, Q4 driven, and one of my own
method lines withdrawn

Q5, measured off two boots: up/down move one item and WRAP at both ends
on the 5-item main menu and the 3-item EXTRAS alike; left/right do
nothing; B goes up one level and restores focus to the item you came
from (4/4); B on the main menu returns to the title; B on the title does
nothing. The menu opens on TUTORIAL -- the middle item -- 2/2, though a
third recorded run implies NEW GAME, so that one is reproducible rather
than invariant and says so.

Q4 by driving: LOAD GAME opens the save-slot list, TUTORIAL the lesson
list, OPTIONS the settings menu, EXTRAS build 6, MISSION SELECT the stage
list. NEW GAME is not tested -- A on it hangs the emulator and this
iteration needed the session. The GamePart ID behind each is NOT
measured: it is the entry of the decoded id table whose name matches the
screen I saw, and the page says so rather than wearing the badge.

And the withdrawal. Last iteration I wrote that these menus drop d-pad
presses shorter than ~0.3s. They do not. Once wrap is measured, every
press count I had is exactly right -- four presses moved four steps
THROUGH the bottom, which lands one above where a non-wrapping menu
would. I invented hardware flakiness instead of testing the ends of the
list. METHOD keeps the withdrawal rather than deleting the line.

Also: label brightness is not a cursor oracle here -- the background art
outshines the highlight on some rows. menu_focus.py reads the focus ring
instead, 254 vs <82, no tuning.
2026-08-28 17:53:46 +00:00
Sylpheed RE agent
bebdb18a02 re: Q3 was already answered in docs/re -- deliver it, and test it on EXTRAS
The handoff had Q3 as "runtime-solved only". It is not: the layer key at
+0x0A of the T8aD sprite header, stable-sorted, is a file-derivable paint
order, already checked against five measured orders and already driving
the compositor for every build on the disc. That answer had simply never
reached the page the port agent reads.

The new evidence is EXTRAS -- the first screen composited from the rule
alone and scored against a framebuffer capture the rule had never seen.
0.9620 at zero shift, against a same-tooling control of 0.9657 on the
screen the rule was fitted to.

Written down with its limit rather than its headline: align_to_capture
correlates edges, and a paint-order change moves blends, not edges, by
=<45/255 on a few per cent of pixels. So the score is evidence for
placement and only CONSISTENT with the order. The sharp A/B needs a
rebuild and is named in the doc instead of being claimed.
2026-08-28 17:39:29 +00:00
Sylpheed RE agent
88b3ce9af5 re: which GP_TITLE build is which screen, measured against the game
Q2. The archive is eight screens shipped twice, English and Japanese --
not the "build 4 title, 5 main menu, 6/8/9 submenus" the handoff claimed.
Build 8 is the JAPANESE main menu; 6 and 9 are the EN and JP EXTRAS, and
EXTRAS is the only submenu GP_TITLE holds. The PRESS (A) BUTTON plate is
its own build (2/3), composited over the title art and faded in a beat
later, not a state of build 4.

Confirmed by booting to the main menu and walking it: title, PRESS (A),
main menu and EXTRAS each match their render element for element. Builds
0/1 and 10/11 -- a DELTASABER / SYLPHEED A.I. plate -- were looked for in
the whole boot filmstrip, every title-side screen and the attract loop,
and appear in none of them; the reach of that negative is written down
rather than filled in with a guess.

Two rig traps went into METHOD: the menus drop d-pad presses shorter than
~0.3 s, and a grab 2.5 s after a transition can catch a screen mid-fade
-- which nearly wrote "the returned title has no plate" into the corpus.
2026-08-28 16:55:33 +00:00
Sylpheed RE agent
27e8c51978 re: a keyframe time is worth half a rendered frame, and the ramp is linear
Q1 of the menu port, measured against the running game rather than reasoned
about. The developer-logo splash is the cheap target: it is the first thing the
guest draws and its bundle declares short, unambiguous ramps.

Two results, both frame-exact and both emulator-speed-independent (frame numbers
are VdSwap counts, the guest's own frames):

  * the ramp is LINEAR. A declared 15-unit fade lands on round(255*k/15) for all
    seven of its samples with zero error, k stepping 2,4,6,8,10,12,14. No ease
    can reproduce a constant step of 34 at both ends.
  * the animation clock advances 2.000 time units per submitted frame, over six
    consecutive intervals with no residual, with 1 unit as the quantum
    underneath (one frame in the fade-out advances by 1).

The conversion to seconds is one step further and is flagged as such: 300 frames
took 10.87 s = 27.6 present-frames/second, which reads as a 30 Hz title at 92 %
under the emulator and gives 1 unit = 1/60 s -- the title build 4.2 s, the main
menu build 1.1 s. That reading is not proven, because the rate was measured
while the guest was still streaming from the ISO; the page names the one test
that would settle it and says what changes if it goes the other way.

Committed beside it: the raw draw capture and the per-frame quad CSV, so the
numbers can be re-derived without a disc or an emulator.
2026-08-28 15:56:33 +00:00
Sylpheed RE agent
5cdff5e515 docs/re: the disc atlas -- one map of how the assets reference each other
A reference page covering the four layers (disc media, IPFB archives, container
formats, domain chains) and the three hashes that join them: name_hash for
archive TOC keys (case-insensitive), tag_hash for IDXD records and fields
(case-sensitive), ixud_hash for localised text.

Counts were measured on the retail extract for this page rather than copied
forward from the corpus -- 41 archives, 26443 entries, 166 xpr, 97 wmv -- and
where they overlap with docs/re they reproduce it independently (104 cutscene
slots / 101 movies / 99 / 99 / 22).

It gives the dangling references their own section, because they are properties
of the shipped disc and a port has to survive them: pmbase.t32, SUBTITLE_S12B,
the four boot logos, GP_TEST, the six 2D paks at exactly 0% named, and the 42
provably unrecoverable ISL symbol keys.

Also published as an artifact for reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:51:45 +02:00
Sylpheed RE agent
306a8a5661 viewer: open the whole sound bank, not just the voice half
The library enumerator kept only names containing VOICE or \Briefing\, and read
eng\sounds.tbl unconditionally. So the Explorer could reach 4382 of the 9519
banks in sound.pak: no music, no jingles, no sound effects, and no Japanese
voice at all -- roughly half the disc's audio had no route to the UI.

`slb::list_audio_entries` now returns every named bank with the category its
path implies (Music / Jingles / Sound effects / Radio / Dialogue / Movie voice /
Briefing). `list_voice_clips` is that, restricted to the spoken categories, so
its existing test still guards the old behaviour. The 36 root banks carry no
language component and appear whichever table is read; the window gets an
English/Japanese switch that re-reads the other sounds.tbl, since the table name
IS the selector.

Two defects the decode found, both recorded in
docs/re/structures/sound-pak-contents.md:

* `Static.slb` -- the SFX bank -- declares 616768 bytes more than sound.p04
  holds. Not our extraction: p04 matches the ISO's own directory record, and a
  sweep of every pak on the disc finds this one entry over-running and no other.
  It is the highest-offset entry, so its comp_size is an allocation size. A
  short read is now allowed for the tail entry ONLY; any other overrun stays an
  error, because clamping it would hide real damage behind a half-decoded asset.
  The bank went from unreadable to 514 s of audio.

* the left-channel downmix was applied to everything. Right for voice (mono
  content however stored), wrong for music (a real stereo mix, half of it
  discarded). The caller now decides from the category.

35 of the 36 shared banks decode; JNGL_001 does not, and says so in the player
instead of the panel silently closing. Its payload is not a whole number of XMA1
packets from any known data offset, so it is likely not a plain headerless
stream -- written up rather than papered over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:28:20 +02:00
Sylpheed RE agent
1054d43676 docs/re: finish the INDEX merge I left unresolved
Commit 03ece95 ("Merge branch 'auto/idxd-unnamed-keys'") shipped INDEX.md with
live conflict markers. My check was worthless in a way worth naming: I ran

  git add docs/re/INDEX.md ; git status --short | grep -E '^(UU|AA)'

in that order. Staging a conflicted file is exactly what marks it resolved, so
the grep found nothing and reported "no unmerged" -- the control passed BECAUSE
of the action it was meant to gate. Both halves of the hunk then went into the
tree verbatim, leaving two competing IDXD rows.

Resolved as a union, which is what it always was:
  * HEAD's "IDXD object/table" row -- the 2026-08-25 record/index-region decode
    -- supersedes the branch's older one-line version of the same row;
  * the branch's "IDXD nameless field keys" row is new and is kept.
2026-08-28 15:55:18 +02:00
Sylpheed RE agent
e1dcc689bc agent: move the loop prompt's memory into the corpus, and let it push
The autonomous agent's loop prompt had grown to ~9 000 words of accumulated
findings, refutations and traps. That is a symptom, not a style: it was doing
the job the corpus should do, in the one place that dies with the container.
Three concrete failures followed from it, and each gets a structural fix rather
than a louder instruction.

1. IT REPEATED WORK IT HAD ALREADY DONE.
   The "do not revive" list and the method traps existed ONLY in the prompt, so
   nothing pointed a fresh iteration at them. Extracted verbatim into two
   tracked files:

     docs/re/REFUTED.md  105 claims tested and dead, grouped by subject so a
                         grep for your noun finds the neighbourhood
     docs/re/METHOD.md   the traps already paid for -- controls, inference,
                         searching, reading data, runtime

   Both are linked from INDEX.md, and the loop prompt now opens by requiring
   them to be read. This is the fix for "re-derived something already known":
   the knowledge is now where the next iteration looks, not in a context window.

2. IT FORGOT TO ARM THE NEXT WAKEUP.
   The prompt tried to solve this by shouting at itself in the first line. The
   real fix is to stop asking: `loose` now defaults to a FIXED interval (45m),
   so the harness owns the cadence and a forgotten ScheduleWakeup cannot end the
   run. SYLPH_LOOP_INTERVAL= (empty) restores self-pacing.

3. IT COULD NOT PUBLISH, SO THE WORK ONLY EXISTED IN THE CONTAINER.
   New `push-work`, plus a read-only credentials mount
   (SYLPH_GIT_CREDENTIALS, default ~/.sylph-git-credentials). It pushes the
   CURRENT branch only, refuses anything that is not auto/*, and never
   force-pushes -- so the consolidated line stays a human's decision and a
   confused iteration cannot rewrite history. The loop prompt now requires a
   push on every iteration that commits, rather than at the end of some longer
   arc, which is exactly when a container dies.

The prompt itself drops from ~9 000 words to 85 lines and, more importantly,
stops accumulating: findings go in the corpus, and the prompt points at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 15:46:59 +02:00
Sylpheed RE agent
03ece95c06 Merge branch 'auto/idxd-unnamed-keys'
# Conflicts:
#	docs/re/INDEX.md
2026-08-28 15:27:51 +02:00
Sylpheed RE agent
1fce3f9c71 Merge branch 'auto/slb-loader'
# Conflicts:
#	docs/re/INDEX.md
#	docs/re/structures/slb-data-offset.md
2026-08-28 15:27:46 +02:00
Sylpheed RE agent
8124b85fc7 Merge branch 'auto/regn-reader'
# Conflicts:
#	docs/re/BACKLOG.md
#	docs/re/INDEX.md
#	docs/re/structures/regn-map-grid.md
#	tools/re-capture/regn_decode.py
2026-08-28 15:26:35 +02:00
Sylpheed RE agent
245b73243b Merge branch 'auto/isl-builtins-26-28-29'
# Conflicts:
#	docs/re/INDEX.md
#	tools/re-capture/isl.py
2026-08-28 15:24:55 +02:00
Sylpheed RE agent
35538e3b5b re: the Time/Points record is not on the debriefing path - and a ring drainer matches the message bus
Read 0x821A0AD0, the debriefing, end to end, resolving every call and string in
code order. The word-B bit-setter sub_8219F2E8 is its FIRST call, before anything
else; the function makes 44 calls and references exactly two strings, DEBRIEFING
and BASE_INFO.

No Time and no Points anywhere on this path - not in the debriefing, not in the
bit-setter. Section 5.3's expectation that clearing a challenge stage records its
metric alongside the bit is not supported by the code that clears it. The reach of
that negative: the debriefing function and its directly-called bit-setter, both
read whole; it does not rule out a store deeper inside one of the other 43
callees.

0x8219F670 turns out to be an online-state predicate: 88 bytes, no strings, reads
the singleton, tests bit 1 of [0]+60, and if set passes [0]+64 to 0x824A9C90 and
returns whether the result is 2. A connection or sign-in check, not a record
store.

The useful find is 0x8219F460, 368 bytes and self-recursive, which drains a ring
based at r29+320 with element base +4, capacity +8, head +12 and count +16. Those
are the corpus's documented message-bus offsets exactly - BACKLOG records
sub_82175C20(bus+4, &msg) as a ring-buffer append with capacity +8, head +12,
count +16 - and sub_82175C20 is the very notifier section 5.6 found the word-B
writer calling with bit + 64. So the open question "ring-buffer PUSH, no handler"
now has a candidate drainer with a byte-for-byte matching field layout, reached
from the debriefing.

What is NOT shown is that these are the same ring instance. The push targets
bus+4, this drain targets r29+320. Matching offsets prove a shared container
shape, not a shared object, and the identity needs the two base pointers tied
together.

All seventeen artefacts byte-identical.
2026-08-28 13:07:02 +00:00
Sylpheed RE agent
f7a0cf47b1 re: progress word B's writer is sub_8219F2E8 - it sets bit (stage - 24)
Section 5.5 narrowed the search to 21 functions but could not name one, because
the scan assumed the copy destination was an r1-relative stack local. It is not.
Reading the known word-A writer instead of guessing its shape shows a frame
register: bl singleton, addi r3,r31,96 (dest), addi r4,r11,80 (src = obj+80), bl
copier, stw r10,96(r31). That one wrong assumption made the first scan return 0
for all 21, the known-good included - the third filter this week killed by its own
control.

With the real shape - addi r3,rF,K before the copier, then stores at K+d on rF -
word B sits at d = 1956-80 = 1876, and exactly one of the 21 hits: sub_8219F2E8,
copy dest r31+560, storing 2436(r31), and 560+1876 = 2436. Control: the word-A
writer 0x821C1630 stores at relative +0, i.e. word A.

The proof is the bias. Word B's writer computes addi r11,r11,-24 then 1 <<
(index-24) before or-ing into the copy; word A's writer shifts by the raw stage
number with no bias. That reproduces section 5.1's read-side rule - n >= 24 means
bit (n-24) of word B - on the write side, so word B's bits 0-5 are challenge
stages 24-29, the same six that 5.4's MISSION_ID names. The two writers are exact
complements.

It has exactly one real caller, 0x821A0B1C inside 0x821A0AD0, whose strings are
BASE_INFO and DEBRIEFING - so clearing a challenge mission is recorded from the
debriefing screen.

A second bitfield falls out of the same function: it reads the copy at +1880
(2440(r31) = obj+1960, the word after B), re-reads obj+1960 after the setter, xors
old against new, and loops the 32 result bits calling 0x82175C20 with bit + 64. So
obj+1960 is a second 32-bit progress bitfield with ids 64-95, whose newly-set bits
are announced one at a time - an unlock-notification loop.

Against 5.3's own prediction: neither sub_8219F2E8 nor its caller references Time
or Points, 0 for both. 5.3 expected word B's writer to also store the stage's
Time/Points record. It does not - this is the bit-setter only, and the record
store is still unlocated.

All seventeen artefacts byte-identical.
2026-08-28 12:50:16 +00:00
Sylpheed RE agent
a41a6b9783 re: progress word B - search space closed at 21 functions, the field itself still unwritten
Section 5.3 left word B (+1956) with no known writer. Three routes ran this turn:
one produced a population, two were refuted, and the field is still unwritten.

The offset route has power here, unlike +184. +1956 is a large unusual offset with
9 stores and 28 loads image-wide, against +80's 5403 and 6923. Control: the known
reader 0x821898C4 sits in the gate's list-builder 0x82189870, exactly where 5.1
puts it. But none of the 9 stores turns out to be a progress write - filtering by
whether the storing function reaches the progress object gives 0 of 8 distinct
functions, and the 9th stores through r1, the stack pointer, so it is a local.
+1956 is simply an offset several unrelated structures also use.

The obvious filter is dead, and it fails its own control. Testing "does the
function reference the singleton global 0x828F48B0 or call 0x821707C0" against the
KNOWN word-A writer returns no contact, because that path reaches the progress
object through the copier and never through the global. A filter that rejects the
known-good answer proves nothing about the others.

What did work: progress is only ever changed copy-modify-store, so every writer
must call both the copier 0x82175110 and the setter 0x8216FF70. Measured, 21
functions call the setter, 21 call the copier, and 21 call both - the two caller
sets are the same set. That is the complete progress-write population and it is
small enough to read exhaustively. Control: the word-A writer is among them as
0x821C1630 (BASE_INFO + DIFFICULTY), and 5.2's store 0x821C1820 lies inside it -
the doc's 0x821C09D8-0x821C29F0 is the enclosing method, 0x821C1630 the function.

No member of the 21 stores to +1956 directly. Their only large store offsets are
into this - 1004, 1980, 2040, 2100, 2199, 2436 - with 1004 being 5.2's own
this+1004 & 0x20000 guard. So word B is written through the stack copy, at
localbase + 1876, which no offset scan can pick out.

One false friend worth recording: two of the 21 reference Points, which looks
exactly like the challenge record-storing path 5.3 predicts. It is not - their full
string sets are Dependency, MissionObjective, Points, WEAPON, WEAPONS, i.e. the
arsenal development economy, and neither references Time. There are two Points
vocabularies, development points and the leaderboard metric.

Next candidate, not yet checked: 0x8218EFE0 is the one progress writer whose
strings are BASE_EXTRA and DIFFICULTY - the only member of the 21 carrying the
EXTRA vocabulary.

All seventeen artefacts byte-identical.
2026-08-28 12:33:32 +00:00
Sylpheed RE agent
3b80779180 re: which challenge stage is which leaderboard mode - CLOSED from the record's own fields
challenge-mission-gate.md section 5.3 left a marked 🟡 on the exact pairing and named
the right route: read the record's binary index section rather than the string pool.
Running that with the corpus's own reader settles all six missions at once.

  TimeAttack    MISSION_ID 24  REQUIREMENT 16  RECORD_TYPE Time
  ScoreAttack   MISSION_ID 25  REQUIREMENT 24  RECORD_TYPE Points
  Extra01       MISSION_ID 26  REQUIREMENT 25  RECORD_TYPE Points
  Extra02       MISSION_ID 27  REQUIREMENT 26  RECORD_TYPE Time
  Extra03       MISSION_ID 28  REQUIREMENT 27  RECORD_TYPE Time
  Extra04       MISSION_ID 29  REQUIREMENT 28  RECORD_TYPE Points

Control: the table appears in 6 pak entries, all in tables.pak, and all six are the
same table byte for byte - one per language, i.e. one user under the per-pak copy
rule. Zero variants.

Three things fall out, none of them assumed. MISSION_ID is the stage number: the six
IDs are 24-29 in slot order, exactly the six challenge stages S24-S29, bijectively -
so the "four Extra0n modes against five _EXn stages" worry dissolves, because _EXn is
a unit-variant suffix that indexes nothing here. REQUIREMENT is a strict chain, 16,
24, 25, 26, 27, 28, each mission requiring the previous one's stage with TimeAttack
anchored on stage 16, the final story mission; 5.3 inferred a chain from the shape of
the string pool, and it is now exact and branchless. RECORD_TYPE is the leaderboard
metric and it is per-stage, not per-name: 3 Time (S24, S27, S28) and 3 Points (S25,
S26, S29), so the tempting reading that the Extra0n family shares one metric is
wrong - Extra01 and Extra04 are Points while Extra02 and Extra03 are Time.

Withdrawn from 5.3: the string-pool reading listed the numeric tokens as 16, 25, 26,
27, 29 with 24 and 28 deduped away. The real REQUIREMENT set is 16, 24, 25, 26, 27,
28 - 29 is a MISSION_ID, never a requirement. A value pulled from a deduped pool by
adjacency got one member wrong; the indexed read has no such failure mode.

Left as an association rather than a proof: RECORD_TYPE takes exactly the two values
Points and Time, and result-screens names two ranking screens POINT_RANKING and
CLEAR_TIME_RANKING, but no code path has been shown selecting one screen from that
field.

All seventeen artefacts byte-identical.
2026-08-28 12:15:48 +00:00
Sylpheed RE agent
0ace5db7c7 re: the 95 BASE_INFO screens partitioned - BASE_INFO discriminates screen-config from table-read
Item (a): mine the screen population found last iteration. Resolving every one's
key list via the string-xref join over its own body gives 298 distinct keys and a
three-way split: 95 = 9 corpus-known + 48 new with a readable key list + 38 bare,
where "bare" means BASE_INFO and nothing else. The bare 38 are the honest limit
of the instrument - a screen whose only catalogued string is BASE_INFO cannot be
named this way.

The control turned the finding into a law. Only 9 of the 19 loaders the corpus
documents appear among the 95, and which nine is not arbitrary. The nine that
reference BASE_INFO all read a SCREEN's own configuration: debriefing, career,
save, medals, evaluation list, gamma, POINT_RANKING, CLEAR_TIME_RANKING, hangar.
The ten that do not all read a DATA TABLE: unit definitions, weapon datasheet,
AIParams, stage settings, chatter, PlayerParams, ControlTweak, the PG* HUD
roster, material slots, and the leaderboard. 9/9 and 10/10, both off-diagonals
empty. The leaderboard falling on the table side is a check rather than an
exception - its own doc records that all 18 of its names are tables.pak names, so
it is a table reader that happens to paint a screen. That exclusion is what last
commit's "universal screen marker" claim was missing.

48 new screens are named by their most distinctive keys, including the title
screen (sub_821C4EB0: TITLE_SCREEN/TITLE_MENU/LOADING), the ready room
(sub_821D6D28: SCRIPT_ID/SCRIPT_PATH, confirmed independently by its own error
string silph::GamePart_ReadyRoom::Impl::PrepareScript), movie subtitles, the
three menu modes STANDARD/EXTRA/TUTORIAL, the speaker window, loadout and gallery
views, mission select, and an equip screen carrying the game's own typo
EQUIIP_LIMITATION.

One caution worth recording: a high key count is not a rich screen. The two
27-key entries sub_82297550 and sub_822A2F00 resolve to pure coordinate pairs
(1080,163 / 734,502 / 1164,403) - a layout table, not configuration. Read the
values before trusting the rank.

Still open: the 38 bare screens, and whether the EX_ in EX_MENU_DISABLE_SKIP and
EX_LETTERS is the same EXTRA mode word as +184.

All seventeen artefacts byte-identical.
2026-08-28 11:58:51 +00:00
Sylpheed RE agent
80406ce3e4 re: the RTTI route is dead - BASE_INFO enumerates 95 screens instead
Item (a): can the vtable scan name the corpus's anonymous classes?

The route I proposed last turn - "the RTTI behind those two anon classes" - does
not exist. Of all 1150 catalogued vtables, 1150 carry an ANON_ class name, 0 have
rtti_present set, and 0 have a non-empty base_classes_json. Not one named class,
not one RTTI flag, not one base-class edge in the whole table. Withdrawn as
unrunnable; the ANON_Class_XXXXXXXX labels are synthesised, not recovered.

The sibling methods cannot name a class either: slots 0, 2, 3 and 4 of both EX_
classes reference zero strings. Every naming datum sits in slot 1, the method the
corpus already had.

Re-verified at the true addresses (0x820A9760 and 0x820A9350) that sub_822814D8
and sub_8227A3A0 are each slot 1 uniquely, and sub_82286BC8 is in no vtable.

What the shape DID give: both classes are 5-slot with an exactly-88-byte slot 0.
83 vtables have 5 slots, 16 also have the 88-byte slot 0, 13 of those have a
catalogued slot 1, and 10 build a key list. Ranked by key count the two known
screens sit at the top and the rest name themselves - a medals screen
(MEDALS/ACHIEVED_MEDALS/NEW_MEDAL), an evaluation list (EVALUATION_LIST plus
eval_%02d_eval / eval_%02d_points), the gamma screen (GAMMA_RGB/GAMMA_WB),
POINT_RANKING, CLEAR_TIME_RANKING, and three bare ones.

POINT_RANKING and CLEAR_TIME_RANKING being two separate screens is a direct lead
for the open item "which challenge stage is which leaderboard mode": the
leaderboard is ranked two ways and each way has its own class.

But the obvious follow-on claim - that BASE_INFO marks this family - is false,
and the control says so. BASE_INFO is a single string at 0x820A20EC referenced by
95 functions: 10 inside the family, 85 outside, including sub_82286BC8 (the save
screen, in no vtable at all) and sub_822AE628 (the hangar loader). So the
direction inverts: BASE_INFO is the universal first key of every screen key list,
a 95-function population, while the 5-slot/88-byte shape is only a weak filter
that catches 10 of them plus 6 non-screens. Enumerate screens by BASE_INFO, not
by vtable shape.

All seventeen artefacts byte-identical.
2026-08-28 11:41:52 +00:00
Sylpheed RE agent
141a65cb4a re: the ind_call damage is bounded - no corpus claim was contaminated
The protective sweep after last commit's refutation. The question was which
existing claims the ind_call cross product voids. Answer: none of the ones the
corpus makes.

Every caller-count claim in the corpus verifies against call edges, with zero
ind_call contamination:

  title-crash-stl-tree      sub_82457780  "one caller"     call=1   ind_call=0
  isl-trigger-queue         sub_8226EAB8  "16 callers"     call=16  ind_call=0
  isl-coroutine-spawner     sub_822737C8  "seven callers"  call=7   ind_call=0
  challenge-mission-gate    0x82175110    "22 callers"     call=22  ind_call=0

4 of 4 exact, and not one of the four carries a single ind_call edge.

There is a structural reason for that rather than luck. Partitioning the two
target populations: 1710 targets have ind_call edges, 12170 have call edges, 103
have BOTH, 1607 are ind_call-only, 12067 are call-only. And all 1710 ind_call
targets are vtable entries - 1710 of 1710, zero partials - against a control
where only 273 of the 12170 call targets are. So ind_call fires only on virtual
methods, and 1607 of them have no direct caller at all, which is exactly why the
two EX_ screens came back empty on call and flooded on ind_call.

The residual risk is narrow and nameable: the 103 targets carrying both kinds,
where a caller count taken without a kind filter would be inflated by about 633.
Everywhere else the two populations are disjoint, so a call-based count is safe
and an ind_call-based one is meaningless.

Rule for future work: always filter kind='call'; and when a function has no call
edges, that is a fact about it - it is a virtual method - not a gap to be filled
from ind_call.

All seventeen artefacts byte-identical.
2026-08-28 11:24:14 +00:00
Sylpheed RE agent
749567e835 re: the +184 writer is blocked - and xrefs.ind_call is a CROSS PRODUCT, not a call graph
Chasing what supplies the EX_ mode word. Two routes, both measured to have no
power, plus one structural fact that did come out.

The offset route is dead. stw ..., 184(rN) occurs 301 times in the image, lwz
from +184 occurs 351 times, and 115 functions touch both +144 and +184. +184 is
an ordinary small offset shared by many unrelated classes - the same shape the
corpus already recorded as failing three times. Nothing narrows to a writer.

The bigger result is an instrument refutation with corpus-wide reach. Asking
xrefs for the callers of the two screens returns 633 sources for EACH, and the
two lists are IDENTICAL, which cannot be right. Measuring the relation itself:

  ind_call rows                       1827297
  distinct targets                       1710
  distinct sources                       6992
  targets with EXACTLY 633 sources        236

236 different functions sharing an identical source count is the signature of an
unresolved-indirect-call cross product, not of a call graph. Any reading that
treats an ind_call edge as "X calls Y" is void, here and anywhere else in the
corpus it may have been used.

The control shows the other kinds are sound: sub_82286BC8 has exactly one
caller, kind call. And both EX_ screens have ZERO non-ind_call edges - they are
reached only through function pointers, which is why the direct graph is empty
for them.

What did come out: scanning the entries of all 1150 catalogued vtables in the
flat .pe, both EX_ screens are slot 1 of their own class - sub_822814D8 in
ANON_Class_271D5F25 and sub_8227A3A0 in ANON_Class_CAA8AD62 - while sub_82286BC8
is in no catalogued vtable at all. So the 2/0/0/1 partition from the previous
commit reflects a structural difference rather than a coincidence: the two
screens that select on the mode word are vtable methods of their own classes and
the one that does not select is not a vtable method.

Still not settled: what writes +184. Both the offset sweep and the call graph are
exhausted for it. A route with actual power would be the RTTI behind those two
anonymous classes, or a runtime watch on the field - not another static offset
search.

All seventeen artefacts byte-identical.
2026-08-28 11:08:18 +00:00
Sylpheed RE agent
35a35f753f re: the EX_ selection is shown - a mode word at +184 tested against 3
result-screens.md left this explicitly open: "the selection has not been shown".
It is shown now. Both screens that reference EX_ names carry the same
five-instruction idiom immediately before the choice - sub_822814D8 at 0x822815b4
and sub_8227A3A0 at 0x8227a408:

  lwz    r11, 4(rX)              the owning object
  lwz    r11, 184(r11)           a mode word at +184
  addi   r11, r11, -3
  cntlzw r11, r11
  rlwinm r11, r11, 27, 31, 31    r11 = 1 iff [+184] == 3
  cmpli  cr6, 0, r11, 0x0
  bc     12, 4*cr6+eq, <plain>   not 3 -> STAGE_RESULT / OVERVIEW
                                 else  -> EX_STAGE_RESULT / EX_OVERVIEW

cntlzw of zero is 32, and rotating left 27 lifts bit 5 into bit 31, so the
sequence is a branchless equals-3 test. The equal-to-zero branch takes the plain
name, so EX_ is chosen exactly when the word is 3.

A partition over the three key-list loaders the corpus already names:

                        loads +184   does not
  references EX_ names       2           0
  does not                   0           1

sub_822814D8 and sub_8227A3A0 both load it; sub_82286BC8, which references no EX_
name, has no +184 access at all. Both off-diagonal cells are empty.

3 is the game's own EXTRA. challenge-mission-gate already established the
vocabulary for this constant at a three-way switch: == 3 is EXTRA, == 5 or 6 is
CHALLENGE, otherwise FILE. So the prefix reads literally - EX_ is the EXTRA
variant.

That REFINES the doc's tentative reading rather than confirming it. The section
proposed "EX_ = the challenge-mission debriefing" by analogy with _EXn on asset
names; the measured constant is EXTRA, which in the game's own three-way
vocabulary is a DIFFERENT kind from CHALLENGE (5/6).

Boundary of what was measured: the EXTRA = 3 constant is established at +144 of
the stage-loader class, while the word tested here is at +184 of [object + 4].
Same constant and the same EX/EXTRA naming, but two different offsets in what may
be two different objects - that the two fields are the same field is NOT shown,
only that both compare a mode word against 3.

All seventeen artefacts byte-identical.
2026-08-28 10:51:16 +00:00
Sylpheed RE agent
33671c5086 re: wep_85 is the ONLY declared-but-unshipped asset; the package gaps are unused numbers
The obvious follow-up to last commit's wep_85 finding: the weapon packages number
00 to 84 with 26 holes, so are those holes cut weapons too? Partitioning every
number 00-85 by package-present x named-anywhere-in-the-parsed-pak-data:

                     named in data   not named
   package present        59             0
   package absent          1            26

59 + 0 + 1 + 26 = 86, the numbers 00 through 85.

The 26 gaps are simply unused numbers, not unshipped weapons - nothing in the
data names them at all. The one occupied cell is the one already known: wep_85,
named 16 times, with no package. It is an isolated cut, not a pattern, which
refutes the natural expectation that finding one cut weapon means finding a
family of them.

The other zero is worth as much: 0 shipped packages are never named. Every
rou_f001_wep_NN.xpr on the disc is referenced by the data, so the package set and
the weapon vocabulary agree in both directions but for that single entry.

Running the identical test on the craft family gives a clean negative. Every
rou_f### number appearing inside an .xpr against every number named in the parsed
data: 16 in both, 0 named-but-unshipped, 0 shipped-but-unnamed. The 16 are
f001-f004, f101, f102, f104-f106, f201, f202, f301-f303, f401, f402 - the
hundreds digit groups them into five families with holes inside each (no f103, no
f203). No craft was cut.

So across both families the disc ships exactly what it declares, with the single
exception of that one weapon.

All seventeen artefacts byte-identical.
2026-08-28 10:33:41 +00:00
Sylpheed RE agent
7ba415fbae re: the last 9 effects are genuinely unshipped; wep_85 accounts for two
EF_IDX_ proved that prefixes exist, so the residual deserved the same treatment
across every package rather than one. Censusing prefixes over all 36
effect-carrying .xpr gives seven in use:

  EF_IDX_       223 names
  mdl_           45
  EF_IDX_mdl_    45
  VolumeLine_    10
  GN_            10
  GN__            6
  bare           the rest

Testing all nine unlocated names against all seven prefixes: 0 of 9 resolve.

That zero has force where the earlier disc-wide zero did not, and for the stated
reason - the control shows each prefix genuinely carries names the same search
reads (223, 45, 45, 10, 10, 6), so the instrument demonstrably works on the
population it is being asked about. The nine are bound by a datasheet field and
shipped in no package.

Two of them turn out to belong to one cut asset. eff_m010_wep_85 and
eff_m011_wep_85 name a weapon, and the weapon is real in the data:
Weapon_DSaber_P_wep_85_Beam with 24 uses, its Shell_, WeaponCannonName_ and
WeaponShellName_ siblings, and GameModel_eff_m010_wep_85 / _m011_ declaring the
two effects. But the weapon packages stop at 84 - hidden/resource3d/ holds 59
rou_f001_wep_NN.xpr files numbering 00 to 84 with gaps, and no wep_85. So wep_85
is a declared-but-unshipped weapon and its two effects go missing with it.

This does not contradict "every weapon is placed - 131 = 105+22+0+4". That
partition is declared x MOUNTED IN A LOADOUT, which is a different question from
whether a package ships.

Seven remain with no account: eff_e0044, eff_f0002, eff_f0002_barn, eff_h308,
eff_j002_e01, eff_j002_e02, eff_n0071. The .xpr route is now exhausted for them
under every prefix the disc uses; a different container or a runtime generator is
what is left.

All seventeen artefacts byte-identical.
2026-08-28 10:17:20 +00:00
Sylpheed RE agent
23a6cfd979 re: the EF_IDX_ prefix - ptc_pack has 727 names, and the map reaches 128 of 137
Censusing ptc_pack's own naming vocabulary turned up a third variant of the
prefix trap, and this one had been corrupting a number the corpus carried.

268 of ptc_pack's names do not start with eff_ at all. They start with EF_IDX_,
as in EF_IDX_eff_d001_f. A regex anchored at eff_ chops that prefix off and
merges distinct names, which is exactly where the earlier figure of 532 came
from. Enumerating maximal [A-Za-z0-9_] runs gives 727. The two earlier traps were
a STORED name being longer (rot_n001_break) and a BOUND name being a prefix
(eff_f0002 inside eff_f0002_barnhaze); this is the third - a prefix the pattern
cannot see at all, because its anchor sits in the middle of the real name.

Looking each bound name up bare AND under EF_IDX_ resolves 25 of the 34 that were
unlocated. The map is now 128 of 137, and the residual is 9, small enough to
print: eff_e0044, eff_f0002, eff_f0002_barn, eff_h308, eff_j002_e01,
eff_j002_e02, eff_m010_wep_85, eff_m011_wep_85, eff_n0071. All 17 eff_l### are
among the recovered.

This withdraws my own previous correction. I had recorded Base.xpr (53) as
holding more bound effects than ptc_pack (46), and struck out "ptc_pack is the
effect library". With the prefixed keys counted ptc_pack holds 71 - it IS the
larger library, and the 46 was an undercount from the same truncating pattern.
Two shared libraries remains right; which one is bigger does not.

The suffix vocabulary: 106 distinct tokens over the 727 names - IDX 223 (the
prefix above), _f 137, _e 119, _root 87, _col 54, _mdl 45, _break 43, _ring 38,
_ALL 17, _haze 14, _thunder 10. That census counts ALL tokens rather than
trailing ones, which is precisely how the EF_IDX_ PREFIX surfaced inside what I
had first labelled a suffix list - the mislabel found the bug.

Testing the structural candidates the way _hangar was tested, does the suffixed
name have a bare parent:

  _ALL      17 names    17 of 17
  _root     87          64 of 87
  _break    30          15 of 30
  _e        74           0 of 74
  _f        61           0 of 61

_root is strictly terminal - 87 of 87, and it never appears mid-name. The
compound shapes put it outermost: _e_root 19, _f_root 18, _break_root 13, bare
_root 30. So the order is <stem>_[<faction>|<break>]_root and _root reads as a
hierarchy marker rather than a variant - though 64 of 87 having a bare parent
means it is not simply the parent of an existing node, and _break at 15 of 30 is
likewise not a plain destroyed-twin-of-everything.

_e/_f never have a bare parent, 0 of 135. That is independent asset-side
confirmation of the faction law: an effect is authored per faction and there is
no faction-neutral original for either side to derive from.

effect-homes.txt changes 5/30 and every line pairs: five values changed (103->128,
34->9, ptc_pack 46->71 and its sort position, the residual header, 3-digit
80->105 of 110) plus 25 pure deletions, exactly the 25 recovered names. All are
3-digit, so the 4-digit line is unchanged at 23 of 27. The other sixteen
artefacts are byte-identical.
2026-08-28 10:01:53 +00:00
Sylpheed RE agent
aa478d9444 re: the faction law generalises - 564 of 564, four fields, all six paks
The previous commit measured _e/_f on one pak and only through the effect
binders. Widening the sweep to EVERY string field of every unit object in ALL
SIX GP_MAIN_GAME_* paks:

                value _e   value _f
  UN_e###          198          0
  UN_f###            0        366

564 of 564 agree and the mismatch residual is empty. The law is not confined to
one field either - it holds separately, at 100%, in each of four:

  LowerHPFxModel            252 of 252
  ShieldHitEffectName       210 of 210
  ShieldRecoverEffectName    84 of 84
  ExplosionFxModel           18 of 18

The two shield fields were not in the earlier measurement at all, so the law
reaches further than the *FxModel family that suggested it.

Scope stated exactly, because "general" would overclaim: this is a law about
EFFECTS, not about assets in general. The sweep covered every field, and every
_e/_f-suffixed value a unit binds turns out to live in those four effect fields.
No model, motion or SE value carries the suffix at all, so the faction pairing is
NOT shown for those kinds - there was simply nothing to test.

UN_n### (TTRL) binds no _e/_f value in any of the six paks: 12 objects, = 2
users, with nothing on either side. That confirms over the whole population what
was only a single-pak observation before.

Also corrects the ID grammar and reconciles a count. The earlier section reported
42 + 26 + 2 = 70 unit objects using a regex that required UN_<letter>###_<FACTION>_;
the looser UN_<letter>###_ finds 71. The extra one is UN_e910_core_ADAN_GeneratorCore,
which inserts a sub-kind token BEFORE the faction tag. So the grammar is
UN_<letter>###_[<subkind>_]<FACTION>_<name>, and both counts were right for their
own pattern.

All seventeen artefacts byte-identical.
2026-08-28 09:44:20 +00:00
Sylpheed RE agent
1265512880 re: _e/_f on an effect name is the binding unit's FACTION (94 of 94)
Chasing the 17 unlocated eff_l### turned up their shape first: they come in
_e/_f PAIRS - eff_l101_e + eff_l101_f, and the same for l102, l104, l105, l106,
l201, plus _e-only l010/l011/l107/l108 and _f-only l002.

Partitioning every eff_<letter><digits>_<e|f> binding by the ID letter of the
OWNING unit (one GP_MAIN_GAME_* pak = one user):

                effect _e   effect _f
  UN_e###           33          0
  UN_f###            0         61

94 of 94 agree and both off-diagonal cells are empty. The control reads the
factions straight off the IDs: UN_e### -> ADAN (42 objects), UN_f### -> TCAF
(26), UN_n### -> TTRL (2, tutorial, binding neither). So an effect ending _e
belongs to an ADAN ship and one ending _f to a TCAF ship - the same visual is
authored twice, once per faction, which is exactly why eff_l### arrives in pairs.

What the 34 unlocated ARE is now also clear, even though where they live is not.
They are one job, not a scatter: Generic binds 32 of the 34, Explosion 19,
Shell 9, Level_0 and Weapon 2 each. The binder fields rank LowerHPFxModel 252,
HitFxModel 144, then JetFxModel_00N and AfterBurnerFxModel_00N. They sit in the
six GP_MAIN_GAME_* paks at 130 bindings each plus 32 in DefTables.pak. Since
LowerHPFxModel is the damaged-ship effect, the residual is largely the
per-faction battle-damage and hit visuals. None of the 34 is a record name and
only one is a field name, so they are asset references.

Stated plainly: they remain unlocated AS ASSETS. Knowing the family and its
naming law does not say where the geometry lives - the .xpr route is exhausted
for them and the parsed pak payloads hold references, not meshes.

Also fixes a defect in the artefact shipped last commit. effect-homes.txt came
back with two equal-count lines swapped: Counter.most_common() breaks ties by
insertion order, so the package listing was not deterministic. Now sorted by
(-count, name) and verified to regenerate byte-identical twice running. This is
the corpus's own rule - any map built by iterating a set or Counter needs
sorted() - and the new tool had violated it.

The other sixteen artefacts are byte-identical; effect-homes.txt changes only in
the tie-break ordering of the five 1-count rows, with every line pairing.
2026-08-28 09:28:39 +00:00
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
7e9d1437c0 re: effects split into two families by digit-width; ptc_pack.xpr holds one
The open question was how an effect mesh is reached at all, after last
iteration's .xpr byte search was refuted by its own control. The corpus already
held the pointer: xbg7-mesh names ptc_pack.xpr, a 20 MB shared particle package
in hidden/resource3d/. It lists 532 distinct eff_* resources - 527 three-digit,
3 four-digit, 2 unnumbered.

Joining it against the 137 effect names the datasheets actually bind:

                    in ptc_pack   not
  3-digit                71        39
  4-digit                 0        27

Zero of the 27 four-digit names resolve in ptc_pack, and that series is a closed
three-letter set: e (10), f (12), n (5). Since eff_e0033 was found in Base.xpr,
the reading is two effect families - a shared three-digit particle library in
ptc_pack.xpr, and a four-digit series that lives in the per-model and base
packages instead.

The instrument passes its own control this time, which is the difference from
last iteration. The same kind of byte search demonstrably reads names out of
this file - 532 of them - so a zero WITHIN ptc_pack is meaningful in a way the
earlier disc-wide zero was not.

Among the 39 three-digit misses the letters cluster hard - j 22, t 14, m 2, h 1 -
and ptc_pack contains just one j name against 149 m and 81 s, so eff_j### is a
third grouping that is almost entirely elsewhere.

Not closed: 66 of the 137 bound effects still have no located home, eff_n0071
among them. But the route is now real and has a number on it, and the next step
is the eff_j### family and the four-digit series rather than another disc-wide
grep.

All sixteen artefacts byte-identical.
2026-08-28 08:55:41 +00:00
Sylpheed RE agent
dd542c23a4 re: the unit family is the only schema on the disc; an .xpr search cannot prove absence
Two exhaustive probes agree on the same 15 records. The six Shift-JIS type words
occur in the six GP_MAIN_GAME_* paks and nowhere else, and a disc-wide sweep of
every parsed record name for a wildcard shape (???, *, ###, NNN, <...>) returns
7 distinct names - exactly the seven already in the schema: Turret_???,
Hatch_???, Bridge_???, Thruster_???, ShieldGenerator_???, Versatile_???, NS_*,
each x6. So the weapon datasheet, the arsenal item and StageResource ship NO
schema; the unit datasheet is the only structure the disc describes to itself.
Wildcard field names are confined to the schema records too - NozzleSpec_???,
NozzleFrame_???, CannonFrame_???, MuzzleFrame_???.

No untyped gaps either. The full residual is 30 slots and every one holds a
sample value rather than a missing type: 28 booleans spelled Yes, plus
Generic.ID = Ship_ and Generic.Type = Vessel. The booleans follow one pattern -
the six destructible part types each carry the same four-boolean core
(IsDestructible, IsInvolved, IsRadarVisible, IsShielded), Turret_??? adds IsAuto,
Generic carries only IsDestructible, and NS_* has its own pair AttenuationAlpha /
AttenuationVolume. 28 + 2 = 30; 7 fully-typed records + 8 with examples = 15.

The third result is a refutation of my own instrument. Testing the four
genuinely-undeclared effects against the 166 .xpr packages put eff_f0002 and
eff_f0002_barn in Base.xpr and found nothing for eff_e0044 or eff_h308 - but the
control kills the negative: eff_n0071, which we measured LIVE as an Explosion
record's ExplosionFxModel, also returns nothing from the same search. A
known-live name the test cannot find means the test has no power here. Only the
positive half counts: eff_f0002/_barn do ship. Nothing follows about eff_e0044
or eff_h308, and the earlier "no mesh" remarks about eff_e0058/_e0059/_e0060 are
weaker than written - absent from a byte search over .xpr is not absent from the
disc. How an effect mesh is actually reached is now the open question, since
eff_n0071 is not a plain name string in any of the 166 packages.

All sixteen artefacts byte-identical.
2026-08-28 08:38:26 +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
44bdbad2d6 re: every bound effect vs every declared effect; a Shift-JIS dev placeholder
The Explosion substructure's field list was already in the corpus, so the open
part was whether the effect names RESOLVE. Declared x used, sweeping 34 binder
field names (*FxModel*, *EffectName, Effect_*, ShellModel, CoverModel,
SilhouetteModel) against every name declared by a LOD_Effect_<n> or
GameModel_<n> field anywhere on the disc:

                 declared   not declared
  used              172          58
  not used          318           -

490 declared, 230 used. Top binders: Effect_Paralyze 2874, ShellModel 996,
HitFxModel 738, JetFxModel_000 408, MuzzleFlashFxModel_Loop 384.

The 58-cell is almost one field. 53 of the 58 are bound by SilhouetteModel
alone and are all rou_f###_wep* names - the arsenal item silhouettes already
documented in arsenal-item-weapon-chain. They are undeclared because they are
the wrong KIND: each resolves as its own standalone package, rou_f001_wep_01.xpr
and friends, never as a LOD-table entry. Nothing is missing; the sweep was
reading an asset-file name as though it were an effect name. That leaves four
genuinely undeclared effects - eff_e0044, eff_f0002, eff_f0002_barn, eff_h308 -
none of which resolves as a record or field name either.

One "effect name" in that cell is not a name at all. Bound by Effect_Explosion
and Effect_Flare, 72 occurrences = 12 users, its bytes are 95 B6 8E 9A 97 F1 -
Shift-JIS for the word "character string". A developer's placeholder. This
exposes a reader defect worth fixing before these strings reach a port:
unitgroup.py hands the value back as Latin-1 mojibake, so an IDXD string field
can carry Shift-JIS and our decode does not know it.

Following the silhouettes into the ISO tree turned up a convention that IS real:
59 of the 166 .xpr packages end _hangar.xpr, and 59 of 59 have a bare twin of
the same stem. The direct contrast to yesterday's refutation, where _all/_child
was 1 of 166 with a single stem. Suffix conventions in this corpus are worth
testing precisely because they are not all real.

Controls reproduced from the previous iteration: eff_n0071 is declared and used
6x (= one user under the per-pak-copy rule); eff_e0033 is declared and used 0x,
sitting in the 318-cell. That cell is expected rather than alarming - the
EnumLODSet/EnumGameModel family is overwhelmingly equipment, which no unit
datasheet binds.

All fifteen artefacts byte-identical.
2026-08-28 08:05:28 +00:00
Sylpheed RE agent
25b5f60ea1 re: _all/_child is not a convention (1 of 166); eff_n0071 is a live explosion
Refuted, and it was my own suggestion from last turn. Across all 166 .xpr
packages the _child suffix occurs in ONE file, Stage_S24.xpr, with exactly one
stem, stg24_04, 20 occurrences. Across the 105394 distinct parsed pak names -
record names, field names and string values - names ending _all or _child occur
ZERO times. So the pair is mesh-internal to a single model and never reaches the
data tables; the tables only ever name the bare stg24_04. n = 1 is not a
convention, and the multi-part mesh mechanism the corpus actually owns is
xbg7-mesh's grouped index/vertex pools, which is a different thing.

The orphan tables' five unique effects split three ways, chased through PARSED
names rather than .xpr bytes:

  eff_n0071                  LIVE - Generic.GameResourceID in DefTables.pak and
                             an Explosion record's ExplosionFxModel in all six
                             GP_MAIN_GAME_* archives (x6 = one user)
  eff_e0033                  a mesh in Base.xpr, but no data reference
  eff_e0058/_e0059/_e0060    neither a mesh nor any reference

So one "abandoned" entry is not abandoned: the orphan LOD tables list an effect
the shipped _test table omits while the game still binds it elsewhere.

Control recorded so the negative has force: effect names bind through a whole
family of fields - Effect_Paralyze (2874 values), HitFxModel (738), ShellModel
(516), JetFxModel_000/001/002, MuzzleFlashFxModel_Loop - so a value that
resolves through none of them really is unreferenced, not merely missed by a
narrow search.

All fifteen artefacts byte-identical.
2026-08-28 07:47:06 +00:00
Sylpheed RE agent
da41f95db1 re: the challenge LOD extras are meshes in that stage's own .xpr - 11 of 11
Perfect diagonal. Each of stg24_01..04, stg26_01..03, stg27_01..02 and
stg29_01..02 appears in Stage_S<NN>.xpr and in no other stage package. So a
challenge stage's bespoke LOD/model table exists to declare the handful of
scenery meshes packaged with that stage, consistent with xbg7-mesh's account of
a stage .xpr as a bag of XBG7 resources.

stg24_04 is a composite: 22 occurrences resolve as 1 bare + 1 _all + 20 _child.
Every other stg* name occurs exactly once.

rou_f004 is NOT in Stage_S28.xpr - it is in DeltaSaber_A.xpr. S28's LOD table
declares the craft but the mesh ships in the player-craft package, confirming
from the ISO side that _A is the f004 variant: the stage package carries
scenery, the craft comes from its own file. rot_n001_break resolves too, in
Tutorial.xpr and Stage_S28.xpr, matching exactly the tables that list it.

The orphan tables' unique effects only partly resolve: eff_e0033 is present in
Base.xpr, but eff_e0058 and eff_n0071 appear in NO .xpr on the disc. So the six
abandoned LOD tables name at least two effects that were never packaged - the
same "declared, never shipped" shape as S14's asteroid meshes, on a much smaller
scale.

Method caveats recorded in the doc: these are byte searches over .xpr files, not
parsed resource-table reads. They establish presence, not an enumeration, and
are acceptable only because the names are long and distinctive and the match was
expanded to the full trailing token. Also grep -c counts LINES, not occurrences;
the counts above come from grep -o | wc -l after that was caught.

All fifteen artefacts byte-identical.
2026-08-28 07:30:37 +00:00
Sylpheed RE agent
d08e464897 re: challenge LOD tables are _test + set dressing; orphan count corrected 8 -> 6
Diffed the field sets, the same move that cracked EnumWeapon_EX5.

The five bespoke challenge tables are EXACT SUPERSETS of _test - 10 of 10 (five
stages x the EnumLODSet and EnumGameModel families) with zero removals. Extras
run +2 to +9 and name the stage's own scenery: stg24_01..04, stg26_01..03,
stg27_01..02, stg29_01..02, plus rot_n001_break shared by S24, S26, S27, S28 and
Tutorial.

S28's nine extras independently confirm the player-craft result:
LOD_Frame_rou_f004, four rou_f004_mnv*/turn180 poses and three eff_j004_*
effects - the LOD table for the one stage that flies rou_f004. Two unrelated
tables agreeing on S28's oddity is a real cross-check.

This does NOT revive "rot_n001 is on the disc": that refutation concerned the
bare name, which still occurs 0 times exactly; only the _break form is real,
exactly as recorded.

Self-correction: 6 orphan tables, not 8. name_hash is case-INSENSITIVE, so _test
and _Test are the same pak entry - EnumLODSet hashes to 3485720498 and
EnumGameModel to 4020329537 for both spellings. The family therefore has 10
distinct keys, not 11 tags, and the unreferenced set is S01, S02, S16 x 2 = 6.

And the six are not stale copies of _test. They are far smaller (120, 129 and
178 EnumLODSet fields against _test's 676) yet each carries 4-5 entries _test
does NOT have: LOD_Effect_eff_e0033, _e0058, _e0059, _e0060, _n0071, and S16
adds five rou_e106_* boss parts matching stage16-boss. So the live table is not
a superset of the abandoned ones. Tutorial has the same shape: 66 fields, 3
unique.

All fifteen artefacts byte-identical.
2026-08-28 07:15:28 +00:00
Sylpheed RE agent
0536e287b5 re: EnumLODSet_test.tbl is the story campaign's live LOD table; S25 premise withdrawn
The question was why S25 is absent from the DefTables +-path families. The
premise is withdrawn: it was an artifact of listing DISTINCT values of
LodEnumTable. S25 is not missing anything - it shares EnumLODSet_test.tbl with
the sixteen story stages, and test is in the list. Counting distinct values
hides who uses them; join per user.

The per-stage join: EnumLODSet_test.tbl / EnumGameModel_test.tbl serve 17
stages - all of S01-S16 plus S25. The five challenge stages S24, S26, S27, S28,
S29 have bespoke tables. Hangar.xpr has no pair at all. 17 + 5 + 1 = 23 objects
carrying a StageResource.

So the test-named table is the live LOD and model table for the whole story
campaign - the strongest case yet of "a test_-named table can be live", serving
17 of the 22 missions.

Eight shipped tables that nothing references: the family covers only 11 tags
(S01, S02, S16, S24, S26-S29, Tutorial, test, Test) and is NOT per stage -
S03-S15, S17-S23 and S25 have none. Seven tags are referenced; S01, S02, S16 and
Test are present in DefTables.pak with zero references disc-wide, i.e. 8 orphan
tables. "Early per-stage LOD sets, abandoned when the campaign was pointed at
the shared test table" is a reading, not a measurement.

Scope note recorded so the family is not mistaken for a stage one:
EnumLODSet/EnumGameModel is overwhelmingly an EQUIPMENT family - 116 of the 130
referenced values are bare Equip_EnumGameModel_wep_NN.tbl names reached from
PlayerLOD, WingmanLOD, PlayerMotion and WingmanMotion (58 distinct each); only
the 14 stage-tagged values use the + path form.

All fifteen artefacts byte-identical.
2026-08-28 07:00:34 +00:00
Sylpheed RE agent
9be52cb2e9 re: the archive.pak+entry dictionary is 20 keys and misses the 2D blocker entirely
A path that names its own archive is a free name->key fact, since a pak TOC holds
only hashes. Swept them disc-wide and aimed the result at the 2D /
GP_READY_ROOM blocker.

Seven fields carry a game: value - BackGroundPackage 18, StageResourcePackage 23,
TBL_3D 6, LodEnumTable 7, MotionEnumTable 7, one PATH, one anonymous - giving 63
distinct paths in two forms: 42 file-form (41 present on the extracted ISO, the
single miss being the bare directory game:\hidden\Resource3D\, which is not a
file) and 21 archive+entry (20 of 20 resolve inside the archive they name; the
21st is sound.pak+ with an empty entry).

The blocker test is a measured zero. The six 2D paks and GP_READY_ROOM hold 1817
distinct keys between them and NONE is named by a + path. The dictionary's total
reach is 20 of 16630 keys, 0.12%, and every one lands in DefTables or
GP_HANGAR_ARSENAL - archives that are already partly named.

The route is exhausted rather than unlucky: these paths exist only in the seven
fields above, and none of those fields ever addresses a 2D or ready-room asset.
The blocker keeps its earlier structural reason (six identical 711-key TOCs, 0 of
711 shared with any other archive).

The full dictionary is recorded: 6 x <lang>\GP_HANGAR_ARSENAL_3D.tbl in
GP_HANGAR_ARSENAL.pak, and EnumGameModel_* plus EnumLODSet_* for S24, S26, S27,
S28, S29, Tutorial and test in DefTables.pak.

Left unexplained: both DefTables families are seven names with S25 absent and no
story stage present at all.

All fifteen artefacts byte-identical.
2026-08-28 06:43:52 +00:00
Sylpheed RE agent
af8292c940 re: the rest of StageResource - two more value kinds, one convention refuted
Swept all 19 StageResource fields by value kind.

Refuted: <X>ID + <X>Package is not a convention. The record has exactly one *ID
field (BackGroundID) and exactly one pairing stem. StageResourcePackage has no
StageResourceID; WingmanIconID_0..3 have no package. n = 1 is not a rule, so
last pass's generalisation is withdrawn.

Fourth kind - the game:\ external path. BackGroundPackage (18 values) and
StageResourcePackage (23) are neither records nor pak keys because they are ISO
file paths. Mapping game:\ to the extract root and \ to /, case-folded, 41 of 41
exist on the extracted disc (hidden/resource3d/BG_Acheron.xpr, Stage_S01.xpr,
...; 166 .xpr files in that directory).

Fifth form - game:\hidden\<archive>.pak+<entry>. LodEnumTable, MotionEnumTable
and TBL_3D name an archive AND an entry inside it, e.g.
game:\hidden\DefTables.pak+EnumLODSet_S24.tbl. Splitting on + and hashing the
right half against THAT archive's keys resolves 20 of 21; the one failure is
sound.pak+ with an empty entry name.

Some of those entry names carry an explicit <lang>\ prefix
(deu\GP_HANGAR_ARSENAL_3D.tbl, eng\, esp\), each present in 0 other archives, so
the language-prefix form is genuinely used by the engine. This does NOT unblock
the 2D naming problem: archive-naming refuted <lang>\ there structurally, since
the six 2D paks share one identical 711-key TOC and cannot carry per-language
names. Both facts stand.

The 4 never-shipped _Test tables - the residual of the 344-name census - are now
named: EnumCharacter_Test.tbl, EnumLocalString_Test.tbl, EnumSquadron_Test.tbl,
StageMessageSet_test.tbl (lower-case test on the last).

WingmanIconID_1..3 hold PGHUD_WING2/WING3/WING4, HUD element names from the PG*
roster; WingmanIconID_0 is never valued.

All fifteen artefacts byte-identical.
2026-08-28 06:28:14 +00:00
Sylpheed RE agent
3da617d04b re: BackGroundID is an identity, not a file name; its sibling names the asset
The one value class that resolved to neither a record nor a file resolves to
neither because it is neither. BackGroundID lives on StageResource (174 records
carry it) directly beside BackGroundPackage, and that sibling holds the real
asset: game:\hidden\Resource3D\BG_<something>.xpr.

My earlier suffix sweep had a real bug: it tried BG_ and .xpr as alternatives,
never as a pair, so BG_Acheron.xpr was never in the search space. A
one-transform-at-a-time sweep cannot find a two-transform name.

But the obvious follow-on hypothesis - "the package is BG_<id>.xpr" - is also
refuted: it holds in only 12 of the 24 StageResource records carrying both
fields. Hargenteen spans six packages (BG_Hargenteen{,_near,_far,_dead}.xpr plus
BG_Acheron.xpr and BG_Stg26.xpr), and BG_Acheron.xpr serves both the Acheron id
(S09) and the Hargenteen id (S25). The relation is many-to-many in both
directions, roughly 18 distinct packages across 10 ids.

So the id is a logical place - Acheron, Earth, Hargenteen, Lebendorf - and the
package is the mesh actually loaded, chosen per stage from that place's _near /
_far / _dead variants. Two details fall out: PD is the Prometheus Driver
(BG_P_Driver.xpr, on S16, the boss stage), and the challenge stages that
re-dress a story sky carry bespoke BG_Stg24/26/27.xpr packages while keeping the
story stage's id - the same borrow-and-re-dress pattern already recorded for
maps and objectives.

Not settled: Original carries no BackGroundPackage at all; whether that means no
sky or an engine default is unknown.

All fifteen artefacts byte-identical.
2026-08-28 06:11:41 +00:00
Sylpheed RE agent
aa60d14ae7 re: value classes resolve by KIND; the 344-name residual drops from 31 to 4
Generalised the "does the referent exist?" sweep that cracked S14. The value
classes resolve as three different kinds:

  AIID                   31 values   31 records    0 files    0 unresolved
  FormationID           126 values  126 records    0 files    0 unresolved
  CollisionMeshes        18 values    0 records   18 files    0 unresolved
  EnumerateSubobjective  17 values    0 records   17 files    0 unresolved
  NamePlate              52 values   27 records   24 files    1 (empty string)
  BackGroundID           10 values    0 records    0 files   10 unresolved

FormationID 126/126 re-derives unit-group-table's own "every FormationID
resolves" from a different direction, as the control.

The 27 NamePlate values that stage-definition-table called "non-file plate ids"
are record names - ACROPOLIS x114, NP_ArrowHead x84, NP_ASFrigate x78 - and the
split 24 files + 27 records + 1 empty = 52 matches the two-vocabularies count
already recorded there. So the census residual shrinks: 313 resolve as files
plus 27 as records = 340 of 344, leaving the 4 never-shipped _Test tables as the
entire residual. The old reading was a KIND error, not a missing-file error.

BackGroundID is a genuinely unresolved third kind: the ten sky names, used 6-36
times each, are neither record names nor pak keys under 10 prefixes x 9 suffix
forms.

Recorded as powerless so it is not re-run: the image holds 0 exact strings for
all ten backgrounds, but also 0 for Formation_4_Bird, AI_Structure,
NP_ArrowHead, ACROPOLIS and CollisionSet_S03.bin, every one of which does
resolve in data. The executable names no data value at all, so the negative
discriminates nothing.

All fifteen artefacts byte-identical.
2026-08-28 05:55:52 +00:00
Sylpheed RE agent
5ec24b8ef9 re: S14's 13 are dangling deployments - the unit chain is absent for them
Two measurements settle the last open cell of the unit partition.

The deployment side is identical. S14's asteroid-cmesh squadron records match a
declaring stage's field for field - same squadron ids (GNN901, GNN902, ...) and
the same Count 1 / SideID Neutral / AIID AI_Structure / FormationID
Formation_1_only / DisableInterval No. Nothing about how S14 deploys them
differs, and these are the same kind of record compared like for like.

The definition side differs completely. Per stage, deployed / declared /
Generic-with-HP / any Generic: S01 8/8/8/8, S04 10/10/10/10, S05 5/5/5/5,
S08 6/6/6/6, S13 4/4/4/4 - 33/33/33 for the five - and S14 p1 9/0/0/0,
S14 p2 4/0/0/0.

So the 13 are not a manifest omission: they have no unit definition anywhere on
the disc, not even an un-HP'd Generic row. UnitGroup_S14.tbl names entities that
do not exist - 13 dangling references. That rescues the law (nothing is
deployed-without-being-loaded in any working sense) and explains why the gap is
exactly the cmesh class and nothing else in S14.

What it predicts needs the emulator: either those collision meshes fail to
appear in Stage 14 - a shipped bug - or another route supplies them. Flying S14
and checking whether the large asteroids collide distinguishes the two; no
static route can. Recorded as NEEDS-HUMAN.

Also recorded so the two asteroid populations are not conflated: asteroid-fields'
AsteroidGroup_00N tables scatter mob_n0NN models, while these are
UN_<stage>_Asteroid_cmesh_* squadron entities. S28.Phase_1 borrows
S14_p2_asteroid.tbl but deploys no cmesh of its own, so the borrow does not
carry the dangling references.

All fifteen artefacts byte-identical.
2026-08-28 05:39:44 +00:00
Sylpheed RE agent
f0dfff4fcd re: EnumUnit_S<NN> is the stage's LOAD MANIFEST; all 9 never-deployed units named
Joining each stage's own EnumerateUnit table to its own EnumerateSquadron table
is far sharper than the global partition: declared == deployed EXACTLY in 20 of
22 stages. S16 declares 4 it never deploys; S14 deploys 13 it never declares.
Every per-stage deployed-but-undeclared count is 0 except S14's 13, and that 13
is the entire disc-wide total - so the exception is one stage, not a scattering.

S16 explains what the table is. It declares 6 and deploys 2, and two of the six
are UN_e901_ADAN_Boss and UN_e910_core_ADAN_GeneratorCore - exactly the units
stage16-boss shows are brought in by their own loader chain (sub_82315AE8),
not by the squadron roster. So EnumUnit_S<NN> is the stage's load manifest, not
a mirror of the roster: a unit that code spawns is declared but never deployed.

That also closes the 9 declared-but-never-deployed units by name: the S16
code-spawned pair above, Test_ADAN_PrometheusDriver_InsideP2_01, and six
Ship_Test* placeholders.

S14 remains the one unexplained cell, and it is not a missing table:
EnumUnit_S14.tbl exists and declares six names, one of which is
UN_mn040_Asteroid_Big. S14 therefore declares an asteroid but not the 13
Asteroid_cmesh entities it deploys, which refutes "asteroids are exempt from the
manifest". Whatever the cause, it is specific to the collision-mesh entities.

All fifteen artefacts byte-identical.
2026-08-28 05:24:09 +00:00
Sylpheed RE agent
0ae295e9eb re: the weapon law does not generalise to units; the whole exception is S14
Applied the declared x deployed partition to units. Three vocabularies had to
be separated first: declared = field names of the 31 EnumUnit records (118);
deployed = slot 0 of each UnitGroup member tuple (122); datasheet = Generic
records carrying HP (114, which re-derives unit-datasheet-static's own number
from a different direction, as a control). Declared and deployed share the UN_*
vocabulary and overlap on 109; the datasheet's IDs are a third naming, disjoint
from both.

                    deployed   not deployed
  declared            109           9
  not declared         13           1

The cell that is empty for weapons (0 mounted-but-undeclared of 131) holds 13
for units, so the general form of that law is refuted: a unit CAN be deployed
with no table declaring it.

But the 13 are one coherent family - every one is UN_S14_p1_Asteroid_cmesh_* or
UN_S14_p2_Asteroid_cmesh_*. Asking the other population settles it: S01 8/8,
S04 10/10, S05 5/5, S08 6/6, S13 4/4 declared - 33 of 33 - while S14 declares
0 of 13, and non-asteroid deployed-but-undeclared is 0. The law survives in a
narrower form: every deployed unit is declared, except S14's asteroid collision
meshes.

Why S14 differs is not settled. It is the stage whose asteroid table S28
borrows, but nothing measured connects the borrow to the missing declarations,
so that is recorded as a coincidence rather than a cause.

All fifteen artefacts byte-identical.
2026-08-28 05:07:35 +00:00