From 2ed475868b65bf09d1ff40028e80b86b11b2cf09 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Wed, 12 Aug 2026 22:47:43 +0000 Subject: [PATCH] re: Size_Y is genuinely defaulted, and the loader leaves it at 0.0 pool_window.rs shows the Destroyer's raw tokens: "200.0" "Size_X" "Size_Y" "2000.0" "Size_Z" -- Size_Y is a bare key, so the 13 runtime values recovered earlier are real defaults, not a reader artefact. The filler reads each size field through sub_822FC5A8, which loads f31 from 0x8209fd28 = 0.0 at entry and returns it on a pool miss, then stores to +52. Nothing else in the 15876-byte filler writes +52, so the definition leaves a defaulted Size_Y at 0.0 and the runtime 200 is written later. Two corrections: the +48/+52/+56 comparison block builds a size-class bitmask against 1000.0 (constant 0x8209fd20), not a has-value mask; and both earlier Size_Y derivation candidates are refuted (one is a conditional pick, the other multiplies into a different struct). Co-Authored-By: Claude Opus 5 (1M context) --- .../sylpheed-formats/examples/pool_window.rs | 29 +++++++++++++ docs/re/live-unit-definitions.md | 43 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 crates/sylpheed-formats/examples/pool_window.rs diff --git a/crates/sylpheed-formats/examples/pool_window.rs b/crates/sylpheed-formats/examples/pool_window.rs new file mode 100644 index 0000000..1cd2dd7 --- /dev/null +++ b/crates/sylpheed-formats/examples/pool_window.rs @@ -0,0 +1,29 @@ +//! Raw token sequence around a key, for one record — the ground truth for +//! "is this field actually defaulted, or is our reader missing it?" +//! Run: pool_window +use sylpheed_formats::idxd::IdxdObject; +use sylpheed_formats::pak::PakArchive; +fn main() { + let disc = std::env::args().nth(1).unwrap(); + let want = std::env::args().nth(2).unwrap(); + let key = std::env::args().nth(3).unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + for e in pak.entries() { + let Ok(b) = pak.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Some(id) = o.get_raw("ID") else { continue }; + if !id.contains(&want) { continue; } + let t = o.tokens(); + println!("=== {id} get_f32({key}) = {:?}", o.get_f32(&key)); + for (i, tok) in t.iter().enumerate() { + if tok == &key { + let lo = i.saturating_sub(6); + let hi = (i + 7).min(t.len()); + for j in lo..hi { + println!(" [{j}]{} {:?}", if j == i { " <-- key" } else { " " }, t[j]); + } + println!(); + } + } + } +} diff --git a/docs/re/live-unit-definitions.md b/docs/re/live-unit-definitions.md index 5b3e26d..b2fc865 100644 --- a/docs/re/live-unit-definitions.md +++ b/docs/re/live-unit-definitions.md @@ -293,3 +293,46 @@ default is 0**, straight from the zero-filling constructor, and the interesting non-zero defaults (`Size_Y` ← `Size_X`) are specific derivations to be found individually in this filler. Worth noting for the reimplementation regardless: **angle fields are stored in radians at runtime and in degrees on disc.** + +## Confirming the field really is defaulted, and where the loader leaves it + +Before chasing the derivation any further it was worth checking the premise. +`examples/pool_window.rs` prints the raw token sequence around a key, and for the +Destroyer it reads: + +``` +[14] "200.0" [15] "Size_X" [16] "Size_Y" [17] "2000.0" [18] "Size_Z" +``` + +`Size_X` takes the value before it; **`Size_Y` is a bare key** — genuinely +defaulted, exactly as the value-before-key rule predicts. So the 13 values +recovered from the running game are real defaults, not a reader artefact. + +The loader's own answer for a missing field is now pinned too. Each size field is +filled by the same three-call sequence — build the key string, call the float +accessor **`sub_822FC5A8`**, store the result: + +``` +0x82341d94 addi r4, r30, -14004 ; "Size_Y" +0x82341da0 bl 0x8217FA08 ; make key +0x82341dac bl 0x822FC5A8 ; read float +0x82341db0 stfs f1, 52(r29) ; -> Size_Y +``` + +and `sub_822FC5A8` loads `f31` from `0x8209fd28` = **0.0** at entry and returns it +when the pool lookup misses. **So the definition leaves a defaulted `Size_Y` at +0.0**, and nothing else in the 15 876-byte filler writes `+52`. + +Two readings corrected on the way: + +- The block at `0x82341e4c`–`0x82341ebc` that loads `+48`/`+52`/`+56` is **not** a + "was this field set" mask. It compares each against the constant at + `0x8209fd20` = **1000.0** and ORs flags into `+108` — a **size-class bitmask** + for large objects. +- Both earlier `Size_Y` candidates are refuted: `0x82217e18` conditionally picks + between two registers rather than copying, and `0x823081e8` *multiplies* `+48` + by a table constant into `+52` of a different structure. + +So the runtime `Size_Y = Size_X` must be written **after** the definition is +loaded — by whatever instantiates a unit from it, or a post-load pass over the +table. `+108` itself is too generic to chase (1 129 loads image-wide).