80 findings, not the 14 the first run showed -- clippy stops at the first failing compilation unit, so `--keep-going` is what makes the list complete. 60 were machine-applicable (`cargo clippy --fix`). The rest by hand: * five descending `sort_by` -> `sort_by_key(Reverse(..))` * `chunks_exact(4)` on both sides of four zips, so the compared items stay `[u8; 4]` rather than one array against one slice * three `type` aliases for the census maps and the captured-quad tuple * `&PathBuf` -> `&Path` in two disc tests * two range loops; one of them keeps `#[allow(needless_range_loop)]` with the reason -- the index is into a map's value, which changes each iteration * the module doc list in `invert_capture` re-indented to markdown's rules * `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed. The sibling test at :162 is the one that asserts an offset; if this one was meant to as well, that is a test change and not a lint fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
4.9 KiB
Rust
139 lines
4.9 KiB
Rust
//! Scratch analysis: which IDXD fields are DECLARED but left at their default on
|
|
//! disc, per schema. Those defaults live in title code, so they can only be read
|
|
//! from the running game — this prints the shopping list for that dynamic capture.
|
|
//!
|
|
//! Run: cargo run -p sylpheed-formats --example defaulted_fields -- <disc-root>
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
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 == '.')
|
|
}
|
|
|
|
/// Same shape-test the parser uses: an identifier-looking token that is not a
|
|
/// value literal is a field-name key.
|
|
fn is_key(s: &str) -> bool {
|
|
!s.is_empty()
|
|
&& s.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
&& s.chars()
|
|
.next()
|
|
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
|
&& !is_number(s)
|
|
}
|
|
|
|
fn is_value(s: &str) -> bool {
|
|
is_number(s) || !is_key(s)
|
|
}
|
|
|
|
fn main() {
|
|
let root = std::env::args()
|
|
.nth(1)
|
|
.unwrap_or_else(|| "/home/fabi/RE - Project Sylpheed/sylph_extract".into());
|
|
let paks: Vec<String> = std::env::args().skip(2).collect();
|
|
let paks = if paks.is_empty() {
|
|
vec![
|
|
"dat/GP_MAIN_GAME_E.pak".to_string(),
|
|
"dat/GP_HANGAR_ARSENAL.pak".to_string(),
|
|
]
|
|
} else {
|
|
paks
|
|
};
|
|
|
|
for pak_rel in &paks {
|
|
let path = std::path::Path::new(&root).join(pak_rel);
|
|
let Ok(arc) = PakArchive::open(&path) else {
|
|
eprintln!("-- skip {pak_rel} (open failed)");
|
|
continue;
|
|
};
|
|
println!("\n================ {pak_rel} ================");
|
|
|
|
// schema -> key -> (n_set, n_defaulted, distinct values, example owners)
|
|
type Stat = (usize, usize, BTreeSet<String>, Vec<String>);
|
|
let mut per_schema: BTreeMap<u32, (usize, BTreeMap<String, Stat>)> = BTreeMap::new();
|
|
|
|
for e in arc.entries() {
|
|
let Ok(bytes) = arc.read(e) else { continue };
|
|
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
|
continue;
|
|
};
|
|
let ident = obj.identity();
|
|
let toks = obj.tokens().to_vec();
|
|
let entry = per_schema.entry(obj.schema_hash).or_default();
|
|
entry.0 += 1;
|
|
// Track which keys this object declares, and whether each is valued.
|
|
let mut seen_here: BTreeMap<String, Option<String>> = BTreeMap::new();
|
|
for (i, t) in toks.iter().enumerate() {
|
|
if !is_key(t) {
|
|
continue;
|
|
}
|
|
let prev = if i == 0 { None } else { Some(&toks[i - 1]) };
|
|
let valued = prev.map(|p| is_value(p)).unwrap_or(false);
|
|
let v = if valued {
|
|
Some(toks[i - 1].clone())
|
|
} else {
|
|
None
|
|
};
|
|
seen_here.entry(t.clone()).or_insert(v);
|
|
}
|
|
for (k, v) in seen_here {
|
|
let s = entry.1.entry(k).or_default();
|
|
match v {
|
|
Some(val) => {
|
|
s.0 += 1;
|
|
if s.2.len() < 12 {
|
|
s.2.insert(val);
|
|
}
|
|
}
|
|
None => {
|
|
s.1 += 1;
|
|
if s.3.len() < 6 {
|
|
s.3.push(ident.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (schema, (n_obj, keys)) in per_schema {
|
|
if n_obj < 2 {
|
|
continue;
|
|
}
|
|
let name = match schema {
|
|
0x0426_e81d => "PLAYER",
|
|
0x6ab4_825a => "WEAPON",
|
|
0x43fa_a517 => "UNIT",
|
|
0x3c5b_0549 => "VESSEL",
|
|
0xbd86_d41c => "CHARACTER",
|
|
0x3c9a_e32e => "STAGE",
|
|
0xb412_e6d8 => "MESSAGE",
|
|
_ => "?",
|
|
};
|
|
let defaulted: Vec<_> = keys.iter().filter(|(_, s)| s.1 > 0).collect();
|
|
if defaulted.is_empty() {
|
|
continue;
|
|
}
|
|
println!("\n--- schema 0x{schema:08x} {name} ({n_obj} objects) ---");
|
|
println!(
|
|
"{:<30} {:>5} {:>5} values seen (≤12) | owners defaulting",
|
|
"KEY", "set", "dflt"
|
|
);
|
|
for (k, (n_set, n_def, vals, owners)) in defaulted {
|
|
let vv: Vec<&str> = vals.iter().map(|s| s.as_str()).collect();
|
|
println!(
|
|
"{:<30} {:>5} {:>5} {} | {}",
|
|
k,
|
|
n_set,
|
|
n_def,
|
|
vv.join(","),
|
|
owners.join(",")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|