The offset->field map read out of sub_82341A20 is now usable rather than just documented: data/unit_definition_layout.txt carries the 159 fields with their provenance and the two conventions (angles are degrees on disc and radians in the object; a defaulted field keeps the accessor's 0.0 miss value), and sylpheed_formats::unit_layout exposes fields()/field_at()/offset_of() so a memory snapshot can be read by name. tests/unit_layout_disc.rs replays the verification against the checked-in live dump -- every mapped float of all 11 identified objects must equal its disc value, angles compared in radians -- asserting 0 disagreements and >=400 agreements. It needs no emulator. Full suite green: 11 binaries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
3.8 KiB
Rust
106 lines
3.8 KiB
Rust
//! 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<Field> {
|
|
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<Field> {
|
|
fields().into_iter().find(|f| f.offset == offset)
|
|
}
|
|
|
|
/// The offset of `name`, if it is mapped.
|
|
pub fn offset_of(name: &str) -> Option<usize> {
|
|
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));
|
|
}
|
|
}
|