Make the tie record relocation a command, and verify it on two levels
The relocation rule from the previous commit was established by a script that lived in a shell history, which means the next person would have had to rediscover it. It is now `gc-trace verify-tie-records <unpacked-dir> <level> <class> <lod> <src>`: resolve the class, parse the block, search the RAM image for the loaded record table, report what it found, and exit 0 only when every record's pointer has become table + word and every field byte is verbatim. Swept over every (class, lod) slot of two levels, from PCSX2 savestates: level 0: 87 slots with a block, 87 verified, 0 mismatched, 61 absent level 1: 104 slots with a block, 104 verified, 0 mismatched, 78 absent So the rule holds on every LOD block of two levels the game had loaded, which is what makes the record layout a finding rather than a reading of one block. Writing the command also caught an off-by-one in the parser it exercises: TieBlockHeader::records started its slice after the 16-byte header, but the header IS the first record -- the table runs from the block start to data_offset, and a block with data_offset 0x50 holds five records, the first being the header itself and its pointer naming where the payload begins. Found because the tool's first run relocated nothing while the same check in a script had passed; the unit test fixture was wrong in the same way and is fixed. Two operational notes now in the format doc: the live emulator is not a stable input (verifying level 1 over PINE matched 104 of 104 while the user was on that planet, and the same command minutes later found no matching copy because they had returned to level 0 -- a miss there is not a counterexample, so the report says "no copy matched" rather than "failed"), and a class index past the table count is an error rather than a mismatch, so a sweep should stop at the level's class count rather than a round number.
This commit is contained in:
@@ -192,15 +192,14 @@ impl TieBlockHeader {
|
||||
/// The block's records, as byte slices of [`TIE_RECORD_SIZE`].
|
||||
///
|
||||
/// Bounds-checked against `block`, which is the block starting at its
|
||||
/// header: the table runs from the end of the header to `data_offset`. A
|
||||
/// header whose table runs past the block yields nothing rather than a
|
||||
/// short slice.
|
||||
/// header. **The header is the first record**: the table runs from the
|
||||
/// block start to `data_offset`, so a block with `data_offset == 0x50`
|
||||
/// holds five records, the first of which is the header itself and whose
|
||||
/// pointer names where the payload begins. A header whose table runs past
|
||||
/// the block yields nothing rather than a short slice.
|
||||
pub fn records<'b>(&self, block: &'b [u8]) -> impl Iterator<Item = &'b [u8]> + 'b {
|
||||
block
|
||||
.get(
|
||||
TIE_BLOCK_HEADER_SIZE
|
||||
..TIE_BLOCK_HEADER_SIZE + self.record_count() * TIE_RECORD_SIZE,
|
||||
)
|
||||
.get(..self.record_count() * TIE_RECORD_SIZE)
|
||||
.unwrap_or(&[])
|
||||
.chunks_exact(TIE_RECORD_SIZE)
|
||||
}
|
||||
@@ -411,11 +410,13 @@ mod tests {
|
||||
assert_eq!(h.lod_for_depth(50.0), None);
|
||||
}
|
||||
|
||||
/// A block whose data offset names a three-record table.
|
||||
/// A block whose data offset names a four-record table, the first of
|
||||
/// which is the header itself.
|
||||
fn block() -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
// data_offset 0x30, reserved 0, packed, then the remaining fields.
|
||||
v.extend_from_slice(&0x30u16.to_le_bytes());
|
||||
// data_offset 0x40 -- four records -- reserved 0, packed, then the
|
||||
// remaining fields.
|
||||
v.extend_from_slice(&0x40u16.to_le_bytes());
|
||||
v.extend_from_slice(&0u16.to_le_bytes());
|
||||
v.extend_from_slice(&0x0501u16.to_le_bytes());
|
||||
v.extend_from_slice(&0x1234u16.to_le_bytes());
|
||||
@@ -440,12 +441,13 @@ mod tests {
|
||||
let b = block();
|
||||
let h = TieBlockHeader::parse(&b).unwrap();
|
||||
assert!(h.invariants_hold());
|
||||
assert_eq!(h.record_count(), 3);
|
||||
assert_eq!(h.record_count(), 4);
|
||||
let records: Vec<_> = h.records(&b).collect();
|
||||
assert_eq!(records.len(), 3);
|
||||
assert_eq!(records.len(), 4);
|
||||
// The first record is the header itself, pointing at the payload.
|
||||
let first = TieRecord::parse(records[0]).unwrap();
|
||||
assert_eq!(first.pointer, 0x40);
|
||||
assert_eq!(first.fields, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
||||
assert_eq!(first.pointer, 0x40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user