Two class-header words are base-relative offsets pointing past the tie packets
Chasing where the AD-GIF is, sincef43a1c2established it is not in the packet. Two of the undecoded class-header words are offsets. base + 0x1c and base + 0x34 land inside core_data on 1,864 of 1,864 classes, so both are relative to the class base rather than absolute, and both are non-zero on every class. Both also land **past** that class's own tie packet blocks on every class, so they address per-class data held elsewhere in core_data rather than the tie geometry. That is consistent withf43a1c2: if the packet's own control region is not a tag stream, the per-class draw state is referenced from somewhere else. WHAT IS THERE, AND WHAT IS NOT CLAIMED The bytes are structured, not random -- class 3 shows a fixed-stride table with `80 01 00` recurring, class 2's field is a monotone byte table, class 5's has `38 38 38` and `ff 00 ff ff`. They read like compiled shader programs with small-integer parameters, or lookup tables. **Not identified, and deliberately not named.** None of those three fragments has been run against a control, and all three were picked because they looked like something. That is precisely the finding rule 4 refuses, and having spent this session on four retractions I would rather record an honest "structured, unidentified" than a plausible format that has not earned it. If any of these turns out to be a shader program the next step is a control, not a better-looking hex dump. ALSO SETTLED 0x24 is demonstrably not an offset: only 17 of 1,864 of its values land in range for core_data. Recorded as packed data. 0x28, 0x2c, 0x38 remain high-entropy and unexplained. 281 passing with the disc present (23 + 19 + 61 + 156 + 22), clippy clean.
This commit is contained in:
@@ -6539,3 +6539,85 @@ fn tie_class_header_tail_words() {
|
|||||||
);
|
);
|
||||||
eprintln!("0x3c histogram: {w3c:?}");
|
eprintln!("0x3c histogram: {w3c:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `0x1c` and `0x34` of the tie class header are base-relative offsets, and they
|
||||||
|
/// land past the class's own tie packets.
|
||||||
|
#[test]
|
||||||
|
fn tie_class_tail_offsets_point_past_the_tie_packets() {
|
||||||
|
require_disc!();
|
||||||
|
let data = std::env::var_os("GC_EXTRACTED")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
PathBuf::from(home).join("Documents/gc-extracted")
|
||||||
|
});
|
||||||
|
if !data.exists() {
|
||||||
|
eprintln!("skipped: unpack the disc first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
use gc_formats::level_core::{ClassEntry, LevelCoreHeader};
|
||||||
|
use gc_formats::tie::TieClassHeader;
|
||||||
|
|
||||||
|
let (mut classes, mut in_range, mut past_packets, mut both_nonzero) =
|
||||||
|
(0usize, 0usize, 0usize, 0usize);
|
||||||
|
for lv in 0..27usize {
|
||||||
|
let (Ok(idx), Ok(core)) = (
|
||||||
|
std::fs::read(data.join(format!("level{lv}/core_index.bin"))),
|
||||||
|
std::fs::read(data.join(format!("level{lv}/core_data.bin"))),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let t = LevelCoreHeader::parse(&idx).expect("hdr").tie_classes;
|
||||||
|
for c in 0..t.count as usize {
|
||||||
|
let o = t.offset as usize + c * 0x20;
|
||||||
|
let Ok(e) = ClassEntry::parse(&idx[o..o + 0x20]) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let base = e.offset_in_asset_wad as usize;
|
||||||
|
if base == 0 || base + 0x48 > core.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(hdr) = TieClassHeader::parse(&core[base..base + 0x48]) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
classes += 1;
|
||||||
|
let word = |k: usize| -> u32 {
|
||||||
|
let at = base + 0x1c + k * 4;
|
||||||
|
u32::from_le_bytes(core[at..at + 4].try_into().unwrap())
|
||||||
|
};
|
||||||
|
let (o1c, o34) = (word(0) as usize, word(6) as usize);
|
||||||
|
if usize::from(o1c) + 32 <= core.len() && o34 + 32 <= core.len() {
|
||||||
|
in_range += 1;
|
||||||
|
}
|
||||||
|
if word(0) != 0 && word(6) != 0 {
|
||||||
|
both_nonzero += 1;
|
||||||
|
}
|
||||||
|
// Past the class's own tie packet blocks.
|
||||||
|
let last_packet = hdr
|
||||||
|
.packets
|
||||||
|
.iter()
|
||||||
|
.filter(|p| **p > 0)
|
||||||
|
.max()
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0);
|
||||||
|
if o1c as i32 > last_packet && o34 as i32 > last_packet {
|
||||||
|
past_packets += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(classes > 1_800, "only {classes} tie classes read");
|
||||||
|
eprintln!("0x1c/0x34 in range {in_range}/{classes}, past own packets {past_packets}/{classes}");
|
||||||
|
// Both are relative to the class base, not absolute: base + offset lands in
|
||||||
|
// core_data, and absolute offsets would not.
|
||||||
|
assert!(
|
||||||
|
in_range * 100 >= classes * 99,
|
||||||
|
"only {in_range}/{classes} offsets in range"
|
||||||
|
);
|
||||||
|
assert_eq!(both_nonzero, classes, "a class has a zero offset");
|
||||||
|
// They are never inside the class's own tie data.
|
||||||
|
assert_eq!(
|
||||||
|
past_packets, classes,
|
||||||
|
"{classes} offsets fall inside the tie packets"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -111,8 +111,14 @@ impl TieClassHeader {
|
|||||||
// bfc_distance is flat across every bucket.
|
// bfc_distance is flat across every bucket.
|
||||||
// 0x40 a float -- 0.00208 to 10.84, no negatives. The bounding
|
// 0x40 a float -- 0.00208 to 10.84, no negatives. The bounding
|
||||||
// sphere radius or the class scale; not distinguished.
|
// sphere radius or the class scale; not distinguished.
|
||||||
// 0x1c, 0x34 look like offsets (0x1e0..0x3cd60, 0x190..0x31e20) and
|
// 0x1c and 0x34 are base-relative offsets: base + offset lands inside
|
||||||
// 0x24, 0x28, 0x2c, 0x38 are high-entropy. Unidentified.
|
// core_data on 1,864 of 1,864 classes, and always *past* the class's own
|
||||||
|
// tie packet blocks, so they address per-class data stored elsewhere.
|
||||||
|
// The bytes there are structured -- repeating small-integer records,
|
||||||
|
// occasional `ff 00 ff ff` -- but are not VIF or GIF DMA tags and are
|
||||||
|
// not identified. See tie_class_tail_offsets_point_past_the_tie_packets.
|
||||||
|
// 0x24, 0x28, 0x2c, 0x38 are high-entropy and unidentified; 0x24 is
|
||||||
|
// demonstrably not an offset (17 of 1,864 land in range).
|
||||||
b.skip(0x44 - 0x1c, "tie class fields not decoded yet")?;
|
b.skip(0x44 - 0x1c, "tie class fields not decoded yet")?;
|
||||||
let o_class = b.i32("tie class number")?;
|
let o_class = b.i32("tie class number")?;
|
||||||
Ok(TieClassHeader {
|
Ok(TieClassHeader {
|
||||||
|
|||||||
@@ -1849,3 +1849,41 @@ vertices, scissor. `bfc_distance` is a per-record quantity relating to the shade
|
|||||||
count whose meaning is not established, and the "quadwords of ad-gif data" reading
|
count whose meaning is not established, and the "quadwords of ad-gif data" reading
|
||||||
of it is wrong. The field doc said so in terms before this measurement; it does
|
of it is wrong. The field doc said so in terms before this measurement; it does
|
||||||
not now.
|
not now.
|
||||||
|
|
||||||
|
|
||||||
|
## Where the AD-GIF is not: class-header offsets 0x1c and 0x34
|
||||||
|
|
||||||
|
Two words in the class header's undecoded tail are offsets, and they are the
|
||||||
|
best remaining lead on the AD-GIF.
|
||||||
|
|
||||||
|
`base + 0x1c` and `base + 0x34` land inside `core_data` on **1,864 of 1,864
|
||||||
|
classes**, so both are relative to the class base rather than absolute, and both
|
||||||
|
are non-zero on every class. And both land **past** that class's own tie packet
|
||||||
|
blocks on every class -- they address per-class data stored somewhere else in
|
||||||
|
core_data, not the tie geometry.
|
||||||
|
|
||||||
|
What is there is structured, not random. Class 0 at 0x1c:
|
||||||
|
|
||||||
|
54 5d 5f 0c 44 44 44 01 03 11 19 00 1c 00 60 0a 30 08 60 0a
|
||||||
|
ff 00 ff ff 20 0c 50 02 44 38 2b 50
|
||||||
|
|
||||||
|
and class 3 at the same field:
|
||||||
|
|
||||||
|
20 05 c5 00 00 80 01 00 26 05 cb 00 21 88 01 00 2c 05 c0 00
|
||||||
|
00 80 01 00 32 05 c0 00 00 80 01 00
|
||||||
|
|
||||||
|
Class 3 is a fixed-stride table with `80 01 00` recurring. Class 5's 0x34 field
|
||||||
|
has `38 38 38` and `ff 00 ff ff`. Class 2's 0x1c field is a monotone byte table:
|
||||||
|
`26 27 0e 0f 10 11 12 13 24 25 15 1e 17 1f 14 15 16 17 20 21 22 23`.
|
||||||
|
|
||||||
|
These read like compiled shader programs with small-integer parameters, or as
|
||||||
|
lookup tables. **They are not identified**, and the resemblance to anything
|
||||||
|
specific is exactly the kind of finding rule 4 exists to refuse -- none of these
|
||||||
|
three fragments has been shown to beat a control, and they were picked because
|
||||||
|
they looked like something.
|
||||||
|
|
||||||
|
What is established is structural: two base-relative offsets, always in range,
|
||||||
|
always past the tie packets, pointing at repeated structured records rather than
|
||||||
|
DMA tags. That is consistent with the earlier finding that the packet's own
|
||||||
|
control region is not a tag stream -- the game's per-class draw state is stored
|
||||||
|
elsewhere and referenced, not inlined.
|
||||||
|
|||||||
Reference in New Issue
Block a user