`cargo fmt --all -- --check` has failed on every run in this repository's history, identically on `main` and on every branch. This is #12. Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other extension touched. `cargo check --workspace` exits 0 afterwards, so nothing changed semantically. ON THE ORDERING, WHICH WAS THE REAL QUESTION. HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree reformat before #7 and #8 return "would put a conflict in every file of 861 commits and make the reviews those items exist to enable unreadable". That is measurably too pessimistic, and it had been reasoned rather than tested. Measured here by three-way merging a rustfmt'd `main` against both unmerged branches, file by file: file/branch pairs tested 32 merges CLEAN 28 merges CONFLICTING 4 (8 conflict hunks total) sylpheed-cli/src/main.rs 1 hunk sylpheed-export/src/check.rs 1 sylpheed-export/src/screen.rs 4 sylpheed-export/src/video.rs 2 All four are against `auto/frame-blend-draw-path` only; `auto/port-p6-audio` does not conflict anywhere. The earlier framing -- 154 dirty files, 133 that cannot collide, 21 that can, the collision set carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say is that most of the 21 still merge cleanly, because rustfmt's edits and the branches' edits rarely land on the same lines. So the cost of sweeping now is 4 files and 8 hunks for one branch, against a check that is otherwise red forever. Deliberately NOT folded into the WASM PR: 154 reformatted files would make that one unreviewable. Closes #12
110 lines
3.9 KiB
Rust
110 lines
3.9 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));
|
|
}
|
|
}
|