Files
Syplheed-Reborn/docs/re/INDEX.md
Claude (auto-RE) ac10d3bd19 t8ad: a surface is a list of sub-rectangles, not a 256 grid -- disc decode 96% -> 100%
The "~15% deferred variants" were not variants. Auditing every T8aD on the disc
gave 19216 surfaces, 18442 decoding (96.0%) and 774 failing in two clusters:
GP_DIALOG strips declaring 524x63 with a "tile count" of 1 or 2 instead of 3, and
small textures in the six *2D language paks whose pixels ran past the end of the
file.

Both fall out of the per-tile header, which is not opaque flags: it is four BE
u32 -- dst X, dst Y, width, height. A 15x18 icon stores a 13x18 rectangle at
(1,0); pdmes010 stores (59,6,256,54) and (315,6,149,54), the second beginning
exactly 16 + 256*54*4 bytes after the first. So 0x1c is a RECTANGLE COUNT and the
256-grid reading was an accident of most surfaces being stored as full-width
bands.

Parser rewritten to that model, still refusing to guess: a rectangle must fit the
declared surface and its pixels must fit the file, else None. Disc decode is now
19216/19216 = 100.00%.

Two test fixtures were built to the old model and are corrected rather than
worked around. lsta's t8ad_frame wrote NO offset-table entry, so the decoder read
"pixels" from inside the header -- the test passed only because it checked
dimensions alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:37:21 +00:00

13 KiB
Raw Blame History

RE knowledge index

Confidence: CONFIRMED · 🟡 PROBABLE · HYPOTHESIS. See README.

Formats we've already reversed are, for now, documented by their parser + disc round-trip tests (the executable spec) rather than a prose file — the "Spec" column points there. Promote to a prose structures/…md file when a format needs behavioural notes beyond layout.

Data structures / formats

