From d41b0658136bf9b6baf40466ebda9a274ba3a685 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 23 Jul 2026 20:30:13 +0200 Subject: [PATCH] =?UTF-8?q?game=5Fdata:=20Arsenal=20loader=20=E2=80=94=20p?= =?UTF-8?q?layer=20weapon=20options=20per=20hardpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decode the player weapon arsenal from the UNITS tables (GP_HANGAR_ARSENAL.pak): the selectable weapons per Delta Saber hardpoint. Each `STANDARD_` header (Nose / Arm1-3) lists its options up to the next header — extracted by shape, unioned + deduped across configs. The arsenal reads with the game's own names: NOSE guns (Stiletto, Rapier, Broad Sword, Machine Cannon), ARM missiles/bombs (Falcon, Eagle, White Shark, Piranha, Tomahawk Rail Gun, Maelstrom Bomb). Example `arsenal` prints them. +1 test. Co-Authored-By: Claude Opus 4.8 --- crates/sylpheed-formats/examples/arsenal.rs | 9 +++ crates/sylpheed-formats/src/game_data.rs | 77 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 crates/sylpheed-formats/examples/arsenal.rs diff --git a/crates/sylpheed-formats/examples/arsenal.rs b/crates/sylpheed-formats/examples/arsenal.rs new file mode 100644 index 0000000..3dbed7d --- /dev/null +++ b/crates/sylpheed-formats/examples/arsenal.rs @@ -0,0 +1,9 @@ +use sylpheed_formats::{game_data as gd, PakArchive}; +fn main(){ + let disc=std::env::var("SYLPHEED_DISC").unwrap(); + let pak=PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap(); + let a=gd::load_arsenal(&pak); + for (hp,list) in [("NOSE",&a.nose),("ARM1",&a.arm1),("ARM2",&a.arm2),("ARM3",&a.arm3)]{ + println!("{hp} ({}): {:?}", list.len(), list.iter().take(8).collect::>()); + } +} diff --git a/crates/sylpheed-formats/src/game_data.rs b/crates/sylpheed-formats/src/game_data.rs index 8b5fb39..faac208 100644 --- a/crates/sylpheed-formats/src/game_data.rs +++ b/crates/sylpheed-formats/src/game_data.rs @@ -689,6 +689,69 @@ pub fn load_pilot_rosters(pak: &PakArchive) -> Vec { out } +// ── Arsenal (player weapon options per hardpoint) ─────────────────────────────── + +/// The player's selectable weapons, by hardpoint (from the `UNITS` player-unit +/// tables in `GP_HANGAR_ARSENAL.pak`). These are the arsenal display ids — the +/// Delta Saber's nose guns (`Stiletto_BG1`, `Broad_Sword_SG1`, …) and arm missiles +/// (`Falcon_9AM`, `White_Shark_T53R`, …) the player equips in the hangar. +#[derive(Debug, Clone, Default)] +pub struct Arsenal { + /// Nose slot — the fixed forward gun options. + pub nose: Vec, + /// The three arm hardpoints — missile / bomb / special options. + pub arm1: Vec, + pub arm2: Vec, + pub arm3: Vec, +} + +/// The weapon ids listed under a `STANDARD_` header (skipping its `Type` key), +/// up to the next `STANDARD_`/`PlayerSET_` header. +fn hardpoint_options(tokens: &[String], header: &str) -> Vec { + let Some(start) = tokens.iter().position(|s| s == header) else { return Vec::new() }; + tokens[start + 1..] + .iter() + .skip_while(|s| *s == "Type") + .take_while(|s| !s.starts_with("STANDARD_") && !s.starts_with("PlayerSET")) + .filter(|s| *s != "Type") + .cloned() + .collect() +} + +/// Load the player arsenal from a pak (`GP_HANGAR_ARSENAL.pak`). Unions the weapon +/// options across every player-unit config, deduped in first-seen order. +pub fn load_arsenal(pak: &PakArchive) -> Arsenal { + let mut a = Arsenal::default(); + let mut seen = [(); 4].map(|_| std::collections::BTreeSet::new()); + let slots: [(&str, usize); 4] = [ + ("STANDARD_NOSE", 0), + ("STANDARD_ARM1", 1), + ("STANDARD_ARM2", 2), + ("STANDARD_ARM3", 3), + ]; + for e in pak.entries() { + let Ok(b) = pak.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { continue }; + if !o.tokens().first().is_some_and(|s| s.ends_with("UNITS")) { + continue; + } + for (header, idx) in slots { + let dst = match idx { + 0 => &mut a.nose, + 1 => &mut a.arm1, + 2 => &mut a.arm2, + _ => &mut a.arm3, + }; + for w in hardpoint_options(o.tokens(), header) { + if seen[idx].insert(w.clone()) { + dst.push(w); + } + } + } + } + a +} + // ── Generic record access (any of the ~105 schemas) ───────────────────────────── /// A generically-decoded IDXD record: its table name, schema id, identity, and @@ -888,6 +951,20 @@ mod tests { assert!(rosters.iter().any(|r| r.pilots.iter().any(|(cs, _)| cs.starts_with("Bird")))); } + #[test] + fn loads_arsenal() { + let Ok(disc) = std::env::var("SYLPHEED_DISC") else { return }; + let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")) else { return }; + let a = load_arsenal(&pak); + assert!(a.nose.len() >= 5 && a.arm1.len() >= 5, "nose {} arm1 {}", a.nose.len(), a.arm1.len()); + assert!(a.nose.iter().any(|w| w.starts_with("Stiletto"))); + assert!(a.arm1.iter().any(|w| w.starts_with("Falcon"))); + // Headers never leak into the option lists. + assert!([&a.nose, &a.arm1, &a.arm2, &a.arm3] + .iter() + .all(|v| v.iter().all(|w| !w.starts_with("STANDARD_") && *w != "Type"))); + } + #[test] fn generic_records_read_any_schema() { let Some(pak) = pak() else { return };