From f5182f9649546d96d31ec42e1c1d436afd570301 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 23 Jul 2026 20:14:22 +0200 Subject: [PATCH] =?UTF-8?q?game=5Fdata:=20PilotRoster=20loader=20=E2=80=94?= =?UTF-8?q?=20player=20squadron=20flight=20assignments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decode the UNITS/ZUNITS player-unit tables (GP_HANGAR_ARSENAL.pak) into flight assignments: which pilot flies each callsign (Rhino1→Katana, Bird1→Sandra, …), plus the player craft. Assignments are `Callsign- Pilot` tokens, extracted by shape. 138 configs; the distinct line-ups trace the campaign's story — Raymond leads Rhino flight early (Rhino1=Raymond, Rhino2=Katana), then Katana takes the lead (Rhino1=Katana, Rhino2=Ellen, Rhino3=Gene, Rhino4=Yoji). Example `flights` prints the line-ups. +1 test. Co-Authored-By: Claude Opus 4.8 --- crates/sylpheed-formats/examples/flights.rs | 17 ++++++ crates/sylpheed-formats/src/game_data.rs | 62 +++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 crates/sylpheed-formats/examples/flights.rs diff --git a/crates/sylpheed-formats/examples/flights.rs b/crates/sylpheed-formats/examples/flights.rs new file mode 100644 index 0000000..b4390f9 --- /dev/null +++ b/crates/sylpheed-formats/examples/flights.rs @@ -0,0 +1,17 @@ +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 rosters=gd::load_pilot_rosters(&pak); + // distinct rosters by their pilot set + let mut seen=std::collections::BTreeSet::new(); let mut shown=0; + println!("{} pilot-roster configs; distinct line-ups:", rosters.len()); + for r in &rosters{ + let key:String=r.pilots.iter().map(|(c,p)|format!("{c}:{p}")).collect::>().join(","); + if seen.insert(key) && shown<8 { + shown+=1; + let flt:Vec=r.pilots.iter().map(|(c,p)|format!("{c}={p}")).collect(); + println!(" {}", flt.join(" ")); + } + } +} diff --git a/crates/sylpheed-formats/src/game_data.rs b/crates/sylpheed-formats/src/game_data.rs index 77f7f40..3cdc95c 100644 --- a/crates/sylpheed-formats/src/game_data.rs +++ b/crates/sylpheed-formats/src/game_data.rs @@ -623,6 +623,54 @@ pub fn load_unit_rosters(pak: &PakArchive) -> Vec { out } +// ── Pilot roster (player squadron assignments) ────────────────────────────────── + +/// The player squadron's flight assignments for a mission — which pilot flies each +/// callsign (`Rhino1` → `Katana`, `Bird1` → `Sandra`, …). Parsed from the +/// `UNITS`/`ZUNITS` player-unit tables (in `GP_HANGAR_ARSENAL.pak`); assignments +/// vary per mission (different missions field different wingmen). +#[derive(Debug, Clone)] +pub struct PilotRoster { + /// `(callsign, pilot)` pairs in flight order, e.g. `("Rhino1", "Katana")`. + pub pilots: Vec<(String, String)>, + /// The player craft unit id (`UN_f002_TCAF_DeltaSaber…`). + pub player_unit: Option, +} + +/// True for a `Callsign-Pilot` token (a single hyphen, digit-suffixed callsign, +/// alphabetic pilot) — the flight-assignment shape. +fn is_assignment(s: &str) -> bool { + let Some((cs, pilot)) = s.split_once('-') else { return false }; + !pilot.is_empty() + && pilot.chars().all(|c| c.is_ascii_alphabetic()) + && cs.len() >= 2 + && cs.as_bytes()[cs.len() - 1].is_ascii_digit() + && cs.chars().all(|c| c.is_ascii_alphanumeric()) +} + +/// Load every player pilot roster from a pak (scan `GP_HANGAR_ARSENAL.pak`). One +/// per player-unit config; rosters with no assignment tokens are skipped. +pub fn load_pilot_rosters(pak: &PakArchive) -> Vec { + let mut out = Vec::new(); + for e in pak.entries() { + let Ok(b) = pak.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { continue }; + let t = o.tokens(); + let pilots: Vec<(String, String)> = t + .iter() + .filter(|s| is_assignment(s)) + .filter_map(|s| s.split_once('-').map(|(c, p)| (c.to_string(), p.to_string()))) + .collect(); + if pilots.len() >= 2 { + out.push(PilotRoster { + pilots, + player_unit: t.iter().find(|s| s.starts_with("UN_")).cloned(), + }); + } + } + out +} + // ── Generic record access (any of the ~105 schemas) ───────────────────────────── /// A generically-decoded IDXD record: its table name, schema id, identity, and @@ -808,6 +856,20 @@ mod tests { assert!(s01.is_some_and(|r| r.units.iter().any(|u| u.contains("DeltaSaber")))); } + #[test] + fn loads_pilot_rosters() { + 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 rosters = load_pilot_rosters(&pak); + assert!(!rosters.is_empty(), "expected player pilot rosters"); + // Some config assigns Katana to a Rhino callsign (she leads Rhino flight). + assert!(rosters.iter().any(|r| { + r.pilots.iter().any(|(cs, p)| cs.starts_with("Rhino") && p == "Katana") + })); + // Bird flight exists too. + assert!(rosters.iter().any(|r| r.pilots.iter().any(|(cs, _)| cs.starts_with("Bird")))); + } + #[test] fn generic_records_read_any_schema() { let Some(pak) = pak() else { return };