Merge remote-tracking branch 'origin/main' into auto/frame-blend-draw-path
# Conflicts: # crates/sylpheed-cli/src/main.rs
This commit is contained in:
@@ -38,7 +38,6 @@ use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::*;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use tracing::info;
|
||||
|
||||
use sylpheed_formats::vfs::{identify_format, GameAssets};
|
||||
use sylpheed_formats::{IdxdObject, PakArchive};
|
||||
@@ -336,7 +335,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();
|
||||
|
||||
@@ -344,24 +343,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),
|
||||
@@ -514,7 +522,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(),
|
||||
@@ -604,6 +614,11 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
// 8 parameters against a threshold of 7 — a plain function, unlike the Bevy
|
||||
// systems in the viewer, so this one is real if mild. Left as-is because the
|
||||
// arguments are the CLI flags this subcommand takes; grouping them into a
|
||||
// struct is a change to the command surface, not a lint fix.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn cmd_screen_render(
|
||||
pak: &Path,
|
||||
output: &Path,
|
||||
@@ -680,7 +695,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
|
||||
);
|
||||
}
|
||||
// 🔴 Report the INDEX, the KIND and WHY, not just the name. A kind-`0x4`
|
||||
// ghost instance carries its template's name, so a bare name list shows
|
||||
@@ -718,9 +736,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}"))?;
|
||||
|
||||
@@ -770,7 +786,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() {
|
||||
@@ -779,7 +799,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)
|
||||
@@ -823,9 +846,18 @@ 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(b) = info.avg_bytes_per_sec {
|
||||
println!(" Byte rate : {} B/s (declared)", b.to_string().yellow());
|
||||
}
|
||||
@@ -840,7 +872,10 @@ fn cmd_audio_info(file: &Path) -> Result<()> {
|
||||
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)",
|
||||
@@ -873,7 +908,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?;
|
||||
@@ -902,7 +937,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?;
|
||||
@@ -947,7 +986,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;
|
||||
@@ -968,9 +1009,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()
|
||||
};
|
||||
@@ -982,13 +1024,9 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
println!();
|
||||
println!("{}", "Format Summary:".bold());
|
||||
let mut summary: Vec<_> = counts.into_iter().collect();
|
||||
summary.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
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(())
|
||||
@@ -997,8 +1035,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)
|
||||
@@ -1006,16 +1043,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(())
|
||||
}
|
||||
@@ -1023,13 +1071,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,
|
||||
@@ -1046,7 +1093,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(())
|
||||
@@ -1124,7 +1175,7 @@ fn cmd_mesh_info(file: &Path) -> Result<()> {
|
||||
let nv = sub.positions.len();
|
||||
let mut referenced = vec![false; nv];
|
||||
let (mut degen, mut oob, mut imax) = (0usize, 0usize, 0u32);
|
||||
for tri in sub.indices.chunks_exact(3) {
|
||||
for tri in sub.indices.as_chunks::<3>().0 {
|
||||
let (a, b, c) = (tri[0], tri[1], tri[2]);
|
||||
imax = imax.max(a).max(b).max(c);
|
||||
if a == b || b == c || a == c {
|
||||
@@ -1146,11 +1197,17 @@ fn cmd_mesh_info(file: &Path) -> Result<()> {
|
||||
};
|
||||
let mut maxedges: Vec<f32> = sub
|
||||
.indices
|
||||
.chunks_exact(3)
|
||||
.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.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}/{}{}",
|
||||
@@ -1270,10 +1327,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])
|
||||
};
|
||||
@@ -1310,7 +1373,9 @@ fn cmd_mesh_render(
|
||||
let med = {
|
||||
let mut e: Vec<f32> = sub
|
||||
.indices
|
||||
.chunks_exact(3)
|
||||
.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|t| (t[0] as usize) < n && (t[1] as usize) < n && (t[2] as usize) < n)
|
||||
.map(|t| {
|
||||
let d = |a: u32, b: u32| {
|
||||
@@ -1337,14 +1402,16 @@ 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],
|
||||
(p[2] - center[2]) * scale * mirror[2] + cell[2],
|
||||
]
|
||||
};
|
||||
for tri in sub.indices.chunks_exact(3) {
|
||||
for tri in sub.indices.as_chunks::<3>().0 {
|
||||
let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
|
||||
if a < n && b < n && c < n {
|
||||
if span_only || span_hide {
|
||||
@@ -1530,7 +1597,9 @@ fn decode_to_rgba8(tex: &sylpheed_formats::texture::X360Texture) -> Result<Vec<u
|
||||
// [A,R,G,B] byte order (verified against the retail Acheron backdrop).
|
||||
// Emit RGBA. X8 has no meaningful alpha.
|
||||
let opaque = matches!(tex.format, F::X8R8G8B8);
|
||||
for (px, out) in tex.data.chunks_exact(4).zip(rgba.chunks_exact_mut(4)) {
|
||||
let src = tex.data.as_chunks::<4>().0;
|
||||
let dst = rgba.as_chunks_mut::<4>().0;
|
||||
for (px, out) in src.iter().zip(dst) {
|
||||
out[0] = px[1]; // R
|
||||
out[1] = px[2]; // G
|
||||
out[2] = px[3]; // B
|
||||
@@ -1726,8 +1795,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,
|
||||
@@ -1749,8 +1820,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!(
|
||||
"{} {} → {}",
|
||||
@@ -1780,12 +1850,12 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> {
|
||||
let mut idx = 0usize;
|
||||
while let Some(pos) = payload[off..]
|
||||
.windows(4)
|
||||
.position(|w| w == &t8ad::T8AD_MAGIC)
|
||||
.position(|w| w == t8ad::T8AD_MAGIC)
|
||||
{
|
||||
let start = off + pos;
|
||||
let next = payload[start + 4..]
|
||||
.windows(4)
|
||||
.position(|w| w == &t8ad::T8AD_MAGIC)
|
||||
.position(|w| w == t8ad::T8AD_MAGIC)
|
||||
.map(|p| start + 4 + p)
|
||||
.unwrap_or(payload.len());
|
||||
emit_t8ad(
|
||||
@@ -1818,7 +1888,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;
|
||||
|
||||
Reference in New Issue
Block a user