Merge pull request 'Clear all 73 clippy lints, and make the Clippy step real' (#14) from fix/clippy-lints into main
Reviewed-on: #14
This commit was merged in pull request #14.
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};
|
||||
@@ -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,9 @@ 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 +1226,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| {
|
||||
@@ -1254,7 +1262,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 +1448,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
|
||||
@@ -1690,12 +1700,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(
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,15 @@ 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 +292,9 @@ 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 }),
|
||||
|
||||
@@ -1131,7 +1131,9 @@ 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])),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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`)
|
||||
//!
|
||||
@@ -339,12 +339,10 @@ fn ascii_runs(bytes: &[u8], min: usize) -> Vec<String> {
|
||||
for &b in bytes {
|
||||
if (0x20..=0x7e).contains(&b) {
|
||||
cur.push(b as char);
|
||||
} else if cur.len() >= min {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
} else {
|
||||
if cur.len() >= min {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
} else {
|
||||
cur.clear();
|
||||
}
|
||||
cur.clear();
|
||||
}
|
||||
}
|
||||
if cur.len() >= min {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -287,13 +287,13 @@ 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,
|
||||
size: u32,
|
||||
stride: u32,
|
||||
pos: &mut Vec<[f32; 3]>,
|
||||
consts: &[(usize, [f64; 4])],
|
||||
seen: &mut std::collections::HashSet<u32>,
|
||||
out: &mut Vec<CapturedDraw>| {
|
||||
let flush = |base: u32,
|
||||
size: u32,
|
||||
stride: u32,
|
||||
pos: &mut Vec<[f32; 3]>,
|
||||
consts: &[(usize, [f64; 4])],
|
||||
seen: &mut std::collections::HashSet<u32>,
|
||||
out: &mut Vec<CapturedDraw>| {
|
||||
let pos = std::mem::take(pos);
|
||||
if base == 0 || stride == 0 || !seen.insert(base) {
|
||||
return;
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -237,7 +237,9 @@ 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]])
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
397
docs/agents/HANDOFF-2026-09-06.md
Normal file
397
docs/agents/HANDOFF-2026-09-06.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Handoff — 2026-09-06
|
||||
|
||||
**For a fresh session on a different machine.** Written to be read cold: it
|
||||
assumes you know nothing about what happened, and it says what is *established*
|
||||
versus what is *someone's claim*.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this project is
|
||||
|
||||
Two things, and they are easy to confuse:
|
||||
|
||||
* **The long game** — *Project Sylpheed: Arc of Deception — Reborn*, a clean-room
|
||||
native port of an Xbox 360 game in Rust + Bevy. Values and behaviour come from
|
||||
the original by **observation and static RE only** — never copied decompiled
|
||||
code. The oracle is **the real game running in Xenia Canary**, never any
|
||||
renderer of ours.
|
||||
* **The work of 2026-09-04/06, which is what this document is about** — moving
|
||||
the project's *working surface* onto a self-hosted Gitea, and getting CI to
|
||||
produce an answer for the first time.
|
||||
|
||||
If you only read one other file, read [`PROTOCOL.md`](PROTOCOL.md).
|
||||
|
||||
## 2. The actors — four, and only two are constrained
|
||||
|
||||
| | what it is | identity | constrained by the gate? |
|
||||
|---|---|---|---|
|
||||
| **the human** | directs everything; the only approver and merger | `fabi` | n/a — is the gate |
|
||||
| **the Pi agent** | supervisor, runs on the Pi beside Gitea | `sylph-pi <pi@sylpheed.local>`, **no Gitea account** | ❌ has `gitea admin`; can mint tokens, edit rules |
|
||||
| **you** (this session) | runs on the x86_64 desktop, holds the push credential | commits as the human ⚠️ | ❌ same carve-out, different mechanism |
|
||||
| **the two loop agents** | Decoder (disc→meaning) and Port (disc→playable) | `sylph-decoder`, `sylph-port`, Write not Admin | ✅ |
|
||||
|
||||
⚠️ **The looping agents are stopped and must stay stopped** until their images
|
||||
carry the Gitea MCP and their issues are ready. Starting them early gives them a
|
||||
brief telling them to read notifications and open issues with no tool that can.
|
||||
|
||||
📌 The two *supervising* agents are the ones the gate does not bind. That is
|
||||
written into `GITEA-SETUP.md` Phase 2 deliberately. Neither has a distinct Gitea
|
||||
identity; both operate through the human's credential or an unlinked git author.
|
||||
**That is a known, unresolved wart**, not an oversight.
|
||||
|
||||
## 3. The machines
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
| **desktop** `fabi-Hyrican-PC` | x86_64, 12 core, 15 GB | agent containers, the repo clone, **the only push credential** |
|
||||
| **the Pi** `raspberrypi.fritz.box` | aarch64, on the LAN | **runs Gitea** (published through a VPS), the CI runner, and the supervising agent |
|
||||
| **Gitea** `git.mc02.dev` | 1.25.5 | resolves to a hosted address — that says nothing about the origin, which is the Pi |
|
||||
|
||||
**File transfer between them is by hand.** The Pi agent has no push credential,
|
||||
so its work arrives as `git bundle` over `scp`, which the human runs. This is the
|
||||
weakest link in the setup: four round trips on 2026-09-04, each needing a
|
||||
password twice, and once a guessed filename that was wrong. A `write:repository`
|
||||
token on the Pi scoped to `pi/*` branches would remove it — **a deliberate
|
||||
decision, deliberately not taken yet.**
|
||||
|
||||
⚠️ **`CARGO_BUILD_JOBS=4`** and limited `-j`. A full-parallel build has
|
||||
OOM-crashed the desktop. One emulator process at a time; Canary runs muted.
|
||||
|
||||
## 4. Where things stand
|
||||
|
||||
### Merged to `main`
|
||||
Nothing since `59649824`. **`main` is protected** — `enable_push=false`, 1
|
||||
approval required, merge and approvals both whitelisted to `fabi` only. Verified
|
||||
behaviourally: a real push was refused with `pre-receive hook declined`, as the
|
||||
repository owner.
|
||||
|
||||
### Open pull requests
|
||||
|
||||
| | head | what | why not merged |
|
||||
|---|---|---|---|
|
||||
| **#10** | `agents/gitea-mcp` @ `a3d99ada` | the Gitea surface: MCP wiring, `gitea-protect`, the runbook, two PROTOCOL rules | waiting on the human. Merge-on-merits: docs and tooling, no `.rs` |
|
||||
| **#14** | `fix/clippy-lints` @ `d8807c4f` | 73 clippy lints → 0 across four crates, `Closes #13` | same |
|
||||
|
||||
🔴 **Both need the admin override to merge.** `fabi` authored them and is the
|
||||
only whitelisted approver, and Gitea bars self-approval — so they can never reach
|
||||
one approval. `block_admin_merge_override` is `false` *deliberately* so that door
|
||||
stays open. Do not tick it.
|
||||
|
||||
### Open issues
|
||||
|
||||
Nine work items, all `state/approved`, awaiting the loop agents:
|
||||
|
||||
```
|
||||
#1 F1 measure (decoder) ←blocks— #2 F1 implement (port)
|
||||
#3 F2 disc gain (decoder) ←blocks— #4 F2 playback gains (port)
|
||||
#5 F3 title cue (decoder)
|
||||
#6 OPTIONS re-propose (port)
|
||||
#8 F5/F6 findings (decoder) ←blocks— #7 F5/F6 port work (port)
|
||||
#9 f6-out-of-sample residue (decoder)
|
||||
```
|
||||
|
||||
Four infrastructure issues:
|
||||
|
||||
* **#11** `state/proposed` — WASM. **Work exists past its shape; see §6.**
|
||||
* **#12** `state/proposed` — rustfmt: 774 hunks, deliberately deferred.
|
||||
* **#13** `state/approved` — clippy; `#14` closes it.
|
||||
* **#15** `state/proposed` — the lint gate floats `@stable`.
|
||||
|
||||
### Unmerged branches
|
||||
|
||||
```
|
||||
agents/gitea-mcp 11 → PR #10
|
||||
fix/clippy-lints 16 → PR #14
|
||||
auto/frame-blend-draw-path 495 the Decoder's corpus — returns via #8
|
||||
auto/port-p6-audio 366 the Port's work — returns via #7
|
||||
```
|
||||
|
||||
The last two are the reason `#7` depends on `#8`: `port/scripts/boot.gd` cites
|
||||
`docs/re/` pages that exist on **neither** its own branch nor `main`.
|
||||
`tools/port/check-citations --for-merge` counts **19** such citations.
|
||||
|
||||
## 5. CI — it produced its first answer on 2026-09-05
|
||||
|
||||
Before that: **23 runs cancelled, 2 waiting, zero successes.** The workflow
|
||||
described GitHub's hosted fleet (`windows-latest`, `macos-latest`, and a
|
||||
`--target x86_64` cross-compile) on a one-runner aarch64 instance, so the run
|
||||
never reached a terminal state — the checks were *unfinished*, not red.
|
||||
|
||||
Now, three jobs, all terminal:
|
||||
|
||||
| job | state | |
|
||||
|---|---|---|
|
||||
| **Native — linux** | **green**, three runs running | `check` `build` `test` `clippy` all pass on aarch64; **207 passed, 0 failed, 14 ignored, 30 suites** |
|
||||
| WASM — Web | red | **#11** |
|
||||
| Formatting | red | **#12** — 774 hunks, identical on `main` |
|
||||
|
||||
The workspace being portable to ARM was unknown before this and is now
|
||||
established. The disk exhaustion that broke the test link is retired: 46 GB
|
||||
reclaimed, `/` at 55%.
|
||||
|
||||
⚠️ **`/var/lib/docker` is still on the Pi's 117 GB SD card while a 916 GB SSD sits
|
||||
at 16%.** This will recur. The fix is `data-root` in `daemon.json` plus a Docker
|
||||
restart, and it wants a moment when every container going down is fine.
|
||||
|
||||
## 6. 🔴 What is in flight and NOT pushed
|
||||
|
||||
**The WASM work.** The Pi agent has `ba6c5da`, bundle at
|
||||
`/tmp/sylph-wasm-compile.bundle` **on the Pi**, base `d8807c4`. It makes
|
||||
`Check WASM compile` exit 0. #11 turned out to be **three stacked blockers**,
|
||||
each invisible until the previous was gone:
|
||||
|
||||
1. `getrandom` needs `--cfg getrandom_backend="wasm_js"` **and** the feature —
|
||||
its own error says either alone is insufficient;
|
||||
2. `sylpheed-formats` declared `tokio` as a **normal** dependency it never used,
|
||||
dragging `tokio/full` → `net` → `mio`, which does not build for wasm32;
|
||||
3. `bevy_egui` needs `--cfg web_sys_unstable_apis`.
|
||||
|
||||
**Two things the human must decide before it lands:**
|
||||
|
||||
* **#11 is `state/proposed` and this goes past its stated shape.** It was written
|
||||
around the getrandom error alone. The tokio removal is a change to another
|
||||
crate, not CI config — look at that one specifically.
|
||||
* **It will not turn the job green.** It unblocks two steps that have never run:
|
||||
`jetli/trunk-action@v0.5.0` and `trunk build --release`. The action's bundled
|
||||
`dist/index.js` names `x86_64-unknown-linux-gnu` once and `aarch64` **never**,
|
||||
so it will fetch an x86_64 binary onto the aarch64 runner. Upstream *does*
|
||||
publish `trunk-aarch64-unknown-linux-gnu.tar.gz` — so this is "the action
|
||||
cannot find it", not "trunk is unavailable on arm". Replacing the install step
|
||||
means picking a version and a method: a decision, not a slip-in.
|
||||
|
||||
## 7. The method lessons — the most transferable part
|
||||
|
||||
One failure shape recurred **five times in two days**, across two agents and the
|
||||
human's assistant. Every instance is: **a property inferred from something
|
||||
adjacent to it, rather than tested directly.**
|
||||
|
||||
| what was inferred | from what | how it failed |
|
||||
|---|---|---|
|
||||
| `main` is protected | the settings page | merging ignores the push whitelist; both agents could have approved each other |
|
||||
| the desktop can't reach Gitea | `curl` being refused | that was a *permission prompt*, not the network |
|
||||
| the tool creates 12 labels | `grep -c '^mklabel'` | one match was the function **definition** |
|
||||
| Gitea is not on the Pi | a DNS lookup | it is, published through a VPS |
|
||||
| CI's clippy == mine | **identical rustfmt output** | rustfmt is output-stable by design; clippy moves lints between groups |
|
||||
|
||||
The last one is the sharpest. `rustfmt 1.8.0` and `1.9.0`, nine months apart,
|
||||
both produce **774** hunks on this tree — so formatting parity carries *no*
|
||||
information about which clippy ran. From it I concluded CI's green must be cached
|
||||
or ungated ("the frozen splash again"). It was neither: `collapsible_else_if` is
|
||||
`warn` on 1.92.0 and **`allow`** on 1.98.1, which is what the runner has.
|
||||
|
||||
> **The check that worked, every time, was counting the same thing with the same
|
||||
> tool against a baseline.**
|
||||
|
||||
Three rules now in `PROTOCOL.md`, each earned:
|
||||
|
||||
* **A finding reaches `main` before the code that cites it.**
|
||||
* **A check may only soften against a condition it can test** — *can this branch
|
||||
tell the difference between "not yet" and "no longer"?*
|
||||
* **If you are writing the softening in the same commit as the check, the thing
|
||||
you want is an issue, not a flag.**
|
||||
|
||||
And the ordering constraint that is not obvious: **the cheap-looking fix for #12
|
||||
is the expensive one.** 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. Measured: 133 files carrying 81% of the debt cannot collide;
|
||||
the real blocker is **21 files**. Land #7/#8 first, then sweep once.
|
||||
|
||||
## 8. Resuming — concrete first steps
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git switch fix/clippy-lints # PR #14's head, d8807c4f
|
||||
python3 tools/gitea-protect --verify # expect: protection holds. rc=0
|
||||
```
|
||||
|
||||
Then, in order of what is actually blocking:
|
||||
|
||||
1. **Merge #10 and #14** — human, with the admin override. Everything else is
|
||||
downstream: the loop agents clone `main`, which has none of this.
|
||||
2. **Decide #11's scope**, then fetch `/tmp/sylph-wasm-compile.bundle` from the
|
||||
Pi and push it.
|
||||
3. **Decide the trunk-action replacement** (§6).
|
||||
4. **Then Phase 7** of [`GITEA-SETUP.md`](GITEA-SETUP.md): start the **decoder
|
||||
alone**, watch one full iteration — notifications polled, a PR rather than a
|
||||
bare push, no merge button — before starting the port.
|
||||
|
||||
### Credentials, all on the desktop, all `chmod 600`
|
||||
|
||||
```
|
||||
~/.sylph-git-credentials write:repository — the push credential
|
||||
~/.sylph-gitea-token-decoder the decoder agent's, four scopes
|
||||
~/.sylph-gitea-token-port the port agent's, four scopes
|
||||
~/.sylph-claude-token long-lived Claude auth for the containers
|
||||
```
|
||||
|
||||
`~/.sylph-gitea-api-token` (`write:issue`) lives **on the Pi**, because that is
|
||||
where `tools/gitea-setup` runs. Never paste any of them into chat.
|
||||
|
||||
⚠️ **This desktop was updated to `rustc 1.98.1` on 2026-09-06** to match the
|
||||
runner. Anything verified here *before* that ran on 1.92.0 — the fmt counts (774)
|
||||
and test counts (207/0/14) matched CI exactly and therefore carry; a clippy
|
||||
result from before does not.
|
||||
|
||||
## 9. Standing constraints
|
||||
|
||||
* **Never commit game content**, under any directory name. 545 MB reached a
|
||||
branch on 2026-09-04 under a name the ignore list did not happen to mention.
|
||||
`.gitignore` now describes the *shape* (`/export*/`, media by extension).
|
||||
* **Agents never merge**, never push to `main`, never rewrite history. One
|
||||
force-push was authorised, once, to fix commit authorship before merge — a
|
||||
recorded exception, not a precedent.
|
||||
* **The Explorer shows static data only** — the ISO, the embedded PE, savegames.
|
||||
Never anything generated by a Sylpheed run.
|
||||
* **Never self-screenshot**; ask the human to capture. Ask before a
|
||||
watch-and-verify `--ui` launch.
|
||||
* **Never judge emulator crash or stability from a Bash-launched run.**
|
||||
|
||||
## 10. The second machine — measured on `fabi-MS-7C37`, 2026-09-06
|
||||
|
||||
**§§1–9 were written on `fabi-Hyrican-PC`.** This section was written on the
|
||||
other desktop, and every line is a command run *here*. Nothing above is carried
|
||||
across untested — §7 is the reason.
|
||||
|
||||
Everything §§4–5 say about the **server** checks out exactly: 13 open issues,
|
||||
#10 and #14 open and `mergeable`, Gitea 1.25.5. What does not transfer is the
|
||||
**box**.
|
||||
|
||||
### What is different here, and what it costs
|
||||
|
||||
| § | says | here |
|
||||
|---|---|---|
|
||||
| 8 | four `~/.sylph-*` credentials, `chmod 600` | **none exist.** `~/.git-credentials` holds a `fabi@git.mc02.dev` token that reads `branch_protections` — an endpoint both agent tokens are refused on — so it is not an agent token. Whether it can *push* is untested |
|
||||
| 8.2 | fetch `/tmp/sylph-wasm-compile.bundle` from the Pi | **`raspberrypi.fritz.box` does not resolve here**, and there is no host key for it. `ba6c5da` is unreachable from this machine |
|
||||
| 8 | "updated to `rustc 1.98.1` … to match the runner" | that was the *other* desktop. Here `stable` = **1.90.0**, with a `1.92.0` also installed |
|
||||
| 5 | the agent images | no `sylph-decoder` / `sylph-port` image on this box |
|
||||
|
||||
So of §8's four steps, only **1** (the human's merges) and **3** (the
|
||||
trunk-action decision, an edit to `ci.yml`) can be done from here. **2 and 4
|
||||
cannot, at all.**
|
||||
|
||||
### 🔴 `git clone` of this repository does not work
|
||||
|
||||
Three attempts died on `GnuTLS recv error (-9)` / `Recv failure: Connection
|
||||
reset by peer`, at 135 MB, 4.6 MB and 73 MB. A default clone fetches **every**
|
||||
branch, and `auto/port-p6-audio`'s history still carries the 545 MB of game
|
||||
content §9 describes — gone from the tree, still in the pack.
|
||||
|
||||
What works, in seconds:
|
||||
|
||||
```bash
|
||||
git -c http.version=HTTP/1.1 clone --filter=blob:none \
|
||||
https://git.mc02.dev/fabi/Sylpheed.git
|
||||
```
|
||||
|
||||
Blobs fault in on demand. The tree is **108 MB in 1 007 files** (98 MB of it
|
||||
`docs/re/captures/`), and it was verified byte-for-byte against
|
||||
`git ls-tree -r -l` — worth doing, because the clone printed *"checkout failed"*
|
||||
partway and then recovered silently. ⚠️ One cost: `check-citations` reaches into
|
||||
peer branches, so on a blobless clone it faults blobs over that same flaky link
|
||||
and takes minutes rather than seconds.
|
||||
|
||||
### The numbers that carry, and the one that does not
|
||||
|
||||
Expected values taken from §§4–5 *before* running, per R2:
|
||||
|
||||
| | expected | measured here | |
|
||||
|---|---|---|---|
|
||||
| `gitea-protect --verify` | protection holds | **holds**, 10/10, rc=0 | ✅ carries |
|
||||
| `cargo fmt --all -- --check` | 774 hunks | **774**, across **154 files** | ✅ carries |
|
||||
| `check-citations --for-merge` | 19 | **19**, rc=1 | ✅ carries |
|
||||
| `cargo test --workspace` | 207 / 0 / 14, 30 suites | **207 / 0 / 14, 30** | ✅ carries — see below |
|
||||
| `cargo clippy --workspace -- -D warnings` | *not comparable* | **exit 101** | 🔴 diverges |
|
||||
|
||||
154 files corroborates rather than adds: §7's split of the fmt debt into "133
|
||||
that cannot collide" and "21 that are the real blocker" sums to exactly it.
|
||||
|
||||
`gitea-protect --verify` needs `SYLPH_GIT_CREDENTIALS=~/.git-credentials` here,
|
||||
since its default is one of the four missing files.
|
||||
|
||||
### 🔴 The test count carries — and that is what is wrong with it
|
||||
|
||||
It matched to the unit. It should not be read as the two runs having done the
|
||||
same thing, because they did not.
|
||||
|
||||
**15 files** under `crates/sylpheed-formats/tests/*_disc.rs` resolve the disc
|
||||
through a `disc_root()` whose second branch is a **hardcoded absolute path** —
|
||||
`/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA,
|
||||
Europe) (En,Ja)`. So `unset SYLPHEED_DISC` does **not** disable them: that
|
||||
directory exists on this box, and the disc suites *ran*.
|
||||
|
||||
| | CI (job 794) | here |
|
||||
|---|---|---|
|
||||
| test **execution** wall time | **2.4 s**, slowest suite 0.29 s | **1 936 s**, `mesh_consistency_disc` alone **1 220 s** |
|
||||
| tally | 207 / 0 / 14, 30 suites | **identical** |
|
||||
|
||||
Identical because the skip path is `eprintln!("SKIP: …")` **plus an early return
|
||||
from a test that still passes**. A skipped disc test and a fully exercised one
|
||||
both score `1 passed`. And the message is invisible either way — `cargo test`
|
||||
captures a passing test's stderr, so *neither* log contains a `SKIP:` line. The
|
||||
absence of one proves nothing; only the clock separated these two runs.
|
||||
|
||||
So ask the question this project keeps having to ask — **what would this check
|
||||
still report if the corpus were entirely absent?** — and the answer is
|
||||
`207 / 0 / 14`.
|
||||
|
||||
Two consequences, and the first is good news:
|
||||
|
||||
* this run is **strictly stronger evidence than CI's**: 207 passed with the disc
|
||||
corpus actually exercised, on x86_64, at `593b378`.
|
||||
* `SYLPHEED_DISC` looks like the control and is not one. Whether the disc suites
|
||||
run is a property of *the machine's directory layout*, invisible in the command
|
||||
and in the output. Worth an issue — it is the `.gitignore` lesson again,
|
||||
**naming an instance instead of the condition.**
|
||||
|
||||
📌 **A sixth instance for §7, and it is mine.** I inferred "the counts cannot
|
||||
match" from "the fallback resolves" — an adjacent property, never tested — and
|
||||
wrote it into this section before the run finished. The run returned 207 / 0 / 14.
|
||||
The correction was the same as every other time in that table: run it, and count.
|
||||
|
||||
### 🔴 The clippy gate genuinely disagrees between the two toolchains
|
||||
|
||||
This is **#15 ceasing to be theoretical**, and it needs stating carefully,
|
||||
because it is §7's lesson 5 arriving from the other side.
|
||||
|
||||
Both sides measured with the same command, both versions read rather than assumed:
|
||||
|
||||
* **the runner** — `rustc 1.98.1 (48a229cea 2026-09-01)`, read out of job 794's
|
||||
own log. `cargo clippy --workspace -- -D warnings` finishes in 9.26 s with no
|
||||
lint. Native is green on runs **206, 207, 208 and 209** — four consecutive,
|
||||
not three.
|
||||
* **here** — `rustc 1.90.0` / `clippy 0.1.90`. The same command exits **101**, on
|
||||
exactly one lint:
|
||||
|
||||
```
|
||||
error: parameter is only used in recursion
|
||||
--> crates/sylpheed-formats/src/vfs.rs:85:10
|
||||
= note: `-D clippy::only-used-in-recursion` implied by `-D warnings`
|
||||
```
|
||||
|
||||
Without `-D warnings` it is a warning and clippy exits 0 — so the disagreement
|
||||
sits exactly at the gate.
|
||||
|
||||
**What this does not mean.** It does not mean #14 is wrong, and it does not mean
|
||||
CI's green is cached or ungated. That is the inference §7 records as the sharpest
|
||||
of its five failures, and the evidence points the other way: the runner's log
|
||||
shows the step running, on this code, clean. `d8807c4` already collapsed one
|
||||
`else { if }` *"so both toolchains agree"* — this is the same class, one lint on.
|
||||
|
||||
What it establishes is narrower and more useful: **the gate's verdict depends on
|
||||
which stable happened to be current**, and there is now a named reproducible case
|
||||
rather than an argument. That is #15's evidence.
|
||||
|
||||
📌 For whoever picks up #13/#14: *"clippy is clean"* is not a property of the
|
||||
tree, it is a property of the tree **and** a toolchain. Until #15 pins one, say
|
||||
which one you ran.
|
||||
|
||||
### Refutation attempted, and survived
|
||||
|
||||
Per the adversarial duty — §6's claim that `sylpheed-formats` declares `tokio` as
|
||||
a normal dependency **it never uses**. It *is* referenced, in `ship.rs` and
|
||||
`xiso.rs`, which looked like a refutation. It is not: every one of those sits
|
||||
inside a `#[cfg(test)]` module (`ship.rs:497`, `xiso.rs:178`). The library's
|
||||
non-test code does not use tokio, so moving it to `dev-dependencies` is sound and
|
||||
the `examples/` targets keep compiling. **Claim survives** — recorded because a
|
||||
survived challenge is stronger than an unchallenged one, not because it changed
|
||||
anything.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user