game_data: PilotRoster loader — player squadron flight assignments
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<N>- 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 <noreply@anthropic.com>
This commit is contained in:
17
crates/sylpheed-formats/examples/flights.rs
Normal file
17
crates/sylpheed-formats/examples/flights.rs
Normal file
@@ -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::<Vec<_>>().join(",");
|
||||
if seen.insert(key) && shown<8 {
|
||||
shown+=1;
|
||||
let flt:Vec<String>=r.pilots.iter().map(|(c,p)|format!("{c}={p}")).collect();
|
||||
println!(" {}", flt.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,6 +623,54 @@ pub fn load_unit_rosters(pak: &PakArchive) -> Vec<UnitRoster> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// True for a `Callsign<N>-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<PilotRoster> {
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user