Close Phase 3's level table: 27 keys, and 21 is not one of them

Picked the first open Phase 3 item. "Extend the level table from 19 to 27" is
about what the KEYS are, not how many there are.

ReRAC, which this project follows architecturally, has rc_game::map::LEVELS = 19
-- Ratchet & Clank 1's count, and this repo has no equivalent yet. Reading every
container header's own id off the disc gives:

    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 22 23 24 25 26 30

27 keys, and 21 is NOT one of them. The container files run LEVEL0.WAD through
LEVEL26.WAD, but LEVEL21.WAD reports id 30, so the id space is 0..20, 22..26, 30 --
one number was cut from the disc and reused by nothing. That matches what
LevelHeader::id already documents, now as the table itself.

THE REFERENCE AGREES, INDEPENDENTLY

Every level key appearing in OpenRAC/rac2-gc-decomp's config/levels/functions.tsv
placements column, across 7,765 function rows, is exactly that set with 21 absent:

    00..20, 22, 23, 24, 25, 26, 30

Ours comes from the container headers; theirs from where functions land across
overlays. Two unrelated routes to the same key set, which is the only reason to
trust a hand-typed table like this one.

Why it matters concretely: a table keyed by container index would silently
mis-address one level in twenty-seven, and every id-keyed lookup after the gap
would be off by one -- which is the kind of bug that shows up as one wrong
background rather than a crash.

MY OWN ARITHMETIC SLIPPED ONCE

The first run asserted the disc's ids against (0..=20).chain(22..=27).chain([30]),
which is 28 entries -- 22..=27 should be 22..=26. The test caught it against the
disc, which is the right way round. Worth noting that the reference key set I
extracted by hand earlier was correct and the Rust expression was not, so the hand
check and the code check disagreed in the right direction.

Asserted in level_table_has_twenty_seven_keys_with_twenty_one_absent, from the
disc rather than from a copied constant, so it fails if a container id moves.

269 tests, clippy clean.
This commit is contained in:
2026-10-14 04:08:56 +02:00
parent 88bc9566f2
commit 56d8dee836
2 changed files with 84 additions and 1 deletions
+74
View File
@@ -6197,3 +6197,77 @@ fn dispatch_tables_contain_far_fewer_behaviours_than_records() {
);
}
}
/// R&C2's level table: 27 entries, keyed by **level id**, not by file order.
///
/// ReRAC, which this project follows architecturally, has
/// `rc_game::map::LEVELS: usize = 19` -- Ratchet & Clank 1 has nineteen levels.
/// This game has twenty-seven, so "extend the level table from 19 to 27" is about
/// *what the keys are*, not only how many there are.
///
/// Reading every container header's own id off the disc gives the key set:
///
/// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 22 23 24 25 26 30
///
/// **27 keys, and 21 is not one of them.** The container files run `LEVEL0.WAD` to
/// `LEVEL26.WAD`, but `LEVEL21.WAD` reports id **30**, so the id space is
/// 0..20, 22..26, 30 -- one number was cut and reused by nothing.
///
/// The reference agrees independently. Every level key appearing in
/// `rac2-gc-decomp`'s `config/levels/functions.tsv` placements column, across 7,765
/// function rows, is exactly that set, with 21 absent. Ours comes from the
/// container headers and theirs from where functions land across overlays.
///
/// A table keyed by container index would silently mis-address one level in
/// twenty-seven, and every id-keyed lookup after the gap would be off by one.
#[test]
fn level_table_has_twenty_seven_keys_with_twenty_one_absent() {
let path = require_disc!();
let iso = gc_formats::iso9660::IsoImage::open(&path).expect("ISO should open");
let mut levels: Vec<(usize, &gc_formats::iso9660::DirEntry)> = iso
.files
.iter()
.filter(|e| e.name.starts_with("G/LEVEL"))
.map(|e| {
let n: usize = e
.name
.trim_start_matches("G/LEVEL")
.trim_end_matches(".WAD")
.parse()
.expect("level container names end in a number");
(n, e)
})
.collect();
levels.sort_by_key(|(n, _)| *n);
assert_eq!(
levels.len(),
27,
"the disc should carry 27 level containers"
);
let mut ids: Vec<i32> = levels
.iter()
.map(|(_, e)| {
let data = gc_formats::iso9660::read_at(&path, e.lba, 0x60).expect("read header");
gc_formats::level_wad::LevelHeader::parse(&data)
.expect("parse")
.id
})
.collect();
ids.sort_unstable();
let expected: Vec<i32> = (0..=20).chain(22..=26).chain([30]).collect();
eprintln!(
"level table: {} ids, 21 absent, highest {}",
ids.len(),
ids.last().unwrap()
);
assert_eq!(ids.len(), 27, "level id count moved");
assert!(
!ids.contains(&21),
"id 21 must not exist; LEVEL21.WAD reports 30"
);
assert_eq!(ids, expected, "the level-id set moved");
assert_eq!(*ids.last().unwrap(), 30, "the highest level id is 30");
}
+10 -1
View File
@@ -678,7 +678,16 @@ reference function's copy on another overlay.
and `copies`
- [x] `$gp` for this game (`0x001AEFF0`, against R&C1's `0x166c00`) -- confirmed
by `OpenRAC/rac2-gc-decomp` `docs/LEVELS.md` for all 27 levels
- [ ] Extend the level table from 19 to 27
- [x] **The level table has 27 keys, and 21 is not one of them.** ReRAC, which this
project follows, has `rc_game::map::LEVELS = 19` -- Ratchet & Clank 1's
count. Reading every container header's own id off the disc gives
`0..=20, 22..=26, 30`: the files run `LEVEL0.WAD`..`LEVEL26.WAD` but
`LEVEL21.WAD` reports **30**, so one number was cut and reused by nothing.
`OpenRAC/rac2-gc-decomp`'s `config/levels/functions.tsv` independently
yields exactly the same 27 keys with 21 absent across 7,765 function rows --
ours from container headers, theirs from where functions land across
overlays. A table keyed by container index would mis-address one level in
twenty-seven and shift every id-keyed lookup after the gap. Asserted.
**Gate:** every level-01 function's relocated copy is found on all 27 overlays;
cross-checked against an independent cluster hash.