game_data: Squadron loader — mission flight groups
Decode the per-stage Enumerate_Squadrons tables (UnitGroup_S<NN>.tbl) into flight groups: formation shape, AI behaviour, side, and the member craft. Definitions are delimited by the FormationID key; members are the first `count` UN_ craft after the header (anything past them belongs to the next squadron or the stage roster — the naive "all UN_ in span" over-collected). e.g. the player's Rhino flight = a 2-craft TCAF squadron of Delta Sabers flying Formation_2_Rhino. Schema hash varies per stage so we match on the table name. Squadron id is positional and left None when the id list and definitions don't line up (honest, not guessed). +1 test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
11
crates/sylpheed-formats/examples/squadrons.rs
Normal file
11
crates/sylpheed-formats/examples/squadrons.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use sylpheed_formats::{game_data, PakArchive};
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let sq=game_data::load_squadrons(&pak);
|
||||
println!("{} squadron definitions", sq.len());
|
||||
for s in sq.iter().filter(|s|s.side.as_deref()==Some("TCAF")).take(6){
|
||||
let mem:Vec<String>=s.members.iter().map(|m|m.trim_start_matches("UN_").chars().take(18).collect()).collect();
|
||||
println!(" {:8} {:22} {:24} x{} {:?}", s.id.clone().unwrap_or("?".into()), s.formation_id.clone().unwrap_or_default(), s.ai_id.clone().unwrap_or_default(), s.count.unwrap_or(0), mem);
|
||||
}
|
||||
}
|
||||
@@ -436,6 +436,84 @@ pub fn load_stages(pak: &PakArchive) -> Vec<Stage> {
|
||||
load_table(pak, schema::STAGE, Stage::from_idxd)
|
||||
}
|
||||
|
||||
// ── Squadron / flight group ─────────────────────────────────────────────────────
|
||||
|
||||
/// A squadron — a flight group that spawns together: its formation shape, AI
|
||||
/// behaviour, side, and member craft (with their pilots). Parsed from the
|
||||
/// per-stage `Enumerate_Squadrons` tables (`UnitGroup_S<NN>.tbl`). The player's
|
||||
/// Rhino flight, for instance, is a 2-craft TCAF squadron flying `Formation_2_Rhino`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Squadron {
|
||||
/// Squadron tag (`TCN001`, `ADT101`, …), matched positionally to its
|
||||
/// definition; `None` if the id list and definitions don't line up.
|
||||
pub id: Option<String>,
|
||||
pub formation_id: Option<String>,
|
||||
pub ai_id: Option<String>,
|
||||
/// Faction — `TCAF` (allied) or `ADAN` (enemy).
|
||||
pub side: Option<String>,
|
||||
pub count: Option<i64>,
|
||||
/// Member craft (`UN_*` unit ids).
|
||||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
/// Load every squadron from a pak. Scans all `Enumerate_Squadrons` tables (their
|
||||
/// schema hash varies per stage, but the table name is stable).
|
||||
pub fn load_squadrons(pak: &PakArchive) -> Vec<Squadron> {
|
||||
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();
|
||||
if !t.first().is_some_and(|s| s.contains("Enumerate_Squadron")) {
|
||||
continue;
|
||||
}
|
||||
// Definitions are delimited by the `FormationID` key (its value, the
|
||||
// formation, sits just before it). The id list precedes the first one.
|
||||
let fpos: Vec<usize> = t
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| *s == "FormationID")
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
let Some(&first) = fpos.first() else { continue };
|
||||
// The squadron id list: tokens after the header up to the first formation.
|
||||
let ids: Vec<&String> = t[1..first.saturating_sub(1)].iter().collect();
|
||||
for (k, &fi) in fpos.iter().enumerate() {
|
||||
let start = fi.saturating_sub(1);
|
||||
let end = fpos.get(k + 1).map(|&n| n.saturating_sub(1)).unwrap_or(t.len());
|
||||
let span = &t[start..end];
|
||||
let before = |key: &str| -> Option<String> {
|
||||
span.iter().position(|s| s == key).and_then(|i| (i > 0).then(|| span[i - 1].clone()))
|
||||
};
|
||||
let count = before("Count").and_then(|v| v.parse::<i64>().ok());
|
||||
// Members are the first `count` `UN_` craft after the header block
|
||||
// (which ends at DisableInterval, else Count); anything past them
|
||||
// belongs to the next squadron or the stage roster.
|
||||
let member_start = span
|
||||
.iter()
|
||||
.position(|s| s == "DisableInterval")
|
||||
.or_else(|| span.iter().position(|s| s == "Count"))
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
let take = count.unwrap_or(0).max(0) as usize;
|
||||
out.push(Squadron {
|
||||
id: (ids.len() == fpos.len()).then(|| ids[k].clone()),
|
||||
formation_id: Some(t[fi - 1].clone()),
|
||||
ai_id: before("AIID"),
|
||||
side: before("SideID"),
|
||||
count,
|
||||
members: span[member_start..]
|
||||
.iter()
|
||||
.filter(|s| s.starts_with("UN_"))
|
||||
.take(take)
|
||||
.cloned()
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ── Generic record access (any of the ~105 schemas) ─────────────────────────────
|
||||
|
||||
/// A generically-decoded IDXD record: its table name, schema id, identity, and
|
||||
@@ -572,6 +650,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_squadrons() {
|
||||
let Some(pak) = pak() else { return };
|
||||
let sq = load_squadrons(&pak);
|
||||
assert!(sq.len() >= 20, "expected many squadrons, got {}", sq.len());
|
||||
// A TCAF Rhino flight of Delta Sabers, membership matching its count.
|
||||
let rhino = sq
|
||||
.iter()
|
||||
.find(|s| {
|
||||
s.side.as_deref() == Some("TCAF")
|
||||
&& s.formation_id.as_deref().is_some_and(|f| f.contains("Rhino"))
|
||||
&& s.members.iter().any(|m| m.contains("DeltaSaber"))
|
||||
})
|
||||
.expect("a TCAF Rhino Delta Saber squadron");
|
||||
assert_eq!(rhino.members.len() as i64, rhino.count.unwrap_or(-1));
|
||||
assert!(rhino.members.iter().all(|m| m.contains("DeltaSaber")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_records_read_any_schema() {
|
||||
let Some(pak) = pak() else { return };
|
||||
|
||||
Reference in New Issue
Block a user