Compare commits

...

3 Commits

Author SHA1 Message Date
sylph-decoder
08416dbd0b fix(viewer,cli,export): clear the remaining 30 clippy lints
`cargo clippy --workspace -- -D warnings` now exits 0. `cargo test
--workspace` still reports 207 passed, 0 failed, 14 ignored across 30
suites — identical to runs 203 and 204, so none of this changed behaviour.

The workspace total was 73, not the 48 run 204 reported. `-D warnings`
turns a lint into a hard compile error, so `sylpheed-formats` failing
stopped its dependents from ever being built: `sylpheed-viewer` (14) and
`sylpheed-cli` (11) had never been linted by anyone. Clearing formats in
bc79817 is what made them visible.

  formats  43 -> 0   (bc79817)
  viewer   14 -> 0
  cli      11 -> 0
  export    5 -> 0

Collision surface, measured rather than assumed. Every viewer file
carrying a lint is byte-identical on both `auto/frame-blend-draw-path`
(495 commits) and `auto/port-p6-audio` (366). All eleven cli sites fall
outside every hunk either branch touches. 68 of the 73 sites could not
collide with anything.

The five that can are all in `sylpheed-export`, and three of those are
real:

  main.rs:278   `&out` -> `out`, inside frame-blend's hunk -278,12
  main.rs:318   `&out` -> `out`, inside port-p6-audio's hunk -303,44
  audio.rs:113  an added `#[allow]` in a file frame-blend DELETES

Each is one line. Resolving the first two means taking the branch's
version and re-applying a borrow removal; the third resolves to the
deletion. Flagged here so neither branch owner meets them cold.

Judgement calls, all stated at the site rather than suppressed globally:

* Three `too_many_arguments` in the viewer are false positives.
  `draw_viewer_ui`, `poll_loader_channel` and `apply_pak` are Bevy
  systems — every parameter is a `Res`/`ResMut`/`EventWriter` the
  scheduler injects, so the count is the framework's dependency list and
  cannot be reduced without a `SystemParam` struct.
* `cmd_screen_render` (cli, 8/7) is a plain function, so that one is real
  if mild; its arguments are the subcommand's flags.
* Two `dead_code` fields in export are serde schema fields. They model
  what the on-disc JSON accepts; deleting them would quietly change that.
* `iso_loader.rs` gains a `FrameRx` alias for the ffmpeg frame channel,
  which is what "very complex type" was asking for.

A site-local `#[allow]` with a reason is a decision recorded where it
applies: one lint, one function, and any new violation elsewhere still
fails the build. That is not the shape PROTOCOL.md forbids.

Closes #13

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-05 17:47:50 +02:00
sylph-decoder
bc79817488 fix(formats): clear all 43 clippy lints in sylpheed-formats
Run 204 gave this repository its first clippy measurement — 48 errors, 43
of them in `sylpheed-formats`. This clears that 43 to zero under the exact
invocation CI runs, `cargo clippy -p sylpheed-formats -- -D warnings`.

Why this crate first, and why it is safe to touch:

Every one of the 43 sites was checked against the line ranges that
`auto/frame-blend-draw-path` (495 commits) and `auto/port-p6-audio` (366)
actually modify. None of them overlap. Eleven of the fifteen affected
files are byte-identical on both branches, including `mesh.rs` and
`texture.rs`, which carry 28 of the hits between them. The three sites in
`audio.rs`, `ui_layout.rs` and `slb.rs` that live in files those branches
do change fall outside every modified hunk. The collision argument that
defers #12 does not transfer here; it was tested rather than assumed.

It also unblocks a measurement. `-D warnings` turns a lint in this crate
into a hard compile error, so its dependents never build — `sylpheed-cli`
and `sylpheed-viewer` have never been linted at all, and viewer is the
largest crate in the workspace. Both depend only on `sylpheed-formats`
(`sylpheed-export` pins it from a git tag instead), so this commit is what
makes their real counts knowable.

  38  applied by `cargo clippy --fix` — chunks_exact_to_as_chunks,
      manual_div_ceil / is_multiple_of / range_contains, unnecessary_map_or,
      needless_borrow, let_and_return, dead_code, unused_mut/variables.
      Purely local expression rewrites: 38 insertions, 39 deletions.
   2  by hand: a doc continuation that markdown was parsing as a list, and
      `d / frame` behind a `frame > 0` guard becoming `checked_div`.
   3  `#[allow(clippy::too_many_arguments)]` with a stated reason.

