game_data: Arsenal loader — player weapon options per hardpoint
Decode the player weapon arsenal from the UNITS tables (GP_HANGAR_ARSENAL.pak): the selectable weapons per Delta Saber hardpoint. Each `STANDARD_<hp>` 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 <noreply@anthropic.com>
This commit is contained in:
9
crates/sylpheed-formats/examples/arsenal.rs
Normal file
9
crates/sylpheed-formats/examples/arsenal.rs
Normal file
@@ -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::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
@@ -689,6 +689,69 @@ pub fn load_pilot_rosters(pak: &PakArchive) -> Vec<PilotRoster> {
|
||||
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<String>,
|
||||
/// The three arm hardpoints — missile / bomb / special options.
|
||||
pub arm1: Vec<String>,
|
||||
pub arm2: Vec<String>,
|
||||
pub arm3: Vec<String>,
|
||||
}
|
||||
|
||||
/// The weapon ids listed under a `STANDARD_<hp>` header (skipping its `Type` key),
|
||||
/// up to the next `STANDARD_`/`PlayerSET_` header.
|
||||
fn hardpoint_options(tokens: &[String], header: &str) -> Vec<String> {
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user