style: rustfmt sweep -- 774 hunks across 154 files -> 0
`cargo fmt --all -- --check` has failed on every run in this repository's history, identically on `main` and on every branch. This is #12. Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other extension touched. `cargo check --workspace` exits 0 afterwards, so nothing changed semantically. ON THE ORDERING, WHICH WAS THE REAL QUESTION. HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree reformat before #7 and #8 return "would put a conflict in every file of 861 commits and make the reviews those items exist to enable unreadable". That is measurably too pessimistic, and it had been reasoned rather than tested. Measured here by three-way merging a rustfmt'd `main` against both unmerged branches, file by file: file/branch pairs tested 32 merges CLEAN 28 merges CONFLICTING 4 (8 conflict hunks total) sylpheed-cli/src/main.rs 1 hunk sylpheed-export/src/check.rs 1 sylpheed-export/src/screen.rs 4 sylpheed-export/src/video.rs 2 All four are against `auto/frame-blend-draw-path` only; `auto/port-p6-audio` does not conflict anywhere. The earlier framing -- 154 dirty files, 133 that cannot collide, 21 that can, the collision set carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say is that most of the 21 still merge cleanly, because rustfmt's edits and the branches' edits rarely land on the same lines. So the cost of sweeping now is 4 files and 8 hunks for one branch, against a check that is otherwise red forever. Deliberately NOT folded into the WASM PR: 154 reformatted files would make that one unreviewable. Closes #12
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