diff --git a/PROVENANCE.md b/PROVENANCE.md index 6125b0c..b2b41d5 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -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 | diff --git a/crates/gc-formats/src/moby.rs b/crates/gc-formats/src/moby.rs index def4fc9..7844694 100644 --- a/crates/gc-formats/src/moby.rs +++ b/crates/gc-formats/src/moby.rs @@ -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 + ); + } +} diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index 6ca2f25..59ae760 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index 4304909..c22e81e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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