Files
Sylpheed/crates/sylpheed-formats/tests/game_data_disc.rs
Fabian Hamm ed54f95d54 style: rustfmt sweep -- 774 hunks across 154 files -> 0
`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
2026-09-08 20:07:01 +02:00

349 lines
13 KiB
Rust

//! Real-disc tests for the record-table game-data loaders. Skipped without
//! `SYLPHEED_DISC`.
//!
//! These pin the values that only became readable once the loaders moved off the
//! string-pool reader (`docs/re/idxd-legacy-reader-audit.md`): the per-hardpoint
//! stats, the per-difficulty scoring and the per-phase play space. A regression
//! to the flat reader collapses each of them to one answer and fails here.
use std::collections::BTreeSet;
use sylpheed_formats::game_data::{self as gd, Difficulty, HardpointKind};
use sylpheed_formats::PakArchive;
fn main_pak() -> Option<PakArchive> {
let disc = std::env::var("SYLPHEED_DISC").ok()?;
PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).ok()
}
fn skip() {
eprintln!("SKIP: set SYLPHEED_DISC");
}
/// The player's craft carries 63 `Turret_NNN` records — one per equippable
/// weapon — each with its own `HP`, while `StructureCount.TurretCount` says 4.
/// The flat reader had one `HP` slot for the whole object.
#[test]
fn craft_hardpoints_are_per_record() {
let Some(pak) = main_pak() else { return skip() };
let units = gd::load_units(&pak);
let saber = units
.iter()
.find(|u| u.id.as_deref() == Some("UN_f001_TCAF_DeltaSaber_T"))
.expect("Delta Saber present");
assert_eq!(saber.hp, Some(1000.0), "hull HP");
assert_eq!(saber.turret_count, Some(4), "StructureCount.TurretCount");
let turrets: Vec<_> = saber
.hardpoints
.iter()
.filter(|h| h.kind == HardpointKind::Turret)
.collect();
assert_eq!(turrets.len(), 63, "Turret_NNN records");
assert!(
turrets.iter().all(|t| t.hp == Some(100.0)),
"every mount has HP 100"
);
// Each mount names its own weapon model, `rou_f001_wep_NN`.
let mount = turrets.iter().find(|t| t.index == 33).expect("Turret_033");
assert_eq!(mount.model.as_deref(), Some("rou_f001_wep_33"));
assert_eq!(mount.id.as_deref(), Some("Turret_033"));
}
/// A capital ship's components each carry their own HP — five distinct values on
/// the SD-Battleship, where the flat reader returned only the hull's 100000.
#[test]
fn vessel_hardpoints_are_per_record() {
let Some(pak) = main_pak() else { return skip() };
let vessels = gd::load_vessels(&pak);
let bs = vessels
.iter()
.find(|v| v.id.as_deref() == Some("UN_e101_ADAN_SDBattleship"))
.expect("SD-Battleship present");
assert_eq!(bs.hp, Some(100000.0));
let hp_of = |kind: HardpointKind, idx: u32| {
bs.hardpoints
.iter()
.find(|h| h.kind == kind && h.index == idx)
.and_then(|h| h.hp)
};
assert_eq!(hp_of(HardpointKind::Bridge, 0), Some(10000.0));
assert_eq!(hp_of(HardpointKind::Thruster, 0), Some(20000.0));
assert_eq!(hp_of(HardpointKind::ShieldGenerator, 0), Some(5000.0));
assert_eq!(hp_of(HardpointKind::Hatch, 0), Some(100.0));
// Distinct HP values across the ship's components.
let distinct: BTreeSet<String> = bs
.hardpoints
.iter()
.filter_map(|h| h.hp.map(|v| format!("{v}")))
.collect();
assert!(
distinct.len() >= 4,
"distinct component HP values: {distinct:?}"
);
// A main gun names the weapon it fires and the shield generator its share.
let asgun = bs
.hardpoints
.iter()
.find(|h| h.weapon_id.as_deref() == Some("Weapon_ADAN_Ship_ASGun"))
.expect("an anti-ship gun");
assert_eq!(asgun.hp, Some(1500.0));
assert_eq!(
bs.hardpoints
.iter()
.find(|h| h.kind == HardpointKind::ShieldGenerator)
.and_then(|h| h.power_ratio),
Some(0.25)
);
// The launch bay names the squadron it scrambles.
assert_eq!(
bs.hardpoints
.iter()
.find(|h| h.kind == HardpointKind::Hatch)
.and_then(|h| h.squadron_id.clone()),
Some("Squadron_Test2".to_string())
);
}
/// `MainMissionBonus` lives in `Score_Easy` / `Score_Normal` / `Score_Hard`, one
/// value each. The flat reader returned the Easy one for every mission.
#[test]
fn main_mission_bonus_is_per_difficulty() {
let Some(pak) = main_pak() else { return skip() };
let cfgs = gd::load_player_configs(&pak);
assert_eq!(cfgs.len(), 24);
let mut distinct3 = 0;
let mut doubling = 0;
let mut all_equal = 0;
for c in &cfgs {
let (e, n, h) = (
c.score.easy.main_mission_bonus,
c.score.normal.main_mission_bonus,
c.score.hard.main_mission_bonus,
);
let set: BTreeSet<_> = [e, n, h].into_iter().flatten().collect();
match set.len() {
3 => distinct3 += 1,
1 => all_equal += 1,
_ => {}
}
if let (Some(e), Some(n), Some(h)) = (e, n, h) {
if n == 2 * e && h == 4 * e {
doubling += 1;
}
}
}
eprintln!(
"MainMissionBonus: 3-distinct {distinct3}/24, all-equal {all_equal}, doubling {doubling}"
);
assert_eq!(distinct3, PIN_BONUS_DISTINCT3);
assert_eq!(doubling, PIN_BONUS_DOUBLING);
// The first config's own numbers.
let c = &cfgs[0];
assert_eq!(c.score.easy.main_mission_bonus, PIN_C0_EASY);
assert_eq!(c.score.normal.main_mission_bonus, PIN_C0_NORMAL);
assert_eq!(c.score.hard.main_mission_bonus, PIN_C0_HARD);
}
/// The rank thresholds repeat identically across the three `Score_*` records —
/// so the one flat answer happened to be right, but only by coincidence.
#[test]
fn rank_scores_repeat_across_difficulties() {
let Some(pak) = main_pak() else { return skip() };
let cfgs = gd::load_player_configs(&pak);
let mut identical = 0;
for c in &cfgs {
let row = |s: &gd::ScoreRules| {
(
s.rank_score_s,
s.rank_score_a,
s.rank_score_b,
s.rank_score_c,
s.rank_score_d,
)
};
if row(&c.score.easy) == row(&c.score.normal) && row(&c.score.normal) == row(&c.score.hard)
{
identical += 1;
}
}
eprintln!(
"rank scores identical across difficulties: {identical}/{}",
cfgs.len()
);
assert_eq!(identical, cfgs.len());
assert_eq!(cfgs[0].score.normal.rank_score_s, Some(10000));
assert_eq!(cfgs[0].score.normal.rank_score_d, Some(1000));
// The difficulty scaling that *is* per record.
assert_eq!(
cfgs[0].difficulty_f32(Difficulty::Easy, "DamageAdjustment"),
PIN_EASY_DMG
);
assert_eq!(
cfgs[0].difficulty_f32(Difficulty::Hard, "ShieldDamageAdjustment"),
PIN_HARD_SHIELD
);
}
/// `SpaceSize` lives in `Phase_1` / `Phase_2` / `Phase_3`, one value each.
#[test]
fn space_size_is_per_phase() {
let Some(pak) = main_pak() else { return skip() };
let cfgs = gd::load_player_configs(&pak);
let mut varying = 0;
let mut p1_250k = 0;
for c in &cfgs {
assert_eq!(c.phases.len(), 3, "every mission has three Phase_N records");
let sizes: Vec<Option<f32>> = c.phases.iter().map(|p| p.space_size).collect();
let shapes: BTreeSet<String> = sizes.iter().map(|v| format!("{v:?}")).collect();
if shapes.len() > 1 {
varying += 1;
}
if sizes[0] == Some(250000.0) {
p1_250k += 1;
}
}
eprintln!("SpaceSize: varying across phases {varying}/24, phase 1 = 250000 in {p1_250k}");
assert_eq!(varying, PIN_SPACE_VARYING);
assert_eq!(p1_250k, PIN_SPACE_P1_250K);
let c = &cfgs[0];
assert_eq!(c.space_size(1), PIN_C0_SPACE1);
assert_eq!(c.space_size(2), PIN_C0_SPACE2);
assert_eq!(c.space_size(3), PIN_C0_SPACE3);
}
/// Fields the string-pool reader returned `None` for now have values, disc-wide.
#[test]
fn previously_unreadable_stats_are_populated() {
let Some(pak) = main_pak() else { return skip() };
let units = gd::load_units(&pak);
let vessels = gd::load_vessels(&pak);
let n = |f: fn(&gd::CraftUnit) -> Option<f32>| units.iter().filter(|u| f(u).is_some()).count();
eprintln!(
"units={} fcs={} shield_ratio={} cruise={} maxvel={} accel={} decel={} shield_max={}",
units.len(),
n(|u| u.fcs_range),
n(|u| u.shield_ratio),
n(|u| u.cruising_velocity),
n(|u| u.maximum_velocity),
n(|u| u.acceleration),
n(|u| u.deceleration),
n(|u| u.shield_max),
);
for (label, count) in [
("fcs_range", n(|u| u.fcs_range)),
("shield_ratio", n(|u| u.shield_ratio)),
("cruising_velocity", n(|u| u.cruising_velocity)),
("maximum_velocity", n(|u| u.maximum_velocity)),
("acceleration", n(|u| u.acceleration)),
("deceleration", n(|u| u.deceleration)),
] {
assert_eq!(count, units.len(), "{label} set on every unit");
}
assert!(vessels
.iter()
.all(|v| v.fcs_range.is_some() && v.shield_ratio.is_some()));
// Hardpoint HP is readable for every vessel component.
let hp_missing = vessels
.iter()
.flat_map(|v| &v.hardpoints)
.filter(|h| h.hp.is_none())
.count();
assert_eq!(hp_missing, 0);
let total_hardpoints: usize = vessels.iter().map(|v| v.hardpoints.len()).sum();
eprintln!("vessel hardpoints: {total_hardpoints}");
assert_eq!(total_hardpoints, PIN_VESSEL_HARDPOINTS);
}
/// The launcher and its projectile are two records with two ids; the tracer has a
/// third `Interval`.
#[test]
fn weapon_launcher_and_shell_stay_separate() {
let Some(pak) = main_pak() else { return skip() };
let ws = gd::load_weapons(&pak);
assert_eq!(ws.len(), 131);
let differing = ws
.iter()
.filter(|w| w.id.is_some() && w.shell_id.is_some() && w.id != w.shell_id)
.count();
eprintln!(
"weapons whose shell id differs from the launcher id: {differing}/{}",
ws.len()
);
assert_eq!(differing, PIN_WEAPON_SHELL_IDS);
// A player missile the corpus quotes through the flat reader — the launcher
// fields agree, and `MaximumRange` (a `Shell` field) reads the same.
let m26 = ws
.iter()
.find(|w| w.id.as_deref() == Some("Weapon_DSaber_P_wep_26_Missile"))
.expect("Weapon_DSaber_P_wep_26_Missile is in GP_MAIN_GAME_E.pak, not the hangar pak");
assert_eq!(m26.target_type.as_deref(), Some("Vessel,Craft"));
assert_eq!(m26.loading_count, Some(144));
assert_eq!(m26.interval, Some(3.0));
assert_eq!(m26.mass, Some(0.77));
assert_eq!(m26.trigger_shot_count, Some(12));
assert_eq!(m26.max_range, Some(10000.0));
let with_wake = ws
.iter()
.filter(|w| w.records.record("ShellWake").is_some())
.count();
eprintln!("weapons with a ShellWake record: {with_wake}");
assert_eq!(with_wake, PIN_WEAPON_WAKE);
// `Power` is a `Shell` field: every weapon but one has it.
let no_power: Vec<&str> = ws
.iter()
.filter(|w| w.power.is_none())
.filter_map(|w| w.id.as_deref())
.collect();
assert_eq!(
no_power,
["Weapon_NULL"],
"only the placeholder weapon has no Shell.Power"
);
}
/// `RecordSet::everywhere` is the API that answers "which record did that come
/// from" — the question the flat map could not express.
#[test]
fn everywhere_enumerates_the_records_that_define_a_field() {
let Some(pak) = main_pak() else { return skip() };
let vessels = gd::load_vessels(&pak);
let bs = vessels
.iter()
.find(|v| v.id.as_deref() == Some("UN_e101_ADAN_SDBattleship"))
.unwrap();
let hps = bs.records.everywhere("HP");
eprintln!("SDBattleship records defining HP: {}", hps.len());
assert_eq!(hps.len(), PIN_BS_HP_RECORDS);
assert!(hps.iter().any(|(r, v)| *r == "Generic" && *v == "100000.0"));
assert!(hps.iter().any(|(r, _)| r.starts_with("Turret_")));
}
// Pinned measurements over `GP_MAIN_GAME_E.pak`.
/// 18 of the 24 missions pay a different main-objective bonus per difficulty;
/// the other 6 pay 0 at every difficulty.
const PIN_BONUS_DISTINCT3: usize = 18;
/// …and in all 24 the ratio is easy : normal : hard = 1 : 2 : 4.
const PIN_BONUS_DOUBLING: usize = 24;
const PIN_C0_EASY: Option<i64> = Some(1000);
const PIN_C0_NORMAL: Option<i64> = Some(2000);
const PIN_C0_HARD: Option<i64> = Some(4000);
const PIN_EASY_DMG: Option<f32> = Some(1.5);
const PIN_HARD_SHIELD: Option<f32> = Some(0.5);
/// `SpaceSize` is stored per phase, but only **one** of the 24 missions actually
/// varies it across its phases (250000 → 100000 → 100000). The other 23 repeat
/// one value: 500000 once, 100000 eighteen times, 50000 four times.
const PIN_SPACE_VARYING: usize = 1;
const PIN_SPACE_P1_250K: usize = 1;
const PIN_C0_SPACE1: Option<f32> = Some(250000.0);
const PIN_C0_SPACE2: Option<f32> = Some(100000.0);
const PIN_C0_SPACE3: Option<f32> = Some(100000.0);
/// Destructible components across the 23 capital ships, each with its own HP.
const PIN_VESSEL_HARDPOINTS: usize = 418;
/// Every weapon's projectile carries an id of its own (`Shell_…` vs `Weapon_…`).
const PIN_WEAPON_SHELL_IDS: usize = 131;
const PIN_WEAPON_WAKE: usize = 40;
/// Records of the SD-Battleship that define an `HP`: the hull plus 36 components.
const PIN_BS_HP_RECORDS: usize = 37;