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) <noreply@anthropic.com>
This commit is contained in:
29
crates/sylpheed-formats/examples/pool_window.rs
Normal file
29
crates/sylpheed-formats/examples/pool_window.rs
Normal file
@@ -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 <disc-root> <ID substring> <key>
|
||||||
|
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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
non-zero defaults (`Size_Y` ← `Size_X`) are specific derivations to be found
|
||||||
individually in this filler. Worth noting for the reimplementation regardless:
|
individually in this filler. Worth noting for the reimplementation regardless:
|
||||||
**angle fields are stored in radians at runtime and in degrees on disc.**
|
**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).
|
||||||
|
|||||||
Reference in New Issue
Block a user