Format Conf. Spec (parser + tests) Notes
IPFB .pak archive sylpheed-formats/src/pak.rs + tests/pak_idxd_disc.rs header + 12-byte TOC, Z1/zlib payloads
name-hash (TOC keys) sylpheed-formats/src/hash.rs Barrett-reduction hash; recovers original paths
IDXD object/table sylpheed-formats/src/idxd.rs self-describing; ship/weapon stats verified vs known values
XPR2 texture + cubemap 🟡/ sylpheed-formats/src/texture.rs + colour check de-tile + A8R8G8B8 and DXT1. Channel order confirmed against the running game: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, zero cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering
T8aD 2D texture sylpheed-formats/src/t8ad.rs 100 % of the disc decodes (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of arbitrary sub-rectangles, each with a 16-byte header of dst X, dst Y, width, height, not a 256×256 grid — 0x1c is the rectangle count. Uncovered area stays transparent. Colours CONFIRMED (k8888)
RATC bundle 🟡 sylpheed-formats/src/ratc.rs child listing confirmed; one level deep
LSTA sprite list 🟡 sylpheed-formats/src/lsta.rs inline T8aD frames
IXUD subtitle 🟡/ sylpheed-formats/src/ixud.rs + movie link timed cues. The movie↔subtitle↔voice link is solved — statically, from the movie config record in tables.pak (schema 0x067025b9), not from the running game as this row previously assumed: 101 movies mapped, 94 with subtitles, 83 with voice, 21 with a telop overlay. 93 of 94 subtitle refs resolve in the language paks; SUBTITLE_S12B.tbl is missing from all six languages — a dangling reference on the disc. Naming is SUBTITLE_<base>.tbl / VOICE_<base> with six documented exceptions. The record's ~104 script ids are — positional pairing drifts by three because the IDXD pool dedupes repeated values
Fonts (ttf/otf/ttc) sylpheed-formats/src/font.rs standard OpenType, parsed via ttf-parser
XBG7 mesh 🟡/ sylpheed-formats/src/mesh.rs + tests/mesh_disc.rs (xbg7) weapons/props: declaration-driven variable stride (36 models), GPU-confirmed. Stage containers: 5662 sub-models across 22 stages via content-anchored grouped pools (stage_models). Quantized hero bodies (DeltaSaber f004) still declined
Capital-ship part placement 🟡 sylpheed-formats/src/ship.rs (static) + runtime capture hull placement static-exact; external parts approximate statically. Runtime capture (Canary F10 → VS-constant WorldView) gives ground truth — validated on e106 destroyer; not yet baked into the viewer
Weapon fields defaulted on disc runtime struct · DATA SHEET route Solved. Canary maps guest RAM into /dev/shm, so the parsed Weapon/Shell objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — 4 393 values the disc does not carry. Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check
Unit (craft/vessel) fields defaulted on disc /🟡 runtime struct The parsed unit\UN_*.tbl definition object, vtable 0x820af844, ≥0x380 bytes, one per unit — discovered, not assumed (unit_discover.py), and distinguished from the spawned-entity class 0x820af030 by being one-per-ID and byte-constant within a run. Across runs only pointer words move — --crosscheck proves no reported field offset is run-dependent (two words, +0x2c8/+0x2d0, are stage-dependent and remain unidentified). 27 fields (21 units, 7 runs); the Maneuver block is schema declaration order, 4 bytes/field, base 0x9c with a two-slot gap after AA_Roll_Min (29 anchors, 0 conflicts), which also pins 5 fields no disc record ever values. Angles are radians at runtime, degrees on disc. Unlike weapons, unit definitions are instantiated per stage, so coverage (21/110) grows by visiting missions — but a defaulted field is not a global constant: Size_Y provably inherits Size_X (7 independent units, 6 distinct values), and three more sibling rules are recorded , recovering 65 values in units never visited — values
Arsenal develop economy / arsenal-develop-economy + conditions The Arsenal reads weapon.tbl (item ids, in the 8-category display order) and strings.tbl (names, descriptions, and a "Conditions to obtain" block per item) out of GP_HANGAR_ARSENAL.pak. All 60 conditions are extracted: gates are stage completion, a predecessor item, or an ace kill; costs run 3 000350 000 P and 20 items are free once gated. weapon.tbl's first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD dedupes repeated values. Used to identify the save blob's index space, now solved: the blob follows strings.tbl's order — the display order plus the cut items only the localisation file lists (Adhesive Mine B2A, Ballista GSH, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. weapon.tbl's id list is not the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered
UI screen layout (.rat) /🟡 ui-rat-layout One pak per UI screen; each RATC = one (context × language) build; every <name>.t32 sprite has a <name>.rat layout record (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, opt link to the focused state). The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc. loop1.rat is decoded — it is a looping sprite animation, not a composition. The screen's draw list is the RATC bundle's own declaration table (elements in back-to-front order, including the eff*/deli*/msg sprites that have no .rat, and excluding focused button variants reached via opt ); its entry also carries a parent element index at +32. A screen is fully reconstructible from its bundle: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the .rat-less sprites — verified 11/11 on the tutorial pause bundle, with pgp_ttrl_btn10's inline (546,288) matching its own record exactly
Save file (savedata) / savegame-format + tools/re-capture/savegame.py GDHA container, zlib payload, chunk stream (GDAA / phase name / GHAD 122 B progress block / 16×20 B slot table / trailer). Container and layout read off the title's own serializer 0x822C00E8 and verified by a byte-identical round-trip; the whole save is 545 B. Payload offsets are also the live save object's offsets (save+8 GHAD, save+136 slots). A second save made in-game names Points (+24), flight time in ms (+4) and clear ratio % (+8) off the game's own Details panel; the payload is a pure function of game state (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 SHAB records are not the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. A third save, taken after developing exactly one Arsenal weapon (Light Machine Gun MG I, 4000 P), moves exactly three things: +24 Points 4101→101 (which separates it from +28, that did not move), +8 clear ratio 5→6 (so the ratio counts collection, not only stages), and two entries of the 54-byte blob — 2→4 for the item bought and 0→2 for the successor the game announced as newly developable, giving the blob its alphabet 0 locked / 2 developable / 4 developed (only the 4s are stored — 2 is re-derived at load). Saves can also be written back: three derived header fields (length at +0x30, payload length at +0x8c, adler32 at +0x8e) are all that stand between a parse and a hand-written save that the title loads, and savegame_edit.py re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the economy note

Runtime / dynamic-capture technique

Technique Conf. Spec Notes
Live guest-memory read tools/re-capture/gmem.py Canary backs the guest address space with /dev/shm/xenia_memory_*; guest VAs map in through Xenia's fixed table. Full-RAM search ~0.2 s (sparse, SEEK_DATA). No debugger, no emulator patch, game keeps running
IDXD object layout solver tools/re-capture/weapon_runtime.py Scan RAM for a class's vtable → enumerate its objects → brute-force (field, offset, encoding) against the disc records. Accepts a binding only on zero contradictions. Generalizes to any IDXD-backed definition
Live entity state, anchored on the definition tools/re-capture/own_state.py · autopilot An undamaged craft holds its definition's own numbers, so a solved definition field locates the matching live field without a value scan: definition HP (1500) → hull at position+0x154, confirmed by a trace across a death (30/60/90 per hit, negative at 0). Reusable for any live counter whose maximum the definition carries
Mission / escort state, every entity's hull tools/re-capture/mission_state.py · escort state hull = position + 0x154 is a property of the entity class, not of the player object: at t=0 it equals each entity's own definition HP across 7 classes and 5 distinct HP values (turret 100, fighter 500, destroyer 10000, cruiser 30000, ACROPOLIS 25000), falls under fire (780 damage events in 240 s), goes negative at death, and the object then leaves the heap. So an escort objective is scoreable live — UN_f101_TCAF_Acropolis measured at 25000 → 23038 over 240 s, attack starting only at t≈170 s. REMAINING OB counts objectives, not hostiles (012 on the HUD vs 118 live ADAN); its address is still
In-flight control mapping /🟡 tools/re-capture/fire_probe.sh · controls Measured by holding each pad input and photographing the HUD ammo counters: RB = nose gun (6000→5956 in 4 s, ~11 rounds/s, HEAT rises), Y = main mount (missiles, 300→299), d-pad = tactical map overlay, nothing else moves a counter. No target-cycle input exists — the TARGET marker is present with nothing pressed, so targeting is automatic and a missile lock is time-on-target. That, not target choice or ballistics, is what caps lethality at 2 kills per 98 missiles
Input → dynamics calibration tools/re-capture/ctrl_probe.py · binq.py Hold each pad input in turn and measure the craft's speed as displacement/s of its own position triple — no speed field needed first. Settled the throttle: RT accelerates, LT brakes, and the setting persists (488 → 1510 → 174 units/s), overturning an earlier field-scan conclusion

Functions / code paths

None documented yet — populated during the dynamic-RE phase.

Function Conf. Reimpl. Summary