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
|
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) ─────────────────────────────
|
// ── Generic record access (any of the ~105 schemas) ─────────────────────────────
|
||||||
|
|
||||||
/// A generically-decoded IDXD record: its table name, schema id, identity, and
|
/// 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"))));
|
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]
|
#[test]
|
||||||
fn generic_records_read_any_schema() {
|
fn generic_records_read_any_schema() {
|
||||||
let Some(pak) = pak() else { return };
|
let Some(pak) = pak() else { return };
|
||||||
|
|||||||
Reference in New Issue
Block a user