re: read the defaulted weapon stats out of live guest memory
Canary backs the whole guest address space with one shared-memory file, so the game's RAM is readable from the host while it runs -- no debugger, no emulator patch. That turns the parked Route-B question (fields the IDXD omits because they sit at their default) from "unreachable" into a table lookup. gmem.py maps guest VAs into /dev/shm/xenia_memory_* via Xenia's fixed table and searches only the allocated extents, so a full-RAM scan is ~0.2 s. weapon_runtime.py finds the parsed objects by scanning for their vtable, then *solves* the struct layout instead of guessing it: every (field, offset, encoding) triple is scored against the disc records, and a binding is accepted only with zero contradictions -- and marked confirmed only when >=10 records agree on >=3 distinct values, because a field whose samples are all one number matches any offset holding that constant. Result: Weapon (0xc0) and Shell (0x200) mapped, 4393 values the disc does not carry, for all 126 weapons at 5 % save progress. Cross-checks hold -- the Arsenal DATA SHEET read 4 for wep_05/wep_60 Max. Lock Ons and the memory read agrees; the in-flight HUD's 06000/00300 match LoadingCount; all 252 objects are byte-identical across a screen change, so this is definition data, not state. Two findings fell out: `IsCharging = Yes` switches LoadingCount and TriggerShotCount to float32 (it partitions the 8 float-encoded records exactly), and two .tbl entries declare Shell_TCAF_Ship_AAGun with conflicting Power -- the runtime keeps the one that omits it. Where the defaulted values *come from* (the IDXD's undecoded binary node region vs code defaults) is not settled here; they vary per weapon, so they are not one constructor constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
crates/sylpheed-formats/examples/idxd_tokens.rs
Normal file
102
crates/sylpheed-formats/examples/idxd_tokens.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! 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();
|
||||
if !toks.iter().any(|t| t.starts_with("Weapon_")) {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user