Transcribe the 256-byte MobyInstance; oClass closes the instance-to-routine loop

The decomp carries an authentic MobyInstance layout of 0x100 bytes with every
field named. It is now in gc_formats::moby with a 67-entry offset map, attributed
as a transcription rather than presented as our own finding.

Two fields matter:

    0x40  animSeq      animation sequence pointer
    0x44  animSeqT     animation time parameter
    0x48  animSpeed    playback speed
    0x50  animLayers   animation layer list
    0x57  jointCnt     joint count -- non-zero means the moby is skinned
    0x58  jointCache   joint matrix cache
    0xBC  oClass       object class id -- the dispatch key

OCLASS CLOSES A LOOP THAT HAS BEEN OPEN FOR SEVERAL COMMITS. It is the field the
runtime uses to reach the lvl.vtbl handler tables, so an instance and its routine
are now linked by a named offset, rather than by an assumption about which axis
the two tables share. That assumption -- the positional correspondence -- is still
refuted, and still does not need to be true.

jointCnt and jointCache settle something from earlier in this log BY CONTRADICTION
rather than confirmation. A tie vertex's i16[3] was tested as a skin/joint index
and disproved: vertices sharing it were 0.95x FARTHER apart than vertices differing
in it. That stands and is unaffected -- this is the moby struct, and a moby is a
different object from a tie. Recording the distinction rather than letting "joints"
appear in two places look like a confirmation of the earlier hypothesis.

WHAT THIS DOES NOT DO IS FIND moby_anim. It names the instance-side state, so the
asset to locate is whatever those pointers reference, and we still do not know
where that is. The item's unblock condition is now sharper -- it needs the data
behind those pointers, not the struct.

PROVENANCE, AND WHAT THE TEST CAN ACTUALLY CHECK

This is not verified against disc data and cannot be: moby instances are runtime
objects and nothing of that shape is in the container. The test asserts what can be
asserted without the bytes -- offsets ascend, do not overlap, the last field lands
exactly on 0x100, and o_class sits at 0xBC immediately before triggers.

Worth keeping distinct from the collision finding. That project's FUNCTION ADDRESSES
did not transfer: they landed in NOBITS memory. A struct's field order is a
different kind of claim from an address in a program's layout, so one failing does
not impeach the other -- but the distinction is real, and PROVENANCE.md now records
which is which rather than citing the project as a single authority.

