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 was merged in pull request #18.
This commit is contained in:
@@ -305,7 +305,7 @@ async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("sylpheed=info".parse().unwrap())
|
||||
.add_directive("sylpheed=info".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
@@ -313,24 +313,33 @@ async fn main() -> Result<()> {
|
||||
|
||||
match cli.command {
|
||||
Commands::Extract { iso, output } => cmd_extract(&iso, &output).await,
|
||||
Commands::List { iso, filter } => cmd_list(&iso, filter).await,
|
||||
Commands::List { iso, filter } => cmd_list(&iso, filter).await,
|
||||
Commands::Sniff { dir, unknown_only } => cmd_sniff(&dir, unknown_only),
|
||||
Commands::Texture { cmd } => match cmd {
|
||||
TextureCommands::Info { file } => cmd_texture_info(&file),
|
||||
TextureCommands::Info { file } => cmd_texture_info(&file),
|
||||
TextureCommands::Export { file, output } => cmd_texture_export(&file, &output),
|
||||
},
|
||||
Commands::Mesh { cmd } => match cmd {
|
||||
MeshCommands::Info { file } => cmd_mesh_info(&file),
|
||||
MeshCommands::Render { file, output, size, yaw, pitch, dist, row, only } => {
|
||||
cmd_mesh_render(&file, &output, size, yaw, pitch, dist, row, only)
|
||||
}
|
||||
MeshCommands::Render {
|
||||
file,
|
||||
output,
|
||||
size,
|
||||
yaw,
|
||||
pitch,
|
||||
dist,
|
||||
row,
|
||||
only,
|
||||
} => cmd_mesh_render(&file, &output, size, yaw, pitch, dist, row, only),
|
||||
},
|
||||
Commands::Pak { cmd } => match cmd {
|
||||
PakCommands::List { pak, idxd_only } => cmd_pak_list(&pak, idxd_only),
|
||||
PakCommands::Dump { pak, hash } => cmd_pak_dump(&pak, &hash),
|
||||
PakCommands::Textures { pak, output, verbose } => {
|
||||
cmd_pak_textures(&pak, &output, verbose)
|
||||
}
|
||||
PakCommands::List { pak, idxd_only } => cmd_pak_list(&pak, idxd_only),
|
||||
PakCommands::Dump { pak, hash } => cmd_pak_dump(&pak, &hash),
|
||||
PakCommands::Textures {
|
||||
pak,
|
||||
output,
|
||||
verbose,
|
||||
} => cmd_pak_textures(&pak, &output, verbose),
|
||||
},
|
||||
Commands::Audio { cmd } => match cmd {
|
||||
AudioCommands::Info { file } => cmd_audio_info(&file),
|
||||
@@ -481,7 +490,9 @@ fn cmd_screen_info(pak: &Path, want: Option<usize>, geometry: bool, all: bool) -
|
||||
"{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} {rest}",
|
||||
el.index,
|
||||
el.name,
|
||||
el.parent.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
|
||||
el.parent
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or_else(|| "-".into()),
|
||||
format!("{:#x}", el.kind),
|
||||
format!("({},{})", el.pivot_x, el.pivot_y),
|
||||
el.keyframes.len(),
|
||||
@@ -623,7 +634,10 @@ fn cmd_screen_render(
|
||||
screen.height
|
||||
);
|
||||
if !screen.missing.is_empty() {
|
||||
println!(" sprites that did not resolve/decode: {:?}", screen.missing);
|
||||
println!(
|
||||
" sprites that did not resolve/decode: {:?}",
|
||||
screen.missing
|
||||
);
|
||||
}
|
||||
let undrawn: Vec<&str> = b
|
||||
.elements
|
||||
@@ -640,9 +654,7 @@ fn cmd_screen_render(
|
||||
// ── save file ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn cmd_save_info(file: &Path, all: bool) -> Result<()> {
|
||||
use sylpheed_formats::savegame::{
|
||||
self, Confidence, DevelopState, FieldKind, GHAD_LAYOUT,
|
||||
};
|
||||
use sylpheed_formats::savegame::{self, Confidence, DevelopState, FieldKind, GHAD_LAYOUT};
|
||||
let raw = std::fs::read(file).context("read save")?;
|
||||
let save = savegame::parse(&raw).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
@@ -692,7 +704,11 @@ fn cmd_save_info(file: &Path, all: bool) -> Result<()> {
|
||||
" {} +{:<3} {:<14} {}",
|
||||
mark(f.confidence),
|
||||
f.offset,
|
||||
if f.name.is_empty() { "(unnamed)" } else { f.name },
|
||||
if f.name.is_empty() {
|
||||
"(unnamed)"
|
||||
} else {
|
||||
f.name
|
||||
},
|
||||
value
|
||||
);
|
||||
if all && !f.note.is_empty() {
|
||||
@@ -701,7 +717,10 @@ fn cmd_save_info(file: &Path, all: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
let dev = save.develop_state();
|
||||
let owned = dev.iter().filter(|d| **d == DevelopState::Developed).count();
|
||||
let owned = dev
|
||||
.iter()
|
||||
.filter(|d| **d == DevelopState::Developed)
|
||||
.count();
|
||||
let ready = dev
|
||||
.iter()
|
||||
.filter(|d| **d == DevelopState::Developable)
|
||||
@@ -745,16 +764,28 @@ fn cmd_audio_info(file: &Path) -> Result<()> {
|
||||
println!("{} {}", "Audio:".green().bold(), file.display());
|
||||
println!(" Codec : {}", info.codec.label().yellow());
|
||||
let opt = |v: Option<String>| v.unwrap_or_else(|| "—".dimmed().to_string());
|
||||
println!(" Channels : {}", opt(info.channels.map(|c| c.to_string())));
|
||||
println!(" Sample rate: {}", opt(info.sample_rate.map(|r| format!("{r} Hz"))));
|
||||
println!(" Bit depth : {}", opt(info.bits_per_sample.map(|b| format!("{b}-bit"))));
|
||||
println!(
|
||||
" Channels : {}",
|
||||
opt(info.channels.map(|c| c.to_string()))
|
||||
);
|
||||
println!(
|
||||
" Sample rate: {}",
|
||||
opt(info.sample_rate.map(|r| format!("{r} Hz")))
|
||||
);
|
||||
println!(
|
||||
" Bit depth : {}",
|
||||
opt(info.bits_per_sample.map(|b| format!("{b}-bit")))
|
||||
);
|
||||
if let Some(d) = info.duration_secs {
|
||||
println!(" Duration : {d:.2} s");
|
||||
}
|
||||
if let Some(p) = info.xma_packets {
|
||||
println!(" XMA packets: {} (2048 B each)", p.to_string().yellow());
|
||||
}
|
||||
println!(" Size : {} bytes", info.size_bytes.to_string().yellow());
|
||||
println!(
|
||||
" Size : {} bytes",
|
||||
info.size_bytes.to_string().yellow()
|
||||
);
|
||||
if info.codec.needs_decoder() {
|
||||
println!(
|
||||
" {} decode not supported (needs an XMA2 decoder + the sound-bank descriptor)",
|
||||
@@ -787,7 +818,7 @@ async fn cmd_extract(iso_path: &Path, output_dir: &Path) -> Result<()> {
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.cyan} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("█▉▊▋▌▍▎▏ ")
|
||||
.progress_chars("█▉▊▋▌▍▎▏ "),
|
||||
);
|
||||
|
||||
let stats = reader.extract_all(output_dir).await?;
|
||||
@@ -816,7 +847,11 @@ async fn cmd_extract(iso_path: &Path, output_dir: &Path) -> Result<()> {
|
||||
// ── list ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_list(iso_path: &Path, filter: Option<String>) -> Result<()> {
|
||||
println!("{} {}", "Listing".green().bold(), iso_path.display().to_string().cyan());
|
||||
println!(
|
||||
"{} {}",
|
||||
"Listing".green().bold(),
|
||||
iso_path.display().to_string().cyan()
|
||||
);
|
||||
|
||||
let mut reader = sylpheed_formats::xiso::open_iso(iso_path).await?;
|
||||
let files = reader.list_all_files().await?;
|
||||
@@ -861,7 +896,9 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||
|
||||
for file in &files {
|
||||
let Ok(bytes) = assets.read(file) else { continue; };
|
||||
let Ok(bytes) = assets.read(file) else {
|
||||
continue;
|
||||
};
|
||||
let fmt = identify_format(&bytes);
|
||||
let label = fmt.extension_hint();
|
||||
*counts.entry(label).or_insert(0) += 1;
|
||||
@@ -882,9 +919,10 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
let hex_preview = if label == "bin" && bytes.len() >= 8 {
|
||||
format!(
|
||||
" {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}",
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7]
|
||||
).dimmed().to_string()
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]
|
||||
)
|
||||
.dimmed()
|
||||
.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -898,11 +936,7 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
let mut summary: Vec<_> = counts.into_iter().collect();
|
||||
summary.sort_by_key(|&(_, count)| std::cmp::Reverse(count));
|
||||
for (fmt, count) in summary {
|
||||
println!(
|
||||
" {:>6} .{}",
|
||||
count.to_string().yellow(),
|
||||
fmt
|
||||
);
|
||||
println!(" {:>6} .{}", count.to_string().yellow(), fmt);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -911,8 +945,7 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
// ── texture info ──────────────────────────────────────────────────────────
|
||||
|
||||
fn cmd_texture_info(file: &Path) -> Result<()> {
|
||||
let bytes = std::fs::read(file)
|
||||
.with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
|
||||
use sylpheed_formats::texture::X360Texture;
|
||||
let tex = X360Texture::from_xpr2(&bytes)
|
||||
@@ -920,16 +953,27 @@ fn cmd_texture_info(file: &Path) -> Result<()> {
|
||||
|
||||
let d = tex.format.desc();
|
||||
println!("{} {}", "Texture:".green().bold(), file.display());
|
||||
println!(" Resolution : {}×{}", tex.width.to_string().yellow(), tex.height.to_string().yellow());
|
||||
println!(
|
||||
" Resolution : {}×{}",
|
||||
tex.width.to_string().yellow(),
|
||||
tex.height.to_string().yellow()
|
||||
);
|
||||
println!(
|
||||
" Format : {} ({:?}) · {} bpp, {}",
|
||||
tex.format.gpu_name().yellow(),
|
||||
tex.format,
|
||||
d.bpp,
|
||||
if d.compressed { "compressed" } else { "uncompressed" },
|
||||
if d.compressed {
|
||||
"compressed"
|
||||
} else {
|
||||
"uncompressed"
|
||||
},
|
||||
);
|
||||
println!(" Mip levels : {}", tex.mip_levels);
|
||||
println!(" Data size : {} bytes", tex.data.len().to_string().yellow());
|
||||
println!(
|
||||
" Data size : {} bytes",
|
||||
tex.data.len().to_string().yellow()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -937,13 +981,12 @@ fn cmd_texture_info(file: &Path) -> Result<()> {
|
||||
// ── texture export ────────────────────────────────────────────────────────
|
||||
|
||||
fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> {
|
||||
let bytes = std::fs::read(file)
|
||||
.with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
|
||||
use sylpheed_formats::texture::X360Texture;
|
||||
let tex = X360Texture::from_xpr2(&bytes)?;
|
||||
let rgba = decode_to_rgba8(&tex)
|
||||
.with_context(|| format!("decoding {:?} texture", tex.format))?;
|
||||
let rgba =
|
||||
decode_to_rgba8(&tex).with_context(|| format!("decoding {:?} texture", tex.format))?;
|
||||
|
||||
image::save_buffer(
|
||||
output,
|
||||
@@ -960,7 +1003,11 @@ fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> {
|
||||
tex.width,
|
||||
tex.height,
|
||||
tex.format,
|
||||
if tex.is_cubemap { " (cubemap face 0)" } else { "" },
|
||||
if tex.is_cubemap {
|
||||
" (cubemap face 0)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
output.display().to_string().cyan(),
|
||||
);
|
||||
Ok(())
|
||||
@@ -1066,7 +1113,11 @@ fn cmd_mesh_info(file: &Path) -> Result<()> {
|
||||
.map(|t| edge(t[0], t[1]).max(edge(t[1], t[2])).max(edge(t[0], t[2])))
|
||||
.collect();
|
||||
maxedges.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let median = maxedges.get(maxedges.len() / 2).copied().unwrap_or(1.0).max(1e-6);
|
||||
let median = maxedges
|
||||
.get(maxedges.len() / 2)
|
||||
.copied()
|
||||
.unwrap_or(1.0)
|
||||
.max(1e-6);
|
||||
let spanning = maxedges.iter().filter(|&&e| e > 6.0 * median).count();
|
||||
println!(
|
||||
" sub{si}: {nv} v, {} tris | degenerate {degen}, unref-verts {unref}, spanning>6×med {spanning}, idx_max {imax}/{}{}",
|
||||
@@ -1186,10 +1237,16 @@ fn cmd_mesh_render(
|
||||
(lo[2] + hi[2]) * 0.5,
|
||||
];
|
||||
let (scale, cell) = if multi {
|
||||
let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]).max(1e-3);
|
||||
let extent = (hi[0] - lo[0])
|
||||
.max(hi[1] - lo[1])
|
||||
.max(hi[2] - lo[2])
|
||||
.max(1e-3);
|
||||
let col = i % cols;
|
||||
let row = i / cols;
|
||||
(CELL / extent, [col as f32 * grid_pitch, -(row as f32) * grid_pitch, 0.0])
|
||||
(
|
||||
CELL / extent,
|
||||
[col as f32 * grid_pitch, -(row as f32) * grid_pitch, 0.0],
|
||||
)
|
||||
} else {
|
||||
(1.0, [0.0, 0.0, 0.0])
|
||||
};
|
||||
@@ -1255,7 +1312,9 @@ fn cmd_mesh_render(
|
||||
};
|
||||
for place in &mine {
|
||||
let f = |i: usize| {
|
||||
let p = place.map(|pl| pl.apply(sub.positions[i])).unwrap_or(sub.positions[i]);
|
||||
let p = place
|
||||
.map(|pl| pl.apply(sub.positions[i]))
|
||||
.unwrap_or(sub.positions[i]);
|
||||
[
|
||||
(p[0] - center[0]) * scale * mirror[0] + cell[0],
|
||||
(p[1] - center[1]) * scale * mirror[1] + cell[1],
|
||||
@@ -1646,8 +1705,10 @@ fn emit_t8ad(
|
||||
}
|
||||
match t8ad::parse(slice) {
|
||||
Some(img) => {
|
||||
let out =
|
||||
output.join(format!("{hash:08x}_{stem}_{}x{}.png", img.width, img.height));
|
||||
let out = output.join(format!(
|
||||
"{hash:08x}_{stem}_{}x{}.png",
|
||||
img.width, img.height
|
||||
));
|
||||
image::save_buffer(
|
||||
&out,
|
||||
&img.rgba,
|
||||
@@ -1669,8 +1730,7 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> {
|
||||
use sylpheed_formats::{lsta, ratc, t8ad};
|
||||
|
||||
let arc = PakArchive::open(pak).with_context(|| format!("opening {}", pak.display()))?;
|
||||
std::fs::create_dir_all(output)
|
||||
.with_context(|| format!("creating {}", output.display()))?;
|
||||
std::fs::create_dir_all(output).with_context(|| format!("creating {}", output.display()))?;
|
||||
|
||||
println!(
|
||||
"{} {} → {}",
|
||||
@@ -1738,7 +1798,14 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> {
|
||||
} else {
|
||||
safe_name(&child.name)
|
||||
};
|
||||
emit_t8ad(&payload[child.offset..end], hash, &stem, output, verbose, &mut stats)?;
|
||||
emit_t8ad(
|
||||
&payload[child.offset..end],
|
||||
hash,
|
||||
&stem,
|
||||
output,
|
||||
verbose,
|
||||
&mut stats,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -19,22 +19,42 @@ fn main() {
|
||||
let w = std::env::temp_dir().join(format!("bk_{i}.wav"));
|
||||
let _ = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
|
||||
.arg(&p).arg(&w).output();
|
||||
.arg(&p)
|
||||
.arg(&w)
|
||||
.output();
|
||||
let out = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-v", "info", "-i"])
|
||||
.arg(&w)
|
||||
.args(["-af", "astats=measure_perchannel=none", "-f", "null", "-"])
|
||||
.output().unwrap();
|
||||
.output()
|
||||
.unwrap();
|
||||
let t = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
let get = |k: &str| t.lines().find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
|
||||
.unwrap_or_else(|| "?".into());
|
||||
let get = |k: &str| {
|
||||
t.lines()
|
||||
.find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
|
||||
.unwrap_or_else(|| "?".into())
|
||||
};
|
||||
let dur = Command::new("ffprobe")
|
||||
.args(["-v","error","-show_entries","format=duration","-of","csv=p=0"])
|
||||
.arg(&w).output().ok()
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(&w)
|
||||
.output()
|
||||
.ok()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_default();
|
||||
println!(" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
|
||||
r.len(), dur, get("Peak level dB:"), get("RMS level dB:"));
|
||||
println!(
|
||||
" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
|
||||
r.len(),
|
||||
dur,
|
||||
get("Peak level dB:"),
|
||||
get("RMS level dB:")
|
||||
);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
let _ = std::fs::remove_file(&w);
|
||||
}
|
||||
|
||||
@@ -24,13 +24,19 @@ fn main() {
|
||||
let (mut found, mut matches) = (0usize, Vec::new());
|
||||
for n in 0..=199u32 {
|
||||
let name = format!("BGM_{n:03}.slb");
|
||||
let Ok(riffs) = media::sound_bank_riffs(&src, &name) else { continue };
|
||||
if riffs.is_empty() { continue }
|
||||
let Ok(riffs) = media::sound_bank_riffs(&src, &name) else {
|
||||
continue;
|
||||
};
|
||||
if riffs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
found += 1;
|
||||
let sizes: Vec<usize> = riffs.iter().map(|r| r.len()).collect();
|
||||
// Compare on the DATA payload the port sums, not on the RIFF wrapper:
|
||||
// a wrapper differs by header bytes and would hide a real collision.
|
||||
let near = sizes.iter().any(|s| WANT.iter().any(|w| s.abs_diff(*w) < 4096));
|
||||
let near = sizes
|
||||
.iter()
|
||||
.any(|s| WANT.iter().any(|w| s.abs_diff(*w) < 4096));
|
||||
if near {
|
||||
matches.push((name.clone(), sizes.clone()));
|
||||
}
|
||||
@@ -39,7 +45,10 @@ fn main() {
|
||||
for (n, s) in &matches {
|
||||
println!(" {n:<14} wave sizes {s:?}");
|
||||
}
|
||||
println!("\n {} bank(s) carry a wave within 4 KiB of {WANT:?}", matches.len());
|
||||
println!(
|
||||
"\n {} bank(s) carry a wave within 4 KiB of {WANT:?}",
|
||||
matches.len()
|
||||
);
|
||||
println!(" Exactly 1 means the census EXCLUDES alternatives and is a real third");
|
||||
println!(" leg. More than 1 means the byte match does not distinguish BGM_103,");
|
||||
println!(" and \"three legs\" is two. Zero means this reader cannot see the");
|
||||
|
||||
@@ -16,27 +16,39 @@
|
||||
//!
|
||||
//! This prints what the differences actually look like, so the reading is judged
|
||||
//! against the names rather than accepted as plausible.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
use std::collections::BTreeSet;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let ar = pak::PakArchive::open(format!("{root}/dat/GP_DIALOG.pak")).expect("GP_DIALOG.pak");
|
||||
let sets: Vec<Option<BTreeSet<String>>> = ar.entries().iter().map(|e| {
|
||||
let by = ar.read(e).ok()?;
|
||||
if !ratc::is_ratc(&by) { return None }
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|el| el.name.clone()).collect())
|
||||
}).collect();
|
||||
let sets: Vec<Option<BTreeSet<String>>> = ar
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let by = ar.read(e).ok()?;
|
||||
if !ratc::is_ratc(&by) {
|
||||
return None;
|
||||
}
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|el| el.name.clone()).collect())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let (mut same, mut diff, mut pairs) = (0usize, 0usize, 0usize);
|
||||
let mut shown = 0;
|
||||
for i in (0..sets.len().saturating_sub(1)).step_by(2) {
|
||||
let (Some(a), Some(b)) = (&sets[i], &sets[i + 1]) else { continue };
|
||||
let (Some(a), Some(b)) = (&sets[i], &sets[i + 1]) else {
|
||||
continue;
|
||||
};
|
||||
pairs += 1;
|
||||
if a == b {
|
||||
same += 1;
|
||||
println!(" entries {i:>3}/{:<3} IDENTICAL sets, {} element(s)", i + 1, a.len());
|
||||
println!(
|
||||
" entries {i:>3}/{:<3} IDENTICAL sets, {} element(s)",
|
||||
i + 1,
|
||||
a.len()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
diff += 1;
|
||||
@@ -49,19 +61,37 @@ fn main() {
|
||||
if (10..=15).contains(&i) {
|
||||
let sp = |x: &BTreeSet<String>| x.iter().filter(|n| n.ends_with(".t32")).count();
|
||||
let stage = |x: &BTreeSet<String>| -> Vec<String> {
|
||||
let mut v: Vec<String> = x.iter().filter_map(|n| n.strip_prefix("pzstg")
|
||||
.and_then(|r| r.get(..2)).map(|s| s.to_string())).collect();
|
||||
v.sort(); v.dedup(); v
|
||||
let mut v: Vec<String> = x
|
||||
.iter()
|
||||
.filter_map(|n| {
|
||||
n.strip_prefix("pzstg")
|
||||
.and_then(|r| r.get(..2))
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect();
|
||||
v.sort();
|
||||
v.dedup();
|
||||
v
|
||||
};
|
||||
println!(" entries {i:>3}/{:<3} stages {:?} vs {:?} sprites {} vs {}",
|
||||
i + 1, stage(a), stage(b), sp(a), sp(b));
|
||||
println!(
|
||||
" entries {i:>3}/{:<3} stages {:?} vs {:?} sprites {} vs {}",
|
||||
i + 1,
|
||||
stage(a),
|
||||
stage(b),
|
||||
sp(a),
|
||||
sp(b)
|
||||
);
|
||||
}
|
||||
if shown < 3 {
|
||||
shown += 1;
|
||||
let only_a: Vec<_> = a.difference(b).cloned().collect();
|
||||
let only_b: Vec<_> = b.difference(a).cloned().collect();
|
||||
println!(" entries {i:>3}/{:<3} differ: {} only-in-first, {} only-in-second",
|
||||
i + 1, only_a.len(), only_b.len());
|
||||
println!(
|
||||
" entries {i:>3}/{:<3} differ: {} only-in-first, {} only-in-second",
|
||||
i + 1,
|
||||
only_a.len(),
|
||||
only_b.len()
|
||||
);
|
||||
println!(" first : {:?}", &only_a[..only_a.len().min(4)]);
|
||||
println!(" second : {:?}", &only_b[..only_b.len().min(4)]);
|
||||
}
|
||||
@@ -71,12 +101,17 @@ fn main() {
|
||||
// different dialogs and the whole adjacent-pairing premise is wrong -- which
|
||||
// is a stronger statement than "the language reading is untested".
|
||||
let btns = |s: &Option<BTreeSet<String>>| -> usize {
|
||||
s.as_ref().map_or(0, |x| x.iter().filter(|n| n.contains("btn")).count())
|
||||
s.as_ref()
|
||||
.map_or(0, |x| x.iter().filter(|n| n.contains("btn")).count())
|
||||
};
|
||||
let mut mismatched = 0;
|
||||
for i in (0..sets.len().saturating_sub(1)).step_by(2) {
|
||||
if sets[i].is_none() || sets[i + 1].is_none() { continue }
|
||||
if btns(&sets[i]) != btns(&sets[i + 1]) { mismatched += 1 }
|
||||
if sets[i].is_none() || sets[i + 1].is_none() {
|
||||
continue;
|
||||
}
|
||||
if btns(&sets[i]) != btns(&sets[i + 1]) {
|
||||
mismatched += 1
|
||||
}
|
||||
}
|
||||
println!("\n adjacent pairs whose BUTTON COUNTS differ: {mismatched}");
|
||||
println!(" A language pair cannot. Every one of these is two different dialogs.");
|
||||
|
||||
@@ -26,40 +26,66 @@ fn main() {
|
||||
// with a different reader, because its whole content is an absence.
|
||||
const WANT: [i32; 4] = [259, 329, 399, 469];
|
||||
const TOL: i32 = 6;
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
|
||||
.flatten().map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut hits, mut scanned) = (0usize, 0usize);
|
||||
for path in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(path) else { continue };
|
||||
let arch = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
scanned += 1;
|
||||
// Any button-shaped record, not just `pcbtn`: a rival need not share the
|
||||
// naming convention, and restricting by name would answer a narrower
|
||||
// question than the one asked.
|
||||
let mut rows: Vec<(String, i32)> = b.elements.iter()
|
||||
.filter(|el| el.name.contains("btn"))
|
||||
.filter_map(|el| el.rest().map(|r| (el.name.clone(), r.y)))
|
||||
.collect();
|
||||
if rows.is_empty() { continue }
|
||||
rows.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
|
||||
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
if rows.len() == 4 && ys.iter().zip(WANT.iter()).all(|(a, b)| (a - b).abs() <= TOL) {
|
||||
hits += 1;
|
||||
println!(" {arch} entry {i:>2} {} record(s): {}", rows.len(),
|
||||
rows.iter().map(|r| r.0.as_str()).collect::<Vec<_>>().join(" "));
|
||||
println!(" rows {ys:?} gaps {gaps:?}");
|
||||
let Ok(ar) = pak::PakArchive::open(path) else {
|
||||
continue;
|
||||
};
|
||||
let arch = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
scanned += 1;
|
||||
// Any button-shaped record, not just `pcbtn`: a rival need not share the
|
||||
// naming convention, and restricting by name would answer a narrower
|
||||
// question than the one asked.
|
||||
let mut rows: Vec<(String, i32)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.name.contains("btn"))
|
||||
.filter_map(|el| el.rest().map(|r| (el.name.clone(), r.y)))
|
||||
.collect();
|
||||
if rows.is_empty() {
|
||||
continue;
|
||||
}
|
||||
rows.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
|
||||
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
if rows.len() == 4
|
||||
&& ys
|
||||
.iter()
|
||||
.zip(WANT.iter())
|
||||
.all(|(a, b)| (a - b).abs() <= TOL)
|
||||
{
|
||||
hits += 1;
|
||||
println!(
|
||||
" {arch} entry {i:>2} {} record(s): {}",
|
||||
rows.len(),
|
||||
rows.iter()
|
||||
.map(|r| r.0.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
println!(" rows {ys:?} gaps {gaps:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n {scanned} build(s) scanned across {} pak(s); {hits} match the",
|
||||
paks.len());
|
||||
println!(
|
||||
"\n {scanned} build(s) scanned across {} pak(s); {hits} match the",
|
||||
paks.len()
|
||||
);
|
||||
println!(" DIFFICULTY row signature within +/-{TOL} px.");
|
||||
println!(" Expected: exactly 2 -- the EN/JP pair. More means a RIVAL exists and");
|
||||
println!(" the geometric identification is not unique; fewer means this reader");
|
||||
|
||||
@@ -13,26 +13,43 @@ fn main() {
|
||||
let e = &ar.entries()[4]; // entry 4 = the English title
|
||||
let bundle = ar.read(e).expect("read");
|
||||
let b = ui_layout::parse_build(&bundle).expect("parse");
|
||||
println!("build has {} elements, {} records", b.elements.len(), b.records.len());
|
||||
println!(
|
||||
"build has {} elements, {} records",
|
||||
b.elements.len(),
|
||||
b.records.len()
|
||||
);
|
||||
let mut names: Vec<&String> = b.records.keys().collect();
|
||||
names.sort();
|
||||
println!("records: {names:?}");
|
||||
for el in &b.elements {
|
||||
if !el.name.starts_with("ptloop") { continue; }
|
||||
if !el.name.starts_with("ptloop") {
|
||||
continue;
|
||||
}
|
||||
let r = el.rest();
|
||||
println!("\nPARENT {} sprite={:?} -> rest scale {:?} rot {:?}", el.name, el.sprite,
|
||||
r.map(|r| (r.scale_x, r.scale_y)), r.map(|r| r.rotation_deg));
|
||||
println!(
|
||||
"\nPARENT {} sprite={:?} -> rest scale {:?} rot {:?}",
|
||||
el.name,
|
||||
el.sprite,
|
||||
r.map(|r| (r.scale_x, r.scale_y)),
|
||||
r.map(|r| r.rotation_deg)
|
||||
);
|
||||
if let Some(&(off, size)) = b.records.get(&el.name) {
|
||||
match ui_layout::parse_build(&bundle[off..off + size]) {
|
||||
Some(leaf) => {
|
||||
println!(" LEAF {} parses: {} element(s)", el.name, leaf.elements.len());
|
||||
println!(
|
||||
" LEAF {} parses: {} element(s)",
|
||||
el.name,
|
||||
leaf.elements.len()
|
||||
);
|
||||
for le in &leaf.elements {
|
||||
let lr = le.rest();
|
||||
println!(" {:<20} rest scale {:?} rot {:?} pos {:?}",
|
||||
println!(
|
||||
" {:<20} rest scale {:?} rot {:?} pos {:?}",
|
||||
le.name,
|
||||
lr.map(|r| (r.scale_x, r.scale_y)),
|
||||
lr.map(|r| r.rotation_deg),
|
||||
lr.map(|r| (r.x, r.y)));
|
||||
lr.map(|r| (r.x, r.y))
|
||||
);
|
||||
for k in &le.keyframes {
|
||||
println!(" t={:?} scale=({},{}) rot={} pos=({},{}) fade={:#010x} u4={} u8={}",
|
||||
k.time, k.scale_x, k.scale_y, k.rotation_deg, k.x, k.y,
|
||||
|
||||
@@ -26,15 +26,14 @@
|
||||
//! and adds the one they could not run: the same two, restricted to the records
|
||||
//! **this port actually animates**. A disc-wide 0.00 % violation rate says
|
||||
//! nothing about my six screens if all six sit in the exceptional tail.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
/// The records the port animates: the plate glow, the five menu focus records,
|
||||
/// and the title's two sweeps. Named rather than pattern-matched, because the
|
||||
/// point is to check the ones that are shipped, not the ones that match a glob.
|
||||
const SHIPPED: &[&str] = &[
|
||||
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f",
|
||||
"ptloop01", "ptloop02",
|
||||
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f", "ptloop01", "ptloop02",
|
||||
];
|
||||
|
||||
/// Which header word to read as the loop length. `0x08` is the decoded one;
|
||||
@@ -44,14 +43,18 @@ const SHIPPED: &[&str] = &[
|
||||
static mut OFFSET: usize = 8;
|
||||
|
||||
fn main() {
|
||||
let off: usize = std::env::args().find_map(|a| a.strip_prefix("--offset=")
|
||||
.and_then(|v| v.parse().ok())).unwrap_or(8);
|
||||
let off: usize = std::env::args()
|
||||
.find_map(|a| a.strip_prefix("--offset=").and_then(|v| v.parse().ok()))
|
||||
.unwrap_or(8);
|
||||
unsafe { OFFSET = off };
|
||||
println!(" reading the loop length at header +0x{off:02x}");
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
|
||||
.flatten().map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let (mut total, mut exact, mut holds, mut violations) = (0usize, 0usize, 0usize, 0usize);
|
||||
@@ -59,14 +62,24 @@ fn main() {
|
||||
let mut shipped: BTreeMap<String, (i64, i64)> = BTreeMap::new();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else { continue };
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (rn, &(o, s)) in &b.records {
|
||||
if o + off + 4 > by.len() || o + s > by.len() { continue }
|
||||
if &by[o..o + 4] != b"RATC" { continue }
|
||||
if o + off + 4 > by.len() || o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
if &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
// 🔴 THE FALSIFIER IS RUN AT NEIGHBOURING OFFSETS TOO. The
|
||||
// Decoder's struct-layout control showed that a homogeneous
|
||||
// repeated table type-checks at every field boundary, so an
|
||||
@@ -77,15 +90,28 @@ fn main() {
|
||||
// "confirmation" without asking whether it discriminates the
|
||||
// OFFSET or merely the file.
|
||||
let len = u32::from_be_bytes(by[o + off..o + off + 4].try_into().unwrap()) as i64;
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
|
||||
let maxt = lb.elements.iter()
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max().unwrap_or(0) as i64;
|
||||
if maxt == 0 { continue } // static: declares no cycle at all
|
||||
.max()
|
||||
.unwrap_or(0) as i64;
|
||||
if maxt == 0 {
|
||||
continue;
|
||||
} // static: declares no cycle at all
|
||||
total += 1;
|
||||
let slack = len - maxt;
|
||||
*slack_hist.entry(slack).or_default() += 1;
|
||||
if slack == 0 { exact += 1 } else if slack > 0 { holds += 1 } else { violations += 1 }
|
||||
if slack == 0 {
|
||||
exact += 1
|
||||
} else if slack > 0 {
|
||||
holds += 1
|
||||
} else {
|
||||
violations += 1
|
||||
}
|
||||
let stem = rn.trim_end_matches(".rat");
|
||||
if SHIPPED.contains(&stem) {
|
||||
shipped.entry(stem.to_string()).or_insert((len, maxt));
|
||||
@@ -95,36 +121,68 @@ fn main() {
|
||||
}
|
||||
|
||||
println!("disc-wide, records with timed keyframes: {total}");
|
||||
println!(" +08 == max t (exact) : {exact:5} {:5.1} %", pc(exact, total));
|
||||
println!(" +08 > max t (a hold) : {holds:5} {:5.1} %", pc(holds, total));
|
||||
println!(" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %", pc(violations, total));
|
||||
println!(
|
||||
" +08 == max t (exact) : {exact:5} {:5.1} %",
|
||||
pc(exact, total)
|
||||
);
|
||||
println!(
|
||||
" +08 > max t (a hold) : {holds:5} {:5.1} %",
|
||||
pc(holds, total)
|
||||
);
|
||||
println!(
|
||||
" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %",
|
||||
pc(violations, total)
|
||||
);
|
||||
println!("\nslack distribution, most common first:");
|
||||
let mut h: Vec<_> = slack_hist.iter().collect();
|
||||
h.sort_by_key(|&(_, n)| std::cmp::Reverse(*n));
|
||||
for (k, n) in h.iter().take(8) { println!(" slack {k:>6} : {n}"); }
|
||||
for (k, n) in h.iter().take(8) {
|
||||
println!(" slack {k:>6} : {n}");
|
||||
}
|
||||
|
||||
println!("\nthe records THIS PORT animates:");
|
||||
println!(" {:<12} {:>6} {:>7} {:>7}", "record", "+0x08", "max t", "slack");
|
||||
println!(
|
||||
" {:<12} {:>6} {:>7} {:>7}",
|
||||
"record", "+0x08", "max t", "slack"
|
||||
);
|
||||
let (mut ship_exact, mut ship_hold, mut ship_bad) = (0, 0, 0);
|
||||
for (n, (len, maxt)) in &shipped {
|
||||
let slack = len - maxt;
|
||||
match slack { 0 => ship_exact += 1, s if s > 0 => ship_hold += 1, _ => ship_bad += 1 }
|
||||
println!(" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
|
||||
if slack < 0 { " 🔴 FALSIFIED" } else { "" });
|
||||
match slack {
|
||||
0 => ship_exact += 1,
|
||||
s if s > 0 => ship_hold += 1,
|
||||
_ => ship_bad += 1,
|
||||
}
|
||||
println!(
|
||||
" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
|
||||
if slack < 0 { " 🔴 FALSIFIED" } else { "" }
|
||||
);
|
||||
}
|
||||
println!("\n shipped: {ship_exact} exact, {ship_hold} hold, {ship_bad} falsified");
|
||||
if shipped.len() < SHIPPED.len() {
|
||||
let missing: Vec<_> = SHIPPED.iter().filter(|s| !shipped.contains_key(**s)).collect();
|
||||
let missing: Vec<_> = SHIPPED
|
||||
.iter()
|
||||
.filter(|s| !shipped.contains_key(**s))
|
||||
.collect();
|
||||
println!(" ⚠️ not found on the disc: {missing:?} -- a name the port ships and");
|
||||
println!(" this control never checked is worse than a violation it found.");
|
||||
}
|
||||
println!("\n verdict: {}", if ship_bad > 0 {
|
||||
"🔴 the reading fails on a record the port animates -- do NOT adopt"
|
||||
} else if ship_hold == 0 {
|
||||
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
|
||||
} else {
|
||||
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
|
||||
});
|
||||
println!(
|
||||
"\n verdict: {}",
|
||||
if ship_bad > 0 {
|
||||
"🔴 the reading fails on a record the port animates -- do NOT adopt"
|
||||
} else if ship_hold == 0 {
|
||||
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
|
||||
} else {
|
||||
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn pc(n: usize, d: usize) -> f64 { if d == 0 { 0.0 } else { 100.0 * n as f64 / d as f64 } }
|
||||
fn pc(n: usize, d: usize) -> f64 {
|
||||
if d == 0 {
|
||||
0.0
|
||||
} else {
|
||||
100.0 * n as f64 / d as f64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,29 +12,47 @@ use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
|
||||
.flatten().map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut records, mut in_bounds, mut magic, mut parsed, mut timed) = (0, 0, 0, 0, 0);
|
||||
let (mut untimed, mut all_at_zero) = (0usize, 0usize);
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else { continue };
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (_, &(o, s)) in &b.records {
|
||||
records += 1;
|
||||
if o + 12 > by.len() || o + s > by.len() { continue }
|
||||
if o + 12 > by.len() || o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
in_bounds += 1;
|
||||
if &by[o..o + 4] != b"RATC" { continue }
|
||||
if &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
magic += 1;
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
parsed += 1;
|
||||
let maxt = lb.elements.iter()
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max().unwrap_or(0);
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
// 🔴 `maxt == 0` merges two different populations, and the
|
||||
// Decoder's cause -- `.max()` returning `Some(0)` -- is only one
|
||||
// of them. A record with NO timed keyframe has no largest
|
||||
@@ -43,10 +61,16 @@ fn main() {
|
||||
// content. Both of us called all 1 530 "the question has no
|
||||
// meaning"; that is true of one group and an assumption about
|
||||
// the other.
|
||||
let any_timed = lb.elements.iter()
|
||||
let any_timed = lb
|
||||
.elements
|
||||
.iter()
|
||||
.any(|el| el.keyframes.iter().any(|k| k.time.is_some()));
|
||||
if maxt == 0 {
|
||||
if any_timed { all_at_zero += 1 } else { untimed += 1 }
|
||||
if any_timed {
|
||||
all_at_zero += 1
|
||||
} else {
|
||||
untimed += 1
|
||||
}
|
||||
continue;
|
||||
}
|
||||
timed += 1;
|
||||
@@ -55,8 +79,10 @@ fn main() {
|
||||
}
|
||||
println!(" records declared by parse_build : {records}");
|
||||
println!(" within the entry's bounds : {in_bounds}");
|
||||
println!(" carrying the RATC magic : {magic} <- {} dropped here",
|
||||
in_bounds - magic);
|
||||
println!(
|
||||
" carrying the RATC magic : {magic} <- {} dropped here",
|
||||
in_bounds - magic
|
||||
);
|
||||
println!(" parsing as a nested build : {parsed}");
|
||||
println!(" with a largest keyframe time > 0: {timed}");
|
||||
println!(" of the {} excluded:", untimed + all_at_zero);
|
||||
|
||||
@@ -16,13 +16,25 @@ fn main() {
|
||||
let (mut total, mut hits, mut multipose) = (0usize, 0usize, 0usize);
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (name, &(o, s)) in &b.records {
|
||||
if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" { continue }
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
|
||||
let maxt = lb.elements.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time)).max().unwrap_or(0);
|
||||
if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let len = ui_layout::loop_length_units(&by[o..o + s]).unwrap_or(0);
|
||||
total += 1;
|
||||
if maxt == 0 && len > 0 {
|
||||
@@ -31,10 +43,19 @@ fn main() {
|
||||
// to move between. All-at-t=0 with a single keyframe per element
|
||||
// is visually inert however it is played.
|
||||
let kf: usize = lb.elements.iter().map(|el| el.keyframes.len()).sum();
|
||||
let multi = lb.elements.iter().filter(|el| el.keyframes.len() > 1).count();
|
||||
if multi > 0 { multipose += 1 }
|
||||
println!(" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \
|
||||
across {} element(s), {multi} with >1 pose", lb.elements.len());
|
||||
let multi = lb
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.keyframes.len() > 1)
|
||||
.count();
|
||||
if multi > 0 {
|
||||
multipose += 1
|
||||
}
|
||||
println!(
|
||||
" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \
|
||||
across {} element(s), {multi} with >1 pose",
|
||||
lb.elements.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,11 @@ fn main() {
|
||||
continue;
|
||||
};
|
||||
let riffs = media::voice_region_riffs(&src, s, e).expect("riffs");
|
||||
println!("{movie}: region [{s}, {e}) = {} bytes, {} chunk(s)", e - s, riffs.len());
|
||||
println!(
|
||||
"{movie}: region [{s}, {e}) = {} bytes, {} chunk(s)",
|
||||
e - s,
|
||||
riffs.len()
|
||||
);
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let p = std::env::temp_dir().join(format!("vc_{movie}_{i}.xma.wav"));
|
||||
std::fs::write(&p, r).unwrap();
|
||||
@@ -28,7 +32,14 @@ fn main() {
|
||||
.arg(&w)
|
||||
.output();
|
||||
let out = Command::new("ffprobe")
|
||||
.args(["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0"])
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(&w)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
@@ -169,10 +169,9 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
|
||||
#[serde(default)]
|
||||
voice: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
let file: File = serde_json::from_str(&raw)
|
||||
.with_context(|| format!("parse {}", path.display()))?;
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
let file: File =
|
||||
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
|
||||
|
||||
// `_` is the house convention for a prose block explaining the section it
|
||||
// sits in -- see `authored/flow.json` and `authored/timing.json`. It is
|
||||
@@ -211,9 +210,12 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
|
||||
let size: usize = k
|
||||
.parse()
|
||||
.with_context(|| format!("authored/audio.json: voice.stream_weights key {k}"))?;
|
||||
let w = v.get("weight").and_then(serde_json::Value::as_f64).with_context(|| {
|
||||
format!("authored/audio.json: voice.stream_weights.{k} has no numeric weight")
|
||||
})?;
|
||||
let w = v
|
||||
.get("weight")
|
||||
.and_then(serde_json::Value::as_f64)
|
||||
.with_context(|| {
|
||||
format!("authored/audio.json: voice.stream_weights.{k} has no numeric weight")
|
||||
})?;
|
||||
stream_weights.insert(size, w);
|
||||
}
|
||||
}
|
||||
@@ -282,8 +284,16 @@ fn run_ffmpeg(argv: &[String], out: &Path) -> Result<()> {
|
||||
// temp name -- it is a hard failure before a byte is written: "Unable to
|
||||
// choose an output format". `video.rs` already had this shape; this
|
||||
// function was written from scratch and did not.
|
||||
let stem = out.file_stem().unwrap_or_default().to_string_lossy().into_owned();
|
||||
let ext = out.extension().unwrap_or_default().to_string_lossy().into_owned();
|
||||
let stem = out
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let ext = out
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let partial = out.with_file_name(format!(".{stem}.partial.{ext}"));
|
||||
let mut argv = argv.to_vec();
|
||||
let last = argv.len() - 1;
|
||||
@@ -328,8 +338,12 @@ fn measure(path: &Path) -> (Option<f32>, Option<f32>) {
|
||||
.find_map(|l| l.split_once(KEY)?.1.trim().parse().ok());
|
||||
let dur = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(path)
|
||||
.output()
|
||||
@@ -368,7 +382,12 @@ pub fn export_cues<S: DiscSource + ?Sized>(
|
||||
// short read rather than returning a truncated stream, because a
|
||||
// truncated XMA decodes to plausible-sounding garbage.
|
||||
let riff = media::se_wave_riff(
|
||||
source, &cue.bank, offset, cue.packets, cue.channels, cue.rate,
|
||||
source,
|
||||
&cue.bank,
|
||||
offset,
|
||||
cue.packets,
|
||||
cue.channels,
|
||||
cue.rate,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)
|
||||
.with_context(|| format!("assemble the {event} cue"))?;
|
||||
@@ -376,9 +395,16 @@ pub fn export_cues<S: DiscSource + ?Sized>(
|
||||
let staged = stage_riff(&dir, event, &riff)?;
|
||||
let ogg = dir.join(format!("{event}.ogg"));
|
||||
let argv: Vec<String> = [
|
||||
"-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", &staged.display().to_string(),
|
||||
"-c:a", "libvorbis", "-q:a", VORBIS_Q,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
&staged.display().to_string(),
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
VORBIS_Q,
|
||||
&ogg.display().to_string(),
|
||||
]
|
||||
.iter()
|
||||
@@ -537,9 +563,15 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
|
||||
argv.push(format!("{end}"));
|
||||
}
|
||||
argv.extend(
|
||||
["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
[
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
VORBIS_Q,
|
||||
&ogg.display().to_string(),
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
);
|
||||
let command = format!("ffmpeg {}", argv.join(" "));
|
||||
run_ffmpeg(&argv, &ogg)?;
|
||||
@@ -911,7 +943,10 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
// 238-packet late start was found. Applied positionally instead, the
|
||||
// weights would have gone onto the wrong streams in silence.
|
||||
let sizes: Vec<usize> = keep.iter().map(|&i| riffs[i].len() - RIFF_HEADER).collect();
|
||||
let ws: Option<Vec<f64>> = sizes.iter().map(|s| stream_weights.get(s).copied()).collect();
|
||||
let ws: Option<Vec<f64>> = sizes
|
||||
.iter()
|
||||
.map(|s| stream_weights.get(s).copied())
|
||||
.collect();
|
||||
match ws {
|
||||
Some(w) if w.len() == staged.len() => {
|
||||
// Weights sum to one, so the total is the movie's own and what
|
||||
@@ -937,9 +972,15 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
argv.push("-map".into());
|
||||
argv.push("[a]".into());
|
||||
argv.extend(
|
||||
["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
[
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
VORBIS_Q,
|
||||
&ogg.display().to_string(),
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
);
|
||||
let command = format!("ffmpeg {}", argv.join(" "));
|
||||
run_ffmpeg(&argv, &ogg)?;
|
||||
@@ -1039,8 +1080,14 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
fn probe_channels(path: &Path) -> Option<u8> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "error", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=channels", "-of", "csv=p=0",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=channels",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(path)
|
||||
.output()
|
||||
@@ -1056,8 +1103,12 @@ fn probe_channels(path: &Path) -> Option<u8> {
|
||||
pub fn probe_duration(path: &Path) -> Option<f32> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(path)
|
||||
.output()
|
||||
@@ -1094,7 +1145,6 @@ fn decoded_chunk(riff: &Path) -> (f32, f32) {
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
/// Which channel indices of a decoded stream are not digitally silent.
|
||||
///
|
||||
/// `astats` reports per-channel blocks: a `Channel: N` line followed by that
|
||||
|
||||
@@ -50,8 +50,9 @@ impl Ctx {
|
||||
/// name. Anything else means a consumer has to guess, which is the whole thing
|
||||
/// the format exists to prevent.
|
||||
fn is_hex32(v: Option<&Value>) -> bool {
|
||||
v.and_then(Value::as_str)
|
||||
.is_some_and(|s| s.len() == 10 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit()))
|
||||
v.and_then(Value::as_str).is_some_and(|s| {
|
||||
s.len() == 10 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_pose(c: &mut Ctx, where_: &str, p: &Value) {
|
||||
@@ -89,12 +90,17 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
// an invented one, so the provenance is mandatory and closed.
|
||||
match v.get("name_source").and_then(Value::as_str) {
|
||||
Some("authored") => {
|
||||
if v.get("name_why").and_then(Value::as_str).is_none_or(str::is_empty) {
|
||||
if v.get("name_why")
|
||||
.and_then(Value::as_str)
|
||||
.is_none_or(str::is_empty)
|
||||
{
|
||||
c.err("name_source is `authored` but there is no `name_why`");
|
||||
}
|
||||
}
|
||||
Some("index") => {}
|
||||
other => c.err(format!("name_source must be `authored` or `index`, got {other:?}")),
|
||||
other => c.err(format!(
|
||||
"name_source must be `authored` or `index`, got {other:?}"
|
||||
)),
|
||||
}
|
||||
if let Some(s) = v.get("source") {
|
||||
for key in ["archive", "entry", "build"] {
|
||||
@@ -119,9 +125,22 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
let mut indices = Vec::new();
|
||||
let mut buttons_by_y: Vec<(i64, String)> = Vec::new();
|
||||
for (i, el) in elements.iter().enumerate() {
|
||||
let id = el.get("id").and_then(Value::as_str).unwrap_or("<no id>").to_string();
|
||||
let id = el
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("<no id>")
|
||||
.to_string();
|
||||
let at = format!("element {i} ({id})");
|
||||
for key in ["index", "id", "declared", "role", "kind_raw", "pivot", "layer_source", "keyframes"] {
|
||||
for key in [
|
||||
"index",
|
||||
"id",
|
||||
"declared",
|
||||
"role",
|
||||
"kind_raw",
|
||||
"pivot",
|
||||
"layer_source",
|
||||
"keyframes",
|
||||
] {
|
||||
if el.get(key).is_none() {
|
||||
c.err(format!("{at}: missing `{key}`"));
|
||||
}
|
||||
@@ -131,7 +150,9 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
continue;
|
||||
};
|
||||
if idx as usize != i {
|
||||
c.err(format!("{at}: `index` {idx} does not match its position {i}"));
|
||||
c.err(format!(
|
||||
"{at}: `index` {idx} does not match its position {i}"
|
||||
));
|
||||
}
|
||||
indices.push(idx as usize);
|
||||
|
||||
@@ -152,15 +173,21 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
match el.get("layer_source").and_then(Value::as_str) {
|
||||
Some("sprite") | Some("implied") => {
|
||||
if !is_hex32(el.get("layer")) {
|
||||
c.err(format!("{at}: layer_source claims a key but `layer` is not one"));
|
||||
c.err(format!(
|
||||
"{at}: layer_source claims a key but `layer` is not one"
|
||||
));
|
||||
}
|
||||
}
|
||||
Some("none") => {
|
||||
if el.get("layer").is_some() {
|
||||
c.err(format!("{at}: layer_source `none` but a `layer` is present"));
|
||||
c.err(format!(
|
||||
"{at}: layer_source `none` but a `layer` is present"
|
||||
));
|
||||
}
|
||||
}
|
||||
other => c.err(format!("{at}: layer_source must be sprite/implied/none, got {other:?}")),
|
||||
other => c.err(format!(
|
||||
"{at}: layer_source must be sprite/implied/none, got {other:?}"
|
||||
)),
|
||||
}
|
||||
|
||||
for key in ["sprite", "focus_sprite"] {
|
||||
@@ -169,7 +196,9 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
if !path.exists() {
|
||||
c.err(format!("{at}: `{key}` points at {p}, which does not exist"));
|
||||
} else if let Err(e) = image::open(&path) {
|
||||
c.err(format!("{at}: `{key}` {p} does not decode as an image: {e}"));
|
||||
c.err(format!(
|
||||
"{at}: `{key}` {p} does not decode as an image: {e}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +206,11 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
if let Some(r) = el.get("rest") {
|
||||
check_pose(&mut c, &at, r);
|
||||
if role == "button" {
|
||||
if let Some(y) = r.get("pos").and_then(Value::as_array).and_then(|a| a[1].as_i64()) {
|
||||
if let Some(y) = r
|
||||
.get("pos")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|a| a[1].as_i64())
|
||||
{
|
||||
buttons_by_y.push((y, id.clone()));
|
||||
}
|
||||
}
|
||||
@@ -214,7 +247,10 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
// either drops an element or draws one twice.
|
||||
match v.get("paint_order").and_then(Value::as_array) {
|
||||
Some(po) => {
|
||||
let mut got: Vec<usize> = po.iter().filter_map(|x| x.as_u64().map(|v| v as usize)).collect();
|
||||
let mut got: Vec<usize> = po
|
||||
.iter()
|
||||
.filter_map(|x| x.as_u64().map(|v| v as usize))
|
||||
.collect();
|
||||
if got.len() != po.len() {
|
||||
c.err("`paint_order` holds a non-integer");
|
||||
}
|
||||
@@ -256,7 +292,10 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
pub fn run(root: &Path) -> Result<usize> {
|
||||
let manifest_path = root.join("manifest.json");
|
||||
if !manifest_path.exists() {
|
||||
bail!("{} has no manifest.json — is that an export tree?", root.display());
|
||||
bail!(
|
||||
"{} has no manifest.json — is that an export tree?",
|
||||
root.display()
|
||||
);
|
||||
}
|
||||
let m: Value = serde_json::from_str(&std::fs::read_to_string(&manifest_path)?)?;
|
||||
let mut errors = Vec::new();
|
||||
@@ -328,9 +367,13 @@ fn check_audio(root: &Path, m: &Value, errors: &mut Vec<String>) {
|
||||
errors.push(format!("manifest.json: audio `{name}` has no `{key}`"));
|
||||
}
|
||||
}
|
||||
let Some(file) = a.get("file").and_then(Value::as_str) else { continue };
|
||||
let Some(file) = a.get("file").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
if !root.join(file).exists() {
|
||||
errors.push(format!("manifest.json: lists audio {file}, which does not exist"));
|
||||
errors.push(format!(
|
||||
"manifest.json: lists audio {file}, which does not exist"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
match a.get("peak_dbfs").and_then(Value::as_f64) {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
mod audio;
|
||||
mod check;
|
||||
mod video;
|
||||
mod screen;
|
||||
mod video;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
@@ -164,8 +164,7 @@ fn load_names(authored: &Path) -> Result<NameMap> {
|
||||
#[serde(default)]
|
||||
also_export: AlsoExport,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
Ok(serde_json::from_str::<File>(&raw)
|
||||
.with_context(|| format!("parse {}", path.display()))?
|
||||
.archives)
|
||||
@@ -173,8 +172,7 @@ fn load_names(authored: &Path) -> Result<NameMap> {
|
||||
|
||||
/// Extra pak entries to export that `is_build` does not accept, keyed by
|
||||
/// archive. AUTHORED, and each carries its own `why`.
|
||||
type AlsoExport =
|
||||
std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
|
||||
type AlsoExport = std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
|
||||
|
||||
fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
let path = authored.join("screen_names.json");
|
||||
@@ -186,8 +184,7 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
#[serde(default)]
|
||||
also_export: AlsoExport,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
Ok(serde_json::from_str::<File>(&raw)
|
||||
.with_context(|| format!("parse {}", path.display()))?
|
||||
.also_export)
|
||||
@@ -211,9 +208,10 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
/// exactly four bundles and all four are real screens, with zero fragments. In
|
||||
/// another archive it would not be, which is why this is an allow-list and not
|
||||
/// a widened predicate.
|
||||
fn screen_builds(ar: &PakArchive, also: Option<&std::collections::BTreeMap<String, NameEntry>>)
|
||||
-> Vec<(usize, Vec<u8>)>
|
||||
{
|
||||
fn screen_builds(
|
||||
ar: &PakArchive,
|
||||
also: Option<&std::collections::BTreeMap<String, NameEntry>>,
|
||||
) -> Vec<(usize, Vec<u8>)> {
|
||||
let mut out = Vec::new();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
@@ -234,7 +232,11 @@ fn main() -> Result<()> {
|
||||
} => run_export(&disc, &out, &authored),
|
||||
Cmd::Check { out } => {
|
||||
let n = check::run(&out)?;
|
||||
println!("{} screen(s) in {} validate against sylpheed.screen/3", n, out.display());
|
||||
println!(
|
||||
"{} screen(s) in {} validate against sylpheed.screen/3",
|
||||
n,
|
||||
out.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -394,12 +396,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
Some(cfg) => {
|
||||
let source = media::DirectorySource::new(disc);
|
||||
for a in audio::export_cues(&source, out, &cfg.se)? {
|
||||
println!(
|
||||
" se {:<8} -> {} ({})",
|
||||
a.name,
|
||||
a.file,
|
||||
describe(&a)
|
||||
);
|
||||
println!(" se {:<8} -> {} ({})", a.name, a.file, describe(&a));
|
||||
audio.push(ManifestAudio::from(a));
|
||||
}
|
||||
for (role, spec) in &cfg.bgm {
|
||||
@@ -461,7 +458,10 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
// there is no `authored/audio.json` -- the voice binding is decoded,
|
||||
// so the dialogue exports either way and only the choice defaults.
|
||||
let want = audio_cfg.as_ref().map(|c| c.voice).unwrap_or_default();
|
||||
let weights = audio_cfg.as_ref().map(|c| c.stream_weights.clone()).unwrap_or_default();
|
||||
let weights = audio_cfg
|
||||
.as_ref()
|
||||
.map(|c| c.stream_weights.clone())
|
||||
.unwrap_or_default();
|
||||
match audio::export_voice(&source, out, stem, *len, want, &weights)? {
|
||||
Some(a) => {
|
||||
// 🔴 A TOP-LEVEL WARNING, not just a `why` on the entry. The
|
||||
@@ -517,7 +517,6 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
impl From<audio::Exported> for ManifestAudio {
|
||||
fn from(a: audio::Exported) -> Self {
|
||||
ManifestAudio {
|
||||
@@ -560,7 +559,6 @@ fn describe(a: &audio::Exported) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Delete anything in `video/` this run did not produce.
|
||||
///
|
||||
/// `video/` is the one directory the wholesale wipe spares, so that the
|
||||
|
||||
@@ -416,7 +416,6 @@ pub fn export_build(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
/// The highlighted twin of a sprite name: `ptbtn01.t32` → `ptbtn01f.t32`.
|
||||
fn highlight_name(sprite: &str) -> Option<String> {
|
||||
let (stem, ext) = sprite.rsplit_once('.')?;
|
||||
@@ -460,7 +459,9 @@ pub fn export_build(
|
||||
written: &mut std::collections::BTreeMap<String, ()>,
|
||||
missing: &mut Vec<String>|
|
||||
-> Result<Option<Focus>> {
|
||||
let Some(&(off, size)) = b.records.get(rec) else { return Ok(None) };
|
||||
let Some(&(off, size)) = b.records.get(rec) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(leaf) = ui_layout::parse_build(&bundle[off..off + size]) else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -468,8 +469,13 @@ pub fn export_build(
|
||||
for fe in &leaf.elements {
|
||||
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
|
||||
let mut fsprite = None;
|
||||
if write_from(&sprite_dir, written, sp, &bundle[off..off + size], &leaf.sprites)?
|
||||
|| write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
|
||||
if write_from(
|
||||
&sprite_dir,
|
||||
written,
|
||||
sp,
|
||||
&bundle[off..off + size],
|
||||
&leaf.sprites,
|
||||
)? || write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
|
||||
{
|
||||
fsprite = Some(sprite_rel(sp));
|
||||
} else if sp.ends_with(".t32") {
|
||||
@@ -481,8 +487,11 @@ pub fn export_build(
|
||||
declared: fe.name.clone(),
|
||||
sprite: fsprite,
|
||||
blend_additive: ui_layout::blend_additive_by_name(
|
||||
&leaf, &bundle[off..off + size], sp)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
&leaf,
|
||||
&bundle[off..off + size],
|
||||
sp,
|
||||
)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
pivot: [fe.pivot_x, fe.pivot_y],
|
||||
rest: Rest {
|
||||
pos: [r.x, r.y],
|
||||
@@ -533,9 +542,13 @@ pub fn export_build(
|
||||
// into the leaf slice) or in the parent's.
|
||||
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
|
||||
let mut fsprite = None;
|
||||
if write_from(&sprite_dir, &mut written, sp,
|
||||
&bundle[off..off + size], &leaf.sprites)?
|
||||
|| write_from(&sprite_dir, &mut written, sp, bundle, &b.sprites)?
|
||||
if write_from(
|
||||
&sprite_dir,
|
||||
&mut written,
|
||||
sp,
|
||||
&bundle[off..off + size],
|
||||
&leaf.sprites,
|
||||
)? || write_from(&sprite_dir, &mut written, sp, bundle, &b.sprites)?
|
||||
{
|
||||
fsprite = Some(sprite_rel(sp));
|
||||
} else if sp.ends_with(".t32") {
|
||||
@@ -547,8 +560,11 @@ pub fn export_build(
|
||||
declared: fe.name.clone(),
|
||||
sprite: fsprite,
|
||||
blend_additive: ui_layout::blend_additive_by_name(
|
||||
&leaf, &bundle[off..off + size], sp)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
&leaf,
|
||||
&bundle[off..off + size],
|
||||
sp,
|
||||
)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
pivot: [fe.pivot_x, fe.pivot_y],
|
||||
rest: Rest {
|
||||
pos: [r.x, r.y],
|
||||
@@ -575,7 +591,9 @@ pub fn export_build(
|
||||
if !fes.is_empty() {
|
||||
focus = Some(Focus {
|
||||
record: rec,
|
||||
loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]),
|
||||
loop_length_units: ui_layout::loop_length_units(
|
||||
&bundle[off..off + size],
|
||||
),
|
||||
elements: fes,
|
||||
});
|
||||
}
|
||||
@@ -704,7 +722,6 @@ pub fn export_build(
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// The longest interval containing no keyframe time, over TOP-LEVEL elements.
|
||||
///
|
||||
/// See [`Screen::settle_window`] for why this is the settled instant and why
|
||||
@@ -750,12 +767,12 @@ fn settle_window(elements: &[Element]) -> Option<[i64; 3]> {
|
||||
Some([a, b, (a + b) / 2])
|
||||
}
|
||||
|
||||
|
||||
/// Alpha of one element at instant `t`, under the linear ramp the port uses.
|
||||
fn alpha_at(e: &Element, t: i64) -> u8 {
|
||||
let ks = &e.keyframes;
|
||||
let a = |k: &Keyframe| (u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16)
|
||||
.unwrap_or(0) >> 24) as i64;
|
||||
let a = |k: &Keyframe| {
|
||||
(u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16).unwrap_or(0) >> 24) as i64
|
||||
};
|
||||
let timed: Vec<&Keyframe> = ks.iter().filter(|k| k.t.is_some()).collect();
|
||||
if timed.is_empty() {
|
||||
return 0;
|
||||
@@ -888,7 +905,9 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
|
||||
.filter_map(|k| k.t)
|
||||
.map(i64::from)
|
||||
.collect();
|
||||
let Some(&lo) = span.first() else { return false };
|
||||
let Some(&lo) = span.first() else {
|
||||
return false;
|
||||
};
|
||||
// 🔴 COVERAGE IS TESTED AT EACH INSTANT, NOT ONCE FROM `size`.
|
||||
// Declared size alone is not what the element draws: scale is a
|
||||
// percent per axis and it animates. `pbafc.prm` is the disc's own
|
||||
@@ -921,9 +940,10 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
|
||||
return false;
|
||||
}
|
||||
// Every OTHER element must be visible somewhere inside that span.
|
||||
elements.iter().enumerate().all(|(j, o)| {
|
||||
j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0)
|
||||
})
|
||||
elements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(j, o)| j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
@@ -97,13 +97,22 @@ const DOWNMIX_51: &str = "pan=stereo|FL=0.4142*FL+0.2929*FC+0.2929*BL
|
||||
fn channels(src: &Path) -> Result<u32> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "error", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=channels", "-of", "csv=p=0",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=channels",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(src)
|
||||
.output()
|
||||
.context("run ffprobe -- is it on PATH?")?;
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().parse().unwrap_or(2))
|
||||
Ok(String::from_utf8_lossy(&out.stdout)
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(2))
|
||||
}
|
||||
|
||||
/// Duration and frame rate of a finished transcode, straight from the file.
|
||||
@@ -119,7 +128,8 @@ fn probe_timebase(out: &Path) -> (f64, f64) {
|
||||
if stream {
|
||||
c.args(["-select_streams", "v:0"]);
|
||||
}
|
||||
c.args(["-show_entries", entries, "-of", "csv=p=0"]).arg(out);
|
||||
c.args(["-show_entries", entries, "-of", "csv=p=0"])
|
||||
.arg(out);
|
||||
c.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_default()
|
||||
@@ -136,10 +146,20 @@ fn probe_timebase(out: &Path) -> (f64, f64) {
|
||||
|
||||
fn args(src: &Path, out: &Path, channels: u32) -> Vec<String> {
|
||||
let mut v: Vec<String> = [
|
||||
"-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", &src.display().to_string(),
|
||||
"-c:v", "libtheora", "-q:v", "8",
|
||||
"-c:a", "libvorbis", "-q:a", "5",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
&src.display().to_string(),
|
||||
"-c:v",
|
||||
"libtheora",
|
||||
"-q:v",
|
||||
"8",
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
"5",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
@@ -268,7 +288,10 @@ pub fn transcode(disc: &Path, out: &Path, m: &Movie) -> Result<Option<Transcoded
|
||||
// updates it. The explanation would be correct in the source and absent on
|
||||
// disc, which is the same shape as every other documented-but-unexercised
|
||||
// thing this port has had to find the hard way.
|
||||
if std::fs::read_to_string(&stamp).map(|s| s != want).unwrap_or(true) {
|
||||
if std::fs::read_to_string(&stamp)
|
||||
.map(|s| s != want)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
std::fs::write(&stamp, &want)?;
|
||||
}
|
||||
let (duration_s, fps) = probe_timebase(&ogv);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user