8d1c9d1c77667cb994c24b217330f2ab96c17457
571 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8d1c9d1c77 |
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 |
||
|
|
6ce1a87d49 |
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
|
||
|
|
62d8264cbb |
chore: drop zz_scratch_measure.rs, swept into 49a09a9 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
|
||
|
|
510dcf29d8 |
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 |
||
|
|
679812f1c9 |
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 |
||
|
|
4764e3d5f2 |
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 |
||
|
|
a0cfa68ed8 |
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 |
||
|
|
5693a1e373 |
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 |
||
|
|
3dc3fbadb8 |
re: built-in 15's write side verified; its consumer NOT found, filters recorded
Read directly at 0x8232C7CC, the unit message pump switches on [msg+36]: mode 1 [unit+196] = [unit+192] restore the unit's stored default mode 2 [unit+196] = [r27+13912] a global constant mode 3 [unit+196] = [msg+40] the value the script passed So the field has a per-unit default at +192 and a distinguished global value. The shipped scripts only ever use mode 3 -- slot 8 is the double tag in 1961/1961 call sites -- so the two defaulting modes are dead in this build. The consumer I did not find, and the failed approaches are recorded so the next attempt does not repeat them: * searching the flight/AI range for 196(rN) gives 170 hits, almost all on unrelated structs -- the N(rM) trap this file already warns about; * narrowing to functions touching BOTH +192 and +196 still leaves 50; * unit_definition_layout.txt names offsets 192/196 as AV_PitchMinus_Max/Min, but that is the unit DEFINITION object (vtable 0x820af844), not the spawned entity built-in 15 writes to. It does not apply, and taking it would have been an easy wrong turn -- angular-velocity names would have "explained" the turret anomaly while being about a different struct entirely. What would settle it is pinning the spawned-entity struct first, then reading +196's consumers within it. Built-in 15 stays unnamed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
dd3c27f288 |
re: built-in 15 takes a craft-class scalar — but "set_speed" does not survive
15 is the biggest unnamed built-in: 1360 sites across 27 of 28 stages, with
an invariant (unit, double) operand shape. Joining every call site's unit to
its craft type through stage\UnitGroup_S<NN>.tbl resolves 1360 of 1360, none
unknown, and the values stratify hard:
capital hulls (Destroyer, Carrier, CruiserEX, Cruiser, Acropolis,
BattleshipEX, ASFrigate) 0-100
mobile craft (Elan 500, ArrowHead 400, DeltaSaber_T 400,
DeltaSaber_W 600, Attacker_S 250-400,
Phantom 300) 250-600
asteroid (mn040_Asteroid_Big, 74 sites) 0, only 0
Capital ships crawl, fighters run, the player's craft is fastest at 600, and
a rock never moves. That is a speed's signature.
Except turrets break it. UN_e007_ADAN_Turret carries 400 x89 / 280 x17 /
380 x4 / 250 x2 and UN_e008_ADAN_TurretPlus 450 x62 / 500 x2 -- 176 of the
1360 sites, 13%, at fighter-class values on something that does not move.
The asteroid is what makes this sharp rather than dismissible: a genuinely
immobile object gets 0 every single time, so "immobile things get junk" does
not explain the turrets.
So the operand is a craft-class-dependent scalar with speed's range and
ordering, but set_speed is not a sound reading of it. Left UNNAMED. The
handler writes [unit+196], initialised from [unit+192] at spawn; reading its
consumer is what would settle it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
fa20db3db8 |
re: four ISL built-ins are tutorial-only, and 96 encodes the tutorial index
96, 97, 98 and 104 were unnamed. Measured over all 28 stages they form a
family that appears nowhere outside the six tutorials:
96 8 sites S18-S23 only
97 38 sites S18-S23 only, followed by start_coroutine 27/38
98 0 sites never called anywhere
104 39 sites S18-S23 only, followed by wait_s 39/39,
preceded by end_coroutine 37/39
104's adjacency is a textbook poll loop with no exceptions: coroutine starts,
tests the predicate, waits, repeats.
96's operand is the giveaway. Its eight payloads in stage order are
S18 -> 101, 102, 103; S19 -> 201; S20 -> 301; S21 -> 401; S22 -> 501;
S23 -> 601, i.e. (stage - 17) * 100 + section. Tutorial 1 has three sections
and the other five have one each, and the tutorial index is encoded in the
argument.
Names are deliberately NOT applied. tutorial_begin / tutorial_end /
tutorial_message_pending fit the shape, and [phase+340]/[phase+344] are said
to carry the state and payload, but I have not read that handler myself and
this file has already had to withdraw five names guessed from shape. The
distribution and the argument encoding are established and are what a port
needs; the labels can wait for someone to read the code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
cb830e233e |
re: name the five mission-banner built-ins from call-site structure
77, 78, 81, 82 and 135 were unnamed. The engine has five contiguous strings
-- MISSION_START_PRT at 0x820A83F0, then _END_, _UPDATE_, _FAILED_,
_RESTART_ -- and five sequential ScriptPhase fields at +388/+392/+396/+400/
+404, stored in ascending order by one constructor region. Five names, five
fields, five unnamed built-ins.
Which is which is decided by call-site structure, measured over all 28
stages, and it is exact:
39 MARK_LAST_PHASE 89 sites -> 82 in 89/89
82 banner_mission_failed <- 39 in 89/89, then wait_s 89/89
40 mark_not_last 50 sites -> 78 (27) + 81 (17) + END_PHASE (6) = 50
78 banner_mission_complete <- 40 in 27/27
81 banner_objective_update <- 40 in 17/17
77 banner_mission_start 22 sites in 22 stages, one per stage,
after play_bgm
135 banner_mission_restart 16 sites, after play_bgm, phase >= 2
39 -> 82 is a perfect pairing and 40's sites partition exactly three ways.
Stated as inferred rather than read: the string-to-field pairing itself comes
from both sequences ascending in the same order; my operand tracker did not
catch the string loads in that constructor. The ROLES above do not depend on
it.
76 is left unnamed on purpose. It has 38 sites = 22 + 16, exactly 77's count
plus 135's, and precedes them; its body sets [phase+332] = 1 and nothing in
the image reads that field. Suggestive arithmetic is not a name.
Flagged as a consequence: MARK_LAST_PHASE is followed by the FAILED banner in
89 of 89 sites and mark_not_last by END or UPDATE. So [phase+300] = 2 reads
less like "this is the last phase" than "end the mission now,
unsuccessfully" -- the existing names for that pair may be mis-framing it.
Artifact regenerated: docs/re/data/isl-stage02.txt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
|
||
|
|
a32c00057e |
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 |
||
|
|
ea0eedda86 |
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 |
||
|
|
b32006d2a8 |
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 |
||
|
|
b324278ae9 |
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 |
||
|
|
978ccf882a |
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 |
||
|
|
116fd7d2ff |
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 |
||
|
|
af320466bb |
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 |
||
|
|
49a09a9496 |
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 |
||
|
|
6fa6564be8 |
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 |
||
|
|
fedb31a5f9 |
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 |
||
|
|
49c00e0955 |
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 |
||
|
|
75f0664bfe |
re: an ISL symbol operand is a (tag, index) pair — and two more names withdrawn
Verified rather than adopted: a subagent proposed that every even operand slot is a type tag. Measured, the strong form is false and a precise form is true. TRUE: a SYMBOL operand is two words, a tag holding the constant 1 followed by the index. Slot 0 is the integer 1 in 19899/19899 calls whose slot 4 is a unit; slot 8 is tag-shaped in 100% of calls for every built-in taking a second unit; slot 16 is 1 in 152/152 for built-in 128, the only one taking a third. The 24 built-ins whose slot 0 is NOT the constant are exactly those taking no symbol there. This explains the unit slots 4/12/20 rather than replacing them. FALSE as stated: slot 8 is a bare double for built-ins 4, 20, 24, 26, 28, 29, 90, 106 and 127, and built-in 75 carries five bare indices at 0/4/8/12/16 with no tags at all. Each built-in has a fixed signature and is 100% self-consistent; none of the 34 with >=20 sites mixes the two. Symbol table 1 has three types -- 1 routes (1362), 6 messages (2247), 7 effects (81) -- and its operand slots are type-pure, measured the same way. Resolving them makes listings say what the script means: `request_script_message(MSG_VOICE_D_257, ...)`, a fourth independent confirmation of that name. Slots 24@4, 46@12 and 114@4 resolve 100% but MIX types 6 and 1, so they are left unresolved rather than guessed. Two more names withdrawn, neither replaced: * 88 `camera_at` -- ZERO call sites in all 28 stages; never testable. * 90 `camera_at_route` -- 8 sites, all Stage 02 phase 3, first operand is symtab-1 type 7 `eff_n0071`, an EFFECT name, in 8/8, with a per-missile Route_ADT301..308_p3M at slot 20. Not aimed at a camera. Left unnamed on purpose: replacing a guessed name with another guess is how the three names corrected earlier today went wrong. Also flagged: 115 `named_event`'s only symbol operand is an eff_* name in 84/84 sites, so that name is suspect too. Not renamed pending a handler read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
608a0cc710 |
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 |
||
|
|
501298de81 |
re: the trigger container's "push" is a clear — and the live test is blocked
Third independent line for yesterday's built-in 100 rename, from the callee this time. sub_8226E3B8 was labelled "push", which is what made built-in 100 look like push_trigger. It reads the element count, returns immediately when the container is EMPTY, and otherwise walks the node list splicing nodes out until it is empty. A push links one node; this unlinks all of them. It is clear(). The append is sub_8226E160, reached from built-ins 19 and 25. So the rename now rests on the handler, the usage (all 12 Stage 02 sites sit in the phase terminator next to timer_stop / clear_flag(-1) / MARK_LAST_PHASE), and the callee. The dynamic half did NOT run, and the write-up says so. phase_watch.py now samples [phase+272+20] (triggers queued) and [phase+216+8] (coroutines alive) so a phase terminator's effect on the VM is visible in one line — written here, never yet exercised against a live guest. Boot-nav could not reach the title in 381 s. Diagnosed rather than retried: skip_intro.sh only runs the title test on a static frame, gated at rmse <= 1500, and this run measured 1503 at 104 s and 1549 at 139 s — just above the cut — so is_title.py was never called and the one allowed press was never spent. Recorded in BACKLOG with the explicit instruction NOT to raise the constant: the first step is to log rmse and the glyph count through a whole boot and look at the two distributions, because tuning a threshold to make one run pass is fitting to a single sample. Also reaped a stale lock: a gdb orphaned 2h14m earlier was holding /tmp/xenia-canary.lock with an already-defunct emulator child. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
9a9921b759 |
re: three ISL built-in names were wrong, including the most-used one
All re-read twice — the handler, and the thing it calls — because each had been named from its shape rather than its effect. * id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ; stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from the active list and returns it to the free list. It destroys the thread. 2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was. * id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the thread parked at the target pc, or itself if the target is its own pc. It waits for nothing. * id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger container and then frees every thread whose pc differs from the caller's — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage 02 sites all sit in the phase terminator, next to timer_stop, clear_flag(-1) and MARK_LAST_PHASE. One name recovered from the game's own text: opcode 992 prints "RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message (2683 sites). Return codes documented properly: 1 = restart the coroutine from its entry (previously not recorded at all), 3 = terminate. And the blocking set was wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block; its handler ends `b 0x822724F8`, so it always returns 0. Unit-operand resolution settled from DATA over all 28 stages rather than by reading 147 handlers: a slot qualifies only if every value is a valid symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most of the table — that last clause is what discriminates, since every small integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the table, and the resolver now declines rather than inventing a name. New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and 128 take type 8 at slot 12 in 100% of their sites. A downstream inference is withdrawn with it: the note reading the live trigger counter attributed it to "the script arming watches as it goes" via built-in 100. The measurement stands; the attribution does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE |
||
|
|
9141ea2b24 |
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
|
||
|
|
af32540190 |
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
|
||
|
|
34c99a1568 |
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. |
||
|
|
dc2054f0be |
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.
|
||
|
|
770ef2ea2b |
re: CONFIRMED -- the poke produced 'absent', not 'destroyed'
Settled from the disassembly, no run needed. Built-in 69's tail maps the lifecycle lookup into [phase+164]: handle == 0 takes the early exit at 0x8226AF44 and returns 0, while the destroyed states return 2, 3 or 4 (and two of those also normalise the record's +16). Those are different values. So poking rec+4 = 0 made the predicate report ABSENT -- the same answer an undeployed unit gives -- and never the answer the script branches on. The condition was polling at 5 Hz throughout and correctly saw 'not here'. Both null results are now fully explained, and neither was evidence about the condition: the first poke wrote a field nobody reads, the second wrote the wrong value into the right field. Simulating a kill needs the handle to stay valid while the lifecycle lookup returns 3/4/5, i.e. the write belongs in whatever sub_82301240 reads, not in the script's own record. Also names built-ins 46/47/48 as squadron_trace / squadron_attack / squadron_escort in isl.py. |
||
|
|
72e24acdfe |
re: WITHDRAWN -- the phase-1 condition IS polled, at 5 Hz
Tracing Stage 02 phase 1 by control flow (following op 12 jumps and the six conditional branches) gives the whole chain: timer5 @ 4.0s -> trigger 0x2D30 (which also does timer_set/timer_resume) -> start_coroutine at 0x2FE8 -> coroutine 0xBB9C, whose body is wait_s(0.2) ; yield -> ... -> 0xF524, the ADN110/111/112 polls that latch set_flag(8). Exactly one of phase 1's 79 coroutine entries reaches 0xF524 under a real control-flow walk, and it is a 0.2s polling loop. So the condition is checked at 5 Hz from 4 seconds into the phase, and my 'the condition coroutine is not polling' explanation -- which I had promoted to real evidence after the poke experiments -- is withdrawn. Two weaker methods gave wrong answers first and are named: linear decode fell through into unrelated code and implicated 30-odd entries; 'nearest preceding entry' implicated the right one only by proximity luck, 0x3988 bytes away. And I matched start_coroutine operands across phases at first, which is meaningless since the operand is relative to [phase+232] -- the same per-phase base trap that broke isl.py's jump targets, hit again three iterations later. The poke results now need a different explanation. The likeliest is that neither poke produced the tested state: +16 is never read by built-in 69, and +4 = 0 takes the early-exit path an UNDEPLOYED unit also takes. Recorded as a hypothesis with the experiment that would settle it. |
||
|
|
0b06e29d58 |
re: Stage 02 phase 1 read as a timeline; what arms the mission clock
Disassembling all 25 timer triggers in threshold order turns the phase into readable script: fade, BGM, then squadron deployments at 30/60/90/120/170/210/ 240s each as deploy + move_order + objective_marker, with radio messages interleaved and a late block at 1020-1170s. Answers the open question from the previous entry: timer 0 is armed by timer_set(1200, 180) followed by timer_resume, fired at 4.0s on the phase-intro clock (timer 5), which is already running when the phase begins. So the mission clock has a 1200-second limit. And it independently confirms the arrival measurement: the timer0 @ 170.0s trigger deploys symbol index 0x01 = ADN110, the first of the three squadrons the phase-1 condition polls. The live run measured those three going active at ~155-165s of mission time, and the route table also says 170. Three independent sources agree -- the route table, the trigger table, and the running game. Open: whether a coroutine can re-arm its own trigger, and timer_set's second argument (180), for which 'warning threshold' is a guess rather than a finding. |
||
|
|
0cf75f998f |
re: phase coroutines are started by a per-frame timer table
sub_822748D0, called from ScriptPhase::Update every frame, walks the table at [phase+240]: stride 24, each record carrying a coroutine offset (rec+4), an f32 threshold (rec+12) and a timer index (rec+20). It compares the previous and current copies of the 32-entry float register file ([phase+104] and [phase+88]) and starts the coroutine at codebase+off on a RISING EDGE, prev <= t < cur, so each fires once. Verified instruction by instruction. The table comes from the mission-level begin_phase opcode 0x83's fourth operand (the fifth is the phase end-event); sub_82270DF8 stores them at [phase+240] and [phase+236]. On disc it is tagged constants -- 0x819 int, 0x81A float, both past the ISL dispatcher's bound so they never execute. Verified on Stage 02: counts 25/13/18 with the tag triple correct in every record. Only timers 0 (1-1170s, mission clock) and 5 (0-5s, phase intro) are used corpus-wide. This explains the poke results. Two experiments set a squadron to 'destroyed' and nothing happened; the leading explanation was that the condition coroutine is not polling. It is not -- coroutines are started on a schedule by timer crossings, so state written between firings is read by nobody. It also reframes the arrival timetable: the routes' t=170 and these thresholds are the same kind of thing. The mission is substantially a timeline, with unit predicates deciding what happens at each scheduled point rather than when. Open: which built-in arms or resets each timer. |
||
|
|
7a88d24e66 |
re: correct 'no mission counts kills' -- attribution IS used, globals are not
I wrote that no mission counts kills. Too strong. What holds is narrower: no mission reads the two GLOBAL counters (33/34, verified 0 sites). The game does track kills and missions test them through attribution: built-in 105 killed_by_player(unit) -- reads [squadron+600], which the unit-death handler sets from the damage message's _BY_PLAYER flag -- 117 sites across 14 stages (S02: 38, S01: 32), the 17th-most-used built-in in Stage 02. built-in 72 -- numerator is a per-(killer, victim) squadron KILL COUNTER, incremented in the unit-death handler; 13 sites, S01 only. So 'did the player get that kill' is a common predicate; the game just never asks 'how many kills in total'. Evidence for 105 is the game's own strings: the death handler branches into KILLED_BY_PLAYER vs KILLED, beside LOST_TURRET_BY_PLAYER, SHIELD_DOWNED_BY_PLAYER and the rest. Also withdraws the 'current / initial members' gloss on built-in 72: it is 100 x (units of B killed by A) / (declared members of B), attributed kills rather than survivors, and reads 0 unless the script registered the (A,B) pair. Built-in 55 resolved and shown to be effectively dead (0 sites corpus-wide); built-in 75 confirmed as five HUD text lines via a [phase+376] dictionary. |
||
|
|
ac4f53b8a1 |
re: interpreter command table recovered; withdraw my 'constructors' reading
sub_822FE040 is a fully unrolled registration: 1023 slots filled with a default, then 57 explicit writes, of which 48 are real handlers and nine are a shared accept-and-discard stub. Return convention is nonzero = consumed, 0 = retry, which is how the interpreter waits for a named unit to exist. Opcode 995 is the ONLY handler touching the phase mirror [*(0x828F35F8)+236] -- the sole read and sole write in the table -- independently confirming why polling that mirror saw nothing during phase 1. And no handler spawns or despawns a unit: 256 is the strongest deploy candidate but is unconfirmed because the message ids are write-only in this image. WITHDRAWN, verified wrong: I had recorded the writes to '+20' in sub_8226E7D8 / sub_8226E930 as block initialisations by container constructors. At 0x8226E86C-0x8226E8E0 they do li r3,28 / bl 0x8230C160 then lis r10,0xAB03 / ori r7,r10,0xE4BA / stw r7,4(r3): they build an INTERPRETER COMMAND RECORD for opcode 996 and push it, i.e. AddSelector and RemoveSelector, with a 32-entry cap. The stw to 20(r3) is the command record's +20, a different object. Wrong twice: not constructors, and not that container. Also flags that sub_8230C398 -- gated on *(0x82899CE0) == 16 at both call sites -- looks like Stage 16's script compiled in C++, which 'debug defaults' does not survive given the .ssb loader explicitly refuses mission 16. |
||
|
|
13ec75465b |
re: sub_8226E458 is a splice, but not shown to touch the trigger queue
Chasing which of sub_8226EAB8's 16 callers grows the trigger count: sub_8226E458 decrements a count at 8(r30) then calls the insert helper -- a splice -- and has exactly one caller, inside sub_8226D740, the per-frame engine->script drain. That suggests the engine moves records into the phase queue each frame. But the call site refutes the neat reading: at 0x8226D780 the argument is lwz r4, 324(r29), the UNIT ARRAY, not the trigger container. So I have not shown sub_8226E458 touches the trigger queue at all, and 'the engine feeds triggers each frame' is my inference rather than the disassembly's. Recorded unresolved. Taking a function's shape for its purpose is what produced the 'push' mislabel on sub_8226E3B8 and the ADN110-for-null pretty-print, each of which cost an iteration. Unaffected and solid: sub_8226EAB8 increments a count at +8 of its container, the trigger container embeds its list at +12, so the watched word at phase+272+20 is that inner list's count, and the guest was inside sub_8226EAB8 at the write. Next: instead of guessing among 16 callers, re-run the watchpoint and read the guest LR from the context -- the technique that named the writer will name its caller. |
||
|
|
33013a9b84 |
re: the trigger-queue appender found -- sub_8226EAB8, count at inner+8
The watchpoint plus Canary's source settle it. At the write, the guest context (rsi, per x64_emitter.cc:881) holds 0x8226EAE0, inside sub_8226EAB8. That function is a generic list-node insert: it reads the count at 8(r30), guards against 0x3FFFFFFF overflow, does addi r11,r11,1 / stw r11,8(r30), then links the node. It has 16 callers, so it is a shared container helper. That explains why two static searches missed it. The trigger container at phase+272 EMBEDS an inner list object at +12 -- which is why the push does 'addi r31, r30, 12' -- and the inner object keeps its count at its own +8. So 272 + 12 + 8 = 292 = the watched word, and the instruction is stw r11, 8(r30) with r30 = phase+284. Searching for 'stw rN, 20(rM)' could never have found it. Also resolves the earlier open item on +12: it is the embedded list object, not a list head pointer, which is why it read 0x000A0009 instead of an address. Method note kept: the static hunt assumed the field's offset in the OUTER object would appear in the writing instruction. A watchpoint is indifferent to the addressing form, which is why it was the right tool after two failed offset searches. |
||
|
|
121004ef3c |
re: the mission script is resident immediately -- two explanations refuted
ssb_watch.py polls the .ssb header, a symbol string and find_mission from the moment flight is detected. On a normal run all three are present at the FIRST sample (t=0.0, mission 0xBC79C960), so 'the script loads later than assumed' and 'the probe raced a load' are both out -- there is no window in which a healthy mission flies without its script in memory. A detail worth more than it looks: at that sample screen_id reported 'other', not 'flight'. The script is loaded and the ScriptMission locatable BEFORE the flight HUD appears, so residency is not gated on the HUD, and a run showing the HUD without the script is in a state a healthy run never passes through. That leaves the third candidate: the anomalous run's mission never loaded and its IN FLIGHT was a misdetection -- consistent with it being frozen on a black screen when checked afterwards. Not proven, but now the only surviving explanation rather than one of three. |
||
|
|
761dcd004c |
re: the JIT context is in %rsi; but the re-run could not locate the mission
From Canary's own source (x64_emitter.cc:881) GetContextReg() returns rsi, so at any JIT instruction %rsi is the PPCContext* -- which is also why the faulting instruction read 0x110(%rsi), a guest register load. That is the way past the watchpoint's ceiling: the guest register file is available at the write, and a 0x82xxxxxx word picked out of it resolves against sylpheed.db to name the caller. trigger_watch.sh now dumps x/128wx instead of a useless host backtrace. The re-run then failed for an unrelated and unexplained reason: it reached flight, the pilot bound, the guest was animating, and find_mission returned NOTFOUND. Narrowed: the .ssb header is absent from guest memory (0 hits where earlier runs hit immediately), ADN110 is absent too, but the manifest string 'Stage02.ssb' IS present at 0xBDA6C50B. So memory is readable and the manifest is loaded while the script is not, in a mission that is flying. No explanation offered. The cheap discriminator for next time is to poll for the header from the moment flight starts and record when it appears, instead of sampling once. |
||
|
|
c50ab7275f |
re: survey all 28 mission scripts -- kill counters are dead game-wide
All 28 StageNN.ssb decoded: 2,085,628 bytes, 25,705 call sites, 108 of 147 built-ins used. Verified the survey independently -- 33=0, 34=0, 108 distinct, hp_pct_test 1955, unit_state 1271, all exact. global_counter0/1 have ZERO call sites in all 28 stages. The handlers exist and are wired; no mission calls them. So 'does a wave start after N kills?' is answered for the whole game, not just Stage 02: no mission counts kills. Two idioms change how the counts read. hp_pct_test(unit, 0.0) IS a destruction test -- 1786 of 1955 calls (91%) pass 0.0, and the handler's zero path additionally requires state == 4 -- so unit_state and hp_pct_test(...,0) are interchangeable and stages just pick one. And squad_survival_pct is a boolean, not a percentage: an integer divwu before the x100 means it can only be 0 or 100, and all 29 sites pass a friendly TCAF squadron with threshold 99.9, making it 'has this escort lost anybody'. The game has no destroy-N%-of-a-squadron objective. Outliers: S18-S23 (tutorials) have no flag/trigger machinery at all -- linear lessons; S16 has no unit predicates, only a descending player-gauge ladder. Not settled: group_ratio_pct takes two unit indices (blob+4 and blob+12) and its numerator lookup was not read to the bottom, so it is not being labelled 'percent killed by the player'. isl.py's UNIT_ARG omitted 71/72. |
||
|
|
da2d02db77 |
re: the trigger-count watchpoint fires, but the writer is JIT guest code
trigger_watch.sh + host_addr.py translate the guest VA to a host address (0xBE14DEA4 -> host 0x1BE14DEA4) and watch it. It fires: Thread 50 'Main XThread', old 0, new 16777216 -- which is big-endian 1 read little-endian, so the count going 0 -> 1, independently confirming the field. The write happens on the guest's own main thread, not an emulator worker. But the writer cannot be named from the host stack: the faulting PC is 0xa0c65f23 with no symbol, executing 'mov 0x110(%rsi),%rbx', i.e. Xenia's JIT-compiled guest code, and the frames above it are not host-unwindable. So the watchpoint answers when and which thread, not which guest function. Recorded as a ceiling of the method rather than retried blindly. The way past it is that the JIT holds the guest context in a register (%rsi here), so the guest PC is recoverable from the context block -- which needs Xenia's context layout from the xenia-rs sources on this box, a separate tractable piece of work. |
||
|
|
28a4b1ead1 |
re: ISL opcodes decoded; the branch base is PER PHASE and isl.py was wrong
All 25 opcodes now have meanings. Ops 2/4/6/8 are integer compound assignment (+= -= *= /=) and 3/5/7/9 the float versions; 10 and 11 are integer and float compare writing three condition bits; 13-18 are je/jne/jl/jle/jg/jge; 21-24 are push.i/push.f/pop.i/pop.f over deques at phase+44 and phase+64. The shared-handler question is answered: the dispatcher leaves the opcode in r4 and the shared thunks never overwrite it, so those helpers take an extra opcode argument and index a secondary table (0x82271448, 0x8227152C). CORRECTION to my own tool and note: the branch/jump base is [phase+232], which the phase initialiser sets to 0x24 + the phase's entry from the mission-level stream -- 0xE4 / 0x14AA8 / 0x24B4C for Stage 02's three phases, not the file's 0x24. Measured on phase 1: base 0xE4 puts 525 of 525 branch targets on an instruction boundary; base 0x24 manages 188. isl.py had been using 0x24 for every phase, so its jump targets were wrong throughout. Fixed via isl.phase_bases(). That also settles two things mission-script-ssb.md left open: offsets ARE code-base-relative, and 0x1883's operand IS a code pointer -- the earlier worry that some 'land on IEEE floats' was an artefact of adding the wrong base. |
||
|
|
415dd75a8b |
re: what appends a trigger node -- not found, recorded as such
Three approaches, none successful. sub_8226E160, earlier flagged as 'enqueue a pending trigger', has exactly one caller and is a specific operation rather than the general append. Writes to the count at +20 inside the container code number only four, and all four are part of a block initialisation (stw to 0/8/12/16/20/ 24 in consecutive instructions) in sub_8226E7D8 and sub_8226E930 -- constructors, called from 0x8226E560 and from ScriptMission's own constructor at 0x822608A0. So the increment that takes the count 0 -> 1 -> 2, which is measured live, does not appear as a plain stw to 20(rM) anywhere in the container's code. It is inlined, uses another addressing form, or lives somewhere I have not looked. Recorded as not found rather than guessed: inferring from the shape of nearby functions is exactly what produced the 'push' mislabel last iteration. Names the approach that would settle it: a gdb watchpoint on ScriptPhase+272+20 during a live mission. The address is known at runtime, the count demonstrably changes within ~2 minutes, and a watchpoint reports the writing instruction directly instead of inferring it from static shape. |
||
|
|
25a98fb204 |
re: trigger queue walks live -- structure confirmed, record contents refuted
Walked the container's list from +16 for 200s. The structure holds: +20 tracks the node count, nodes chain through their first word, and entries appear as the mission runs (0 -> 1 -> 2, then stable). The record layout does not. I expected node+8 to hold small symbol indices, which the pop's out-parameters made natural. Every field is a guest heap pointer (0xBC..), so trigger records reference objects rather than table indices, and those objects are unidentified. Also records a false resolution I introduced: a line printed 'f4=0(ADN101)' because the raw value is 0 and my formatter mapped index 0 to symbol-table-2's first entry. ADN101 is not in that record -- the pretty-printer invented a name for a null. A resolver must refuse values that were never indices. And corrects the previous section: sub_8226E3B8 is a CLEAR, not a push. Its tail decrements a counter, calls an erase helper, and loops while [+20] != 0. So built-in 100 clears the queue then rebuilds the thread list, matching the built-in table's own wording; 'push' was my label, not the disassembly's. Two callers: vt2 (script) and 0x8226D420 (an engine site). What appends a node is still unidentified. |
||
|
|
eeff706134 |
re: the trigger-queue pending count verified live -- 0 -> 1 -> 2
Read from a running Stage 02 mission: the container at ScriptPhase+272 has +20 moving 0 -> 1 -> 2 over the first two minutes while the phase ordinal stays 1. So +20 is a real count of currently registered triggers -- the script arming watches as it goes, and Stage 02 has 12 push_trigger sites -- and it is readable from /dev/shm with no debugger. First direct view of what the script is waiting for. Corrects one field from the static reading: +12 is not (only) a list head. It reads 0x000A0009, which is not a pointer, even though the push's 'addi r31, r30, 12' made list-head the obvious interpretation. Recorded as unresolved rather than kept quietly. |
||
|
|
63d3ce089f |
re: trigger queue layout at phase+272, and a readable pending count
Chasing what makes the phase-1 condition re-evaluate, since the polls do not run continuously. Two method corrections: searching the VM range for '272(rN)' mostly returns VTABLE slot offsets -- 0x82273174 lwz r11,272(r11) is followed by mtctr/bctrl, a virtual call through slot 68, not an access to the phase field. And [phase+272] is not a pointer to a queue but an EMBEDDED container: vt2 (sub_82265DD0) is 'addi r3,r3,272 ; b 0x8226E3B8', passing phase+272 as this. Layout from the push/pop pair (sub_8226E3B8 from built-in 100, sub_8226E220 called every frame from sub_8226D740): +12 list head, +16 current node, +20 element count (zero = empty, tested first by the pop), +24 scratch. The pop returns the record through out-parameters read from node+8: three u32s, a double at +16, another u32 at +24 -- matching the six pointers sub_8226D740 passes in. The actionable part is [phase+272+20], a live pending-trigger count readable from /dev/shm. Watching it alongside [ScriptMission+40] should show when the engine hands the script an event, which is when condition coroutines start -- the thing every phase experiment so far has been blind to. Layout is from disassembly only; not yet verified live. |
||
|
|
67f3d621e6 |
re: the poke control PASSES -- writes reach the guest, hull is authoritative
Hammering settles what a single write could not: hull 0x44BB8000 (1500.0f), 944,387 writes of 1 over 15s, and afterwards the value HELD at 1 -- the game stopped rewriting it. The screen left 'flight', the HUD is gone, the ship is burning, and Natalie radios 'I've lost contact with Rhino 3!', the player's own callsign. The game read the poked value and killed the player. Established: writes to /dev/shm reach the running guest; hull at pos+0x154 is authoritative, not a readout; and a single write loses a race against the game's own continuous writes. This upgrades two earlier results from inconclusive to genuine negatives. The unit-record pokes were downgraded because I could not tell 'ignored' from 'never arrived'. The write arrives -- and those pokes persisted untouched for 60s, so the game genuinely saw state=4 and handle=0 on all three objective squadrons and did nothing. That is real evidence the phase-1 condition coroutine is not polling and its checks run only when a trigger starts them. Withdrawn: last iteration's claim that the pilot's hull= is a different field or scale. I read 1000.0f at pos+0x154 and inferred a mismatch with the logged 1500; this run reads 1500.0f at the same offset. Same field, different value per run. |
||
|
|
77bc300692 |
re: the positive control ran, and is still inconclusive
poke_control.sh (self-retrying, succeeded on attempt 1 with no freeze) set the player's hull at pos+0x154 to 1. Twelve seconds later the game had put 0x447A0000 back. That establishes an asymmetry worth having: the hull word is continuously rewritten by the game, while the unit-record fields held our value untouched for 60s. It separates fields the game maintains from fields nobody writes. But it is not yet a control. The after-frame shows a red WARNING banner -- and the before-frame already shows MISSILE ALERT, so the ship was under attack in both and the banner is not attributable to the poke. A value being overwritten proves the game writes that address, not that it read ours. Correction: hull at pos+0x154 is 0x447A0000, a FLOAT 1000.0, not the 1500 the pilot logs -- those are different fields or scales and should not be conflated. The settling refinement: poke in a tight loop for several seconds so the value is low whenever the game samples it. If hull is authoritative the ship dies and the screen goes to GAME OVER; if it survives, the field is a readout. The reusable part is the harness: boot -> verify animating -> locate -> act, with a freeze at any step costing one retry rather than the iteration. |