268 tests.
This commit is contained in:
2026-10-14 03:47:15 +02:00
parent 3785195ecd
commit f182a4a0ac
4 changed files with 360 additions and 1 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ code, and those notices travel with anything derived from it.
| Project | Role | Licence | Code copied into this repo? |
|---|---|---|---|
| [OpenRAC/rac2-gc-decomp](https://github.com/OpenRAC/rac2-gc-decomp) | Byte-exact reconstruction research for the same disc (SHA-256 verified identical) -- supplied `$gp`, the `.lit` load base and the seven-section program layout | GPL-3.0-or-later | Not yet; vendored read-only at `vendor/rac2-gc-decomp` |
| [OpenRAC/rac2-gc-decomp](https://github.com/OpenRAC/rac2-gc-decomp) | Byte-exact reconstruction research for the same disc (SHA-256 verified identical) -- supplied `$gp`, the `.lit` load base, the seven-section program layout, the `lvl.vtbl`/`lvl.camvtbl`/`lvl.sndvtbl` dispatch record shapes, and the 256-byte `MobyInstance` field map transcribed into `gc_formats::moby` | GPL-3.0-or-later | Not yet; vendored read-only at `vendor/rac2-gc-decomp`. Note: its *function addresses* did not transfer (they land in NOBITS memory); its *struct layouts* are used as layout claims, not verified against disc bytes |
| [ReRAC](https://github.com/re-rac/rerac) | Native Rust/Bevy port of Ratchet & Clank (2002) — the architecture this follows | ISC | Not yet; derived from, attribute when used |
| [OpenRAC / rac2-gc-decomp](https://github.com/OpenRAC/rac2-gc-decomp) | Matching decompilation research for this game | MIT | No — consulted for documentation |
| [rac2-gc-decomp community reference](https://github.com/OpenRAC/rac2-gc-decomp/blob/RAC2/docs/COMMUNITY-ENGINE-REFERENCE.md) | Engine architecture notes, moby struct layout | MIT | Documentation only |
+304
View File
@@ -236,3 +236,307 @@ mod tests {
assert!(g.is_empty());
}
}
/// The runtime moby instance: 256 bytes, every field named.
///
/// **Transcribed from `OpenRAC/rac2-gc-decomp`'s `include/structs.h`, not
/// derived here.** That project's disc SHA-256 matches ours exactly (both
/// `9db2e33e...b1ce5`, USA v1.01), and it states this layout as authentic and
/// 0x100 bytes.
///
/// Two things about its status, stated rather than glossed:
///
/// * This is a **layout claim from a third party**, and it is not verified
/// against disc data here. It cannot be: moby instances are runtime objects
/// built by the game, and nothing of that shape is in the container. The test
/// alongside asserts the offset table is internally well-formed -- ascending,
/// non-overlapping, summing to 0x100 -- which is what can be checked without
/// the bytes.
/// * The same project's **function addresses** proved unusable for collision,
/// landing in NOBITS memory. That was a different kind of claim (an address in
/// a program's layout, rather than a struct's field order) and it does not
/// impeach this one, but the distinction is worth keeping.
///
/// The field that matters most for the open `moby_anim` item is at `0x40`: the
/// animation block. `animSeq`, `animLayers`, `jointCnt` and `jointCache` together
/// say that a moby carries a *runtime* animation state -- a sequence pointer, a
/// layer list and a joint matrix cache. The asset those point at is still what
/// needs locating; the instance side is now named rather than inferred.
///
/// `oClass` at `0xBC` is the other one: it is the field the runtime dispatch uses,
/// and it is what links an instance to the `lvl.vtbl` handler tables.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MobyInstance {
/// `0x00`, 16 bytes: bounding sphere in world space.
pub b_sphere: [u8; 0x10],
/// `0x10`, 16 bytes: world position x, y, z, w.
pub pos: [u8; 0x10],
/// `0x20`: current moby state (`0xFE` free, `0xFF` tail).
pub state: u8,
/// `0x21`: collision / grouping index.
pub group: u8,
/// `0x22`: class sub-type.
pub m_class: u8,
/// `0x23`: opacity / blend alpha, default `0x80`.
pub alpha: u8,
/// `0x24`: pointer to the class definition.
pub p_class: u32,
/// `0x28`: next moby in the chain or update list.
pub p_chain: u32,
/// `0x2C`: collision damage value.
pub coll_damage: u8,
/// `0x2D`: death counter.
pub death_cnt: u8,
/// `0x2E`: occlusion index.
pub occl_index: u16,
/// `0x30`: update distance.
pub update_dist: u8,
/// `0x31`: drawn flag.
pub drawn: u8,
/// `0x32`: draw distance threshold.
pub draw_dist: u16,
/// `0x34`: mode flags (`0x40` = no pre-update).
pub mode_bits: u16,
/// `0x36`: secondary mode flags.
pub mode_bits2: u16,
/// `0x38`: light bitmask or colour.
pub lights: u32,
/// `0x40`: animation sequence pointer.
pub anim_seq: u32,
/// `0x44`: animation time parameter.
pub anim_seq_t: f32,
/// `0x48`: animation playback speed.
pub anim_speed: f32,
/// `0x4C`: animation interpolation scale.
pub anim_i_scale: i16,
/// `0x4E`: pose cache index.
pub pose_cache_entry_index: i16,
/// `0x50`: animation layer list.
pub anim_layers: u32,
/// `0x54`: active sequence id.
pub anim_seq_id: u8,
/// `0x55`: animation flags.
pub anim_flags: u8,
/// `0x56`: loop / layer sequence.
pub l_seq: u8,
/// `0x57`: joint count. Non-zero means the moby is skinned.
pub joint_cnt: u8,
/// `0x58`: joint matrix cache.
pub joint_cache: u32,
/// `0x5C`: IK / joint manipulator.
pub p_manipulator: u32,
/// `0x60`: glow colour, RGBA.
pub glow_rgba: u32,
/// `0x64`: LOD transition.
pub lod_trans: u8,
/// `0x65`: secondary LOD transition.
pub lod_trans2: u8,
/// `0x66`: metal surface flag.
pub metal: u8,
/// `0x67`: sub-state.
pub sub_state: u8,
/// `0x68`: previous state.
pub prev_state: u8,
/// `0x69`: state type.
pub state_type: u8,
/// `0x6A`: frames in the current state.
pub state_timer: u16,
/// `0x6C`: sound trigger.
pub sound_trigger: u8,
/// `0x6D`: sound desired.
pub sound_desired: u8,
/// `0x6E`: audio channel index.
pub sound_channel: u16,
/// `0x70`: actor scale.
pub scale: f32,
/// `0x74`: bangles / attachment bits.
pub bangles: u16,
/// `0x76`: shadow flag.
pub shadow: u8,
/// `0x77`: shadow texture index.
pub shadow_index: u8,
/// `0x78`: shadow ground plane Y.
pub shadow_plane: f32,
/// `0x7C`: shadow max distance.
pub shadow_range: f32,
/// `0x80`, 16 bytes: local bounding sphere.
pub l_sphere: [u8; 0x10],
/// `0x90`: network / multiplayer object.
pub net_object: u32,
/// `0x94`: update tick id.
pub update_id: u16,
/// `0x96`: scratchpad scratch variable.
pub spad0: u16,
/// `0x98`: collision mesh or pill data.
pub coll_data: u32,
/// `0x9C`: active collision bitmask.
pub coll_active: u32,
/// `0xA0`: collision check count.
pub coll_cnt: i32,
/// `0xA4`: PVS grid bounds, minimum X.
pub grid_min_x: u8,
/// `0xA5`: PVS grid bounds, minimum Y.
pub grid_min_y: u8,
/// `0xA6`: PVS grid bounds, maximum X.
pub grid_max_x: u8,
/// `0xA7`: PVS grid bounds, maximum Y.
pub grid_max_y: u8,
/// `0xA8`: per-tick update function.
pub p_update: u32,
/// `0xAC`: private moby state, `0x80` bytes per moby.
pub p_var: u32,
/// `0xB0`: mission id.
pub mission: u8,
/// `0xB1`: padding.
pub pad: u8,
/// `0xB2`: unique instance id within the level.
pub uid: i16,
/// `0xB4`: bolt drop reward.
pub bolts: i16,
/// `0xB6`: experience / nanotech reward.
pub xp: u16,
/// `0xB8`: parent moby pointer.
pub p_parent: u32,
/// `0xBC`: object class id -- the dispatch key into `lvl.vtbl`.
pub o_class: i16,
/// `0xBE`: trigger flags.
pub triggers: u8,
/// `0xBF`: death handler invoked flag.
pub standarddeathcalled: u8,
/// `0xC0`, 48 bytes: 3x4 orientation matrix.
pub r_mtx: [u8; 0x30],
/// `0xF0`, 16 bytes: Euler rotation, pitch / yaw / roll.
pub rot: [u8; 0x10],
}
/// Size of [`MobyInstance`], as the reference states it.
pub const MOBY_INSTANCE_SIZE: usize = 0x100;
/// The named offsets of [`MobyInstance`], in ascending order.
///
/// Used by the layout test, and useful on its own: it is the field map without
/// the payload types.
pub const MOBY_INSTANCE_FIELDS: &[(usize, &str)] = &[
(0x00, "b_sphere"),
(0x10, "pos"),
(0x20, "state"),
(0x21, "group"),
(0x22, "m_class"),
(0x23, "alpha"),
(0x24, "p_class"),
(0x28, "p_chain"),
(0x2C, "coll_damage"),
(0x2D, "death_cnt"),
(0x2E, "occl_index"),
(0x30, "update_dist"),
(0x31, "drawn"),
(0x32, "draw_dist"),
(0x34, "mode_bits"),
(0x36, "mode_bits2"),
(0x38, "lights"),
(0x40, "anim_seq"),
(0x44, "anim_seq_t"),
(0x48, "anim_speed"),
(0x4C, "anim_i_scale"),
(0x4E, "pose_cache_entry_index"),
(0x50, "anim_layers"),
(0x54, "anim_seq_id"),
(0x55, "anim_flags"),
(0x56, "l_seq"),
(0x57, "joint_cnt"),
(0x58, "joint_cache"),
(0x5C, "p_manipulator"),
(0x60, "glow_rgba"),
(0x64, "lod_trans"),
(0x65, "lod_trans2"),
(0x66, "metal"),
(0x67, "sub_state"),
(0x68, "prev_state"),
(0x69, "state_type"),
(0x6A, "state_timer"),
(0x6C, "sound_trigger"),
(0x6D, "sound_desired"),
(0x6E, "sound_channel"),
(0x70, "scale"),
(0x74, "bangles"),
(0x76, "shadow"),
(0x77, "shadow_index"),
(0x78, "shadow_plane"),
(0x7C, "shadow_range"),
(0x80, "l_sphere"),
(0x90, "net_object"),
(0x94, "update_id"),
(0x96, "spad0"),
(0x98, "coll_data"),
(0x9C, "coll_active"),
(0xA0, "coll_cnt"),
(0xA4, "grid_min_x"),
(0xA5, "grid_min_y"),
(0xA6, "grid_max_x"),
(0xA7, "grid_max_y"),
(0xA8, "p_update"),
(0xAC, "p_var"),
(0xB0, "mission"),
(0xB1, "pad"),
(0xB2, "uid"),
(0xB4, "bolts"),
(0xB6, "xp"),
(0xB8, "p_parent"),
(0xBC, "o_class"),
(0xBE, "triggers"),
(0xBF, "standarddeathcalled"),
(0xC0, "r_mtx"),
(0xF0, "rot"),
];
#[cfg(test)]
mod instance_layout_tests {
use super::{MOBY_INSTANCE_FIELDS, MOBY_INSTANCE_SIZE};
#[test]
fn offsets_ascend_and_stay_inside_the_struct() {
let mut prev = 0usize;
for &(off, name) in MOBY_INSTANCE_FIELDS {
assert!(
off >= prev,
"{name} at {off:#x} overlaps the field before it"
);
assert!(
off < MOBY_INSTANCE_SIZE,
"{name} at {off:#x} is past the end"
);
prev = off;
}
}
#[test]
fn the_struct_ends_exactly_at_its_stated_size() {
let last = MOBY_INSTANCE_FIELDS.last().unwrap();
assert_eq!(
last.0 + 0x10,
MOBY_INSTANCE_SIZE,
"last field {:?} overruns",
last.1
);
}
#[test]
fn the_dispatch_key_is_where_the_dispatch_tables_say_it_is() {
let o = MOBY_INSTANCE_FIELDS
.iter()
.find(|(_, n)| *n == "o_class")
.unwrap()
.0;
// a signed 16-bit at 0xBC leaves room for `triggers` and the death flag,
// which is what the reference says follows it
assert_eq!(o, 0xBC);
assert_eq!(
o + 2,
MOBY_INSTANCE_FIELDS
.iter()
.find(|(_, n)| *n == "triggers")
.unwrap()
.0
);
}
}
+46
View File
@@ -3269,3 +3269,49 @@ Asserted in `dispatch_tables_contain_far_fewer_behaviours_than_records`, which
also fails if a table ever reports more behaviours than records -- an
impossibility worth pinning because it is the shape the counts would take if the
record widths were wrong.
## The moby instance, 256 bytes, named
The decomp carries an authentic `MobyInstance` layout of 0x100 bytes with every
field named, and it is now transcribed into `gc_formats::moby::MobyInstance`
with a 67-entry offset map. Two fields matter for the open items:
0x40 animSeq animation sequence pointer
0x44 animSeqT animation time parameter
0x48 animSpeed playback speed
0x50 animLayers animation layer list
0x57 jointCnt joint count -- non-zero means the moby is skinned
0x58 jointCache joint matrix cache
0xBC oClass object class id -- the dispatch key
`oClass` closes a loop that was open for several commits. It is the field the
runtime uses to reach the `lvl.vtbl` handler tables, so an instance and its
routine are now linked by a named offset rather than by an assumption about which
axis the two tables share.
`jointCnt` and `jointCache` also settle something from earlier in this log by
contradiction rather than confirmation. A tie vertex's `i16[3]` was tested as a
skin/joint index and **disproved** -- vertices sharing it were 0.95x *farther*
apart than vertices differing in it. That result stands and is unaffected: this is
the *moby* struct, and a moby is a different object from a tie.
What this does **not** do is find `moby_anim`. It names the instance-side state --
a sequence pointer, a layer list, a joint cache -- which means the asset to locate
is whatever those point at, and we still do not know where that is. The item's
unblock condition is now sharper: it needs the data those pointers reference, not
the struct.
### Provenance and what the test can check
The layout is transcribed, not derived here, and it is not verified against disc
data because it cannot be: moby instances are runtime objects and nothing of that
shape is in the container. What the test asserts is what can be asserted without
the bytes -- that the offsets ascend, do not overlap, and land the last field
exactly on 0x100, and that `o_class` sits at 0xBC immediately before `triggers`.
Worth keeping distinct from the collision finding: that project's **function
addresses** did not transfer, landing in NOBITS memory. A struct's field order is a
different kind of claim from an address in a program's layout, so the failure of
one does not impeach the other -- but the distinction is real, and `PROVENANCE.md`
now says which is which.
+9
View File
@@ -427,6 +427,15 @@ Phase 1 hands over the extents; this phase works out what each one contains.
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.
- [x] The **moby instance is named**: `MobyInstance`, 256 bytes, 67 fields,
transcribed from the decomp's `include/structs.h` into `gc_formats::moby`
with an offset map. `oClass` at `0xBC` is the dispatch key into `lvl.vtbl`,
which closes the instance-to-routine link; `animSeq`/`animLayers`/
`jointCnt`/`jointCache` at `0x40..0x5C` name the instance-side animation
state. **Not verified against disc bytes** -- mobies are runtime objects and
nothing of that shape is in the container; the test asserts what can be
asserted without them (offsets ascend, do not overlap, land exactly on
0x100). Layout claim from a third party, attributed as such.
- [ ] `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