//! The runtime layout of a unit / vessel definition object. //! //! The game parses an [`crate::idxd`] record into a fixed 880-byte object whose //! field offsets are **not** guessable from the disc data: the record is a //! reflective key/value pool, and the loader assigns each key to a member by //! name. This table is that assignment, read out of the loader itself //! (`sub_82341A20` — every key is built as `addi r4, r30, -N`, so the field name //! for each store is a string in the executable image), and verified against //! objects dumped from a running mission: **406 values agree with the disc //! records, 0 disagree**, over 11 objects covering both schemas. //! //! Why a reimplementation wants it: //! //! - it names the field behind every word of a live definition object, so a //! memory snapshot can be read directly; //! - it says which fields a record leaves **defaulted**, and what the loader //! leaves there — the float accessor returns `0.0` on a pool miss; //! - it carries two conventions that are invisible on disc: **angles are degrees //! in the data and radians in the object**, and **`Size_Y` takes `Size_X`** //! when omitted. //! //! See `docs/re/live-unit-definitions.md` for the derivation and the measured //! default values. /// How a field is stored in the definition object. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Kind { /// IEEE-754 single, big-endian. F32, /// Pointer to a string. Str, /// 32-bit word (bool / enum / count / id). Word, } /// One field of the definition object. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Field { /// Byte offset from the start of the object. pub offset: usize, /// Field name, exactly as the disc record spells it. pub name: &'static str, /// Storage kind. pub kind: Kind, } const TABLE: &str = include_str!("../data/unit_definition_layout.txt"); /// Every mapped field, in offset order. pub fn fields() -> Vec { TABLE .lines() .filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty()) .filter_map(|l| { let mut it = l.split_whitespace(); let offset = it.next()?.parse().ok()?; let kind = match it.next()? { "f32" => Kind::F32, "str" => Kind::Str, _ => Kind::Word, }; // `name` is a &'static str because TABLE is 'static. let name = it.next()?; let name: &'static str = TABLE.get( TABLE.find(name).map(|s| s..s + name.len())?, )?; Some(Field { offset, name, kind }) }) .collect() } /// The field at `offset`, if one is mapped there. pub fn field_at(offset: usize) -> Option { fields().into_iter().find(|f| f.offset == offset) } /// The offset of `name`, if it is mapped. pub fn offset_of(name: &str) -> Option { fields().into_iter().find(|f| f.name == name).map(|f| f.offset) } #[cfg(test)] mod tests { use super::*; #[test] fn table_parses_and_is_ordered() { let f = fields(); assert!(f.len() > 150, "expected the full map, got {}", f.len()); assert!(f.windows(2).all(|w| w[0].offset < w[1].offset), "offsets must be strictly increasing"); } #[test] fn known_fields_sit_where_the_loader_puts_them() { // Spot-checks from the verified map; these four also anchor the // identification of a live object (see docs/re/live-unit-definitions.md). assert_eq!(offset_of("Size_X"), Some(48)); assert_eq!(offset_of("Size_Y"), Some(52)); assert_eq!(offset_of("Size_Z"), Some(56)); assert_eq!(offset_of("HP"), Some(84)); assert_eq!(offset_of("HQRatio"), Some(88)); assert_eq!(field_at(96).map(|f| f.name), Some("ThrusterRatio")); assert_eq!(field_at(48).map(|f| f.kind), Some(Kind::F32)); } }