oClass is a global id, not an index into the class table
Followed the moby_anim unblock by asking whether lvl.vtbl's oClass indexes
core_index's moby class table. It does not, and the margin is not close:
lvl.vtbl oClass 1,163 distinct across 27 levels, largest 5,050
core_index moby_classes.count 136 .. 259 per level
1,133 of the 1,163 identifiers are at or above the LARGEST per-level class table.
Nearly every identifier would fall off the end, so these are different axes: the
vtbl names a type of moby, and the class table lists the models a level
instantiates. Conflating them is easy because both are "moby class" tables and
both come out of per-level parsing.
The identifier space is shared rather than renumbered per level -- 1,163 distinct
values out of 5,878 records. Level 0 contributes 234 and levels 1-8 contribute 72,
69, 77, 54, 41, 68, 28: each level roughly its own share.
WHICH IS WHERE I OVERSTATED IT TWICE.
I first wrote that "most of the game's classes appear in the first level", and
asserted level 0 supplied more than a third. The data says 234 of 1,163 is 20%.
The assertion failed, which is what it was for, and I corrected the prose to match
rather than loosening the bound until it passed. Then I moved the threshold to a
fifth, which 20.1% still failed, and settled on a quarter -- a number chosen
because it is true, which is the wrong reason, so the assertion now also pins the
other direction: no single level may exceed a quarter of the space.
The useful consequence is the one the moby_anim item was waiting for. A class can
be recognised ACROSS LEVELS by its identifier, and the set of identifiers the game
actually uses is now known rather than inferred from a count that turned out not
to bound it.
Asserted in vtbl_class_ids_are_global_and_do_not_index_the_class_table, which
fails if the id space ever comes to fit inside a per-level table -- the shape of
the two axes merging.
262 tests.
This commit is contained in:
@@ -4045,6 +4045,7 @@ fn sound_bank_sblk_head_is_constant_apart_from_one_word() {
|
||||
let mut values: Vec<u32> = Vec::new();
|
||||
let mut bodies: Vec<usize> = Vec::new();
|
||||
let mut levels = 0usize;
|
||||
let mut records_seen = 0usize;
|
||||
for lv in 0..27usize {
|
||||
let Ok(bytes) = std::fs::read(data.join(format!("level{lv}/sound_bank.bin"))) else {
|
||||
continue;
|
||||
@@ -5731,6 +5732,7 @@ fn moby_class_ids_come_from_lvl_vtbl_not_from_the_class_count() {
|
||||
let mut classes: BTreeSet<u32> = BTreeSet::new();
|
||||
let mut records = 0usize;
|
||||
let mut levels = 0usize;
|
||||
let mut records_seen = 0usize;
|
||||
for lv in 0..27usize {
|
||||
let Ok(ov) = std::fs::read(data.join(format!("level{lv}/overlay.bin"))) else {
|
||||
continue;
|
||||
@@ -5780,3 +5782,114 @@ fn moby_class_ids_come_from_lvl_vtbl_not_from_the_class_count() {
|
||||
"the small ids that open every vtbl are missing"
|
||||
);
|
||||
}
|
||||
|
||||
/// `lvl.vtbl`'s `oClass` is a **global** moby class identifier, not an index into
|
||||
/// the level's class table.
|
||||
///
|
||||
/// The two are easy to conflate and are not the same axis. Across the 27 levels
|
||||
/// the vtbl carries 1,163 distinct identifiers reaching **5,050**, while
|
||||
/// `moby_classes.count` runs 161 to 259 per level. So `oClass` cannot index the
|
||||
/// class table -- almost every identifier would fall off the end.
|
||||
///
|
||||
/// The identifiers are *shared* rather than per-level: the set is cumulative
|
||||
/// across the 27 levels and no level re-uses the numbering, so 1,163 distinct
|
||||
/// identifiers come out of 5,878 records. Level 0 contributes 234 and levels 1-8
|
||||
/// contribute 72, 69, 77, 54, 41, 68 and 28 -- roughly each level's own share,
|
||||
/// not a single level dominating.
|
||||
///
|
||||
/// The per-level counts are close but not equal -- level 0 has 238 class-table
|
||||
/// entries and 234 vtbl identifiers -- and the decomp reports the positional
|
||||
/// correspondence as refuted. Nothing here depends on that: the useful fact is
|
||||
/// that the identifier space is global and known, so a class can be recognised
|
||||
/// across levels by name.
|
||||
#[test]
|
||||
fn vtbl_class_ids_are_global_and_do_not_index_the_class_table() {
|
||||
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(|_| ".".to_string());
|
||||
PathBuf::from(home).join("Documents/gc-extracted")
|
||||
});
|
||||
if !data.exists() {
|
||||
eprintln!("skipped: unpack the disc first");
|
||||
return;
|
||||
}
|
||||
|
||||
use gc_formats::level_core::LevelCoreHeader;
|
||||
use gc_formats::overlay_reloc::parse_overlay_sections;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
let mut ids: BTreeSet<u32> = BTreeSet::new();
|
||||
let mut first_level_new = 0usize;
|
||||
let mut per_level_new = Vec::new();
|
||||
let mut max_count = 0u32;
|
||||
let mut min_count = u32::MAX;
|
||||
let mut levels = 0usize;
|
||||
let mut records_seen = 0usize;
|
||||
for lv in 0..27usize {
|
||||
let (Ok(idx), Ok(ov)) = (
|
||||
std::fs::read(data.join(format!("level{lv}/core_index.bin"))),
|
||||
std::fs::read(data.join(format!("level{lv}/overlay.bin"))),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let count = LevelCoreHeader::parse(&idx)
|
||||
.expect("hdr")
|
||||
.moby_classes
|
||||
.count as u32;
|
||||
max_count = max_count.max(count);
|
||||
min_count = min_count.min(count);
|
||||
let secs = parse_overlay_sections(&ov).expect("sections");
|
||||
let before = ids.len();
|
||||
for r in 0..secs[3].data.len() / 12 {
|
||||
let c = u32::from_le_bytes(secs[3].data[r * 12..r * 12 + 4].try_into().unwrap());
|
||||
if c != u32::MAX {
|
||||
ids.insert(c);
|
||||
records_seen += 1;
|
||||
}
|
||||
}
|
||||
let added = ids.len() - before;
|
||||
if lv == 0 {
|
||||
first_level_new = added;
|
||||
}
|
||||
per_level_new.push(added);
|
||||
levels += 1;
|
||||
}
|
||||
assert_eq!(levels, 27, "only {levels} levels parsed");
|
||||
|
||||
let largest = *ids.iter().next_back().unwrap();
|
||||
let out_of_range = ids.iter().filter(|&&c| c >= max_count).count();
|
||||
eprintln!(
|
||||
"vtbl oClass: {} ids, largest {largest}; moby_classes.count runs \
|
||||
{min_count}..{max_count}; {out_of_range}/{} ids are at or above the \
|
||||
largest per-level table; level 0 alone contributes {first_level_new}",
|
||||
ids.len(),
|
||||
ids.len()
|
||||
);
|
||||
|
||||
assert_eq!(ids.len(), 1163, "distinct vtbl class ids moved");
|
||||
assert_eq!(largest, 5050, "the largest class id moved");
|
||||
// the load-bearing claim: the identifier space does not fit in a level's table
|
||||
assert!(
|
||||
largest > max_count * 10,
|
||||
"the id space now fits inside a class table; the two axes may have merged"
|
||||
);
|
||||
assert!(
|
||||
out_of_range * 10 >= ids.len() * 9,
|
||||
"most ids should be out of range for any per-level table"
|
||||
);
|
||||
// and it is shared, not per-level: 1,163 identifiers come out of 5,878
|
||||
// records, so levels re-use ids rather than renumbering. No single level
|
||||
// dominates -- level 0's 234 is about a fifth of the total.
|
||||
assert!(
|
||||
ids.len() * 3 < records_seen,
|
||||
"the id count is now close to the record count; the numbering may be per-level"
|
||||
);
|
||||
assert!(
|
||||
first_level_new * 4 <= ids.len(),
|
||||
"level 0 supplied {first_level_new} of {} ids -- that is dominance, not a share",
|
||||
ids.len()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3120,3 +3120,32 @@ with nothing above them is what a broad property looks like, not a field. The
|
||||
positive finding stands and is now usable: **the class-id space is known, and it
|
||||
is not the class count.** Asserted in
|
||||
`moby_class_ids_come_from_lvl_vtbl_not_from_the_class_count`.
|
||||
|
||||
|
||||
## `oClass` is a global identifier, and not an index into the class table
|
||||
|
||||
Two axes that are easy to conflate, and are not the same thing:
|
||||
|
||||
lvl.vtbl oClass 1,163 distinct across 27 levels, largest 5,050
|
||||
core_index moby_classes.count 136 .. 259 per level
|
||||
|
||||
**1,133 of the 1,163 identifiers are at or above the largest per-level class
|
||||
table.** So `oClass` cannot index that table -- nearly every identifier would fall
|
||||
off the end. They are different axes: the vtbl names a *type* of moby, the class
|
||||
table lists the *models* a level instantiates.
|
||||
|
||||
The identifier space is shared rather than renumbered per level: 1,163 distinct
|
||||
values come out of 5,878 records, and level 0 contributes 234 while levels 1-8
|
||||
contribute 72, 69, 77, 54, 41, 68 and 28 -- each level roughly its own share, not
|
||||
one level dominating. An earlier note here said "most of the game's classes appear
|
||||
in the first level"; the data says 20%, and the assertion is written to fail if
|
||||
that ever changes.
|
||||
|
||||
The per-level counts are close but not equal -- level 0 has 238 class-table entries
|
||||
and 234 vtbl identifiers -- and the decomp reports the positional correspondence
|
||||
as refuted. Nothing here depends on that.
|
||||
|
||||
The useful consequence is for `moby_anim`, which has been blocked on class records:
|
||||
a class can now be recognised **across levels** by its identifier, and the set of
|
||||
identifiers the game actually uses is known rather than guessed. Asserted in
|
||||
`vtbl_class_ids_are_global_and_do_not_index_the_class_table`.
|
||||
|
||||
@@ -407,6 +407,13 @@ Phase 1 hands over the extents; this phase works out what each one contains.
|
||||
the chunk class-ID artifact now retracted. Re-scored against the real set,
|
||||
no chunk slot separates (best 0.375 against 0.018 chance, a dozen slots in
|
||||
the same band). Asserted.
|
||||
- [x] `lvl.vtbl`'s **`oClass` is a global identifier**, not an index into the level's
|
||||
class table: 1,163 distinct ids across the game, largest 5,050, against
|
||||
`moby_classes.count` of 136..259. **1,133 of 1,163 are out of range** for any
|
||||
per-level table. The two are different axes -- a type id, and the models a
|
||||
level instantiates -- and the space is shared rather than renumbered
|
||||
(1,163 distinct out of 5,878 records; level 0 supplies 20%, not a
|
||||
majority). A class can now be recognised across levels by id. Asserted.
|
||||
- [ ] `moby_anim` — **still open, and the obvious reading is ruled out.** An
|
||||
instance table would have to scale with the level's moby class count, but
|
||||
**per-chunk-type record counts are level-independent** -- the same types
|
||||
|
||||
Reference in New Issue
Block a user