On those three allows: 8 parameters against a threshold of 7, in the mesh
anchor path. The real fix is a shared params struct across
`anchor_pool_mesh`, `validate_block` and `validate_block_report` — the
latter two take the same eight arguments and one delegates to the other —
which is a change to the decoder's signatures and belongs to whoever owns
that path, not to a CI-lint pass.

This is not the shape PROTOCOL.md forbids. `continue-on-error` suppresses
everything, present and future, at the job level, and cannot tell "not
yet" from "no longer". A site-local `#[allow]` with a reason is a decision
recorded where it applies: one lint, one function, and any new violation
anywhere else still fails the build.

`sylpheed-export`'s remaining 5 are deliberately untouched — three of them
sit inside hunks both long-lived branches modify, and that crate blocks
nothing. Left for #13.

Refs #13

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-05 16:59:57 +02:00
sylph-decoder
d8dcc1fc29 docs: record the third softening, which was authored dirty
The two instances already in "Checks that were kind once" were correct
when written and decayed. The third was wrong on its first commit, and it
arrived by a different route: the check and the tree's failure to pass it
land in the same change, so the softening writes itself.

Concretely — the Clippy step had never run (no component in the
toolchain), and the tree is not clippy-clean, so fixing the step and
turning it red are the same commit. The first draft paired the fix with
`continue-on-error: true` and a comment promising removal once the debt
was paid: an expiry date nobody set, in the shape #12's closing line had
already ruled out for rustfmt. Reverted on reading it.

Adds the distinction, a table separating decay from dirty authorship, and
an earlier tell than the mechanical test:

    If you are writing the softening in the same commit as the check,
    the thing you want is an issue, not a flag.

The mechanical test is unchanged and still correct; this only catches the
same failure sooner, at the keyboard rather than at review.

Refs #12, #13

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-05 15:55:04 +02:00
21 changed files with 150 additions and 66 deletions

View File

