cddf1deedb602ddc7bedc1210f03ea477d1ca0f5
589 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cddf1deedb |
re: the sound-cue table, and cutscene dialogue's speaker/portrait/timing
dat/tables.pak holds a 5798-entry SOUNDS record (cue name -> sound id) and a 5135-entry FILES record (.slb bank paths). Cue names are the join key, so a script message id now resolves all the way to the bank that voices it: MSG_VOICE_D_257 -> VOICE_D_257 -> 6945 -> jpn\etc\VOICE_D_257.slb. The prefix rule is MSG_ -> VOICE_, not strip-MSG_. My first rule was the latter; it left 88 names unresolved and I was about to write those families up as text-only announcements, until VOICE_TCAF_592.slb turned up in FILES and refuted it. Corrected rule resolves 1326 of 1338, and SOUNDS and FILES agree on exactly the same 12 absentees. Separately, MSG_DEMO_* is driven by its own IDXD tables in the language packs, which carry speaker, portrait, on-screen seconds and audio cue per page. Field count is 9*PageCount+2 for all 7 distinct PageCounts; 1252/1252 caption-key slots match <ID>_<page>_<line>; the 78 multi-page records equal the 78 counted independently from the caption side; 138 ids close exactly against the caption table both ways. Does not settle the known VOICE_D_452 wrong-recording case -- every cue id is distinct, so bank sharing is not happening at this layer. |
||
|
|
f400f4e973 | re: backlog no longer lists multi-page captions as open | ||
|
|
66c3551a95 |
re: caption pages are utterances, and the dialogue tool was truncating 356 names
Measured the <id>_<page>_<line> key structure: a page is one subtitle box of 3 or 4 wrapped lines, and successive pages are successive utterances by possibly different speakers. 452 of 4091 ids span more than one page, up to 8. That refutes the isl_dialogue.py committed two commits ago, which read only page 000 -- 356 of the 1338 script message names are multi-page, so a quarter of its output was truncated to the opening utterance. Tool now walks pages until one is empty; Stage 02 sample regenerated (43 of 213 calls multi-page). The 2683/2683 resolution figure is unaffected: it counted ids that have text, and every id does have a page 000. What was wrong was the rendered text. |
||
|
|
ed7c147041 | re: index + backlog rows for the script-dialogue link | ||
|
|
f611e0b618 |
re: mission scripts are readable as dialogue (2683/2683 message calls resolve)
Built-in 64's slot-0 operand is a symbol-table-1 type-6 message id, and every one of them now has caption text: 2683 of 2683 call sites across the 28 stage scripts, 1338 distinct names, no residue of any kind. This only became reachable once build_caption_text was switched to the IXUD field table (537 -> 8800 lines); before that most of these names had nothing to resolve to. Adds isl_dialogue.py plus a committed Stage 02 sample. Does not settle which recording plays for a given line, multi-page captions, or the other five languages. |
||
|
|
ff09b8a8b4 |
re: INDEX row for IXUD predated all of today's IXUD work
The row still described ixud.rs as a cue reader and said nothing about the record table or the caption families. Rewritten with the measured numbers: * the IXUD record/field table is decoded and wired in -- IxudObject mirrors IdxdObject, uniform 16-byte records, 12-byte fields, every offset in CHARS, and the word at 0x08 is record 0's hash rather than a schema id. Verified 1104/1104 objects, 1476/1476 records, 628165/628165 named fields. * caption text goes 537 -> 8800 lines, which is 8800 of 8800 distinct keys, in two steps: generalising the key parser to all eight families took it to 8074, and switching from token adjacency to record fields finished it. The row also carries my correction rather than quietly dropping it: the earlier "1.3% of the game's text" counted occurrences across blocks, and the honest denominator is 8800 distinct keys, so the starting point was 6.1%. And it keeps the DEMO control, which is the part a reader should remember: token adjacency finds 537 lines in that family, fields find 541. The old reader was dropping lines in the one family it was written for. Status moved from 🟡/✅ to ✅. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
ff83918572 |
re: wire the IXUD record table into the crate — captions go 537 to 8800 of 8800
ixud.rs now has an IdxdObject-shaped reader, IxudObject, and build_caption_text reads captions as FIELDS instead of pairing them with whatever token follows in the pool. build_demo_text token adjacency 134 ids 537 lines build_caption_text token adjacency 3721 8074 build_caption_text record fields 4085 8800 = all of them Verified over the whole disc by tests/ixud_records_disc.rs: 1104/1104 objects parse, 1476/1476 records and 628165/628165 named fields reproduce their ixud_hash, 48 positional, zero failures. The header word at 0x08 is record 0's hash, asserted per object -- there is no schema field, exactly as for IDXD. The module doc described a 12-byte record directory and a "schema/type hash"; both were wrong and are corrected. I also have to correct my own number from the previous commit. "1.3% of the game's text" counted OCCURRENCES: each family lives in 24-45 IXUD blocks and the same key repeats across them. Distinct text-bearing MSG_* keys number 8800, not 44579, and every one has the <id>_<page>_<line> shape. So the real coverage was 537/8800 = 6.1%, and I overstated the gap about fivefold. Direction right, magnitude wrong. The DEMO control is the sharpest evidence for the change: token adjacency finds 537 lines there, the field reader 541. It was dropping lines even in the one family it was written for -- which is why the test now asserts "must not lose lines" rather than "must be identical". Same lesson twice in one session: pool adjacency is a consequence of how records are written, not a rule of the format. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
914e6651cd |
re: read all eight caption families — 15x more text, and the same lesson twice
build_caption_text generalises the key parser from MSG_DEMO_* to all eight
families. The shapes are uniform and each family is 100% consistent with its
own: seven use MSG_<FAM>_<id>_<page>_<line>, and VOICE alone inserts a family
letter before the id.
ids lines
build_demo_text 134 537
build_caption_text 3721 8074
The DEMO family comes out identical through both readers -- 537 lines either
way -- which is the control that generalising changed nothing that already
worked. Pinned by tests/caption_families_disc.rs, along with VOICE ids keeping
their family letter.
But this does NOT close the gap, and the write-up says so: 8074 against the
44579 text-bearing fields the record-level scan counts is about 18%.
The reason is the same lesson this session already learned once.
build_caption_text pairs a value with the key that happens to follow it in the
raw UTF-16 token stream -- the adjacency heuristic that was wrong for IDXD and
is wrong here for the same reason. ixud.rs has no record/field reader at all.
The IXUD record table IS decoded and verified disc-wide (1104/1104 objects,
628165/628165 fields reproducing their key) and was simply never wired into
the crate.
Next step recorded: give ixud.rs an IdxdObject-shaped reader and read captions
as fields rather than adjacent tokens. The decode exists; only the plumbing is
missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
8d0d5f81da |
re: the crate reads 1.3% of the game's text — seven caption families unread
movie_subtitle handles MSG_DEMO_*, the cutscene captions. Counting every IXUD block in GP_MAIN_GAME_E.pak, that is the SMALLEST of eight families: MSG_ADAN 23236 keys 9801 with text ADAN combat chatter MSG_RHIN 21196 8509 Rhino squadron MSG_TCAF 17148 6728 TCAF MSG_VOICE 13060 6776 in-mission scripted dialogue MSG_BIRD 14036 5834 Bird squadron MSG_ADPL 12640 4127 ADAN pilots MSG_ACRO 4804 2244 Acropolis MSG_DEMO 1252 560 cutscene captions <- the only one read total 107372 44579 560 of 44579 text-bearing keys = 1.3%. I report the text-bearing column rather than raw keys because only 41.5% of keys carry text -- the rest are the empty line slots this container pads with, and counting those would flatter the denominator. MSG_VOICE_* is the family the message tables reference -- the dialogue whose voice bindings this file now analyses in detail -- and nothing in crates/ parses it. So the corpus knows which bank plays for a line it cannot read. First step recorded: build_demo_text already pairs a text value with the MSG_DEMO_<demo>_<page>_<line> key that follows it, and the other seven families use the same <id>_<page>_<line> shape, so generalising the key parser is most of the work. With a warning attached: do NOT assume the id spaces relate, since the voice-bank id and the caption id within one message page are different numbers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
66f31d5486 |
re: the genuinely-cut voice lines still have subtitles — and two id spaces differ
The 60 tokens absent from BOTH voice languages, itemised:
45 VOICE_E_ family -- 44 numeric [0..43] plus the lettered VOICE_E_012B
13 VOICE_C_ at 421, 423-426, 430, 432, 447-450, 470, 471 -- INSIDE the
listed range [0..489], so interior gaps rather than a truncated tail
2 VOICE_D_182 and _183, adjacent
These lines were written and captioned; only the audio is missing. Their
caption keys resolve to real text in the IXUD blocks:
MSG_VOICE_C_355_000_00 "What are you doing? Quit wasting..."
MSG_VOICE_C_367_000_00 "The final defense weapon is..."
MSG_VOICE_C_347_000_00 (Japanese)
MSG_VOICE_D_152_000_00 (Japanese)
MSG_VOICE_E_044_000_00 (Japanese)
Three of the five sampled are still Japanese INSIDE the English pak --
captioned but never translated, matching the untranslated entries already
noted for the localised-text container.
Separately, a trap worth its own heading: within one message page the voice
bank token and the caption keys use DIFFERENT numbering.
Message_106 voice VOICE_C_468 lines MSG_VOICE_C_385_000_00..02
Message_129 voice VOICE_D_182 lines MSG_VOICE_D_152_000_00..02
Message_044 voice VOICE_E_012B ID MSG_VOICE_E_044
Same family letter, different index space. Deriving one id from the other
will silently mis-pair audio with text -- which is the same class of mistake
as the demo-id voice binding this corpus already had to reject in-game.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
a3fd51b380 |
re: most of the 268 missing voice banks are UNDUBBED, not cut — I had it wrong
Last iteration I read the scattered missing runs as cut dialogue. Checking the
other languages refutes that for most of them.
Only two voice languages exist: tables.pak holds eng\sounds.tbl (4418 banks)
and jpn\sounds.tbl (5136), and NO fra/deu/esp/ita. The other four
localisations are subtitle-only, which is itself a fact the corpus did not
record.
The English set is a strict subset of the Japanese one:
eng jpn
listed 4418 5136
resolved 4175 4383
missing 268 60
lacked by
the other 0 718
Every id missing in Japanese is also missing in English, and NONE is missing
in Japanese but present in English. So the 268 split cleanly:
208 exist in Japanese, not in English -- recorded in the original and
never dubbed. A localisation gap, not cut content.
60 absent from both -- genuinely cut, including all 44 VOICE_E_ (listed
0 times in either language) plus 16 others.
Per family eng/jpn: A 683/771, B 383/423, C 366/474, D 387/546. The 718
Japanese-only banks include whole families English never had -- VOICE_ADAN_
(110), VOICE_ZZZZ_ (104), VOICE_TCAF_ (41), VOICE_RHIN_ (27).
So VOICE_E_ survives as the one genuinely-cut family, and my "short runs of
cut dialogue" reading was wrong: those lines are on the disc, in Japanese. The
previous text is kept above the correction.
This is why the "measured on English only" limit was worth writing down rather
than quietly generalising -- the second language changed the conclusion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
566953b4f2 |
re: 268 voice references name a bank that does not exist — one whole family
Following up the incidental gap from the DEMO_* work. Resolving every VOICE_* token the way the engine does -- token, then eng\sounds.tbl for its directory, then sound.pak -- gives 4175 resolved and 268 unresolved of 4443 distinct. The gap is in the manifest, not the archive: the unresolved tokens are not listed in sounds.tbl at all, and of the 4175 that ARE listed, every one is present in the pak. Nothing is listed-then-missing. By family, script-used ids against sounds.tbl-listed ids: VOICE_A_ 696 [0..772] listed 683 missing 14 VOICE_B_ 391 [0..422] listed 383 missing 8 VOICE_C_ 429 [0..489] listed 366 missing 63 VOICE_D_ 508 [0..598] listed 387 missing 126 VOICE_E_ 44 [0..43] listed 0 missing 44 <- the whole family Two different shapes, and the distinction matters. A/B/C/D lose short runs scattered through an otherwise dense range -- 11,12,13 / 56,57 / 181,182,183 / 389-392 -- which is what cut dialogue looks like. VOICE_E_ is absent entirely: 44 ids used, none listed, no VOICE_E_* anywhere in the manifest. A whole speaker or category, cut. For the port: a missing voice bank is a RETAIL condition, not a decode bug. A reimplementation must tolerate an unresolvable VOICE_* reference, the same way it must tolerate SUBTITLE_S12B.tbl resolving in no language. Limit stated: measured on GP_MAIN_GAME_E.pak and eng\sounds.tbl only; whether the other five languages drop the same ids is untested. An earlier probe of mine checked only three sound.pak directories and would have mis-attributed this; sounds.tbl lists five, including Briefing. Resolving through the manifest rather than guessing directories is what makes the 268 trustworthy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
64f54b82ef |
re: DEMO_* in a message's voice slot is not a sound bank
An open question from the game_data migration: 132 dialogue lines carry an
unprefixed DEMO_* token where a VOICE_* one is expected, and whether DEMO_*
names a bank was untested. It does not.
VOICE_* 4443 distinct, 4175 resolve in sound.pak
(2360 eng\Voice\, 1815 eng\etc\)
DEMO_* 296 distinct, 0 resolve
Zero resolve under any of the three directories, and looking them up through
the convention -- VOICE_DEMO_nnn -- resolves 0 of 296 as well. So the slot is
not "the voice bank"; it can hold either a bank reference or a demo one.
The tokens run DEMO_000 to DEMO_310, 287 distinct in [0, 310] -- a dense
sequential id space, which is the shape of the demo id this file already uses
in MSG_DEMO_<demo>_<page>_<line> caption keys. I did NOT confirm the overlap
and say so: those keys live in IXUD wide-string blocks while my scan walked
IDXD objects only, so it found 0 of them. That needs an IXUD-aware sweep.
Incidental and left open: 268 of the 4443 VOICE_* tokens do not resolve
either. That is a separate gap and the DEMO_* finding does not explain it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
2d3a0948a6 |
re: the stage-02 "defaulted field" capture is wrong for 99 of 101 fields
The last file still resting on the "defaulted on disc" premise. Re-checked through the record table over every unit-like object on the disc -- 114 with a Generic.Type of Craft or Vessel: present in ALL 114 objects 50 present in some 49 present in NONE 2 <- FiringRange, SELength So only FiringRange and SELength are genuinely absent, out of 101 listed. FiringRange was already recorded correctly elsewhere (runtime 0 is the accessor's miss value); SELength joins it. One pattern worth keeping rather than dismissing: 15 of the fields sit in exactly 5 objects each -- AB_* (afterburner), PowerCut*, AA_/AV_AxisMode_*. That is the Delta Saber family, so those fields are player-craft-only rather than defaulted, which is a different fact than either the file or my correction to it would suggest on its own. The runtime values themselves stay -- they are real measurements. What is withdrawn is the claim that the disc does not carry the fields. That completes the sweep: weapon-datasheet-runtime.md, unit-struct-runtime.md, live-unit-definitions.md, INDEX.md and now this capture all carried the same wrong premise from the same blind reader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
4d4b3a2e5b |
re: INDEX still advertised the withdrawn "defaulted on disc" framing
The two headline rows for weapons and units were the last place still telling a reader that these values are not on the disc. Both corrected in place, with numbers I measured rather than adopted. Weapons: re-reading captures/weapon-runtime-fields.csv through the record table, 1514 of the 4393 rows labelled defaulted-on-disc DO have a value on disc; 2879 are genuinely absent. I state that as an upper bound -- my matcher counts a field when it appears in ANY record of the object, and a per-record count gives ~1448. Either way the headline "4393 values the disc does not carry" is wrong by about a third. The spot checks are exact rather than aggregate: wep_05/wep_60 TriggerShotCount 4, wep_02 Power 100.0, wep_60 Power 1000.0 (refuting the recorded "C band 150-500" bracket), wep_25 MaximumRange 4000.0, wep_11/28/36/70 LoadingCount 6/5/5/0. Units: the ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the Generic / Shield / Mass / SE records -- which is why a reader that could not name a record saw them as absent. And "18 of 23 vessel records are missing at least one of Size_X/Y/Z/HP" is false: 0 of 114 objects with a Generic.Type (43 Craft + 71 Vessel) miss any of them. Status markers moved from ✅ and ✅/🟡 to ✅/❌ so the rows no longer read as settled-and-correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
d04fedb101 |
re: the unit "defaulted on disc" tables are wrong too — corrected
unit-struct-runtime.md lists ~30 player-craft fields as defaulted on disc with values "recovered from the running game". All of them are on the disc at exactly those values. Verified myself for UN_f001_TCAF_DeltaSaber_T: Size_Radius 10.0, FCSRange 500000.0, ResistanceToOptics/HQRatio/ ShieldRatio/ThrusterRatio/ResistanceToShell/ResistanceToExplosion 1.0, MassScore 0 [Generic] ChargeSpeed 25.0, ChargeDelay_Break 10.0 [Shield] DryMass 100.0 [Mass] LowerHPThresholdRatio 0.3 [SE] The record column is the explanation: the values are spread across Generic, Shield, Mass and SE. That is exactly why the old flat reader missed them -- it could not say which record it meant, so a per-record field read as absent. Also wrong, and measured: "18 of the 23 vessel records are missing at least one of Size_X/Y/Z/HP, nearly always Size_Y". Over every object carrying a Generic.Type -- 43 Craft plus 71 Vessel, 114 objects -- ZERO are missing any of the four. live-unit-definitions.md's "Route B: 13 defaulted fields read out of the running game" goes with it: all 13 Size_Y values are on disc, each exactly equal to the value the runtime run "recovered". Nothing was recovered that the disc did not hold; the run confirms only that the parser reads the disc. As with the weapon sheet, the runtime captures stand as measurements. What is withdrawn is the premise that they were reaching values the disc lacks. Correction banners at the top of both; original text intact beneath. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
ded7fed2a8 |
re: the weapon DATA SHEET's "defaulted on disc" premise is false
Every field weapon-datasheet-runtime.md calls defaulted is on the disc. The record table reads them directly; the old string-pool reader could not see a value whose string is shared or reordered and reported it absent. Verified against the disc myself rather than adopted: wep_05_ASMissile TriggerShotCount (defaulted) -> 4 wep_60_ASMissile TriggerShotCount (defaulted) -> 4 wep_02_Missile Power (defaulted) -> 100.0 [Shell] wep_60_ASMissile Power (defaulted) -> 1000.0 [Shell] wep_25_HBeam MaximumRange (defaulted) -> 4000.0 [Shell] wep_11/28/36/70 LoadingCount (defaulted) -> 6/5/5/0 Two consequences recorded: * The hypothesis "the title-code default for TriggerShotCount is 4" answers a question that does not exist -- both weapons state 4 explicitly. * The bracket "wep_60_ASMissile's defaulted Power sits in the C band (150-500)" is refuted: it is 1000.0, outside the band. That also puts a question over the bucket model or over that row's identification. What is NOT withdrawn: the runtime capture itself. Every number it read matches the disc. What goes is the reason for taking it -- the Gallery panel was not recovering values the disc lacks, it was reading values our parser was missing. Still useful as an oracle, redundant as a source. Correction banner at the top; the original document is left intact beneath it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
217c339fa7 |
re: WITHDRAWN — "FormationID does not hash into the table". It does.
The refutation used the wrong hash function. A record key is tag_hash of the
record's own name, not the pak-TOC name_hash which lowercases and uses a
different modulus:
tag_hash("Formation_4_Bird") = 0x22a5eeed <- exactly the record key
name_hash("Formation_4_Bird") = 0x6286edad <- the wrong hash, hence
the recorded "0 of 16"
Formation_4_Bird IS a record name in FormationSet_S02.tbl and every record in
that table keys by tag_hash of its name. Across all 28 UnitGroup_S<NN>.tbl the
same holds for field keys: 6960 of 6960 are tag_hash(field name), 0 are
name_hash. So FormationID resolves by a direct hash lookup and the roster
indirection is NOT required for it.
This is exactly the trap idxd-container.md warns about, and it cost this file
a spurious refutation. The original text is kept below the correction.
The squadron half of the claim still stands and is marked so: a squadron
record's key is not tag_hash of the squadron id either -- tag_hash("TCN001")
is 0x760cdeaa against keys running 0x659aff47 -- because the record's NAME is
not the squadron id. That roster indirection is real.
Also closes the file's standing "the key derivation stays open, a second hash
function is unidentified": that function is tag_hash, recovered since.
Found while checking a subagent's claim rather than taking it -- it reported
the hash correction, and verifying it turned up that the FormationID half was
a stronger result than reported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
1daf2d6171 |
re: recover the ISL interpreter's command table — 57 opcodes, committed
sub_822FE040 fills 1023 eight-byte slots at table+32 with a default and then overwrites individual ones; slot = (N - 32) / 8 from each std r9, N(r31). Symbolically executing it yields 57 populated slots, matching the count the corpus recorded, now with the full opcode -> handler map committed as docs/re/data/isl-command-table.txt and regenerable from tools/re-capture/isl_cmdtab.py. Nine opcodes point at 0x82391BA8, which is `li r3,1 ; blr` -- accept and discard. 768, 769, 774, 775, 776, 791, 792, 793 and 805 are dead in this build, which is why the built-ins posting them do nothing. Opcodes 800-802's entries are thunks 8 bytes apart into 60-instruction handlers that differ in exactly two words: a descriptor offset and a unit message id. 800 builtin 26 0xED0802DE 801 builtin 28 0xED0803DE 802 builtin 29, 101 0xED0804DE That fixes the id format as 0xED08 nn DE, and the ids known from other work fit it: opcode 514 -> 00DE, 803 -> 07DE, 999 -> 0FDE. Stopped one link short of the semantics, and saying so: the pump's arm for 0xED0802DE does not apply an effect. It walks the unit's child list at [unit+320]/[unit+324] and REBROADCASTS to each child as 0xED0902DE. So 0xED08 is the to-unit family and 0xED09 the to-child one, and the terminal effect is further on. 26/28/29 remain unnamed. The command table is the reusable part -- it answers "what does this opcode reach" for every future built-in question, not just this family. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
781f552bbf |
re: pin the per-record game data — and correct the "SpaceSize is per phase" claim
A disc-gated test that fails if the loaders regress to one flat answer per object. It checks the three structural claims that came with the migration request, and one of them is overstated: * ✅ MainMissionBonus IS per difficulty — 18 of the 24 missions pay three different values, in a strict 1:2:4 easy:normal:hard ratio (all 24); the other 6 pay 0 at every difficulty. The flat reader returned the Easy value. * ✅ RankScore_S/A/B/C/D repeat identically across the three Score_* records in 24 of 24 missions, so the single flat answer happened to be right. The difficulty scaling lives in the earnings (CraftScore_Adjustment, KillBonus_Maximum, …), not in the thresholds. * 🟡 SpaceSize is *stored* per phase (three Phase_N records), but only **1 of the 24** missions varies it — the one that reads 250000/100000/100000. The other 23 repeat a single value (500000 ×1, 100000 ×18, 50000 ×4). "Phase_1 = 250000, Phase_2/3 = 100000" is one mission's numbers, not a rule. Also pinned: the Delta Saber's 63 Turret_NNN mounts at HP 100 each against a TurretCount of 4; the SD-Battleship's per-component HP (bridge 10000, thruster 20000, shield generator 5000 at PowerRatio 0.25, hatch 100 launching Squadron_Test2, anti-ship gun 1500) against a hull of 100000, and its 37 records that define an HP; 418 hardpoints across the 23 capital ships, none missing HP; 131 weapons whose shell id always differs from the launcher id, 40 with a ShellWake, and Weapon_NULL as the only one without a Shell.Power; and fcs_range/shield_ratio/cruising_velocity/maximum_velocity/acceleration/ deceleration now set on 89 of 89 units, all of which the pool reader could miss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
12fe7ceecd |
re: rebuild game_data on the IDXD record table — 966 misses and 596 flattened reads
Measured first, over GP_MAIN_GAME_E.pak, comparing every named-field read the
six struct loaders performed against the record table: 4435 reads, 2872 agreed,
**966 returned None for a field that has a value**, **596 flattened a field that
several records carry**, 1 was wrong (a weapon whose TargetType is the empty
string read back as the neighbouring token "Skip"). The prior report of
4453/2887/974/591/1 is the same picture; the small differences are definitional
(I count a read as flattened only when the records disagree).
Every read now goes through IdxdObject::record, and the types say where a value
comes from:
* Weapon = the `Weapon` record (launcher) + the `Shell` record (projectile).
Both carry an ID and a Name and — with `ShellWake` — an `Interval`, which the
flat reader merged; they are separate fields now. Power/Velocity/ranges/
LifeTime are Shell fields, which is why 427 weapon reads used to miss.
* CraftUnit/Vessel = `Generic` (hull) + `Maneuver` (flight model) +
`StructureCount` (counts) + `Shield`, plus a new `hardpoints: Vec<Hardpoint>`
— one entry per Turret_/Bridge_/Thruster_/Hatch_/ShieldGenerator_ record, each
with its own HP. A flat HP could only ever be one of them.
* PlayerConfig = `Player`, plus `phases: Vec<PlayerPhase>` (SpaceSize/SupplyRange
are per Phase_N) and `score: ByDifficulty<ScoreRules>` (MainMissionBonus is per
Score_<difficulty>; the flat answer was the Easy one).
* Character faces come from the `Faces` record's field names (identical output to
the old token scrape, 0 of 68 objects differ — now by construction).
* Stage = `StageResource` + `phases: Vec<StagePhase>`, and the packages it names.
* The `fields: BTreeMap` on every struct became `records: RecordSet`, which keeps
the record boundary; `RecordSet::everywhere(field)` answers "which record".
The token-scraping loaders move too, and this is where the old reader was worst:
* Arsenal: options are the positional fields of the STANDARD_<slot> records. The
scrape returned 16 nose options of which 8 were field keys and pilot names, and
47 for arm3 of which 38 were junk, while missing Mine_B2A and No_Equipment.
Now 8/12/9/9, all real weapons.
* Squadron: one record per squadron, members are Count*4 positional slots
(unit, message set, n, pilot) — 1160 squadrons with ids and 2295 member tuples,
against 28 idless squadrons and 47 members before. Agrees exactly with the
independent Python decode in docs/re/structures/unit-group-table.md.
* DemoMessage: 11775 lines against 10263, every one with a speaker, a portrait,
a delivery mode and a voice token, from fixed positional slots.
* PilotRoster: assignments are the records the `UNITS` record names, so each one
now carries its unit id, its loadout and the player marker.
* UnitRoster: the roster is the field *names* of the single `EnumUnit` record.
* load_weapons selects on the records (Weapon + Shell) rather than on token[0],
whose first byte is often a stray pool byte ("#Weapon", "%Weapon"). Same 131
objects, no heuristic. GP_HANGAR_ARSENAL.pak holds none of them — the module
doc's claim that player weapons live there was wrong.
schema:: constants keep their names and values but are documented for what they
are: record 0's name hash (PLAYER = Difficulty_Easy, UNIT = Maneuver, VESSEL =
Bridge_000, MESSAGE = Message_000), not a schema id.
Two things the migration exposes and does not fix, flagged in the docs instead:
load_units' bucket is 43 Type=Craft + 46 Type=Vessel objects (new `unit_type`
field lets a caller separate them), and StructureCount.TurretCount is not the
number of Turret_* records (the player's craft says 4 and has 63).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
1972942587 |
chore: drop zz_scratch_measure.rs, swept into 26bb0ec by accident
It is a throwaway measurement harness I wrote in this session ("TEMPORARY
measurement scratch — not for commit") that a concurrent commit picked up
along with its own files. It asserts nothing and duplicates work that is now
covered by tests/game_data_disc.rs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
1e0bcb9cc8 |
re: built-ins 26/28/29 are one family — and damage_unit looks mis-named
Method-diffing settled the structure but not the semantics, so nothing is named this time. 26 damage_unit vt76 sub_8226ACD0 (67) opcode 800 97 sites 28 vt84 sub_82268F98 (69) opcode 801 410 sites 29 vt88 sub_822690B0 (69) opcode 802 164 sites 101 vt276 sub_822691C8 (78) opcode 802 133 sites (broadcast) 28 and 29 differ in two words only -- the opcode (0x21BA vs 0x22BA) and a descriptor pointer 8 bytes apart -- and are otherwise instruction-identical. All three take (unit, double). 26 differs from both by one guard: it rejects only state 3, while 28 and 29 reject states 1 and 3. The operand distributions separate them: 26 97 sites 7 distinct [0, 100] 0 x69, 80 x10, 100 x6 28 410 sites 13 distinct [0, 2000] 200 x116, 120 x76, 300 x74 29 164 sites 6 distinct [0, 100] 0 x64, 100 x53, 50 x38 26 and 29 are percentage-shaped; 28 is an absolute quantity an order of magnitude larger. That casts doubt on an existing name. damage_unit (26) passes 0 in 69 of its 97 calls -- dealing zero damage is a no-op, so 71% of its call sites would do nothing, while SETTING a percentage-valued property to 0 is a natural thing to do 69 times. 29 has the same shape. The name predates this session and I have not withdrawn it, but it should not be relied on. Where this stopped: the three commands' descriptors at 0x820A8D10/+8/+16 point into 0x8210E5xx, which is BELOW the disassembly DB's range (it starts at 0x82150000) and holds no code, so that route cannot reach an execute method. Opcodes 800-802's semantics need the interpreter command table instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
99f06bbd1f |
re: built-in 108 is deploy_squadron_ex — deploy plus a 1<<n selector
1146 sites in 22 stages, the second-largest unnamed built-in. Its method sub_822646B8 (vtable slot 300) and built-in 2's sub_822642E0 (slot 12) are 190 and 199 instructions and differ in exactly one block. Diffed instruction by instruction, 108 adds: lwz r11, 16(r29) the blob's slot-16 int cmpwi/blt/cmpwi/bgt range-check n to [0, 31] slw r21, r25, r11 r21 = 1 << n <- a 32-bit selector ... default 1 when out of range Everywhere built-in 2 passes its r21, 108 passes r20 and reserves r21 for the mask, so the bit is an EXTRA argument to the same call rather than a replacement. Both post the identical command word AB0100BA, opcode 256. The operand is always a valid bit index: over all 1146 sites slot 16 is in [0, 31], 1146 of 1146, none outside, so the out-of-range default never fires in shipped content. Fifteen distinct values clustered at 16 (531x), 31 (165x), 20 (161x) and 2 (90x); 21 of 22 stages use more than one. What the bit SELECTS is not established and the name does not claim it. There is plainly a 32-bit space -- built-in 92 reportedly allocates a free bit by OR-ing over live units, which would make 108 its "place in a named slot" counterpart -- but I have not verified that, so the name says only what is proven: the same deploy as built-in 2, with an extra selector. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
56a40aa4da |
re: built-in 12 is activate_unit — 517/517 on the ordering test
The highest-traffic unnamed built-in: 1197 sites across all 28 stages. sub_822659F0 read directly: * indexes [phase+324]'s record array by the slot-4 symbol; * returns 0 immediately when the live object [record+4] is NULL, so it registers an object that already exists rather than spawning one; * sets [record+16] = 2, the documented "active" state every unit predicate tests; * stores sub_82301118's packed result into [record+20] (low 16) and [record+24] (high 16) -- member counts; * posts opcode 513 (0xAB0201BA) either way; the slot-8 mode (1 in 999 sites, 0 in 198) only decides whether cmd+20 is also set to 1. The ordering test: if this activates a unit for the script, no predicate should test a unit before it. Over all 28 stages, for every (stage, unit) pair having both, activate_unit comes first 517 times and a predicate first 0 times. Recorded as file order rather than proven execution order -- coroutines can interleave -- but 517 with no exceptions is not a coincidence. 344 units are tested without ever being activated (live from mission start) and 203 activated without being tested. Also recorded: a tidy closure that FAILS. squad_survival_pct reports current over initial and activate_unit snapshots counts, so the snapshot looks like the baseline. It is not -- built-in 24 reads [record+16] for the state then calls sub_823011B0 and sub_82301118 on the LIVE object, never touching +20/+24. What reads those two fields is unidentified. Named coverage is now 57 of 108 distinct ids and ~80% of call-site traffic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
dde827cb3c |
re: built-in 15 IS set_group_speed — and the turret anomaly was my own artefact
The member object sub_82348830 returns is the per-member unit DEFINITION, and that identification is not a guess: the same spawn loop builds two aggregates and each lands on a semantically apt field with the apt reducer. group +192 min, seeded FLT_MAX member +164 = CruisingVelocity group +472 sum member +84 = HP A wrong struct would have to make both offsets land on apt fields AND pair each with the apt reducer. Minimum of a speed, sum of hit points: a formation's cruise limit and its total health. The quantitative test over all 1360 sites, joining each to its craft's definition: value <= the craft's MaximumVelocity 1355 / 1360 = 99.6% (5 fail) value <= the craft's CruisingVelocity 1042 / 1360 = 76.6% (318 fail) The test discriminates -- the cruise bound breaks 318 times, the hull maximum 5 -- so the script sets a COMMANDED SPEED, free to exceed the cruise default and bounded by what the hull can do. The turret anomaly that stopped me naming this two iterations ago was my own artefact. UN_e007_ADAN_Turret's definition carries MaximumVelocity 500 and CruisingVelocity 280: the data models turrets as if mobile, so a script value of 400 is legal and simply never manifests. I had assumed turrets have no velocity fields and treated 13% of the traffic as a refutation. Recorded as unsettled: the five overshoots are UN_e106_ADAN_Destroyer 200 vs a 150 maximum (x2) and UN_e011_ADAN_Attacker_B_HF/_Wayne 500 vs 450 (x3). Designer overrides or an engine clamp; not established. Named set_group_speed. Default = the slowest member's CruisingVelocity; mode 1 restores it, mode 3 sets it, mode 2 hands it a global constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
161ee4df3a |
re: built-in 15 targets a GROUP, and +192 is a MIN over its members
The spawn routine sub_8232B538 settles what the field is without needing its consumer: 8232B5B0 lfs f0, 25184(r11) -> 0x820B6260 = 3.4028235e38 (FLT_MAX) 8232B5B4 stfs f0, 192(r30) seed +192 with FLT_MAX 8232B674 lfs f13, 164(r3) each member's own value 8232B688 fsel f0, f11, f0, f13 f0 = min(f0, f13) 8232B68C stfs f0, 192(r30) 8232BA28 stfs f0, 196(r30) +196 starts equal to +192 The FLT_MAX seed is what makes it unambiguous: +192 is the MINIMUM of [member+164] over the members. So built-in 15's target is a GROUP, not an individual unit -- this file and my own earlier notes both called it a unit. sub_82348830 hands back a per-member object and the reduction runs over all of them. A minimum over members is how a formation limit works: the group goes as fast as its slowest ship. On that reading +192 is the natural cap and +196 the effective one. And a CAP explains the turrets where a commanded speed could not -- capping a static object at 400 is a no-op, so a designer can set it uniformly from a template without consequence, and the asteroid's invariant 0 fits the same reading. Recorded as unproven: mode 2's constant is [r27 + 13912] where r27 comes from a runtime pointer at 0x8232C718, not a static base, so it cannot be resolved from the image. If it is FLT_MAX then mode 2 is literally "uncapped" and the three modes are set / uncap / restore. Tidy, and not shown. The consumer is still not found. Also recorded: the image has NO RTTI at all (0 of 1150 vtables), so class names are synthetic and the two constructors writing vtable 0x820AF030 are 28 and 30 instructions touching neither field. Bounding the group struct is the remaining prerequisite. Built-in 15 stays unnamed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
a140ad6894 |
re: built-in 15's write side verified; its consumer NOT found, filters recorded
Read directly at 0x8232C7CC, the unit message pump switches on [msg+36]: mode 1 [unit+196] = [unit+192] restore the unit's stored default mode 2 [unit+196] = [r27+13912] a global constant mode 3 [unit+196] = [msg+40] the value the script passed So the field has a per-unit default at +192 and a distinguished global value. The shipped scripts only ever use mode 3 -- slot 8 is the double tag in 1961/1961 call sites -- so the two defaulting modes are dead in this build. The consumer I did not find, and the failed approaches are recorded so the next attempt does not repeat them: * searching the flight/AI range for 196(rN) gives 170 hits, almost all on unrelated structs -- the N(rM) trap this file already warns about; * narrowing to functions touching BOTH +192 and +196 still leaves 50; * unit_definition_layout.txt names offsets 192/196 as AV_PitchMinus_Max/Min, but that is the unit DEFINITION object (vtable 0x820af844), not the spawned entity built-in 15 writes to. It does not apply, and taking it would have been an easy wrong turn -- angular-velocity names would have "explained" the turret anomaly while being about a different struct entirely. What would settle it is pinning the spawned-entity struct first, then reading +196's consumers within it. Built-in 15 stays unnamed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
a5432a9a3b |
re: built-in 15 takes a craft-class scalar — but "set_speed" does not survive
15 is the biggest unnamed built-in: 1360 sites across 27 of 28 stages, with
an invariant (unit, double) operand shape. Joining every call site's unit to
its craft type through stage\UnitGroup_S<NN>.tbl resolves 1360 of 1360, none
unknown, and the values stratify hard:
capital hulls (Destroyer, Carrier, CruiserEX, Cruiser, Acropolis,
BattleshipEX, ASFrigate) 0-100
mobile craft (Elan 500, ArrowHead 400, DeltaSaber_T 400,
DeltaSaber_W 600, Attacker_S 250-400,
Phantom 300) 250-600
asteroid (mn040_Asteroid_Big, 74 sites) 0, only 0
Capital ships crawl, fighters run, the player's craft is fastest at 600, and
a rock never moves. That is a speed's signature.
Except turrets break it. UN_e007_ADAN_Turret carries 400 x89 / 280 x17 /
380 x4 / 250 x2 and UN_e008_ADAN_TurretPlus 450 x62 / 500 x2 -- 176 of the
1360 sites, 13%, at fighter-class values on something that does not move.
The asteroid is what makes this sharp rather than dismissible: a genuinely
immobile object gets 0 every single time, so "immobile things get junk" does
not explain the turrets.
So the operand is a craft-class-dependent scalar with speed's range and
ordering, but set_speed is not a sound reading of it. Left UNNAMED. The
handler writes [unit+196], initialised from [unit+192] at spawn; reading its
consumer is what would settle it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
9547618572 |
re: four ISL built-ins are tutorial-only, and 96 encodes the tutorial index
96, 97, 98 and 104 were unnamed. Measured over all 28 stages they form a
family that appears nowhere outside the six tutorials:
96 8 sites S18-S23 only
97 38 sites S18-S23 only, followed by start_coroutine 27/38
98 0 sites never called anywhere
104 39 sites S18-S23 only, followed by wait_s 39/39,
preceded by end_coroutine 37/39
104's adjacency is a textbook poll loop with no exceptions: coroutine starts,
tests the predicate, waits, repeats.
96's operand is the giveaway. Its eight payloads in stage order are
S18 -> 101, 102, 103; S19 -> 201; S20 -> 301; S21 -> 401; S22 -> 501;
S23 -> 601, i.e. (stage - 17) * 100 + section. Tutorial 1 has three sections
and the other five have one each, and the tutorial index is encoded in the
argument.
Names are deliberately NOT applied. tutorial_begin / tutorial_end /
tutorial_message_pending fit the shape, and [phase+340]/[phase+344] are said
to carry the state and payload, but I have not read that handler myself and
this file has already had to withdraw five names guessed from shape. The
distribution and the argument encoding are established and are what a port
needs; the labels can wait for someone to read the code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
f41ef43fa4 |
re: name the five mission-banner built-ins from call-site structure
77, 78, 81, 82 and 135 were unnamed. The engine has five contiguous strings
-- MISSION_START_PRT at 0x820A83F0, then _END_, _UPDATE_, _FAILED_,
_RESTART_ -- and five sequential ScriptPhase fields at +388/+392/+396/+400/
+404, stored in ascending order by one constructor region. Five names, five
fields, five unnamed built-ins.
Which is which is decided by call-site structure, measured over all 28
stages, and it is exact:
39 MARK_LAST_PHASE 89 sites -> 82 in 89/89
82 banner_mission_failed <- 39 in 89/89, then wait_s 89/89
40 mark_not_last 50 sites -> 78 (27) + 81 (17) + END_PHASE (6) = 50
78 banner_mission_complete <- 40 in 27/27
81 banner_objective_update <- 40 in 17/17
77 banner_mission_start 22 sites in 22 stages, one per stage,
after play_bgm
135 banner_mission_restart 16 sites, after play_bgm, phase >= 2
39 -> 82 is a perfect pairing and 40's sites partition exactly three ways.
Stated as inferred rather than read: the string-to-field pairing itself comes
from both sequences ascending in the same order; my operand tracker did not
catch the string loads in that constructor. The ROLES above do not depend on
it.
76 is left unnamed on purpose. It has 38 sites = 22 + 16, exactly 77's count
plus 135's, and precedes them; its body sets [phase+332] = 1 and nothing in
the image reads that field. Suggestive arithmetic is not a name.
Flagged as a consequence: MARK_LAST_PHASE is followed by the FAILED banner in
89 of 89 sites and mark_not_last by END or UPDATE. So [phase+300] = 2 reads
less like "this is the last phase" than "end the mission now,
unsuccessfully" -- the existing names for that pair may be mis-framing it.
Artifact regenerated: docs/re/data/isl-stage02.txt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
ede9a26150 |
re: recover the .slb leading segment — mono, and scoped by measurement
to_xma_riffs now emits the leading headerless segment when it sits at a whole number of XMA1 packets and carries a non-zero byte. VOICE_D_453 goes from a 0.14 s trailing fragment to a 45116-byte leading sub-wave that dominates it. I withdrew this exact change earlier for two reasons. Both are now answered rather than argued away: * "It recovers no audio" -- it used the STEREO format. At two channels every bank yields exactly 1792 bytes, one frame, whatever its size. Mono yields up to 113x more. * "It matches 1524 of 8021 RIFF-bearing entries" -- the byte-level reach is still 1524, but the audible reach is not. Across the 84 movie-bound banks the segment adds >1 s to exactly 7, the hokyu_*_H tankers on D_453/D_454 -- precisely the broken ones -- and <=0.25 s to 66 of the rest. The largest non-resupply addition is S04A at +0.66 s on a 256 s movie. The safety oracle is recorded with its limits: 8 of the 84 banks ALREADY exceed their movie's duration before the change, by hundredths of a second, so it cannot resolve differences at that scale. It establishes scoping, not correctness. Callers clamp to the movie length regardless. VOICE_D_451's all-zero leading region is skipped by the non-zero guard, so the rule cannot prepend silence to a bank that does not need it. Pinned, as is the packet arithmetic (n = 8, 1, 7, 22, 29) which has no tunable. slb_disc, movie_subtitle_disc and movie_manifest_disc all still pass. NOT verified by ear -- that needs a human. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
4d1be2f175 |
re: the .slb parts are sequential segments — and that closes the original mystery
The last open question was whether the leading XMA1-mono region duplicates the RIFF sub-wave, which would make the earlier totals double-count. It does not. Decoding both parts of every bank to mono PCM and measuring energy: bank leading secs / RMS riff secs / RMS VOICE_D_450 0.49 / 158 2.82 / 9898 VOICE_D_451 0.01 / 0 1.58 / 9128 VOICE_D_452 0.31 / 301 2.18 / 9061 VOICE_D_453 2.12 / 9770 0.14 / 14462 VOICE_D_454 3.07 / 10428 0.43 / 11639 Two shapes, and no bank holds the same content twice. In 450/451/452 the leading region is silence or near-silence (RMS 0-301 against ~9000 for speech) and the RIFF holds the line. In 453/454 the leading region holds the line and the RIFF is a short loud tail fragment. Sequential segments of one clip, so the totals stand and with them the 48 kHz fit. This also closes the mystery that started the whole thread. The corpus recorded 450 = 2.8 s, 451 = 1.6 s, 452 = 2.2 s as plausible but 453 = 0.14 s and 454 = 0.43 s as "far too short". The decoder skips everything before the first RIFF: for the first three that discards only silence, so they looked fine; for the last two it discards the line itself and leaves the trailing fragment. One rule, two outcomes, depending on which segment holds the speech. The fix is now well-posed in a way the withdrawn attempt was not: emit the leading region only when it carries signal. That also avoids the 1524-bank blast radius that sank the earlier version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
0136ce6d3b |
re: RETRACTED "audio is missing" — a subtitle cue is a START time
The load-bearing error of the whole voice-bank thread, and it is mine. It stood for three iterations across two write-ups that each called the result proven. I treated a subtitle cue as a timestamp that must fall INSIDE the voice clip, and concluded a 0.07 s clip could not host a cue at 4.70 s. A cue is when the line STARTS. The voice plays from the cue, so the clip only has to fit the window between the cue and the end of the movie. Under that reading every bank fits at plain 48 kHz: bank samples @48kHz cue window D_450 158967 3.31 4.00 5.30 D_451 76084 1.59 3.70 5.60 D_452 119562 2.49 0.00 8.34 D_453 108608 2.26 4.70 4.60 D_454 167828 3.50 0.00 9.50 2-3.5 s is also the right length for the lines. Nothing is missing, and the 17091-20563 Hz window from the previous commit is void with it -- its lower bound came from the same misreading. What survives, because it was measured rather than interpreted: the leading region is XMA1 mono, the decode runs to the final frame, and cue values are seconds. Separately settled, and it is what exposed the error: each shared bank holds ONE generic line. The 3-5 movies bound to a bank have IDENTICAL subtitle text, 5 banks out of 5 -- "Rhino 3 has landed. Commencing resupply.", "Resupply complete. You are cleared for take-off!", and so on. That also explains the historical in-game rejection of hokyu_DS_s13A -> VOICE_D_452 that started this whole thread. The line is generic, identical for s02A/s07A/s08A/s13A. Someone expecting a stage-13-specific line would hear the generic one and call it wrong -- while the binding is exactly right. The disc said so; the subtitle text now says so independently. Still open: whether the leading mono region is additional audio or an alternate take, since the totals above add it to the RIFF sub-waves. Artifact: examples/shared_bank_takes.rs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
44dae8d387 |
re: cue times ARE seconds — and "the rate does not converge" was my error
Two things settled, one of them a correction of my own claim from the last iteration. The cue unit is verified rather than assumed. parse_timing computes mm*60+ss, but only if the token really is mm:ss.cc, so I checked against an independent oracle: the movies are on the disc and a cue must land inside its own movie. 66 English movies with subtitle tracks, 0 cues land after the movie ends. Centiseconds would have overflowed essentially all 66. The seconds reading stands and the verdicts built on it survive. "The sample rate does not converge" does not. I reported implied rates of 39742 / 20563 / 23108 Hz as irreconcilable. They are not estimates of the same quantity -- each is a ONE-SIDED BOUND. The audio must be at least as long as the last cue, so samples/cue is an UPPER bound; it cannot outlast its movie, so samples/movie is a LOWER bound. Intersecting: bank samples cue movie lower Hz upper Hz VOICE_D_450 158967 4.00 9.30 17091 39742 VOICE_D_451 76084 3.70 9.30 8180 20563 VOICE_D_453 108608 4.70 9.30 11677 23108 => 17091-20563 Hz, non-empty. A single rate IS consistent. I had been comparing them as competing point estimates, which is why they looked contradictory. What is still open, and stated as such: that window contains no standard XMA rate. The lower bound assumes a whole bank plays inside one movie, and each of these banks is bound to 3-5 movie slots -- so if a bank holds several takes the lower bound is void, leaving rate <= 20563, which 22050 nearly meets. Next step recorded: establish whether a shared bank is one line or several. Artifact: examples/cue_unit_check.rs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
cfe9bd863f |
re: FFmpeg stops on the LAST frame — my "partial decode" claim was wrong
I had been discarding FFmpeg's stderr with Stdio::null(). It says exactly what happens: an unimplemented "Reserved bit", then a negative bit-skip -- and the failing frame index is always the last one. bank packets frames fr/packet fails at VOICE_D_450 8 45.7 5.71 44 VOICE_D_452 7 29.4 4.21 28 VOICE_D_453 22 198.9 9.04 198 VOICE_D_454 29 287.5 9.92 287 So the previous entry's "the decodes are visibly partial" is wrong, and it was mine. I read "samples per input byte ranges 2.10-4.96" as truncation; it is ordinary XMA1 variable bitrate. Only the final frame of each stream is lost. The decode is essentially complete. The sample rate still does not converge. I tried the obvious repair -- counting the whole bank, leading region plus RIFF sub-waves, since the two split the audio very differently per bank. Two banks then agreed at a tidy ~2.1x ratio pointing near 22 kHz, and the third refuted it: implied rates are 39742, 20563 and 23108 Hz. So the container is identified, the decode is essentially complete, and the duration still does not reconcile -- which moves suspicion to the other side of the comparison. movie_subtitle::track_voice_cues returns (u32, f32) and I have been reading that f32 as SECONDS on the strength of the format notes describing mm:ss.cc cue text. If it is centiseconds, a frame index or a per-page offset, every "audio missing" verdict inherits the error. Recorded as the next thing to check, and to be checked BEFORE any more audio work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
58ee6ac52c |
re: the .slb leading region is XMA1 MONO — 113x more audio than stereo
Retried the format probe with the fmt chunk built to synth_xma1_fmt's exact byte layout, and with the bank's own RIFF sub-wave decoded through the same pipe as a CONTROL so a broken harness cannot masquerade as a result. The channel count is the whole story: bank lead B channels=2 channels=1 VOICE_D_450 16384 1792 46756 VOICE_D_451 2048 1792 896 (all-zero region: control) VOICE_D_452 14336 1792 30154 VOICE_D_453 45056 1792 203648 VOICE_D_454 59392 1792 294440 channels=2 yields EXACTLY 1792 bytes for every bank regardless of size -- one frame, then it stops. That constant is the tell. At channels=1 the same data yields up to 113x more, and the control sub-wave decodes to 13568, so the pipe works. Why the previous probe got 0 bytes everywhere is now named: I read synth_xma1_fmt(2, 2, 48000)'s second argument as a STREAM COUNT when it is a CHANNEL MASK, and built the WAVEFORMATEX around that misreading. Also recorded as a refutation, because it was tempting: solving for the sample rate as decoded-samples / last-subtitle-cue does NOT converge. D_453 implies 21665 Hz -- close enough to 22050 that I nearly wrote it down -- but D_450 implies 5844 Hz. No single rate explains both, and the decodes are visibly partial (samples per input byte ranges 2.10-4.96 where a clean decode would be near-constant). So the container is identified and the duration is not. Next step recorded: find why FFmpeg stops early, likely the hardcoded packet/subframe fields. Artifact: examples/slb_fmt_probe.rs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
391f4bd5e6 |
re: the resupply banks really are missing audio — the subtitles prove it
The corpus said 0.14 s is "far too short for the spoken line". That is a judgement about audio, and audio judgements cannot be made in this container. The subtitle tracks settle it without listening: each carries cue START times, and a subtitle that appears at t seconds cannot sit inside a clip shorter than t. FFmpeg-measured (not estimated from a compression ratio -- the first version of this example used an 8:1 guess, which is not good enough to hang a conclusion on): hokyu_LS_s02A D_450 cue 4.00 s audio 1.41 s MISSING hokyu_LS_s09A D_451 cue 3.70 s audio 1.81 s MISSING hokyu_LS_s02H D_453 cue 4.70 s audio 0.07 s MISSING hokyu_DS_s13A D_452 cue 0.00 s audio 1.21 s no signal hokyu_DS_s07H D_454 cue 0.00 s audio 0.21 s no signal Three of five are decisive; the other two have their only cue at 0.0 s and say nothing either way. So something is genuinely missing from these banks -- established independently of the leading-region work, and measured rather than felt. The fmt-variation probe I recorded as the next step is INCONCLUSIVE and is written up as such: 36 combinations over VOICE_D_453's 22-packet leading region all produced 0 PCM bytes, including ones that should be equivalent to the crate's own synth_xma1_fmt, which does parse. So the probe tested my hand-built fmt chunk, not the hypothesis, and it is NOT evidence that the region is non-XMA. The retry should use the crate's helper. Artifact: examples/voice_len_vs_subs.rs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
26bb0ec7a6 |
re: the "sibling default" rules are a dedup artefact — WITHDRAWN
The corpus recorded that some unit fields the disc leaves defaulted inherit from a sibling: Size_Y from Size_X, FCSRange from RadarRange, DefencePoint from AttackVesselPoint. Size_Y was marked the one to trust, on 9/9 support across 7 independent ships, and it is restated in INDEX.md. The premise is false. These fields are not defaulted -- they are on disc for 113-114 of 114 unit tables -- and Size_Y DIFFERS from Size_X in 90 of them. The mechanism, cross-tabulating "legacy reader missed it" against "equal on disc": pair seen+differ seen+equal miss+differ miss+equal Size_Y / Size_X 90 0 0 24 FCSRange / RadarRange 54 0 1 58 DefencePoint / AttackVesselPoint 51 0 1 61 seen+equal is 0 for all three: a value shared with a sibling is ALWAYS invisible to the string-pool reader, because the pool stores each distinct string once. And the reader almost never misses a value that differs. So "the missing value equals the sibling's" was true BY CONSTRUCTION -- the rule re-derived the very condition that made the field go missing. That is why the support looked perfect: it could not fail on the cases it was fitted to. The two miss+differ cells are its real wrong predictions, both named: UN_e104_ADAN_Carrier DefencePoint is 0.2 (rule says 0.003), and UN_e011_ADAN_Attacker_B_HF_Wayne FCSRange is 3000.0 (rule says 6000.0). Retracted in unit-struct-runtime.md (original reasoning kept below the correction), live-unit-definitions.md and INDEX.md. Pinned by a disc test that asserts the seen+equal cells stay zero, so the mechanism itself is guarded, not just the counts. Artifact: examples/sibling_rule_check.rs. This one was found by my own check after the subagent assigned to it stalled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
61387d5ceb |
re: the .slb leading region is 1392+n*2048 — and my fix for it is withdrawn
The structure is exact. In all five resupply banks the first RIFF sits at HEADERLESS_DATA_OFFSET + n*2048, where 1392 is a constant this crate already had and 2048 is the XMA1 packet size: n = 8, 1, 7, 22, 29. No free parameter to tune, and the raw bytes agree -- high entropy from offset 0, then a zero run immediately before the RIFF. VOICE_D_451 is the control, its single packet being all zeros. So I made the obvious fix, emitting that region as a sub-wave, and then withdrew it on two measurements: * It does not recover audio. Coverage went 5.4% -> 89.9% for VOICE_D_453, but the emitted stream decodes through FFmpeg to 1792 PCM bytes -- silence -- while the RIFF sub-waves from the same banks decode to 150-270 KB. Byte coverage was the wrong success metric and it looked like progress. * It is not narrow. The rule matches 1524 of the 8021 RIFF-bearing entries in sound.pak, including RT* movie banks that decode correctly today. Landing it would have risked a wide regression in order to not-fix five banks. to_xma_riffs is back to its previous behaviour, verified by re-measuring: coverage is 5.4% / 9.7% again. The refuted attempt is recorded in the code beside the branch it would have changed, so the next person does not re-derive the arithmetic and re-make the change. XMA1_PACKET is kept as a named constant because the blast-radius scan uses it. Artifacts: examples/voice_bank_shape.rs (structure), voice_bank_dump.rs (sub-waves for decoding), slb_hybrid_scan.rs (the 1524 count). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
0cf0cda09e |
re: rebuild the movie manifest on the record table — the old counts measured pool strings
movie_manifest::parse now reads BASE_INFO's positional field keys (the game's own cutscene ids, stage*100 + slot) and follows each to its record, instead of scraping the string pool. The pool stores each distinct string once, so a REPEAT reference produced no token and read as "no binding". That single cause explains every wrong cell: 13 later references to VOICE_D_450..454, two to SUBTITLE_hokyu_LS_s11A.tbl, and MS01A's share of pwterop_s01a.prt. All 18 hokyu movies are bound, not five. Counts, verified independently by me against the disc before recording: 104 cutscene SLOTS binding 101 distinct MOVIES; 99 slots / 96 movies with a voice track, 99 / 96 with a subtitle, 22 / 22 with a telop. The docs' old 94 / 83 / 21 are exactly the counts of DISTINCT POOL STRINGS -- not wrong measurements, measurements of the wrong thing. Three denominators were being conflated; the new test pins all three. Two assertions in movie_manifest_disc.rs were false and are corrected: hokyu_DS_s13A binds VOICE_D_452 and resolves to eng\etc\VOICE_D_452.slb. The in-game verdict that rejected that value tested an INFERENCE from a shared demo id, on a decoder that discards 85-87% of banks in this class -- see voice-bank-leading-region.md, committed earlier today. The ~104 script ids are no longer open: they are literal positional keys, each naming its record, and all 104 resolve. The old "counts differ by three, positional pairing does not work" has a concrete cause -- three resupply movies are bound by TWO slots each. Also corrected: the naming convention has 3 subtitle exceptions (s24A/s27A borrow s11A's track) and 18 voice exceptions, not one and five. The legacy scraper is kept as a fallback for blobs with no record table, so the synthetic unit fixtures still exercise it. Artifacts: examples/movie_map_csv.rs regenerates the CSV, now slot-keyed (104 rows; the movie-keyed version silently dropped one slot of each duplicate). Disc tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
8190bbe15e |
re: the voice decoder discards up to 87% of a bank — "multi-subwave" refuted
The record table gives a DIRECT binding hokyu_DS_s13A -> VOICE_D_452, where the corpus records the movie as unbound and movie_manifest_disc.rs asserts None, citing an in-game verdict that this exact value was "the wrong recording". That is the only place on the disc where a runtime observation disagrees with the record table, so it was worth settling. First, shape: these banks are SHARED. Five slots bind VOICE_D_452, five bind 451, four 450, four 453, three 454 -- 21 hokyu slots over five banks, and the movies repeat too. Generic resupply cutscenes, not per-stage recordings. The recorded explanation for 453 decoding to 0.14 s and 454 to 0.43 s was that the banks are "likely multi-subwave / not cleanly sliced". Refuted: the count of RIFF magics EQUALS the number of sub-waves recovered in all five banks, and the last data chunk ends exactly at EOF in four of them. Nothing between or after sub-waves is being missed. The real defect: slb::to_xma_riffs finds audio by searching for the RIFF magic, and a large region PRECEDES it. 87% of VOICE_D_453 and 85% of VOICE_D_454 sit in front of the first RIFF -- 21-27% zero over 256 distinct byte values, i.e. content, not padding. VOICE_D_451 is the control: its leading region is 100% zero, 1 distinct value, real padding. So the in-game verdict listened to a decode that had discarded most of the bank, for exactly this bank class. It is evidence about the decoder, not about the mapping. Note also that what was rejected was a value INFERRED from a shared demo id; the record table supplies the same value as a stored field, and only the inference was ever tested. This does NOT establish the binding is right -- it removes the only recorded evidence against it. What the leading region actually holds is undecoded, and confirming the binding needs a human listening. Artifact: examples/voice_bank_shape.rs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
009bca880e |
re: an ISL symbol operand is a (tag, index) pair — and two more names withdrawn
Verified rather than adopted: a subagent proposed that every even operand slot is a type tag. Measured, the strong form is false and a precise form is true. TRUE: a SYMBOL operand is two words, a tag holding the constant 1 followed by the index. Slot 0 is the integer 1 in 19899/19899 calls whose slot 4 is a unit; slot 8 is tag-shaped in 100% of calls for every built-in taking a second unit; slot 16 is 1 in 152/152 for built-in 128, the only one taking a third. The 24 built-ins whose slot 0 is NOT the constant are exactly those taking no symbol there. This explains the unit slots 4/12/20 rather than replacing them. FALSE as stated: slot 8 is a bare double for built-ins 4, 20, 24, 26, 28, 29, 90, 106 and 127, and built-in 75 carries five bare indices at 0/4/8/12/16 with no tags at all. Each built-in has a fixed signature and is 100% self-consistent; none of the 34 with >=20 sites mixes the two. Symbol table 1 has three types -- 1 routes (1362), 6 messages (2247), 7 effects (81) -- and its operand slots are type-pure, measured the same way. Resolving them makes listings say what the script means: `request_script_message(MSG_VOICE_D_257, ...)`, a fourth independent confirmation of that name. Slots 24@4, 46@12 and 114@4 resolve 100% but MIX types 6 and 1, so they are left unresolved rather than guessed. Two more names withdrawn, neither replaced: * 88 `camera_at` -- ZERO call sites in all 28 stages; never testable. * 90 `camera_at_route` -- 8 sites, all Stage 02 phase 3, first operand is symtab-1 type 7 `eff_n0071`, an EFFECT name, in 8/8, with a per-missile Route_ADT301..308_p3M at slot 20. Not aimed at a camera. Left unnamed on purpose: replacing a guessed name with another guess is how the three names corrected earlier today went wrong. Also flagged: 115 `named_event`'s only symbol operand is an eff_* name in 84/84 sites, so that name is suspect too. Not renamed pending a handler read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
cb1b634adf |
re: measure the boot-nav title gate — and withdraw my own diagnosis
Last iteration I said launch_mission died because skip_intro only tests for the title on a static frame, gated at rmse <= 1500, and that run logged 1503 and 1549 just above the cut. I also said the fix was NOT to nudge 1500 but to measure both signals through a boot first. Measured, and the diagnosis does not survive. boot_trace.sh logs the two signals skip_intro decides on -- frame-to-frame RMSE and the is_title.py green-glyph count -- through a clean boot with no presses at all. One run, 29 samples over 484 s: 8 samples had rmse <= 1500, so the gate OPENED eight times 0 samples had glyph > 0, so the title was never seen At t=145 s the RMSE was 1205, comfortably inside the threshold, and the glyph test was called and answered zero. A frame can be perfectly static without being the title -- the intro movie has long quiet stretches, three reading RMSE exactly 0. So 1503/1549 were almost certainly movie frames too, and raising the constant would have admitted two more of them. What is left is narrower and honest: the interactive title never appeared, rather than appearing and being missed by a threshold. The limitation is recorded rather than buried: the tracer intended 1 s sampling and achieved 16.9 s, because each iteration forks two screenshots, ImageMagick compare and a fresh Python. So this does NOT prove the title never appeared -- only that it was absent from 29 samples. A window shorter than ~17 s falls between them. The recorded next step is to make the tracer sample at the rate it claims before concluding anything stronger. Artifact: docs/re/captures/boot-signal-trace.tsv. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
febdc1bdec |
re: the trigger container's "push" is a clear — and the live test is blocked
Third independent line for yesterday's built-in 100 rename, from the callee this time. sub_8226E3B8 was labelled "push", which is what made built-in 100 look like push_trigger. It reads the element count, returns immediately when the container is EMPTY, and otherwise walks the node list splicing nodes out until it is empty. A push links one node; this unlinks all of them. It is clear(). The append is sub_8226E160, reached from built-ins 19 and 25. So the rename now rests on the handler, the usage (all 12 Stage 02 sites sit in the phase terminator next to timer_stop / clear_flag(-1) / MARK_LAST_PHASE), and the callee. The dynamic half did NOT run, and the write-up says so. phase_watch.py now samples [phase+272+20] (triggers queued) and [phase+216+8] (coroutines alive) so a phase terminator's effect on the VM is visible in one line — written here, never yet exercised against a live guest. Boot-nav could not reach the title in 381 s. Diagnosed rather than retried: skip_intro.sh only runs the title test on a static frame, gated at rmse <= 1500, and this run measured 1503 at 104 s and 1549 at 139 s — just above the cut — so is_title.py was never called and the one allowed press was never spent. Recorded in BACKLOG with the explicit instruction NOT to raise the constant: the first step is to log rmse and the glyph count through a whole boot and look at the two distributions, because tuning a threshold to make one run pass is fitting to a single sample. Also reaped a stale lock: a gdb orphaned 2h14m earlier was holding /tmp/xenia-canary.lock with an already-defunct emulator child. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
1ba5d0a4a3 |
re: three ISL built-in names were wrong, including the most-used one
All re-read twice — the handler, and the thing it calls — because each had been named from its shape rather than its effect. * id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ; stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from the active list and returns it to the free list. It destroys the thread. 2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was. * id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the thread parked at the target pc, or itself if the target is its own pc. It waits for nothing. * id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger container and then frees every thread whose pc differs from the caller's — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage 02 sites all sit in the phase terminator, next to timer_stop, clear_flag(-1) and MARK_LAST_PHASE. One name recovered from the game's own text: opcode 992 prints "RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message (2683 sites). Return codes documented properly: 1 = restart the coroutine from its entry (previously not recorded at all), 3 = terminate. And the blocking set was wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block; its handler ends `b 0x822724F8`, so it always returns 0. Unit-operand resolution settled from DATA over all 28 stages rather than by reading 147 handlers: a slot qualifies only if every value is a valid symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most of the table — that last clause is what discriminates, since every small integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the table, and the resolver now declines rather than inventing a name. New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and 128 take type 8 at slot 12 in 100% of their sites. A downstream inference is withdrawn with it: the note reading the live trigger counter attributed it to "the script arming watches as it goes" via built-in 100. The measurement stands; the attribution does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
723fc9b890 |
re: audit the legacy IDXD reader against the real field table — and fix a test that encoded its error
With the record table decoded there is finally a ground truth to check the
old string-pool reader against. It infers `key -> value` from pool adjacency,
which is a consequence of how records are written, not a rule of the format.
Verified by hand against the disc, with an independent parser:
* `FCSRange` = 500000.0 — the module docs' own canonical example of a field
"left at its default" that "omits the value string".
* `ShieldRatio` = 1.0, where `tests/pak_idxd_disc.rs` asserted None with the
comment "a defaulted/omitted field must be None". That test encoded the
false belief; it now keeps the None as a deliberate characterisation of the
legacy reader, with the true value asserted beside it.
* `get_raw("Model")` on GP_HANGAR_ARSENAL returns the first record's model for
every record — silent corruption, not an absent value. New test pins four
records that disagree with it.
The cause is the flat API having no way to name a record: only 548 of 6325
objects have one. `HP` on the DeltaSaber answers 1000.0, the hull, while 63
Turret_* records each carry their own 100.0 (measured — a first draft said 34,
taken from a report rather than from the disc).
Disc-wide rates are recorded as single-source and labelled as such: get_raw
52% wrong, typed getters 38% miss, but 100% correct on single-record objects.
Also records a negative result: the 504 unnamed field keys were NOT recovered.
A 572464-string dictionary and 73191 variants gave 0/42. The key deltas do
prove the preimage ends with the two decimal digits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
b411d03bd4 |
re: decode the IDXD/IXUD record table — and there is no schema hash
The binary region in front of the string pool was the parser's oldest open
note ("Not yet decoded"). It is a uniform 16-byte record array sorted by
name hash, a field count, a 12-byte field array sorted by key, a pool size,
and the pool. The trailing `pool_size == file_len - pool_base` identity makes
the layout self-checking, which is what caught the first wrong version.
Verified over the WHOLE disc with zero failures: 7750/7750 IDXD objects,
190782/190782 records reproducing their stored tag_hash, 1271462/1271462
named fields reproducing their key. IXUD is the same container with
ixud_hash, UTF-16BE and every offset in chars — 1104/1104 objects,
628165/628165 fields, checked with an independent parser.
Field names are stored on disc, so no preimage search is needed: a field's
middle word points at its own name. Only 504 fields disc-wide are hash-keyed
with no name; the other 1485073 nameless fields are positional, keyed by a
literal integer (line slots, movie ids).
Two long-held beliefs are WITHDRAWN:
* The word at 0x08 is not a schema hash. It is record 0's name_hash — the
format has no type field at all, and an object's kind is known only from
the caller that loads it. It survived as "schema" because tables of one
kind share their lowest-hashed record name. Caught by a test asserting
every movie id names a real record: 1005 -> STAGE10_PHASE01 failed because
tag_hash("STAGE10_PHASE01") IS 0x067025B9, that table's supposed schema id.
* The field's middle word is not an always-0xFFFFFFFF flags word. It is
0xFFFFFFFF for 54% of fields, enough to look constant in a small sample;
the tell was that it is constant per key ACROSS records, which a per-record
flag cannot be but a per-name pointer must.
`schema_hash` keeps its name rather than churn 33 call sites, with corrected
docs. The first sweep globbed dat/** and missed hidden/DefTables.pak (1425
objects); the test now walks the whole disc root.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
fd113868bc |
re: decode the IXUD localised text -- 534 entries, six languages
534 IXUD entries across 36 paks, 94 distinct keys, 92 resolved by name. Content identified: mission objective/lose/hint panels, 91 bonus-objective banners, scripted radio, cutscene dialogue, the reactive combat-chatter pool (MSG_ADAN 23,236 fields, MSG_RHIN 21,196, ...), resupply barks, speaker labels and the hangar weapon Name/Desc/Condition. Verified independently: language\MissionDialog_local_string.tbl decodes to 600 fields with the trailer identity holding, giving Stage 02's real objectives -- phase 1 'Shoot down all invading enemy fighters / while watching out for attacks / on the ACROPOLIS', phase 2 'Protect the Admiral's ship the CALIBAN', phase 3 'Destroy the interstellar cruise missiles'. That matches the script exactly: the phase-1 condition polls three enemy squadrons and the escort's loss is the fail branch, and phase 2 is positional, which is why no kill count appears there. The .ssb symbol names (S02_P1_OBJECTIVE etc.) are NOT IXUD keys -- they are records in an IDXD map, message\MissionDialogMessage.tbl, whose positional fields list the lowercase per-line IXUD names; *_GRAPH holds a texture instead. Structure: one record per object (except the six 63-record hangar files), 624,440 named fields, 55% of them empty line slots, no control characters, the only escape a literal two-char \n in block-text tables, and records/fields in ascending key order so lookup is a binary search. '[OB]' turns out to be a HUD marker label in this text -- the same one the REMAINING OB work chased. Six languages confirmed D/E/F/I/J/S; J omits the trailing empty line slot in 78 of 86 objects. Two keys remain unresolved, byte-identical across all six paks (untranslated Japanese voice transcripts). Also corrects mission-objectives-text.md: SUBOBJ_009 does have text, and SUBOBJ_005 is two fields. |
||
|
|
708a1ea56a |
re: name the four squadron order classes
Three come from the game's own strings, verified present: 'Set Squadron order
attack/trace/escort' at 0x820AF17C/1A4/1CC, loaded once each at entry to the
unit message pump sub_8232C4C0 so the pairing is unambiguous. The printf is
compiled out (bl 0x82674028 = li r3,0; blr) but the format strings and arguments
survive, which is what makes them usable as labels. The same poster functions
serve both the message path and the interpreter-command path, so the identity
transfers to opcodes 517 trace / 784 attack / 790 escort, i.e. ISL built-ins
46/47/48, now named in isl.py.
The fourth, opcode 512, is recorded at LOWER confidence on purpose: those strings
name only three. Its label rests on the route-generation strings on its own
vtable, a symbol-table-1 (route) argument rather than the unit table, a route
point-count lookup, and the pre-existing move_order label -- four independent
lines, but none of them the game naming it.
Also records a trap: sub_82320B48's ORDER_{WINGMAN,SQUAD,SQUADRON}_{FORMATION,
ATTACK,COVER,EXTENDED} enum looks like it should map onto the order classes and
there is no evidence it does. It is a voice/comms axis; not connected.
|