idxd_tokens took the sub-record type list as an argument already but still hard-filtered entries to those declaring a `Weapon_*` id, so it could not be pointed at another schema. Filter on the requested type names instead -- it now splits `EnumUnit` (Generic/Maneuver/Shield/Explosion/Frame/Turret/...) too. gmem: honour $GMEM_FILE. `cp --sparse=always /dev/shm/xenia_memory_* snap.bin` takes ~2 s and reads identically, which matters because a running Canary pegs every core under lavapipe and makes repeated live reads stall unpredictably. Groundwork for the craft (UNIT) stats. Not a finding yet: in menus only the player craft's *name* is resident -- the flight-model object is not instantiated until a mission loads, so that needs an in-mission snapshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
4.4 KiB
Rust
109 lines
4.4 KiB
Rust
//! RE diagnostic: emit every `weapon\Weapon_*.tbl` in a pak as machine-readable
|
|
//! records, split at the sub-object boundaries the schema declares.
|
|
//!
|
|
//! An IDXD `.tbl` for a weapon holds `count` sub-records — a `Weapon` and its
|
|
//! `Shell` — flattened into one string pool. `sylpheed-cli pak dump` shows them
|
|
//! merged, which is ambiguous (both declare `ID`/`Name`). The title's schema
|
|
//! name pool gives the split point: the pool contains the literal type names
|
|
//! (`Weapon`, `Shell`), and each sub-record's fields follow its type name.
|
|
//!
|
|
//! Here we split on a type-name token appearing as a *key* position, so each
|
|
//! record is emitted with its own ID and field set:
|
|
//!
|
|
//! ```text
|
|
//! REC <tbl-hash> <index> <TypeName> <ID>
|
|
//! F <key> <value>
|
|
//! ```
|
|
//!
|
|
//! Run: cargo run --release -p sylpheed-formats --example idxd_tokens -- <pak> [types...]
|
|
|
|
use sylpheed_formats::idxd::IdxdObject;
|
|
use sylpheed_formats::pak::PakArchive;
|
|
|
|
fn is_number(s: &str) -> bool {
|
|
let s = s.strip_suffix(['f', 'F']).unwrap_or(s);
|
|
let t = s.strip_prefix(['-', '+']).unwrap_or(s);
|
|
!t.is_empty() && t.chars().all(|c| c.is_ascii_digit() || c == '.')
|
|
}
|
|
|
|
/// `Yes`/`No`-style scalar literals are values, not field names, even though
|
|
/// they are identifier-shaped.
|
|
fn is_enum_value(s: &str) -> bool {
|
|
matches!(s, "Yes" | "No" | "YES" | "NO" | "On" | "Off" | "ON" | "OFF")
|
|
}
|
|
|
|
fn is_key_like(s: &str) -> bool {
|
|
!s.is_empty()
|
|
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
|
&& s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
|
&& !is_number(s)
|
|
&& !is_enum_value(s)
|
|
}
|
|
|
|
fn main() {
|
|
let mut args = std::env::args().skip(1);
|
|
let pak_path = args.next().expect("usage: idxd_tokens <pak> [TypeName...]");
|
|
let types: Vec<String> = {
|
|
let v: Vec<String> = args.collect();
|
|
if v.is_empty() {
|
|
vec!["Weapon".into(), "Shell".into()]
|
|
} else {
|
|
v
|
|
}
|
|
};
|
|
|
|
let pak = PakArchive::open(&pak_path).expect("open pak");
|
|
for entry in pak.entries() {
|
|
let Ok(bytes) = pak.read(entry) else { continue };
|
|
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
|
let toks = obj.tokens();
|
|
// Only entries that actually declare one of the requested sub-record
|
|
// types (the pool-start heuristic can prefix one stray byte, so match a
|
|
// short suffix rather than equality -- same rule as the splitter below).
|
|
if !toks
|
|
.iter()
|
|
.any(|t| types.iter().any(|ty| t == ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if std::env::var("SYLPH_RAW").is_ok() {
|
|
println!("RAW {:08x}", entry.name_hash);
|
|
for t in toks {
|
|
println!("T {t}");
|
|
}
|
|
}
|
|
|
|
// Sub-record boundaries: a bare type-name token. The first one is the
|
|
// schema's own declaration (`EnumWeapon`-style header lives in title
|
|
// code, not here), so we take every occurrence in order.
|
|
let mut bounds: Vec<(usize, &str)> = Vec::new();
|
|
for (i, t) in toks.iter().enumerate() {
|
|
// The pool-start heuristic can glue one stray binary byte onto the
|
|
// first token ("YWeapon"), so match a short suffix, not equality.
|
|
if let Some(ty) = types
|
|
.iter()
|
|
.find(|ty| t == *ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2))
|
|
{
|
|
bounds.push((i, ty.as_str()));
|
|
}
|
|
}
|
|
for (n, &(start, ty)) in bounds.iter().enumerate() {
|
|
let end = bounds.get(n + 1).map(|b| b.0).unwrap_or(toks.len());
|
|
let slice = &toks[start..end];
|
|
// The record's own ID value is the token straight after its type
|
|
// name (`Shell` -> `Shell_DSaber_P_wep_01_Beam`). The `ID`/`Name`
|
|
// *keys* are interned once in the pool, so only the first record
|
|
// shows them -- position-of-key lookup would miss the rest.
|
|
let id = slice.get(1).map(|s| s.as_str()).unwrap_or("?");
|
|
println!("REC {:08x} {n} {ty} {id}", entry.name_hash);
|
|
for i in 1..slice.len() {
|
|
let (k, v) = (slice[i].as_str(), slice[i - 1].as_str());
|
|
if is_key_like(k) && (!is_key_like(v) || is_number(v)) {
|
|
println!("F {k} {v}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|