Merge pull request 'style: rustfmt sweep — 774 hunks across 154 files → 0 (#12)' (#18) from chore/rustfmt-sweep into main
Reviewed-on: #18
This commit is contained in:
@@ -29,7 +29,9 @@ fn main() {
|
||||
paks.sort();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
let Ok(arc) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if !find(&b, b"ACHIEVEMENTS_REQUIREMENTS") {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
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 a=gd::load_arsenal(&pak);
|
||||
for (hp,list) in [("NOSE",&a.nose),("ARM1",&a.arm1),("ARM2",&a.arm2),("ARM3",&a.arm3)]{
|
||||
println!("{hp} ({}): {:?}", list.len(), list.iter().take(8).collect::<Vec<_>>());
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap();
|
||||
let a = gd::load_arsenal(&pak);
|
||||
for (hp, list) in [
|
||||
("NOSE", &a.nose),
|
||||
("ARM1", &a.arm1),
|
||||
("ARM2", &a.arm2),
|
||||
("ARM3", &a.arm3),
|
||||
] {
|
||||
println!(
|
||||
"{hp} ({}): {:?}",
|
||||
list.len(),
|
||||
list.iter().take(8).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
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_MAIN_GAME_E.pak")).unwrap();
|
||||
let units=gd::load_units(&pak); let vessels=gd::load_vessels(&pak);
|
||||
let hp=|id:&str|->String{
|
||||
units.iter().find(|u|u.id.as_deref()==Some(id)).and_then(|u|u.hp).map(|h|format!("{h:.0}hp"))
|
||||
.or_else(||vessels.iter().find(|v|v.id.as_deref()==Some(id)).and_then(|v|v.hp).map(|h|format!("{h:.0}HP")))
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let units = gd::load_units(&pak);
|
||||
let vessels = gd::load_vessels(&pak);
|
||||
let hp = |id: &str| -> String {
|
||||
units
|
||||
.iter()
|
||||
.find(|u| u.id.as_deref() == Some(id))
|
||||
.and_then(|u| u.hp)
|
||||
.map(|h| format!("{h:.0}hp"))
|
||||
.or_else(|| {
|
||||
vessels
|
||||
.iter()
|
||||
.find(|v| v.id.as_deref() == Some(id))
|
||||
.and_then(|v| v.hp)
|
||||
.map(|h| format!("{h:.0}HP"))
|
||||
})
|
||||
.unwrap_or("·".into())
|
||||
};
|
||||
let rosters=gd::load_unit_rosters(&pak);
|
||||
for r in rosters.iter().filter(|r|r.stage.is_some()).take(4){
|
||||
println!("\n{} — {} combatants:", r.stage.as_deref().unwrap(), r.units.len());
|
||||
for u in r.units.iter().filter(|u|u.contains("ADAN")).take(6){
|
||||
let short=u.trim_start_matches("UN_").split('_').skip(1).collect::<Vec<_>>().join("_");
|
||||
let rosters = gd::load_unit_rosters(&pak);
|
||||
for r in rosters.iter().filter(|r| r.stage.is_some()).take(4) {
|
||||
println!(
|
||||
"\n{} — {} combatants:",
|
||||
r.stage.as_deref().unwrap(),
|
||||
r.units.len()
|
||||
);
|
||||
for u in r.units.iter().filter(|u| u.contains("ADAN")).take(6) {
|
||||
let short = u
|
||||
.trim_start_matches("UN_")
|
||||
.split('_')
|
||||
.skip(1)
|
||||
.collect::<Vec<_>>()
|
||||
.join("_");
|
||||
println!(" {short:32} {}", hp(u));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,22 @@ fn main() {
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let names = sylpheed_formats::mesh::xbg7_resource_names(&bytes);
|
||||
let ids: Vec<String> = {
|
||||
let mut v: Vec<String> = names.iter().filter(|n| is_base_part(n))
|
||||
.filter_map(|n| ship_id_of(n).map(|s| s.to_string())).collect();
|
||||
v.sort(); v.dedup(); v.truncate(5); v
|
||||
let mut v: Vec<String> = names
|
||||
.iter()
|
||||
.filter(|n| is_base_part(n))
|
||||
.filter_map(|n| ship_id_of(n).map(|s| s.to_string()))
|
||||
.collect();
|
||||
v.sort();
|
||||
v.dedup();
|
||||
v.truncate(5);
|
||||
v
|
||||
};
|
||||
for id in &ids {
|
||||
let want: HashSet<String> = names.iter()
|
||||
.filter(|n| ship_id_of(n) == Some(id.as_str())).cloned().collect();
|
||||
let want: HashSet<String> = names
|
||||
.iter()
|
||||
.filter(|n| ship_id_of(n) == Some(id.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let t = Instant::now();
|
||||
let got = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
println!("{id}: {} models in {:?}", got.len(), t.elapsed());
|
||||
|
||||
@@ -28,7 +28,10 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
};
|
||||
let (vc, ic) = markers[0];
|
||||
println!("{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}", markers.len());
|
||||
println!(
|
||||
"{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}",
|
||||
markers.len()
|
||||
);
|
||||
|
||||
// Where did the decoder put it?
|
||||
let ours = Xbg7Model::stage_models(&bytes)
|
||||
@@ -38,7 +41,10 @@ fn main() {
|
||||
println!("our anchor: {ours:?}");
|
||||
|
||||
let starts = debug_vertex_run_starts(&bytes, stride);
|
||||
println!("{} candidate vertex-run starts for stride {stride}", starts.len());
|
||||
println!(
|
||||
"{} candidate vertex-run starts for stride {stride}",
|
||||
starts.len()
|
||||
);
|
||||
|
||||
// Score every (start, pad): degenerate triangles, winding against the stored
|
||||
// normals, and whether the run covers the pool exactly.
|
||||
@@ -67,7 +73,10 @@ fn main() {
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
if pos.iter().any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6)) {
|
||||
if pos
|
||||
.iter()
|
||||
.any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut degen = 0usize;
|
||||
@@ -82,8 +91,16 @@ fn main() {
|
||||
// stand-in so this stays declaration-agnostic: a consistent mesh
|
||||
// has all faces pointing away from the centroid on a convex-ish
|
||||
// hull. Weak, so degeneracy leads the sort.
|
||||
let e1 = [pos[y][0] - pos[x][0], pos[y][1] - pos[x][1], pos[y][2] - pos[x][2]];
|
||||
let e2 = [pos[z][0] - pos[x][0], pos[z][1] - pos[x][1], pos[z][2] - pos[x][2]];
|
||||
let e1 = [
|
||||
pos[y][0] - pos[x][0],
|
||||
pos[y][1] - pos[x][1],
|
||||
pos[y][2] - pos[x][2],
|
||||
];
|
||||
let e2 = [
|
||||
pos[z][0] - pos[x][0],
|
||||
pos[z][1] - pos[x][1],
|
||||
pos[z][2] - pos[x][2],
|
||||
];
|
||||
let f = [
|
||||
e1[1] * e2[2] - e1[2] * e2[1],
|
||||
e1[2] * e2[0] - e1[0] * e2[2],
|
||||
@@ -104,12 +121,19 @@ fn main() {
|
||||
agree += 1;
|
||||
}
|
||||
}
|
||||
let w = if counted == 0 { 0.0 } else { agree as f32 / counted as f32 };
|
||||
let w = if counted == 0 {
|
||||
0.0
|
||||
} else {
|
||||
agree as f32 / counted as f32
|
||||
};
|
||||
rows.push((degen, w.max(1.0 - w), vb, pad, max_idx, max_idx + 1 == vc));
|
||||
}
|
||||
}
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.total_cmp(&a.1)));
|
||||
println!("\n{} in-range candidates; best 12 by (degenerate, winding):", rows.len());
|
||||
println!(
|
||||
"\n{} in-range candidates; best 12 by (degenerate, winding):",
|
||||
rows.len()
|
||||
);
|
||||
for (d, w, vb, pad, mx, cov) in rows.iter().take(12) {
|
||||
let mark = if Some(*vb) == ours { " <-- ours" } else { "" };
|
||||
println!(
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//! those float values.
|
||||
//!
|
||||
//! Usage: bounds_in_descriptor <container.xpr> <resource>...
|
||||
use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model};
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model};
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
@@ -24,7 +24,9 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else { continue };
|
||||
let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else {
|
||||
continue;
|
||||
};
|
||||
println!(
|
||||
"{} descriptor 0x{d0:x}..0x{d1:x} ({} bytes), box lo{:?} hi{:?}",
|
||||
m.name,
|
||||
@@ -34,8 +36,12 @@ fn main() {
|
||||
);
|
||||
// Where in the descriptor does each bound value appear (±0.01)?
|
||||
let targets: Vec<(&str, f32)> = vec![
|
||||
("lo.x", lo[0]), ("lo.y", lo[1]), ("lo.z", lo[2]),
|
||||
("hi.x", hi[0]), ("hi.y", hi[1]), ("hi.z", hi[2]),
|
||||
("lo.x", lo[0]),
|
||||
("lo.y", lo[1]),
|
||||
("lo.z", lo[2]),
|
||||
("hi.x", hi[0]),
|
||||
("hi.y", hi[1]),
|
||||
("hi.z", hi[2]),
|
||||
];
|
||||
for (label, v) in targets {
|
||||
let mut at: Vec<usize> = Vec::new();
|
||||
@@ -47,7 +53,10 @@ fn main() {
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
println!(" {label:5} {v:10.3} at descriptor offsets {:x?}", &at[..at.len().min(6)]);
|
||||
println!(
|
||||
" {label:5} {v:10.3} at descriptor offsets {:x?}",
|
||||
&at[..at.len().min(6)]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, 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 text=TextIndex::build(&pak);
|
||||
let mut stages=game_data::load_stages(&pak);
|
||||
stages.retain(|s|s.id.starts_with('S') && s.id.len()==3 && s.id[1..].parse::<u32>().map(|n|n<=16).unwrap_or(false));
|
||||
stages.sort_by(|a,b|a.id.cmp(&b.id));
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text = TextIndex::build(&pak);
|
||||
let mut stages = game_data::load_stages(&pak);
|
||||
stages.retain(|s| {
|
||||
s.id.starts_with('S')
|
||||
&& s.id.len() == 3
|
||||
&& s.id[1..].parse::<u32>().map(|n| n <= 16).unwrap_or(false)
|
||||
});
|
||||
stages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
println!("═══ CAMPAIGN (S01–S16) ═══");
|
||||
for s in &stages{
|
||||
let obj=text.objectives(&s.id,1);
|
||||
for s in &stages {
|
||||
let obj = text.objectives(&s.id, 1);
|
||||
println!("\n{} · {}", s.id, s.location.as_deref().unwrap_or("?"));
|
||||
for o in obj.iter().take(2){ println!(" ▸ {o}"); }
|
||||
for o in obj.iter().take(2) {
|
||||
println!(" ▸ {o}");
|
||||
}
|
||||
}
|
||||
// roster with real names
|
||||
let mut chars=game_data::load_characters(&pak);
|
||||
chars.retain(|c|c.faction.as_deref()==Some("TCAF") && c.unique==Some(true) && c.faces.len()>=4);
|
||||
let mut chars = game_data::load_characters(&pak);
|
||||
chars.retain(|c| {
|
||||
c.faction.as_deref() == Some("TCAF") && c.unique == Some(true) && c.faces.len() >= 4
|
||||
});
|
||||
println!("\n═══ PRINCIPAL CAST (TCAF, named) ═══");
|
||||
for c in &chars{
|
||||
let id=c.id.as_deref().unwrap_or("");
|
||||
let name=text.character_name(id.trim_start_matches("Character")).or_else(||c.name_key.as_deref().and_then(|k|text.get(k))).unwrap_or("?");
|
||||
println!(" {name:12} ({} portraits) [{}]", c.faces.len(), id.trim_start_matches("Character"));
|
||||
for c in &chars {
|
||||
let id = c.id.as_deref().unwrap_or("");
|
||||
let name = text
|
||||
.character_name(id.trim_start_matches("Character"))
|
||||
.or_else(|| c.name_key.as_deref().and_then(|k| text.get(k)))
|
||||
.unwrap_or("?");
|
||||
println!(
|
||||
" {name:12} ({} portraits) [{}]",
|
||||
c.faces.len(),
|
||||
id.trim_start_matches("Character")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --release --example capture_ib_truth -- <Stage_SNN.xpr> <capture.log>...
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn q(v: f32) -> i64 {
|
||||
(v as f64 * 1e4).round() as i64
|
||||
@@ -60,7 +60,13 @@ fn main() {
|
||||
let mut o = 0usize;
|
||||
while o + 12 <= bytes.len() {
|
||||
let (x, y, z) = (be(o), be(o + 4), be(o + 8));
|
||||
if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 {
|
||||
if x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& z.is_finite()
|
||||
&& x.abs() < 1e6
|
||||
&& y.abs() < 1e6
|
||||
&& z.abs() < 1e6
|
||||
{
|
||||
index.entry((q(x), q(y), q(z))).or_default().push(o as u32);
|
||||
}
|
||||
o += 4;
|
||||
@@ -72,7 +78,9 @@ fn main() {
|
||||
for dx in -1..=1i64 {
|
||||
for dy in -1..=1i64 {
|
||||
for dz in -1..=1i64 {
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue };
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else {
|
||||
continue;
|
||||
};
|
||||
for &off in cands {
|
||||
for stride in (12..=64).step_by(4) {
|
||||
let ok = (1..4).all(|j| {
|
||||
@@ -96,7 +104,9 @@ fn main() {
|
||||
eprintln!("no draw could be placed in this container");
|
||||
std::process::exit(1);
|
||||
};
|
||||
println!("container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)");
|
||||
println!(
|
||||
"container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)"
|
||||
);
|
||||
|
||||
// ── Our decoder's view of the same container.
|
||||
let models = Xbg7Model::stage_models(&bytes);
|
||||
@@ -104,14 +114,19 @@ fn main() {
|
||||
for m in &models {
|
||||
for sm in &m.meshes {
|
||||
if let Some(off) = sm.vbuf_offset {
|
||||
by_off
|
||||
.entry(off)
|
||||
.or_default()
|
||||
.push((m.name.clone(), sm.positions.len(), sm.indices.len()));
|
||||
by_off.entry(off).or_default().push((
|
||||
m.name.clone(),
|
||||
sm.positions.len(),
|
||||
sm.indices.len(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("decoded {} resources, {} distinct vertex offsets\n", models.len(), by_off.len());
|
||||
println!(
|
||||
"decoded {} resources, {} distinct vertex offsets\n",
|
||||
models.len(),
|
||||
by_off.len()
|
||||
);
|
||||
|
||||
// Declared-but-not-decoded resources, indexed by their first marker's
|
||||
// (vertex, index) counts. A drawn buffer our decoder cannot name is the one
|
||||
@@ -133,13 +148,21 @@ fn main() {
|
||||
}
|
||||
|
||||
// ── The report: one row per drawn BUFFER, aggregating its index batches.
|
||||
let mut per_buf: HashMap<u32, (usize, Vec<sylpheed_formats::ship_capture::CapturedIndexBuffer>, u32)> =
|
||||
HashMap::new();
|
||||
let mut per_buf: HashMap<
|
||||
u32,
|
||||
(
|
||||
usize,
|
||||
Vec<sylpheed_formats::ship_capture::CapturedIndexBuffer>,
|
||||
u32,
|
||||
),
|
||||
> = HashMap::new();
|
||||
for (delta, voff, d) in &hits {
|
||||
if *delta != base_delta {
|
||||
continue;
|
||||
}
|
||||
let e = per_buf.entry(d.vbase).or_insert((*voff, Vec::new(), d.vcount));
|
||||
let e = per_buf
|
||||
.entry(d.vbase)
|
||||
.or_insert((*voff, Vec::new(), d.vcount));
|
||||
let ib = d.ib.unwrap();
|
||||
if !e.1.contains(&ib) {
|
||||
e.1.push(ib);
|
||||
@@ -147,7 +170,8 @@ fn main() {
|
||||
}
|
||||
|
||||
let (mut pad0, mut pad_small, mut pad_off, mut unnamed) = (0usize, 0usize, 0usize, 0usize);
|
||||
let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) = (0usize, 0usize, 0usize, 0usize);
|
||||
let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) =
|
||||
(0usize, 0usize, 0usize, 0usize);
|
||||
let mut rows: Vec<(usize, String)> = Vec::new();
|
||||
for (_, (voff, ibs, vcount)) in per_buf.iter() {
|
||||
let batches = ibs.len();
|
||||
@@ -157,8 +181,11 @@ fn main() {
|
||||
let umax = ibs.iter().map(|i| i.imax).max().unwrap();
|
||||
let gap = *voff as i64 - hi; // bytes from the end of the index data to the vertex buffer
|
||||
let names = by_off.get(voff);
|
||||
let dec_idx = names
|
||||
.and_then(|v| v.iter().find(|(_, p, _)| *p as u32 == *vcount).map(|(_, _, i)| *i as u32));
|
||||
let dec_idx = names.and_then(|v| {
|
||||
v.iter()
|
||||
.find(|(_, p, _)| *p as u32 == *vcount)
|
||||
.map(|(_, _, i)| *i as u32)
|
||||
});
|
||||
// The decoder's assumption, scored: it expects the whole index buffer at
|
||||
// `vb - 2*idx_count - pad`, pad ≤ 3.
|
||||
let dec_pad = dec_idx.map(|i| *voff as i64 - (i as i64) * 2 - lo);
|
||||
@@ -218,5 +245,7 @@ fn main() {
|
||||
println!(
|
||||
"index extent: our idx_count == sum of captured batches for {idx_equal} buffers, differs for {idx_partial}"
|
||||
);
|
||||
println!("vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}");
|
||||
println!(
|
||||
"vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --release --example capture_index_bytes -- <Stage_SNN.xpr> <capture.log>...
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn q(v: f32) -> i64 {
|
||||
(v as f64 * 1e4).round() as i64
|
||||
@@ -34,7 +34,10 @@ fn main() {
|
||||
let text = std::fs::read_to_string(log).expect("log");
|
||||
for d in parse_capture(&text) {
|
||||
let k = d.ib.map(|i| (i.ibase, i.icount)).unwrap_or((0, 0));
|
||||
if d.ib.map_or(false, |i| i.head_len > 0) && d.pos.len() >= 4 && seen.insert((log.clone(), d.vbase, k)) {
|
||||
if d.ib.map_or(false, |i| i.head_len > 0)
|
||||
&& d.pos.len() >= 4
|
||||
&& seen.insert((log.clone(), d.vbase, k))
|
||||
{
|
||||
draws.push(d);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +51,13 @@ fn main() {
|
||||
let mut o = 0usize;
|
||||
while o + 12 <= bytes.len() {
|
||||
let (x, y, z) = (be(o), be(o + 4), be(o + 8));
|
||||
if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 {
|
||||
if x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& z.is_finite()
|
||||
&& x.abs() < 1e6
|
||||
&& y.abs() < 1e6
|
||||
&& z.abs() < 1e6
|
||||
{
|
||||
index.entry((q(x), q(y), q(z))).or_default().push(o as u32);
|
||||
}
|
||||
o += 4;
|
||||
@@ -60,7 +69,9 @@ fn main() {
|
||||
for dx in -1..=1i64 {
|
||||
for dy in -1..=1i64 {
|
||||
for dz in -1..=1i64 {
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue };
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else {
|
||||
continue;
|
||||
};
|
||||
for &off in cands {
|
||||
for stride in (12..=64).step_by(4) {
|
||||
let ok = (1..4).all(|j| {
|
||||
@@ -92,10 +103,11 @@ fn main() {
|
||||
for m in &models {
|
||||
for sm in &m.meshes {
|
||||
if let Some(off) = sm.vbuf_offset {
|
||||
by_off
|
||||
.entry(off)
|
||||
.or_default()
|
||||
.push((m.name.clone(), sm.positions.len(), sm.indices.clone()));
|
||||
by_off.entry(off).or_default().push((
|
||||
m.name.clone(),
|
||||
sm.positions.len(),
|
||||
sm.indices.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +124,10 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
let ibase = d.ib.unwrap().ibase as i64;
|
||||
ib_start.entry(d.vbase).and_modify(|e| *e = (*e).min(ibase)).or_insert(ibase);
|
||||
ib_start
|
||||
.entry(d.vbase)
|
||||
.and_modify(|e| *e = (*e).min(ibase))
|
||||
.or_insert(ibase);
|
||||
}
|
||||
|
||||
let (mut agree, mut disagree, mut unmatched, mut nooverlap) = (0usize, 0usize, 0usize, 0usize);
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
//! Identify which stage + ship a capture log came from: match the capture's
|
||||
//! draw vertex counts against every resource (incl. LODs) of every Stage_SNN.xpr.
|
||||
//! cargo run --release --example capture_match -- <capture.log> <resource3d dir>
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let text = std::fs::read_to_string(&a[1]).unwrap();
|
||||
let mut draws = parse_capture(&text);
|
||||
if draws.is_empty() { draws = parse_drawlog(&text); }
|
||||
let caps: HashSet<u32> = draws.iter().map(|d| d.vcount).filter(|v| *v >= 100).collect();
|
||||
eprintln!("{} draws, {} distinct vcounts>=100", draws.len(), caps.len());
|
||||
if draws.is_empty() {
|
||||
draws = parse_drawlog(&text);
|
||||
}
|
||||
let caps: HashSet<u32> = draws
|
||||
.iter()
|
||||
.map(|d| d.vcount)
|
||||
.filter(|v| *v >= 100)
|
||||
.collect();
|
||||
eprintln!(
|
||||
"{} draws, {} distinct vcounts>=100",
|
||||
draws.len(),
|
||||
caps.len()
|
||||
);
|
||||
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&a[2]).unwrap().filter_map(|e| e.ok())
|
||||
.filter(|e| { let n = e.file_name().to_string_lossy().to_string(); n.starts_with("Stage_") && n.ends_with(".xpr") })
|
||||
.map(|e| e.path()).collect();
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&a[2])
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
let n = e.file_name().to_string_lossy().to_string();
|
||||
n.starts_with("Stage_") && n.ends_with(".xpr")
|
||||
})
|
||||
.map(|e| e.path())
|
||||
.collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
@@ -28,12 +44,18 @@ fn main() {
|
||||
let vc: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||
if vc >= 100 && caps.contains(&(vc as u32)) {
|
||||
let id = m.name.get(..4).unwrap_or("?").to_string();
|
||||
hits.entry(id).or_default().push((m.name.clone(), vc as u32));
|
||||
hits.entry(id)
|
||||
.or_default()
|
||||
.push((m.name.clone(), vc as u32));
|
||||
}
|
||||
}
|
||||
let total: usize = hits.values().map(|v| v.len()).sum();
|
||||
if total >= 3 {
|
||||
println!("== {} : {} matching resources ==", path.file_name().unwrap().to_string_lossy(), total);
|
||||
println!(
|
||||
"== {} : {} matching resources ==",
|
||||
path.file_name().unwrap().to_string_lossy(),
|
||||
total
|
||||
);
|
||||
for (id, v) in &hits {
|
||||
let s: Vec<String> = v.iter().map(|(n, c)| format!("{n}({c})")).collect();
|
||||
println!(" {id}: {}", s.join(" "));
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
//! `Stage_S01` gave us.
|
||||
//!
|
||||
//! Usage: capture_truth_scan <resource3d_dir> <capture.log>...
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
|
||||
|
||||
fn q(v: f32) -> i64 {
|
||||
(v as f64 * 1e4).round() as i64
|
||||
@@ -47,7 +47,9 @@ fn main() {
|
||||
|
||||
let mut placed = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
// Index quantised position triples. Junk floats (NaN/huge) are skipped,
|
||||
// which prunes most of a texture-heavy container.
|
||||
let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||
@@ -55,7 +57,12 @@ fn main() {
|
||||
let mut o = 0usize;
|
||||
while o + 12 <= bytes.len() {
|
||||
let (x, y, z) = (be(o), be(o + 4), be(o + 8));
|
||||
if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6
|
||||
if x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& z.is_finite()
|
||||
&& x.abs() < 1e6
|
||||
&& y.abs() < 1e6
|
||||
&& z.abs() < 1e6
|
||||
{
|
||||
index.entry((q(x), q(y), q(z))).or_default().push(o as u32);
|
||||
}
|
||||
@@ -77,9 +84,8 @@ fn main() {
|
||||
let ok = (1..4).all(|j| {
|
||||
let at = off as usize + j * stride;
|
||||
at + 12 <= bytes.len()
|
||||
&& (0..3).all(|c| {
|
||||
(be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4
|
||||
})
|
||||
&& (0..3)
|
||||
.all(|c| (be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4)
|
||||
});
|
||||
if ok {
|
||||
deltas
|
||||
|
||||
@@ -71,15 +71,27 @@ fn main() {
|
||||
for p in &base_parts {
|
||||
let mut vcounts = Vec::new();
|
||||
let mut pos = Vec::new();
|
||||
for cand in [p.clone(), format!("{p}_m"), format!("{p}_l"), format!("{p}_d")] {
|
||||
for cand in [
|
||||
p.clone(),
|
||||
format!("{p}_m"),
|
||||
format!("{p}_l"),
|
||||
format!("{p}_d"),
|
||||
] {
|
||||
if let Some(m) = models.iter().find(|m| m.name == cand) {
|
||||
let mp: Vec<[f32; 3]> =
|
||||
m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect();
|
||||
let mp: Vec<[f32; 3]> = m
|
||||
.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.collect();
|
||||
vcounts.push(mp.len() as u32);
|
||||
pos.extend(mp);
|
||||
}
|
||||
}
|
||||
parts.push(Part { base: p.clone(), vcounts, pos });
|
||||
parts.push(Part {
|
||||
base: p.clone(),
|
||||
vcounts,
|
||||
pos,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate a draw against a part (direct or X-mirrored), like ship_capture.
|
||||
@@ -88,7 +100,9 @@ fn main() {
|
||||
return None;
|
||||
}
|
||||
let near = |a: &[f32; 3], b: &[f32; 3]| {
|
||||
(a[0] - b[0]).abs() <= 1e-2 && (a[1] - b[1]).abs() <= 1e-2 && (a[2] - b[2]).abs() <= 1e-2
|
||||
(a[0] - b[0]).abs() <= 1e-2
|
||||
&& (a[1] - b[1]).abs() <= 1e-2
|
||||
&& (a[2] - b[2]).abs() <= 1e-2
|
||||
};
|
||||
let (mut direct, mut mirror) = (0usize, 0usize);
|
||||
for q in &d.pos {
|
||||
@@ -144,7 +158,11 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("{log}: {} validated draws, {} bdy_04 refs", labeled.len(), refs.len());
|
||||
eprintln!(
|
||||
"{log}: {} validated draws, {} bdy_04 refs",
|
||||
labeled.len(),
|
||||
refs.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Cluster per part (greedy, 8-unit radius), print clusters with ≥3 samples.
|
||||
|
||||
@@ -22,14 +22,19 @@ fn main() {
|
||||
let mut rows = vec![];
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
if o.schema_hash != 0x3c9ae32e {
|
||||
continue;
|
||||
}
|
||||
let t = o.tokens();
|
||||
let stage = t
|
||||
.iter()
|
||||
.find_map(|s| s.strip_prefix("EnumUnit_").map(|x| x.trim_end_matches(".tbl").to_string()))
|
||||
.find_map(|s| {
|
||||
s.strip_prefix("EnumUnit_")
|
||||
.map(|x| x.trim_end_matches(".tbl").to_string())
|
||||
})
|
||||
.unwrap_or("?".into());
|
||||
let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string();
|
||||
rows.push((stage, bg, t.len()));
|
||||
@@ -47,11 +52,20 @@ fn main() {
|
||||
let mut all_tokens: Vec<(u32, Vec<String>)> = vec![];
|
||||
for e in ch.entries() {
|
||||
let Ok(b) = ch.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
*by_schema.entry(o.schema_hash).or_default() += 1;
|
||||
all_tokens.push((o.schema_hash, o.tokens().iter().map(|s| s.to_string()).collect()));
|
||||
all_tokens.push((
|
||||
o.schema_hash,
|
||||
o.tokens().iter().map(|s| s.to_string()).collect(),
|
||||
));
|
||||
}
|
||||
println!(" {} entries, {} IDXD objects", ch.entries().len(), all_tokens.len());
|
||||
println!(
|
||||
" {} entries, {} IDXD objects",
|
||||
ch.entries().len(),
|
||||
all_tokens.len()
|
||||
);
|
||||
for (h, n) in &by_schema {
|
||||
println!(" schema {h:08x} x{n}");
|
||||
}
|
||||
@@ -66,10 +80,16 @@ fn main() {
|
||||
let mut hits: std::collections::BTreeSet<String> = Default::default();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
for t in o.tokens() {
|
||||
let l = t.to_ascii_lowercase();
|
||||
if l.contains("challenge") || t.ends_with("_EX") || t.contains("_EX4") || t.contains("_EX5") {
|
||||
if l.contains("challenge")
|
||||
|| t.ends_with("_EX")
|
||||
|| t.contains("_EX4")
|
||||
|| t.contains("_EX5")
|
||||
{
|
||||
hits.insert(format!("{:08x} {t}", o.schema_hash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ fn main() {
|
||||
paks.sort();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
let Ok(arc) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if KEYS.iter().filter(|k| find(&b, k.as_bytes())).count() < KEYS.len() {
|
||||
|
||||
@@ -15,7 +15,9 @@ fn main() {
|
||||
files.sort();
|
||||
let (mut total, mut composite, mut composite_named) = (0usize, 0usize, 0usize);
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
total += 1;
|
||||
let has_nodes = !scene_world_nodes(&bytes, &m.name).is_empty();
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//! shift chain after the exact-coverage fix.
|
||||
//!
|
||||
//! Usage: consensus_check <resource3d_dir> [--list]
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -25,7 +25,9 @@ fn main() {
|
||||
// name -> [(container, verts, tris, span)]
|
||||
let mut seen: BTreeMap<String, Vec<(String, usize, usize, [i64; 3])>> = BTreeMap::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let where_ = f.file_name().unwrap().to_string_lossy().to_string();
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
@@ -47,7 +49,9 @@ fn main() {
|
||||
(hi[1] - lo[1]).round() as i64,
|
||||
(hi[2] - lo[2]).round() as i64,
|
||||
];
|
||||
seen.entry(m.name.clone()).or_default().push((where_.clone(), v, t, span));
|
||||
seen.entry(m.name.clone())
|
||||
.or_default()
|
||||
.push((where_.clone(), v, t, span));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +85,5 @@ fn main() {
|
||||
println!("{r}");
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{minority} minority decodes across {resources} resources that have a majority"
|
||||
);
|
||||
println!("{minority} minority decodes across {resources} resources that have a majority");
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
//! cargo run --release --example correlate_capture -- \
|
||||
//! xenia_ship_capture.log Stage_S01 e106 bdy_04 --emit
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||
use sylpheed_formats::ship_capture::{
|
||||
correlate, parse_capture, parse_drawlog, serialize_table, PartKey,
|
||||
};
|
||||
use sylpheed_formats::xiso::open_iso;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
@@ -58,10 +58,15 @@ fn main() {
|
||||
// Decode the ship's base parts AND their LOD copies (vcount + leading
|
||||
// positions are the match keys; LODs share the base part's local frame).
|
||||
let bytes = {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
let mut r = open_iso(Path::new(&iso)).await.unwrap();
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap()
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr"))
|
||||
.await
|
||||
.unwrap()
|
||||
})
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
@@ -83,7 +88,12 @@ fn main() {
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
let positions_of = |name: &str| -> Option<Vec<[f32; 3]>> {
|
||||
let m = models.iter().find(|m| m.name == name)?;
|
||||
Some(m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect())
|
||||
Some(
|
||||
m.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// One PartKey per (part, variant-vcount present in the capture) — correlate
|
||||
@@ -94,18 +104,33 @@ fn main() {
|
||||
// variant is the same part in the same local frame.
|
||||
let mut keys: Vec<PartKey> = Vec::new();
|
||||
for part in &base_parts {
|
||||
let variants =
|
||||
[part.clone(), format!("{part}_m"), format!("{part}_l"), format!("{part}_d")];
|
||||
let union: Vec<[f32; 3]> =
|
||||
variants.iter().filter_map(|v| positions_of(v)).flatten().collect();
|
||||
let variants = [
|
||||
part.clone(),
|
||||
format!("{part}_m"),
|
||||
format!("{part}_l"),
|
||||
format!("{part}_d"),
|
||||
];
|
||||
let union: Vec<[f32; 3]> = variants
|
||||
.iter()
|
||||
.filter_map(|v| positions_of(v))
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut any = false;
|
||||
for cand in &variants {
|
||||
if let Some(pos) = positions_of(cand) {
|
||||
let vcount = pos.len() as u32;
|
||||
if draws.iter().any(|d| d.vcount == vcount) {
|
||||
let lod = if cand == part { "full" } else { cand.rsplit('_').next().unwrap_or("?") };
|
||||
let lod = if cand == part {
|
||||
"full"
|
||||
} else {
|
||||
cand.rsplit('_').next().unwrap_or("?")
|
||||
};
|
||||
eprintln!(" {part:20} try vcount={vcount:6} [{lod}]");
|
||||
keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() });
|
||||
keys.push(PartKey {
|
||||
part: part.clone(),
|
||||
vcount,
|
||||
ref_pos: union.clone(),
|
||||
});
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
@@ -120,9 +145,15 @@ fn main() {
|
||||
return;
|
||||
};
|
||||
|
||||
eprintln!("\nreference = {} → ship-relative placement:", ship.reference);
|
||||
eprintln!(
|
||||
"\nreference = {} → ship-relative placement:",
|
||||
ship.reference
|
||||
);
|
||||
for p in &ship.parts {
|
||||
eprintln!(" {:18} T=[{:9.1}{:9.1}{:9.1}]", p.part, p.t[0], p.t[1], p.t[2]);
|
||||
eprintln!(
|
||||
" {:18} T=[{:9.1}{:9.1}{:9.1}]",
|
||||
p.part, p.t[0], p.t[1], p.t[2]
|
||||
);
|
||||
}
|
||||
for part in &base_parts {
|
||||
if !ship.parts.iter().any(|p| &p.part == part) {
|
||||
|
||||
@@ -16,19 +16,23 @@
|
||||
//! SYLPHEED_ISO=... cargo run --release --example correlate_frames -- \
|
||||
//! <capture.log> <Stage_SNN> <ship_id> [ref_part_substr] [--min-parts N]
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::Path;
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||
use sylpheed_formats::ship_capture::{
|
||||
correlate, parse_capture, parse_drawlog, segment_frames, PartKey,
|
||||
};
|
||||
use sylpheed_formats::xiso::open_iso;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
fn median(mut v: Vec<f32>) -> f32 {
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let n = v.len();
|
||||
if n % 2 == 1 { v[n / 2] } else { 0.5 * (v[n / 2 - 1] + v[n / 2]) }
|
||||
if n % 2 == 1 {
|
||||
v[n / 2]
|
||||
} else {
|
||||
0.5 * (v[n / 2 - 1] + v[n / 2])
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -54,13 +58,22 @@ fn main() {
|
||||
draws = parse_drawlog(&text);
|
||||
}
|
||||
let frames = segment_frames(&draws);
|
||||
println!("{} draws → {} camera-consistent blocks", draws.len(), frames.len());
|
||||
println!(
|
||||
"{} draws → {} camera-consistent blocks",
|
||||
draws.len(),
|
||||
frames.len()
|
||||
);
|
||||
|
||||
let bytes = {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
let mut r = open_iso(Path::new(&iso)).await.unwrap();
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap()
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr"))
|
||||
.await
|
||||
.unwrap()
|
||||
})
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
@@ -81,7 +94,12 @@ fn main() {
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
let positions_of = |name: &str| -> Option<Vec<[f32; 3]>> {
|
||||
let m = models.iter().find(|m| m.name == name)?;
|
||||
Some(m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect())
|
||||
Some(
|
||||
m.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// part -> [T per frame], and how many frames placed it at all.
|
||||
@@ -91,20 +109,33 @@ fn main() {
|
||||
for (fi, fr) in frames.iter().enumerate() {
|
||||
let mut keys: Vec<PartKey> = Vec::new();
|
||||
for part in &base_parts {
|
||||
let variants =
|
||||
[part.clone(), format!("{part}_m"), format!("{part}_l"), format!("{part}_d")];
|
||||
let union: Vec<[f32; 3]> =
|
||||
variants.iter().filter_map(|v| positions_of(v)).flatten().collect();
|
||||
let variants = [
|
||||
part.clone(),
|
||||
format!("{part}_m"),
|
||||
format!("{part}_l"),
|
||||
format!("{part}_d"),
|
||||
];
|
||||
let union: Vec<[f32; 3]> = variants
|
||||
.iter()
|
||||
.filter_map(|v| positions_of(v))
|
||||
.flatten()
|
||||
.collect();
|
||||
for cand in &variants {
|
||||
if let Some(pos) = positions_of(cand) {
|
||||
let vcount = pos.len() as u32;
|
||||
if fr.iter().any(|d| d.vcount == vcount) {
|
||||
keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() });
|
||||
keys.push(PartKey {
|
||||
part: part.clone(),
|
||||
vcount,
|
||||
ref_pos: union.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(ship) = correlate(id, fr, &keys, ref_sub) else { continue };
|
||||
let Some(ship) = correlate(id, fr, &keys, ref_sub) else {
|
||||
continue;
|
||||
};
|
||||
if ship.parts.len() < min_parts {
|
||||
continue;
|
||||
}
|
||||
@@ -114,7 +145,10 @@ fn main() {
|
||||
// Averaging them together is what makes an otherwise clean result look
|
||||
// like it disagrees by exactly the distance between the two references.
|
||||
if !ship.reference.contains(ref_sub) {
|
||||
println!(" block {fi:2}: skipped — reference fell back to {}", ship.reference);
|
||||
println!(
|
||||
" block {fi:2}: skipped — reference fell back to {}",
|
||||
ship.reference
|
||||
);
|
||||
continue;
|
||||
}
|
||||
used_frames += 1;
|
||||
@@ -173,7 +207,8 @@ fn main() {
|
||||
let spread: Vec<f32> = (0..3)
|
||||
.map(|a| {
|
||||
let v: Vec<f32> = cl.iter().map(|t| t[a]).collect();
|
||||
v.iter().cloned().fold(f32::MIN, f32::max) - v.iter().cloned().fold(f32::MAX, f32::min)
|
||||
v.iter().cloned().fold(f32::MIN, f32::max)
|
||||
- v.iter().cloned().fold(f32::MAX, f32::min)
|
||||
})
|
||||
.collect();
|
||||
let verdict = if cl.len() < 2 {
|
||||
@@ -187,14 +222,21 @@ fn main() {
|
||||
// (turret aiming, engine gimballing) does not — which is what separates
|
||||
// "the assembler has the rotation wrong" from "the part moved".
|
||||
let all_ms = rots.get(part).cloned().unwrap_or_default();
|
||||
let ms: Vec<[[f32; 3]; 3]> =
|
||||
cl_idx.iter().filter_map(|&i| all_ms.get(i).copied()).collect();
|
||||
let ms: Vec<[[f32; 3]; 3]> = cl_idx
|
||||
.iter()
|
||||
.filter_map(|&i| all_ms.get(i).copied())
|
||||
.collect();
|
||||
// Keep a rotation from INSIDE the cluster: the first sample overall can
|
||||
// belong to another instance, and diffing static against that reads as a
|
||||
// rotation error that is really an instance mix-up.
|
||||
consensus.insert(
|
||||
part.clone(),
|
||||
(med, ms.first().copied().unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])),
|
||||
(
|
||||
med,
|
||||
ms.first()
|
||||
.copied()
|
||||
.unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
|
||||
),
|
||||
);
|
||||
let rot_var = ms
|
||||
.iter()
|
||||
@@ -219,8 +261,12 @@ fn main() {
|
||||
// re-expressed in the reference part's frame before comparing — and the
|
||||
// rotation is compared too, because "wrong orientation" is half of the
|
||||
// reported viewer symptom and a translation-only check cannot see it.
|
||||
let Some(si) = args.iter().position(|a| a == "--static") else { return };
|
||||
let Some(spath) = args.get(si + 1) else { return };
|
||||
let Some(si) = args.iter().position(|a| a == "--static") else {
|
||||
return;
|
||||
};
|
||||
let Some(spath) = args.get(si + 1) else {
|
||||
return;
|
||||
};
|
||||
let sbytes = std::fs::read(spath).expect("read stage container");
|
||||
// `include_external = true` — the engine cluster, the bridge and cross-id
|
||||
// turrets live in SEPARATE composites (`e_rou_e106_eng`, 3 nodes) that the
|
||||
@@ -269,12 +315,19 @@ fn main() {
|
||||
.fold(0.0f32, f32::max);
|
||||
worst_t = worst_t.max(dtm);
|
||||
worst_r = worst_r.max(drm);
|
||||
let mark = if dtm < 1.0 && drm < 0.02 { "MATCH" } else { "DIFFERS" };
|
||||
let mark = if dtm < 1.0 && drm < 0.02 {
|
||||
"MATCH"
|
||||
} else {
|
||||
"DIFFERS"
|
||||
};
|
||||
println!(
|
||||
" {part:18} static=[{:9.1}{:9.1}{:9.1}] dT={dtm:7.2} dR={drm:6.3} {mark}",
|
||||
r[0], r[1], r[2]
|
||||
);
|
||||
}
|
||||
println!("\nworst dT={worst_t:.2} worst dR={worst_r:.3} ({} static parts, {} captured)",
|
||||
scene.len(), samples.len());
|
||||
println!(
|
||||
"\nworst dT={worst_t:.2} worst dR={worst_r:.3} ({} static parts, {} captured)",
|
||||
scene.len(),
|
||||
samples.len()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! is well founded. This measures the slack on every block that DOES decode: if
|
||||
//! real geometry always covers its pool, under-coverage is good evidence of a
|
||||
//! wrong candidate and the gate stands.
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -22,7 +22,9 @@ fn main() {
|
||||
let mut hist: BTreeMap<i64, usize> = BTreeMap::new();
|
||||
let mut worst: Vec<(i64, String)> = Vec::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
for sub in &m.meshes {
|
||||
if sub.positions.is_empty() || sub.indices.is_empty() {
|
||||
@@ -32,14 +34,21 @@ fn main() {
|
||||
let slack = sub.positions.len() as i64 - 1 - max_idx;
|
||||
*hist.entry(slack.min(20)).or_default() += 1;
|
||||
if slack > 4 {
|
||||
worst.push((slack, format!("{} in {}", m.name, f.file_name().unwrap().to_string_lossy())));
|
||||
worst.push((
|
||||
slack,
|
||||
format!("{} in {}", m.name, f.file_name().unwrap().to_string_lossy()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("unreferenced tail vertices (vtx_count − 1 − max index), over decoded sub-meshes:");
|
||||
for (slack, n) in &hist {
|
||||
println!(" {:>3}{} : {n}", slack, if *slack == 20 { "+" } else { " " });
|
||||
println!(
|
||||
" {:>3}{} : {n}",
|
||||
slack,
|
||||
if *slack == 20 { "+" } else { " " }
|
||||
);
|
||||
}
|
||||
worst.sort_by_key(|(s, _)| std::cmp::Reverse(*s));
|
||||
for (s, w) in worst.iter().take(5) {
|
||||
|
||||
@@ -12,7 +12,13 @@ use sylpheed_formats::{movie_subtitle, PakArchive};
|
||||
fn movie_secs(path: &str) -> Option<f32> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path,
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
path,
|
||||
])
|
||||
.output()
|
||||
.ok()?;
|
||||
@@ -27,7 +33,9 @@ fn main() {
|
||||
let mut over = 0;
|
||||
let mut worst: Vec<(String, f32, f32)> = Vec::new();
|
||||
let dir = format!("{disc}/dat/movie");
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { return };
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else {
|
||||
return;
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.extension().is_none_or(|x| x != "wmv") {
|
||||
@@ -39,7 +47,9 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max);
|
||||
let Some(secs) = movie_secs(&p.to_string_lossy()) else { continue };
|
||||
let Some(secs) = movie_secs(&p.to_string_lossy()) else {
|
||||
continue;
|
||||
};
|
||||
checked += 1;
|
||||
if last > secs {
|
||||
over += 1;
|
||||
|
||||
@@ -16,12 +16,16 @@ use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
const OFFS: [usize; 4] = [28, 36, 44, 56];
|
||||
|
||||
fn be32(b: &[u8], o: usize) -> u32 {
|
||||
if o + 4 > b.len() { return 0; }
|
||||
if o + 4 > b.len() {
|
||||
return 0;
|
||||
}
|
||||
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: decl_word_probe <pak> [entry]");
|
||||
let path = std::env::args()
|
||||
.nth(1)
|
||||
.expect("usage: decl_word_probe <pak> [entry]");
|
||||
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
@@ -29,34 +33,62 @@ fn main() {
|
||||
let mut hit = [0usize; 4];
|
||||
let mut tot = 0usize;
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
if want.is_some_and(|w| w != i) { continue; }
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
// Index space to test against: the T8aD children, in child order.
|
||||
let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect();
|
||||
if build.elements.iter().all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) {
|
||||
if want.is_some_and(|w| w != i) {
|
||||
continue;
|
||||
}
|
||||
println!("== entry {i} ({} elements, {} T8aD children)", build.elements.len(), t8.len());
|
||||
for (n, c) in t8.iter().enumerate() { println!(" child[{n:2}] {}", c.name); }
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Some(kids) = ratc::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
// Index space to test against: the T8aD children, in child order.
|
||||
let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect();
|
||||
if build
|
||||
.elements
|
||||
.iter()
|
||||
.all(|el| el.sprite.is_some() || el.kind & 0x10 != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
"== entry {i} ({} elements, {} T8aD children)",
|
||||
build.elements.len(),
|
||||
t8.len()
|
||||
);
|
||||
for (n, c) in t8.iter().enumerate() {
|
||||
println!(" child[{n:2}] {}", c.name);
|
||||
}
|
||||
for el in &build.elements {
|
||||
if el.kind & 0x10 != 0 { continue; }
|
||||
if el.kind & 0x10 != 0 {
|
||||
continue;
|
||||
}
|
||||
let d = &bytes[0x20 + el.index * 60..0x20 + (el.index + 1) * 60];
|
||||
let words: Vec<u32> = OFFS.iter().map(|&o| be32(d, o)).collect();
|
||||
// The control: for a RESOLVED element, which T8aD child is it?
|
||||
let truth = el.sprite.as_ref()
|
||||
let truth = el
|
||||
.sprite
|
||||
.as_ref()
|
||||
.and_then(|s| t8.iter().position(|c| &c.name == s));
|
||||
if let Some(t) = truth {
|
||||
tot += 1;
|
||||
for (k, w) in words.iter().enumerate() {
|
||||
if *w as usize == t { hit[k] += 1; }
|
||||
if *w as usize == t {
|
||||
hit[k] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" [{:2}] {:26} sprite={:?} child={:?} +28={} +36={} +44={} +56={}",
|
||||
el.index, el.name, el.sprite, truth,
|
||||
words[0] as i32, words[1] as i32, words[2] as i32, words[3] as i32
|
||||
el.index,
|
||||
el.name,
|
||||
el.sprite,
|
||||
truth,
|
||||
words[0] as i32,
|
||||
words[1] as i32,
|
||||
words[2] as i32,
|
||||
words[3] as i32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,108 @@
|
||||
use sylpheed_formats::{ratc, idxd::IdxdObject, PakArchive};
|
||||
use std::collections::BTreeMap;
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
use sylpheed_formats::{idxd::IdxdObject, ratc, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
// 1. RATC child-type census across the big RATC paks
|
||||
let mut childtypes:BTreeMap<String,u64>=BTreeMap::new();
|
||||
let mut ratc_unparsed=0u64;
|
||||
for pk in ["GP_READY_ROOM","GP_MOVIE_THEATER","GP_DIALOG","GP_DEBRIEFING_PILOTLOG","GP_TITLE","GP_BUNK"]{
|
||||
let Ok(arc)=PakArchive::open(format!("{disc}/dat/{pk}.pak")) else{continue};
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
if !ratc::is_ratc(&b){continue;}
|
||||
match ratc::parse(&b){
|
||||
Some(kids)=> for k in kids{
|
||||
let ext=k.name.rsplit('.').next().unwrap_or("?").to_lowercase();
|
||||
*childtypes.entry(ext).or_default()+=1;
|
||||
},
|
||||
None=>ratc_unparsed+=1,
|
||||
let mut childtypes: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut ratc_unparsed = 0u64;
|
||||
for pk in [
|
||||
"GP_READY_ROOM",
|
||||
"GP_MOVIE_THEATER",
|
||||
"GP_DIALOG",
|
||||
"GP_DEBRIEFING_PILOTLOG",
|
||||
"GP_TITLE",
|
||||
"GP_BUNK",
|
||||
] {
|
||||
let Ok(arc) = PakArchive::open(format!("{disc}/dat/{pk}.pak")) else {
|
||||
continue;
|
||||
};
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if !ratc::is_ratc(&b) {
|
||||
continue;
|
||||
}
|
||||
match ratc::parse(&b) {
|
||||
Some(kids) => {
|
||||
for k in kids {
|
||||
let ext = k.name.rsplit('.').next().unwrap_or("?").to_lowercase();
|
||||
*childtypes.entry(ext).or_default() += 1;
|
||||
}
|
||||
}
|
||||
None => ratc_unparsed += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("=== RATC child-type census (big RATC paks) ===");
|
||||
let mut cv:Vec<_>=childtypes.into_iter().collect(); cv.sort_by_key(|x|std::cmp::Reverse(x.1));
|
||||
for (e,c) in &cv{ println!(" .{e:8} ×{c}"); }
|
||||
let mut cv: Vec<_> = childtypes.into_iter().collect();
|
||||
cv.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
for (e, c) in &cv {
|
||||
println!(" .{e:8} ×{c}");
|
||||
}
|
||||
println!(" (RATC bundles that failed to parse: {ratc_unparsed})");
|
||||
|
||||
// 2. The 00000002 mystery format (GP_MAIN_GAME_E)
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
if b.len()>=4 && b[0..4]==[0,0,0,2]{
|
||||
let hx:String=b[..48.min(b.len())].iter().map(|x|format!("{x:02x}")).collect::<Vec<_>>().join(" ");
|
||||
let asc:String=b[..64.min(b.len())].iter().map(|&x|if(0x20..0x7f).contains(&x){x as char}else{'.'}).collect();
|
||||
println!("\n=== 00000002 format sample ({}B) ===\n{hx}\n{asc}",b.len());
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if b.len() >= 4 && b[0..4] == [0, 0, 0, 2] {
|
||||
let hx: String = b[..48.min(b.len())]
|
||||
.iter()
|
||||
.map(|x| format!("{x:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let asc: String = b[..64.min(b.len())]
|
||||
.iter()
|
||||
.map(|&x| {
|
||||
if (0x20..0x7f).contains(&x) {
|
||||
x as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
"\n=== 00000002 format sample ({}B) ===\n{hx}\n{asc}",
|
||||
b.len()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sample the top undecoded IDXD schemas: first tokens (guess semantics)
|
||||
println!("\n=== top undecoded IDXD schemas — sample tokens (semantic hints) ===");
|
||||
let targets:[u32;6]=[0xb412e6d8,0x026379ab,0x43faa517,0x3c5b0549,0x6ab4825a,0x0426e81d];
|
||||
for want in targets{
|
||||
let mut shown=false;
|
||||
for e in arc.entries(){
|
||||
if shown{break;}
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
if b.len()<12 || &b[0..4]!=b"IDXD"{continue;}
|
||||
let s=u32::from_be_bytes([b[8],b[9],b[10],b[11]]);
|
||||
if s!=want{continue;}
|
||||
if let Ok(o)=IdxdObject::parse(&b){
|
||||
let t=o.tokens();
|
||||
let sample:Vec<String>=t.iter().take(14).map(|s|{let s=s.chars().take(18).collect::<String>();s}).collect();
|
||||
let targets: [u32; 6] = [
|
||||
0xb412e6d8, 0x026379ab, 0x43faa517, 0x3c5b0549, 0x6ab4825a, 0x0426e81d,
|
||||
];
|
||||
for want in targets {
|
||||
let mut shown = false;
|
||||
for e in arc.entries() {
|
||||
if shown {
|
||||
break;
|
||||
}
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if b.len() < 12 || &b[0..4] != b"IDXD" {
|
||||
continue;
|
||||
}
|
||||
let s = u32::from_be_bytes([b[8], b[9], b[10], b[11]]);
|
||||
if s != want {
|
||||
continue;
|
||||
}
|
||||
if let Ok(o) = IdxdObject::parse(&b) {
|
||||
let t = o.tokens();
|
||||
let sample: Vec<String> = t
|
||||
.iter()
|
||||
.take(14)
|
||||
.map(|s| {
|
||||
let s = s.chars().take(18).collect::<String>();
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
println!(" {want:08x}: {sample:?}");
|
||||
shown=true;
|
||||
shown = true;
|
||||
}
|
||||
}
|
||||
if !shown{ println!(" {want:08x}: (parse failed / not found)"); }
|
||||
if !shown {
|
||||
println!(" {want:08x}: (parse failed / not found)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ fn is_number(s: &str) -> bool {
|
||||
}
|
||||
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 == '_')
|
||||
&& 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 {
|
||||
@@ -29,7 +32,9 @@ fn main() {
|
||||
|
||||
for e in arc.entries() {
|
||||
let Ok(bytes) = arc.read(e) else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let toks = obj.tokens().to_vec();
|
||||
let mut hits: Vec<String> = vec![];
|
||||
for (i, t) in toks.iter().enumerate() {
|
||||
@@ -44,7 +49,12 @@ fn main() {
|
||||
});
|
||||
}
|
||||
if !hits.is_empty() {
|
||||
println!("0x{:08x} {:<44} {}", obj.schema_hash, obj.identity(), hits.join(" "));
|
||||
println!(
|
||||
"0x{:08x} {:<44} {}",
|
||||
obj.schema_hash,
|
||||
obj.identity(),
|
||||
hits.join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ 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 == '_')
|
||||
&& s.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
&& !is_number(s)
|
||||
}
|
||||
|
||||
@@ -71,7 +73,11 @@ fn main() {
|
||||
}
|
||||
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 };
|
||||
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 {
|
||||
@@ -107,10 +113,7 @@ fn main() {
|
||||
0xb412_e6d8 => "MESSAGE",
|
||||
_ => "?",
|
||||
};
|
||||
let defaulted: Vec<_> = keys
|
||||
.iter()
|
||||
.filter(|(_, s)| s.1 > 0)
|
||||
.collect();
|
||||
let defaulted: Vec<_> = keys.iter().filter(|(_, s)| s.1 > 0).collect();
|
||||
if defaulted.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -49,12 +49,16 @@ fn main() {
|
||||
if r + 12 > desc.len() {
|
||||
break;
|
||||
}
|
||||
let (o, code, usage) = (be32(desc, r), be32(desc, r + 4), be32(desc, r + 8) >> 16);
|
||||
let (o, code, usage) =
|
||||
(be32(desc, r), be32(desc, r + 4), be32(desc, r + 8) >> 16);
|
||||
if o == 0x00FF_0000 || code == 0xFFFF_FFFF || o > 0x1000 {
|
||||
println!(" end marker @0x{r:X}: off=0x{o:X} code=0x{code:X}");
|
||||
break;
|
||||
}
|
||||
println!(" off {o:>3} code 0x{:06X} usage {usage}", code & 0xFF_FFFF);
|
||||
println!(
|
||||
" off {o:>3} code 0x{:06X} usage {usage}",
|
||||
code & 0xFF_FFFF
|
||||
);
|
||||
stride = stride.max(o + 4);
|
||||
r += 12;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, 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 text=TextIndex::build(&pak);
|
||||
let msgs=game_data::load_demo_messages(&pak);
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text = TextIndex::build(&pak);
|
||||
let msgs = game_data::load_demo_messages(&pak);
|
||||
println!("{} dialogue lines total\n", msgs.len());
|
||||
for m in msgs.iter().filter(|m|m.character.is_some()&&!m.page_keys.is_empty()).take(8){
|
||||
let who=m.character.as_deref().unwrap_or("?").trim_start_matches("Character");
|
||||
let line:String=m.page_keys.iter().filter_map(|k|text.get(k)).collect::<Vec<_>>().join(" ");
|
||||
println!(" {who:10} [{}] “{}”", m.voice_clip.as_deref().unwrap_or("-"), line.chars().take(64).collect::<String>());
|
||||
for m in msgs
|
||||
.iter()
|
||||
.filter(|m| m.character.is_some() && !m.page_keys.is_empty())
|
||||
.take(8)
|
||||
{
|
||||
let who = m
|
||||
.character
|
||||
.as_deref()
|
||||
.unwrap_or("?")
|
||||
.trim_start_matches("Character");
|
||||
let line: String = m
|
||||
.page_keys
|
||||
.iter()
|
||||
.filter_map(|k| text.get(k))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
println!(
|
||||
" {who:10} [{}] “{}”",
|
||||
m.voice_clip.as_deref().unwrap_or("-"),
|
||||
line.chars().take(64).collect::<String>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,60 @@
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
use std::collections::BTreeMap;
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let tables:[(u32,&str);4]=[(0x0426e81d,"Player / physics+scoring"),(0x6ab4825a,"Weapon"),(0x43faa517,"Unit / craft"),(0x3c5b0549,"Vessel / capital ship")];
|
||||
for (want,label) in tables{
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let tables: [(u32, &str); 4] = [
|
||||
(0x0426e81d, "Player / physics+scoring"),
|
||||
(0x6ab4825a, "Weapon"),
|
||||
(0x43faa517, "Unit / craft"),
|
||||
(0x3c5b0549, "Vessel / capital ship"),
|
||||
];
|
||||
for (want, label) in tables {
|
||||
// collect all records of this schema
|
||||
let mut recs:Vec<IdxdObject>=vec![];
|
||||
for e in arc.entries(){ let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; if o.schema_hash==want{recs.push(o);} }
|
||||
let mut recs: Vec<IdxdObject> = vec![];
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
if o.schema_hash == want {
|
||||
recs.push(o);
|
||||
}
|
||||
}
|
||||
// field-union (explicit-valued keys), with occurrence count
|
||||
let mut cols:BTreeMap<String,u32>=BTreeMap::new();
|
||||
for o in &recs{ for (k,_) in o.resolved_fields(){ *cols.entry(k.into()).or_default()+=1; } }
|
||||
let mut cols: BTreeMap<String, u32> = BTreeMap::new();
|
||||
for o in &recs {
|
||||
for (k, _) in o.resolved_fields() {
|
||||
*cols.entry(k.into()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
println!("\n╔══ {label} [{want:08x}] {} records ══", recs.len());
|
||||
let mut cv:Vec<_>=cols.into_iter().collect(); cv.sort_by_key(|x|std::cmp::Reverse(x.1));
|
||||
let colstr:String=cv.iter().take(28).map(|(k,c)|format!("{k}({c})")).collect::<Vec<_>>().join(" ");
|
||||
let mut cv: Vec<_> = cols.into_iter().collect();
|
||||
cv.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
let colstr: String = cv
|
||||
.iter()
|
||||
.take(28)
|
||||
.map(|(k, c)| format!("{k}({c})"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
println!("║ numeric/enum fields: {colstr}");
|
||||
// dump 2 sample records: identity + explicit fields
|
||||
for o in recs.iter().take(2){
|
||||
let id=o.get_raw("ID").unwrap_or("?");
|
||||
let name=o.get_raw("Name").unwrap_or("");
|
||||
for o in recs.iter().take(2) {
|
||||
let id = o.get_raw("ID").unwrap_or("?");
|
||||
let name = o.get_raw("Name").unwrap_or("");
|
||||
print!("║ • {id}");
|
||||
if !name.is_empty(){print!(" «{name}»");}
|
||||
if !name.is_empty() {
|
||||
print!(" «{name}»");
|
||||
}
|
||||
println!();
|
||||
let fields:Vec<String>=o.resolved_fields().iter().map(|(k,v)|format!("{k}={v}")).collect();
|
||||
for chunk in fields.chunks(5){ println!("║ {}", chunk.join(" ")); }
|
||||
let fields: Vec<String> = o
|
||||
.resolved_fields()
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}={v}"))
|
||||
.collect();
|
||||
for chunk in fields.chunks(5) {
|
||||
println!("║ {}", chunk.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
use sylpheed_formats::{game_data, PakArchive};
|
||||
use std::collections::BTreeMap;
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let chars=game_data::load_characters(&pak);
|
||||
let mut byfac:BTreeMap<String,Vec<String>>=Default::default();
|
||||
for c in &chars{ byfac.entry(c.faction.clone().unwrap_or("·".into())).or_default().push(format!("{}({})",c.id.clone().unwrap_or_default().trim_start_matches("Character"),c.faces.len())); }
|
||||
println!("{} characters across {} factions:",chars.len(),byfac.len());
|
||||
for (f,mut v) in byfac{ v.sort(); println!(" [{f}] {}", v.join(" ")); }
|
||||
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 chars = game_data::load_characters(&pak);
|
||||
let mut byfac: BTreeMap<String, Vec<String>> = Default::default();
|
||||
for c in &chars {
|
||||
byfac
|
||||
.entry(c.faction.clone().unwrap_or("·".into()))
|
||||
.or_default()
|
||||
.push(format!(
|
||||
"{}({})",
|
||||
c.id.clone()
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches("Character"),
|
||||
c.faces.len()
|
||||
));
|
||||
}
|
||||
println!(
|
||||
"{} characters across {} factions:",
|
||||
chars.len(),
|
||||
byfac.len()
|
||||
);
|
||||
for (f, mut v) in byfac {
|
||||
v.sort();
|
||||
println!(" [{f}] {}", v.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,83 @@
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
fn short(s:&str)->String{ s.trim_start_matches("Weapon_").trim_start_matches("UN_").trim_start_matches("UnitName_UN_").into() }
|
||||
fn g<'a>(o:&'a IdxdObject,k:&str)->String{ o.get_f32(k).map(|v|{if v==v.trunc(){format!("{}",v as i64)}else{format!("{v}")}}).or_else(||o.get_raw(k).filter(|x|x.chars().next().map(|c|c.is_ascii_digit()).unwrap_or(false)).map(|s|s.to_string())).unwrap_or("·".into()) }
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let recs=|want:u32|->Vec<IdxdObject>{ arc.entries().iter().filter_map(|e|arc.read(e).ok()).filter_map(|b|IdxdObject::parse(&b).ok()).filter(|o|o.schema_hash==want).collect() };
|
||||
println!("### WEAPONS (name | target | load | int | power | vel | range | trig)");
|
||||
for o in recs(0x6ab4825a).iter().take(16){ println!("{:<34}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), o.get_raw("TargetType").unwrap_or("·"), g(o,"LoadingCount"),g(o,"Interval"),g(o,"Power"),g(o,"Velocity"),g(o,"MaximumRange"),g(o,"TriggerShotCount")); }
|
||||
println!("\n### UNITS/CRAFT (name | HP | cruise | accel | radar | turrets | score)");
|
||||
for o in recs(0x43faa517).iter().take(16){ println!("{:<38}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), g(o,"HP"),g(o,"CruisingVelocity"),g(o,"Acceleration"),g(o,"RadarRange"),g(o,"TurretCount"),g(o,"ScorePoint")); }
|
||||
println!("\n### VESSELS/CAPITAL SHIPS (name | HP | Sz_X | Sz_Z | radar | turrets | bridges | hatches | shieldgen | thrusters)");
|
||||
for o in recs(0x3c5b0549).iter(){ println!("{:<30}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), g(o,"HP"),g(o,"Size_X"),g(o,"Size_Z"),g(o,"RadarRange"),g(o,"TurretCount"),g(o,"BridgeCount"),g(o,"HatchCount"),g(o,"ShieldGeneratorCount"),g(o,"ThrusterCount")); }
|
||||
fn short(s: &str) -> String {
|
||||
s.trim_start_matches("Weapon_")
|
||||
.trim_start_matches("UN_")
|
||||
.trim_start_matches("UnitName_UN_")
|
||||
.into()
|
||||
}
|
||||
fn g<'a>(o: &'a IdxdObject, k: &str) -> String {
|
||||
o.get_f32(k)
|
||||
.map(|v| {
|
||||
if v == v.trunc() {
|
||||
format!("{}", v as i64)
|
||||
} else {
|
||||
format!("{v}")
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
o.get_raw(k)
|
||||
.filter(|x| {
|
||||
x.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.unwrap_or("·".into())
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let recs = |want: u32| -> Vec<IdxdObject> {
|
||||
arc.entries()
|
||||
.iter()
|
||||
.filter_map(|e| arc.read(e).ok())
|
||||
.filter_map(|b| IdxdObject::parse(&b).ok())
|
||||
.filter(|o| o.schema_hash == want)
|
||||
.collect()
|
||||
};
|
||||
println!("### WEAPONS (name | target | load | int | power | vel | range | trig)");
|
||||
for o in recs(0x6ab4825a).iter().take(16) {
|
||||
println!(
|
||||
"{:<34}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
short(o.get_raw("ID").unwrap_or("?")),
|
||||
o.get_raw("TargetType").unwrap_or("·"),
|
||||
g(o, "LoadingCount"),
|
||||
g(o, "Interval"),
|
||||
g(o, "Power"),
|
||||
g(o, "Velocity"),
|
||||
g(o, "MaximumRange"),
|
||||
g(o, "TriggerShotCount")
|
||||
);
|
||||
}
|
||||
println!("\n### UNITS/CRAFT (name | HP | cruise | accel | radar | turrets | score)");
|
||||
for o in recs(0x43faa517).iter().take(16) {
|
||||
println!(
|
||||
"{:<38}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
short(o.get_raw("ID").unwrap_or("?")),
|
||||
g(o, "HP"),
|
||||
g(o, "CruisingVelocity"),
|
||||
g(o, "Acceleration"),
|
||||
g(o, "RadarRange"),
|
||||
g(o, "TurretCount"),
|
||||
g(o, "ScorePoint")
|
||||
);
|
||||
}
|
||||
println!("\n### VESSELS/CAPITAL SHIPS (name | HP | Sz_X | Sz_Z | radar | turrets | bridges | hatches | shieldgen | thrusters)");
|
||||
for o in recs(0x3c5b0549).iter() {
|
||||
println!(
|
||||
"{:<30}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
short(o.get_raw("ID").unwrap_or("?")),
|
||||
g(o, "HP"),
|
||||
g(o, "Size_X"),
|
||||
g(o, "Size_Z"),
|
||||
g(o, "RadarRange"),
|
||||
g(o, "TurretCount"),
|
||||
g(o, "BridgeCount"),
|
||||
g(o, "HatchCount"),
|
||||
g(o, "ShieldGeneratorCount"),
|
||||
g(o, "ThrusterCount")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, 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 text=TextIndex::build(&pak);
|
||||
for n in 1..=16u32{
|
||||
let sid=format!("S{n:02}");
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text = TextIndex::build(&pak);
|
||||
for n in 1..=16u32 {
|
||||
let sid = format!("S{n:02}");
|
||||
// primary objective across phases (first that resolves)
|
||||
let obj:Vec<String>=(1..=3).flat_map(|p|text.objectives(&sid,p)).map(|s|s.to_string()).collect();
|
||||
let lose:Vec<String>=(1..=3).flat_map(|p|text.lose_conditions(&sid,p)).map(|s|s.to_string()).collect();
|
||||
let full_obj=obj.join(" ");
|
||||
let full_lose=lose.into_iter().take(2).collect::<Vec<_>>().join(" ");
|
||||
let obj: Vec<String> = (1..=3)
|
||||
.flat_map(|p| text.objectives(&sid, p))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let lose: Vec<String> = (1..=3)
|
||||
.flat_map(|p| text.lose_conditions(&sid, p))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let full_obj = obj.join(" ");
|
||||
let full_lose = lose.into_iter().take(2).collect::<Vec<_>>().join(" ");
|
||||
println!("{sid}\t{full_obj}\t{full_lose}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! `XBG7_EDGE_CAP` sets the cap; this reports, for one setting, how much
|
||||
//! geometry decodes and how self-consistent it is across containers — the two
|
||||
//! numbers any change to the cap has to trade off. Run it once per cap value.
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -20,7 +20,9 @@ fn main() {
|
||||
let mut seen: BTreeMap<String, Vec<([i64; 3], usize, usize)>> = BTreeMap::new();
|
||||
let (mut models, mut verts) = (0usize, 0usize);
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for s in &m.meshes {
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
//! however big, is part of the silhouette.
|
||||
//!
|
||||
//! Usage: envelope_screen <resource3d_dir> [protrusion_fraction]
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship::{assemble_ship, ship_id_of};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let limit: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(0.35);
|
||||
let limit: f32 = std::env::args()
|
||||
.nth(2)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0.35);
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
@@ -25,7 +28,9 @@ fn main() {
|
||||
|
||||
let mut flagged = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let ids: BTreeSet<String> = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false)
|
||||
.iter()
|
||||
.filter_map(|m| ship_id_of(&m.name).map(|s| s.to_string()))
|
||||
@@ -40,7 +45,9 @@ fn main() {
|
||||
// World box per placement.
|
||||
let mut boxes: Vec<(String, [f32; 3], [f32; 3])> = Vec::new();
|
||||
for p in &placed {
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue };
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else {
|
||||
continue;
|
||||
};
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for s in &m.meshes {
|
||||
for q in &s.positions {
|
||||
@@ -102,5 +109,8 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{flagged} parts protrude more than {:.0}% of their ship's size", 100.0 * limit);
|
||||
println!(
|
||||
"{flagged} parts protrude more than {:.0}% of their ship's size",
|
||||
100.0 * limit
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! assembler and the viewer) can reach a different answer from a whole-container
|
||||
//! decode. This measures that directly.
|
||||
//! Usage: filter_consistency <container.xpr> [resource...]
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).expect("container");
|
||||
@@ -27,9 +27,16 @@ fn main() {
|
||||
if off(f) != off(s) {
|
||||
differ += 1;
|
||||
if differ <= 10 {
|
||||
println!("{n}: full decode at 0x{:x}, filtered at 0x{:x}", off(f), off(s));
|
||||
println!(
|
||||
"{n}: full decode at 0x{:x}, filtered at 0x{:x}",
|
||||
off(f),
|
||||
off(s)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{differ} of {} resources decode differently when filtered", names.len());
|
||||
println!(
|
||||
"{differ} of {} resources decode differently when filtered",
|
||||
names.len()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//! mirrored pair.
|
||||
//!
|
||||
//! Usage: find_mirror <container.xpr> <resource>...
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
@@ -18,7 +18,12 @@ fn main() {
|
||||
let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||
|
||||
for m in &models {
|
||||
let pos: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).take(8).collect();
|
||||
let pos: Vec<[f32; 3]> = m
|
||||
.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.clone())
|
||||
.take(8)
|
||||
.collect();
|
||||
if pos.len() < 8 {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
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);
|
||||
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;
|
||||
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();
|
||||
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(" "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//! Which gate stops the resources that never decode?
|
||||
//! Usage: gate_histogram <resource3d_dir> [max_resources]
|
||||
use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let cap: usize = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX);
|
||||
let cap: usize = std::env::args()
|
||||
.nth(2)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(usize::MAX);
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
@@ -20,7 +23,9 @@ fn main() {
|
||||
if done >= cap {
|
||||
break;
|
||||
}
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
if names.is_empty() {
|
||||
continue;
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
fn main(){let a:Vec<String>=std::env::args().collect();let b=std::fs::read(&a[1]).unwrap();
|
||||
for l in sylpheed_formats::mesh::debug_grouped_report(&b,&a[2],a[3].parse().unwrap()){println!("{l}")}}
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let b = std::fs::read(&a[1]).unwrap();
|
||||
for l in sylpheed_formats::mesh::debug_grouped_report(&b, &a[2], a[3].parse().unwrap()) {
|
||||
println!("{l}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,144 @@
|
||||
use sylpheed_formats::{hash::name_hash, movie_manifest, movie_subtitle as ms, movie_voice, slb, PakArchive};
|
||||
use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command;
|
||||
fn rg(disc:&str,g:u64,n:usize)->Vec<u8>{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out}
|
||||
fn ct(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)}
|
||||
fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()}
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap();
|
||||
let man=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap();
|
||||
let lpak=PakArchive::open(format!("{disc}/dat/movie/eng.pak")).unwrap();
|
||||
let reg=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|ct(b,b"eng\\Movie\\VOICE_ADV.slb"))).unwrap();
|
||||
let ids=movie_voice::registry_voice_ids(®);
|
||||
let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap();
|
||||
let ents=PakArchive::parse_toc(&stoc).unwrap();
|
||||
use std::fs;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::process::Command;
|
||||
use sylpheed_formats::{
|
||||
hash::name_hash, movie_manifest, movie_subtitle as ms, movie_voice, slb, PakArchive,
|
||||
};
|
||||
fn rg(disc: &str, g: u64, n: usize) -> Vec<u8> {
|
||||
let mut segs = vec![];
|
||||
let mut cum = 0u64;
|
||||
for i in 0..5 {
|
||||
let p = format!("{disc}/dat/sound.p{i:02}");
|
||||
if let Ok(m) = fs::metadata(&p) {
|
||||
segs.push((cum, m.len(), p));
|
||||
cum += m.len();
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let (mut need, mut pos) = (n, g);
|
||||
for (base, len, path) in &segs {
|
||||
if need == 0 || pos >= base + len || pos < *base {
|
||||
continue;
|
||||
}
|
||||
let local = pos - base;
|
||||
let take = need.min((len - local) as usize);
|
||||
let mut f = fs::File::open(path).unwrap();
|
||||
f.seek(SeekFrom::Start(local)).unwrap();
|
||||
let mut b = vec![0u8; take];
|
||||
f.read_exact(&mut b).unwrap();
|
||||
out.extend_from_slice(&b);
|
||||
need -= take;
|
||||
pos += take as u64;
|
||||
}
|
||||
out
|
||||
}
|
||||
fn ct(h: &[u8], n: &[u8]) -> bool {
|
||||
h.windows(n.len()).any(|w| w == n)
|
||||
}
|
||||
fn dur(w: &str) -> String {
|
||||
let o = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
w,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&o.stdout).trim().to_string()
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap();
|
||||
let man = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b)))
|
||||
.unwrap();
|
||||
let lpak = PakArchive::open(format!("{disc}/dat/movie/eng.pak")).unwrap();
|
||||
let reg = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| {
|
||||
tpak.read(e)
|
||||
.ok()
|
||||
.filter(|b| ct(b, b"eng\\Movie\\VOICE_ADV.slb"))
|
||||
})
|
||||
.unwrap();
|
||||
let ids = movie_voice::registry_voice_ids(®);
|
||||
let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap();
|
||||
let ents = PakArchive::parse_toc(&stoc).unwrap();
|
||||
// demo->token from bound hokyu
|
||||
let hok:Vec<_>=movie_manifest::parse(&man).into_iter().filter(|m|m.movie.starts_with("hokyu_")).collect();
|
||||
let resolve=|movie:&str|->Option<String>{
|
||||
movie_manifest::voice_token(&man,movie).or_else(||{
|
||||
let want=ms::track_voice_cues(&lpak,movie).first().map(|&(d,_)|d)?;
|
||||
hok.iter().find_map(|e|{let t=e.voice_token.clone().filter(|_|e.movie.starts_with("hokyu_"))?; ms::track_voice_cues(&lpak,&e.movie).iter().any(|&(d,_)|d==want).then_some(t)})
|
||||
let hok: Vec<_> = movie_manifest::parse(&man)
|
||||
.into_iter()
|
||||
.filter(|m| m.movie.starts_with("hokyu_"))
|
||||
.collect();
|
||||
let resolve = |movie: &str| -> Option<String> {
|
||||
movie_manifest::voice_token(&man, movie).or_else(|| {
|
||||
let want = ms::track_voice_cues(&lpak, movie)
|
||||
.first()
|
||||
.map(|&(d, _)| d)?;
|
||||
hok.iter().find_map(|e| {
|
||||
let t = e
|
||||
.voice_token
|
||||
.clone()
|
||||
.filter(|_| e.movie.starts_with("hokyu_"))?;
|
||||
ms::track_voice_cues(&lpak, &e.movie)
|
||||
.iter()
|
||||
.any(|&(d, _)| d == want)
|
||||
.then_some(t)
|
||||
})
|
||||
})
|
||||
};
|
||||
for e in &hok{
|
||||
let mv=&e.movie;
|
||||
let bound=e.voice_token.is_some();
|
||||
let tok=resolve(mv);
|
||||
let demo=ms::track_voice_cues(&lpak,mv).first().map(|&(d,_)|d);
|
||||
let mut d="—".to_string();
|
||||
if let Some(tok)=&tok{
|
||||
if let Some(&id)=ids.get(tok){
|
||||
if let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|dir|{let h=name_hash(&format!("eng\\{dir}\\{tok}.slb"));ents.binary_search_by_key(&h,|x|x.name_hash).ok().map(|i|ents[i].offset as u64)}){
|
||||
let ws=(anchor.saturating_sub(2*1024*1024))&!3; let win=rg(&disc,ws,8*1024*1024);
|
||||
if let Some(el)=movie_voice::find_descriptor(&win,id){let end=ws+el as u64;
|
||||
let start=movie_voice::find_descriptor(&win,id.wrapping_sub(1)).or_else(||movie_voice::find_descriptor_before(&win,el)).map(|o|ws+o as u64).filter(|&s|s<end&&end-s<1_500_000).unwrap_or(anchor);
|
||||
let region=rg(&disc,start,(end-start)as usize); let mut rf=slb::to_xma_riffs(®ion); if rf.is_empty(){rf=slb::to_xma_riff_best(®ion).into_iter().collect();}
|
||||
if let Some(r)=rf.first(){let xp=format!("/tmp/hd_{mv}.xma.wav");let wp=format!("/tmp/hd_{mv}.wav");fs::write(&xp,r).unwrap();let _=Command::new("ffmpeg").args(["-hide_banner","-v","error","-y","-i",&xp,&wp]).status();d=dur(&wp);}
|
||||
for e in &hok {
|
||||
let mv = &e.movie;
|
||||
let bound = e.voice_token.is_some();
|
||||
let tok = resolve(mv);
|
||||
let demo = ms::track_voice_cues(&lpak, mv).first().map(|&(d, _)| d);
|
||||
let mut d = "—".to_string();
|
||||
if let Some(tok) = &tok {
|
||||
if let Some(&id) = ids.get(tok) {
|
||||
if let Some(anchor) = ["Movie", "etc", "Voice"].iter().find_map(|dir| {
|
||||
let h = name_hash(&format!("eng\\{dir}\\{tok}.slb"));
|
||||
ents.binary_search_by_key(&h, |x| x.name_hash)
|
||||
.ok()
|
||||
.map(|i| ents[i].offset as u64)
|
||||
}) {
|
||||
let ws = (anchor.saturating_sub(2 * 1024 * 1024)) & !3;
|
||||
let win = rg(&disc, ws, 8 * 1024 * 1024);
|
||||
if let Some(el) = movie_voice::find_descriptor(&win, id) {
|
||||
let end = ws + el as u64;
|
||||
let start = movie_voice::find_descriptor(&win, id.wrapping_sub(1))
|
||||
.or_else(|| movie_voice::find_descriptor_before(&win, el))
|
||||
.map(|o| ws + o as u64)
|
||||
.filter(|&s| s < end && end - s < 1_500_000)
|
||||
.unwrap_or(anchor);
|
||||
let region = rg(&disc, start, (end - start) as usize);
|
||||
let mut rf = slb::to_xma_riffs(®ion);
|
||||
if rf.is_empty() {
|
||||
rf = slb::to_xma_riff_best(®ion).into_iter().collect();
|
||||
}
|
||||
if let Some(r) = rf.first() {
|
||||
let xp = format!("/tmp/hd_{mv}.xma.wav");
|
||||
let wp = format!("/tmp/hd_{mv}.wav");
|
||||
fs::write(&xp, r).unwrap();
|
||||
let _ = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp])
|
||||
.status();
|
||||
d = dur(&wp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{mv:16} {:8} demo={:?} -> {:11} voice={d}s", if bound{"BOUND"}else{"unbound"}, demo, tok.unwrap_or("(silent)".into()));
|
||||
println!(
|
||||
"{mv:16} {:8} demo={:?} -> {:11} voice={d}s",
|
||||
if bound { "BOUND" } else { "unbound" },
|
||||
demo,
|
||||
tok.unwrap_or("(silent)".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,148 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::process::Command;
|
||||
use sylpheed_formats::{hash::name_hash, movie_manifest, movie_voice, slb, PakArchive};
|
||||
use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command;
|
||||
fn rg(disc:&str,g:u64,n:usize)->Vec<u8>{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out}
|
||||
fn contains(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)}
|
||||
fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()}
|
||||
fn hokyu_fallback(m:&str)->Option<String>{if !m.starts_with("hokyu_"){return None}let ls=m.contains("_LS_");let ds=m.contains("_DS_");let a=m.ends_with('A');let h=m.ends_with('H');Some(match(ls,ds,a,h){(true,_,true,_)=>"VOICE_D_450",(true,_,_,true)=>"VOICE_D_453",(_,true,true,_)=>"VOICE_D_452",(_,true,_,true)=>"VOICE_D_454",_=>return None}.into())}
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap();
|
||||
let manifest=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap();
|
||||
let code="eng";
|
||||
let registry=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|contains(b,format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes()))).unwrap();
|
||||
let ids=movie_voice::registry_voice_ids(®istry);
|
||||
let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap();
|
||||
let entries=PakArchive::parse_toc(&stoc).unwrap();
|
||||
let hokyu:Vec<String>=movie_manifest::parse(&manifest).into_iter().filter(|m|m.movie.starts_with("hokyu_")).map(|m|m.movie).collect();
|
||||
for movie in &hokyu{
|
||||
let bound=movie_manifest::voice_token(&manifest,movie);
|
||||
let token=bound.clone().or_else(||hokyu_fallback(movie));
|
||||
let Some(token)=token else{println!("{movie:16} NO TOKEN"); continue};
|
||||
let src = if bound.is_some(){"bound"}else{"fallback"};
|
||||
let Some(&id)=ids.get(&token) else{println!("{movie:16} {token} not in registry");continue};
|
||||
let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|d|{let h=name_hash(&format!("{code}\\{d}\\{token}.slb"));entries.binary_search_by_key(&h,|e|e.name_hash).ok().map(|i|entries[i].offset as u64)}) else{println!("{movie:16} no anchor");continue};
|
||||
let win_start=(anchor.saturating_sub(2*1024*1024))&!3;
|
||||
let window=rg(&disc,win_start,8*1024*1024);
|
||||
let Some(el)=movie_voice::find_descriptor(&window,id) else{println!("{movie:16} desc not found");continue};
|
||||
let end=win_start+el as u64;
|
||||
let start=movie_voice::find_descriptor(&window,id.wrapping_sub(1)).or_else(||movie_voice::find_descriptor_before(&window,el)).map(|o|win_start+o as u64).filter(|&s|s<end&&end-s<1_500_000).unwrap_or(anchor);
|
||||
let region=rg(&disc,start,(end-start)as usize);
|
||||
let mut riffs=slb::to_xma_riffs(®ion); if riffs.is_empty(){riffs=slb::to_xma_riff_best(®ion).into_iter().collect();}
|
||||
let mut d="?".into();
|
||||
if let Some(r)=riffs.first(){let xp=format!("/tmp/hf_{movie}.xma.wav");let wp=format!("/tmp/hf_{movie}.wav");fs::write(&xp,r).unwrap();let _=Command::new("ffmpeg").args(["-hide_banner","-v","error","-y","-i",&xp,&wp]).status();d=dur(&wp);}
|
||||
let mv=dur(&format!("{disc}/dat/movie/{movie}.wmv"));
|
||||
fn rg(disc: &str, g: u64, n: usize) -> Vec<u8> {
|
||||
let mut segs = vec![];
|
||||
let mut cum = 0u64;
|
||||
for i in 0..5 {
|
||||
let p = format!("{disc}/dat/sound.p{i:02}");
|
||||
if let Ok(m) = fs::metadata(&p) {
|
||||
segs.push((cum, m.len(), p));
|
||||
cum += m.len();
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let (mut need, mut pos) = (n, g);
|
||||
for (base, len, path) in &segs {
|
||||
if need == 0 || pos >= base + len || pos < *base {
|
||||
continue;
|
||||
}
|
||||
let local = pos - base;
|
||||
let take = need.min((len - local) as usize);
|
||||
let mut f = fs::File::open(path).unwrap();
|
||||
f.seek(SeekFrom::Start(local)).unwrap();
|
||||
let mut b = vec![0u8; take];
|
||||
f.read_exact(&mut b).unwrap();
|
||||
out.extend_from_slice(&b);
|
||||
need -= take;
|
||||
pos += take as u64;
|
||||
}
|
||||
out
|
||||
}
|
||||
fn contains(h: &[u8], n: &[u8]) -> bool {
|
||||
h.windows(n.len()).any(|w| w == n)
|
||||
}
|
||||
fn dur(w: &str) -> String {
|
||||
let o = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
w,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&o.stdout).trim().to_string()
|
||||
}
|
||||
fn hokyu_fallback(m: &str) -> Option<String> {
|
||||
if !m.starts_with("hokyu_") {
|
||||
return None;
|
||||
}
|
||||
let ls = m.contains("_LS_");
|
||||
let ds = m.contains("_DS_");
|
||||
let a = m.ends_with('A');
|
||||
let h = m.ends_with('H');
|
||||
Some(
|
||||
match (ls, ds, a, h) {
|
||||
(true, _, true, _) => "VOICE_D_450",
|
||||
(true, _, _, true) => "VOICE_D_453",
|
||||
(_, true, true, _) => "VOICE_D_452",
|
||||
(_, true, _, true) => "VOICE_D_454",
|
||||
_ => return None,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap();
|
||||
let manifest = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b)))
|
||||
.unwrap();
|
||||
let code = "eng";
|
||||
let registry = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| {
|
||||
tpak.read(e)
|
||||
.ok()
|
||||
.filter(|b| contains(b, format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes()))
|
||||
})
|
||||
.unwrap();
|
||||
let ids = movie_voice::registry_voice_ids(®istry);
|
||||
let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap();
|
||||
let entries = PakArchive::parse_toc(&stoc).unwrap();
|
||||
let hokyu: Vec<String> = movie_manifest::parse(&manifest)
|
||||
.into_iter()
|
||||
.filter(|m| m.movie.starts_with("hokyu_"))
|
||||
.map(|m| m.movie)
|
||||
.collect();
|
||||
for movie in &hokyu {
|
||||
let bound = movie_manifest::voice_token(&manifest, movie);
|
||||
let token = bound.clone().or_else(|| hokyu_fallback(movie));
|
||||
let Some(token) = token else {
|
||||
println!("{movie:16} NO TOKEN");
|
||||
continue;
|
||||
};
|
||||
let src = if bound.is_some() { "bound" } else { "fallback" };
|
||||
let Some(&id) = ids.get(&token) else {
|
||||
println!("{movie:16} {token} not in registry");
|
||||
continue;
|
||||
};
|
||||
let Some(anchor) = ["Movie", "etc", "Voice"].iter().find_map(|d| {
|
||||
let h = name_hash(&format!("{code}\\{d}\\{token}.slb"));
|
||||
entries
|
||||
.binary_search_by_key(&h, |e| e.name_hash)
|
||||
.ok()
|
||||
.map(|i| entries[i].offset as u64)
|
||||
}) else {
|
||||
println!("{movie:16} no anchor");
|
||||
continue;
|
||||
};
|
||||
let win_start = (anchor.saturating_sub(2 * 1024 * 1024)) & !3;
|
||||
let window = rg(&disc, win_start, 8 * 1024 * 1024);
|
||||
let Some(el) = movie_voice::find_descriptor(&window, id) else {
|
||||
println!("{movie:16} desc not found");
|
||||
continue;
|
||||
};
|
||||
let end = win_start + el as u64;
|
||||
let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1))
|
||||
.or_else(|| movie_voice::find_descriptor_before(&window, el))
|
||||
.map(|o| win_start + o as u64)
|
||||
.filter(|&s| s < end && end - s < 1_500_000)
|
||||
.unwrap_or(anchor);
|
||||
let region = rg(&disc, start, (end - start) as usize);
|
||||
let mut riffs = slb::to_xma_riffs(®ion);
|
||||
if riffs.is_empty() {
|
||||
riffs = slb::to_xma_riff_best(®ion).into_iter().collect();
|
||||
}
|
||||
let mut d = "?".into();
|
||||
if let Some(r) = riffs.first() {
|
||||
let xp = format!("/tmp/hf_{movie}.xma.wav");
|
||||
let wp = format!("/tmp/hf_{movie}.wav");
|
||||
fs::write(&xp, r).unwrap();
|
||||
let _ = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp])
|
||||
.status();
|
||||
d = dur(&wp);
|
||||
}
|
||||
let mv = dur(&format!("{disc}/dat/movie/{movie}.wmv"));
|
||||
println!("{movie:16} {src:8} {token:12} id={id} voice={d}s movie={mv}s");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ fn is_enum_value(s: &str) -> bool {
|
||||
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 == '_')
|
||||
&& s.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
&& !is_number(s)
|
||||
&& !is_enum_value(s)
|
||||
}
|
||||
@@ -55,15 +57,18 @@ fn main() {
|
||||
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 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)))
|
||||
{
|
||||
if !toks.iter().any(|t| {
|
||||
types
|
||||
.iter()
|
||||
.any(|ty| t == ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ fn main() {
|
||||
.collect();
|
||||
files.sort();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let w = f.file_name().unwrap().to_string_lossy().to_string();
|
||||
for m in Xbg7Model::stage_models(&bytes) {
|
||||
for (k, sm) in m.meshes.iter().enumerate() {
|
||||
|
||||
@@ -36,8 +36,16 @@ fn score(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> (usize, f32, usize)
|
||||
if a >= pos.len() || b >= pos.len() || c >= pos.len() {
|
||||
continue;
|
||||
}
|
||||
let e1 = [pos[b][0] - pos[a][0], pos[b][1] - pos[a][1], pos[b][2] - pos[a][2]];
|
||||
let e2 = [pos[c][0] - pos[a][0], pos[c][1] - pos[a][1], pos[c][2] - pos[a][2]];
|
||||
let e1 = [
|
||||
pos[b][0] - pos[a][0],
|
||||
pos[b][1] - pos[a][1],
|
||||
pos[b][2] - pos[a][2],
|
||||
];
|
||||
let e2 = [
|
||||
pos[c][0] - pos[a][0],
|
||||
pos[c][1] - pos[a][1],
|
||||
pos[c][2] - pos[a][2],
|
||||
];
|
||||
let f = [
|
||||
e1[1] * e2[2] - e1[2] * e2[1],
|
||||
e1[2] * e2[0] - e1[0] * e2[2],
|
||||
@@ -53,7 +61,11 @@ fn score(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> (usize, f32, usize)
|
||||
agree += 1;
|
||||
}
|
||||
}
|
||||
let frac = if counted == 0 { 0.0 } else { agree as f32 / counted as f32 };
|
||||
let frac = if counted == 0 {
|
||||
0.0
|
||||
} else {
|
||||
agree as f32 / counted as f32
|
||||
};
|
||||
(degen, frac.max(1.0 - frac), counted)
|
||||
}
|
||||
|
||||
@@ -82,7 +94,8 @@ fn main() {
|
||||
if n < 6 || sm.normals.is_empty() || vb < n * 2 + 2 {
|
||||
continue;
|
||||
}
|
||||
let read_at = |start: usize| -> Vec<u32> { (0..n).map(|k| be16(&bytes, start + k * 2)).collect() };
|
||||
let read_at =
|
||||
|start: usize| -> Vec<u32> { (0..n).map(|k| be16(&bytes, start + k * 2)).collect() };
|
||||
let l0 = read_at(vb - n * 2);
|
||||
let l2 = read_at(vb - n * 2 - 2);
|
||||
// Sanity: l0 must be what the decoder emitted, or the assumption that we
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
//! <capture.log> <Stage_SNN> [top_n] [--all]
|
||||
//! `--all` lists every capture vcount, not just the `top_n` (default 40) largest.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog};
|
||||
use sylpheed_formats::xiso::open_iso;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
@@ -47,10 +47,15 @@ fn main() {
|
||||
|
||||
// Decode EVERY geometry resource in the stage container, not just one ship's.
|
||||
let bytes = {
|
||||
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
let mut r = open_iso(Path::new(&iso)).await.unwrap();
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap()
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr"))
|
||||
.await
|
||||
.unwrap()
|
||||
})
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
@@ -101,7 +106,11 @@ fn main() {
|
||||
100.0 * matched_draws as f64 / draws.len().max(1) as f64
|
||||
);
|
||||
|
||||
let shown = if all { vcounts.len() } else { top_n.min(vcounts.len()) };
|
||||
let shown = if all {
|
||||
vcounts.len()
|
||||
} else {
|
||||
top_n.min(vcounts.len())
|
||||
};
|
||||
println!("\nlargest capture vcounts (draws / distinct vbufs) → matching resources:");
|
||||
for &v in vcounts.iter().take(shown) {
|
||||
let n = draw_count[&v];
|
||||
@@ -134,9 +143,19 @@ fn main() {
|
||||
.collect();
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let drawn = rows.iter().filter(|r| r.2 > 0).count();
|
||||
println!("\n{id} resources in {stage}.xpr ({drawn}/{} with a drawn vcount):", rows.len());
|
||||
println!(
|
||||
"\n{id} resources in {stage}.xpr ({drawn}/{} with a drawn vcount):",
|
||||
rows.len()
|
||||
);
|
||||
for (name, v, n) in rows {
|
||||
println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "—".into() });
|
||||
println!(
|
||||
" {name:28} vcount {v:6} {}",
|
||||
if n > 0 {
|
||||
format!("DRAWN ×{n}")
|
||||
} else {
|
||||
"—".into()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,12 +164,24 @@ fn main() {
|
||||
// whether the capture ever drew that many vertices.
|
||||
let mut sizes: Vec<(u32, String)> = models
|
||||
.iter()
|
||||
.map(|m| (m.meshes.iter().map(|s| s.positions.len()).sum::<usize>() as u32, m.name.clone()))
|
||||
.map(|m| {
|
||||
(
|
||||
m.meshes.iter().map(|s| s.positions.len()).sum::<usize>() as u32,
|
||||
m.name.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
sizes.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
||||
println!("\nlargest resources in {stage}.xpr → drawn in the capture?");
|
||||
for (v, name) in sizes.iter().take(top_n.min(sizes.len())) {
|
||||
let n = draw_count.get(v).copied().unwrap_or(0);
|
||||
println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "not drawn".into() });
|
||||
println!(
|
||||
" {name:28} vcount {v:6} {}",
|
||||
if n > 0 {
|
||||
format!("DRAWN ×{n}")
|
||||
} else {
|
||||
"not drawn".into()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,125 @@
|
||||
use sylpheed_formats::PakArchive;
|
||||
use std::collections::BTreeMap;
|
||||
fn be32(b:&[u8],o:usize)->u32{ if o+4<=b.len(){u32::from_be_bytes([b[o],b[o+1],b[o+2],b[o+3]])}else{0} }
|
||||
fn magic(b:&[u8])->String{
|
||||
if b.len()<4 {return "(<4B)".into();}
|
||||
let m=&b[0..4];
|
||||
if m.iter().all(|&c|(0x20..0x7f).contains(&c)){ String::from_utf8_lossy(m).into() }
|
||||
else if m==b"\x89PNG"{"PNG".into()} else if m==[0,1,0,0]{"ttf".into()}
|
||||
else { format!("{:02x}{:02x}{:02x}{:02x}",m[0],m[1],m[2],m[3]) }
|
||||
use sylpheed_formats::PakArchive;
|
||||
fn be32(b: &[u8], o: usize) -> u32 {
|
||||
if o + 4 <= b.len() {
|
||||
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
fn magic(b: &[u8]) -> String {
|
||||
if b.len() < 4 {
|
||||
return "(<4B)".into();
|
||||
}
|
||||
let m = &b[0..4];
|
||||
if m.iter().all(|&c| (0x20..0x7f).contains(&c)) {
|
||||
String::from_utf8_lossy(m).into()
|
||||
} else if m == b"\x89PNG" {
|
||||
"PNG".into()
|
||||
} else if m == [0, 1, 0, 0] {
|
||||
"ttf".into()
|
||||
} else {
|
||||
format!("{:02x}{:02x}{:02x}{:02x}", m[0], m[1], m[2], m[3])
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
// Known/understood IDXD schemas (semantic parsers we have)
|
||||
let known_schema:BTreeMap<u32,&str>=[(0x067025b9,"ADVERTISE_MOVIE (movie_manifest)"),(0x13cb84ba,"sound registry / sounds.tbl")].into();
|
||||
let mut fmt_count:BTreeMap<String,(u64,u64)>=BTreeMap::new(); // fmt -> (count, bytes)
|
||||
let mut schema_census:BTreeMap<u32,(u64,u64,String)>=BTreeMap::new(); // schema -> (count,bytes,sample pak)
|
||||
let mut unknown_magics:BTreeMap<String,(u64,Vec<String>)>=BTreeMap::new();
|
||||
let paks:Vec<String>={let mut v=vec![]; for e in std::fs::read_dir(format!("{disc}/dat")).unwrap(){let p=e.unwrap().path(); if p.extension().map(|x|x=="pak").unwrap_or(false){v.push(p.file_stem().unwrap().to_string_lossy().into());}} v.sort(); v};
|
||||
println!("pak entries decompressed formats");
|
||||
for pk in &paks{
|
||||
let Ok(arc)=PakArchive::open(format!("{disc}/dat/{pk}.pak")) else{continue};
|
||||
let mut per:BTreeMap<String,u64>=BTreeMap::new();
|
||||
let mut tot=0u64; let n=arc.entries().len();
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
tot+=b.len() as u64;
|
||||
let mut f=magic(&b);
|
||||
if f=="IDXD"{ let s=be32(&b,8); schema_census.entry(s).or_insert((0,0,pk.clone())).0+=1; schema_census.get_mut(&s).unwrap().1+=b.len() as u64; f=format!("IDXD:{s:08x}"); }
|
||||
else if !["XPR2","IXUD","LSTA","RATC","RIFF","XBG7","OTTO","PNG","ttf","DDS ","T8AD"].contains(&f.as_str()){
|
||||
let ent=unknown_magics.entry(f.clone()).or_insert((0,vec![])); ent.0+=1; if ent.1.len()<3 && !ent.1.contains(pk){ent.1.push(pk.clone());}
|
||||
let known_schema: BTreeMap<u32, &str> = [
|
||||
(0x067025b9, "ADVERTISE_MOVIE (movie_manifest)"),
|
||||
(0x13cb84ba, "sound registry / sounds.tbl"),
|
||||
]
|
||||
.into();
|
||||
let mut fmt_count: BTreeMap<String, (u64, u64)> = BTreeMap::new(); // fmt -> (count, bytes)
|
||||
let mut schema_census: BTreeMap<u32, (u64, u64, String)> = BTreeMap::new(); // schema -> (count,bytes,sample pak)
|
||||
let mut unknown_magics: BTreeMap<String, (u64, Vec<String>)> = BTreeMap::new();
|
||||
let paks: Vec<String> = {
|
||||
let mut v = vec![];
|
||||
for e in std::fs::read_dir(format!("{disc}/dat")).unwrap() {
|
||||
let p = e.unwrap().path();
|
||||
if p.extension().map(|x| x == "pak").unwrap_or(false) {
|
||||
v.push(p.file_stem().unwrap().to_string_lossy().into());
|
||||
}
|
||||
*per.entry(if f.starts_with("IDXD:"){"IDXD".into()}else{f.clone()}).or_default()+=1;
|
||||
let g=fmt_count.entry(if f.starts_with("IDXD:"){"IDXD".into()}else{f}).or_insert((0,0)); g.0+=1; g.1+=b.len() as u64;
|
||||
}
|
||||
let mut fs:Vec<_>=per.into_iter().collect(); fs.sort_by_key(|x|std::cmp::Reverse(x.1));
|
||||
let top:String=fs.iter().take(4).map(|(k,v)|format!("{k}×{v}")).collect::<Vec<_>>().join(" ");
|
||||
println!("{pk:26} {n:>7} {:>10}KB {top}", tot/1024);
|
||||
v.sort();
|
||||
v
|
||||
};
|
||||
println!("pak entries decompressed formats");
|
||||
for pk in &paks {
|
||||
let Ok(arc) = PakArchive::open(format!("{disc}/dat/{pk}.pak")) else {
|
||||
continue;
|
||||
};
|
||||
let mut per: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut tot = 0u64;
|
||||
let n = arc.entries().len();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
tot += b.len() as u64;
|
||||
let mut f = magic(&b);
|
||||
if f == "IDXD" {
|
||||
let s = be32(&b, 8);
|
||||
schema_census.entry(s).or_insert((0, 0, pk.clone())).0 += 1;
|
||||
schema_census.get_mut(&s).unwrap().1 += b.len() as u64;
|
||||
f = format!("IDXD:{s:08x}");
|
||||
} else if ![
|
||||
"XPR2", "IXUD", "LSTA", "RATC", "RIFF", "XBG7", "OTTO", "PNG", "ttf", "DDS ",
|
||||
"T8AD",
|
||||
]
|
||||
.contains(&f.as_str())
|
||||
{
|
||||
let ent = unknown_magics.entry(f.clone()).or_insert((0, vec![]));
|
||||
ent.0 += 1;
|
||||
if ent.1.len() < 3 && !ent.1.contains(pk) {
|
||||
ent.1.push(pk.clone());
|
||||
}
|
||||
}
|
||||
*per.entry(if f.starts_with("IDXD:") {
|
||||
"IDXD".into()
|
||||
} else {
|
||||
f.clone()
|
||||
})
|
||||
.or_default() += 1;
|
||||
let g = fmt_count
|
||||
.entry(if f.starts_with("IDXD:") {
|
||||
"IDXD".into()
|
||||
} else {
|
||||
f
|
||||
})
|
||||
.or_insert((0, 0));
|
||||
g.0 += 1;
|
||||
g.1 += b.len() as u64;
|
||||
}
|
||||
let mut fs: Vec<_> = per.into_iter().collect();
|
||||
fs.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
let top: String = fs
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|(k, v)| format!("{k}×{v}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
println!("{pk:26} {n:>7} {:>10}KB {top}", tot / 1024);
|
||||
}
|
||||
println!("\n=== FORMAT TOTALS (across all paks) ===");
|
||||
let mut fv:Vec<_>=fmt_count.into_iter().collect(); fv.sort_by_key(|x|std::cmp::Reverse(x.1.1));
|
||||
for (f,(c,b)) in &fv{ println!(" {f:12} {c:>6} entries {:>8}KB", b/1024); }
|
||||
println!("\n=== IDXD SCHEMA CENSUS ({} distinct schemas) ===", schema_census.len());
|
||||
let mut sv:Vec<_>=schema_census.into_iter().collect(); sv.sort_by_key(|x|std::cmp::Reverse(x.1.1));
|
||||
for (s,(c,b,pk)) in &sv{
|
||||
let tag=known_schema.get(s).copied().unwrap_or("??? UNDECODED");
|
||||
println!(" {s:08x} {c:>5} ent {:>7}KB e.g. {pk:22} {tag}", b/1024);
|
||||
let mut fv: Vec<_> = fmt_count.into_iter().collect();
|
||||
fv.sort_by_key(|x| std::cmp::Reverse(x.1 .1));
|
||||
for (f, (c, b)) in &fv {
|
||||
println!(" {f:12} {c:>6} entries {:>8}KB", b / 1024);
|
||||
}
|
||||
println!(
|
||||
"\n=== IDXD SCHEMA CENSUS ({} distinct schemas) ===",
|
||||
schema_census.len()
|
||||
);
|
||||
let mut sv: Vec<_> = schema_census.into_iter().collect();
|
||||
sv.sort_by_key(|x| std::cmp::Reverse(x.1 .1));
|
||||
for (s, (c, b, pk)) in &sv {
|
||||
let tag = known_schema.get(s).copied().unwrap_or("??? UNDECODED");
|
||||
println!(
|
||||
" {s:08x} {c:>5} ent {:>7}KB e.g. {pk:22} {tag}",
|
||||
b / 1024
|
||||
);
|
||||
}
|
||||
println!("\n=== UNKNOWN / UNCLASSIFIED MAGICS ===");
|
||||
for (m,(c,pks)) in &unknown_magics{ println!(" {m:12} ×{c:<5} in {pks:?}"); }
|
||||
for (m, (c, pks)) in &unknown_magics {
|
||||
println!(" {m:12} ×{c:<5} in {pks:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ fn main() {
|
||||
let disc = a.next().expect("disc root");
|
||||
let dump = a.next().expect("live dump");
|
||||
let pairs: Vec<(String, String)> = a
|
||||
.filter_map(|s| s.split_once('=').map(|(i, v)| (i.to_string(), v.to_string())))
|
||||
.filter_map(|s| {
|
||||
s.split_once('=')
|
||||
.map(|(i, v)| (i.to_string(), v.to_string()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// live: va -> offset -> f32
|
||||
@@ -28,8 +31,7 @@ fn main() {
|
||||
}
|
||||
let f: Vec<&str> = line.split_whitespace().collect();
|
||||
if f.len() >= 4 && f[1].starts_with('+') {
|
||||
if let (Ok(off), Ok(val)) =
|
||||
(usize::from_str_radix(&f[1][1..], 16), f[3].parse::<f32>())
|
||||
if let (Ok(off), Ok(val)) = (usize::from_str_radix(&f[1][1..], 16), f[3].parse::<f32>())
|
||||
{
|
||||
live.entry(cur.clone()).or_default().insert(off, val);
|
||||
}
|
||||
@@ -42,9 +44,15 @@ fn main() {
|
||||
let mut units = 0usize;
|
||||
for entry in pak.entries() {
|
||||
let Ok(bytes) = pak.read(entry) else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let Some(id) = obj.get_raw("ID") else { continue };
|
||||
let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Some(id) = obj.get_raw("ID") else {
|
||||
continue;
|
||||
};
|
||||
let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(words) = live.get(va) else { continue };
|
||||
units += 1;
|
||||
let mut numeric = 0usize;
|
||||
@@ -58,7 +66,11 @@ fn main() {
|
||||
let mut found = false;
|
||||
for (off, w) in words {
|
||||
if (*w - v).abs() <= v.abs() * 1e-6 {
|
||||
*votes.entry(key.clone()).or_default().entry(*off).or_default() += 1;
|
||||
*votes
|
||||
.entry(key.clone())
|
||||
.or_default()
|
||||
.entry(*off)
|
||||
.or_default() += 1;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
//! fixing, but the geometry is already recoverable, so a capture is not needed.
|
||||
//!
|
||||
//! Usage: miss_targets <resource3d_dir>
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -30,14 +30,18 @@ fn main() {
|
||||
let mut ok: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
let mut miss: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let where_ = f.file_name().unwrap().to_string_lossy().to_string();
|
||||
let declared: BTreeSet<String> = xbg7_resource_names(&bytes).into_iter().collect();
|
||||
if declared.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let decoded: BTreeSet<String> =
|
||||
Xbg7Model::stage_models(&bytes).into_iter().map(|m| m.name).collect();
|
||||
let decoded: BTreeSet<String> = Xbg7Model::stage_models(&bytes)
|
||||
.into_iter()
|
||||
.map(|m| m.name)
|
||||
.collect();
|
||||
for n in &declared {
|
||||
if decoded.contains(n) {
|
||||
ok.entry(n.clone()).or_default().insert(where_.clone());
|
||||
@@ -52,7 +56,10 @@ fn main() {
|
||||
let recoverable: Vec<&String> = miss.keys().filter(|n| ok.contains_key(*n)).collect();
|
||||
|
||||
println!("resources that decode NOWHERE: {}", never.len());
|
||||
println!("resources that miss somewhere but decode elsewhere: {}\n", recoverable.len());
|
||||
println!(
|
||||
"resources that miss somewhere but decode elsewhere: {}\n",
|
||||
recoverable.len()
|
||||
);
|
||||
|
||||
// Rank containers by how many never-decoding resources they hold.
|
||||
let mut per_container: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
|
||||
@@ -73,7 +80,10 @@ fn main() {
|
||||
// reachable through whatever loads that container.
|
||||
let excl: Vec<(&&String, &&BTreeSet<String>)> =
|
||||
never.iter().filter(|(_, w)| w.len() == 1).collect();
|
||||
println!("\nof the never-decoding, {} live in exactly one container", excl.len());
|
||||
println!(
|
||||
"\nof the never-decoding, {} live in exactly one container",
|
||||
excl.len()
|
||||
);
|
||||
let mut by_one: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
|
||||
for (n, w) in &excl {
|
||||
by_one.entry(w.iter().next().unwrap()).or_default().push(n);
|
||||
@@ -81,6 +91,15 @@ fn main() {
|
||||
let mut b: Vec<_> = by_one.iter().collect();
|
||||
b.sort_by_key(|(_, v)| std::cmp::Reverse(v.len()));
|
||||
for (w, v) in b.iter().take(10) {
|
||||
println!(" {:<24} {:>3} {}", w, v.len(), v.iter().take(5).map(|s| s.as_str()).collect::<Vec<_>>().join(", "));
|
||||
println!(
|
||||
" {:<24} {:>3} {}",
|
||||
w,
|
||||
v.len(),
|
||||
v.iter()
|
||||
.take(5)
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,96 @@
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
// 1. All stages: BackGroundID + phases + stage tag (from EnumUnit_SNN)
|
||||
println!("=== STAGES (StageResource 3c9ae32e) ===");
|
||||
let mut rows=vec![];
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
if o.schema_hash!=0x3c9ae32e {continue;}
|
||||
let t=o.tokens();
|
||||
let bg=o.get_raw("BackGroundID").unwrap_or("?").to_string();
|
||||
let stage=t.iter().find_map(|s|s.strip_prefix("EnumUnit_").map(|x|x.trim_end_matches(".tbl").to_string())).unwrap_or("?".into());
|
||||
let phases=t.iter().filter(|s|s.starts_with("Phase_")).count();
|
||||
let unitgrp=t.iter().any(|s|s.starts_with("UnitGroup_"));
|
||||
let route=t.iter().any(|s|s.starts_with("Route_"));
|
||||
let formation=t.iter().any(|s|s.starts_with("FormationSet_"));
|
||||
rows.push((stage,bg,phases,unitgrp,route,formation));
|
||||
let mut rows = vec![];
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
if o.schema_hash != 0x3c9ae32e {
|
||||
continue;
|
||||
}
|
||||
let t = o.tokens();
|
||||
let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string();
|
||||
let stage = t
|
||||
.iter()
|
||||
.find_map(|s| {
|
||||
s.strip_prefix("EnumUnit_")
|
||||
.map(|x| x.trim_end_matches(".tbl").to_string())
|
||||
})
|
||||
.unwrap_or("?".into());
|
||||
let phases = t.iter().filter(|s| s.starts_with("Phase_")).count();
|
||||
let unitgrp = t.iter().any(|s| s.starts_with("UnitGroup_"));
|
||||
let route = t.iter().any(|s| s.starts_with("Route_"));
|
||||
let formation = t.iter().any(|s| s.starts_with("FormationSet_"));
|
||||
rows.push((stage, bg, phases, unitgrp, route, formation));
|
||||
}
|
||||
rows.sort();
|
||||
for (s,bg,p,ug,rt,fm) in &rows{ println!(" {s:8} location={bg:14} phases={p} [squadrons:{} route:{} formations:{}]", if *ug{"Y"}else{"·"},if *rt{"Y"}else{"·"},if *fm{"Y"}else{"·"}); }
|
||||
for (s, bg, p, ug, rt, fm) in &rows {
|
||||
println!(
|
||||
" {s:8} location={bg:14} phases={p} [squadrons:{} route:{} formations:{}]",
|
||||
if *ug { "Y" } else { "·" },
|
||||
if *rt { "Y" } else { "·" },
|
||||
if *fm { "Y" } else { "·" }
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Parse objectives table into (stage,phase) -> key counts
|
||||
println!("\n=== OBJECTIVES (033b5b7e) — per stage/phase key groups ===");
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
if o.schema_hash!=0x033b5b7e {continue;}
|
||||
let t=o.tokens();
|
||||
let mut groups:std::collections::BTreeMap<String,u32>=Default::default();
|
||||
for tok in t{ if let Some(p)=tok.find("_Objective_").or_else(||tok.find("_Lose_")).or_else(||tok.find("_Hint_")){ let key=&tok[..p]; *groups.entry(key.trim_start_matches(|c:char|!c.is_ascii_alphabetic()).into()).or_default()+=1; } }
|
||||
let stages:std::collections::BTreeSet<String>=groups.keys().filter_map(|k|k.get(..3).map(|s|s.to_string())).collect();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
if o.schema_hash != 0x033b5b7e {
|
||||
continue;
|
||||
}
|
||||
let t = o.tokens();
|
||||
let mut groups: std::collections::BTreeMap<String, u32> = Default::default();
|
||||
for tok in t {
|
||||
if let Some(p) = tok
|
||||
.find("_Objective_")
|
||||
.or_else(|| tok.find("_Lose_"))
|
||||
.or_else(|| tok.find("_Hint_"))
|
||||
{
|
||||
let key = &tok[..p];
|
||||
*groups
|
||||
.entry(
|
||||
key.trim_start_matches(|c: char| !c.is_ascii_alphabetic())
|
||||
.into(),
|
||||
)
|
||||
.or_default() += 1;
|
||||
}
|
||||
}
|
||||
let stages: std::collections::BTreeSet<String> = groups
|
||||
.keys()
|
||||
.filter_map(|k| k.get(..3).map(|s| s.to_string()))
|
||||
.collect();
|
||||
println!(" covers {} stages: {:?}", stages.len(), stages);
|
||||
println!(" sample S01_P1 group counts: {:?}", groups.iter().filter(|(k,_)|k.starts_with("S01_P1")).collect::<Vec<_>>());
|
||||
println!(
|
||||
" sample S01_P1 group counts: {:?}",
|
||||
groups
|
||||
.iter()
|
||||
.filter(|(k, _)| k.starts_with("S01_P1"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. Resolve objective TEXT: search all pak entries for the key "S01_P1_Objective_00" as an ID with English text
|
||||
println!("\n=== OBJECTIVE TEXT resolution probe ===");
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
let t=o.tokens();
|
||||
if let Some(i)=t.iter().position(|s|s=="S01_P1_Objective_00"){
|
||||
let lo=i.saturating_sub(1); let hi=(i+3).min(t.len());
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let t = o.tokens();
|
||||
if let Some(i) = t.iter().position(|s| s == "S01_P1_Objective_00") {
|
||||
let lo = i.saturating_sub(1);
|
||||
let hi = (i + 3).min(t.len());
|
||||
println!(" schema {:08x}: ...{:?}...", o.schema_hash, &t[lo..hi]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: name_resolution <pak> [entry]");
|
||||
let path = std::env::args()
|
||||
.nth(1)
|
||||
.expect("usage: name_resolution <pak> [entry]");
|
||||
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
@@ -19,16 +21,24 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let unresolved: Vec<&ui_layout::Element> = build
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.sprite.is_none() && el.kind & 0x10 == 0)
|
||||
.collect();
|
||||
let claimed: std::collections::HashSet<&str> =
|
||||
build.elements.iter().filter_map(|e| e.sprite.as_deref()).collect();
|
||||
let unclaimed: Vec<&String> =
|
||||
build.sprites.keys().filter(|k| !claimed.contains(k.as_str())).collect();
|
||||
let claimed: std::collections::HashSet<&str> = build
|
||||
.elements
|
||||
.iter()
|
||||
.filter_map(|e| e.sprite.as_deref())
|
||||
.collect();
|
||||
let unclaimed: Vec<&String> = build
|
||||
.sprites
|
||||
.keys()
|
||||
.filter(|k| !claimed.contains(k.as_str()))
|
||||
.collect();
|
||||
if want.is_none() && unresolved.is_empty() && unclaimed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -39,12 +49,21 @@ fn main() {
|
||||
.collect();
|
||||
println!(
|
||||
"entry {i:3} {:2} elements UNRESOLVED {:?} UNCLAIMED {:?}",
|
||||
build.elements.len(), un, uc
|
||||
build.elements.len(),
|
||||
un,
|
||||
uc
|
||||
);
|
||||
if want.is_some() {
|
||||
for el in &build.elements {
|
||||
let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 { " <-- UNRESOLVED" } else { "" };
|
||||
println!(" [{:2}] kind {:#06x} {:28} -> {:?}{mark}", el.index, el.kind, el.name, el.sprite);
|
||||
let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 {
|
||||
" <-- UNRESOLVED"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
" [{:2}] kind {:#06x} {:28} -> {:?}{mark}",
|
||||
el.index, el.kind, el.name, el.sprite
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,11 @@ fn main() {
|
||||
j += 1;
|
||||
}
|
||||
let name = std::str::from_utf8(&d[i..j]).unwrap_or("?").to_string();
|
||||
if name.ends_with("_col") || name.ends_with("_spc") || name.ends_with("_gls") || name.ends_with("_lum") {
|
||||
if name.ends_with("_col")
|
||||
|| name.ends_with("_spc")
|
||||
|| name.ends_with("_gls")
|
||||
|| name.ends_with("_lum")
|
||||
{
|
||||
i = j.max(i + 1);
|
||||
continue;
|
||||
}
|
||||
@@ -74,10 +78,18 @@ fn main() {
|
||||
if p == 0 || p + 24 > d.len() {
|
||||
continue;
|
||||
}
|
||||
let a = if p >= 0x48 { be_f64(d, p - 0x48) } else { f64::NAN };
|
||||
let a = if p >= 0x48 {
|
||||
be_f64(d, p - 0x48)
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
let b = be_f64(d, p + 8);
|
||||
let head = if p >= 0x50 { be32(d, p - 0x50) } else { 0 };
|
||||
let t_a = if p >= 0x50 { be_f64(d, p - 0x50) } else { f64::NAN };
|
||||
let t_a = if p >= 0x50 {
|
||||
be_f64(d, p - 0x50)
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
let t_b = be_f64(d, p);
|
||||
println!(
|
||||
" slot{k:2} @{p:#x} head={head:08X}: A={a:12.5} (t={t_a:8.3}) B={b:12.5} (t={t_b:8.3})"
|
||||
|
||||
@@ -12,9 +12,15 @@ fn main() {
|
||||
println!(
|
||||
"{:<26} s[{:7.3}{:7.3}{:7.3}] |m rows|[{:7.3}{:7.3}{:7.3}] t[{:9.1}{:9.1}{:9.1}]",
|
||||
n.resource,
|
||||
n.s[0], n.s[1], n.s[2],
|
||||
norm(n.m[0]), norm(n.m[1]), norm(n.m[2]),
|
||||
n.t[0], n.t[1], n.t[2]
|
||||
n.s[0],
|
||||
n.s[1],
|
||||
n.s[2],
|
||||
norm(n.m[0]),
|
||||
norm(n.m[1]),
|
||||
norm(n.m[2]),
|
||||
n.t[0],
|
||||
n.t[1],
|
||||
n.t[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,16 @@ fn winding(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> f32 {
|
||||
if a == b || b == c || a == c || a.max(b).max(c) >= pos.len() || a >= nrm.len() {
|
||||
continue;
|
||||
}
|
||||
let e1 = [pos[b][0] - pos[a][0], pos[b][1] - pos[a][1], pos[b][2] - pos[a][2]];
|
||||
let e2 = [pos[c][0] - pos[a][0], pos[c][1] - pos[a][1], pos[c][2] - pos[a][2]];
|
||||
let e1 = [
|
||||
pos[b][0] - pos[a][0],
|
||||
pos[b][1] - pos[a][1],
|
||||
pos[b][2] - pos[a][2],
|
||||
];
|
||||
let e2 = [
|
||||
pos[c][0] - pos[a][0],
|
||||
pos[c][1] - pos[a][1],
|
||||
pos[c][2] - pos[a][2],
|
||||
];
|
||||
let f = [
|
||||
e1[1] * e2[2] - e1[2] * e2[1],
|
||||
e1[2] * e2[0] - e1[0] * e2[2],
|
||||
@@ -63,7 +71,9 @@ fn main() {
|
||||
let mut weak_wind: Vec<(f32, f32, String)> = Vec::new();
|
||||
let mut worst: Vec<(usize, String, String)> = Vec::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let where_ = f.file_name().unwrap().to_string_lossy().to_string();
|
||||
let mut c_moved = 0usize;
|
||||
for m in Xbg7Model::stage_models(&bytes) {
|
||||
@@ -102,7 +112,9 @@ fn main() {
|
||||
// noise between two equally clean readings.
|
||||
let w0 = winding(&pad0, &sm.positions, &sm.normals);
|
||||
let w2: f32 = {
|
||||
let l2: Vec<u32> = (0..n).map(|k| be16(&bytes, vb - n * 2 - 2 + k * 2)).collect();
|
||||
let l2: Vec<u32> = (0..n)
|
||||
.map(|k| be16(&bytes, vb - n * 2 - 2 + k * 2))
|
||||
.collect();
|
||||
winding(&l2, &sm.positions, &sm.normals)
|
||||
};
|
||||
weak_wind.push((w0, w2, m.name.clone()));
|
||||
@@ -115,9 +127,17 @@ fn main() {
|
||||
}
|
||||
println!("\n{moved} of {total} single-block resources moved off the pad-0 run");
|
||||
println!("{still_degen} decoded runs still contain a degenerate triangle");
|
||||
println!("of the moved, {weak} had a pad-0 run with no degenerate triangle (moved on winding alone)");
|
||||
let clear = weak_wind.iter().filter(|(w0, w2, _)| *w2 - *w0 > 0.15).count();
|
||||
let close = weak_wind.iter().filter(|(w0, w2, _)| (*w2 - *w0).abs() <= 0.05).count();
|
||||
println!(
|
||||
"of the moved, {weak} had a pad-0 run with no degenerate triangle (moved on winding alone)"
|
||||
);
|
||||
let clear = weak_wind
|
||||
.iter()
|
||||
.filter(|(w0, w2, _)| *w2 - *w0 > 0.15)
|
||||
.count();
|
||||
let close = weak_wind
|
||||
.iter()
|
||||
.filter(|(w0, w2, _)| (*w2 - *w0).abs() <= 0.05)
|
||||
.count();
|
||||
println!(" of those: {clear} show the shift signature (pad-2 winding > pad-0 by >0.15), {close} are within 0.05 (noise)");
|
||||
for (w0, w2, n) in weak_wind.iter().take(8) {
|
||||
println!(" {n}: pad0 wind {w0:.3} -> pad2 {w2:.3}");
|
||||
|
||||
@@ -11,84 +11,151 @@ use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn measured(names: &[&str]) -> Option<(&'static str, Vec<usize>)> {
|
||||
const TITLE: [&str; 24] = [
|
||||
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
|
||||
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
|
||||
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
|
||||
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
|
||||
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
|
||||
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
|
||||
"ptlogo1.t32",
|
||||
"ptlogo2.t32",
|
||||
"ptlogo1.t32",
|
||||
"ptlogo2.t32",
|
||||
"ptlogo1.t32",
|
||||
"ptlogo2.t32",
|
||||
"pteff01.t32",
|
||||
"ptlogo_tm.t32",
|
||||
"pteff00.prm",
|
||||
"ptbase2.t32",
|
||||
"pteff04.t32",
|
||||
"ptloop01.rat",
|
||||
"ptloop02.rat",
|
||||
"pteff02.prm",
|
||||
"ptlogo_back2eff1.t32",
|
||||
"ptlogo_back2eff2.t32",
|
||||
"ptlogo_back2eff3.t32",
|
||||
"ptlogo_back2eff4.t32",
|
||||
"ptlogo_back2eff5.t32",
|
||||
"ptlogo_back2.t32",
|
||||
"ptlogo_back2eff.t32",
|
||||
"ptcopyright.t32",
|
||||
"ptlogoall_eff.t32",
|
||||
"ptlogoall_eff2.t32",
|
||||
];
|
||||
const SPLASH: [&str; 7] = [
|
||||
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
|
||||
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
|
||||
"palogo_eff0.prm",
|
||||
"palogo_gamearts.t32",
|
||||
"palogo_gamearts_eff.t32",
|
||||
"palogo_seta.t32",
|
||||
"palogo_seta_eff.t32",
|
||||
"palogo_anima.t32",
|
||||
"palogo_anima_eff.t32",
|
||||
];
|
||||
const MENU: [&str; 16] = [
|
||||
"pteff00.prm", "ptbase.t32", "pteff05.t32", "ptloop01.rat",
|
||||
"ptloop02.rat", "pteff02.prm", "ptframe1.t32", "ptframe2.t32",
|
||||
"pteff10.t32", "pteff12.t32", "ptbtn01.rat", "ptbtn02.rat",
|
||||
"ptbtn03.rat", "ptbtn04.rat", "ptbtn05.rat", "ptmsg.t32",
|
||||
"pteff00.prm",
|
||||
"ptbase.t32",
|
||||
"pteff05.t32",
|
||||
"ptloop01.rat",
|
||||
"ptloop02.rat",
|
||||
"pteff02.prm",
|
||||
"ptframe1.t32",
|
||||
"ptframe2.t32",
|
||||
"pteff10.t32",
|
||||
"pteff12.t32",
|
||||
"ptbtn01.rat",
|
||||
"ptbtn02.rat",
|
||||
"ptbtn03.rat",
|
||||
"ptbtn04.rat",
|
||||
"ptbtn05.rat",
|
||||
"ptmsg.t32",
|
||||
];
|
||||
if names == TITLE {
|
||||
return Some(("title", vec![9,11,12,10,13,6,20,19,14,15,18,16,17,0,2,4,7,1,3,5,22,23,21,8]));
|
||||
return Some((
|
||||
"title",
|
||||
vec![
|
||||
9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21,
|
||||
8,
|
||||
],
|
||||
));
|
||||
}
|
||||
if names == SPLASH {
|
||||
return Some(("splash", vec![0, 2, 4, 6, 1, 3, 5]));
|
||||
}
|
||||
if names == SPLASH { return Some(("splash", vec![0,2,4,6,1,3,5])); }
|
||||
if names == MENU {
|
||||
return Some(("main menu", vec![1,3,4,2,5,8,9,6,7,15,10,11,12,13,14,0]));
|
||||
return Some((
|
||||
"main menu",
|
||||
vec![1, 3, 4, 2, 5, 8, 9, 6, 7, 15, 10, 11, 12, 13, 14, 0],
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: paint_order_audit <pak>");
|
||||
let path = std::env::args()
|
||||
.nth(1)
|
||||
.expect("usage: paint_order_audit <pak>");
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let mut checked = 0;
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
|
||||
// Every build: how exposed is it to tie-breaking? A tie between
|
||||
// OVERLAPPING elements is where a derived order can go visibly wrong.
|
||||
let keys_all: Vec<u32> = build.elements.iter()
|
||||
let keys_all: Vec<u32> = build
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
||||
.collect();
|
||||
let mut tie_pairs = 0;
|
||||
for a in 0..keys_all.len() {
|
||||
for b in (a + 1)..keys_all.len() {
|
||||
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX { tie_pairs += 1; }
|
||||
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX {
|
||||
tie_pairs += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Of the tied pairs, how many OVERLAP? Only those can paint visibly
|
||||
// differently under an arbitrary tie-break. Rect from the declared
|
||||
// pivot (= half the sprite for a .t32) at the resting placement.
|
||||
let rect = |e: &ui_layout::Element| -> Option<(i32,i32,i32,i32)> {
|
||||
let rect = |e: &ui_layout::Element| -> Option<(i32, i32, i32, i32)> {
|
||||
let kf = e.rest()?;
|
||||
let (w, h) = ((e.pivot_x * 2) as i32, (e.pivot_y * 2) as i32);
|
||||
if w == 0 || h == 0 { return None; }
|
||||
if w == 0 || h == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((kf.x, kf.y, w, h))
|
||||
};
|
||||
let mut tie_overlap = 0;
|
||||
for a in 0..keys_all.len() {
|
||||
for b in (a + 1)..keys_all.len() {
|
||||
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
||||
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
||||
else { continue };
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
||||
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
||||
if ox > 0 && oy > 0 { tie_overlap += 1; }
|
||||
if ox > 0 && oy > 0 {
|
||||
tie_overlap += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((label, want)) = measured(&names) else {
|
||||
println!("entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
|
||||
{tie_overlap} of them OVERLAPPING", build.elements.len());
|
||||
println!(
|
||||
"entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
|
||||
{tie_overlap} of them OVERLAPPING",
|
||||
build.elements.len()
|
||||
);
|
||||
// Name them: these are the only pairs whose order can show.
|
||||
for a in 0..keys_all.len() {
|
||||
for b in (a + 1)..keys_all.len() {
|
||||
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
||||
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
||||
else { continue };
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
||||
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
||||
if ox > 0 && oy > 0 {
|
||||
@@ -101,7 +168,9 @@ fn main() {
|
||||
};
|
||||
checked += 1;
|
||||
let got = ui_layout::derived_paint_order(&build, &bytes);
|
||||
let keys: Vec<u32> = build.elements.iter()
|
||||
let keys: Vec<u32> = build
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
||||
.collect();
|
||||
let exact = got == want;
|
||||
@@ -110,7 +179,9 @@ fn main() {
|
||||
// tie the sort cannot resolve) versus a genuine key-order conflict?
|
||||
let pos_got: Vec<usize> = {
|
||||
let mut p = vec![0; got.len()];
|
||||
for (r, &e) in got.iter().enumerate() { p[e] = r; }
|
||||
for (r, &e) in got.iter().enumerate() {
|
||||
p[e] = r;
|
||||
}
|
||||
p
|
||||
};
|
||||
let (mut inv, mut tied) = (0, 0);
|
||||
@@ -119,12 +190,17 @@ fn main() {
|
||||
let (x, y) = (want[a], want[b]);
|
||||
if pos_got[x] > pos_got[y] {
|
||||
inv += 1;
|
||||
if keys[x] == keys[y] { tied += 1; }
|
||||
if keys[x] == keys[y] {
|
||||
tied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("entry {i:2} {label:10} {} elements", want.len());
|
||||
println!(" derived == measured : {}", if exact { "YES" } else { "NO" });
|
||||
println!(
|
||||
" derived == measured : {}",
|
||||
if exact { "YES" } else { "NO" }
|
||||
);
|
||||
println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})");
|
||||
if !exact {
|
||||
println!(" measured: {want:?}");
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
fn main(){
|
||||
let a:Vec<String>=std::env::args().collect();
|
||||
let bytes=std::fs::read(&a[1]).unwrap();
|
||||
let filter=a.get(2).cloned().unwrap_or_default();
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let filter = a.get(2).cloned().unwrap_or_default();
|
||||
for n in sylpheed_formats::mesh::xbg7_resource_names(&bytes) {
|
||||
if !filter.is_empty() && !n.contains(&filter) { continue }
|
||||
if let Some((m,stride))=sylpheed_formats::mesh::debug_resource_params(&bytes,&n) {
|
||||
if !filter.is_empty() && !n.contains(&filter) {
|
||||
continue;
|
||||
}
|
||||
if let Some((m, stride)) = sylpheed_formats::mesh::debug_resource_params(&bytes, &n) {
|
||||
let strides = sylpheed_formats::mesh::debug_decl_strides(&bytes, &n);
|
||||
println!("{n:<28} decl-stride {stride} markers {:?} per-sub-mesh strides {:?}",
|
||||
&m[..m.len().min(4)], strides);
|
||||
println!(
|
||||
"{n:<28} decl-stride {stride} markers {:?} per-sub-mesh strides {:?}",
|
||||
&m[..m.len().min(4)],
|
||||
strides
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let pak_path = std::env::args().nth(1).expect("usage: <GP_TITLE.pak> <outdir> <entry>...");
|
||||
let pak_path = std::env::args()
|
||||
.nth(1)
|
||||
.expect("usage: <GP_TITLE.pak> <outdir> <entry>...");
|
||||
let ar = pak::PakArchive::open(&pak_path).expect("open");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
let outdir = std::env::args().nth(2).expect("outdir");
|
||||
@@ -23,30 +25,43 @@ fn main() {
|
||||
for spec in std::env::args().skip(3) {
|
||||
let idx: usize = spec.parse().unwrap();
|
||||
let bytes = ar.read(&entries[idx]).expect("read");
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
// plateau-less = rest() had to guess: no two adjacent keyframes share a pose
|
||||
// Default: suppress plateau-less elements. With SUPPRESS_SUBSTR set,
|
||||
// suppress every element whose NAME contains it instead — used to test
|
||||
// the entry→hold→exit model's prediction that the splash glows are all
|
||||
// finished by the moment the logos are up.
|
||||
let by_name = std::env::var("SUPPRESS_SUBSTR").ok();
|
||||
let mask: Vec<bool> = build.elements.iter().map(|e| {
|
||||
if let Some(sub) = &by_name {
|
||||
return !e.name.to_lowercase().contains(sub.as_str());
|
||||
}
|
||||
let k = &e.keyframes;
|
||||
(0..k.len().saturating_sub(1)).any(|i| {
|
||||
k[i].fade == k[i+1].fade && k[i].scale_x == k[i+1].scale_x
|
||||
&& k[i].scale_y == k[i+1].scale_y && k[i].x == k[i+1].x && k[i].y == k[i+1].y
|
||||
let mask: Vec<bool> = build
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| {
|
||||
if let Some(sub) = &by_name {
|
||||
return !e.name.to_lowercase().contains(sub.as_str());
|
||||
}
|
||||
let k = &e.keyframes;
|
||||
(0..k.len().saturating_sub(1)).any(|i| {
|
||||
k[i].fade == k[i + 1].fade
|
||||
&& k[i].scale_x == k[i + 1].scale_x
|
||||
&& k[i].scale_y == k[i + 1].scale_y
|
||||
&& k[i].x == k[i + 1].x
|
||||
&& k[i].y == k[i + 1].y
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
let suppressed = mask.iter().filter(|m| !**m).count();
|
||||
let opts = ui_layout::ComposeOptions::default();
|
||||
let a = ui_layout::compose(&build, &bytes, opts, None);
|
||||
let b = ui_layout::compose(&build, &bytes, opts, Some(&mask));
|
||||
std::fs::write(format!("{outdir}/entry{idx:02}_asis.raw"), &a.rgba).unwrap();
|
||||
std::fs::write(format!("{outdir}/entry{idx:02}_suppressed.raw"), &b.rgba).unwrap();
|
||||
println!("entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}",
|
||||
a.width, a.height, build.elements.len());
|
||||
println!(
|
||||
"entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}",
|
||||
a.width,
|
||||
a.height,
|
||||
build.elements.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@ fn main() {
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
for e in pak.entries() {
|
||||
let Ok(b) = pak.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Some(id) = o.get_raw("ID") else { continue };
|
||||
if !id.contains(&want) { continue; }
|
||||
if !id.contains(&want) {
|
||||
continue;
|
||||
}
|
||||
let t = o.tokens();
|
||||
println!("=== {id} get_f32({key}) = {:?}", o.get_f32(&key));
|
||||
for (i, tok) in t.iter().enumerate() {
|
||||
@@ -20,7 +24,11 @@ fn main() {
|
||||
let lo = i.saturating_sub(6);
|
||||
let hi = (i + 7).min(t.len());
|
||||
for j in lo..hi {
|
||||
println!(" [{j}]{} {:?}", if j == i { " <-- key" } else { " " }, t[j]);
|
||||
println!(
|
||||
" [{j}]{} {:?}",
|
||||
if j == i { " <-- key" } else { " " },
|
||||
t[j]
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
@@ -2,5 +2,8 @@ fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let vb: usize = a[3].parse().unwrap();
|
||||
println!("{:?}", sylpheed_formats::mesh::debug_try_anchor(&bytes, &a[2], vb, 3));
|
||||
println!(
|
||||
"{:?}",
|
||||
sylpheed_formats::mesh::debug_try_anchor(&bytes, &a[2], vb, 3)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ fn main() {
|
||||
|
||||
for (ei, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
// Only the title-family bundles carry ptbtn records.
|
||||
if !kids.iter().any(|c| c.name == "ptbtn01f.rat") {
|
||||
continue;
|
||||
|
||||
@@ -41,15 +41,21 @@ fn main() {
|
||||
let mut agree = 0usize;
|
||||
let mut disagree: Vec<(String, usize, String, String)> = Vec::new();
|
||||
for path in std::env::args().skip(1) {
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else {
|
||||
continue;
|
||||
};
|
||||
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
for c in &kids {
|
||||
children += 1;
|
||||
let Some(o) = opt_name(&bytes, c.offset) else { continue };
|
||||
let Some(o) = opt_name(&bytes, c.offset) else {
|
||||
continue;
|
||||
};
|
||||
with_opt += 1;
|
||||
if o == c.name {
|
||||
agree += 1;
|
||||
|
||||
@@ -70,15 +70,21 @@ fn main() {
|
||||
let mut rows: Vec<(String, usize, usize, String, String, Why)> = Vec::new();
|
||||
let mut first_child_of_bundle = 0usize;
|
||||
for path in std::env::args().skip(1) {
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else {
|
||||
continue;
|
||||
};
|
||||
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (ei, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
for (ci, c) in kids.iter().enumerate() {
|
||||
total += 1;
|
||||
let Some(why) = classify(&bytes, c.offset) else { continue };
|
||||
let Some(why) = classify(&bytes, c.offset) else {
|
||||
continue;
|
||||
};
|
||||
if ci == 0 {
|
||||
first_child_of_bundle += 1;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,136 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::process::Command;
|
||||
use sylpheed_formats::{hash::name_hash, movie_manifest, movie_voice, slb, PakArchive};
|
||||
use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command;
|
||||
fn rg(disc:&str,g:u64,n:usize)->Vec<u8>{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out}
|
||||
fn contains(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)}
|
||||
fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()}
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap();
|
||||
let manifest=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap();
|
||||
let code="eng";
|
||||
let registry=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|contains(b,format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes()))).unwrap();
|
||||
let ids=movie_voice::registry_voice_ids(®istry);
|
||||
let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap();
|
||||
let entries=PakArchive::parse_toc(&stoc).unwrap();
|
||||
for movie in ["ADV","RT01A","RT01B","RT01C_1","S00A","S01A","hokyu_LS_s02A","hokyu_LS_s09A","hokyu_LS_s02H","hokyu_DS_s07H"]{
|
||||
let Some(token)=movie_manifest::voice_token(&manifest,movie) else { println!("{movie:16} no token"); continue };
|
||||
let Some(&id)=ids.get(&token) else { println!("{movie:16} token {token} not in registry"); continue };
|
||||
let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|d|{let h=name_hash(&format!("{code}\\{d}\\{token}.slb"));entries.binary_search_by_key(&h,|e|e.name_hash).ok().map(|i|entries[i].offset as u64)}) else { println!("{movie:16} no anchor for {token}"); continue };
|
||||
let win_start=(anchor.saturating_sub(2*1024*1024))&!3;
|
||||
let window=rg(&disc,win_start,8*1024*1024);
|
||||
let Some(end_local)=movie_voice::find_descriptor(&window,id) else { println!("{movie:16} id {id}: desc NOT FOUND"); continue };
|
||||
let end=win_start+end_local as u64;
|
||||
let start=movie_voice::find_descriptor(&window,id.wrapping_sub(1))
|
||||
.or_else(||movie_voice::find_descriptor_before(&window,end_local))
|
||||
.map(|o|win_start+o as u64)
|
||||
.filter(|&s|s<end && end-s<1_500_000)
|
||||
fn rg(disc: &str, g: u64, n: usize) -> Vec<u8> {
|
||||
let mut segs = vec![];
|
||||
let mut cum = 0u64;
|
||||
for i in 0..5 {
|
||||
let p = format!("{disc}/dat/sound.p{i:02}");
|
||||
if let Ok(m) = fs::metadata(&p) {
|
||||
segs.push((cum, m.len(), p));
|
||||
cum += m.len();
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let (mut need, mut pos) = (n, g);
|
||||
for (base, len, path) in &segs {
|
||||
if need == 0 || pos >= base + len || pos < *base {
|
||||
continue;
|
||||
}
|
||||
let local = pos - base;
|
||||
let take = need.min((len - local) as usize);
|
||||
let mut f = fs::File::open(path).unwrap();
|
||||
f.seek(SeekFrom::Start(local)).unwrap();
|
||||
let mut b = vec![0u8; take];
|
||||
f.read_exact(&mut b).unwrap();
|
||||
out.extend_from_slice(&b);
|
||||
need -= take;
|
||||
pos += take as u64;
|
||||
}
|
||||
out
|
||||
}
|
||||
fn contains(h: &[u8], n: &[u8]) -> bool {
|
||||
h.windows(n.len()).any(|w| w == n)
|
||||
}
|
||||
fn dur(w: &str) -> String {
|
||||
let o = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
w,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&o.stdout).trim().to_string()
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap();
|
||||
let manifest = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b)))
|
||||
.unwrap();
|
||||
let code = "eng";
|
||||
let registry = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| {
|
||||
tpak.read(e)
|
||||
.ok()
|
||||
.filter(|b| contains(b, format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes()))
|
||||
})
|
||||
.unwrap();
|
||||
let ids = movie_voice::registry_voice_ids(®istry);
|
||||
let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap();
|
||||
let entries = PakArchive::parse_toc(&stoc).unwrap();
|
||||
for movie in [
|
||||
"ADV",
|
||||
"RT01A",
|
||||
"RT01B",
|
||||
"RT01C_1",
|
||||
"S00A",
|
||||
"S01A",
|
||||
"hokyu_LS_s02A",
|
||||
"hokyu_LS_s09A",
|
||||
"hokyu_LS_s02H",
|
||||
"hokyu_DS_s07H",
|
||||
] {
|
||||
let Some(token) = movie_manifest::voice_token(&manifest, movie) else {
|
||||
println!("{movie:16} no token");
|
||||
continue;
|
||||
};
|
||||
let Some(&id) = ids.get(&token) else {
|
||||
println!("{movie:16} token {token} not in registry");
|
||||
continue;
|
||||
};
|
||||
let Some(anchor) = ["Movie", "etc", "Voice"].iter().find_map(|d| {
|
||||
let h = name_hash(&format!("{code}\\{d}\\{token}.slb"));
|
||||
entries
|
||||
.binary_search_by_key(&h, |e| e.name_hash)
|
||||
.ok()
|
||||
.map(|i| entries[i].offset as u64)
|
||||
}) else {
|
||||
println!("{movie:16} no anchor for {token}");
|
||||
continue;
|
||||
};
|
||||
let win_start = (anchor.saturating_sub(2 * 1024 * 1024)) & !3;
|
||||
let window = rg(&disc, win_start, 8 * 1024 * 1024);
|
||||
let Some(end_local) = movie_voice::find_descriptor(&window, id) else {
|
||||
println!("{movie:16} id {id}: desc NOT FOUND");
|
||||
continue;
|
||||
};
|
||||
let end = win_start + end_local as u64;
|
||||
let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1))
|
||||
.or_else(|| movie_voice::find_descriptor_before(&window, end_local))
|
||||
.map(|o| win_start + o as u64)
|
||||
.filter(|&s| s < end && end - s < 1_500_000)
|
||||
.unwrap_or(anchor);
|
||||
let region=rg(&disc,start,(end-start) as usize);
|
||||
let mut riffs=slb::to_xma_riffs(®ion);
|
||||
if riffs.is_empty(){ riffs=slb::to_xma_riff_best(®ion).into_iter().collect(); }
|
||||
let mut d="?".into();
|
||||
if let Some(r)=riffs.first(){ let xp=format!("/tmp/ra2_{movie}.xma.wav");let wp=format!("/tmp/ra2_{movie}.wav");fs::write(&xp,r).unwrap();let _=Command::new("ffmpeg").args(["-hide_banner","-v","error","-y","-i",&xp,&wp]).status();d=dur(&wp);}
|
||||
let mv=dur(&format!("{disc}/dat/movie/{movie}.wmv"));
|
||||
println!("{movie:16} id={id:5} region={}KB riffs={} voice={d}s movie={mv}s", (end-start)/1024, riffs.len());
|
||||
let region = rg(&disc, start, (end - start) as usize);
|
||||
let mut riffs = slb::to_xma_riffs(®ion);
|
||||
if riffs.is_empty() {
|
||||
riffs = slb::to_xma_riff_best(®ion).into_iter().collect();
|
||||
}
|
||||
let mut d = "?".into();
|
||||
if let Some(r) = riffs.first() {
|
||||
let xp = format!("/tmp/ra2_{movie}.xma.wav");
|
||||
let wp = format!("/tmp/ra2_{movie}.wav");
|
||||
fs::write(&xp, r).unwrap();
|
||||
let _ = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp])
|
||||
.status();
|
||||
d = dur(&wp);
|
||||
}
|
||||
let mv = dur(&format!("{disc}/dat/movie/{movie}.wmv"));
|
||||
println!(
|
||||
"{movie:16} id={id:5} region={}KB riffs={} voice={d}s movie={mv}s",
|
||||
(end - start) / 1024,
|
||||
riffs.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
//! harvested CSV.
|
||||
//!
|
||||
//! Usage: roster_target <disc-root> <unit-runtime-fields.csv>
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use sylpheed_formats::hash::name_hash;
|
||||
use sylpheed_formats::idxd::IdxdObject;
|
||||
use sylpheed_formats::pak::PakArchive;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
@@ -31,15 +31,22 @@ fn main() {
|
||||
let stage = format!("S{i:02}");
|
||||
for pre in ["", "unit\\", "battle\\", "stage\\", "enemy\\"] {
|
||||
for suf in [".tbl", ""] {
|
||||
by_hash.insert(name_hash(&format!("{pre}EnumUnit_{stage}{suf}")), stage.clone());
|
||||
by_hash.insert(
|
||||
name_hash(&format!("{pre}EnumUnit_{stage}{suf}")),
|
||||
stage.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rows: Vec<(usize, String, Vec<String>)> = Vec::new();
|
||||
for e in pak.entries() {
|
||||
let Some(stage) = by_hash.get(&e.name_hash).cloned() else { continue };
|
||||
let Some(stage) = by_hash.get(&e.name_hash).cloned() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(b) = pak.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let mut units: Vec<String> = Vec::new();
|
||||
for id in o.tokens().iter().filter(|s| s.starts_with("UN_")) {
|
||||
let prop = id.contains("Asteroid") || id.contains("cmesh") || id.contains("_Box");
|
||||
@@ -47,7 +54,11 @@ fn main() {
|
||||
units.push(id.clone());
|
||||
}
|
||||
}
|
||||
let missing: Vec<String> = units.iter().filter(|u| !have.contains(*u)).cloned().collect();
|
||||
let missing: Vec<String> = units
|
||||
.iter()
|
||||
.filter(|u| !have.contains(*u))
|
||||
.cloned()
|
||||
.collect();
|
||||
rows.push((missing.len(), stage, missing));
|
||||
}
|
||||
rows.sort_by_key(|(n, _, _)| std::cmp::Reverse(*n));
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
//! RE probe: dump tables.pak screen-config records that mention a given token.
|
||||
//! Usage: screen_configs <disc-root> <substring>
|
||||
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
|
||||
fn main(){
|
||||
let a:Vec<String>=std::env::args().collect();
|
||||
let needle=a.get(2).cloned().unwrap_or_else(||"CHALLENGE".into());
|
||||
let arc=PakArchive::open(format!("{}/dat/tables.pak",a[1])).unwrap();
|
||||
for (i,e) in arc.entries().iter().enumerate(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
let t=o.tokens();
|
||||
if !t.iter().any(|s|s.to_lowercase().contains(&needle.to_lowercase())){continue}
|
||||
let path=t.iter().find(|s|s.contains(".pak+")).cloned().unwrap_or_default();
|
||||
if !path.is_empty() && !path.contains("+eng"){continue}
|
||||
println!("\n=== entry #{i} schema {:08x} [{path}] {} tokens ===",o.schema_hash,t.len());
|
||||
for (j,tok) in t.iter().enumerate(){println!(" {j:3} {tok}");}
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let needle = a.get(2).cloned().unwrap_or_else(|| "CHALLENGE".into());
|
||||
let arc = PakArchive::open(format!("{}/dat/tables.pak", a[1])).unwrap();
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let t = o.tokens();
|
||||
if !t
|
||||
.iter()
|
||||
.any(|s| s.to_lowercase().contains(&needle.to_lowercase()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let path = t
|
||||
.iter()
|
||||
.find(|s| s.contains(".pak+"))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !path.is_empty() && !path.contains("+eng") {
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
"\n=== entry #{i} schema {:08x} [{path}] {} tokens ===",
|
||||
o.schema_hash,
|
||||
t.len()
|
||||
);
|
||||
for (j, tok) in t.iter().enumerate() {
|
||||
println!(" {j:3} {tok}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ fn main() {
|
||||
let h = u32::from_str_radix(h.trim_start_matches("0x"), 16).expect("hash");
|
||||
arc.read_by_hash(h).expect("entry").expect("decompress")
|
||||
}
|
||||
Some(name) => arc.read_by_name(&name).expect("entry present").expect("decompress"),
|
||||
Some(name) => arc
|
||||
.read_by_name(&name)
|
||||
.expect("entry present")
|
||||
.expect("decompress"),
|
||||
None => arc
|
||||
.entries()
|
||||
.iter()
|
||||
@@ -57,23 +60,36 @@ fn main() {
|
||||
// X/Y are SIGNED: off-screen animation starts are negative (e.g. -516).
|
||||
let mut placements: Vec<Vec<(i32, i32, u32)>> = vec![Vec::new(); count];
|
||||
for _ in 0..count {
|
||||
if pos + 8 > bytes.len() { break }
|
||||
if pos + 8 > bytes.len() {
|
||||
break;
|
||||
}
|
||||
let idx = be32(&bytes, pos) as usize;
|
||||
let frames = be32(&bytes, pos + 4) as usize;
|
||||
if idx >= count || frames == 0 || frames > 4096 { break }
|
||||
if idx >= count || frames == 0 || frames > 4096 {
|
||||
break;
|
||||
}
|
||||
let first = pos + 28; // header + lead-in, verified on the pause bundles
|
||||
let mut group = Vec::with_capacity(frames);
|
||||
for k in 0..frames {
|
||||
let blk = first + k * 40;
|
||||
if blk + 20 > bytes.len() { break }
|
||||
group.push((be32(&bytes, blk + 12) as i32, be32(&bytes, blk + 16) as i32, be32(&bytes, blk + 20)));
|
||||
if blk + 20 > bytes.len() {
|
||||
break;
|
||||
}
|
||||
group.push((
|
||||
be32(&bytes, blk + 12) as i32,
|
||||
be32(&bytes, blk + 16) as i32,
|
||||
be32(&bytes, blk + 20),
|
||||
));
|
||||
}
|
||||
placements[idx] = group;
|
||||
pos = first + frames * 40 - 20;
|
||||
}
|
||||
|
||||
println!("{} elements", count);
|
||||
println!("{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} placement", "#", "element", "parent", "kind", "pivot", "kf");
|
||||
println!(
|
||||
"{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} placement",
|
||||
"#", "element", "parent", "kind", "pivot", "kf"
|
||||
);
|
||||
for i in 0..count {
|
||||
let (parent, kind, px, py) = meta[i];
|
||||
let p = &placements[i];
|
||||
@@ -88,20 +104,32 @@ fn main() {
|
||||
let mut best = (0usize, 0u32);
|
||||
for k in 0..p.len() - 1 {
|
||||
let d = p[k + 1].2.saturating_sub(p[k].2);
|
||||
if d > best.1 { best = (k, d) }
|
||||
if d > best.1 {
|
||||
best = (k, d)
|
||||
}
|
||||
}
|
||||
let r = p[best.0];
|
||||
format!(
|
||||
"rest ({},{}) t={}..{} [{}]",
|
||||
r.0, r.1, r.2, p[best.0 + 1].2,
|
||||
p.iter().map(|(x, y, t)| format!("{t}:{x},{y}")).collect::<Vec<_>>().join(" ")
|
||||
r.0,
|
||||
r.1,
|
||||
r.2,
|
||||
p[best.0 + 1].2,
|
||||
p.iter()
|
||||
.map(|(x, y, t)| format!("{t}:{x},{y}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} {}",
|
||||
i,
|
||||
names[i],
|
||||
if parent == u32::MAX { "-".into() } else { parent.to_string() },
|
||||
if parent == u32::MAX {
|
||||
"-".into()
|
||||
} else {
|
||||
parent.to_string()
|
||||
},
|
||||
format!("{kind:#x}"),
|
||||
format!("({px},{py})"),
|
||||
p.len(),
|
||||
|
||||
@@ -9,7 +9,11 @@ fn main() {
|
||||
let out = std::env::args().nth(1).unwrap_or_else(|| "/tmp".into());
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(&disc);
|
||||
for (name, off, pkts) in [("move", 0x1ec0usize, 4usize), ("back", 0x0ec0, 2), ("confirm", 0x5d6c0, 6)] {
|
||||
for (name, off, pkts) in [
|
||||
("move", 0x1ec0usize, 4usize),
|
||||
("back", 0x0ec0, 2),
|
||||
("confirm", 0x5d6c0, 6),
|
||||
] {
|
||||
match media::se_wave_riff(&src, "Static.slb", off, pkts, 1, 48000) {
|
||||
Ok(riff) => {
|
||||
let p = format!("{out}/{name}.riff");
|
||||
|
||||
@@ -17,13 +17,38 @@ fn main() {
|
||||
|
||||
// bank → the movies bound to it, from the record table (see movie-subtitle-link).
|
||||
let banks: BTreeMap<&str, Vec<&str>> = BTreeMap::from([
|
||||
("VOICE_D_450", vec!["hokyu_LS_s02A", "hokyu_LS_s03A", "hokyu_LS_s06A"]),
|
||||
(
|
||||
"VOICE_D_450",
|
||||
vec!["hokyu_LS_s02A", "hokyu_LS_s03A", "hokyu_LS_s06A"],
|
||||
),
|
||||
(
|
||||
"VOICE_D_451",
|
||||
vec!["hokyu_LS_s09A", "hokyu_LS_s11A", "hokyu_LS_s15A", "hokyu_LS_s24A", "hokyu_LS_s27A"],
|
||||
vec![
|
||||
"hokyu_LS_s09A",
|
||||
"hokyu_LS_s11A",
|
||||
"hokyu_LS_s15A",
|
||||
"hokyu_LS_s24A",
|
||||
"hokyu_LS_s27A",
|
||||
],
|
||||
),
|
||||
(
|
||||
"VOICE_D_452",
|
||||
vec![
|
||||
"hokyu_DS_s02A",
|
||||
"hokyu_DS_s07A",
|
||||
"hokyu_DS_s08A",
|
||||
"hokyu_DS_s13A",
|
||||
],
|
||||
),
|
||||
(
|
||||
"VOICE_D_453",
|
||||
vec![
|
||||
"hokyu_LS_s02H",
|
||||
"hokyu_LS_s03H",
|
||||
"hokyu_LS_s06H",
|
||||
"hokyu_LS_s09H",
|
||||
],
|
||||
),
|
||||
("VOICE_D_452", vec!["hokyu_DS_s02A", "hokyu_DS_s07A", "hokyu_DS_s08A", "hokyu_DS_s13A"]),
|
||||
("VOICE_D_453", vec!["hokyu_LS_s02H", "hokyu_LS_s03H", "hokyu_LS_s06H", "hokyu_LS_s09H"]),
|
||||
("VOICE_D_454", vec!["hokyu_DS_s07H", "hokyu_DS_s14H"]),
|
||||
]);
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
//! Usage:
|
||||
//! cargo run --release --example shared_vbase_check -- \
|
||||
//! <Stage_SNN.xpr> <capture.log>...
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Quantised position key — the logs print 4 decimals, so compare at that scale.
|
||||
fn key(p: [f32; 3]) -> (i64, i64, i64) {
|
||||
@@ -101,7 +101,10 @@ fn main() {
|
||||
}
|
||||
let mut top: Vec<_> = delta.iter().collect();
|
||||
top.sort_by_key(|(_, n)| std::cmp::Reverse(**n));
|
||||
println!("{log}: {} distinct vbases located, {unfound} not in this container", seen.len() - unfound);
|
||||
println!(
|
||||
"{log}: {} distinct vbases located, {unfound} not in this container",
|
||||
seen.len() - unfound
|
||||
);
|
||||
for (d, n) in top.iter().take(5) {
|
||||
println!(" vbase - offset = 0x{:X} ×{n}", d);
|
||||
}
|
||||
@@ -125,7 +128,9 @@ fn main() {
|
||||
for m in &models {
|
||||
for sub in &m.meshes {
|
||||
if let Some(o) = sub.vbuf_offset {
|
||||
ours.entry(o).or_default().push((m.name.clone(), sub.positions.len()));
|
||||
ours.entry(o)
|
||||
.or_default()
|
||||
.push((m.name.clone(), sub.positions.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,15 +161,25 @@ fn main() {
|
||||
}
|
||||
// Is a capture-proven offset even a candidate the scan considers?
|
||||
// Absent ⇒ the run scan misses it; present ⇒ selection picked another.
|
||||
let starts: BTreeSet<usize> =
|
||||
sylpheed_formats::mesh::debug_vertex_run_starts(&bytes, 24).into_iter().collect();
|
||||
eprintln!("{} stride-24 candidate starts in this container", starts.len());
|
||||
println!("{:<12} {:>7} {:>9} {:<44} our resources with that vcount", "file offset", "vcount", "candidate", "claimed by our decode");
|
||||
let starts: BTreeSet<usize> = sylpheed_formats::mesh::debug_vertex_run_starts(&bytes, 24)
|
||||
.into_iter()
|
||||
.collect();
|
||||
eprintln!(
|
||||
"{} stride-24 candidate starts in this container",
|
||||
starts.len()
|
||||
);
|
||||
println!(
|
||||
"{:<12} {:>7} {:>9} {:<44} our resources with that vcount",
|
||||
"file offset", "vcount", "candidate", "claimed by our decode"
|
||||
);
|
||||
for (off, vcount) in &drawn {
|
||||
let who = ours
|
||||
.get(off)
|
||||
.map(|v| {
|
||||
v.iter().map(|(n, c)| format!("{n}({c})")).collect::<Vec<_>>().join(", ")
|
||||
v.iter()
|
||||
.map(|(n, c)| format!("{n}({c})"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
})
|
||||
.unwrap_or_else(|| "— NOBODY".into());
|
||||
let same = by_count
|
||||
@@ -185,8 +200,12 @@ fn main() {
|
||||
}
|
||||
let mut groups: BTreeMap<Vec<(i64, i64, i64)>, Vec<String>> = BTreeMap::new();
|
||||
for m in &models {
|
||||
let pos: Vec<(i64, i64, i64)> =
|
||||
m.meshes.iter().flat_map(|s| s.positions.iter().copied()).map(key).collect();
|
||||
let pos: Vec<(i64, i64, i64)> = m
|
||||
.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.map(key)
|
||||
.collect();
|
||||
if pos.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -205,7 +224,11 @@ fn main() {
|
||||
// A draw belongs to this geometry if every dumped position is one of
|
||||
// the decoded ones (the log dumps at most the first 64).
|
||||
let want: BTreeSet<(i64, i64, i64)> = pos.iter().copied().collect();
|
||||
println!("\n{} ({vcount} verts, {} resources)", names.join(" ≡ "), names.len());
|
||||
println!(
|
||||
"\n{} ({vcount} verts, {} resources)",
|
||||
names.join(" ≡ "),
|
||||
names.len()
|
||||
);
|
||||
for (log, draws) in &logs {
|
||||
let hits: Vec<&CapturedDraw> = draws.iter().filter(|d| d.vcount == vcount).collect();
|
||||
let all: BTreeSet<u32> = hits.iter().map(|d| d.vbase).collect();
|
||||
@@ -222,7 +245,9 @@ fn main() {
|
||||
.filter(|d| !ok.contains(&d.vbase))
|
||||
.filter(|d| {
|
||||
!d.pos.is_empty()
|
||||
&& d.pos.iter().all(|p| want.contains(&key([-p[0], p[1], p[2]])))
|
||||
&& d.pos
|
||||
.iter()
|
||||
.all(|p| want.contains(&key([-p[0], p[1], p[2]])))
|
||||
})
|
||||
.map(|d| d.vbase)
|
||||
.collect();
|
||||
@@ -249,12 +274,19 @@ fn main() {
|
||||
"other"
|
||||
};
|
||||
let at = locate_run(&bytes, &d.pos);
|
||||
let shown: Vec<String> =
|
||||
at.iter().take(4).map(|(o, s)| format!("0x{o:x}/stride{s}")).collect();
|
||||
let shown: Vec<String> = at
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|(o, s)| format!("0x{o:x}/stride{s}"))
|
||||
.collect();
|
||||
println!(
|
||||
" vbase=0x{:08X} [{kind:6}] in container at: {}",
|
||||
d.vbase,
|
||||
if shown.is_empty() { "NOT FOUND".into() } else { shown.join(" ") }
|
||||
if shown.is_empty() {
|
||||
"NOT FOUND".into()
|
||||
} else {
|
||||
shown.join(" ")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ fn be32(d: &[u8], o: usize) -> u32 {
|
||||
|
||||
/// Count joint-track records whose key count != 1 in a composite's descriptor.
|
||||
fn multikey_tracks(bytes: &[u8], comp: &str) -> usize {
|
||||
let Some((desc, desc_end)) = xbg7_descriptor_range(bytes, comp) else { return 0 };
|
||||
let Some((desc, desc_end)) = xbg7_descriptor_range(bytes, comp) else {
|
||||
return 0;
|
||||
};
|
||||
let d = &bytes[desc..desc_end];
|
||||
let mut n = 0usize;
|
||||
let mut i = 0usize;
|
||||
@@ -61,7 +63,11 @@ fn main() {
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| {
|
||||
let n = p.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
let n = p
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
n.starts_with("Stage_") && n.ends_with(".xpr")
|
||||
})
|
||||
.collect();
|
||||
@@ -73,7 +79,10 @@ fn main() {
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
|
||||
// Multi-key animation tracks per composite (breaks the single-key read).
|
||||
for n in names.iter().filter(|n| n.contains("rou_") && !n.contains("break")) {
|
||||
for n in names
|
||||
.iter()
|
||||
.filter(|n| n.contains("rou_") && !n.contains("break"))
|
||||
{
|
||||
let mk = multikey_tracks(&bytes, n);
|
||||
if mk > 0 {
|
||||
println!("MULTIKEY {stage} {n}: {mk} tracks");
|
||||
@@ -93,7 +102,9 @@ fn main() {
|
||||
// Per-placement world centroid.
|
||||
let mut cents: Vec<(String, [f32; 3])> = Vec::new();
|
||||
for p in &placed {
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue };
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else {
|
||||
continue;
|
||||
};
|
||||
let (mut c, mut n) = ([0.0f32; 3], 0f32);
|
||||
for sub in &m.meshes {
|
||||
for v in &sub.positions {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Dump a ship's static placement + scene-graph nodes for a stage container FILE
|
||||
//! (works offline from an extracted disc, no ISO needed).
|
||||
//! cargo run --release --example ship_dump -- <Stage_SNN.xpr path> <ship_id>
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::{scene_world_nodes, xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship::{assemble_ship, is_base_part, ship_id_of};
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
@@ -12,7 +12,11 @@ fn main() {
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
|
||||
// Base parts + their vertex counts.
|
||||
let want: HashSet<String> = names.iter().filter(|n| is_base_part(n) && ship_id_of(n)==Some(id.as_str())).cloned().collect();
|
||||
let want: HashSet<String> = names
|
||||
.iter()
|
||||
.filter(|n| is_base_part(n) && ship_id_of(n) == Some(id.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
println!("== base parts ({}) ==", want.len());
|
||||
for m in &models {
|
||||
@@ -32,19 +36,50 @@ fn main() {
|
||||
// assemble_ship result: centroids + extent.
|
||||
for ext in [false, true] {
|
||||
let placed = assemble_ship(&bytes, id, ext);
|
||||
println!("== assemble_ship(external={ext}) -> {} parts ==", placed.len());
|
||||
let mut lo=[f32::MAX;3]; let mut hi=[f32::MIN;3];
|
||||
println!(
|
||||
"== assemble_ship(external={ext}) -> {} parts ==",
|
||||
placed.len()
|
||||
);
|
||||
let mut lo = [f32::MAX; 3];
|
||||
let mut hi = [f32::MIN; 3];
|
||||
for p in &placed {
|
||||
let src = models.iter().find(|m| m.name==p.resource);
|
||||
let (mut c, mut n) = ([0.0f32;3], 0f32);
|
||||
if let Some(src)=src { for sub in &src.meshes { for v in &sub.positions {
|
||||
let w=p.apply(*v); for k in 0..3 { c[k]+=w[k]; lo[k]=lo[k].min(w[k]); hi[k]=hi[k].max(w[k]); } n+=1.0; }}}
|
||||
let n=n.max(1.0);
|
||||
println!(" {:20} T=[{:8.1}{:8.1}{:8.1}] centroid=[{:8.1}{:8.1}{:8.1}]",
|
||||
p.resource, p.t[0],p.t[1],p.t[2], c[0]/n,c[1]/n,c[2]/n);
|
||||
let src = models.iter().find(|m| m.name == p.resource);
|
||||
let (mut c, mut n) = ([0.0f32; 3], 0f32);
|
||||
if let Some(src) = src {
|
||||
for sub in &src.meshes {
|
||||
for v in &sub.positions {
|
||||
let w = p.apply(*v);
|
||||
for k in 0..3 {
|
||||
c[k] += w[k];
|
||||
lo[k] = lo[k].min(w[k]);
|
||||
hi[k] = hi[k].max(w[k]);
|
||||
}
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = n.max(1.0);
|
||||
println!(
|
||||
" {:20} T=[{:8.1}{:8.1}{:8.1}] centroid=[{:8.1}{:8.1}{:8.1}]",
|
||||
p.resource,
|
||||
p.t[0],
|
||||
p.t[1],
|
||||
p.t[2],
|
||||
c[0] / n,
|
||||
c[1] / n,
|
||||
c[2] / n
|
||||
);
|
||||
}
|
||||
if placed.iter().any(|p| models.iter().any(|m| m.name==p.resource)) {
|
||||
println!(" EXTENT=[{:.0} {:.0} {:.0}]", hi[0]-lo[0], hi[1]-lo[1], hi[2]-lo[2]);
|
||||
if placed
|
||||
.iter()
|
||||
.any(|p| models.iter().any(|m| m.name == p.resource))
|
||||
{
|
||||
println!(
|
||||
" EXTENT=[{:.0} {:.0} {:.0}]",
|
||||
hi[0] - lo[0],
|
||||
hi[1] - lo[1],
|
||||
hi[2] - lo[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ fn main() {
|
||||
// World triangles + per-part bounds.
|
||||
let mut tris: Vec<[[f32; 3]; 3]> = Vec::new();
|
||||
for p in &placed {
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue };
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else {
|
||||
continue;
|
||||
};
|
||||
let mut lo = [f32::MAX; 3];
|
||||
let mut hi = [f32::MIN; 3];
|
||||
for sub in &m.meshes {
|
||||
@@ -59,12 +61,18 @@ fn main() {
|
||||
for s in 0..SEG {
|
||||
tris.push([apex, ring[(s + 1) % SEG], ring[s]]);
|
||||
}
|
||||
println!(" exhaust frame {:16} T=[{:8.1}{:8.1}{:8.1}]", f.resource, f.t[0], f.t[1], f.t[2]);
|
||||
println!(
|
||||
" exhaust frame {:16} T=[{:8.1}{:8.1}{:8.1}]",
|
||||
f.resource, f.t[0], f.t[1], f.t[2]
|
||||
);
|
||||
}
|
||||
println!("total {} tris from {} placements", tris.len(), placed.len());
|
||||
|
||||
// Orthographic z-buffered flat renders: top (X/Z, look down −Y) and side (Z/Y, look down −X).
|
||||
for (name, ax, ay, az) in [("top", 0usize, 2usize, 1usize), ("side", 2usize, 1usize, 0usize)] {
|
||||
for (name, ax, ay, az) in [
|
||||
("top", 0usize, 2usize, 1usize),
|
||||
("side", 2usize, 1usize, 0usize),
|
||||
] {
|
||||
let size = 900usize;
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for t in &tris {
|
||||
@@ -105,18 +113,29 @@ fn main() {
|
||||
let lum = (n[az].abs() / nl * 200.0 + 40.0) as u8;
|
||||
// bbox raster
|
||||
let minx = p.iter().map(|v| v[0]).fold(f32::MAX, f32::min).max(0.0) as usize;
|
||||
let maxx = (p.iter().map(|v| v[0]).fold(f32::MIN, f32::max).min(size as f32 - 1.0)) as usize;
|
||||
let maxx = (p
|
||||
.iter()
|
||||
.map(|v| v[0])
|
||||
.fold(f32::MIN, f32::max)
|
||||
.min(size as f32 - 1.0)) as usize;
|
||||
let miny = p.iter().map(|v| v[1]).fold(f32::MAX, f32::min).max(0.0) as usize;
|
||||
let maxy = (p.iter().map(|v| v[1]).fold(f32::MIN, f32::max).min(size as f32 - 1.0)) as usize;
|
||||
let det = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]);
|
||||
let maxy = (p
|
||||
.iter()
|
||||
.map(|v| v[1])
|
||||
.fold(f32::MIN, f32::max)
|
||||
.min(size as f32 - 1.0)) as usize;
|
||||
let det = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1])
|
||||
- (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]);
|
||||
if det.abs() < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
for y in miny..=maxy {
|
||||
for x in minx..=maxx {
|
||||
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||||
let w0 = ((p[1][0] - fx) * (p[2][1] - fy) - (p[2][0] - fx) * (p[1][1] - fy)) / det;
|
||||
let w1 = ((p[2][0] - fx) * (p[0][1] - fy) - (p[0][0] - fx) * (p[2][1] - fy)) / det;
|
||||
let w0 =
|
||||
((p[1][0] - fx) * (p[2][1] - fy) - (p[2][0] - fx) * (p[1][1] - fy)) / det;
|
||||
let w1 =
|
||||
((p[2][0] - fx) * (p[0][1] - fy) - (p[0][0] - fx) * (p[2][1] - fy)) / det;
|
||||
let w2 = 1.0 - w0 - w1;
|
||||
if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
|
||||
continue;
|
||||
|
||||
@@ -22,8 +22,12 @@ fn main() {
|
||||
if !IdxdObject::is_idxd(&bytes) {
|
||||
continue;
|
||||
}
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let Some(generic) = obj.record("Generic") else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Some(generic) = obj.record("Generic") else {
|
||||
continue;
|
||||
};
|
||||
if generic.get("Size_X").is_none() {
|
||||
continue;
|
||||
}
|
||||
@@ -45,11 +49,17 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{:<14} {:>10} {:>10} {:>10} {:>10}", "pair", "seen+diff", "seen+eq", "MISS+diff", "MISS+eq");
|
||||
println!(
|
||||
"{:<14} {:>10} {:>10} {:>10} {:>10}",
|
||||
"pair", "seen+diff", "seen+eq", "MISS+diff", "MISS+eq"
|
||||
);
|
||||
for (i, (field, sibling)) in pairs.iter().enumerate() {
|
||||
println!(
|
||||
"{:<14} {:>10} {:>10} {:>10} {:>10}",
|
||||
format!("{field}/{sibling}").chars().take(14).collect::<String>(),
|
||||
format!("{field}/{sibling}")
|
||||
.chars()
|
||||
.take(14)
|
||||
.collect::<String>(),
|
||||
tally[i][0][0],
|
||||
tally[i][0][1],
|
||||
tally[i][1][0],
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
//! this does the same comparison numerically.
|
||||
//!
|
||||
//! Usage: slab_screen <resource3d_dir> [factor]
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let factor: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(4.0);
|
||||
let factor: f32 = std::env::args()
|
||||
.nth(2)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(4.0);
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
@@ -24,13 +27,17 @@ fn main() {
|
||||
|
||||
let mut flagged = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let mut by_ship: BTreeMap<String, Vec<(String, f32)>> = BTreeMap::new();
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
if !is_base_part(&m.name) {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = ship_id_of(&m.name) else { continue };
|
||||
let Some(id) = ship_id_of(&m.name) else {
|
||||
continue;
|
||||
};
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for s in &m.meshes {
|
||||
for q in &s.positions {
|
||||
@@ -49,7 +56,10 @@ fn main() {
|
||||
// mis-anchored block is bulky in all three, which is what the e106
|
||||
// slab looked like (600×1600×998 beside siblings of ~250).
|
||||
let thin = (hi[0] - lo[0]).min(hi[1] - lo[1]).min(hi[2] - lo[2]);
|
||||
by_ship.entry(id.to_string()).or_default().push((m.name.clone(), thin));
|
||||
by_ship
|
||||
.entry(id.to_string())
|
||||
.or_default()
|
||||
.push((m.name.clone(), thin));
|
||||
}
|
||||
for (id, parts) in &by_ship {
|
||||
if parts.len() < 3 {
|
||||
|
||||
@@ -87,9 +87,13 @@ fn main() {
|
||||
);
|
||||
for (movie, n) in bound {
|
||||
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
|
||||
let Some(entry) = snd.find_by_name(&path) else { continue };
|
||||
let Some(entry) = snd.find_by_name(&path) else {
|
||||
continue;
|
||||
};
|
||||
let bytes = snd.read(entry).expect("read");
|
||||
let Some(first_riff) = bytes.windows(4).position(|w| w == b"RIFF") else { continue };
|
||||
let Some(first_riff) = bytes.windows(4).position(|w| w == b"RIFF") else {
|
||||
continue;
|
||||
};
|
||||
if first_riff <= slb::HEADERLESS_DATA_OFFSET {
|
||||
println!("{movie:<16} {:>6} (no leading region)", format!("D_{n}"));
|
||||
continue;
|
||||
@@ -102,7 +106,11 @@ fn main() {
|
||||
.iter()
|
||||
.map(|(_, t)| *t)
|
||||
.fold(0.0f32, f32::max);
|
||||
let implied = if cue > 0.0 { samples as f32 / cue } else { f32::NAN };
|
||||
let implied = if cue > 0.0 {
|
||||
samples as f32 / cue
|
||||
} else {
|
||||
f32::NAN
|
||||
};
|
||||
println!(
|
||||
"{movie:<16} {:>6} {stereo:>9} {mono:>9} {samples:>10} {cue:>9.2} {implied:>12.0}",
|
||||
format!("D_{n}")
|
||||
|
||||
@@ -13,7 +13,9 @@ fn main() {
|
||||
for e in snd.entries() {
|
||||
let Ok(b) = snd.read(e) else { continue };
|
||||
total += 1;
|
||||
let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { continue };
|
||||
let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else {
|
||||
continue;
|
||||
};
|
||||
has_riff += 1;
|
||||
if ri > slb::HEADERLESS_DATA_OFFSET
|
||||
&& (ri - slb::HEADERLESS_DATA_OFFSET) % slb::XMA1_PACKET == 0
|
||||
|
||||
@@ -1,31 +1,82 @@
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
use std::collections::BTreeMap;
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
// schemas whose type0/content is squadron/formation/route/group/position
|
||||
let mut hit:BTreeMap<u32,(u32,String,String)>=BTreeMap::new();
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
let t=o.tokens(); let t0=t.first().cloned().unwrap_or_default();
|
||||
let joined:String=t.iter().take(20).map(|s|s.as_str()).collect::<Vec<_>>().join(" ");
|
||||
if ["Squadron","Formation","Route","UnitGroup","Position","Spawn","Wave","Frame","NullFrame"].iter().any(|k|t0.contains(k)||joined.contains(k)){
|
||||
let ent=hit.entry(o.schema_hash).or_insert((0,t0.clone(),joined.chars().take(90).collect()));
|
||||
ent.0+=1;
|
||||
let mut hit: BTreeMap<u32, (u32, String, String)> = BTreeMap::new();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let t = o.tokens();
|
||||
let t0 = t.first().cloned().unwrap_or_default();
|
||||
let joined: String = t
|
||||
.iter()
|
||||
.take(20)
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if [
|
||||
"Squadron",
|
||||
"Formation",
|
||||
"Route",
|
||||
"UnitGroup",
|
||||
"Position",
|
||||
"Spawn",
|
||||
"Wave",
|
||||
"Frame",
|
||||
"NullFrame",
|
||||
]
|
||||
.iter()
|
||||
.any(|k| t0.contains(k) || joined.contains(k))
|
||||
{
|
||||
let ent = hit.entry(o.schema_hash).or_insert((
|
||||
0,
|
||||
t0.clone(),
|
||||
joined.chars().take(90).collect(),
|
||||
));
|
||||
ent.0 += 1;
|
||||
}
|
||||
}
|
||||
let mut v:Vec<_>=hit.into_iter().collect(); v.sort_by_key(|x|std::cmp::Reverse(x.1.0));
|
||||
for (s,(c,t0,sample)) in &v{ println!("[{s:08x}] ×{c:<4} type0={t0:<18.18} :: {sample}"); }
|
||||
let mut v: Vec<_> = hit.into_iter().collect();
|
||||
v.sort_by_key(|x| std::cmp::Reverse(x.1 .0));
|
||||
for (s, (c, t0, sample)) in &v {
|
||||
println!("[{s:08x}] ×{c:<4} type0={t0:<18.18} :: {sample}");
|
||||
}
|
||||
// dump a Squadron/UnitGroup blob fully to see if it has positions (binary floats) or refs
|
||||
println!("\n─── sample squadron/formation blob (first matching) ───");
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
let t=o.tokens(); let t0=t.first().cloned().unwrap_or_default();
|
||||
if t0.contains("Squadron")||t0.contains("Formation")||t0.contains("UnitGroup"){
|
||||
println!("schema {:08x} type0={t0} count={} tokens={}, blob {}B", o.schema_hash,o.count,t.len(),b.len());
|
||||
for (i,tk) in t.iter().take(30).enumerate(){ print!("{i:>2}:{tk:<20.20}"); if i%4==3{println!();} } println!();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let t = o.tokens();
|
||||
let t0 = t.first().cloned().unwrap_or_default();
|
||||
if t0.contains("Squadron") || t0.contains("Formation") || t0.contains("UnitGroup") {
|
||||
println!(
|
||||
"schema {:08x} type0={t0} count={} tokens={}, blob {}B",
|
||||
o.schema_hash,
|
||||
o.count,
|
||||
t.len(),
|
||||
b.len()
|
||||
);
|
||||
for (i, tk) in t.iter().take(30).enumerate() {
|
||||
print!("{i:>2}:{tk:<20.20}");
|
||||
if i % 4 == 3 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
println!();
|
||||
// scan blob for float-like values (reasonable coords) in the binary region
|
||||
let nfloats=(0..b.len().saturating_sub(4)).step_by(4).filter(|&i|{let f=f32::from_be_bytes([b[i],b[i+1],b[i+2],b[i+3]]); f.is_finite()&&f.abs()>1.0&&f.abs()<1e6}).count();
|
||||
let nfloats = (0..b.len().saturating_sub(4))
|
||||
.step_by(4)
|
||||
.filter(|&i| {
|
||||
let f = f32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
|
||||
f.is_finite() && f.abs() > 1.0 && f.abs() < 1e6
|
||||
})
|
||||
.count();
|
||||
println!(" ~{nfloats} plausible BE-float words in blob (positions live in binary region if high)");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
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);
|
||||
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|format!("{}/{}",m.unit.trim_start_matches("UN_").chars().take(18).collect::<String>(),m.pilot.clone().unwrap_or("·".into()))).collect();
|
||||
println!(" {:8} {:22} {:24} x{} {:?}", s.id.clone(), s.formation_id.clone().unwrap_or_default(), s.ai_id.clone().unwrap_or_default(), s.count.unwrap_or(0), mem);
|
||||
for s in sq
|
||||
.iter()
|
||||
.filter(|s| s.side.as_deref() == Some("TCAF"))
|
||||
.take(6)
|
||||
{
|
||||
let mem: Vec<String> = s
|
||||
.members
|
||||
.iter()
|
||||
.map(|m| {
|
||||
format!(
|
||||
"{}/{}",
|
||||
m.unit
|
||||
.trim_start_matches("UN_")
|
||||
.chars()
|
||||
.take(18)
|
||||
.collect::<String>(),
|
||||
m.pilot.clone().unwrap_or("·".into())
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" {:8} {:22} {:24} x{} {:?}",
|
||||
s.id.clone(),
|
||||
s.formation_id.clone().unwrap_or_default(),
|
||||
s.ai_id.clone().unwrap_or_default(),
|
||||
s.count.unwrap_or(0),
|
||||
mem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ fn main() {
|
||||
println!("{} candidate starts at stride {stride}", starts.len());
|
||||
for off in &a[3..] {
|
||||
let o = usize::from_str_radix(off.trim_start_matches("0x"), 16).unwrap();
|
||||
println!(" 0x{o:x} in candidate list: {}", starts.binary_search(&o).is_ok());
|
||||
println!(
|
||||
" 0x{o:x} in candidate list: {}",
|
||||
starts.binary_search(&o).is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
fn main(){let a:Vec<String>=std::env::args().collect();let b=std::fs::read(&a[1]).unwrap();
|
||||
let stride:usize=a[2].parse().unwrap(); let want:usize=a[3].parse().unwrap();
|
||||
let s=sylpheed_formats::mesh::debug_vertex_run_starts(&b,stride);
|
||||
println!("{} candidate starts at stride {}", s.len(), stride);
|
||||
println!("contains {:#x}: {}", want, s.contains(&want));
|
||||
let near:Vec<String>=s.iter().filter(|&&o| o.abs_diff(want)<0x200).map(|o|format!("{o:#x}")).collect();
|
||||
println!("nearby: {}", near.join(" "));}
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let b = std::fs::read(&a[1]).unwrap();
|
||||
let stride: usize = a[2].parse().unwrap();
|
||||
let want: usize = a[3].parse().unwrap();
|
||||
let s = sylpheed_formats::mesh::debug_vertex_run_starts(&b, stride);
|
||||
println!("{} candidate starts at stride {}", s.len(), stride);
|
||||
println!("contains {:#x}: {}", want, s.contains(&want));
|
||||
let near: Vec<String> = s
|
||||
.iter()
|
||||
.filter(|&&o| o.abs_diff(want) < 0x200)
|
||||
.map(|o| format!("{o:#x}"))
|
||||
.collect();
|
||||
println!("nearby: {}", near.join(" "));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
//! Per-sub-mesh vertex/index/coverage dump for one resource.
|
||||
//! Usage: submesh_dump <container.xpr> <resource>...
|
||||
use sylpheed_formats::mesh::{debug_resource_params, Xbg7Model};
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::{debug_resource_params, Xbg7Model};
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).expect("container");
|
||||
let want: HashSet<String> = a[2..].iter().cloned().collect();
|
||||
for m in Xbg7Model::models_named(&bytes, &want, &|| false) {
|
||||
let markers = debug_resource_params(&bytes, &m.name).map(|(mk, _)| mk).unwrap_or_default();
|
||||
println!("{} — {} sub-meshes decoded, {} markers declared", m.name, m.meshes.len(), markers.len());
|
||||
let markers = debug_resource_params(&bytes, &m.name)
|
||||
.map(|(mk, _)| mk)
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
"{} — {} sub-meshes decoded, {} markers declared",
|
||||
m.name,
|
||||
m.meshes.len(),
|
||||
markers.len()
|
||||
);
|
||||
for (i, s) in m.meshes.iter().enumerate() {
|
||||
let max_idx = s.indices.iter().max().copied().unwrap_or(0) as usize;
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
|
||||
@@ -5,12 +5,17 @@ fn main() {
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
// The production scan tries pads 0..=3; pass a bigger one to ask whether the
|
||||
// block would validate at all with a wider index/vertex gap.
|
||||
let max_pad: usize = std::env::var("MAX_PAD").ok().and_then(|v| v.parse().ok()).unwrap_or(3);
|
||||
let max_pad: usize = std::env::var("MAX_PAD")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(3);
|
||||
for pair in a[2..].iter() {
|
||||
let (name, off) = pair.split_once('@').unwrap();
|
||||
let off = usize::from_str_radix(off.trim_start_matches("0x"), 16).unwrap();
|
||||
match debug_try_anchor(&bytes, name, off, max_pad) {
|
||||
Some((v, i, pad)) => println!("{name:22} @ 0x{off:x} ACCEPTED v={v} idx={i} pad={pad}"),
|
||||
Some((v, i, pad)) => {
|
||||
println!("{name:22} @ 0x{off:x} ACCEPTED v={v} idx={i} pad={pad}")
|
||||
}
|
||||
None => println!("{name:22} @ 0x{off:x} rejected"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//! mis-anchor no count-based metric can see).
|
||||
//!
|
||||
//! Usage: twin_mirror_audit <resource3d_dir>
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
fn key(p: [f32; 3]) -> (i64, i64, i64) {
|
||||
(
|
||||
@@ -39,13 +39,19 @@ fn main() {
|
||||
let (mut same, mut mirrored, mut unrelated, mut related) = (0usize, 0usize, 0usize, 0usize);
|
||||
let mut examples: Vec<String> = Vec::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false);
|
||||
let by_name: BTreeMap<&str, &Xbg7Model> =
|
||||
models.iter().map(|m| (m.name.as_str(), m)).collect();
|
||||
for m in &models {
|
||||
let Some(stem) = m.name.strip_suffix("_01") else { continue };
|
||||
let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { continue };
|
||||
let Some(stem) = m.name.strip_suffix("_01") else {
|
||||
continue;
|
||||
};
|
||||
let Some(t) = by_name.get(format!("{stem}_02").as_str()) else {
|
||||
continue;
|
||||
};
|
||||
let a: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).collect();
|
||||
let b: Vec<[f32; 3]> = t.meshes.iter().flat_map(|s| s.positions.clone()).collect();
|
||||
if a.len() != b.len() || a.is_empty() {
|
||||
@@ -108,7 +114,10 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("twin pairs of equal vertex count: {}", same + mirrored + unrelated + related);
|
||||
println!(
|
||||
"twin pairs of equal vertex count: {}",
|
||||
same + mirrored + unrelated + related
|
||||
);
|
||||
println!(" exact X-mirror (expected) : {mirrored}");
|
||||
println!(" IDENTICAL, one buffer (collapse) : {same}");
|
||||
println!(" related other way (Y/Z mirror, reordered): {related}");
|
||||
|
||||
@@ -9,13 +9,18 @@ use sylpheed_formats::ratc;
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let pak = args.next().expect("usage: ui_screen <pak> [--dump 0xHASH out.bin]");
|
||||
let pak = args
|
||||
.next()
|
||||
.expect("usage: ui_screen <pak> [--dump 0xHASH out.bin]");
|
||||
let arc = PakArchive::open(&pak).expect("open pak");
|
||||
|
||||
let rest: Vec<String> = args.collect();
|
||||
if rest.first().map(|s| s == "--dump").unwrap_or(false) {
|
||||
let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap();
|
||||
let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress");
|
||||
let bytes = arc
|
||||
.read_by_hash(h)
|
||||
.expect("entry present")
|
||||
.expect("decompress");
|
||||
std::fs::write(&rest[2], &bytes).unwrap();
|
||||
println!("wrote {} bytes to {}", bytes.len(), rest[2]);
|
||||
return;
|
||||
@@ -25,11 +30,20 @@ fn main() {
|
||||
// --child 0xHASH <child-name> <out>: carve one RATC child out by its
|
||||
// listed offset/size, so a 165-byte layout record can be hexdumped alone.
|
||||
let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap();
|
||||
let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress");
|
||||
let bytes = arc
|
||||
.read_by_hash(h)
|
||||
.expect("entry present")
|
||||
.expect("decompress");
|
||||
let kids = ratc::parse(&bytes).expect("ratc");
|
||||
let k = kids.iter().find(|k| k.name == rest[2]).expect("child not found");
|
||||
let k = kids
|
||||
.iter()
|
||||
.find(|k| k.name == rest[2])
|
||||
.expect("child not found");
|
||||
std::fs::write(&rest[3], &bytes[k.offset..k.offset + k.size]).unwrap();
|
||||
println!("wrote {} B ({} @ {:#x}) to {}", k.size, k.name, k.offset, rest[3]);
|
||||
println!(
|
||||
"wrote {} B ({} @ {:#x}) to {}",
|
||||
k.size, k.name, k.offset, rest[3]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! Coverage has been reported as "resources decoded" without a denominator. This
|
||||
//! prints both, per container and in total, and names the misses so the gate
|
||||
//! attribution (`why_rejected`) has a work list.
|
||||
use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model};
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -20,7 +20,9 @@ fn main() {
|
||||
let (mut total, mut decoded, mut no_decl) = (0usize, 0usize, 0usize);
|
||||
let mut misses: Vec<String> = Vec::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
if names.is_empty() {
|
||||
continue;
|
||||
|
||||
@@ -13,8 +13,12 @@ fn main() {
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("pak");
|
||||
for entry in pak.entries() {
|
||||
let Ok(bytes) = pak.read(entry) else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let Some(id) = obj.get_raw("ID") else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Some(id) = obj.get_raw("ID") else {
|
||||
continue;
|
||||
};
|
||||
if !want.iter().any(|w| id.contains(w.as_str())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ use sylpheed_formats::PakArchive;
|
||||
fn main() {
|
||||
let disc = std::env::args().nth(1).expect("disc root");
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("pak");
|
||||
println!("{:<34} {:>9} {:>8} {:>8} {:>8} kind", "id", "hp", "size_x", "size_y", "size_z");
|
||||
println!(
|
||||
"{:<34} {:>9} {:>8} {:>8} {:>8} kind",
|
||||
"id", "hp", "size_x", "size_y", "size_z"
|
||||
);
|
||||
for u in load_units(&pak) {
|
||||
println!(
|
||||
"{:<34} {:>9} {:>8} {:>8} {:>8} unit",
|
||||
|
||||
@@ -6,9 +6,13 @@ fn main() {
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
for e in pak.entries() {
|
||||
let Ok(b) = pak.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Some(id) = o.get_raw("ID") else { continue };
|
||||
if !id.contains(&want) { continue; }
|
||||
if !id.contains(&want) {
|
||||
continue;
|
||||
}
|
||||
println!("=== {id}");
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for k in o.tokens() {
|
||||
|
||||
@@ -1,30 +1,91 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::process::Command;
|
||||
use sylpheed_formats::slb;
|
||||
use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command;
|
||||
fn rg(disc:&str,g:u64,n:usize)->Vec<u8>{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out}
|
||||
fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration:stream=channels","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).replace('\n'," ")}
|
||||
fn movdur(disc:&str,m:&str)->String{ dur(&format!("{disc}/dat/movie/{m}")) }
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
fn rg(disc: &str, g: u64, n: usize) -> Vec<u8> {
|
||||
let mut segs = vec![];
|
||||
let mut cum = 0u64;
|
||||
for i in 0..5 {
|
||||
let p = format!("{disc}/dat/sound.p{i:02}");
|
||||
if let Ok(m) = fs::metadata(&p) {
|
||||
segs.push((cum, m.len(), p));
|
||||
cum += m.len();
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let (mut need, mut pos) = (n, g);
|
||||
for (base, len, path) in &segs {
|
||||
if need == 0 || pos >= base + len || pos < *base {
|
||||
continue;
|
||||
}
|
||||
let local = pos - base;
|
||||
let take = need.min((len - local) as usize);
|
||||
let mut f = fs::File::open(path).unwrap();
|
||||
f.seek(SeekFrom::Start(local)).unwrap();
|
||||
let mut b = vec![0u8; take];
|
||||
f.read_exact(&mut b).unwrap();
|
||||
out.extend_from_slice(&b);
|
||||
need -= take;
|
||||
pos += take as u64;
|
||||
}
|
||||
out
|
||||
}
|
||||
fn dur(w: &str) -> String {
|
||||
let o = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration:stream=channels",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
w,
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
String::from_utf8_lossy(&o.stdout).replace('\n', " ")
|
||||
}
|
||||
fn movdur(disc: &str, m: &str) -> String {
|
||||
dur(&format!("{disc}/dat/movie/{m}"))
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
// descriptor trailer global offsets (from scan) => cue N data = [desc(N-1)..desc(N)]
|
||||
// (id, end_off, movie)
|
||||
let cues=[(1600u32,437044592u64,"ADV.wmv"),(1601,437345648,"RT01A.wmv"),(1602,437712240,"RT01B.wmv"),
|
||||
(1603,438080880,"RT01C_1.wmv"),(1604,438451568,"RT01C_2.wmv"),(1605,438789488,"RT02A.wmv")];
|
||||
for k in 1..cues.len(){
|
||||
let (id,end,mov)=cues[k];
|
||||
let start=cues[k-1].1;
|
||||
let region=rg(&disc,start,(end-start) as usize + 12000); // +tail to include full data before next trailer
|
||||
let riffs=slb::to_xma_riffs(®ion);
|
||||
let mut total=0.0f32; let mut parts=vec![];
|
||||
for (j,r) in riffs.iter().enumerate(){
|
||||
let xp=format!("/tmp/cue{id}_{j}.xma.wav"); let wp=format!("/tmp/cue{id}_{j}.wav");
|
||||
fs::write(&xp,r).unwrap();
|
||||
let _=Command::new("ffmpeg").args(["-hide_banner","-v","error","-y","-i",&xp,&wp]).status();
|
||||
let d=dur(&wp); parts.push(d.clone());
|
||||
total+=d.split_whitespace().next().unwrap_or("0").parse::<f32>().unwrap_or(0.0);
|
||||
let cues = [
|
||||
(1600u32, 437044592u64, "ADV.wmv"),
|
||||
(1601, 437345648, "RT01A.wmv"),
|
||||
(1602, 437712240, "RT01B.wmv"),
|
||||
(1603, 438080880, "RT01C_1.wmv"),
|
||||
(1604, 438451568, "RT01C_2.wmv"),
|
||||
(1605, 438789488, "RT02A.wmv"),
|
||||
];
|
||||
for k in 1..cues.len() {
|
||||
let (id, end, mov) = cues[k];
|
||||
let start = cues[k - 1].1;
|
||||
let region = rg(&disc, start, (end - start) as usize + 12000); // +tail to include full data before next trailer
|
||||
let riffs = slb::to_xma_riffs(®ion);
|
||||
let mut total = 0.0f32;
|
||||
let mut parts = vec![];
|
||||
for (j, r) in riffs.iter().enumerate() {
|
||||
let xp = format!("/tmp/cue{id}_{j}.xma.wav");
|
||||
let wp = format!("/tmp/cue{id}_{j}.wav");
|
||||
fs::write(&xp, r).unwrap();
|
||||
let _ = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp])
|
||||
.status();
|
||||
let d = dur(&wp);
|
||||
parts.push(d.clone());
|
||||
total += d
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("0")
|
||||
.parse::<f32>()
|
||||
.unwrap_or(0.0);
|
||||
}
|
||||
// spanning?
|
||||
let span_adv_end=437547264u64;
|
||||
let spans = start < span_adv_end && end > span_adv_end || (start/1_000 != end/1_000);
|
||||
let span_adv_end = 437547264u64;
|
||||
let spans = start < span_adv_end && end > span_adv_end || (start / 1_000 != end / 1_000);
|
||||
println!("cue {id} ({mov}): region[{start}..{end}] {} bytes, {} riff(s), Σ={:.1}s | movie={} parts={:?}",
|
||||
end-start, riffs.len(), total, movdur(&disc,mov), parts);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
//! cargo run --release --example vcount_index -- <resource3d_dir> <capture.log> [top_n]
|
||||
//! cargo run --release --example vcount_index -- <resource3d_dir> --vcounts 10891,6000
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
@@ -27,7 +27,10 @@ fn main() {
|
||||
let mut draw_count: HashMap<u32, usize> = HashMap::new();
|
||||
let mut bufs: HashMap<u32, HashSet<u32>> = HashMap::new();
|
||||
if args[2] == "--vcounts" {
|
||||
for v in args[3].split(',').filter_map(|s| s.trim().parse::<u32>().ok()) {
|
||||
for v in args[3]
|
||||
.split(',')
|
||||
.filter_map(|s| s.trim().parse::<u32>().ok())
|
||||
{
|
||||
draw_count.insert(v, 0);
|
||||
}
|
||||
} else {
|
||||
@@ -42,7 +45,10 @@ fn main() {
|
||||
bufs.entry(d.vcount).or_default().insert(d.vbase);
|
||||
}
|
||||
}
|
||||
let top_n: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX);
|
||||
let top_n: usize = args
|
||||
.get(3)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
// Decode every container once; keep only the vcount → names mapping.
|
||||
let mut by_vcount: HashMap<u32, Vec<String>> = HashMap::new();
|
||||
@@ -54,7 +60,9 @@ fn main() {
|
||||
files.sort();
|
||||
let mut total_res = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
if names.is_empty() {
|
||||
continue;
|
||||
@@ -65,7 +73,10 @@ fn main() {
|
||||
for m in &models {
|
||||
total_res += 1;
|
||||
let whole: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||
by_vcount.entry(whole as u32).or_default().push(format!("{container}:{}", m.name));
|
||||
by_vcount
|
||||
.entry(whole as u32)
|
||||
.or_default()
|
||||
.push(format!("{container}:{}", m.name));
|
||||
if m.meshes.len() > 1 {
|
||||
for (i, s) in m.meshes.iter().enumerate() {
|
||||
by_vcount
|
||||
@@ -76,7 +87,11 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("indexed {} resources from {} containers", total_res, files.len());
|
||||
eprintln!(
|
||||
"indexed {} resources from {} containers",
|
||||
total_res,
|
||||
files.len()
|
||||
);
|
||||
|
||||
let mut vcounts: Vec<u32> = draw_count.keys().copied().collect();
|
||||
vcounts.sort_unstable_by(|a, b| b.cmp(a));
|
||||
@@ -91,7 +106,15 @@ fn main() {
|
||||
let mut h = hit.clone();
|
||||
h.sort();
|
||||
let shown = h.len().min(8);
|
||||
format!("{}{}", h[..shown].join(", "), if h.len() > shown { format!(", … ({} total)", h.len()) } else { String::new() })
|
||||
format!(
|
||||
"{}{}",
|
||||
h[..shown].join(", "),
|
||||
if h.len() > shown {
|
||||
format!(", … ({} total)", h.len())
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
)
|
||||
};
|
||||
println!("{v:6} {n:5} {b:4} {label}");
|
||||
}
|
||||
|
||||
@@ -10,19 +10,149 @@ use sylpheed_formats::pak::PakArchive;
|
||||
// `addi r4, r30, -N` (r30 = 0x82088f94, so the name is a string in the image),
|
||||
// and the value lands in the first store into the object after the accessor
|
||||
// call. 125 float-typed fields.
|
||||
const MAP: &[(usize, &str)] = &[(48, "Size_X"), (52, "Size_Y"), (56, "Size_Z"), (64, "Color_R"), (68, "Color_G"), (72, "Color_B"), (80, "Size_Radius"), (84, "HP"), (88, "HQRatio"), (92, "ShieldRatio"), (96, "ThrusterRatio"), (116, "ResistanceToOptics"), (120, "ResistanceToShell"), (124, "ResistanceToExplosion"), (128, "ResistanceToPlayer"), (132, "ResistanceParalyze"), (156, "MinimumVelocity"), (160, "MaximumVelocity"), (164, "CruisingVelocity"), (168, "Acceleration"), (172, "Deceleration"), (176, "AV_PitchPlus_Max"), (180, "AV_PitchPlus_Min"), (184, "AA_PitchPlus_Max"), (188, "AA_PitchPlus_Min"), (192, "AV_PitchMinus_Max"), (196, "AV_PitchMinus_Min"), (200, "AA_PitchMinus_Max"), (204, "AA_PitchMinus_Min"), (208, "AV_Yaw_Max"), (212, "AV_Yaw_Min"), (216, "AA_Yaw_Max"), (220, "AA_Yaw_Min"), (224, "AV_Roll_Max"), (228, "AV_Roll_Min"), (232, "AA_Roll_Max"), (236, "AA_Roll_Min"), (248, "SideThrustVelocity_Max"), (252, "SideThrustAcceleration"), (256, "MaximumBank_Normal"), (260, "YawDragFactor"), (264, "PitchDragFactor"), (268, "RollDragFactor"), (272, "DragFactorThreshold"), (276, "ArterBurner_Vc"), (280, "ReverseThrust_Vc"), (284, "ArterBurner_Acc"), (288, "ReverseThrust_Acc"), (292, "AccPitchFactor"), (296, "DecPitchFactor"), (300, "AV_AxisMode_Max"), (304, "AV_AxisMode_Min"), (308, "AA_AxisMode_Max"), (312, "AA_AxisMode_Min"), (316, "PowerCutConsumeShield"), (320, "PowerCutDeceleration"), (324, "AB_ConsumeShield_Begin"), (328, "AB_ConsumeShield"), (332, "AB_AV_PitchPlus"), (336, "AB_AA_PitchPlus"), (340, "AB_AV_PitchMinus"), (344, "AB_AA_PitchMinus"), (348, "AB_AV_Yaw"), (352, "AB_AA_Yaw"), (356, "AB_AV_Roll"), (360, "AB_AA_Roll"), (368, "SideRoll_Time"), (372, "SideRoll_Length"), (380, "BarrelRoll_CountMinimum"), (384, "BarrelRoll_CountMaximum"), (388, "BarrelRoll_Time"), (392, "BarrelRoll_Radius"), (400, "TurnAttack_CutoffRatio"), (404, "TurnAttack_DoubleRatio"), (408, "CutoffTimeMin"), (412, "CutoffTimeMax"), (416, "TurnAttack_DoubleTimeMin"), (420, "TurnAttack_DoubleTimeMax"), (428, "Turn_AngularVelocity"), (432, "TurnAway_Time_Minimum"), (436, "TurnAway_Time_Maximum"), (444, "BoostAway_Time_Minimum"), (448, "BoostAway_Time_Maximum"), (456, "HoldPosition_LengthMin"), (460, "HoldPosition_LengthMax"), (464, "HoldPosition_MinimumTime"), (468, "HoldPosition_MaximumTime"), (472, "HoldPosition_SideRatio"), (476, "HoldPosition_BackRatio"), (480, "HoldPosition_CutoffRatio"), (484, "HoldPosition_CancelTime"), (492, "Slalom_CutoffRatio"), (496, "Slalom_TurnCount_Min"), (500, "Slalom_TurnCount_Max"), (508, "Through_CutoffRatio"), (512, "Through_AngleMinimum"), (516, "Through_AngleMaximum"), (520, "Through_Time1Max"), (524, "Through_Time1Min"), (528, "Through_Time2Max"), (532, "Through_Time2Min"), (536, "Through_LengthMinimum"), (540, "Through_LengthMaximum"), (548, "SolidCutoff_Ratio"), (552, "SolidCutoff_LengthMin"), (556, "SolidCutoff_LengthMax"), (560, "HomingResistAdjustment"), (564, "UsingChaffRatio"), (568, "MaxValue"), (572, "ChargeDelay"), (576, "ChargeDelay_Break"), (580, "ChargeSpeed"), (584, "Delay"), (588, "DelayAdjustment"), (624, "DestroyMotionTime"), (628, "DryMass"), (632, "GrossMass"), (664, "LowerHPThresholdRatio"), (668, "SELength"), (672, "RadarRange"), (676, "FCSRange"), (680, "FiringRange"), (692, "AttackVesselPoint"), (696, "AttackCraftPoint"), (700, "DefencePoint")];
|
||||
const MAP: &[(usize, &str)] = &[
|
||||
(48, "Size_X"),
|
||||
(52, "Size_Y"),
|
||||
(56, "Size_Z"),
|
||||
(64, "Color_R"),
|
||||
(68, "Color_G"),
|
||||
(72, "Color_B"),
|
||||
(80, "Size_Radius"),
|
||||
(84, "HP"),
|
||||
(88, "HQRatio"),
|
||||
(92, "ShieldRatio"),
|
||||
(96, "ThrusterRatio"),
|
||||
(116, "ResistanceToOptics"),
|
||||
(120, "ResistanceToShell"),
|
||||
(124, "ResistanceToExplosion"),
|
||||
(128, "ResistanceToPlayer"),
|
||||
(132, "ResistanceParalyze"),
|
||||
(156, "MinimumVelocity"),
|
||||
(160, "MaximumVelocity"),
|
||||
(164, "CruisingVelocity"),
|
||||
(168, "Acceleration"),
|
||||
(172, "Deceleration"),
|
||||
(176, "AV_PitchPlus_Max"),
|
||||
(180, "AV_PitchPlus_Min"),
|
||||
(184, "AA_PitchPlus_Max"),
|
||||
(188, "AA_PitchPlus_Min"),
|
||||
(192, "AV_PitchMinus_Max"),
|
||||
(196, "AV_PitchMinus_Min"),
|
||||
(200, "AA_PitchMinus_Max"),
|
||||
(204, "AA_PitchMinus_Min"),
|
||||
(208, "AV_Yaw_Max"),
|
||||
(212, "AV_Yaw_Min"),
|
||||
(216, "AA_Yaw_Max"),
|
||||
(220, "AA_Yaw_Min"),
|
||||
(224, "AV_Roll_Max"),
|
||||
(228, "AV_Roll_Min"),
|
||||
(232, "AA_Roll_Max"),
|
||||
(236, "AA_Roll_Min"),
|
||||
(248, "SideThrustVelocity_Max"),
|
||||
(252, "SideThrustAcceleration"),
|
||||
(256, "MaximumBank_Normal"),
|
||||
(260, "YawDragFactor"),
|
||||
(264, "PitchDragFactor"),
|
||||
(268, "RollDragFactor"),
|
||||
(272, "DragFactorThreshold"),
|
||||
(276, "ArterBurner_Vc"),
|
||||
(280, "ReverseThrust_Vc"),
|
||||
(284, "ArterBurner_Acc"),
|
||||
(288, "ReverseThrust_Acc"),
|
||||
(292, "AccPitchFactor"),
|
||||
(296, "DecPitchFactor"),
|
||||
(300, "AV_AxisMode_Max"),
|
||||
(304, "AV_AxisMode_Min"),
|
||||
(308, "AA_AxisMode_Max"),
|
||||
(312, "AA_AxisMode_Min"),
|
||||
(316, "PowerCutConsumeShield"),
|
||||
(320, "PowerCutDeceleration"),
|
||||
(324, "AB_ConsumeShield_Begin"),
|
||||
(328, "AB_ConsumeShield"),
|
||||
(332, "AB_AV_PitchPlus"),
|
||||
(336, "AB_AA_PitchPlus"),
|
||||
(340, "AB_AV_PitchMinus"),
|
||||
(344, "AB_AA_PitchMinus"),
|
||||
(348, "AB_AV_Yaw"),
|
||||
(352, "AB_AA_Yaw"),
|
||||
(356, "AB_AV_Roll"),
|
||||
(360, "AB_AA_Roll"),
|
||||
(368, "SideRoll_Time"),
|
||||
(372, "SideRoll_Length"),
|
||||
(380, "BarrelRoll_CountMinimum"),
|
||||
(384, "BarrelRoll_CountMaximum"),
|
||||
(388, "BarrelRoll_Time"),
|
||||
(392, "BarrelRoll_Radius"),
|
||||
(400, "TurnAttack_CutoffRatio"),
|
||||
(404, "TurnAttack_DoubleRatio"),
|
||||
(408, "CutoffTimeMin"),
|
||||
(412, "CutoffTimeMax"),
|
||||
(416, "TurnAttack_DoubleTimeMin"),
|
||||
(420, "TurnAttack_DoubleTimeMax"),
|
||||
(428, "Turn_AngularVelocity"),
|
||||
(432, "TurnAway_Time_Minimum"),
|
||||
(436, "TurnAway_Time_Maximum"),
|
||||
(444, "BoostAway_Time_Minimum"),
|
||||
(448, "BoostAway_Time_Maximum"),
|
||||
(456, "HoldPosition_LengthMin"),
|
||||
(460, "HoldPosition_LengthMax"),
|
||||
(464, "HoldPosition_MinimumTime"),
|
||||
(468, "HoldPosition_MaximumTime"),
|
||||
(472, "HoldPosition_SideRatio"),
|
||||
(476, "HoldPosition_BackRatio"),
|
||||
(480, "HoldPosition_CutoffRatio"),
|
||||
(484, "HoldPosition_CancelTime"),
|
||||
(492, "Slalom_CutoffRatio"),
|
||||
(496, "Slalom_TurnCount_Min"),
|
||||
(500, "Slalom_TurnCount_Max"),
|
||||
(508, "Through_CutoffRatio"),
|
||||
(512, "Through_AngleMinimum"),
|
||||
(516, "Through_AngleMaximum"),
|
||||
(520, "Through_Time1Max"),
|
||||
(524, "Through_Time1Min"),
|
||||
(528, "Through_Time2Max"),
|
||||
(532, "Through_Time2Min"),
|
||||
(536, "Through_LengthMinimum"),
|
||||
(540, "Through_LengthMaximum"),
|
||||
(548, "SolidCutoff_Ratio"),
|
||||
(552, "SolidCutoff_LengthMin"),
|
||||
(556, "SolidCutoff_LengthMax"),
|
||||
(560, "HomingResistAdjustment"),
|
||||
(564, "UsingChaffRatio"),
|
||||
(568, "MaxValue"),
|
||||
(572, "ChargeDelay"),
|
||||
(576, "ChargeDelay_Break"),
|
||||
(580, "ChargeSpeed"),
|
||||
(584, "Delay"),
|
||||
(588, "DelayAdjustment"),
|
||||
(624, "DestroyMotionTime"),
|
||||
(628, "DryMass"),
|
||||
(632, "GrossMass"),
|
||||
(664, "LowerHPThresholdRatio"),
|
||||
(668, "SELength"),
|
||||
(672, "RadarRange"),
|
||||
(676, "FCSRange"),
|
||||
(680, "FiringRange"),
|
||||
(692, "AttackVesselPoint"),
|
||||
(696, "AttackCraftPoint"),
|
||||
(700, "DefencePoint"),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let disc = a.next().unwrap();
|
||||
let dump = a.next().unwrap();
|
||||
let pairs: Vec<(String, String)> =
|
||||
a.filter_map(|s| s.split_once('=').map(|(i, v)| (i.into(), v.into()))).collect();
|
||||
let pairs: Vec<(String, String)> = a
|
||||
.filter_map(|s| s.split_once('=').map(|(i, v)| (i.into(), v.into())))
|
||||
.collect();
|
||||
|
||||
let mut live: BTreeMap<String, BTreeMap<usize, f32>> = BTreeMap::new();
|
||||
let mut cur = String::new();
|
||||
for line in std::fs::read_to_string(&dump).unwrap().lines() {
|
||||
if let Some(r) = line.strip_prefix("=== ") { cur = r.trim().into(); continue; }
|
||||
if let Some(r) = line.strip_prefix("=== ") {
|
||||
cur = r.trim().into();
|
||||
continue;
|
||||
}
|
||||
let f: Vec<&str> = line.split_whitespace().collect();
|
||||
// dump columns: addr +off hex u32 f32 -- the float is f[4], not f[3]
|
||||
if f.len() >= 5 && f[1].starts_with('+') {
|
||||
@@ -37,9 +167,13 @@ fn main() {
|
||||
let mut defaults: BTreeMap<String, Vec<(String, f32)>> = BTreeMap::new();
|
||||
for e in pak.entries() {
|
||||
let Ok(b) = pak.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Some(id) = o.get_raw("ID") else { continue };
|
||||
let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue };
|
||||
let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(w) = live.get(va) else { continue };
|
||||
for (off, key) in MAP {
|
||||
let Some(got) = w.get(off) else { continue };
|
||||
@@ -50,12 +184,17 @@ fn main() {
|
||||
let rad = want * std::f32::consts::PI / 180.0;
|
||||
let ok = (want - got).abs() <= want.abs() * 1e-4
|
||||
|| (rad - got).abs() <= rad.abs() * 1e-4;
|
||||
if ok { agree += 1 } else {
|
||||
if ok {
|
||||
agree += 1
|
||||
} else {
|
||||
disagree += 1;
|
||||
println!(" MISMATCH {id} {key}: disc {want}, live +{off} = {got}");
|
||||
}
|
||||
}
|
||||
None => defaults.entry((*key).into()).or_default().push((id.into(), *got)),
|
||||
None => defaults
|
||||
.entry((*key).into())
|
||||
.or_default()
|
||||
.push((id.into(), *got)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +203,14 @@ fn main() {
|
||||
for (key, hits) in &defaults {
|
||||
let vals: Vec<String> = hits.iter().map(|(_, v)| format!("{v}")).collect();
|
||||
let uniq: std::collections::BTreeSet<&String> = vals.iter().collect();
|
||||
println!(" {:<22} {:<28} ({} units)", key,
|
||||
uniq.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "), hits.len());
|
||||
println!(
|
||||
" {:<22} {:<28} ({} units)",
|
||||
key,
|
||||
uniq.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
hits.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ fn main() {
|
||||
let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak");
|
||||
for n in 450..=454 {
|
||||
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
|
||||
let Some(entry) = snd.find_by_name(&path) else { continue };
|
||||
let Some(entry) = snd.find_by_name(&path) else {
|
||||
continue;
|
||||
};
|
||||
let bytes = snd.read(entry).expect("read");
|
||||
for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() {
|
||||
let f = format!("{out}/VOICE_D_{n}_{i}.xma");
|
||||
|
||||
@@ -17,7 +17,9 @@ fn main() {
|
||||
for n in 450..=454 {
|
||||
for dir in ["etc", "Voice", "Movie"] {
|
||||
let path = format!("eng\\{dir}\\VOICE_D_{n}.slb");
|
||||
let Some(entry) = snd.find_by_name(&path) else { continue };
|
||||
let Some(entry) = snd.find_by_name(&path) else {
|
||||
continue;
|
||||
};
|
||||
let bytes = snd.read(entry).expect("read");
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
let sizes: Vec<usize> = riffs.iter().map(|r| r.len()).collect();
|
||||
|
||||
@@ -55,7 +55,9 @@ fn main() {
|
||||
let cues = movie_subtitle::track_voice_cues(&lang, movie);
|
||||
let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max);
|
||||
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
|
||||
let Some(entry) = snd.find_by_name(&path) else { continue };
|
||||
let Some(entry) = snd.find_by_name(&path) else {
|
||||
continue;
|
||||
};
|
||||
let bytes = snd.read(entry).expect("read");
|
||||
// Sum the sub-waves' payloads as the decoder currently sees them.
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
//! Why does a named resource never decode? Reports the furthest gate its best
|
||||
//! candidate reached. Usage: why_missed <container.xpr> [name-substring]
|
||||
use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model};
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model};
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let filter = a.get(2).cloned().unwrap_or_default();
|
||||
let decoded: HashSet<String> = Xbg7Model::stage_models(&bytes).into_iter().map(|m| m.name).collect();
|
||||
let decoded: HashSet<String> = Xbg7Model::stage_models(&bytes)
|
||||
.into_iter()
|
||||
.map(|m| m.name)
|
||||
.collect();
|
||||
for n in xbg7_resource_names(&bytes) {
|
||||
if decoded.contains(&n) || (!filter.is_empty() && !n.contains(&filter)) {
|
||||
continue;
|
||||
|
||||
@@ -176,8 +176,7 @@ fn parse_riff_wave(bytes: &[u8]) -> Option<AudioInfo> {
|
||||
return None;
|
||||
}
|
||||
let le16 = |o: usize| u16::from_le_bytes([bytes[o], bytes[o + 1]]);
|
||||
let le32 =
|
||||
|o: usize| u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]);
|
||||
let le32 = |o: usize| u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]);
|
||||
|
||||
let mut pos = 12;
|
||||
let (mut tag, mut channels, mut rate, mut bits) = (0u16, 0u16, 0u32, 0u16);
|
||||
@@ -266,8 +265,12 @@ impl GameAudio {
|
||||
return Err(AudioError::NeedsDecoder(info.codec));
|
||||
}
|
||||
let channels = info.channels.ok_or(AudioError::Malformed("no channels"))?;
|
||||
let rate = info.sample_rate.ok_or(AudioError::Malformed("no sample rate"))?;
|
||||
let bits = info.bits_per_sample.ok_or(AudioError::Malformed("no bit depth"))?;
|
||||
let rate = info
|
||||
.sample_rate
|
||||
.ok_or(AudioError::Malformed("no sample rate"))?;
|
||||
let bits = info
|
||||
.bits_per_sample
|
||||
.ok_or(AudioError::Malformed("no bit depth"))?;
|
||||
|
||||
// Locate the `data` chunk body.
|
||||
let (off, len) = riff_data_span(bytes).ok_or(AudioError::Malformed("no data chunk"))?;
|
||||
@@ -299,7 +302,11 @@ impl GameAudio {
|
||||
.collect(),
|
||||
_ => return Err(AudioError::UnsupportedPcm { tag: 0, bits }),
|
||||
};
|
||||
Ok(Self { samples, channels, sample_rate: rate })
|
||||
Ok(Self {
|
||||
samples,
|
||||
channels,
|
||||
sample_rate: rate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,8 +315,9 @@ fn riff_data_span(bytes: &[u8]) -> Option<(usize, usize)> {
|
||||
if bytes.len() < 12 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
|
||||
return None;
|
||||
}
|
||||
let le32 =
|
||||
|o: usize| u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize;
|
||||
let le32 = |o: usize| {
|
||||
u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize
|
||||
};
|
||||
let mut pos = 12;
|
||||
while pos + 8 <= bytes.len() {
|
||||
let size = le32(pos + 4);
|
||||
|
||||
@@ -251,7 +251,11 @@ fn load_objects<T>(
|
||||
}
|
||||
|
||||
/// Read every object whose record 0 hashes to `schema` (see [`schema`]).
|
||||
fn load_table<T>(pak: &PakArchive, schema: u32, build: impl Fn(&IdxdObject) -> Option<T>) -> Vec<T> {
|
||||
fn load_table<T>(
|
||||
pak: &PakArchive,
|
||||
schema: u32,
|
||||
build: impl Fn(&IdxdObject) -> Option<T>,
|
||||
) -> Vec<T> {
|
||||
load_objects(pak, |o| o.schema_hash == schema, build)
|
||||
}
|
||||
|
||||
@@ -474,7 +478,10 @@ pub fn load_weapons(pak: &PakArchive) -> Vec<Weapon> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
load_objects(pak, is_weapon, Weapon::from_idxd)
|
||||
.into_iter()
|
||||
.filter(|w| w.id.as_deref().is_some_and(|id| seen.insert(id.to_string())))
|
||||
.filter(|w| {
|
||||
w.id.as_deref()
|
||||
.is_some_and(|id| seen.insert(id.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -792,7 +799,9 @@ pub struct ScoreRules {
|
||||
|
||||
impl ScoreRules {
|
||||
fn from_record(f: Option<&RecordFields>) -> Self {
|
||||
let Some(f) = f else { return ScoreRules::default() };
|
||||
let Some(f) = f else {
|
||||
return ScoreRules::default();
|
||||
};
|
||||
ScoreRules {
|
||||
main_mission_bonus: f.i64("MainMissionBonus"),
|
||||
main_mission_count: f.i64("MainMissionCount"),
|
||||
@@ -1121,14 +1130,20 @@ 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 Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Some(index) = squadron_index(&o).map(str::to_string) else {
|
||||
continue;
|
||||
};
|
||||
let records = RecordSet::from_idxd(&o);
|
||||
let Some(list) = records.record(&index) else { continue };
|
||||
let Some(list) = records.record(&index) else {
|
||||
continue;
|
||||
};
|
||||
for id in list.named.keys() {
|
||||
let Some(f) = records.record(id) else { continue };
|
||||
let Some(f) = records.record(id) else {
|
||||
continue;
|
||||
};
|
||||
let slots: Vec<&str> = f.positional.iter().map(|(_, v)| v.as_str()).collect();
|
||||
let members = slots
|
||||
.as_chunks::<4>()
|
||||
@@ -1213,7 +1228,11 @@ pub fn load_demo_messages(pak: &PakArchive) -> Vec<DemoMessage> {
|
||||
duration: as_f32(f.at(3)),
|
||||
voice_clip: text(f.at(4)),
|
||||
page_count: f.i64("PageCount"),
|
||||
page_keys: f.slots_from(5).filter(|s| !s.is_empty()).map(str::to_string).collect(),
|
||||
page_keys: f
|
||||
.slots_from(5)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -1263,8 +1282,16 @@ pub fn load_unit_rosters(pak: &PakArchive) -> Vec<UnitRoster> {
|
||||
|id: &str| id.contains("Asteroid") || id.contains("cmesh") || id.contains("_Box");
|
||||
let roster = UnitRoster {
|
||||
stage,
|
||||
units: ids.iter().filter(|i| !is_prop(i)).map(|i| i.to_string()).collect(),
|
||||
props: ids.iter().filter(|i| is_prop(i)).map(|i| i.to_string()).collect(),
|
||||
units: ids
|
||||
.iter()
|
||||
.filter(|i| !is_prop(i))
|
||||
.map(|i| i.to_string())
|
||||
.collect(),
|
||||
props: ids
|
||||
.iter()
|
||||
.filter(|i| is_prop(i))
|
||||
.map(|i| i.to_string())
|
||||
.collect(),
|
||||
};
|
||||
(!roster.units.is_empty()).then_some(roster)
|
||||
},
|
||||
@@ -1329,7 +1356,9 @@ pub fn load_pilot_rosters(pak: &PakArchive) -> Vec<PilotRoster> {
|
||||
let mut assignments = Vec::new();
|
||||
let mut player_unit = None;
|
||||
for name in list.named.keys() {
|
||||
let Some(f) = records.record(name) else { continue };
|
||||
let Some(f) = records.record(name) else {
|
||||
continue;
|
||||
};
|
||||
let Some((callsign, pilot)) = name.split_once('-') else {
|
||||
continue;
|
||||
};
|
||||
@@ -1384,7 +1413,9 @@ pub fn load_arsenal(pak: &PakArchive) -> Arsenal {
|
||||
let mut seen = [(); 4].map(|_| std::collections::BTreeSet::new());
|
||||
for e in pak.entries() {
|
||||
let Ok(b) = pak.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let records = RecordSet::from_idxd(&o);
|
||||
for (idx, slot) in ["NOSE", "ARM1", "ARM2", "ARM3"].into_iter().enumerate() {
|
||||
let Some(f) = records.record(&format!("STANDARD_{slot}")) else {
|
||||
@@ -1429,11 +1460,7 @@ pub struct Record {
|
||||
impl Record {
|
||||
fn from_idxd(o: &IdxdObject) -> Option<Self> {
|
||||
let records = RecordSet::from_idxd(o);
|
||||
let first = |field: &str| {
|
||||
records
|
||||
.names()
|
||||
.find_map(|n| records.record(n)?.text(field))
|
||||
};
|
||||
let first = |field: &str| records.names().find_map(|n| records.record(n)?.text(field));
|
||||
Some(Record {
|
||||
table: records.first_name()?.to_string(),
|
||||
schema: o.schema_hash,
|
||||
@@ -1490,10 +1517,16 @@ mod tests {
|
||||
// Launcher record.
|
||||
assert_eq!(rocket.interval, Some(1.0));
|
||||
assert_eq!(rocket.loading_count, Some(1000));
|
||||
assert_eq!(rocket.target_type.as_deref(), Some("Vessel,Craft,Structure"));
|
||||
assert_eq!(
|
||||
rocket.target_type.as_deref(),
|
||||
Some("Vessel,Craft,Structure")
|
||||
);
|
||||
assert_eq!(rocket.shot_type.as_deref(), Some("Single"));
|
||||
// Shell record — a separate ID/Name the flat reader merged with the above.
|
||||
assert_eq!(rocket.shell_id.as_deref(), Some("Shell_TCAF_DeltaSaber_Rocket_P"));
|
||||
assert_eq!(
|
||||
rocket.shell_id.as_deref(),
|
||||
Some("Shell_TCAF_DeltaSaber_Rocket_P")
|
||||
);
|
||||
assert_eq!(rocket.power, Some(100.0));
|
||||
assert_eq!(rocket.velocity, Some(3000.0));
|
||||
assert_eq!(rocket.max_range, Some(6000.0));
|
||||
@@ -1565,7 +1598,11 @@ mod tests {
|
||||
assert_eq!(raymond.faces.len(), 13);
|
||||
assert!(raymond.faces.iter().all(|f| f.texture.ends_with(".t32")));
|
||||
assert_eq!(
|
||||
raymond.faces.iter().find(|f| f.id == "FaceRAYMOND_07").map(|f| f.texture.as_str()),
|
||||
raymond
|
||||
.faces
|
||||
.iter()
|
||||
.find(|f| f.id == "FaceRAYMOND_07")
|
||||
.map(|f| f.texture.as_str()),
|
||||
Some("pjf003_C02.t32")
|
||||
);
|
||||
}
|
||||
@@ -1574,15 +1611,24 @@ mod tests {
|
||||
fn loads_stages() {
|
||||
let Some(pak) = pak() else { return };
|
||||
let stages = load_stages(&pak);
|
||||
let s01 = stages.iter().find(|s| s.id == "S01").expect("stage S01 present");
|
||||
let s01 = stages
|
||||
.iter()
|
||||
.find(|s| s.id == "S01")
|
||||
.expect("stage S01 present");
|
||||
assert_eq!(s01.location.as_deref(), Some("Lebendorf"));
|
||||
assert_eq!(s01.phase_count(), 3);
|
||||
assert_eq!(s01.unit_table.as_deref(), Some("EnumUnit_S01.tbl"));
|
||||
assert_eq!(s01.squadron_table.as_deref(), Some("UnitGroup_S01.tbl"));
|
||||
assert!(s01.stage_package.as_deref().is_some_and(|p| p.contains("Stage_S01")));
|
||||
assert!(s01
|
||||
.stage_package
|
||||
.as_deref()
|
||||
.is_some_and(|p| p.contains("Stage_S01")));
|
||||
// 16-mission main campaign is present.
|
||||
for n in 1..=16 {
|
||||
assert!(stages.iter().any(|s| s.id == format!("S{n:02}")), "missing S{n:02}");
|
||||
assert!(
|
||||
stages.iter().any(|s| s.id == format!("S{n:02}")),
|
||||
"missing S{n:02}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1592,18 +1638,21 @@ mod tests {
|
||||
let sq = load_squadrons(&pak);
|
||||
assert_eq!(sq.len(), 1160, "squadron records across all stage tables");
|
||||
// Every squadron's member list matches its declared count.
|
||||
assert!(sq.iter().all(|s| s.members.len() as i64 == s.count.unwrap_or(-1)));
|
||||
assert!(sq
|
||||
.iter()
|
||||
.all(|s| s.members.len() as i64 == s.count.unwrap_or(-1)));
|
||||
// The player's Rhino flight: 2 Delta Sabers, Katana leading.
|
||||
let rhino = sq
|
||||
.iter()
|
||||
.find(|s| {
|
||||
s.id == "TCN001" && s.formation_id.as_deref() == Some("Formation_2_Rhino1")
|
||||
})
|
||||
.find(|s| s.id == "TCN001" && s.formation_id.as_deref() == Some("Formation_2_Rhino1"))
|
||||
.expect("TCN001 Rhino flight");
|
||||
assert_eq!(rhino.side.as_deref(), Some("TCAF"));
|
||||
assert_eq!(rhino.count, Some(2));
|
||||
assert!(rhino.members.iter().all(|m| m.unit.contains("DeltaSaber")));
|
||||
assert_eq!(rhino.members[0].message_set.as_deref(), Some("MessageSet_Katana"));
|
||||
assert_eq!(
|
||||
rhino.members[0].message_set.as_deref(),
|
||||
Some("MessageSet_Katana")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1613,8 +1662,13 @@ mod tests {
|
||||
assert_eq!(msgs.len(), 11775);
|
||||
// Every line names a speaker and a portrait; the old reader attributed
|
||||
// only a minority.
|
||||
assert!(msgs.iter().all(|m| m.character.is_some() && m.face.is_some()));
|
||||
assert_eq!(msgs.iter().filter(|m| m.voice_clip.is_some()).count(), 11758);
|
||||
assert!(msgs
|
||||
.iter()
|
||||
.all(|m| m.character.is_some() && m.face.is_some()));
|
||||
assert_eq!(
|
||||
msgs.iter().filter(|m| m.voice_clip.is_some()).count(),
|
||||
11758
|
||||
);
|
||||
assert!(msgs.iter().all(|m| !m.page_keys.is_empty()));
|
||||
}
|
||||
|
||||
@@ -1623,7 +1677,9 @@ mod tests {
|
||||
let Some(pak) = pak() else { return };
|
||||
let rosters = load_unit_rosters(&pak);
|
||||
assert_eq!(rosters.len(), 31);
|
||||
assert!(rosters.iter().all(|r| r.units.iter().all(|u| !u.contains("Asteroid"))));
|
||||
assert!(rosters
|
||||
.iter()
|
||||
.all(|r| r.units.iter().all(|u| !u.contains("Asteroid"))));
|
||||
// A roster tagged S01 exists and names the player's Delta Saber.
|
||||
let s01 = rosters.iter().find(|r| r.stage.as_deref() == Some("S01"));
|
||||
assert!(s01.is_some_and(|r| r.units.iter().any(|u| u.contains("DeltaSaber"))));
|
||||
@@ -1631,16 +1687,24 @@ mod tests {
|
||||
|
||||
#[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 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_eq!(rosters.len(), 168);
|
||||
// Every config seats the player and names the craft flown.
|
||||
assert!(rosters.iter().all(|r| r.player_unit.is_some()));
|
||||
assert!(rosters.iter().any(|r| {
|
||||
r.assignments.iter().any(|a| a.callsign.starts_with("Rhino") && a.pilot == "Katana")
|
||||
r.assignments
|
||||
.iter()
|
||||
.any(|a| a.callsign.starts_with("Rhino") && a.pilot == "Katana")
|
||||
}));
|
||||
assert!(rosters.iter().any(|r| r.assignments.iter().any(|a| a.callsign.starts_with("Bird"))));
|
||||
assert!(rosters
|
||||
.iter()
|
||||
.any(|r| r.assignments.iter().any(|a| a.callsign.starts_with("Bird"))));
|
||||
let a = rosters
|
||||
.iter()
|
||||
.flat_map(|r| &r.assignments)
|
||||
@@ -1651,10 +1715,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn loads_arsenal() {
|
||||
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 Ok(disc) = std::env::var("SYLPHEED_DISC") else {
|
||||
return;
|
||||
};
|
||||
let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")) else {
|
||||
return;
|
||||
};
|
||||
let a = load_arsenal(&pak);
|
||||
assert_eq!((a.nose.len(), a.arm1.len(), a.arm2.len(), a.arm3.len()), (8, 12, 9, 9));
|
||||
assert_eq!(
|
||||
(a.nose.len(), a.arm1.len(), a.arm2.len(), a.arm3.len()),
|
||||
(8, 12, 9, 9)
|
||||
);
|
||||
assert!(a.nose.iter().any(|w| w.starts_with("Stiletto")));
|
||||
assert!(a.arm1.iter().any(|w| w.starts_with("Falcon")));
|
||||
// No field keys, headers or pilot names leak into the option lists — the
|
||||
|
||||
@@ -192,7 +192,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reference_track() {
|
||||
let b = synth(&["SUBTITLE", "MSG_DEMO_240", "00:01.00", "MSG_DEMO_241", "00:03.20"]);
|
||||
let b = synth(&[
|
||||
"SUBTITLE",
|
||||
"MSG_DEMO_240",
|
||||
"00:01.00",
|
||||
"MSG_DEMO_241",
|
||||
"00:03.20",
|
||||
]);
|
||||
let sub = parse(&b).unwrap();
|
||||
assert_eq!(sub.cues.len(), 2);
|
||||
assert_eq!(sub.cues[0].end, None);
|
||||
@@ -205,7 +211,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A parsed IXUD object: its records and their fields, addressable by name.
|
||||
///
|
||||
/// The counterpart of [`crate::IdxdObject`] for wide strings. Prefer this to the
|
||||
|
||||
@@ -90,10 +90,10 @@ pub use audio::{AudioCodec, AudioInfo, GameAudio};
|
||||
pub use font::FontInfo;
|
||||
pub use idxd::{IdxdError, IdxdObject};
|
||||
pub use ixud::{Cue, IxudField, IxudObject, IxudRecord, Subtitle};
|
||||
pub use movie_subtitle::{SubCue, SubLang};
|
||||
pub use mesh::{GameMesh, Xbg7Model};
|
||||
pub use movie_subtitle::{SubCue, SubLang};
|
||||
pub use pak::{PakArchive, PakEntry, PakError};
|
||||
pub use ratc::RatcChild;
|
||||
pub use t8ad::T8adImage;
|
||||
pub use pak::{PakArchive, PakEntry, PakError};
|
||||
pub use texture::{X360Texture, X360TextureFormat};
|
||||
pub use vfs::{GameAssets, VfsError};
|
||||
|
||||
@@ -165,8 +165,12 @@ mod tests {
|
||||
// Disc-backed — skipped unless SYLPHEED_DISC is set.
|
||||
#[test]
|
||||
fn resolves_real_text() {
|
||||
let Ok(disc) = std::env::var("SYLPHEED_DISC") else { return };
|
||||
let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")) else { return };
|
||||
let Ok(disc) = std::env::var("SYLPHEED_DISC") else {
|
||||
return;
|
||||
};
|
||||
let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")) else {
|
||||
return;
|
||||
};
|
||||
let t = TextIndex::build(&pak);
|
||||
assert!(t.len() > 5000, "expected a large index, got {}", t.len());
|
||||
assert_eq!(t.character_name("CharacterRAYMOND"), Some("Raymond"));
|
||||
|
||||
@@ -138,8 +138,7 @@ pub fn se_wave_riff<S: DiscSource + ?Sized>(
|
||||
e.comp_size
|
||||
));
|
||||
}
|
||||
let packets =
|
||||
source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?;
|
||||
let packets = source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?;
|
||||
Ok(crate::slb::xma1_wave_riff(&packets, channels, rate))
|
||||
}
|
||||
|
||||
@@ -218,7 +217,9 @@ pub fn hokyu_voice_token<S: DiscSource + ?Sized>(
|
||||
let lang_pak = source
|
||||
.open_pak(&format!("dat/movie/{}.pak", lang.code_pub()))
|
||||
.ok()?;
|
||||
let want = ms::track_voice_cues(&lang_pak, movie).first().map(|&(d, _)| d)?;
|
||||
let want = ms::track_voice_cues(&lang_pak, movie)
|
||||
.first()
|
||||
.map(|&(d, _)| d)?;
|
||||
crate::movie_manifest::parse(manifest)
|
||||
.into_iter()
|
||||
.find_map(|e| {
|
||||
@@ -338,7 +339,9 @@ impl DiscSource for DirectorySource {
|
||||
break;
|
||||
}
|
||||
let path = self.root.join(format!("{stem}.p{i:02}"));
|
||||
let Ok(meta) = std::fs::metadata(&path) else { break };
|
||||
let Ok(meta) = std::fs::metadata(&path) else {
|
||||
break;
|
||||
};
|
||||
let seg_len = meta.len();
|
||||
if skip >= seg_len {
|
||||
skip -= seg_len;
|
||||
|
||||
@@ -232,8 +232,7 @@ impl Xbg7Model {
|
||||
// XBG7 layout is NOT fixed-stride — models omit UV or use fewer elements
|
||||
// (stride 20 = pos+normal, stride 24 = pos+normal+uv, …). Confirmed
|
||||
// against a Canary GPU vertex-fetch capture (see docs/re/xbg7-mesh.md).
|
||||
let decl = parse_vertex_decl(&bytes[desc..desc_end])
|
||||
.ok_or(MeshError::UnsupportedLayout)?;
|
||||
let decl = parse_vertex_decl(&bytes[desc..desc_end]).ok_or(MeshError::UnsupportedLayout)?;
|
||||
|
||||
// Extract the ordered list of sub-mesh (vtx_count, idx_count) records.
|
||||
let records = submesh_records(&bytes[desc..desc_end]);
|
||||
@@ -481,7 +480,10 @@ impl Xbg7Model {
|
||||
return Self::anchor_models_uncached(bytes, min_consistency, should_cancel, None);
|
||||
};
|
||||
let full = full_decode_cached(bytes, min_consistency, should_cancel);
|
||||
full.iter().filter(|m| w.contains(&m.name)).cloned().collect()
|
||||
full.iter()
|
||||
.filter(|m| w.contains(&m.name))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn anchor_models_uncached(
|
||||
@@ -554,7 +556,11 @@ impl Xbg7Model {
|
||||
// at a different offset when requested alone. The filter is applied
|
||||
// to the OUTPUT instead, so a subset is always a subset of the
|
||||
// container's own answer.
|
||||
let mut decls = if submesh_decls() { all_vertex_decls(d) } else { Vec::new() };
|
||||
let mut decls = if submesh_decls() {
|
||||
all_vertex_decls(d)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if decls.len() != markers.len() {
|
||||
// Never seen on the disc, but if the two walks disagree fall back
|
||||
// to the single declaration rather than mis-pair them.
|
||||
@@ -623,8 +629,14 @@ impl Xbg7Model {
|
||||
} else {
|
||||
// Several sub-meshes sharing grouped index/vertex pools → the
|
||||
// deterministic grouped-pool decode (hero ships et al.).
|
||||
let grouped =
|
||||
anchor_grouped_meshes(bytes, data_base, starts, &r.markers, &r.decls, &empty_taken);
|
||||
let grouped = anchor_grouped_meshes(
|
||||
bytes,
|
||||
data_base,
|
||||
starts,
|
||||
&r.markers,
|
||||
&r.decls,
|
||||
&empty_taken,
|
||||
);
|
||||
if !grouped.is_empty() {
|
||||
grouped
|
||||
} else {
|
||||
@@ -781,7 +793,9 @@ pub fn debug_try_anchor(
|
||||
let header = Xpr2Header::read(&mut cur).ok()?;
|
||||
const DIR_BASE: usize = 0x10;
|
||||
for _ in 0..header.num_resources {
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break };
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else {
|
||||
break;
|
||||
};
|
||||
if &e.type_tag != b"XBG7" {
|
||||
continue;
|
||||
}
|
||||
@@ -861,7 +875,11 @@ pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)>
|
||||
let span = rel_ib[n - 1] + markers[n - 1].1 * 2;
|
||||
let kmax = (0..n).max_by_key(|&i| markers[i].1).unwrap_or(0);
|
||||
let (vck, ick) = markers[kmax];
|
||||
let off_v: usize = markers.iter().take(kmax).map(|&(vc, _)| vc * decl.stride).sum();
|
||||
let off_v: usize = markers
|
||||
.iter()
|
||||
.take(kmax)
|
||||
.map(|&(vc, _)| vc * decl.stride)
|
||||
.sum();
|
||||
let mut best = (0usize, String::from("no pool start reached any gate"));
|
||||
for &vb0 in &starts {
|
||||
for pad in 0..=3usize {
|
||||
@@ -928,7 +946,9 @@ pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)>
|
||||
/// Diagnostic: the stride of every sub-mesh declaration of a named resource.
|
||||
/// A grouped pool may mix them (`n201_01` → 24, 24, 24, 28).
|
||||
pub fn debug_decl_strides(bytes: &[u8], name: &str) -> Vec<usize> {
|
||||
decls_of(bytes, name).map(|v| v.iter().map(|d| d.stride).collect()).unwrap_or_default()
|
||||
decls_of(bytes, name)
|
||||
.map(|v| v.iter().map(|d| d.stride).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Every sub-mesh declaration of a named resource (see [`all_vertex_decls`]).
|
||||
@@ -940,7 +960,9 @@ fn decls_of(bytes: &[u8], name: &str) -> Option<Vec<VertexDecl>> {
|
||||
let header = Xpr2Header::read(&mut cur).ok()?;
|
||||
const DIR_BASE: usize = 0x10;
|
||||
for _ in 0..header.num_resources {
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break };
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else {
|
||||
break;
|
||||
};
|
||||
if &e.type_tag != b"XBG7" {
|
||||
continue;
|
||||
}
|
||||
@@ -1074,14 +1096,20 @@ pub fn debug_find_index_buffer(bytes: &[u8], name: &str, vb: usize) -> Vec<(usiz
|
||||
/// value that captures the whole measured gain. `XBG7_EDGE_CAP` overrides it (see
|
||||
/// docs/re/structures/xbg7-mesh.md).
|
||||
fn edge_cap() -> f32 {
|
||||
std::env::var("XBG7_EDGE_CAP").ok().and_then(|v| v.parse().ok()).unwrap_or(1.0)
|
||||
std::env::var("XBG7_EDGE_CAP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// Winding floor for the grouped-pool **pivot** (default `0.85`). The single-block
|
||||
/// path settled at 0.70 on measurement; this is the same question for the pivot,
|
||||
/// and `XBG7_GROUPED_CONSISTENCY` sweeps it.
|
||||
fn grouped_consistency() -> f32 {
|
||||
std::env::var("XBG7_GROUPED_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.85)
|
||||
std::env::var("XBG7_GROUPED_CONSISTENCY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0.85)
|
||||
}
|
||||
|
||||
/// Coverage requirement, as `max_index + N >= vtx_count`. **`1` since
|
||||
@@ -1096,14 +1124,20 @@ fn grouped_consistency() -> f32 {
|
||||
/// disc-wide; capture oracle unchanged at 46/46. `XBG7_COVER_SLACK` overrides
|
||||
/// (note `0` rejects everything — the comparison is `max_idx + N >= vtx_count`).
|
||||
fn cover_slack() -> usize {
|
||||
std::env::var("XBG7_COVER_SLACK").ok().and_then(|v| v.parse().ok()).unwrap_or(1)
|
||||
std::env::var("XBG7_COVER_SLACK")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Smallest bounding-box extent a block may have (default `0.5`). An absolute
|
||||
/// floor on a format with no unit convention is a scale assumption, so it is a
|
||||
/// knob: `XBG7_MIN_EXTENT`.
|
||||
fn min_extent() -> f32 {
|
||||
std::env::var("XBG7_MIN_EXTENT").ok().and_then(|v| v.parse().ok()).unwrap_or(0.5)
|
||||
std::env::var("XBG7_MIN_EXTENT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0.5)
|
||||
}
|
||||
|
||||
/// Use a scale-free collinearity test for degeneracy instead of the absolute
|
||||
@@ -1136,7 +1170,10 @@ fn rel_degen() -> bool {
|
||||
/// perfect cross-container consistency for 442 resources — recorded rather than
|
||||
/// taken, since consistency is the weaker witness (see the docs).
|
||||
fn pad0_consistency() -> f32 {
|
||||
std::env::var("XBG7_PAD0_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.70)
|
||||
std::env::var("XBG7_PAD0_CONSISTENCY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0.70)
|
||||
}
|
||||
|
||||
/// Revert knob for the 2026-08-13 pad scoring: with `XBG7_PAD_FIRST_MATCH=1`,
|
||||
@@ -1159,22 +1196,32 @@ fn pad0_consistency() -> f32 {
|
||||
/// of the stage-05 mission rise **124 → 128** identical. See
|
||||
/// docs/re/structures/xbg7-mesh.md.
|
||||
fn submesh_decls() -> bool {
|
||||
std::env::var("XBG7_SUBMESH_DECLS").map(|v| v != "0").unwrap_or(true)
|
||||
std::env::var("XBG7_SUBMESH_DECLS")
|
||||
.map(|v| v != "0")
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn pad_first_match() -> bool {
|
||||
std::env::var("XBG7_PAD_FIRST_MATCH").map(|v| v == "1").unwrap_or(false)
|
||||
std::env::var("XBG7_PAD_FIRST_MATCH")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Triangle count below which the looser [`small_cap`] applies. `0` (default)
|
||||
/// disables the split, so the flat [`edge_cap`] governs every block.
|
||||
fn small_tris() -> usize {
|
||||
std::env::var("XBG7_SMALL_TRIS").ok().and_then(|v| v.parse().ok()).unwrap_or(0)
|
||||
std::env::var("XBG7_SMALL_TRIS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The connectivity cap for blocks below [`small_tris`] triangles.
|
||||
fn small_cap() -> f32 {
|
||||
std::env::var("XBG7_EDGE_CAP_SMALL").ok().and_then(|v| v.parse().ok()).unwrap_or(0.45)
|
||||
std::env::var("XBG7_EDGE_CAP_SMALL")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0.45)
|
||||
}
|
||||
|
||||
/// Internal: the descriptor parameters the diagnostics need.
|
||||
@@ -1186,7 +1233,9 @@ fn decl_of(bytes: &[u8], name: &str) -> Option<(VertexDecl, Vec<(usize, usize)>)
|
||||
let header = Xpr2Header::read(&mut cur).ok()?;
|
||||
const DIR_BASE: usize = 0x10;
|
||||
for _ in 0..header.num_resources {
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break };
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else {
|
||||
break;
|
||||
};
|
||||
if &e.type_tag != b"XBG7" {
|
||||
continue;
|
||||
}
|
||||
@@ -1216,7 +1265,9 @@ pub fn debug_resource_params(bytes: &[u8], name: &str) -> Option<(Vec<(usize, us
|
||||
let header = Xpr2Header::read(&mut cur).ok()?;
|
||||
const DIR_BASE: usize = 0x10;
|
||||
for _ in 0..header.num_resources {
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break };
|
||||
let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else {
|
||||
break;
|
||||
};
|
||||
if &e.type_tag != b"XBG7" {
|
||||
continue;
|
||||
}
|
||||
@@ -1427,7 +1478,14 @@ fn anchor_pool_mesh(
|
||||
// exactly ONE degenerate-free block in their container, sitting later
|
||||
// in file order than the lookalike we were taking.
|
||||
if degen == 0 || pad_first_match() {
|
||||
return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl));
|
||||
return Some(read_pool_mesh(
|
||||
bytes,
|
||||
vb - idx_bytes - pad,
|
||||
vb,
|
||||
index_count,
|
||||
vtx_count,
|
||||
decl,
|
||||
));
|
||||
}
|
||||
if dirty.is_none() {
|
||||
dirty = Some((vb, pad));
|
||||
@@ -1446,7 +1504,14 @@ fn anchor_pool_mesh(
|
||||
// Nothing clean anywhere: keep the first accepted block, so this can never
|
||||
// cost coverage relative to first-match.
|
||||
if let Some((vb, pad)) = dirty {
|
||||
return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl));
|
||||
return Some(read_pool_mesh(
|
||||
bytes,
|
||||
vb - idx_bytes - pad,
|
||||
vb,
|
||||
index_count,
|
||||
vtx_count,
|
||||
decl,
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -1631,7 +1696,11 @@ fn validate_block_report(
|
||||
// capture-proven false positive (`e106_eng_02_l`, ratio 0.417). Under
|
||||
// `XBG7_SMALL_TRIS` blocks below that triangle count get the looser
|
||||
// `XBG7_EDGE_CAP_SMALL` instead — a targeted relaxation, off by default.
|
||||
let cap = if tris < small_tris() { small_cap() } else { edge_cap() };
|
||||
let cap = if tris < small_tris() {
|
||||
small_cap()
|
||||
} else {
|
||||
edge_cap()
|
||||
};
|
||||
if mean_edge / diag > cap {
|
||||
return Err(format!(
|
||||
"connectivity: mean_edge/diag {:.3} > cap {cap:.2}",
|
||||
@@ -1737,7 +1806,9 @@ fn anchor_grouped_meshes(
|
||||
let (vc, ic) = markers[i];
|
||||
let ib = ib0 + rel_ib[i];
|
||||
if ib + ic * 2 > bytes.len()
|
||||
|| vc.checked_mul(decls[i].stride).is_none_or(|b| vb + b > bytes.len())
|
||||
|| vc
|
||||
.checked_mul(decls[i].stride)
|
||||
.is_none_or(|b| vb + b > bytes.len())
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -1793,7 +1864,16 @@ fn anchor_grouped_meshes(
|
||||
// collapses well below 0.85, so only the true pad/pivot passes.
|
||||
let ib_k = ib0 + rel_ib[kmax];
|
||||
let vb_k = vb0 + off_v[kmax];
|
||||
if !validate_block(bytes, ib_k, vb_k, vck, ick, &decls[kmax], grouped_consistency(), true) {
|
||||
if !validate_block(
|
||||
bytes,
|
||||
ib_k,
|
||||
vb_k,
|
||||
vck,
|
||||
ick,
|
||||
&decls[kmax],
|
||||
grouped_consistency(),
|
||||
true,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let (degen, wind) = index_run_quality(bytes, ib_k, vb_k, ick, &decls[kmax]);
|
||||
@@ -2642,16 +2722,28 @@ fn saber_measured(part: &str) -> Option<(M3, [f32; 3])> {
|
||||
Some(match part {
|
||||
// fin bdy_04 — V-tail, canted; mounted on the nacelle.
|
||||
"bdy_04" => (
|
||||
[[0.9563, -0.2924, 0.0], [0.2924, 0.9563, 0.0], [0.0, 0.0, 1.0]],
|
||||
[
|
||||
[0.9563, -0.2924, 0.0],
|
||||
[0.2924, 0.9563, 0.0],
|
||||
[0.0, 0.0, 1.0],
|
||||
],
|
||||
[-5.2350, 1.0679, -9.7182],
|
||||
),
|
||||
// flaps bdy_04_2 / bdy_04_3 — ride the fin (compound rotation).
|
||||
"bdy_04_2" => (
|
||||
[[0.9563, -0.2766, -0.0947], [0.2924, 0.9048, 0.3098], [-0.0001, -0.3240, 0.9461]],
|
||||
[
|
||||
[0.9563, -0.2766, -0.0947],
|
||||
[0.2924, 0.9048, 0.3098],
|
||||
[-0.0001, -0.3240, 0.9461],
|
||||
],
|
||||
[-6.1562, 3.3121, -10.5028],
|
||||
),
|
||||
"bdy_04_3" => (
|
||||
[[0.9563, -0.2766, -0.0947], [0.2924, 0.9048, 0.3098], [-0.0001, -0.3240, 0.9461]],
|
||||
[
|
||||
[0.9563, -0.2766, -0.0947],
|
||||
[0.2924, 0.9048, 0.3098],
|
||||
[-0.0001, -0.3240, 0.9461],
|
||||
],
|
||||
[-5.7261, 3.4436, -10.5026],
|
||||
),
|
||||
// winglet bdy_06 + tip bdy_07 — on the nacelle (no cant).
|
||||
@@ -2775,7 +2867,15 @@ pub fn node_transforms(bytes: &[u8], resource_name: &str) -> Vec<NodePlacement>
|
||||
.checked_sub(4)
|
||||
.filter(|&s| s > i && s + 4 <= head_end && &d[s..s + 4] == b"rou_");
|
||||
let part = name.strip_prefix(&prefix).unwrap_or(name).to_string();
|
||||
recs.push(Rec { name_start: i, part, vtx, t48, local_m, local_t, sib_end });
|
||||
recs.push(Rec {
|
||||
name_start: i,
|
||||
part,
|
||||
vtx,
|
||||
t48,
|
||||
local_m,
|
||||
local_t,
|
||||
sib_end,
|
||||
});
|
||||
i = j.max(i + 1);
|
||||
}
|
||||
|
||||
@@ -2796,7 +2896,10 @@ pub fn node_transforms(bytes: &[u8], resource_name: &str) -> Vec<NodePlacement>
|
||||
while stack.last().is_some_and(|s| s.0 <= rec.name_start) {
|
||||
stack.pop();
|
||||
}
|
||||
let (pm, pt) = stack.last().map(|s| (s.1, s.2)).unwrap_or((M3_ID, [0.0; 3]));
|
||||
let (pm, pt) = stack
|
||||
.last()
|
||||
.map(|s| (s.1, s.2))
|
||||
.unwrap_or((M3_ID, [0.0; 3]));
|
||||
// world = parent ∘ local
|
||||
let wm = m3_mul(pm, rec.local_m);
|
||||
let r = m3_vec(pm, rec.local_t);
|
||||
@@ -2950,7 +3053,14 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec<ScenePart> {
|
||||
let sib_end = sib_ptr.checked_sub(4).filter(|&s| {
|
||||
s > i && s + 4 <= head_end && (&d[s..s + 4] == b"rou_" || &d[s..s + 3] == b"GN_")
|
||||
});
|
||||
recs.push(Rec { name_start: i, name, local_m, local_t, local_s, sib_end });
|
||||
recs.push(Rec {
|
||||
name_start: i,
|
||||
name,
|
||||
local_m,
|
||||
local_t,
|
||||
local_s,
|
||||
sib_end,
|
||||
});
|
||||
i = j.max(i + 1);
|
||||
}
|
||||
|
||||
@@ -2961,11 +3071,19 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec<ScenePart> {
|
||||
while stack.last().is_some_and(|s| s.0 <= rec.name_start) {
|
||||
stack.pop();
|
||||
}
|
||||
let (pm, pt) = stack.last().map(|s| (s.1, s.2)).unwrap_or((M3_ID, [0.0; 3]));
|
||||
let (pm, pt) = stack
|
||||
.last()
|
||||
.map(|s| (s.1, s.2))
|
||||
.unwrap_or((M3_ID, [0.0; 3]));
|
||||
let wm = m3_mul(pm, rec.local_m);
|
||||
let r = m3_vec(pm, rec.local_t);
|
||||
let wt = [r[0] + pt[0], r[1] + pt[1], r[2] + pt[2]];
|
||||
out.push(ScenePart { resource: rec.name.clone(), m: wm, t: wt, s: rec.local_s });
|
||||
out.push(ScenePart {
|
||||
resource: rec.name.clone(),
|
||||
m: wm,
|
||||
t: wt,
|
||||
s: rec.local_s,
|
||||
});
|
||||
let end = rec
|
||||
.sib_end
|
||||
.unwrap_or_else(|| stack.last().map(|s| s.0).unwrap_or(head_end));
|
||||
|
||||
@@ -135,8 +135,12 @@ fn parse_records(bytes: &[u8]) -> Option<Vec<MovieEntry>> {
|
||||
ids.sort_unstable();
|
||||
let mut out = Vec::with_capacity(ids.len());
|
||||
for (_, slot) in ids {
|
||||
let Some(rec) = obj.record(slot) else { continue };
|
||||
let Some(movie) = rec.get("MOVIE") else { continue };
|
||||
let Some(rec) = obj.record(slot) else {
|
||||
continue;
|
||||
};
|
||||
let Some(movie) = rec.get("MOVIE") else {
|
||||
continue;
|
||||
};
|
||||
let (kind, mission, phase) = classify_slot(slot);
|
||||
out.push(MovieEntry {
|
||||
slot: slot.to_string(),
|
||||
@@ -162,16 +166,18 @@ fn parse_string_pool(bytes: &[u8]) -> Vec<MovieEntry> {
|
||||
let first_wmv = toks.iter().position(|t| t.ends_with(".wmv"));
|
||||
let key_start = toks.iter().position(|t| t == "LOGO1");
|
||||
let (slot_keys, value_toks): (Vec<&str>, &[String]) = match (key_start, first_wmv) {
|
||||
(Some(k), Some(w)) if k < w => (
|
||||
toks[k..w].iter().map(String::as_str).collect(),
|
||||
&toks[w..],
|
||||
),
|
||||
(Some(k), Some(w)) if k < w => {
|
||||
(toks[k..w].iter().map(String::as_str).collect(), &toks[w..])
|
||||
}
|
||||
_ => (Vec::new(), toks.as_slice()),
|
||||
};
|
||||
|
||||
// Build the value records (drop the schema-template field keys).
|
||||
let mut records: Vec<MovieEntry> = Vec::new();
|
||||
for t in value_toks.iter().filter(|t| !FIELD_KEYS.contains(&t.as_str())) {
|
||||
for t in value_toks
|
||||
.iter()
|
||||
.filter(|t| !FIELD_KEYS.contains(&t.as_str()))
|
||||
{
|
||||
if let Some(name) = t.strip_suffix(".wmv") {
|
||||
records.push(MovieEntry {
|
||||
slot: String::new(),
|
||||
@@ -198,10 +204,8 @@ fn parse_string_pool(bytes: &[u8]) -> Vec<MovieEntry> {
|
||||
|
||||
// Attach slot keys to records, recovering valueless-slot gaps via MS anchors.
|
||||
if !slot_keys.is_empty() {
|
||||
let ms_targets: std::collections::HashSet<String> = slot_keys
|
||||
.iter()
|
||||
.filter_map(|k| ms_target(k))
|
||||
.collect();
|
||||
let ms_targets: std::collections::HashSet<String> =
|
||||
slot_keys.iter().filter_map(|k| ms_target(k)).collect();
|
||||
let mut j = 0usize;
|
||||
for slot in &slot_keys {
|
||||
if j >= records.len() {
|
||||
@@ -419,13 +423,21 @@ mod tests {
|
||||
&[
|
||||
&["logo1.wmv", "MOVIE"],
|
||||
&["S01A.wmv", "eng.pak+SUBTITLE_S01A.tbl", "VOICE_S01A"],
|
||||
&["RT01A.wmv", "eng.pak+pwrt01.prt", "VOICE_RT01A", "VOICETRACK"],
|
||||
&[
|
||||
"RT01A.wmv",
|
||||
"eng.pak+pwrt01.prt",
|
||||
"VOICE_RT01A",
|
||||
"VOICETRACK",
|
||||
],
|
||||
&["RT01C_2.wmv", "VOICE_RT01C_2"],
|
||||
],
|
||||
);
|
||||
let m = parse(&blob);
|
||||
assert_eq!(m.len(), 4);
|
||||
assert_eq!((m[0].slot.as_str(), m[0].kind), ("LOGO1", MovieKind::System));
|
||||
assert_eq!(
|
||||
(m[0].slot.as_str(), m[0].kind),
|
||||
("LOGO1", MovieKind::System)
|
||||
);
|
||||
assert_eq!(
|
||||
(m[1].slot.as_str(), m[1].kind, m[1].mission),
|
||||
("MS01A", MovieKind::Intro, Some(1))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user