@@ -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};
@@ -572,6 +571,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,
@@ -892,7 +896,7 @@ 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} .{}",
@@ -1034,7 +1038,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 {
@@ -1056,7 +1060,7 @@ 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());
@@ -1220,7 +1224,7 @@ 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| {
@@ -1254,7 +1258,7 @@ fn cmd_mesh_render(
(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 {
@@ -1440,7 +1444,7 @@ 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)) {
for (px, out) in tex.data.as_chunks::<4>().0.iter().zip(rgba.as_chunks_mut::<4>().0) {
out[0] = px[1]; // R
out[1] = px[2]; // G
out[2] = px[3]; // B
@@ -1690,12 +1694,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(

View File

@@ -109,6 +109,8 @@ pub struct BgmSpec {
pub loop_start_s: Option<f64>,
#[serde(default)]
pub loop_end_s: Option<f64>,
// Deserialised to model the sidecar schema, not read in Rust.
#[allow(dead_code)]
#[serde(default)]
pub loop_end_why: Option<serde_json::Value>,
#[serde(default)]

View File

@@ -158,6 +158,9 @@ fn load_names(authored: &Path) -> Result<NameMap> {
#[derive(serde::Deserialize)]
struct File {
archives: NameMap,
// Deserialised to model the on-disc schema, not read in Rust.
// Removing it would silently change what this struct accepts.
#[allow(dead_code)]
#[serde(default)]
also_export: AlsoExport,
}
@@ -275,7 +278,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
// `video/` that this run did not claim, so a movie that stops being exported
// still stops existing.
if out.exists() {
for entry in std::fs::read_dir(&out).context("clear the output tree")? {
for entry in std::fs::read_dir(out).context("clear the output tree")? {
let entry = entry?;
if entry.file_name() == "video" {
continue;
@@ -288,7 +291,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
.with_context(|| format!("clear {}", entry.path().display()))?;
}
}
std::fs::create_dir_all(&out)?;
std::fs::create_dir_all(out)?;
let archive = "dat/GP_TITLE.pak";
let pak = disc.join(archive);
@@ -315,7 +318,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
None => (format!("build_{entry:02}"), "index", None),
};
let ex = screen::export_build(
&out,
out,
archive,
*entry,
build_idx,

View File

@@ -226,9 +226,10 @@ fn parse_riff_wave(bytes: &[u8]) -> Option<AudioInfo> {
match codec {
AudioCodec::Pcm | AudioCodec::PcmFloat => {
if let (Some(d), true) = (data_bytes, channels > 0 && bits > 0) {
// `bits` under 8 makes the frame size zero, so this stays
// fallible: `checked_div` states that once, where it happens.
let frame = channels as u64 * (bits as u64 / 8);
if frame > 0 {
let spc = d / frame;
if let Some(spc) = d.checked_div(frame) {
info.samples_per_channel = Some(spc);
if rate > 0 {
info.duration_secs = Some(spc as f32 / rate as f32);
@@ -275,11 +276,11 @@ impl GameAudio {
let samples: Vec<f32> = match (info.codec, bits) {
(AudioCodec::Pcm, 8) => data.iter().map(|&b| (b as f32 - 128.0) / 128.0).collect(),
(AudioCodec::Pcm, 16) => data
.chunks_exact(2)
.as_chunks::<2>().0.iter()
.map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0)
.collect(),
(AudioCodec::Pcm, 24) => data
.chunks_exact(3)
.as_chunks::<3>().0.iter()
.map(|c| {
let v = ((c[2] as i32) << 16) | ((c[1] as i32) << 8) | c[0] as i32;
let v = (v << 8) >> 8; // sign-extend 24→32
@@ -287,7 +288,7 @@ impl GameAudio {
})
.collect(),
(AudioCodec::PcmFloat, 32) => data
.chunks_exact(4)
.as_chunks::<4>().0.iter()
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
_ => return Err(AudioError::UnsupportedPcm { tag: 0, bits }),

View File

@@ -1131,7 +1131,7 @@ pub fn load_squadrons(pak: &PakArchive) -> Vec<Squadron> {
let Some(f) = records.record(id) else { continue };
let slots: Vec<&str> = f.positional.iter().map(|(_, v)| v.as_str()).collect();
let members = slots
.chunks_exact(4)
.as_chunks::<4>().0.iter()
.map(|m| SquadronMember {
unit: m[0].to_string(),
message_set: text(Some(m[1])),

View File

@@ -71,7 +71,7 @@ pub fn name_hash(name: &str) -> u32 {
// through untouched, including any high-bit bytes, which are then sign-extended).
let mut bytes = name.as_bytes().to_vec();
for byte in &mut bytes {
if (b'A'..=b'Z').contains(byte) {
if byte.is_ascii_uppercase() {
*byte += 0x20;
}
}

View File

@@ -119,7 +119,7 @@ pub fn parse(bytes: &[u8]) -> Option<Subtitle> {
fn utf16be_tokens(bytes: &[u8]) -> Vec<String> {
let mut tokens = Vec::new();
let mut cur = String::new();
for pair in bytes.chunks_exact(2) {
for pair in bytes.as_chunks::<2>().0 {
let u = u16::from_be_bytes([pair[0], pair[1]]);
match char::from_u32(u as u32) {
Some(c) if !c.is_control() => cur.push(c),

View File

@@ -700,7 +700,7 @@ impl Xbg7Model {
let (vc, ic) = r.markers[0];
let sig = (r.decl.stride, vc, ic);
let floor = last_by_sig.get(&sig).map_or(0, |o| o + 1);
if m.meshes[0].vbuf_offset.map_or(false, |o| o < floor) {
if m.meshes[0].vbuf_offset.is_some_and(|o| o < floor) {
if let Some(alt) = anchor_pool_mesh(
bytes,
starts,
@@ -1339,6 +1339,13 @@ fn vertex_run_starts(bytes: &[u8], data_base: usize, stride: usize) -> Vec<usize
/// the `index_count` indices ending just before it are all `< vtx_count`,
/// reference (nearly) all vertices, and produce non-degenerate triangles with a
/// real spatial extent — a signature strong enough to pin the block.
// 8 parameters against clippy's threshold of 7. Not refactored here on
// purpose: the fix is a shared params struct across this and the two
// `validate_block*` functions below, which is a change to the decoder's
// mesh-anchoring signatures and belongs to whoever owns that path — not to
// a CI-lint pass. Recorded rather than silenced: this allow covers one lint
// at one site, and any new violation elsewhere still fails the build.
#[allow(clippy::too_many_arguments)]
fn anchor_pool_mesh(
bytes: &[u8],
starts: &[usize],
@@ -1406,7 +1413,7 @@ fn anchor_pool_mesh(
}
let (degen, wind) = index_run_quality(bytes, ib, vb, index_count, decl);
let cand = (degen, -wind, pad);
if best.map_or(true, |b| cand < b) {
if best.is_none_or(|b| cand < b) {
best = Some(cand);
}
}
@@ -1452,6 +1459,11 @@ fn anchor_pool_mesh(
/// for both the adjacency anchor ([`anchor_pool_mesh`]) and the grouped-pool
/// anchor ([`anchor_grouped_meshes`]); the two differ only in how they *place*
/// `ib`/`vb`, not in how they validate a placement.
// 8/7, as above. This and `validate_block_report` take the *same* eight
// parameters — one delegates straight to the other — which is exactly why a
// params struct is the right fix and why it should be done once, deliberately,
// rather than piecemeal here.
#[allow(clippy::too_many_arguments)]
fn validate_block(
bytes: &[u8],
ib: usize,
@@ -1478,6 +1490,8 @@ fn validate_block(
/// [`validate_block`], but naming the gate that rejected a block. A runtime
/// capture can prove a block is real; when the decoder still refuses it, this
/// says which test is wrong rather than leaving a threshold to be guessed at.
// 8/7 — the signature `validate_block` delegates into. See above.
#[allow(clippy::too_many_arguments)]
fn validate_block_report(
bytes: &[u8],
ib: usize,
@@ -1679,7 +1693,7 @@ fn anchor_grouped_meshes(
if n == 0 || decls.len() < n {
return Vec::new();
}
let decl = &decls[0];
let _decl = &decls[0];
// Relative index-buffer offsets (ib0 = 0), 4-byte aligned between buffers,
// and cumulative vertex offsets (vb0 = 0) — both derived from the marker list.
@@ -1723,7 +1737,7 @@ fn anchor_grouped_meshes(
let (vc, ic) = markers[i];
let ib = ib0 + rel_ib[i];
if ib + ic * 2 > bytes.len()
|| vc.checked_mul(decls[i].stride).map_or(true, |b| vb + b > bytes.len())
|| vc.checked_mul(decls[i].stride).is_none_or(|b| vb + b > bytes.len())
{
break;
}
@@ -1784,7 +1798,7 @@ fn anchor_grouped_meshes(
}
let (degen, wind) = index_run_quality(bytes, ib_k, vb_k, ick, &decls[kmax]);
let cand = (degen, -wind, pad);
if best.map_or(true, |b| cand < b) {
if best.is_none_or(|b| cand < b) {
best = Some(cand);
}
if pad_first_match() {
@@ -1797,7 +1811,7 @@ fn anchor_grouped_meshes(
if meshes.len() == n {
return meshes;
}
if partial.as_ref().map_or(true, |p| meshes.len() > p.len()) {
if partial.as_ref().is_none_or(|p| meshes.len() > p.len()) {
partial = Some(meshes);
}
}
@@ -1929,7 +1943,7 @@ fn find_index_marker(desc: &[u8]) -> Option<(usize, usize)> {
while rel + 40 <= desc.len() {
let a = be32(desc, rel);
let c = be32(desc, rel + 4);
if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 {
if c >= 3 && c.is_multiple_of(3) && c < 400_000 && a == c * 2 {
return Some((rel, c as usize));
}
rel += 4;
@@ -1951,7 +1965,7 @@ fn all_index_markers(desc: &[u8]) -> Vec<(usize, usize)> {
while rel + 8 <= desc.len() {
let a = be32(desc, rel);
let c = be32(desc, rel + 4);
if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 && rel >= 32 {
if c >= 3 && c.is_multiple_of(3) && c < 400_000 && a == c * 2 && rel >= 32 {
let vc = be32(desc, rel - 32) as usize;
if (3..=200_000).contains(&vc) {
out.push((vc, c as usize));
@@ -1979,7 +1993,7 @@ fn all_vertex_decls(desc: &[u8]) -> Vec<VertexDecl> {
while rel + 8 <= desc.len() {
let a = be32(desc, rel);
let c = be32(desc, rel + 4);
if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 && rel >= 32 {
if c >= 3 && c.is_multiple_of(3) && c < 400_000 && a == c * 2 && rel >= 32 {
let vc = be32(desc, rel - 32) as usize;
if (3..=200_000).contains(&vc) {
match parse_decl_at(desc, rel + 8) {
@@ -2086,9 +2100,8 @@ fn submesh_records(desc: &[u8]) -> Vec<(usize, usize)> {
let t = be32(desc, rel + 12);
if (3..=65535).contains(&a)
&& z == 0
&& c >= 3
&& c <= 200_000
&& c % 3 == 0
&& (3..=200_000).contains(&c)
&& c.is_multiple_of(3)
&& (1..=64).contains(&t)
{
out.push((a as usize, c as usize));
@@ -2151,7 +2164,7 @@ fn topology_report(
let mut agree = 0usize;
let mut counted = 0usize;
let mut edges: HashMap<(u32, u32), u32> = HashMap::new();
for t in indices.chunks_exact(3) {
for t in indices.as_chunks::<3>().0 {
let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize);
if a == b || b == c || a == c {
degen += 1;
@@ -2330,7 +2343,7 @@ pub fn submesh_albedos(bytes: &[u8], resource_name: &str) -> Vec<(usize, usize,
if (3..=65535).contains(&vtx)
&& z == 0
&& idx >= 3
&& idx % 3 == 0
&& idx.is_multiple_of(3)
&& idx < 400_000
&& tail > 0
&& tail < 0x10_0000
@@ -2423,7 +2436,7 @@ pub fn material_groups(bytes: &[u8], resource_name: &str, target: usize) -> Vec<
if (1..=70000).contains(&vtx)
&& tail == 4
&& cnt >= 3
&& cnt % 3 == 0
&& cnt.is_multiple_of(3)
&& cnt < 400_000
&& (off as usize) < 4_000_000
{

View File

@@ -2,7 +2,7 @@
//! table (statically reversed). Beyond the movie→voice binding, it defines the
//! whole cutscene *structure*: which video (+ subtitle, + on-screen text overlay,
//! + voice) plays at each mission phase, and of what kind (story intro, in-mission
//! phase cutscene, resupply, …).
//! phase cutscene, resupply, …).
//!
//! ## On-disc structure (`dat/tables.pak`, entry hash `0x5b983a08`)
//!

View File

@@ -96,7 +96,7 @@ pub fn find_descriptor_before(buf: &[u8], before: usize) -> Option<usize> {
let mut o = (before - 1).min(cap) & !3;
loop {
let id = be(o);
if id >= 1 && id < ID_MAX && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
if (1..ID_MAX).contains(&id) && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
return Some(o);
}
if o < 4 {

View File

@@ -287,7 +287,7 @@ pub fn parse_drawlog(text: &str) -> Vec<CapturedDraw> {
let mut pos: Vec<[f32; 3]> = Vec::new();
let mut consts: Vec<(usize, [f64; 4])> = Vec::new();
let mut flush = |base: u32,
let flush = |base: u32,
size: u32,
stride: u32,
pos: &mut Vec<[f32; 3]>,
@@ -426,7 +426,7 @@ pub fn correlate(
let Some((score, mirrored)) = pos_validate(d, &key.ref_pos) else {
continue; // positions disagree — not this part
};
if best.map_or(true, |(_, s, _)| score > s) {
if best.is_none_or(|(_, s, _)| score > s) {
best = Some((d, score, mirrored));
}
}

View File

@@ -517,7 +517,7 @@ pub fn to_xma_riff_best(slb: &[u8]) -> Option<Vec<u8>> {
while let Some(di) = find(slb, b"data", i) {
let declared = le32(slb, di + 4).unwrap_or(0) as usize;
let usable = declared.min(slb.len().saturating_sub(di + 8));
if best.map_or(true, |(_, b)| usable > b) {
if best.is_none_or(|(_, b)| usable > b) {
best = Some((di, usable));
}
i = di + 4;

View File

@@ -587,19 +587,19 @@ pub fn apply_endian_swap(data: &mut [u8], endianness: u8) {
match endianness {
1 => {
// k8in16 — swap the two bytes of each 16-bit half.
for c in data.chunks_exact_mut(2) {
for c in data.as_chunks_mut::<2>().0 {
c.swap(0, 1);
}
}
2 => {
// k8in32 — reverse each 32-bit word.
for c in data.chunks_exact_mut(4) {
for c in data.as_chunks_mut::<4>().0 {
c.reverse();
}
}
3 => {
// k16in32 — swap the two 16-bit halves of each 32-bit word.
for c in data.chunks_exact_mut(4) {
for c in data.as_chunks_mut::<4>().0 {
c.swap(0, 2);
c.swap(1, 3);
}
@@ -626,8 +626,8 @@ fn decode_surface(
} else {
// Linear layout — copy only the mip-0 slice.
let block_size = format.block_size() as u32;
let bw = ((width + block_size - 1) / block_size).max(1);
let bh = ((height + block_size - 1) / block_size).max(1);
let bw = width.div_ceil(block_size).max(1);
let bh = height.div_ceil(block_size).max(1);
let needed = bw as usize * bh as usize * format.bytes_per_block();
if raw_data.len() < needed {
return Err(TextureError::BufferTooSmall { needed, have: raw_data.len() });
@@ -651,7 +651,7 @@ fn decode_surface(
match format {
X360TextureFormat::Dxt1 => swap_bc_block_dwords(&mut linear_data),
X360TextureFormat::Dxt3 | X360TextureFormat::Dxt5 => {
for block in linear_data.chunks_exact_mut(16) {
for block in linear_data.as_chunks_mut::<16>().0 {
swap_bc_block_dwords(&mut block[8..16]);
}
}
@@ -666,8 +666,8 @@ fn decode_surface(
/// rounded up to the 4 KiB subresource alignment (`kTextureSubresourceAlignmentBytes`).
fn tiled_face_stride(width: u32, height: u32, format: X360TextureFormat) -> usize {
let bs = format.block_size() as u32;
let bw = ((width + bs - 1) / bs).max(1);
let bh = ((height + bs - 1) / bs).max(1);
let bw = width.div_ceil(bs).max(1);
let bh = height.div_ceil(bs).max(1);
let pitch_aligned = align_up(bw, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS);
let height_aligned = align_up(bh, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS);
let surface = pitch_aligned as usize * height_aligned as usize * format.bytes_per_block();
@@ -682,7 +682,7 @@ fn tiled_face_stride(width: u32, height: u32, format: X360TextureFormat) -> usiz
/// after the byte-level endian swap. Any trailing bytes that don't fill a full
/// 8-byte group are left untouched.
pub fn swap_bc_block_dwords(data: &mut [u8]) {
for unit in data.chunks_exact_mut(8) {
for unit in data.as_chunks_mut::<8>().0 {
// [d0 d1 d2 d3 | d4 d5 d6 d7] → [d4 d5 d6 d7 | d0 d1 d2 d3]
let (lo, hi) = unit.split_at_mut(4);
lo.swap_with_slice(hi);
@@ -742,8 +742,8 @@ pub fn detile(
let bpb_log2 = (bpb as u32).trailing_zeros();
// Visible dimensions in blocks, and the padded storage pitch/height.
let blocks_wide = ((width + block_size - 1) / block_size).max(1);
let blocks_tall = ((height + block_size - 1) / block_size).max(1);
let blocks_wide = width.div_ceil(block_size).max(1);
let blocks_tall = height.div_ceil(block_size).max(1);
let pitch_aligned = align_up(blocks_wide, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS);
let height_aligned = align_up(blocks_tall, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS);

View File

@@ -910,7 +910,7 @@ pub fn compose(
// A dim backdrop stands in for the PRMD dim-quad + the live 3D scene behind
// an in-mission screen.
let mut canvas = vec![0u8; (w as usize) * (h as usize) * 4];
for px in canvas.chunks_exact_mut(4) {
for px in canvas.as_chunks_mut::<4>().0 {
px.copy_from_slice(&opts.backdrop);
}
let mut drawn = Vec::new();

View File

@@ -237,7 +237,7 @@ pub fn decode_text(bytes: &[u8]) -> (String, &'static str) {
fn decode_utf16(bytes: &[u8], big_endian: bool) -> String {
let units: Vec<u16> = bytes
.chunks_exact(2)
.as_chunks::<2>().0.iter()
.map(|c| {
if big_endian {
u16::from_be_bytes([c[0], c[1]])

View File

@@ -53,7 +53,7 @@ impl<F: Read + Seek + Send + Sync + 'static> XisoReader<F> {
info!(
"Opened XISO: root directory table at sector {}",
{ let s = volume.root_table.region.sector; s }
{ volume.root_table.region.sector }
);
Ok(Self { volume, file: wrapper })
}

View File

@@ -101,7 +101,7 @@ pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result<Image, Xpr2LoadErr
X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => {
let opaque = matches!(tex.format, X360TextureFormat::X8R8G8B8);
let mut out = tex.data;
for px in out.chunks_exact_mut(4) {
for px in out.as_chunks_mut::<4>().0 {
let (a, r, g, b) = (px[0], px[1], px[2], px[3]);
px[0] = r;
px[1] = g;

View File

@@ -2035,6 +2035,10 @@ fn decode_audio_wav(video: &Path, wav: &Path) -> Result<(), String> {
// ── Video decode + audio (main thread) ────────────────────────────────────────
/// `(pts, rgba_bytes)` as the reader thread forwards them from `ffmpeg`.
#[cfg(not(target_arch = "wasm32"))]
type FrameRx = mpsc::Receiver<(f32, Vec<u8>)>;
/// Spawn an `ffmpeg` process decoding to raw RGBA on stdout, plus a reader
/// thread that chunks it into frames and forwards `(pts, bytes)` over a bounded
/// channel. Optional `-ss start` seeks the input; output pts re-base to 0, so we
@@ -2046,7 +2050,7 @@ fn spawn_video_decoder(
h: u32,
fps: f32,
start: f32,
) -> Result<(Child, mpsc::Receiver<(f32, Vec<u8>)>), String> {
) -> Result<(Child, FrameRx), String> {
let mut cmd = Command::new("ffmpeg");
cmd.arg("-v").arg("error");
if start > 0.0 {
@@ -2177,6 +2181,12 @@ fn scrub_worker(
/// Polls the mpsc channel, updating `IsoState`, `FileBrowserState`, and
/// `PendingFileBytes` as messages arrive.
// Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the
// scheduler injects. The count is the framework's dependency list, not a
// signature anyone calls by hand, and it cannot be reduced without
// bundling into a `SystemParam` struct. clippy's general heuristic does
// not know about the idiom.
#[allow(clippy::too_many_arguments)]
#[cfg(not(target_arch = "wasm32"))]
fn poll_loader_channel(
channels: Res<IsoChannels>,
@@ -2557,7 +2567,7 @@ fn pick_albedo_index(model_name: &str, tex_names: &[String]) -> Option<usize> {
#[cfg(not(target_arch = "wasm32"))]
fn compute_smooth_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
let mut acc = vec![Vec3::ZERO; positions.len()];
for tri in indices.chunks_exact(3) {
for tri in indices.as_chunks::<3>().0 {
let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
if a >= positions.len() || b >= positions.len() || c >= positions.len() {
continue;
@@ -3106,7 +3116,7 @@ fn prepare_models_impl(
// Append the slice's triangles, re-emitting vertices per index
// (no dedup) so each material buffer stays self-contained. A
// reflected instance swaps two corners to keep front faces out.
for tri in sub.indices[(*off).min(end)..end].chunks_exact(3) {
for tri in sub.indices[(*off).min(end)..end].as_chunks::<3>().0 {
let corners = if reflect {
[tri[0], tri[2], tri[1]]
} else {
@@ -3405,6 +3415,12 @@ 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
// scheduler injects. The count is the framework's dependency list, not a
// signature anyone calls by hand, and it cannot be reduced without
// bundling into a `SystemParam` struct. clippy's general heuristic does
// not know about the idiom.
#[allow(clippy::too_many_arguments)]
#[cfg(not(target_arch = "wasm32"))]
fn apply_pak(
mut pending: ResMut<PendingPak>,
@@ -4076,7 +4092,7 @@ fn build_game_snapshot(source: &SourceKind) -> Option<GameSnapshot> {
Some(CharRow { name, faction: c.faction.unwrap_or_default(), faces: c.faces.len() })
})
.collect();
characters.sort_by(|a, b| (a.faction.clone(), a.name.clone()).cmp(&(b.faction.clone(), b.name.clone())));
characters.sort_by_key(|a| (a.faction.clone(), a.name.clone()));
// Combat rosters, keyed by stage where the table self-identifies.
let rosters = gd::load_unit_rosters(&main);
@@ -4742,7 +4758,7 @@ fn build_ship_model(
*nrm = rot(&p.m, nrm);
}
if det < 0.0 {
for tri in sub.indices.chunks_exact_mut(3) {
for tri in sub.indices.as_chunks_mut::<3>().0 {
tri.swap(1, 2);
}
}
@@ -4911,7 +4927,7 @@ fn handle_cutscene_cues_request(
// The naming convention is the obvious route and it is wrong often
// enough to matter -- resolving the region is the only reading that
// yields the right audio.
let voice = (|| {
let voice = {
use sylpheed_formats::{hash::name_hash, media, PakArchive};
let vlang = match lang.pak_code() {
"jpn" => sylpheed_formats::slb::VoiceLang::Japanese,
@@ -4934,7 +4950,7 @@ fn handle_cutscene_cues_request(
named_range,
region,
})
})();
};
let _ = sender.send(IsoLoaderMsg::CutsceneCuesLoaded {
generation,

View File

@@ -50,7 +50,7 @@ pub fn run() {
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(),
resolution: (1280.0, 720.0).into(),
resolution: (1280.0_f32, 720.0_f32).into(),
..default()
}),
..default()

View File

@@ -157,6 +157,12 @@ struct UiEvents<'w> {
cutscenes: EventWriter<'w, RequestCutscenes>,
}
// Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the
// scheduler injects. The count is the framework's dependency list, not a
// signature anyone calls by hand, and it cannot be reduced without
// bundling into a `SystemParam` struct. clippy's general heuristic does
// not know about the idiom.
#[allow(clippy::too_many_arguments)]
fn draw_viewer_ui(
mut contexts: EguiContexts,
mut viewer: ResMut<ViewerState>,
@@ -656,7 +662,7 @@ fn draw_viewer_ui(
ui.strong("Notes");
ui.end_row();
let mut row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| {
let row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| {
ui.label(fmt);
ui.colored_label(c, status);
ui.label(notes);
@@ -1236,7 +1242,7 @@ fn draw_video_player(
// Subtitle + voice controls (right-aligned): language, CC, and Voice.
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let before = subs.lang;
egui::ComboBox::from_id_source("subtitle_lang")
egui::ComboBox::from_id_salt("subtitle_lang")
.selected_text(subs.lang.label())
.show_ui(ui, |ui| {
for lang in SubLang::ALL {

View File

@@ -167,7 +167,7 @@ neither its own branch nor `main`.
## Checks that were kind once
Two rules that look unrelated and are the same failure.
Three instances now, and they are the same failure.
**A check may only soften against a condition it can test.**
@@ -189,6 +189,45 @@ a collaborator exists, so the "yet" was never needed.
stays. That is why they survive review, and why the smell is worth naming:
*leniency with an expiry date nobody set.*
### The third instance was authored dirty, not decayed into
The two above were **correct when written**. The third was not, and it is worth
separating because it arrives by a different route and is caught at a different
moment.
CI's `Clippy` step turned out never to have run — the toolchain shipped without
the component, so `cargo clippy -- -D warnings` died on *"not installed"* on
every commit in the repo's history. Fixing that is two lines. But the tree is
not clippy-clean: the build already emits ~13 rustc warnings that `-D warnings`
promotes to errors. So the fix and the first red result arrive together, and the
first draft paired the two-line fix with `continue-on-error: true` and a comment
saying *delete this line once the debt is paid* — which is precisely an expiry
date nobody set. It was reverted within the hour, on reading #12's own closing
line ruling the same shape out for rustfmt.
The difference that matters:
| | first two | third |
|---|---|---|
| when it was wrong | became wrong later | wrong on the first commit |
| what caused it | the world moved | the tree was already dirty |
| what catches it | auditing old allowances | noticing the impulse at the keyboard |
**This is the default way a check gets written when the tree is not clean yet.**
Not a rare slip — the ordinary shape of the first draft. Whenever a real check
goes in against a tree that does not yet pass it, the softening is *right there*,
it looks like pragmatism, and it comes with a sincere comment promising removal.
The mechanical test still catches it after the fact. The earlier tell is this:
> **If you are writing the softening in the same commit as the check, the thing
> you want is an issue, not a flag.**
A red check that measures something is worth more than a green one that measures
nothing, and it is worth strictly more than a green one that *used to* measure
something. Land the check gating, let it be red, and scope the debt where it can
be read, argued with and closed — #12 for rustfmt, #13 for clippy. An issue has
the expiry date the flag never gets.
`share put <file> --note "…" --for port` records the sender, the time, **the
commit they were on**, and whether their tree was dirty. A capture with no
provenance is not evidence, it is a picture.