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:
@@ -16,7 +16,9 @@ use bevy::asset::{AssetLoader, LoadContext};
|
||||
use bevy::image::ImageSampler;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_asset::RenderAssetUsages;
|
||||
use bevy::render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
|
||||
use bevy::render::render_resource::{
|
||||
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
};
|
||||
use sylpheed_formats::texture::{X360Texture, X360TextureFormat};
|
||||
|
||||
// ── Plugin ────────────────────────────────────────────────────────────────────
|
||||
@@ -153,9 +155,7 @@ fn x360_format_to_wgpu(format: &X360TextureFormat) -> Result<TextureFormat, Xpr2
|
||||
// DXT5A / BC4 — single-channel (gloss, specular, luminance maps)
|
||||
X360TextureFormat::Dxt5A => TextureFormat::Bc4RUnorm,
|
||||
// Uncompressed — reordered to [R,G,B,A] by `x360_texture_to_bevy_image`.
|
||||
X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => {
|
||||
TextureFormat::Rgba8UnormSrgb
|
||||
}
|
||||
X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => TextureFormat::Rgba8UnormSrgb,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! - Scroll wheel → zoom
|
||||
//! - R → reset to default view
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_asset::RenderAssetUsages;
|
||||
use bevy::render::render_resource::{
|
||||
Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, TextureViewDimension,
|
||||
@@ -18,7 +18,7 @@ pub struct OrbitCameraPlugin;
|
||||
impl Plugin for OrbitCameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, spawn_camera)
|
||||
.add_systems(Update, orbit_camera);
|
||||
.add_systems(Update, orbit_camera);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,9 @@ fn orbit_camera(
|
||||
mut mouse_motion: EventReader<MouseMotion>,
|
||||
mut scroll: EventReader<MouseWheel>,
|
||||
) {
|
||||
let Ok((mut cam, mut transform, mut projection)) = query.get_single_mut() else { return };
|
||||
let Ok((mut cam, mut transform, mut projection)) = query.get_single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut delta_motion = Vec2::ZERO;
|
||||
for ev in mouse_motion.read() {
|
||||
@@ -173,7 +175,7 @@ fn orbit_camera(
|
||||
|
||||
// Orbit (left mouse drag)
|
||||
if mouse_buttons.pressed(MouseButton::Left) {
|
||||
cam.yaw -= delta_motion.x * cam.orbit_sensitivity;
|
||||
cam.yaw -= delta_motion.x * cam.orbit_sensitivity;
|
||||
cam.pitch -= delta_motion.y * cam.orbit_sensitivity;
|
||||
// Clamp pitch to avoid gimbal lock
|
||||
cam.pitch = cam.pitch.clamp(-1.5, 1.5);
|
||||
@@ -182,12 +184,12 @@ fn orbit_camera(
|
||||
// Pan (right mouse drag)
|
||||
if mouse_buttons.pressed(MouseButton::Right) {
|
||||
let right = transform.rotation * Vec3::X;
|
||||
let up = transform.rotation * Vec3::Y;
|
||||
let up = transform.rotation * Vec3::Y;
|
||||
// Copy fields before mutably borrowing `cam.focus`
|
||||
let pan_sens = cam.pan_sensitivity;
|
||||
let radius = cam.radius;
|
||||
let radius = cam.radius;
|
||||
cam.focus -= right * delta_motion.x * pan_sens * radius;
|
||||
cam.focus += up * delta_motion.y * pan_sens * radius;
|
||||
cam.focus += up * delta_motion.y * pan_sens * radius;
|
||||
}
|
||||
|
||||
// Zoom (scroll)
|
||||
@@ -212,6 +214,5 @@ fn orbit_camera(
|
||||
fn orbit_transform(cam: &OrbitCamera) -> Transform {
|
||||
let rotation = Quat::from_euler(EulerRot::YXZ, cam.yaw, cam.pitch, 0.0);
|
||||
let offset = rotation * Vec3::new(0.0, 0.0, cam.radius);
|
||||
Transform::from_translation(cam.focus + offset)
|
||||
.looking_at(cam.focus, Vec3::Y)
|
||||
Transform::from_translation(cam.focus + offset).looking_at(cam.focus, Vec3::Y)
|
||||
}
|
||||
|
||||
@@ -251,7 +251,6 @@ pub struct ModelPreview {
|
||||
pub info: String,
|
||||
}
|
||||
|
||||
|
||||
/// The currently-open `.wmv` cutscene, shown in the central panel with a
|
||||
/// transport bar. Registered unconditionally (the decode/audio engine below is
|
||||
/// native-only); on wasm `active` stays false and `.wmv` shows the info panel.
|
||||
@@ -1383,10 +1382,7 @@ fn handle_open_iso(
|
||||
let mut reader = sylpheed_formats::xiso::open_iso(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let files = reader
|
||||
.list_all_files()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let files = reader.list_all_files().await.map_err(|e| e.to_string())?;
|
||||
Ok::<_, String>((iso_path, label, files))
|
||||
});
|
||||
|
||||
@@ -1465,7 +1461,11 @@ fn handle_file_selected(
|
||||
// decode is heavy — assemble + label off-thread, then hand the UI plain
|
||||
// rows. Everything else falls through to the single-file read below.
|
||||
if file_path.to_ascii_lowercase().ends_with(".pak") {
|
||||
let name = file_path.rsplit('/').next().unwrap_or(&file_path).to_string();
|
||||
let name = file_path
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(&file_path)
|
||||
.to_string();
|
||||
let base = file_path[..file_path.len() - 4].to_string(); // strip ".pak"
|
||||
match &iso_state.source_kind {
|
||||
SourceKind::Iso(iso_path) => {
|
||||
@@ -1534,7 +1534,11 @@ fn handle_file_selected(
|
||||
// real path), probe dimensions/duration, and pre-decode the audio track
|
||||
// to a temp WAV — all off-thread. The video frame pipe is opened later.
|
||||
if file_path.to_ascii_lowercase().ends_with(".wmv") {
|
||||
let name = file_path.rsplit('/').next().unwrap_or(&file_path).to_string();
|
||||
let name = file_path
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(&file_path)
|
||||
.to_string();
|
||||
match &iso_state.source_kind {
|
||||
SourceKind::Iso(iso_path) => {
|
||||
let iso_path = iso_path.clone();
|
||||
@@ -1579,10 +1583,9 @@ fn handle_file_selected(
|
||||
let iso_path = iso_path.clone();
|
||||
std::thread::spawn(move || {
|
||||
let result = futures::executor::block_on(async {
|
||||
let mut reader =
|
||||
sylpheed_formats::xiso::open_iso(&iso_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut reader = sylpheed_formats::xiso::open_iso(&iso_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let bytes = reader
|
||||
.read_file(&file_path)
|
||||
.await
|
||||
@@ -1592,8 +1595,7 @@ fn handle_file_selected(
|
||||
|
||||
match result {
|
||||
Ok((path, bytes)) => {
|
||||
let _ = sender
|
||||
.send(IsoLoaderMsg::FileLoaded { path, bytes });
|
||||
let _ = sender.send(IsoLoaderMsg::FileLoaded { path, bytes });
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = sender.send(IsoLoaderMsg::Error(e));
|
||||
@@ -1602,8 +1604,7 @@ fn handle_file_selected(
|
||||
});
|
||||
}
|
||||
SourceKind::Directory(root) => {
|
||||
let assets =
|
||||
sylpheed_formats::vfs::GameAssets::from_directory(root);
|
||||
let assets = sylpheed_formats::vfs::GameAssets::from_directory(root);
|
||||
match assets.read(&file_path) {
|
||||
Ok(bytes) => {
|
||||
let _ = sender.send(IsoLoaderMsg::FileLoaded {
|
||||
@@ -1901,8 +1902,7 @@ fn prepare_video(name: &str, bytes: Vec<u8>) -> Result<PreparedVideo, String> {
|
||||
let dir = std::env::temp_dir();
|
||||
let stem = format!("sylph_vid_{}_{}", std::process::id(), sanitize_stem(name));
|
||||
let temp_video = dir.join(format!("{stem}.wmv"));
|
||||
std::fs::write(&temp_video, &bytes)
|
||||
.map_err(|e| format!("writing temp video: {e}"))?;
|
||||
std::fs::write(&temp_video, &bytes).map_err(|e| format!("writing temp video: {e}"))?;
|
||||
|
||||
let (width, height, fps, duration) = ffprobe_video(&temp_video)?;
|
||||
|
||||
@@ -1971,14 +1971,19 @@ fn ffprobe_video(path: &Path) -> Result<(u32, u32, f32, f32), String> {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
let (mut w, mut h, mut fps, mut dur) = (0u32, 0u32, 30.0f32, 0.0f32);
|
||||
for line in text.lines() {
|
||||
let Some((k, v)) = line.split_once('=') else { continue };
|
||||
let Some((k, v)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
match k.trim() {
|
||||
"width" => w = v.trim().parse().unwrap_or(0),
|
||||
"height" => h = v.trim().parse().unwrap_or(0),
|
||||
"avg_frame_rate" => {
|
||||
// "30/1" (or "0/0" for VFR/unknown → keep the 30 fps default).
|
||||
if let Some((n, d)) = v.trim().split_once('/') {
|
||||
let (n, d) = (n.parse::<f32>().unwrap_or(0.0), d.parse::<f32>().unwrap_or(0.0));
|
||||
let (n, d) = (
|
||||
n.parse::<f32>().unwrap_or(0.0),
|
||||
d.parse::<f32>().unwrap_or(0.0),
|
||||
);
|
||||
if n > 0.0 && d > 0.0 {
|
||||
fps = n / d;
|
||||
}
|
||||
@@ -2159,13 +2164,7 @@ fn grab_one_frame(path: &Path, t: f32, frame_len: usize) -> Option<Vec<u8>> {
|
||||
/// recent** (so fast dragging skips stale targets), grabs that frame, and sends
|
||||
/// it back. One ffmpeg at a time; exits when the request channel closes.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn scrub_worker(
|
||||
rx: mpsc::Receiver<f32>,
|
||||
tx: mpsc::Sender<Vec<u8>>,
|
||||
path: PathBuf,
|
||||
w: u32,
|
||||
h: u32,
|
||||
) {
|
||||
fn scrub_worker(rx: mpsc::Receiver<f32>, tx: mpsc::Sender<Vec<u8>>, path: PathBuf, w: u32, h: u32) {
|
||||
let frame_len = (w as usize) * (h as usize) * 4;
|
||||
while let Ok(mut t) = rx.recv() {
|
||||
while let Ok(newer) = rx.try_recv() {
|
||||
@@ -2348,7 +2347,8 @@ fn poll_loader_channel(
|
||||
game_data.loading = false;
|
||||
// An empty snapshot means the decode failed (no game source, or a
|
||||
// bare pak) — leave `loaded=false` so re-opening retries.
|
||||
let empty = snap.weapons.is_empty() && snap.craft.is_empty() && snap.missions.is_empty();
|
||||
let empty =
|
||||
snap.weapons.is_empty() && snap.craft.is_empty() && snap.missions.is_empty();
|
||||
if empty {
|
||||
game_data.loaded = false;
|
||||
} else {
|
||||
@@ -2560,7 +2560,9 @@ fn pick_albedo_index(model_name: &str, tex_names: &[String]) -> Option<usize> {
|
||||
return Some(*i);
|
||||
}
|
||||
// 3. Stem mentioned anywhere in a colour map's name.
|
||||
cols.iter().find(|(_, n)| n.contains(&stem)).map(|(i, _)| *i)
|
||||
cols.iter()
|
||||
.find(|(_, n)| n.contains(&stem))
|
||||
.map(|(i, _)| *i)
|
||||
}
|
||||
|
||||
/// Per-vertex smooth normals = normalized sum of incident face normals.
|
||||
@@ -2582,7 +2584,13 @@ fn compute_smooth_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32;
|
||||
}
|
||||
acc.into_iter()
|
||||
.map(|n| n.normalize_or_zero().to_array())
|
||||
.map(|n| if n == [0.0, 0.0, 0.0] { [0.0, 1.0, 0.0] } else { n })
|
||||
.map(|n| {
|
||||
if n == [0.0, 0.0, 0.0] {
|
||||
[0.0, 1.0, 0.0]
|
||||
} else {
|
||||
n
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -2642,11 +2650,7 @@ fn apply_loaded_texture(
|
||||
*model_preview = ModelPreview::default();
|
||||
|
||||
// Populate FileInfo for the status / info panel.
|
||||
file_info.name = path
|
||||
.split('/')
|
||||
.next_back()
|
||||
.unwrap_or(&path)
|
||||
.to_string();
|
||||
file_info.name = path.split('/').next_back().unwrap_or(&path).to_string();
|
||||
file_info.size_bytes = bytes.len();
|
||||
file_info.detected_format = Some(fmt);
|
||||
|
||||
@@ -2820,7 +2824,10 @@ fn pack_metallic_roughness(spc: Option<&Image>, gls: Option<&Image>) -> Option<I
|
||||
let ih = img.texture_descriptor.size.height.max(1);
|
||||
let sx = (x * iw / w).min(iw - 1);
|
||||
let sy = (y * ih / h).min(ih - 1);
|
||||
img.data.get(((sy * iw + sx) * 4) as usize).copied().unwrap_or(dflt)
|
||||
img.data
|
||||
.get(((sy * iw + sx) * 4) as usize)
|
||||
.copied()
|
||||
.unwrap_or(dflt)
|
||||
};
|
||||
let mut data = vec![0u8; (w * h * 4) as usize];
|
||||
for y in 0..h {
|
||||
@@ -2832,7 +2839,11 @@ fn pack_metallic_roughness(spc: Option<&Image>, gls: Option<&Image>) -> Option<I
|
||||
}
|
||||
}
|
||||
Some(Image::new(
|
||||
Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
||||
Extent3d {
|
||||
width: w,
|
||||
height: h,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
TextureDimension::D2,
|
||||
data,
|
||||
TextureFormat::Rgba8Unorm, // linear — metallic/roughness are not colour
|
||||
@@ -2909,7 +2920,9 @@ fn prepare_models_impl(
|
||||
.and_then(|t| crate::asset_loader::x360_texture_to_bevy_image(t).ok())
|
||||
};
|
||||
let load_named = |name: &str| -> Option<Image> {
|
||||
let ti = tex_names.iter().position(|t| t.eq_ignore_ascii_case(name))?;
|
||||
let ti = tex_names
|
||||
.iter()
|
||||
.position(|t| t.eq_ignore_ascii_case(name))?;
|
||||
load_idx(ti)
|
||||
};
|
||||
let slot_for_ti = |ti: usize,
|
||||
@@ -2927,12 +2940,22 @@ fn prepare_models_impl(
|
||||
.and_then(|n| n.strip_suffix("_col").or(Some(n.as_str())))
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let spc = (!base.is_empty()).then(|| load_named(&format!("{base}_spc"))).flatten();
|
||||
let gls = (!base.is_empty()).then(|| load_named(&format!("{base}_gls"))).flatten();
|
||||
let emissive = (!base.is_empty()).then(|| load_named(&format!("{base}_lum"))).flatten();
|
||||
let spc = (!base.is_empty())
|
||||
.then(|| load_named(&format!("{base}_spc")))
|
||||
.flatten();
|
||||
let gls = (!base.is_empty())
|
||||
.then(|| load_named(&format!("{base}_gls")))
|
||||
.flatten();
|
||||
let emissive = (!base.is_empty())
|
||||
.then(|| load_named(&format!("{base}_lum")))
|
||||
.flatten();
|
||||
let metallic_roughness = pack_metallic_roughness(spc.as_ref(), gls.as_ref());
|
||||
let slot = materials.len();
|
||||
materials.push(PreparedMat { albedo, metallic_roughness, emissive });
|
||||
materials.push(PreparedMat {
|
||||
albedo,
|
||||
metallic_roughness,
|
||||
emissive,
|
||||
});
|
||||
tex_slot.insert(ti, slot);
|
||||
slot
|
||||
};
|
||||
@@ -3043,12 +3066,18 @@ fn prepare_models_impl(
|
||||
sylpheed_formats::mesh::material_groups(bytes, &model.name, sub.indices.len())
|
||||
};
|
||||
let slices: Vec<(usize, usize, Option<usize>)> = if groups.is_empty() {
|
||||
vec![(0, sub.indices.len(), pick_albedo_index(&model.name, &tex_names))]
|
||||
vec![(
|
||||
0,
|
||||
sub.indices.len(),
|
||||
pick_albedo_index(&model.name, &tex_names),
|
||||
)]
|
||||
} else {
|
||||
groups
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let ti = tex_names.iter().position(|t| t.eq_ignore_ascii_case(&g.albedo));
|
||||
let ti = tex_names
|
||||
.iter()
|
||||
.position(|t| t.eq_ignore_ascii_case(&g.albedo));
|
||||
(g.idx_offset, g.idx_count, ti)
|
||||
})
|
||||
.collect()
|
||||
@@ -3146,7 +3175,10 @@ fn prepare_models_impl(
|
||||
if buf.pos.is_empty() || buf.idx.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||
let mut mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::default(),
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, buf.pos);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, buf.nrm);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, buf.uv);
|
||||
@@ -3412,7 +3444,6 @@ fn apply_prepared_xpr(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Consumes a staged pack, freeing the previous texture/text previews and
|
||||
/// populating `PakView` for the master-detail browser.
|
||||
// Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the
|
||||
@@ -3706,7 +3737,9 @@ fn handle_subtitle_request(
|
||||
&source,
|
||||
&format!("dat/GP_MAIN_GAME_{}.pak", lang.game_code()),
|
||||
)?;
|
||||
Ok(sylpheed_formats::movie_subtitle::load(&movie, &lang_pak, &text_pak))
|
||||
Ok(sylpheed_formats::movie_subtitle::load(
|
||||
&movie, &lang_pak, &text_pak,
|
||||
))
|
||||
})()
|
||||
.unwrap_or_default();
|
||||
let _ = sender.send(IsoLoaderMsg::SubtitlesLoaded {
|
||||
@@ -3732,9 +3765,7 @@ fn read_source_file(source: &SourceKind, path: &str) -> Result<Vec<u8>, String>
|
||||
reader.read_file(&path).await.map_err(|e| e.to_string())
|
||||
})
|
||||
}
|
||||
SourceKind::Directory(root) => {
|
||||
std::fs::read(root.join(path)).map_err(|e| e.to_string())
|
||||
}
|
||||
SourceKind::Directory(root) => std::fs::read(root.join(path)).map_err(|e| e.to_string()),
|
||||
SourceKind::None => Err("no source open".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -3762,7 +3793,9 @@ fn read_segment_range(
|
||||
match source {
|
||||
SourceKind::Directory(root) => {
|
||||
let path = root.join(&seg);
|
||||
let Ok(meta) = std::fs::metadata(&path) else { break };
|
||||
let Ok(meta) = std::fs::metadata(&path) else {
|
||||
break;
|
||||
};
|
||||
let seg_len = meta.len();
|
||||
if skip >= seg_len {
|
||||
skip -= seg_len;
|
||||
@@ -3779,7 +3812,9 @@ fn read_segment_range(
|
||||
}
|
||||
SourceKind::Iso(_) => {
|
||||
// No partial read on ISO; read the whole segment and slice.
|
||||
let Ok(bytes) = read_source_file(source, &seg) else { break };
|
||||
let Ok(bytes) = read_source_file(source, &seg) else {
|
||||
break;
|
||||
};
|
||||
let seg_len = bytes.len() as u64;
|
||||
if skip >= seg_len {
|
||||
skip -= seg_len;
|
||||
@@ -3871,7 +3906,9 @@ fn decode_riffs_to_wav(
|
||||
};
|
||||
let filter = format!(
|
||||
"{}concat=n={}:v=0:a=1{}[a]",
|
||||
(0..inputs.len()).map(|i| format!("[{i}:a]")).collect::<String>(),
|
||||
(0..inputs.len())
|
||||
.map(|i| format!("[{i}:a]"))
|
||||
.collect::<String>(),
|
||||
inputs.len(),
|
||||
if mono { ",pan=mono|c0=c0" } else { "" },
|
||||
);
|
||||
@@ -3888,12 +3925,17 @@ fn decode_riffs_to_wav(
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
match status {
|
||||
Ok(o) if o.status.success() && out_path.metadata().map(|m| m.len() > 44).unwrap_or(false) => {
|
||||
Ok(o)
|
||||
if o.status.success() && out_path.metadata().map(|m| m.len() > 44).unwrap_or(false) =>
|
||||
{
|
||||
Ok(out_path)
|
||||
}
|
||||
Ok(o) => Err(format!(
|
||||
"ffmpeg voice decode failed: {}",
|
||||
String::from_utf8_lossy(&o.stderr).lines().last().unwrap_or("")
|
||||
String::from_utf8_lossy(&o.stderr)
|
||||
.lines()
|
||||
.last()
|
||||
.unwrap_or("")
|
||||
)),
|
||||
Err(e) => Err(format!("spawning ffmpeg: {e}")),
|
||||
}
|
||||
@@ -3941,7 +3983,8 @@ fn handle_voice_request(
|
||||
let wav = sylpheed_formats::media::resolve_movie_voice_region(&source, &movie, lang)
|
||||
.and_then(|(s, e)| decode_voice_region(&source, s, e, duration).ok())
|
||||
.or_else(|| {
|
||||
let clip = sylpheed_formats::media::resolve_movie_voice_clip(&source, &movie, lang)?;
|
||||
let clip =
|
||||
sylpheed_formats::media::resolve_movie_voice_clip(&source, &movie, lang)?;
|
||||
if clip.contains("\\Movie\\") {
|
||||
return None;
|
||||
}
|
||||
@@ -4030,15 +4073,18 @@ fn handle_audio_request(
|
||||
// stream (same resolution as playback), falling back to a non-`\Movie\`
|
||||
// clip (hokyu `\etc\`); for a named library clip, decode it directly.
|
||||
let wav = match movie {
|
||||
Some((m, lang)) => sylpheed_formats::media::resolve_movie_voice_region(&source, &m, lang)
|
||||
.and_then(|(s, e)| decode_voice_region(&source, s, e, f32::INFINITY).ok())
|
||||
.or_else(|| {
|
||||
let c = sylpheed_formats::media::resolve_movie_voice_clip(&source, &m, lang)?;
|
||||
if c.contains("\\Movie\\") {
|
||||
return None;
|
||||
}
|
||||
decode_sound_clip(&source, &c, f32::INFINITY, true).ok()
|
||||
}),
|
||||
Some((m, lang)) => {
|
||||
sylpheed_formats::media::resolve_movie_voice_region(&source, &m, lang)
|
||||
.and_then(|(s, e)| decode_voice_region(&source, s, e, f32::INFINITY).ok())
|
||||
.or_else(|| {
|
||||
let c =
|
||||
sylpheed_formats::media::resolve_movie_voice_clip(&source, &m, lang)?;
|
||||
if c.contains("\\Movie\\") {
|
||||
return None;
|
||||
}
|
||||
decode_sound_clip(&source, &c, f32::INFINITY, true).ok()
|
||||
})
|
||||
}
|
||||
None => decode_sound_clip(&source, &clip, f32::INFINITY, mono).ok(),
|
||||
}
|
||||
.and_then(|p| wav_duration(&p).map(|d| (p, d)));
|
||||
@@ -4089,7 +4135,11 @@ fn build_game_snapshot(source: &SourceKind) -> Option<GameSnapshot> {
|
||||
let id = c.id?;
|
||||
let short = id.trim_start_matches("Character").to_string();
|
||||
let name = text.character_name(&short).unwrap_or(&short).to_string();
|
||||
Some(CharRow { name, faction: c.faction.unwrap_or_default(), faces: c.faces.len() })
|
||||
Some(CharRow {
|
||||
name,
|
||||
faction: c.faction.unwrap_or_default(),
|
||||
faces: c.faces.len(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
characters.sort_by_key(|a| (a.faction.clone(), a.name.clone()));
|
||||
@@ -4097,8 +4147,16 @@ fn build_game_snapshot(source: &SourceKind) -> Option<GameSnapshot> {
|
||||
// Combat rosters, keyed by stage where the table self-identifies.
|
||||
let rosters = gd::load_unit_rosters(&main);
|
||||
let stat_hp = |id: &str| -> Option<f32> {
|
||||
craft.iter().find(|u| u.id.as_deref() == Some(id)).and_then(|u| u.hp)
|
||||
.or_else(|| vessels.iter().find(|v| v.id.as_deref() == Some(id)).and_then(|v| v.hp))
|
||||
craft
|
||||
.iter()
|
||||
.find(|u| u.id.as_deref() == Some(id))
|
||||
.and_then(|u| u.hp)
|
||||
.or_else(|| {
|
||||
vessels
|
||||
.iter()
|
||||
.find(|v| v.id.as_deref() == Some(id))
|
||||
.and_then(|v| v.hp)
|
||||
})
|
||||
};
|
||||
let mut missions: Vec<MissionRow> = Vec::new();
|
||||
for s in gd::load_stages(&main) {
|
||||
@@ -4106,21 +4164,34 @@ fn build_game_snapshot(source: &SourceKind) -> Option<GameSnapshot> {
|
||||
if s.id.len() != 3 || !s.id.starts_with('S') || s.id[1..].parse::<u32>().is_err() {
|
||||
continue;
|
||||
}
|
||||
let objectives: Vec<String> =
|
||||
(1..=s.phase_count()).flat_map(|p| text.objectives(&s.id, p)).map(str::to_string).collect();
|
||||
let lose: Vec<String> =
|
||||
(1..=s.phase_count()).flat_map(|p| text.lose_conditions(&s.id, p)).map(str::to_string).collect();
|
||||
let objectives: Vec<String> = (1..=s.phase_count())
|
||||
.flat_map(|p| text.objectives(&s.id, p))
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
let lose: Vec<String> = (1..=s.phase_count())
|
||||
.flat_map(|p| text.lose_conditions(&s.id, p))
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
let enemies: Vec<String> = rosters
|
||||
.iter()
|
||||
.find(|r| r.stage.as_deref() == Some(s.id.as_str()))
|
||||
.map(|r| {
|
||||
r.units.iter().filter(|u| u.contains("ADAN")).map(|u| {
|
||||
let name = u.trim_start_matches("UN_").splitn(3, '_').nth(2).unwrap_or(u).replace('_', " ");
|
||||
match stat_hp(u) {
|
||||
Some(h) => format!("{name} ({h:.0} HP)"),
|
||||
None => name,
|
||||
}
|
||||
}).collect()
|
||||
r.units
|
||||
.iter()
|
||||
.filter(|u| u.contains("ADAN"))
|
||||
.map(|u| {
|
||||
let name = u
|
||||
.trim_start_matches("UN_")
|
||||
.splitn(3, '_')
|
||||
.nth(2)
|
||||
.unwrap_or(u)
|
||||
.replace('_', " ");
|
||||
match stat_hp(u) {
|
||||
Some(h) => format!("{name} ({h:.0} HP)"),
|
||||
None => name,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
missions.push(MissionRow {
|
||||
@@ -4141,7 +4212,12 @@ fn build_game_snapshot(source: &SourceKind) -> Option<GameSnapshot> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut flights = Vec::new();
|
||||
for r in gd::load_pilot_rosters(&h) {
|
||||
let key: String = r.pilots().iter().map(|(c, p)| format!("{c}:{p}")).collect::<Vec<_>>().join(",");
|
||||
let key: String = r
|
||||
.pilots()
|
||||
.iter()
|
||||
.map(|(c, p)| format!("{c}:{p}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
if seen.insert(key) {
|
||||
flights.push(r.pilots());
|
||||
}
|
||||
@@ -4150,7 +4226,15 @@ fn build_game_snapshot(source: &SourceKind) -> Option<GameSnapshot> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(GameSnapshot { weapons, craft, vessels, characters, missions, arsenal, flights })
|
||||
Some(GameSnapshot {
|
||||
weapons,
|
||||
craft,
|
||||
vessels,
|
||||
characters,
|
||||
missions,
|
||||
arsenal,
|
||||
flights,
|
||||
})
|
||||
}
|
||||
|
||||
// ── UI screens ───────────────────────────────────────────────────────────────
|
||||
@@ -4207,11 +4291,7 @@ fn handle_screen_catalog_request(
|
||||
|
||||
/// Open every `GP_*.pak` and record which entries parse as a screen build.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_screen_catalog(
|
||||
source: &SourceKind,
|
||||
files: &[String],
|
||||
fragments: bool,
|
||||
) -> Vec<ScreenPak> {
|
||||
fn build_screen_catalog(source: &SourceKind, files: &[String], fragments: bool) -> Vec<ScreenPak> {
|
||||
use sylpheed_formats::ui_layout;
|
||||
let mut out = Vec::new();
|
||||
let mut candidates: Vec<&String> = files
|
||||
@@ -4571,9 +4651,9 @@ fn handle_ship_catalog_request(
|
||||
/// and join each to its [`sylpheed_formats::game_data::Vessel`] stats.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_ship_catalog(source: &SourceKind) -> Vec<ShipRow> {
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::game_data::{self as gd, Vessel};
|
||||
use sylpheed_formats::ship::ships_in_container;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Vessel stats keyed by family id (`rou_e105` → `e105`).
|
||||
let vessels: Vec<Vessel> = read_pak_archive_blocking(source, "dat/GP_MAIN_GAME_E.pak")
|
||||
@@ -4664,8 +4744,12 @@ fn handle_ship_render_request(
|
||||
let Some(req) = events.read().last() else {
|
||||
return;
|
||||
};
|
||||
let (file, id, external, label) =
|
||||
(req.file.clone(), req.id.clone(), req.external, req.label.clone());
|
||||
let (file, id, external, label) = (
|
||||
req.file.clone(),
|
||||
req.id.clone(),
|
||||
req.external,
|
||||
req.label.clone(),
|
||||
);
|
||||
|
||||
// Supersede any in-flight XPR/stage decode, exactly like a file selection.
|
||||
xpr_load.generation = xpr_load.generation.wrapping_add(1);
|
||||
@@ -4774,7 +4858,10 @@ fn build_ship_model(
|
||||
// capture-verified). Draw a simple cone at each frame so the assembled ship
|
||||
// reads correctly.
|
||||
if external {
|
||||
for (i, f) in sylpheed_formats::ship::exhaust_frames(&bytes, id).iter().enumerate() {
|
||||
for (i, f) in sylpheed_formats::ship::exhaust_frames(&bytes, id)
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let mut cone = exhaust_cone_mesh();
|
||||
for v in &mut cone.positions {
|
||||
*v = f.apply(*v);
|
||||
@@ -5107,9 +5194,17 @@ fn advance_video_playback(
|
||||
if let Some(wav) = voice.pending_wav.take() {
|
||||
if let Some(handle) = &inner.stream_handle {
|
||||
let vol = if voice.enabled { video.volume } else { 0.0 };
|
||||
info!("[voice] APPLY sink movie={:?} wav={wav:?} pos={}", voice.movie, video.position);
|
||||
inner.voice_sink =
|
||||
build_audio_sink(handle, &wav, video.position, vol, video.playing && !video.scrubbing);
|
||||
info!(
|
||||
"[voice] APPLY sink movie={:?} wav={wav:?} pos={}",
|
||||
voice.movie, video.position
|
||||
);
|
||||
inner.voice_sink = build_audio_sink(
|
||||
handle,
|
||||
&wav,
|
||||
video.position,
|
||||
vol,
|
||||
video.playing && !video.scrubbing,
|
||||
);
|
||||
inner.voice_applied_volume = vol;
|
||||
inner.voice_wav = Some(wav);
|
||||
}
|
||||
@@ -5120,7 +5215,13 @@ fn advance_video_playback(
|
||||
// until the restarted stream produces its first frame — no black flash. ──
|
||||
if let Some(target) = video.seek_request.take() {
|
||||
let target = target.clamp(0.0, video.duration);
|
||||
match spawn_video_decoder(&inner.temp_video, inner.width, inner.height, inner.fps, target) {
|
||||
match spawn_video_decoder(
|
||||
&inner.temp_video,
|
||||
inner.width,
|
||||
inner.height,
|
||||
inner.fps,
|
||||
target,
|
||||
) {
|
||||
Ok((new_child, new_rx)) => {
|
||||
let _ = inner.child.kill();
|
||||
let _ = inner.child.wait();
|
||||
@@ -5265,7 +5366,11 @@ mod albedo_match_tests {
|
||||
use super::*;
|
||||
|
||||
fn cue(start: f32, end: Option<f32>, text: &str) -> sylpheed_formats::SubCue {
|
||||
sylpheed_formats::SubCue { start, end, text: text.into() }
|
||||
sylpheed_formats::SubCue {
|
||||
start,
|
||||
end,
|
||||
text: text.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5273,12 +5378,19 @@ mod albedo_match_tests {
|
||||
// Single start-only cue (a resupply line): shown from its start for an
|
||||
// estimated reading span, then hidden — NOT for the whole video.
|
||||
let subs = MovieSubtitles {
|
||||
cues: vec![cue(0.0, None, "Resupply complete. You are cleared for take-off!")],
|
||||
cues: vec![cue(
|
||||
0.0,
|
||||
None,
|
||||
"Resupply complete. You are cleared for take-off!",
|
||||
)],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(subs.active_at(0.0).is_some());
|
||||
assert!(subs.active_at(3.0).is_some());
|
||||
assert!(subs.active_at(30.0).is_none(), "must not linger the whole video");
|
||||
assert!(
|
||||
subs.active_at(30.0).is_none(),
|
||||
"must not linger the whole video"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5286,12 +5398,21 @@ mod albedo_match_tests {
|
||||
// Two far-apart radio lines: each visible near its own start, with a gap
|
||||
// in between (the first doesn't stretch to the second).
|
||||
let subs = MovieSubtitles {
|
||||
cues: vec![cue(0.5, None, "We did it!"), cue(19.3, None, "Yeah, but Brandon...")],
|
||||
cues: vec![
|
||||
cue(0.5, None, "We did it!"),
|
||||
cue(19.3, None, "Yeah, but Brandon..."),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(subs.active_at(1.0).map(|c| c.text.as_str()), Some("We did it!"));
|
||||
assert_eq!(
|
||||
subs.active_at(1.0).map(|c| c.text.as_str()),
|
||||
Some("We did it!")
|
||||
);
|
||||
assert!(subs.active_at(12.0).is_none(), "gap between the two lines");
|
||||
assert_eq!(subs.active_at(20.0).map(|c| c.text.as_str()), Some("Yeah, but Brandon..."));
|
||||
assert_eq!(
|
||||
subs.active_at(20.0).map(|c| c.text.as_str()),
|
||||
Some("Yeah, but Brandon...")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5432,6 +5553,9 @@ mod font_sample_tests {
|
||||
.expect("real TrueType font should rasterize a sample");
|
||||
assert!(img.width > 0 && img.height > 0);
|
||||
assert_eq!(img.rgba.len(), (img.width * img.height * 4) as usize);
|
||||
assert!(img.rgba.iter().skip(3).step_by(4).any(|&a| a > 0), "some ink");
|
||||
assert!(
|
||||
img.rgba.iter().skip(3).step_by(4).any(|&a| a > 0),
|
||||
"some ink"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +46,14 @@ impl Default for ViewerState {
|
||||
pub fn run() {
|
||||
let mut app = App::new();
|
||||
|
||||
app.add_plugins(
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(),
|
||||
resolution: (1280.0_f32, 720.0_f32).into(),
|
||||
..default()
|
||||
}),
|
||||
app.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(),
|
||||
resolution: (1280.0_f32, 720.0_f32).into(),
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
..default()
|
||||
}));
|
||||
|
||||
app.add_plugins(EguiPlugin);
|
||||
app.add_plugins(asset_loader::SylpheedAssetPlugin);
|
||||
@@ -81,7 +79,7 @@ fn setup_scene(mut commands: Commands) {
|
||||
// camera always has some light on the visible side — the single front light
|
||||
// left the backside unreadable.
|
||||
let lights = [
|
||||
(Vec3::new(1.0, 2.0, 1.5), 10_000.0, true), // key: front-top-right
|
||||
(Vec3::new(1.0, 2.0, 1.5), 10_000.0, true), // key: front-top-right
|
||||
(Vec3::new(-2.0, 1.0, 0.5), 4_500.0, false), // fill: left
|
||||
(Vec3::new(0.5, 0.8, -2.0), 6_000.0, false), // rim: behind
|
||||
(Vec3::new(0.0, -1.5, 0.5), 2_500.0, false), // underside fill
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user