Compare commits
4 Commits
fix/decode
...
pi/clippy-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08416dbd0b | ||
|
|
bc79817488 | ||
|
|
d8dcc1fc29 | ||
|
|
4ac5c9f419 |
@@ -1,29 +0,0 @@
|
||||
# getrandom 0.3 refuses to build for wasm32-unknown-unknown unless it is told
|
||||
# which backend to use. The target has no OS entropy source, so the crate will
|
||||
# not guess: it wants the `wasm_js` backend named explicitly, AND the matching
|
||||
# feature enabled on the crate itself. Its own error is unusually clear that one
|
||||
# without the other is not enough:
|
||||
#
|
||||
# error: The wasm32-unknown-unknown targets are not supported by default; you
|
||||
# may need to enable the "wasm_js" configuration flag. Note that enabling the
|
||||
# `wasm_js` feature flag alone is insufficient.
|
||||
#
|
||||
# This is the cfg half. The feature half is a wasm32-only dependency in
|
||||
# crates/sylpheed-viewer/Cargo.toml — that is the only crate reaching getrandom
|
||||
# here, transitively through `ahash`.
|
||||
#
|
||||
# Scoped to the wasm target, so native builds are untouched. Note that a
|
||||
# RUSTFLAGS environment variable, if one is ever set, replaces this rather than
|
||||
# adding to it.
|
||||
#
|
||||
# The second cfg is bevy_egui's. Its web clipboard support calls web-sys APIs
|
||||
# that are still gated behind an unstable flag, and it refuses to build without
|
||||
# it rather than silently dropping the feature:
|
||||
#
|
||||
# error: bevy_egui uses unstable APIs to support clipboard on web.
|
||||
#
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
'--cfg', 'getrandom_backend="wasm_js"',
|
||||
'--cfg', 'web_sys_unstable_apis',
|
||||
]
|
||||
66
.github/workflows/ci.yml
vendored
66
.github/workflows/ci.yml
vendored
@@ -31,24 +31,6 @@ env:
|
||||
# So: one job, on the machine that exists, building for the machine that exists.
|
||||
# If a second architecture is ever wanted here it needs a second RUNNER, not a
|
||||
# second matrix row.
|
||||
#
|
||||
# ── The toolchain is PINNED, in three places, deliberately ───────────────────
|
||||
#
|
||||
# All three jobs used `dtolnay/rust-toolchain@stable`, which resolves to whatever
|
||||
# stable is on the day the job runs. A lint gate that floats is not a gate: the
|
||||
# same tree goes green or red depending on the date, and this repo has already
|
||||
# produced a disagreement between two people reading the same commit (#15).
|
||||
# `collapsible_else_if` is the example — `warn` on 1.92.0, `allow`-by-default
|
||||
# pedantic on 1.98.1, so a clean local run and a red CI run were both correct.
|
||||
#
|
||||
# `1.98.1` is the version run 206 resolved, and `docker/ci/Dockerfile` pins the
|
||||
# same one, so `docker/ci/run cargo clippy …` on a desktop is a true stand-in for
|
||||
# this workflow rather than an approximation of it.
|
||||
#
|
||||
# To bump: change all three `dtolnay/rust-toolchain@` refs here AND the `FROM
|
||||
# rust:<version>-bookworm` in `docker/ci/Dockerfile` in one commit, so the two
|
||||
# can never drift apart silently. A bump is a change to the gate and belongs in
|
||||
# its own PR, where the new lints it turns on are the diff.
|
||||
|
||||
jobs:
|
||||
# ── Native build, on the one runner there is ────────────────────────────────
|
||||
@@ -60,15 +42,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
# Pinned — see the toolchain note at the top of this file.
|
||||
#
|
||||
# This action installs a MINIMAL profile: rustc, cargo, rust-std and no
|
||||
# `stable` installs a MINIMAL profile: rustc, cargo, rust-std and no
|
||||
# more. Components have to be named. Without this line the Clippy step
|
||||
# below dies on "'cargo-clippy' is not installed for the toolchain
|
||||
# 'stable-aarch64-unknown-linux-gnu'" — which is not a lint result, it
|
||||
# is the step never having run. The `fmt` job below always got this
|
||||
# right; this one never did.
|
||||
uses: dtolnay/rust-toolchain@1.98.1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
@@ -97,17 +77,6 @@ jobs:
|
||||
- name: Run tests
|
||||
run: cargo test --workspace
|
||||
|
||||
# The tally above cannot tell you what it verified. `cargo test` reports
|
||||
# the same count whether the disc corpus was exercised or entirely
|
||||
# absent -- a gated suite that skips still counts as passed, and the
|
||||
# `ignored` column is a static count of `#[ignore]` attributes that cannot
|
||||
# move at runtime. Issue #16. This prints what the run ACTUALLY had, from
|
||||
# a file, because a passing test's stdout is captured and would be
|
||||
# invisible in exactly this log.
|
||||
- name: Report which corpora the tests actually had
|
||||
if: always()
|
||||
run: cat target/sylpheed-corpus-report.txt || echo "(no corpus report produced -- did corpus_report run?)"
|
||||
|
||||
# This step has never once executed on this codebase: the toolchain above
|
||||
# shipped without the component, so every run died on "not installed"
|
||||
# before clippy saw a line of source. Its result was never pass or fail,
|
||||
@@ -134,7 +103,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain + WASM target
|
||||
uses: dtolnay/rust-toolchain@1.98.1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
@@ -142,16 +111,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Trunk
|
||||
# v0.5.0 selects the download by PLATFORM ONLY and never consults the
|
||||
# architecture -- `case 'linux': arch = 'x86_64-unknown-linux-gnu'` --
|
||||
# so on this aarch64 runner it fetches an x86_64 binary. v0.5.1 adds
|
||||
# `process.arch` with 'x64' -> 'x86_64', 'arm64' -> 'aarch64' and
|
||||
# core.setFailed otherwise, so a wrong arch now fails loudly instead of
|
||||
# silently. It also moves the download host thedodd/trunk ->
|
||||
# trunk-rs/trunk (trunk moved repositories; v0.5.0 still points at the
|
||||
# old one), and swaps io.mv for io.cp, which is what avoids EXDEV on a
|
||||
# self-hosted runner whose /tmp is a separate filesystem -- ours.
|
||||
uses: jetli/trunk-action@v0.5.1
|
||||
uses: jetli/trunk-action@v0.5.0
|
||||
|
||||
- name: Check WASM compile
|
||||
run: >
|
||||
@@ -163,17 +123,11 @@ jobs:
|
||||
- name: Build WASM release with Trunk
|
||||
run: trunk build --release
|
||||
|
||||
# No artifact upload. actions/upload-artifact@v4 hard-refuses on Gitea --
|
||||
# Gitea presents as GHES and @actions/artifact v2+ aborts there
|
||||
# (go-gitea/gitea#31256, #36024). Nothing consumes `web-dist`: it had
|
||||
# exactly one reference in this repository, the line that produced it,
|
||||
# and there is no download-artifact and no second workflow. The job's
|
||||
# purpose -- proving the web build compiles -- is met by the step above.
|
||||
# Add it back when something consumes the bundle, and decide then between
|
||||
# actions/upload-artifact@v3 (the GHES guidance names v3.2.2 / the
|
||||
# -node20 tag, so check the runner's node first) and the Gitea-specific
|
||||
# christopherHX/gitea-upload-artifact@v4, which is a third-party
|
||||
# dependency and therefore a decision, not a swap.
|
||||
- name: Upload WASM dist artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: dist/
|
||||
|
||||
# ── Format check ─────────────────────────────────────────────────────────────
|
||||
fmt:
|
||||
@@ -181,7 +135,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.98.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -18,13 +18,6 @@ Thumbs.db
|
||||
# Local dev overrides
|
||||
.env
|
||||
|
||||
# The static-analysis database `tools/zq.py` reads by default. A build artefact
|
||||
# of several hundred MB (`sylph-xexdb dis ... --db sylpheed.db`), and zq.py
|
||||
# now tells people to put it exactly here -- so it must never be committable.
|
||||
# One `git add -A` would otherwise put it in public history for good.
|
||||
/sylpheed.db
|
||||
/sylpheed.db.wal
|
||||
|
||||
# Trunk build output
|
||||
dist/
|
||||
__pycache__/
|
||||
|
||||
795
Cargo.lock
generated
795
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,6 @@ members = [
|
||||
"crates/sylpheed-viewer",
|
||||
"crates/sylpheed-cli",
|
||||
"crates/sylpheed-export",
|
||||
"crates/sylpheed-xex",
|
||||
"crates/sylpheed-ppc",
|
||||
"crates/sylpheed-xexdb",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Fabian
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -194,36 +194,6 @@ enum ScreenCommands {
|
||||
/// `--build`**, which is why it is a flag and not the default.
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
/// Pose every element at this KEYFRAME TIME instead of at its resting
|
||||
/// pose (60 units = 1 second). The resting pose is each element's last
|
||||
/// *hold* keyframe, picked independently of every other element, so it is
|
||||
/// not the screen at any one moment: it omits anything still moving (the
|
||||
/// title's light sweeps hold off the right edge) and freezes a transient
|
||||
/// at its PEAK (the title's five two-frame flashes burn forever).
|
||||
/// ⚠️ This help used to end "Prefer `--settle`". That is WITHDRAWN and was
|
||||
/// never measured: scored against a live capture of the JP title, settle
|
||||
/// gives RMSE 40.210 and rest 41.690 — a margin of 1.48 against that
|
||||
/// instrument's own noise floor of 1.2, which is NOT decisive. `--settle`
|
||||
/// also has its own failure mode (25.5 % of elements are mid-ramp at their
|
||||
/// screen's settle instant). Neither is established as better; pick by what
|
||||
/// you are measuring. See `docs/re/structures/ui-resting-pose.md`.
|
||||
#[arg(long, conflicts_with = "settle")]
|
||||
at: Option<u32>,
|
||||
/// Pose every element at the instant the screen is SETTLED, derived from
|
||||
/// the disc: the midpoint of the longest interval containing no keyframe
|
||||
/// of any element. Prints the window it used, whose width is how much the
|
||||
/// midpoint is worth — a narrow one means the bundle never settles.
|
||||
/// ⚠️ That is **38 % of the screen builds this command renders** (185 of
|
||||
/// 491 carrying two or more keyframe times) and 39 % of the wider set
|
||||
/// `--all` admits (862 of 2 211), mostly `loop*` fragments. This help used
|
||||
/// to say "42 % of them" without saying of WHAT: 42 % was 731/1 758 over
|
||||
/// composable bundles, computed before the keyframe record-layout fix,
|
||||
/// which times a group's final pose and so admits ~450 bundles that
|
||||
/// previously had only one timed keyframe. ⚠️ NOT established as better
|
||||
/// than the resting pose — see the note on `--at`. See
|
||||
/// `docs/re/structures/ui-settle-time.md`.
|
||||
#[arg(long)]
|
||||
settle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -335,7 +305,7 @@ async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("sylpheed=info".parse().unwrap()),
|
||||
.add_directive("sylpheed=info".parse().unwrap())
|
||||
)
|
||||
.init();
|
||||
|
||||
@@ -343,33 +313,24 @@ async fn main() -> Result<()> {
|
||||
|
||||
match cli.command {
|
||||
Commands::Extract { iso, output } => cmd_extract(&iso, &output).await,
|
||||
Commands::List { iso, filter } => cmd_list(&iso, filter).await,
|
||||
Commands::List { iso, filter } => cmd_list(&iso, filter).await,
|
||||
Commands::Sniff { dir, unknown_only } => cmd_sniff(&dir, unknown_only),
|
||||
Commands::Texture { cmd } => match cmd {
|
||||
TextureCommands::Info { file } => cmd_texture_info(&file),
|
||||
TextureCommands::Info { file } => cmd_texture_info(&file),
|
||||
TextureCommands::Export { file, output } => cmd_texture_export(&file, &output),
|
||||
},
|
||||
Commands::Mesh { cmd } => match cmd {
|
||||
MeshCommands::Info { file } => cmd_mesh_info(&file),
|
||||
MeshCommands::Render {
|
||||
file,
|
||||
output,
|
||||
size,
|
||||
yaw,
|
||||
pitch,
|
||||
dist,
|
||||
row,
|
||||
only,
|
||||
} => cmd_mesh_render(&file, &output, size, yaw, pitch, dist, row, only),
|
||||
MeshCommands::Render { file, output, size, yaw, pitch, dist, row, only } => {
|
||||
cmd_mesh_render(&file, &output, size, yaw, pitch, dist, row, only)
|
||||
}
|
||||
},
|
||||
Commands::Pak { cmd } => match cmd {
|
||||
PakCommands::List { pak, idxd_only } => cmd_pak_list(&pak, idxd_only),
|
||||
PakCommands::Dump { pak, hash } => cmd_pak_dump(&pak, &hash),
|
||||
PakCommands::Textures {
|
||||
pak,
|
||||
output,
|
||||
verbose,
|
||||
} => cmd_pak_textures(&pak, &output, verbose),
|
||||
PakCommands::List { pak, idxd_only } => cmd_pak_list(&pak, idxd_only),
|
||||
PakCommands::Dump { pak, hash } => cmd_pak_dump(&pak, &hash),
|
||||
PakCommands::Textures { pak, output, verbose } => {
|
||||
cmd_pak_textures(&pak, &output, verbose)
|
||||
}
|
||||
},
|
||||
Commands::Audio { cmd } => match cmd {
|
||||
AudioCommands::Info { file } => cmd_audio_info(&file),
|
||||
@@ -391,10 +352,8 @@ async fn main() -> Result<()> {
|
||||
black,
|
||||
all,
|
||||
primitives,
|
||||
at,
|
||||
settle,
|
||||
} => cmd_screen_render(
|
||||
&pak, &output, build, focus, animated, black, all, primitives, at, settle,
|
||||
&pak, &output, build, focus, animated, black, all, primitives,
|
||||
),
|
||||
},
|
||||
Commands::Save { cmd } => match cmd {
|
||||
@@ -522,9 +481,7 @@ fn cmd_screen_info(pak: &Path, want: Option<usize>, geometry: bool, all: bool) -
|
||||
"{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} {rest}",
|
||||
el.index,
|
||||
el.name,
|
||||
el.parent
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or_else(|| "-".into()),
|
||||
el.parent.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
|
||||
format!("{:#x}", el.kind),
|
||||
format!("({},{})", el.pivot_x, el.pivot_y),
|
||||
el.keyframes.len(),
|
||||
@@ -628,42 +585,12 @@ fn cmd_screen_render(
|
||||
black: bool,
|
||||
all: bool,
|
||||
primitives: bool,
|
||||
at: Option<u32>,
|
||||
settle: bool,
|
||||
) -> Result<()> {
|
||||
use sylpheed_formats::ui_layout::{self, ComposeOptions};
|
||||
let builds = screen_builds(pak, all)?;
|
||||
let idx = pick_build(&builds, want)?;
|
||||
let bytes = &builds[idx].1;
|
||||
let b = ui_layout::parse_build(bytes).context("build did not parse")?;
|
||||
let at = if settle {
|
||||
match (b.settle_window(), b.settle_time()) {
|
||||
(Some((lo, hi)), Some(t)) => {
|
||||
// Report the width, not just the answer. A 4-unit window and a
|
||||
// 190-unit one give the same kind of number and mean entirely
|
||||
// different things.
|
||||
println!(
|
||||
"settle window [{lo}, {hi}] = {} units ({:.2} s) -> posing at t={t}{}",
|
||||
hi - lo,
|
||||
(hi - lo) as f64 / 60.0,
|
||||
if hi - lo < 30 {
|
||||
" ⚠️ narrow — this bundle may never settle"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
Some(t)
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"no settle window (fewer than two distinct keyframe times) — using rest()"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
at
|
||||
};
|
||||
let screen = ui_layout::compose(
|
||||
&b,
|
||||
bytes,
|
||||
@@ -676,7 +603,6 @@ fn cmd_screen_render(
|
||||
ComposeOptions::default().backdrop
|
||||
},
|
||||
include_primitives: primitives,
|
||||
at,
|
||||
},
|
||||
None,
|
||||
);
|
||||
@@ -697,40 +623,16 @@ fn cmd_screen_render(
|
||||
screen.height
|
||||
);
|
||||
if !screen.missing.is_empty() {
|
||||
println!(
|
||||
" sprites that did not resolve/decode: {:?}",
|
||||
screen.missing
|
||||
);
|
||||
println!(" sprites that did not resolve/decode: {:?}", screen.missing);
|
||||
}
|
||||
// 🔴 Report the INDEX, the KIND and WHY, not just the name. A kind-`0x4`
|
||||
// ghost instance carries its template's name, so a bare name list shows
|
||||
// `ptlogo1.t32` twice and reads as "the logo is missing" when what is
|
||||
// skipped is two motion-trail ghosts sitting at alpha 0 off-screen. That
|
||||
// misreading cost this project a wrong finding sent to another agent.
|
||||
let undrawn: Vec<String> = b
|
||||
let undrawn: Vec<&str> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| !screen.drawn.contains(&e.index))
|
||||
.map(|e| {
|
||||
let why = if e.name.ends_with(".prm") {
|
||||
"untextured primitive, needs --primitives"
|
||||
} else if e.name.ends_with(".rat") {
|
||||
"animation, needs --animated"
|
||||
} else if e.kind == 0x4 {
|
||||
"kind 0x4 ghost instance"
|
||||
} else if e.rest().map(|k| k.fade >> 24) == Some(0) {
|
||||
"transparent at its pose"
|
||||
} else {
|
||||
"no reason established"
|
||||
};
|
||||
format!("[{}] {} ({why})", e.index, e.name)
|
||||
})
|
||||
.map(|e| e.name.as_str())
|
||||
.collect();
|
||||
if !undrawn.is_empty() {
|
||||
println!(" not drawn ({}):", undrawn.len());
|
||||
for u in &undrawn {
|
||||
println!(" {u}");
|
||||
}
|
||||
println!(" not drawn ({}): {undrawn:?}", undrawn.len());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -738,7 +640,9 @@ fn cmd_screen_render(
|
||||
// ── save file ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn cmd_save_info(file: &Path, all: bool) -> Result<()> {
|
||||
use sylpheed_formats::savegame::{self, Confidence, DevelopState, FieldKind, GHAD_LAYOUT};
|
||||
use sylpheed_formats::savegame::{
|
||||
self, Confidence, DevelopState, FieldKind, GHAD_LAYOUT,
|
||||
};
|
||||
let raw = std::fs::read(file).context("read save")?;
|
||||
let save = savegame::parse(&raw).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
@@ -788,11 +692,7 @@ fn cmd_save_info(file: &Path, all: bool) -> Result<()> {
|
||||
" {} +{:<3} {:<14} {}",
|
||||
mark(f.confidence),
|
||||
f.offset,
|
||||
if f.name.is_empty() {
|
||||
"(unnamed)"
|
||||
} else {
|
||||
f.name
|
||||
},
|
||||
if f.name.is_empty() { "(unnamed)" } else { f.name },
|
||||
value
|
||||
);
|
||||
if all && !f.note.is_empty() {
|
||||
@@ -801,10 +701,7 @@ fn cmd_save_info(file: &Path, all: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
let dev = save.develop_state();
|
||||
let owned = dev
|
||||
.iter()
|
||||
.filter(|d| **d == DevelopState::Developed)
|
||||
.count();
|
||||
let owned = dev.iter().filter(|d| **d == DevelopState::Developed).count();
|
||||
let ready = dev
|
||||
.iter()
|
||||
.filter(|d| **d == DevelopState::Developable)
|
||||
@@ -848,36 +745,16 @@ fn cmd_audio_info(file: &Path) -> Result<()> {
|
||||
println!("{} {}", "Audio:".green().bold(), file.display());
|
||||
println!(" Codec : {}", info.codec.label().yellow());
|
||||
let opt = |v: Option<String>| v.unwrap_or_else(|| "—".dimmed().to_string());
|
||||
println!(
|
||||
" Channels : {}",
|
||||
opt(info.channels.map(|c| c.to_string()))
|
||||
);
|
||||
println!(
|
||||
" Sample rate: {}",
|
||||
opt(info.sample_rate.map(|r| format!("{r} Hz")))
|
||||
);
|
||||
println!(
|
||||
" Bit depth : {}",
|
||||
opt(info.bits_per_sample.map(|b| format!("{b}-bit")))
|
||||
);
|
||||
if let Some(b) = info.avg_bytes_per_sec {
|
||||
println!(" Byte rate : {} B/s (declared)", b.to_string().yellow());
|
||||
}
|
||||
println!(" Channels : {}", opt(info.channels.map(|c| c.to_string())));
|
||||
println!(" Sample rate: {}", opt(info.sample_rate.map(|r| format!("{r} Hz"))));
|
||||
println!(" Bit depth : {}", opt(info.bits_per_sample.map(|b| format!("{b}-bit"))));
|
||||
if let Some(d) = info.duration_secs {
|
||||
let how = if info.codec == sylpheed_formats::AudioCodec::Xma {
|
||||
" (from the declared byte rate, not decoded)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(" Duration : {d:.2} s{how}");
|
||||
println!(" Duration : {d:.2} s");
|
||||
}
|
||||
if let Some(p) = info.xma_packets {
|
||||
println!(" XMA packets: {} (2048 B each)", p.to_string().yellow());
|
||||
}
|
||||
println!(
|
||||
" Size : {} bytes",
|
||||
info.size_bytes.to_string().yellow()
|
||||
);
|
||||
println!(" Size : {} bytes", info.size_bytes.to_string().yellow());
|
||||
if info.codec.needs_decoder() {
|
||||
println!(
|
||||
" {} decode not supported (needs an XMA2 decoder + the sound-bank descriptor)",
|
||||
@@ -910,7 +787,7 @@ async fn cmd_extract(iso_path: &Path, output_dir: &Path) -> Result<()> {
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.cyan} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("█▉▊▋▌▍▎▏ "),
|
||||
.progress_chars("█▉▊▋▌▍▎▏ ")
|
||||
);
|
||||
|
||||
let stats = reader.extract_all(output_dir).await?;
|
||||
@@ -939,11 +816,7 @@ async fn cmd_extract(iso_path: &Path, output_dir: &Path) -> Result<()> {
|
||||
// ── list ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_list(iso_path: &Path, filter: Option<String>) -> Result<()> {
|
||||
println!(
|
||||
"{} {}",
|
||||
"Listing".green().bold(),
|
||||
iso_path.display().to_string().cyan()
|
||||
);
|
||||
println!("{} {}", "Listing".green().bold(), iso_path.display().to_string().cyan());
|
||||
|
||||
let mut reader = sylpheed_formats::xiso::open_iso(iso_path).await?;
|
||||
let files = reader.list_all_files().await?;
|
||||
@@ -988,9 +861,7 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||
|
||||
for file in &files {
|
||||
let Ok(bytes) = assets.read(file) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = assets.read(file) else { continue; };
|
||||
let fmt = identify_format(&bytes);
|
||||
let label = fmt.extension_hint();
|
||||
*counts.entry(label).or_insert(0) += 1;
|
||||
@@ -1011,10 +882,9 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
let hex_preview = if label == "bin" && bytes.len() >= 8 {
|
||||
format!(
|
||||
" {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}",
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]
|
||||
)
|
||||
.dimmed()
|
||||
.to_string()
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7]
|
||||
).dimmed().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -1028,7 +898,11 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
let mut summary: Vec<_> = counts.into_iter().collect();
|
||||
summary.sort_by_key(|&(_, count)| std::cmp::Reverse(count));
|
||||
for (fmt, count) in summary {
|
||||
println!(" {:>6} .{}", count.to_string().yellow(), fmt);
|
||||
println!(
|
||||
" {:>6} .{}",
|
||||
count.to_string().yellow(),
|
||||
fmt
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1037,7 +911,8 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> {
|
||||
// ── texture info ──────────────────────────────────────────────────────────
|
||||
|
||||
fn cmd_texture_info(file: &Path) -> Result<()> {
|
||||
let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
let bytes = std::fs::read(file)
|
||||
.with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
|
||||
use sylpheed_formats::texture::X360Texture;
|
||||
let tex = X360Texture::from_xpr2(&bytes)
|
||||
@@ -1045,27 +920,16 @@ fn cmd_texture_info(file: &Path) -> Result<()> {
|
||||
|
||||
let d = tex.format.desc();
|
||||
println!("{} {}", "Texture:".green().bold(), file.display());
|
||||
println!(
|
||||
" Resolution : {}×{}",
|
||||
tex.width.to_string().yellow(),
|
||||
tex.height.to_string().yellow()
|
||||
);
|
||||
println!(" Resolution : {}×{}", tex.width.to_string().yellow(), tex.height.to_string().yellow());
|
||||
println!(
|
||||
" Format : {} ({:?}) · {} bpp, {}",
|
||||
tex.format.gpu_name().yellow(),
|
||||
tex.format,
|
||||
d.bpp,
|
||||
if d.compressed {
|
||||
"compressed"
|
||||
} else {
|
||||
"uncompressed"
|
||||
},
|
||||
if d.compressed { "compressed" } else { "uncompressed" },
|
||||
);
|
||||
println!(" Mip levels : {}", tex.mip_levels);
|
||||
println!(
|
||||
" Data size : {} bytes",
|
||||
tex.data.len().to_string().yellow()
|
||||
);
|
||||
println!(" Data size : {} bytes", tex.data.len().to_string().yellow());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1073,12 +937,13 @@ fn cmd_texture_info(file: &Path) -> Result<()> {
|
||||
// ── texture export ────────────────────────────────────────────────────────
|
||||
|
||||
fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> {
|
||||
let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
let bytes = std::fs::read(file)
|
||||
.with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
|
||||
use sylpheed_formats::texture::X360Texture;
|
||||
let tex = X360Texture::from_xpr2(&bytes)?;
|
||||
let rgba =
|
||||
decode_to_rgba8(&tex).with_context(|| format!("decoding {:?} texture", tex.format))?;
|
||||
let rgba = decode_to_rgba8(&tex)
|
||||
.with_context(|| format!("decoding {:?} texture", tex.format))?;
|
||||
|
||||
image::save_buffer(
|
||||
output,
|
||||
@@ -1095,11 +960,7 @@ fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> {
|
||||
tex.width,
|
||||
tex.height,
|
||||
tex.format,
|
||||
if tex.is_cubemap {
|
||||
" (cubemap face 0)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if tex.is_cubemap { " (cubemap face 0)" } else { "" },
|
||||
output.display().to_string().cyan(),
|
||||
);
|
||||
Ok(())
|
||||
@@ -1199,17 +1060,11 @@ fn cmd_mesh_info(file: &Path) -> Result<()> {
|
||||
};
|
||||
let mut maxedges: Vec<f32> = sub
|
||||
.indices
|
||||
.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.as_chunks::<3>().0.iter()
|
||||
.map(|t| edge(t[0], t[1]).max(edge(t[1], t[2])).max(edge(t[0], t[2])))
|
||||
.collect();
|
||||
maxedges.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let median = maxedges
|
||||
.get(maxedges.len() / 2)
|
||||
.copied()
|
||||
.unwrap_or(1.0)
|
||||
.max(1e-6);
|
||||
let median = maxedges.get(maxedges.len() / 2).copied().unwrap_or(1.0).max(1e-6);
|
||||
let spanning = maxedges.iter().filter(|&&e| e > 6.0 * median).count();
|
||||
println!(
|
||||
" sub{si}: {nv} v, {} tris | degenerate {degen}, unref-verts {unref}, spanning>6×med {spanning}, idx_max {imax}/{}{}",
|
||||
@@ -1329,16 +1184,10 @@ fn cmd_mesh_render(
|
||||
(lo[2] + hi[2]) * 0.5,
|
||||
];
|
||||
let (scale, cell) = if multi {
|
||||
let extent = (hi[0] - lo[0])
|
||||
.max(hi[1] - lo[1])
|
||||
.max(hi[2] - lo[2])
|
||||
.max(1e-3);
|
||||
let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]).max(1e-3);
|
||||
let col = i % cols;
|
||||
let row = i / cols;
|
||||
(
|
||||
CELL / extent,
|
||||
[col as f32 * grid_pitch, -(row as f32) * grid_pitch, 0.0],
|
||||
)
|
||||
(CELL / extent, [col as f32 * grid_pitch, -(row as f32) * grid_pitch, 0.0])
|
||||
} else {
|
||||
(1.0, [0.0, 0.0, 0.0])
|
||||
};
|
||||
@@ -1375,9 +1224,7 @@ fn cmd_mesh_render(
|
||||
let med = {
|
||||
let mut e: Vec<f32> = sub
|
||||
.indices
|
||||
.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.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| {
|
||||
@@ -1404,9 +1251,7 @@ fn cmd_mesh_render(
|
||||
};
|
||||
for place in &mine {
|
||||
let f = |i: usize| {
|
||||
let p = place
|
||||
.map(|pl| pl.apply(sub.positions[i]))
|
||||
.unwrap_or(sub.positions[i]);
|
||||
let p = place.map(|pl| pl.apply(sub.positions[i])).unwrap_or(sub.positions[i]);
|
||||
[
|
||||
(p[0] - center[0]) * scale * mirror[0] + cell[0],
|
||||
(p[1] - center[1]) * scale * mirror[1] + cell[1],
|
||||
@@ -1599,9 +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);
|
||||
let src = tex.data.as_chunks::<4>().0;
|
||||
let dst = rgba.as_chunks_mut::<4>().0;
|
||||
for (px, out) in src.iter().zip(dst) {
|
||||
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
|
||||
@@ -1797,10 +1640,8 @@ fn emit_t8ad(
|
||||
}
|
||||
match t8ad::parse(slice) {
|
||||
Some(img) => {
|
||||
let out = output.join(format!(
|
||||
"{hash:08x}_{stem}_{}x{}.png",
|
||||
img.width, img.height
|
||||
));
|
||||
let out =
|
||||
output.join(format!("{hash:08x}_{stem}_{}x{}.png", img.width, img.height));
|
||||
image::save_buffer(
|
||||
&out,
|
||||
&img.rgba,
|
||||
@@ -1822,7 +1663,8 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> {
|
||||
use sylpheed_formats::{lsta, ratc, t8ad};
|
||||
|
||||
let arc = PakArchive::open(pak).with_context(|| format!("opening {}", pak.display()))?;
|
||||
std::fs::create_dir_all(output).with_context(|| format!("creating {}", output.display()))?;
|
||||
std::fs::create_dir_all(output)
|
||||
.with_context(|| format!("creating {}", output.display()))?;
|
||||
|
||||
println!(
|
||||
"{} {} → {}",
|
||||
@@ -1890,14 +1732,7 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> {
|
||||
} else {
|
||||
safe_name(&child.name)
|
||||
};
|
||||
emit_t8ad(
|
||||
&payload[child.offset..end],
|
||||
hash,
|
||||
&stem,
|
||||
output,
|
||||
verbose,
|
||||
&mut stats,
|
||||
)?;
|
||||
emit_t8ad(&payload[child.offset..end], hash, &stem, output, verbose, &mut stats)?;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -55,19 +55,33 @@ license.workspace = true
|
||||
# a squash-merge can orphan, and no way for the exporter to be built against a
|
||||
# decoder it was never tested with. A decoder change and the exporter change it
|
||||
# requires now land in the same commit or not at all.
|
||||
# ✅ UNPINNED 2026-09-20. The pin above was deliberate and carried its own exit
|
||||
# condition -- "revert to the path dependency the day the tag is an ancestor of
|
||||
# `main`" -- because while it held, `sylpheed-cli` built from the WORKSPACE crate
|
||||
# and the exporter from the tag, so `tools/port/verify-screen` compared two eras
|
||||
# instead of detecting drift. `formats-pin-2026-09-01` is now an ancestor of
|
||||
# `main` (measured), so the two read one decoder again.
|
||||
# PINNED BY TAG, which is what MISSION section 2 prescribes and what the tagging
|
||||
# rule exists for: "the RE agent tags when it lands something you need and tells
|
||||
# you over the message channel -- that is how you stay current without floating."
|
||||
# That is exactly what happened here.
|
||||
#
|
||||
# 🔴 AND THE PIN HAD A COST NOBODY PRICED IN: it made this repository depend on
|
||||
# its OWN history by tag. The issue-#49 history rewrite replaced every commit,
|
||||
# the locked rev vanished, and the build broke for every clean checkout while
|
||||
# still working on the machine that did the rewrite, because its `~/.cargo/git`
|
||||
# still held the old object (PR #60). A path dependency cannot fail that way.
|
||||
sylpheed-formats = { path = "../sylpheed-formats" }
|
||||
# The tag carries the CORRECTED keyframe association: a placement group is an
|
||||
# 8-byte header then `frames` x {u32 time; 36-byte pose}, so pose 0's time is the
|
||||
# group's lead-in word and EVERY POSE IS TIMED, including the last. The working
|
||||
# tree's copy still has the retired `SYLPHEED_KF_TIME_SHIFT` knob -- a superseded
|
||||
# partial fix that got the association right but left pose 0 untimed, which is
|
||||
# why testing it moved the untimed frame from last to first instead of removing
|
||||
# it. The old reading is behind `SYLPHEED_KF_TIME_LEGACY=1` here.
|
||||
#
|
||||
# 🔴 THE COST, STATED: `sylpheed-cli` builds from the WORKSPACE crate, so until
|
||||
# this lands on `main` the exporter and the reference renderer read DIFFERENT
|
||||
# decoders and `tools/port/verify-screen` is comparing two eras rather than
|
||||
# detecting drift. `tools/port/verify-capture` is unaffected -- it compares the
|
||||
# port against oracle CAPTURES and never touches the CLI -- and it is the check
|
||||
# that matters. Revert to the path dependency the day the tag is an ancestor of
|
||||
# `main`.
|
||||
# Bumped c -> d 2026-08-29. What I wanted from the new state: `d` carries parser
|
||||
# and `audio.rs` changes on top of `c`. ⚠️ Its headline change -- Reborn's
|
||||
# renderer drawing `rotation_deg`, and `compose` drawing a leaf that carries
|
||||
# geometry -- does NOT reach this port from here: `sylpheed-cli` builds from the
|
||||
# WORKSPACE crate, so the reference renderer stays unrotated until the tag lands
|
||||
# on `main`. This bump is for the parser, not for the renderer.
|
||||
sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-09-01" }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -7,8 +7,7 @@ use std::process::Command;
|
||||
use sylpheed_formats::media;
|
||||
|
||||
fn main() {
|
||||
let disc =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let src = media::DirectorySource::new(&disc);
|
||||
for bank in ["BGM_103.slb", "BGM_102.slb", "BGM_001.slb"] {
|
||||
match media::sound_bank_riffs(&src, bank) {
|
||||
@@ -20,42 +19,22 @@ fn main() {
|
||||
let w = std::env::temp_dir().join(format!("bk_{i}.wav"));
|
||||
let _ = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
|
||||
.arg(&p)
|
||||
.arg(&w)
|
||||
.output();
|
||||
.arg(&p).arg(&w).output();
|
||||
let out = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-v", "info", "-i"])
|
||||
.arg(&w)
|
||||
.args(["-af", "astats=measure_perchannel=none", "-f", "null", "-"])
|
||||
.output()
|
||||
.unwrap();
|
||||
.output().unwrap();
|
||||
let t = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
let get = |k: &str| {
|
||||
t.lines()
|
||||
.find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
|
||||
.unwrap_or_else(|| "?".into())
|
||||
};
|
||||
let get = |k: &str| t.lines().find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
|
||||
.unwrap_or_else(|| "?".into());
|
||||
let dur = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.arg(&w)
|
||||
.output()
|
||||
.ok()
|
||||
.args(["-v","error","-show_entries","format=duration","-of","csv=p=0"])
|
||||
.arg(&w).output().ok()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
|
||||
r.len(),
|
||||
dur,
|
||||
get("Peak level dB:"),
|
||||
get("RMS level dB:")
|
||||
);
|
||||
println!(" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
|
||||
r.len(), dur, get("Peak level dB:"), get("RMS level dB:"));
|
||||
let _ = std::fs::remove_file(&p);
|
||||
let _ = std::fs::remove_file(&w);
|
||||
}
|
||||
|
||||
@@ -18,26 +18,19 @@
|
||||
use sylpheed_formats::media;
|
||||
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let src = media::DirectorySource::new(&root);
|
||||
const WANT: [usize; 2] = [3_876_864, 3_930_112];
|
||||
let (mut found, mut matches) = (0usize, Vec::new());
|
||||
for n in 0..=199u32 {
|
||||
let name = format!("BGM_{n:03}.slb");
|
||||
let Ok(riffs) = media::sound_bank_riffs(&src, &name) else {
|
||||
continue;
|
||||
};
|
||||
if riffs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Ok(riffs) = media::sound_bank_riffs(&src, &name) else { continue };
|
||||
if riffs.is_empty() { continue }
|
||||
found += 1;
|
||||
let sizes: Vec<usize> = riffs.iter().map(|r| r.len()).collect();
|
||||
// Compare on the DATA payload the port sums, not on the RIFF wrapper:
|
||||
// a wrapper differs by header bytes and would hide a real collision.
|
||||
let near = sizes
|
||||
.iter()
|
||||
.any(|s| WANT.iter().any(|w| s.abs_diff(*w) < 4096));
|
||||
let near = sizes.iter().any(|s| WANT.iter().any(|w| s.abs_diff(*w) < 4096));
|
||||
if near {
|
||||
matches.push((name.clone(), sizes.clone()));
|
||||
}
|
||||
@@ -46,10 +39,7 @@ fn main() {
|
||||
for (n, s) in &matches {
|
||||
println!(" {n:<14} wave sizes {s:?}");
|
||||
}
|
||||
println!(
|
||||
"\n {} bank(s) carry a wave within 4 KiB of {WANT:?}",
|
||||
matches.len()
|
||||
);
|
||||
println!("\n {} bank(s) carry a wave within 4 KiB of {WANT:?}", matches.len());
|
||||
println!(" Exactly 1 means the census EXCLUDES alternatives and is a real third");
|
||||
println!(" leg. More than 1 means the byte match does not distinguish BGM_103,");
|
||||
println!(" and \"three legs\" is two. Zero means this reader cannot see the");
|
||||
|
||||
@@ -16,40 +16,27 @@
|
||||
//!
|
||||
//! This prints what the differences actually look like, so the reading is judged
|
||||
//! against the names rather than accepted as plausible.
|
||||
use std::collections::BTreeSet;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let ar = pak::PakArchive::open(format!("{root}/dat/GP_DIALOG.pak")).expect("GP_DIALOG.pak");
|
||||
let sets: Vec<Option<BTreeSet<String>>> = ar
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let by = ar.read(e).ok()?;
|
||||
if !ratc::is_ratc(&by) {
|
||||
return None;
|
||||
}
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|el| el.name.clone()).collect())
|
||||
})
|
||||
.collect();
|
||||
let sets: Vec<Option<BTreeSet<String>>> = ar.entries().iter().map(|e| {
|
||||
let by = ar.read(e).ok()?;
|
||||
if !ratc::is_ratc(&by) { return None }
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|el| el.name.clone()).collect())
|
||||
}).collect();
|
||||
|
||||
let (mut same, mut diff, mut pairs) = (0usize, 0usize, 0usize);
|
||||
let mut shown = 0;
|
||||
for i in (0..sets.len().saturating_sub(1)).step_by(2) {
|
||||
let (Some(a), Some(b)) = (&sets[i], &sets[i + 1]) else {
|
||||
continue;
|
||||
};
|
||||
let (Some(a), Some(b)) = (&sets[i], &sets[i + 1]) else { continue };
|
||||
pairs += 1;
|
||||
if a == b {
|
||||
same += 1;
|
||||
println!(
|
||||
" entries {i:>3}/{:<3} IDENTICAL sets, {} element(s)",
|
||||
i + 1,
|
||||
a.len()
|
||||
);
|
||||
println!(" entries {i:>3}/{:<3} IDENTICAL sets, {} element(s)", i + 1, a.len());
|
||||
continue;
|
||||
}
|
||||
diff += 1;
|
||||
@@ -62,37 +49,19 @@ fn main() {
|
||||
if (10..=15).contains(&i) {
|
||||
let sp = |x: &BTreeSet<String>| x.iter().filter(|n| n.ends_with(".t32")).count();
|
||||
let stage = |x: &BTreeSet<String>| -> Vec<String> {
|
||||
let mut v: Vec<String> = x
|
||||
.iter()
|
||||
.filter_map(|n| {
|
||||
n.strip_prefix("pzstg")
|
||||
.and_then(|r| r.get(..2))
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect();
|
||||
v.sort();
|
||||
v.dedup();
|
||||
v
|
||||
let mut v: Vec<String> = x.iter().filter_map(|n| n.strip_prefix("pzstg")
|
||||
.and_then(|r| r.get(..2)).map(|s| s.to_string())).collect();
|
||||
v.sort(); v.dedup(); v
|
||||
};
|
||||
println!(
|
||||
" entries {i:>3}/{:<3} stages {:?} vs {:?} sprites {} vs {}",
|
||||
i + 1,
|
||||
stage(a),
|
||||
stage(b),
|
||||
sp(a),
|
||||
sp(b)
|
||||
);
|
||||
println!(" entries {i:>3}/{:<3} stages {:?} vs {:?} sprites {} vs {}",
|
||||
i + 1, stage(a), stage(b), sp(a), sp(b));
|
||||
}
|
||||
if shown < 3 {
|
||||
shown += 1;
|
||||
let only_a: Vec<_> = a.difference(b).cloned().collect();
|
||||
let only_b: Vec<_> = b.difference(a).cloned().collect();
|
||||
println!(
|
||||
" entries {i:>3}/{:<3} differ: {} only-in-first, {} only-in-second",
|
||||
i + 1,
|
||||
only_a.len(),
|
||||
only_b.len()
|
||||
);
|
||||
println!(" entries {i:>3}/{:<3} differ: {} only-in-first, {} only-in-second",
|
||||
i + 1, only_a.len(), only_b.len());
|
||||
println!(" first : {:?}", &only_a[..only_a.len().min(4)]);
|
||||
println!(" second : {:?}", &only_b[..only_b.len().min(4)]);
|
||||
}
|
||||
@@ -102,17 +71,12 @@ fn main() {
|
||||
// different dialogs and the whole adjacent-pairing premise is wrong -- which
|
||||
// is a stronger statement than "the language reading is untested".
|
||||
let btns = |s: &Option<BTreeSet<String>>| -> usize {
|
||||
s.as_ref()
|
||||
.map_or(0, |x| x.iter().filter(|n| n.contains("btn")).count())
|
||||
s.as_ref().map_or(0, |x| x.iter().filter(|n| n.contains("btn")).count())
|
||||
};
|
||||
let mut mismatched = 0;
|
||||
for i in (0..sets.len().saturating_sub(1)).step_by(2) {
|
||||
if sets[i].is_none() || sets[i + 1].is_none() {
|
||||
continue;
|
||||
}
|
||||
if btns(&sets[i]) != btns(&sets[i + 1]) {
|
||||
mismatched += 1
|
||||
}
|
||||
if sets[i].is_none() || sets[i + 1].is_none() { continue }
|
||||
if btns(&sets[i]) != btns(&sets[i + 1]) { mismatched += 1 }
|
||||
}
|
||||
println!("\n adjacent pairs whose BUTTON COUNTS differ: {mismatched}");
|
||||
println!(" A language pair cannot. Every one of these is two different dialogs.");
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
// 🔴 WIDENED 2026-08-31 to every pak, to check the Decoder's rival search
|
||||
// independently. They report zero four-button builds within 6 px of
|
||||
// 259/329/399/469 anywhere on the disc, which turns "another dialog with
|
||||
@@ -27,66 +26,40 @@ fn main() {
|
||||
// with a different reader, because its whole content is an absence.
|
||||
const WANT: [i32; 4] = [259, 329, 399, 469];
|
||||
const TOL: i32 = 6;
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
|
||||
.flatten().map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
|
||||
paks.sort();
|
||||
let (mut hits, mut scanned) = (0usize, 0usize);
|
||||
for path in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(path) else {
|
||||
continue;
|
||||
};
|
||||
let arch = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
scanned += 1;
|
||||
// Any button-shaped record, not just `pcbtn`: a rival need not share the
|
||||
// naming convention, and restricting by name would answer a narrower
|
||||
// question than the one asked.
|
||||
let mut rows: Vec<(String, i32)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.name.contains("btn"))
|
||||
.filter_map(|el| el.rest().map(|r| (el.name.clone(), r.y)))
|
||||
.collect();
|
||||
if rows.is_empty() {
|
||||
continue;
|
||||
}
|
||||
rows.sort_by_key(|r| r.1);
|
||||
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
|
||||
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
if rows.len() == 4
|
||||
&& ys
|
||||
.iter()
|
||||
.zip(WANT.iter())
|
||||
.all(|(a, b)| (a - b).abs() <= TOL)
|
||||
{
|
||||
hits += 1;
|
||||
println!(
|
||||
" {arch} entry {i:>2} {} record(s): {}",
|
||||
rows.len(),
|
||||
rows.iter()
|
||||
.map(|r| r.0.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
println!(" rows {ys:?} gaps {gaps:?}");
|
||||
}
|
||||
let Ok(ar) = pak::PakArchive::open(path) else { continue };
|
||||
let arch = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
scanned += 1;
|
||||
// Any button-shaped record, not just `pcbtn`: a rival need not share the
|
||||
// naming convention, and restricting by name would answer a narrower
|
||||
// question than the one asked.
|
||||
let mut rows: Vec<(String, i32)> = b.elements.iter()
|
||||
.filter(|el| el.name.contains("btn"))
|
||||
.filter_map(|el| el.rest().map(|r| (el.name.clone(), r.y)))
|
||||
.collect();
|
||||
if rows.is_empty() { continue }
|
||||
rows.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
|
||||
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
if rows.len() == 4 && ys.iter().zip(WANT.iter()).all(|(a, b)| (a - b).abs() <= TOL) {
|
||||
hits += 1;
|
||||
println!(" {arch} entry {i:>2} {} record(s): {}", rows.len(),
|
||||
rows.iter().map(|r| r.0.as_str()).collect::<Vec<_>>().join(" "));
|
||||
println!(" rows {ys:?} gaps {gaps:?}");
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n {scanned} build(s) scanned across {} pak(s); {hits} match the",
|
||||
paks.len()
|
||||
);
|
||||
}
|
||||
println!("\n {scanned} build(s) scanned across {} pak(s); {hits} match the",
|
||||
paks.len());
|
||||
println!(" DIFFICULTY row signature within +/-{TOL} px.");
|
||||
println!(" Expected: exactly 2 -- the EN/JP pair. More means a RIVAL exists and");
|
||||
println!(" the geometric identification is not unique; fewer means this reader");
|
||||
|
||||
@@ -8,49 +8,31 @@
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let disc =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let ar = PakArchive::open(format!("{disc}/dat/GP_TITLE.pak")).expect("open");
|
||||
let e = &ar.entries()[4]; // entry 4 = the English title
|
||||
let bundle = ar.read(e).expect("read");
|
||||
let b = ui_layout::parse_build(&bundle).expect("parse");
|
||||
println!(
|
||||
"build has {} elements, {} records",
|
||||
b.elements.len(),
|
||||
b.records.len()
|
||||
);
|
||||
println!("build has {} elements, {} records", b.elements.len(), b.records.len());
|
||||
let mut names: Vec<&String> = b.records.keys().collect();
|
||||
names.sort();
|
||||
println!("records: {names:?}");
|
||||
for el in &b.elements {
|
||||
if !el.name.starts_with("ptloop") {
|
||||
continue;
|
||||
}
|
||||
if !el.name.starts_with("ptloop") { continue; }
|
||||
let r = el.rest();
|
||||
println!(
|
||||
"\nPARENT {} sprite={:?} -> rest scale {:?} rot {:?}",
|
||||
el.name,
|
||||
el.sprite,
|
||||
r.map(|r| (r.scale_x, r.scale_y)),
|
||||
r.map(|r| r.rotation_deg)
|
||||
);
|
||||
println!("\nPARENT {} sprite={:?} -> rest scale {:?} rot {:?}", el.name, el.sprite,
|
||||
r.map(|r| (r.scale_x, r.scale_y)), r.map(|r| r.rotation_deg));
|
||||
if let Some(&(off, size)) = b.records.get(&el.name) {
|
||||
match ui_layout::parse_build(&bundle[off..off + size]) {
|
||||
Some(leaf) => {
|
||||
println!(
|
||||
" LEAF {} parses: {} element(s)",
|
||||
el.name,
|
||||
leaf.elements.len()
|
||||
);
|
||||
println!(" LEAF {} parses: {} element(s)", el.name, leaf.elements.len());
|
||||
for le in &leaf.elements {
|
||||
let lr = le.rest();
|
||||
println!(
|
||||
" {:<20} rest scale {:?} rot {:?} pos {:?}",
|
||||
println!(" {:<20} rest scale {:?} rot {:?} pos {:?}",
|
||||
le.name,
|
||||
lr.map(|r| (r.scale_x, r.scale_y)),
|
||||
lr.map(|r| r.rotation_deg),
|
||||
lr.map(|r| (r.x, r.y))
|
||||
);
|
||||
lr.map(|r| (r.x, r.y)));
|
||||
for k in &le.keyframes {
|
||||
println!(" t={:?} scale=({},{}) rot={} pos=({},{}) fade={:#010x} u4={} u8={}",
|
||||
k.time, k.scale_x, k.scale_y, k.rotation_deg, k.x, k.y,
|
||||
|
||||
@@ -26,14 +26,15 @@
|
||||
//! and adds the one they could not run: the same two, restricted to the records
|
||||
//! **this port actually animates**. A disc-wide 0.00 % violation rate says
|
||||
//! nothing about my six screens if all six sit in the exceptional tail.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// The records the port animates: the plate glow, the five menu focus records,
|
||||
/// and the title's two sweeps. Named rather than pattern-matched, because the
|
||||
/// point is to check the ones that are shipped, not the ones that match a glob.
|
||||
const SHIPPED: &[&str] = &[
|
||||
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f", "ptloop01", "ptloop02",
|
||||
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f",
|
||||
"ptloop01", "ptloop02",
|
||||
];
|
||||
|
||||
/// Which header word to read as the loop length. `0x08` is the decoded one;
|
||||
@@ -43,19 +44,14 @@ const SHIPPED: &[&str] = &[
|
||||
static mut OFFSET: usize = 8;
|
||||
|
||||
fn main() {
|
||||
let off: usize = std::env::args()
|
||||
.find_map(|a| a.strip_prefix("--offset=").and_then(|v| v.parse().ok()))
|
||||
.unwrap_or(8);
|
||||
let off: usize = std::env::args().find_map(|a| a.strip_prefix("--offset=")
|
||||
.and_then(|v| v.parse().ok())).unwrap_or(8);
|
||||
unsafe { OFFSET = off };
|
||||
println!(" reading the loop length at header +0x{off:02x}");
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
|
||||
.flatten().map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
|
||||
paks.sort();
|
||||
|
||||
let (mut total, mut exact, mut holds, mut violations) = (0usize, 0usize, 0usize, 0usize);
|
||||
@@ -63,24 +59,14 @@ fn main() {
|
||||
let mut shipped: BTreeMap<String, (i64, i64)> = BTreeMap::new();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(ar) = pak::PakArchive::open(p) else { continue };
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
for (rn, &(o, s)) in &b.records {
|
||||
if o + off + 4 > by.len() || o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
if &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
if o + off + 4 > by.len() || o + s > by.len() { continue }
|
||||
if &by[o..o + 4] != b"RATC" { continue }
|
||||
// 🔴 THE FALSIFIER IS RUN AT NEIGHBOURING OFFSETS TOO. The
|
||||
// Decoder's struct-layout control showed that a homogeneous
|
||||
// repeated table type-checks at every field boundary, so an
|
||||
@@ -91,28 +77,15 @@ fn main() {
|
||||
// "confirmation" without asking whether it discriminates the
|
||||
// OFFSET or merely the file.
|
||||
let len = u32::from_be_bytes(by[o + off..o + off + 4].try_into().unwrap()) as i64;
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
|
||||
let maxt = lb.elements.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0) as i64;
|
||||
if maxt == 0 {
|
||||
continue;
|
||||
} // static: declares no cycle at all
|
||||
.max().unwrap_or(0) as i64;
|
||||
if maxt == 0 { continue } // static: declares no cycle at all
|
||||
total += 1;
|
||||
let slack = len - maxt;
|
||||
*slack_hist.entry(slack).or_default() += 1;
|
||||
if slack == 0 {
|
||||
exact += 1
|
||||
} else if slack > 0 {
|
||||
holds += 1
|
||||
} else {
|
||||
violations += 1
|
||||
}
|
||||
if slack == 0 { exact += 1 } else if slack > 0 { holds += 1 } else { violations += 1 }
|
||||
let stem = rn.trim_end_matches(".rat");
|
||||
if SHIPPED.contains(&stem) {
|
||||
shipped.entry(stem.to_string()).or_insert((len, maxt));
|
||||
@@ -122,68 +95,36 @@ fn main() {
|
||||
}
|
||||
|
||||
println!("disc-wide, records with timed keyframes: {total}");
|
||||
println!(
|
||||
" +08 == max t (exact) : {exact:5} {:5.1} %",
|
||||
pc(exact, total)
|
||||
);
|
||||
println!(
|
||||
" +08 > max t (a hold) : {holds:5} {:5.1} %",
|
||||
pc(holds, total)
|
||||
);
|
||||
println!(
|
||||
" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %",
|
||||
pc(violations, total)
|
||||
);
|
||||
println!(" +08 == max t (exact) : {exact:5} {:5.1} %", pc(exact, total));
|
||||
println!(" +08 > max t (a hold) : {holds:5} {:5.1} %", pc(holds, total));
|
||||
println!(" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %", pc(violations, total));
|
||||
println!("\nslack distribution, most common first:");
|
||||
let mut h: Vec<_> = slack_hist.iter().collect();
|
||||
h.sort_by_key(|&(_, n)| std::cmp::Reverse(*n));
|
||||
for (k, n) in h.iter().take(8) {
|
||||
println!(" slack {k:>6} : {n}");
|
||||
}
|
||||
for (k, n) in h.iter().take(8) { println!(" slack {k:>6} : {n}"); }
|
||||
|
||||
println!("\nthe records THIS PORT animates:");
|
||||
println!(
|
||||
" {:<12} {:>6} {:>7} {:>7}",
|
||||
"record", "+0x08", "max t", "slack"
|
||||
);
|
||||
println!(" {:<12} {:>6} {:>7} {:>7}", "record", "+0x08", "max t", "slack");
|
||||
let (mut ship_exact, mut ship_hold, mut ship_bad) = (0, 0, 0);
|
||||
for (n, (len, maxt)) in &shipped {
|
||||
let slack = len - maxt;
|
||||
match slack {
|
||||
0 => ship_exact += 1,
|
||||
s if s > 0 => ship_hold += 1,
|
||||
_ => ship_bad += 1,
|
||||
}
|
||||
println!(
|
||||
" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
|
||||
if slack < 0 { " 🔴 FALSIFIED" } else { "" }
|
||||
);
|
||||
match slack { 0 => ship_exact += 1, s if s > 0 => ship_hold += 1, _ => ship_bad += 1 }
|
||||
println!(" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
|
||||
if slack < 0 { " 🔴 FALSIFIED" } else { "" });
|
||||
}
|
||||
println!("\n shipped: {ship_exact} exact, {ship_hold} hold, {ship_bad} falsified");
|
||||
if shipped.len() < SHIPPED.len() {
|
||||
let missing: Vec<_> = SHIPPED
|
||||
.iter()
|
||||
.filter(|s| !shipped.contains_key(**s))
|
||||
.collect();
|
||||
let missing: Vec<_> = SHIPPED.iter().filter(|s| !shipped.contains_key(**s)).collect();
|
||||
println!(" ⚠️ not found on the disc: {missing:?} -- a name the port ships and");
|
||||
println!(" this control never checked is worse than a violation it found.");
|
||||
}
|
||||
println!(
|
||||
"\n verdict: {}",
|
||||
if ship_bad > 0 {
|
||||
"🔴 the reading fails on a record the port animates -- do NOT adopt"
|
||||
} else if ship_hold == 0 {
|
||||
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
|
||||
} else {
|
||||
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
|
||||
}
|
||||
);
|
||||
println!("\n verdict: {}", if ship_bad > 0 {
|
||||
"🔴 the reading fails on a record the port animates -- do NOT adopt"
|
||||
} else if ship_hold == 0 {
|
||||
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
|
||||
} else {
|
||||
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
|
||||
});
|
||||
}
|
||||
|
||||
fn pc(n: usize, d: usize) -> f64 {
|
||||
if d == 0 {
|
||||
0.0
|
||||
} else {
|
||||
100.0 * n as f64 / d as f64
|
||||
}
|
||||
}
|
||||
fn pc(n: usize, d: usize) -> f64 { if d == 0 { 0.0 } else { 100.0 * n as f64 / d as f64 } }
|
||||
|
||||
@@ -11,49 +11,30 @@
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
|
||||
.flatten().map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
|
||||
paks.sort();
|
||||
let (mut records, mut in_bounds, mut magic, mut parsed, mut timed) = (0, 0, 0, 0, 0);
|
||||
let (mut untimed, mut all_at_zero) = (0usize, 0usize);
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(ar) = pak::PakArchive::open(p) else { continue };
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for &(o, s) in b.records.values() {
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
for (_, &(o, s)) in &b.records {
|
||||
records += 1;
|
||||
if o + 12 > by.len() || o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
if o + 12 > by.len() || o + s > by.len() { continue }
|
||||
in_bounds += 1;
|
||||
if &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
if &by[o..o + 4] != b"RATC" { continue }
|
||||
magic += 1;
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
|
||||
parsed += 1;
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
let maxt = lb.elements.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
.max().unwrap_or(0);
|
||||
// 🔴 `maxt == 0` merges two different populations, and the
|
||||
// Decoder's cause -- `.max()` returning `Some(0)` -- is only one
|
||||
// of them. A record with NO timed keyframe has no largest
|
||||
@@ -62,16 +43,10 @@ fn main() {
|
||||
// content. Both of us called all 1 530 "the question has no
|
||||
// meaning"; that is true of one group and an assumption about
|
||||
// the other.
|
||||
let any_timed = lb
|
||||
.elements
|
||||
.iter()
|
||||
let any_timed = lb.elements.iter()
|
||||
.any(|el| el.keyframes.iter().any(|k| k.time.is_some()));
|
||||
if maxt == 0 {
|
||||
if any_timed {
|
||||
all_at_zero += 1
|
||||
} else {
|
||||
untimed += 1
|
||||
}
|
||||
if any_timed { all_at_zero += 1 } else { untimed += 1 }
|
||||
continue;
|
||||
}
|
||||
timed += 1;
|
||||
@@ -80,10 +55,8 @@ fn main() {
|
||||
}
|
||||
println!(" records declared by parse_build : {records}");
|
||||
println!(" within the entry's bounds : {in_bounds}");
|
||||
println!(
|
||||
" carrying the RATC magic : {magic} <- {} dropped here",
|
||||
in_bounds - magic
|
||||
);
|
||||
println!(" carrying the RATC magic : {magic} <- {} dropped here",
|
||||
in_bounds - magic);
|
||||
println!(" parsing as a nested build : {parsed}");
|
||||
println!(" with a largest keyframe time > 0: {timed}");
|
||||
println!(" of the {} excluded:", untimed + all_at_zero);
|
||||
|
||||
@@ -11,31 +11,18 @@
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let ar = pak::PakArchive::open(format!("{root}/dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let (mut total, mut hits, mut multipose) = (0usize, 0usize, 0usize);
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
if !ratc::is_ratc(&by) { continue }
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
for (name, &(o, s)) in &b.records {
|
||||
if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" { continue }
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
|
||||
let maxt = lb.elements.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time)).max().unwrap_or(0);
|
||||
let len = ui_layout::loop_length_units(&by[o..o + s]).unwrap_or(0);
|
||||
total += 1;
|
||||
if maxt == 0 && len > 0 {
|
||||
@@ -44,19 +31,10 @@ fn main() {
|
||||
// to move between. All-at-t=0 with a single keyframe per element
|
||||
// is visually inert however it is played.
|
||||
let kf: usize = lb.elements.iter().map(|el| el.keyframes.len()).sum();
|
||||
let multi = lb
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.keyframes.len() > 1)
|
||||
.count();
|
||||
if multi > 0 {
|
||||
multipose += 1
|
||||
}
|
||||
println!(
|
||||
" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \
|
||||
across {} element(s), {multi} with >1 pose",
|
||||
lb.elements.len()
|
||||
);
|
||||
let multi = lb.elements.iter().filter(|el| el.keyframes.len() > 1).count();
|
||||
if multi > 0 { multipose += 1 }
|
||||
println!(" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \
|
||||
across {} element(s), {multi} with >1 pose", lb.elements.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ use std::process::Command;
|
||||
use sylpheed_formats::{media, slb::VoiceLang};
|
||||
|
||||
fn main() {
|
||||
let disc =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let src = media::DirectorySource::new(&disc);
|
||||
for movie in ["ADV", "S00A", "RT01A"] {
|
||||
let Some((s, e)) = media::resolve_movie_voice_region(&src, movie, VoiceLang::English)
|
||||
@@ -17,11 +16,7 @@ fn main() {
|
||||
continue;
|
||||
};
|
||||
let riffs = media::voice_region_riffs(&src, s, e).expect("riffs");
|
||||
println!(
|
||||
"{movie}: region [{s}, {e}) = {} bytes, {} chunk(s)",
|
||||
e - s,
|
||||
riffs.len()
|
||||
);
|
||||
println!("{movie}: region [{s}, {e}) = {} bytes, {} chunk(s)", e - s, riffs.len());
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let p = std::env::temp_dir().join(format!("vc_{movie}_{i}.xma.wav"));
|
||||
std::fs::write(&p, r).unwrap();
|
||||
@@ -33,14 +28,7 @@ fn main() {
|
||||
.arg(&w)
|
||||
.output();
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
])
|
||||
.args(["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0"])
|
||||
.arg(&w)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
@@ -169,9 +169,10 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
|
||||
#[serde(default)]
|
||||
voice: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
let file: File =
|
||||
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
let file: File = serde_json::from_str(&raw)
|
||||
.with_context(|| format!("parse {}", path.display()))?;
|
||||
|
||||
// `_` is the house convention for a prose block explaining the section it
|
||||
// sits in -- see `authored/flow.json` and `authored/timing.json`. It is
|
||||
@@ -210,12 +211,9 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
|
||||
let size: usize = k
|
||||
.parse()
|
||||
.with_context(|| format!("authored/audio.json: voice.stream_weights key {k}"))?;
|
||||
let w = v
|
||||
.get("weight")
|
||||
.and_then(serde_json::Value::as_f64)
|
||||
.with_context(|| {
|
||||
format!("authored/audio.json: voice.stream_weights.{k} has no numeric weight")
|
||||
})?;
|
||||
let w = v.get("weight").and_then(serde_json::Value::as_f64).with_context(|| {
|
||||
format!("authored/audio.json: voice.stream_weights.{k} has no numeric weight")
|
||||
})?;
|
||||
stream_weights.insert(size, w);
|
||||
}
|
||||
}
|
||||
@@ -284,16 +282,8 @@ fn run_ffmpeg(argv: &[String], out: &Path) -> Result<()> {
|
||||
// temp name -- it is a hard failure before a byte is written: "Unable to
|
||||
// choose an output format". `video.rs` already had this shape; this
|
||||
// function was written from scratch and did not.
|
||||
let stem = out
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let ext = out
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let stem = out.file_stem().unwrap_or_default().to_string_lossy().into_owned();
|
||||
let ext = out.extension().unwrap_or_default().to_string_lossy().into_owned();
|
||||
let partial = out.with_file_name(format!(".{stem}.partial.{ext}"));
|
||||
let mut argv = argv.to_vec();
|
||||
let last = argv.len() - 1;
|
||||
@@ -338,12 +328,8 @@ fn measure(path: &Path) -> (Option<f32>, Option<f32>) {
|
||||
.find_map(|l| l.split_once(KEY)?.1.trim().parse().ok());
|
||||
let dur = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0",
|
||||
])
|
||||
.arg(path)
|
||||
.output()
|
||||
@@ -382,12 +368,7 @@ pub fn export_cues<S: DiscSource + ?Sized>(
|
||||
// short read rather than returning a truncated stream, because a
|
||||
// truncated XMA decodes to plausible-sounding garbage.
|
||||
let riff = media::se_wave_riff(
|
||||
source,
|
||||
&cue.bank,
|
||||
offset,
|
||||
cue.packets,
|
||||
cue.channels,
|
||||
cue.rate,
|
||||
source, &cue.bank, offset, cue.packets, cue.channels, cue.rate,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)
|
||||
.with_context(|| format!("assemble the {event} cue"))?;
|
||||
@@ -395,16 +376,9 @@ pub fn export_cues<S: DiscSource + ?Sized>(
|
||||
let staged = stage_riff(&dir, event, &riff)?;
|
||||
let ogg = dir.join(format!("{event}.ogg"));
|
||||
let argv: Vec<String> = [
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
&staged.display().to_string(),
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
VORBIS_Q,
|
||||
"-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", &staged.display().to_string(),
|
||||
"-c:a", "libvorbis", "-q:a", VORBIS_Q,
|
||||
&ogg.display().to_string(),
|
||||
]
|
||||
.iter()
|
||||
@@ -563,15 +537,9 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
|
||||
argv.push(format!("{end}"));
|
||||
}
|
||||
argv.extend(
|
||||
[
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
VORBIS_Q,
|
||||
&ogg.display().to_string(),
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
);
|
||||
let command = format!("ffmpeg {}", argv.join(" "));
|
||||
run_ffmpeg(&argv, &ogg)?;
|
||||
@@ -943,10 +911,7 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
// 238-packet late start was found. Applied positionally instead, the
|
||||
// weights would have gone onto the wrong streams in silence.
|
||||
let sizes: Vec<usize> = keep.iter().map(|&i| riffs[i].len() - RIFF_HEADER).collect();
|
||||
let ws: Option<Vec<f64>> = sizes
|
||||
.iter()
|
||||
.map(|s| stream_weights.get(s).copied())
|
||||
.collect();
|
||||
let ws: Option<Vec<f64>> = sizes.iter().map(|s| stream_weights.get(s).copied()).collect();
|
||||
match ws {
|
||||
Some(w) if w.len() == staged.len() => {
|
||||
// Weights sum to one, so the total is the movie's own and what
|
||||
@@ -972,15 +937,9 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
argv.push("-map".into());
|
||||
argv.push("[a]".into());
|
||||
argv.extend(
|
||||
[
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
VORBIS_Q,
|
||||
&ogg.display().to_string(),
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
);
|
||||
let command = format!("ffmpeg {}", argv.join(" "));
|
||||
run_ffmpeg(&argv, &ogg)?;
|
||||
@@ -1080,14 +1039,8 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
fn probe_channels(path: &Path) -> Option<u8> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=channels",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"-v", "error", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=channels", "-of", "csv=p=0",
|
||||
])
|
||||
.arg(path)
|
||||
.output()
|
||||
@@ -1103,12 +1056,8 @@ fn probe_channels(path: &Path) -> Option<u8> {
|
||||
pub fn probe_duration(path: &Path) -> Option<f32> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0",
|
||||
])
|
||||
.arg(path)
|
||||
.output()
|
||||
@@ -1145,6 +1094,7 @@ fn decoded_chunk(riff: &Path) -> (f32, f32) {
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
/// Which channel indices of a decoded stream are not digitally silent.
|
||||
///
|
||||
/// `astats` reports per-channel blocks: a `Channel: N` line followed by that
|
||||
|
||||
@@ -50,9 +50,8 @@ impl Ctx {
|
||||
/// name. Anything else means a consumer has to guess, which is the whole thing
|
||||
/// the format exists to prevent.
|
||||
fn is_hex32(v: Option<&Value>) -> bool {
|
||||
v.and_then(Value::as_str).is_some_and(|s| {
|
||||
s.len() == 10 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
})
|
||||
v.and_then(Value::as_str)
|
||||
.is_some_and(|s| s.len() == 10 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
fn check_pose(c: &mut Ctx, where_: &str, p: &Value) {
|
||||
@@ -90,17 +89,12 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
// an invented one, so the provenance is mandatory and closed.
|
||||
match v.get("name_source").and_then(Value::as_str) {
|
||||
Some("authored") => {
|
||||
if v.get("name_why")
|
||||
.and_then(Value::as_str)
|
||||
.is_none_or(str::is_empty)
|
||||
{
|
||||
if v.get("name_why").and_then(Value::as_str).is_none_or(str::is_empty) {
|
||||
c.err("name_source is `authored` but there is no `name_why`");
|
||||
}
|
||||
}
|
||||
Some("index") => {}
|
||||
other => c.err(format!(
|
||||
"name_source must be `authored` or `index`, got {other:?}"
|
||||
)),
|
||||
other => c.err(format!("name_source must be `authored` or `index`, got {other:?}")),
|
||||
}
|
||||
if let Some(s) = v.get("source") {
|
||||
for key in ["archive", "entry", "build"] {
|
||||
@@ -125,22 +119,9 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
let mut indices = Vec::new();
|
||||
let mut buttons_by_y: Vec<(i64, String)> = Vec::new();
|
||||
for (i, el) in elements.iter().enumerate() {
|
||||
let id = el
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("<no id>")
|
||||
.to_string();
|
||||
let id = el.get("id").and_then(Value::as_str).unwrap_or("<no id>").to_string();
|
||||
let at = format!("element {i} ({id})");
|
||||
for key in [
|
||||
"index",
|
||||
"id",
|
||||
"declared",
|
||||
"role",
|
||||
"kind_raw",
|
||||
"pivot",
|
||||
"layer_source",
|
||||
"keyframes",
|
||||
] {
|
||||
for key in ["index", "id", "declared", "role", "kind_raw", "pivot", "layer_source", "keyframes"] {
|
||||
if el.get(key).is_none() {
|
||||
c.err(format!("{at}: missing `{key}`"));
|
||||
}
|
||||
@@ -150,9 +131,7 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
continue;
|
||||
};
|
||||
if idx as usize != i {
|
||||
c.err(format!(
|
||||
"{at}: `index` {idx} does not match its position {i}"
|
||||
));
|
||||
c.err(format!("{at}: `index` {idx} does not match its position {i}"));
|
||||
}
|
||||
indices.push(idx as usize);
|
||||
|
||||
@@ -173,21 +152,15 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
match el.get("layer_source").and_then(Value::as_str) {
|
||||
Some("sprite") | Some("implied") => {
|
||||
if !is_hex32(el.get("layer")) {
|
||||
c.err(format!(
|
||||
"{at}: layer_source claims a key but `layer` is not one"
|
||||
));
|
||||
c.err(format!("{at}: layer_source claims a key but `layer` is not one"));
|
||||
}
|
||||
}
|
||||
Some("none") => {
|
||||
if el.get("layer").is_some() {
|
||||
c.err(format!(
|
||||
"{at}: layer_source `none` but a `layer` is present"
|
||||
));
|
||||
c.err(format!("{at}: layer_source `none` but a `layer` is present"));
|
||||
}
|
||||
}
|
||||
other => c.err(format!(
|
||||
"{at}: layer_source must be sprite/implied/none, got {other:?}"
|
||||
)),
|
||||
other => c.err(format!("{at}: layer_source must be sprite/implied/none, got {other:?}")),
|
||||
}
|
||||
|
||||
for key in ["sprite", "focus_sprite"] {
|
||||
@@ -196,9 +169,7 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
if !path.exists() {
|
||||
c.err(format!("{at}: `{key}` points at {p}, which does not exist"));
|
||||
} else if let Err(e) = image::open(&path) {
|
||||
c.err(format!(
|
||||
"{at}: `{key}` {p} does not decode as an image: {e}"
|
||||
));
|
||||
c.err(format!("{at}: `{key}` {p} does not decode as an image: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,11 +177,7 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
if let Some(r) = el.get("rest") {
|
||||
check_pose(&mut c, &at, r);
|
||||
if role == "button" {
|
||||
if let Some(y) = r
|
||||
.get("pos")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|a| a[1].as_i64())
|
||||
{
|
||||
if let Some(y) = r.get("pos").and_then(Value::as_array).and_then(|a| a[1].as_i64()) {
|
||||
buttons_by_y.push((y, id.clone()));
|
||||
}
|
||||
}
|
||||
@@ -247,10 +214,7 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
// either drops an element or draws one twice.
|
||||
match v.get("paint_order").and_then(Value::as_array) {
|
||||
Some(po) => {
|
||||
let mut got: Vec<usize> = po
|
||||
.iter()
|
||||
.filter_map(|x| x.as_u64().map(|v| v as usize))
|
||||
.collect();
|
||||
let mut got: Vec<usize> = po.iter().filter_map(|x| x.as_u64().map(|v| v as usize)).collect();
|
||||
if got.len() != po.len() {
|
||||
c.err("`paint_order` holds a non-integer");
|
||||
}
|
||||
@@ -292,10 +256,7 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
pub fn run(root: &Path) -> Result<usize> {
|
||||
let manifest_path = root.join("manifest.json");
|
||||
if !manifest_path.exists() {
|
||||
bail!(
|
||||
"{} has no manifest.json — is that an export tree?",
|
||||
root.display()
|
||||
);
|
||||
bail!("{} has no manifest.json — is that an export tree?", root.display());
|
||||
}
|
||||
let m: Value = serde_json::from_str(&std::fs::read_to_string(&manifest_path)?)?;
|
||||
let mut errors = Vec::new();
|
||||
@@ -367,13 +328,9 @@ fn check_audio(root: &Path, m: &Value, errors: &mut Vec<String>) {
|
||||
errors.push(format!("manifest.json: audio `{name}` has no `{key}`"));
|
||||
}
|
||||
}
|
||||
let Some(file) = a.get("file").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let Some(file) = a.get("file").and_then(Value::as_str) else { continue };
|
||||
if !root.join(file).exists() {
|
||||
errors.push(format!(
|
||||
"manifest.json: lists audio {file}, which does not exist"
|
||||
));
|
||||
errors.push(format!("manifest.json: lists audio {file}, which does not exist"));
|
||||
continue;
|
||||
}
|
||||
match a.get("peak_dbfs").and_then(Value::as_f64) {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
mod audio;
|
||||
mod check;
|
||||
mod screen;
|
||||
mod video;
|
||||
mod screen;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
@@ -164,7 +164,8 @@ fn load_names(authored: &Path) -> Result<NameMap> {
|
||||
#[serde(default)]
|
||||
also_export: AlsoExport,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
Ok(serde_json::from_str::<File>(&raw)
|
||||
.with_context(|| format!("parse {}", path.display()))?
|
||||
.archives)
|
||||
@@ -172,7 +173,8 @@ fn load_names(authored: &Path) -> Result<NameMap> {
|
||||
|
||||
/// Extra pak entries to export that `is_build` does not accept, keyed by
|
||||
/// archive. AUTHORED, and each carries its own `why`.
|
||||
type AlsoExport = std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
|
||||
type AlsoExport =
|
||||
std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
|
||||
|
||||
fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
let path = authored.join("screen_names.json");
|
||||
@@ -184,7 +186,8 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
#[serde(default)]
|
||||
also_export: AlsoExport,
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
let raw = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
Ok(serde_json::from_str::<File>(&raw)
|
||||
.with_context(|| format!("parse {}", path.display()))?
|
||||
.also_export)
|
||||
@@ -208,10 +211,9 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
/// exactly four bundles and all four are real screens, with zero fragments. In
|
||||
/// another archive it would not be, which is why this is an allow-list and not
|
||||
/// a widened predicate.
|
||||
fn screen_builds(
|
||||
ar: &PakArchive,
|
||||
also: Option<&std::collections::BTreeMap<String, NameEntry>>,
|
||||
) -> Vec<(usize, Vec<u8>)> {
|
||||
fn screen_builds(ar: &PakArchive, also: Option<&std::collections::BTreeMap<String, NameEntry>>)
|
||||
-> Vec<(usize, Vec<u8>)>
|
||||
{
|
||||
let mut out = Vec::new();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
@@ -232,11 +234,7 @@ fn main() -> Result<()> {
|
||||
} => run_export(&disc, &out, &authored),
|
||||
Cmd::Check { out } => {
|
||||
let n = check::run(&out)?;
|
||||
println!(
|
||||
"{} screen(s) in {} validate against sylpheed.screen/3",
|
||||
n,
|
||||
out.display()
|
||||
);
|
||||
println!("{} screen(s) in {} validate against sylpheed.screen/3", n, out.display());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -396,7 +394,12 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
Some(cfg) => {
|
||||
let source = media::DirectorySource::new(disc);
|
||||
for a in audio::export_cues(&source, out, &cfg.se)? {
|
||||
println!(" se {:<8} -> {} ({})", a.name, a.file, describe(&a));
|
||||
println!(
|
||||
" se {:<8} -> {} ({})",
|
||||
a.name,
|
||||
a.file,
|
||||
describe(&a)
|
||||
);
|
||||
audio.push(ManifestAudio::from(a));
|
||||
}
|
||||
for (role, spec) in &cfg.bgm {
|
||||
@@ -458,10 +461,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
// there is no `authored/audio.json` -- the voice binding is decoded,
|
||||
// so the dialogue exports either way and only the choice defaults.
|
||||
let want = audio_cfg.as_ref().map(|c| c.voice).unwrap_or_default();
|
||||
let weights = audio_cfg
|
||||
.as_ref()
|
||||
.map(|c| c.stream_weights.clone())
|
||||
.unwrap_or_default();
|
||||
let weights = audio_cfg.as_ref().map(|c| c.stream_weights.clone()).unwrap_or_default();
|
||||
match audio::export_voice(&source, out, stem, *len, want, &weights)? {
|
||||
Some(a) => {
|
||||
// 🔴 A TOP-LEVEL WARNING, not just a `why` on the entry. The
|
||||
@@ -517,6 +517,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
impl From<audio::Exported> for ManifestAudio {
|
||||
fn from(a: audio::Exported) -> Self {
|
||||
ManifestAudio {
|
||||
@@ -559,6 +560,7 @@ fn describe(a: &audio::Exported) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Delete anything in `video/` this run did not produce.
|
||||
///
|
||||
/// `video/` is the one directory the wholesale wipe spares, so that the
|
||||
|
||||
@@ -416,6 +416,7 @@ pub fn export_build(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
/// The highlighted twin of a sprite name: `ptbtn01.t32` → `ptbtn01f.t32`.
|
||||
fn highlight_name(sprite: &str) -> Option<String> {
|
||||
let (stem, ext) = sprite.rsplit_once('.')?;
|
||||
@@ -459,9 +460,7 @@ pub fn export_build(
|
||||
written: &mut std::collections::BTreeMap<String, ()>,
|
||||
missing: &mut Vec<String>|
|
||||
-> Result<Option<Focus>> {
|
||||
let Some(&(off, size)) = b.records.get(rec) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(&(off, size)) = b.records.get(rec) else { return Ok(None) };
|
||||
let Some(leaf) = ui_layout::parse_build(&bundle[off..off + size]) else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -469,13 +468,8 @@ pub fn export_build(
|
||||
for fe in &leaf.elements {
|
||||
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
|
||||
let mut fsprite = None;
|
||||
if write_from(
|
||||
&sprite_dir,
|
||||
written,
|
||||
sp,
|
||||
&bundle[off..off + size],
|
||||
&leaf.sprites,
|
||||
)? || write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
|
||||
if write_from(&sprite_dir, written, sp, &bundle[off..off + size], &leaf.sprites)?
|
||||
|| write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
|
||||
{
|
||||
fsprite = Some(sprite_rel(sp));
|
||||
} else if sp.ends_with(".t32") {
|
||||
@@ -487,11 +481,8 @@ pub fn export_build(
|
||||
declared: fe.name.clone(),
|
||||
sprite: fsprite,
|
||||
blend_additive: ui_layout::blend_additive_by_name(
|
||||
&leaf,
|
||||
&bundle[off..off + size],
|
||||
sp,
|
||||
)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
&leaf, &bundle[off..off + size], sp)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
pivot: [fe.pivot_x, fe.pivot_y],
|
||||
rest: Rest {
|
||||
pos: [r.x, r.y],
|
||||
@@ -542,13 +533,9 @@ pub fn export_build(
|
||||
// into the leaf slice) or in the parent's.
|
||||
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
|
||||
let mut fsprite = None;
|
||||
if write_from(
|
||||
&sprite_dir,
|
||||
&mut written,
|
||||
sp,
|
||||
&bundle[off..off + size],
|
||||
&leaf.sprites,
|
||||
)? || write_from(&sprite_dir, &mut written, sp, bundle, &b.sprites)?
|
||||
if write_from(&sprite_dir, &mut written, sp,
|
||||
&bundle[off..off + size], &leaf.sprites)?
|
||||
|| write_from(&sprite_dir, &mut written, sp, bundle, &b.sprites)?
|
||||
{
|
||||
fsprite = Some(sprite_rel(sp));
|
||||
} else if sp.ends_with(".t32") {
|
||||
@@ -560,11 +547,8 @@ pub fn export_build(
|
||||
declared: fe.name.clone(),
|
||||
sprite: fsprite,
|
||||
blend_additive: ui_layout::blend_additive_by_name(
|
||||
&leaf,
|
||||
&bundle[off..off + size],
|
||||
sp,
|
||||
)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
&leaf, &bundle[off..off + size], sp)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
pivot: [fe.pivot_x, fe.pivot_y],
|
||||
rest: Rest {
|
||||
pos: [r.x, r.y],
|
||||
@@ -591,9 +575,7 @@ pub fn export_build(
|
||||
if !fes.is_empty() {
|
||||
focus = Some(Focus {
|
||||
record: rec,
|
||||
loop_length_units: ui_layout::loop_length_units(
|
||||
&bundle[off..off + size],
|
||||
),
|
||||
loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]),
|
||||
elements: fes,
|
||||
});
|
||||
}
|
||||
@@ -722,6 +704,7 @@ pub fn export_build(
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// The longest interval containing no keyframe time, over TOP-LEVEL elements.
|
||||
///
|
||||
/// See [`Screen::settle_window`] for why this is the settled instant and why
|
||||
@@ -767,12 +750,12 @@ fn settle_window(elements: &[Element]) -> Option<[i64; 3]> {
|
||||
Some([a, b, (a + b) / 2])
|
||||
}
|
||||
|
||||
|
||||
/// Alpha of one element at instant `t`, under the linear ramp the port uses.
|
||||
fn alpha_at(e: &Element, t: i64) -> u8 {
|
||||
let ks = &e.keyframes;
|
||||
let a = |k: &Keyframe| {
|
||||
(u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16).unwrap_or(0) >> 24) as i64
|
||||
};
|
||||
let a = |k: &Keyframe| (u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16)
|
||||
.unwrap_or(0) >> 24) as i64;
|
||||
let timed: Vec<&Keyframe> = ks.iter().filter(|k| k.t.is_some()).collect();
|
||||
if timed.is_empty() {
|
||||
return 0;
|
||||
@@ -905,9 +888,7 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
|
||||
.filter_map(|k| k.t)
|
||||
.map(i64::from)
|
||||
.collect();
|
||||
let Some(&lo) = span.first() else {
|
||||
return false;
|
||||
};
|
||||
let Some(&lo) = span.first() else { return false };
|
||||
// 🔴 COVERAGE IS TESTED AT EACH INSTANT, NOT ONCE FROM `size`.
|
||||
// Declared size alone is not what the element draws: scale is a
|
||||
// percent per axis and it animates. `pbafc.prm` is the disc's own
|
||||
@@ -940,10 +921,9 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
|
||||
return false;
|
||||
}
|
||||
// Every OTHER element must be visible somewhere inside that span.
|
||||
elements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(j, o)| j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0))
|
||||
elements.iter().enumerate().all(|(j, o)| {
|
||||
j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0)
|
||||
})
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
@@ -97,22 +97,13 @@ const DOWNMIX_51: &str = "pan=stereo|FL=0.4142*FL+0.2929*FC+0.2929*BL
|
||||
fn channels(src: &Path) -> Result<u32> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"a:0",
|
||||
"-show_entries",
|
||||
"stream=channels",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"-v", "error", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=channels", "-of", "csv=p=0",
|
||||
])
|
||||
.arg(src)
|
||||
.output()
|
||||
.context("run ffprobe -- is it on PATH?")?;
|
||||
Ok(String::from_utf8_lossy(&out.stdout)
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(2))
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().parse().unwrap_or(2))
|
||||
}
|
||||
|
||||
/// Duration and frame rate of a finished transcode, straight from the file.
|
||||
@@ -128,8 +119,7 @@ fn probe_timebase(out: &Path) -> (f64, f64) {
|
||||
if stream {
|
||||
c.args(["-select_streams", "v:0"]);
|
||||
}
|
||||
c.args(["-show_entries", entries, "-of", "csv=p=0"])
|
||||
.arg(out);
|
||||
c.args(["-show_entries", entries, "-of", "csv=p=0"]).arg(out);
|
||||
c.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_default()
|
||||
@@ -146,20 +136,10 @@ fn probe_timebase(out: &Path) -> (f64, f64) {
|
||||
|
||||
fn args(src: &Path, out: &Path, channels: u32) -> Vec<String> {
|
||||
let mut v: Vec<String> = [
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-i",
|
||||
&src.display().to_string(),
|
||||
"-c:v",
|
||||
"libtheora",
|
||||
"-q:v",
|
||||
"8",
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a",
|
||||
"5",
|
||||
"-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", &src.display().to_string(),
|
||||
"-c:v", "libtheora", "-q:v", "8",
|
||||
"-c:a", "libvorbis", "-q:a", "5",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
@@ -288,10 +268,7 @@ pub fn transcode(disc: &Path, out: &Path, m: &Movie) -> Result<Option<Transcoded
|
||||
// updates it. The explanation would be correct in the source and absent on
|
||||
// disc, which is the same shape as every other documented-but-unexercised
|
||||
// thing this port has had to find the hard way.
|
||||
if std::fs::read_to_string(&stamp)
|
||||
.map(|s| s != want)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if std::fs::read_to_string(&stamp).map(|s| s != want).unwrap_or(true) {
|
||||
std::fs::write(&stamp, &want)?;
|
||||
}
|
||||
let (duration_s, fps) = probe_timebase(&ogv);
|
||||
|
||||
@@ -11,12 +11,7 @@ xdvdfs = { workspace = true }
|
||||
binrw = { workspace = true }
|
||||
flate2 = "1" # zlib/DEFLATE for IPFB "Z1" entries (miniz_oxide backend, WASM-safe)
|
||||
ttf-parser = { version = "0.24", default-features = false, features = ["std", "opentype-layout"] } # font metadata (OTF/TTF/ttcf), WASM-safe
|
||||
# tokio is a DEV dependency only (see [dev-dependencies] below). Every use in
|
||||
# this crate is inside a `mod tests`: three runtime builders in ship.rs and one
|
||||
# `#[tokio::test]` in xiso.rs. As a normal dependency it pulled `tokio/full`,
|
||||
# whose `net` feature drags in `mio`, which does not build for wasm32 —
|
||||
# error: This wasm target is unsupported by mio.
|
||||
# so the unused dependency was breaking the WASM job.
|
||||
tokio = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let i: usize = a.next().unwrap().parse().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
let by = ar.read(&ar.entries()[i]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
println!(
|
||||
"entry {i}: {} elements, {} records, {} sprites",
|
||||
b.elements.len(),
|
||||
b.records.len(),
|
||||
b.sprites.len()
|
||||
);
|
||||
let mut rk: Vec<&String> = b.records.keys().collect();
|
||||
rk.sort();
|
||||
println!(" records: {:?}", rk);
|
||||
for (rn, &(o, sz)) in &b.records {
|
||||
if o + sz > by.len() {
|
||||
continue;
|
||||
}
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + sz]) else {
|
||||
continue;
|
||||
};
|
||||
println!(" RECORD {rn}: {} elements", lb.elements.len());
|
||||
for le in &lb.elements {
|
||||
let lts: Vec<String> = le
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"t{}a{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" [{}] {:<22} kind=0x{:<6x} {}",
|
||||
le.index,
|
||||
le.name,
|
||||
le.kind,
|
||||
lts.join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
for e in &b.elements {
|
||||
let ts: Vec<String> = e
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"t{}a{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" [{}] {:<24} kind=0x{:<6x} {}",
|
||||
e.index,
|
||||
e.name,
|
||||
e.kind,
|
||||
ts.join(" ")
|
||||
);
|
||||
if let Some(&(o, s)) = b.records.get(&e.name) {
|
||||
if o + s <= by.len() {
|
||||
if let Some(lb) = ui_layout::parse_build(&by[o..o + s]) {
|
||||
for le in &lb.elements {
|
||||
let lts: Vec<String> = le
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"t{}a{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" leaf {:<20} kind=0x{:<6x} {}",
|
||||
le.name,
|
||||
le.kind,
|
||||
lts.join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
for t in a {
|
||||
let i: usize = t.parse().unwrap();
|
||||
let by = ar.read(&ar.entries()[i]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
println!("=== entry {i} ===");
|
||||
let order = ui_layout::derived_paint_order(&b, &by);
|
||||
for e in &b.elements {
|
||||
let k = ui_layout::sprite_layer_key(&b, &by, e);
|
||||
let pos = order.iter().position(|&x| x == e.index);
|
||||
println!(
|
||||
" [{}] {:<24} kind=0x{:<5x} sprite={:<24} key={:<12} paint#{:?}",
|
||||
e.index,
|
||||
e.name,
|
||||
e.kind,
|
||||
e.sprite.clone().unwrap_or_else(|| "<none>".into()),
|
||||
k.map(|v| format!("0x{v:08x}"))
|
||||
.unwrap_or_else(|| "NONE".into()),
|
||||
pos
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let ar = pak::PakArchive::open(format!("{root}/dat/GP_READY_ROOM.pak")).unwrap();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let Some(p) = b.elements.iter().find(|el| el.name == "pbafc.prm") else {
|
||||
continue;
|
||||
};
|
||||
println!("=== entry {i}: {} elements ===", b.elements.len());
|
||||
println!(
|
||||
" pbafc.prm pivot=({},{}) -> {}x{}",
|
||||
p.pivot_x,
|
||||
p.pivot_y,
|
||||
p.pivot_x * 2,
|
||||
p.pivot_y * 2
|
||||
);
|
||||
for k in &p.keyframes {
|
||||
println!(
|
||||
" t={:<5} fade={:08x} a={:<4} xy=({},{}) s={}/{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade,
|
||||
k.fade >> 24,
|
||||
k.x,
|
||||
k.y,
|
||||
k.scale_x,
|
||||
k.scale_y
|
||||
);
|
||||
}
|
||||
// what does it cover, and is anything visible while it is opaque?
|
||||
let rest = p.rest().unwrap();
|
||||
let (px, py, pw, ph) = (
|
||||
rest.x,
|
||||
rest.y,
|
||||
(p.pivot_x * 2) as i32,
|
||||
(p.pivot_y * 2) as i32,
|
||||
);
|
||||
println!(" its rect at rest: ({px},{py}) {pw}x{ph}");
|
||||
let tmax = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let op: Vec<u32> = (0..=tmax)
|
||||
.filter(|&t| p.pose_at(t).map(|k| k.fade >> 24) == Some(255))
|
||||
.collect();
|
||||
println!(
|
||||
" opaque at {} instants (t={:?}..{:?}) of 0..{tmax}",
|
||||
op.len(),
|
||||
op.first(),
|
||||
op.last()
|
||||
);
|
||||
let mut cov = 0;
|
||||
let mut vis = 0;
|
||||
for o in &b.elements {
|
||||
if o.index == p.index {
|
||||
continue;
|
||||
}
|
||||
let Some(ok) = o.rest() else { continue };
|
||||
let (ow, oh) = ((o.pivot_x * 2) as i32, (o.pivot_y * 2) as i32);
|
||||
let overlap = (px + pw).min(ok.x + ow) - px.max(ok.x) > 0
|
||||
&& (py + ph).min(ok.y + oh) - py.max(ok.y) > 0;
|
||||
if !overlap {
|
||||
continue;
|
||||
}
|
||||
cov += 1;
|
||||
if op
|
||||
.iter()
|
||||
.any(|&t| o.pose_at(t).map(|k| k.fade >> 24).unwrap_or(0) > 0)
|
||||
{
|
||||
vis += 1;
|
||||
if vis <= 6 {
|
||||
println!(" covered AND visible while opaque: {}", o.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" elements its rect covers: {cov}; visible while it is opaque: {vis}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
for t in a {
|
||||
let i: usize = t.parse().unwrap();
|
||||
let by = ar.read(&ar.entries()[i]).unwrap();
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let top = u32::from_be_bytes(by[8..12].try_into().unwrap());
|
||||
println!("entry {i:2} TOP +08 = {top}");
|
||||
let mut ks: Vec<&String> = b.records.keys().collect();
|
||||
ks.sort();
|
||||
for rn in ks {
|
||||
let &(o, s) = b.records.get(rn).unwrap();
|
||||
if o + 16 > by.len() {
|
||||
continue;
|
||||
}
|
||||
let magic = &by[o..o + 4];
|
||||
let h4 = u32::from_be_bytes(by[o + 4..o + 8].try_into().unwrap());
|
||||
let h8 = u32::from_be_bytes(by[o + 8..o + 12].try_into().unwrap());
|
||||
let maxt = ui_layout::parse_build(&by[o..o + s])
|
||||
.map(|lb| {
|
||||
lb.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
println!(" {rn:<24} magic={:?} +04={:08x}({:.1}) +08={h8:<6} max keyframe t={maxt} ratio={:.4}",
|
||||
String::from_utf8_lossy(magic), h4, h4 as f64/65536.0,
|
||||
if maxt>0 {h8 as f64/maxt as f64} else {0.0});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,7 @@ fn main() {
|
||||
paks.sort();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(arc) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if !find(&b, b"ACHIEVEMENTS_REQUIREMENTS") {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").unwrap());
|
||||
println!("# Which elements the reference renderer now draws ADDITIVE, per screen.");
|
||||
println!("# Source: T8aD +0x04 bit 0x02, docs/re/structures/ui-blend-mode-decoded.md.");
|
||||
println!("# Generated after ui_layout::blit gained an additive path (2026-09-01).");
|
||||
println!("# Before that change EVERY row below was drawn alpha-over by our renderer,");
|
||||
println!("# which is why `verify-screen` was structurally incapable on these screens.");
|
||||
for pak in ["GP_TITLE", "GP_OPTIONS"] {
|
||||
let Ok(ar) = PakArchive::open(root.join(format!("dat/{pak}.pak"))) else {
|
||||
continue;
|
||||
};
|
||||
let n = ar.entries().len();
|
||||
for e in 0..n {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut add: Vec<&String> = b
|
||||
.sprites
|
||||
.keys()
|
||||
.filter(|s| ui_layout::blend_additive_by_name(&b, &by, s) == Some(true))
|
||||
.collect();
|
||||
if add.is_empty() {
|
||||
continue;
|
||||
}
|
||||
add.sort();
|
||||
println!(
|
||||
"\n{pak} entry {e} -- {} of {} sprites additive",
|
||||
add.len(),
|
||||
b.sprites.len()
|
||||
);
|
||||
for s in add {
|
||||
println!(" {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//! Does `resolve_movie_voice_region` start LATE, and by exactly how much?
|
||||
//!
|
||||
//! The port agent's arithmetic: the running decoder's three `ADV` XMA contexts sum
|
||||
//! to **3 584 000** payload bytes, but the resolved voice region is **3 114 352** —
|
||||
//! 15 % too small to hold them. One of the two spans is not what the other thinks
|
||||
//! it is, and the disc side is this crate's.
|
||||
//!
|
||||
//! The gap is exact. `ctx0` declares **632** packets (1 294 336 B); the leading
|
||||
//! chunk the resolver yields has **394** (806 912 B). The difference is **238
|
||||
//! packets = 487 424 B**, a whole number of packets — which is what a start offset
|
||||
//! looks like, not corruption.
|
||||
//!
|
||||
//! So: walk the region start backwards and report where `to_xma_riffs` first
|
||||
//! reproduces the decoder's own three sizes. The probe's byte_sizes are the
|
||||
//! control — this is not free to fit, it either lands on them or it does not.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example adv_region_extend
|
||||
|
||||
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
||||
use sylpheed_formats::slb::{self, VoiceLang};
|
||||
|
||||
/// What the running decoder reported (docs/re/structures/voice-three-streams-are-concurrent.md).
|
||||
const WANT: [usize; 3] = [1_294_336, 1_118_208, 1_171_456];
|
||||
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
let (start, end) =
|
||||
media::resolve_movie_voice_region(&src, "ADV", VoiceLang::English).expect("region");
|
||||
println!("resolver says {start}..{end} ({} B)", end - start);
|
||||
println!(
|
||||
"decoder wants {:?} = {} B payload\n",
|
||||
WANT,
|
||||
WANT.iter().sum::<usize>()
|
||||
);
|
||||
|
||||
for back_packets in [0usize, 100, 200, 237, 238, 239, 300, 400] {
|
||||
let back = (back_packets * 2048) as u64;
|
||||
if back > start {
|
||||
continue;
|
||||
}
|
||||
let s = start - back;
|
||||
let Ok(bytes) = src.read_segment_range("dat/sound", s, (end - s) as usize) else {
|
||||
println!("-{back_packets:4} packets: unreadable");
|
||||
continue;
|
||||
};
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
let sizes: Vec<usize> = riffs.iter().map(|r| r.len() - 60).collect();
|
||||
let hit = sizes.len() == 3 && sizes.iter().zip(WANT.iter()).all(|(a, b)| a == b);
|
||||
println!(
|
||||
"-{back_packets:4} packets (start {s}): {} chunk(s) {:?}{}",
|
||||
riffs.len(),
|
||||
sizes,
|
||||
if hit {
|
||||
" <== MATCHES THE DECODER"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//! Dump `ADV`'s voice chunks as RIFF/XMA, so each can be decoded and identified.
|
||||
//!
|
||||
//! `intro-audio-decomposed.md` measured that the intro's output is the movie's own
|
||||
//! WMA Pro 5.1 track at 0.600 **plus** three streams occupying a front pair, a
|
||||
//! centre (with a silent partner) and a rear pair. What it could **not** say is
|
||||
//! *which* stream sits where — the assignment there is by position, not content.
|
||||
//! The port needs that to weight a positional downmix.
|
||||
//!
|
||||
//! This writes the chunks out so they can be decoded (ffmpeg has `xma2`) and
|
||||
//! correlated against the per-channel residuals.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example adv_voice_dump -- OUTDIR [MOVIE]
|
||||
|
||||
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
||||
use sylpheed_formats::slb::{self, VoiceLang};
|
||||
|
||||
fn main() {
|
||||
let out = std::env::args().nth(1).expect("OUTDIR");
|
||||
let movie = std::env::args().nth(2).unwrap_or_else(|| "ADV".into());
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
std::fs::create_dir_all(&out).expect("outdir");
|
||||
|
||||
let (start, end) =
|
||||
media::resolve_movie_voice_region(&src, &movie, VoiceLang::English).expect("voice region");
|
||||
let bytes = src
|
||||
.read_segment_range("dat/sound", start, (end - start) as usize)
|
||||
.expect("region");
|
||||
println!("{movie}: region {start}..{end} = {} B", end - start);
|
||||
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
println!("{} RIFF chunk(s)", riffs.len());
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let p = format!("{out}/{movie}_{i}.xma");
|
||||
std::fs::write(&p, r).expect("write");
|
||||
// the probe reports `byte_size` = RIFF total - 60; print both so the
|
||||
// dump can be tied to a specific XMA context by its own number
|
||||
println!(
|
||||
" chunk {i}: {} B byte_size-equivalent {} -> {p}",
|
||||
r.len(),
|
||||
r.len() as i64 - 60
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,9 @@
|
||||
use sylpheed_formats::{game_data as gd, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap();
|
||||
let a = gd::load_arsenal(&pak);
|
||||
for (hp, list) in [
|
||||
("NOSE", &a.nose),
|
||||
("ARM1", &a.arm1),
|
||||
("ARM2", &a.arm2),
|
||||
("ARM3", &a.arm3),
|
||||
] {
|
||||
println!(
|
||||
"{hp} ({}): {:?}",
|
||||
list.len(),
|
||||
list.iter().take(8).collect::<Vec<_>>()
|
||||
);
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap();
|
||||
let a=gd::load_arsenal(&pak);
|
||||
for (hp,list) in [("NOSE",&a.nose),("ARM1",&a.arm1),("ARM2",&a.arm2),("ARM3",&a.arm3)]{
|
||||
println!("{hp} ({}): {:?}", list.len(), list.iter().take(8).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//! List a sound bank's streams and their declared rates.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example bank_streams -- <disc> BGM_102.slb …
|
||||
use sylpheed_formats::media::{self, DirectorySource};
|
||||
use sylpheed_formats::{hash::name_hash, slb};
|
||||
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let disc = a.next().expect("usage: bank_streams <disc> NAME.slb…");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
for name in a {
|
||||
let h = name_hash(&name);
|
||||
match media::read_sound_bank(&src, h) {
|
||||
Ok(bytes) => {
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
println!(
|
||||
"{name} (hash {h:08x}, {} B on disc) header {:?} -> {} stream(s)",
|
||||
bytes.len(),
|
||||
slb::bank_header_len(&bytes),
|
||||
riffs.len()
|
||||
);
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let rate = if r.len() >= 0x28 {
|
||||
u32::from_le_bytes(r[0x20..0x24].try_into().unwrap())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let payload = r.len() - 60;
|
||||
println!(
|
||||
" stream {i}: payload {payload} B ({} packets) declared {rate} B/s \
|
||||
=> {:.3} s",
|
||||
payload / 2048,
|
||||
if rate > 0 {
|
||||
payload as f64 / rate as f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{name}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,18 @@
|
||||
use sylpheed_formats::{game_data as gd, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let units = gd::load_units(&pak);
|
||||
let vessels = gd::load_vessels(&pak);
|
||||
let hp = |id: &str| -> String {
|
||||
units
|
||||
.iter()
|
||||
.find(|u| u.id.as_deref() == Some(id))
|
||||
.and_then(|u| u.hp)
|
||||
.map(|h| format!("{h:.0}hp"))
|
||||
.or_else(|| {
|
||||
vessels
|
||||
.iter()
|
||||
.find(|v| v.id.as_deref() == Some(id))
|
||||
.and_then(|v| v.hp)
|
||||
.map(|h| format!("{h:.0}HP"))
|
||||
})
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let units=gd::load_units(&pak); let vessels=gd::load_vessels(&pak);
|
||||
let hp=|id:&str|->String{
|
||||
units.iter().find(|u|u.id.as_deref()==Some(id)).and_then(|u|u.hp).map(|h|format!("{h:.0}hp"))
|
||||
.or_else(||vessels.iter().find(|v|v.id.as_deref()==Some(id)).and_then(|v|v.hp).map(|h|format!("{h:.0}HP")))
|
||||
.unwrap_or("·".into())
|
||||
};
|
||||
let rosters = gd::load_unit_rosters(&pak);
|
||||
for r in rosters.iter().filter(|r| r.stage.is_some()).take(4) {
|
||||
println!(
|
||||
"\n{} — {} combatants:",
|
||||
r.stage.as_deref().unwrap(),
|
||||
r.units.len()
|
||||
);
|
||||
for u in r.units.iter().filter(|u| u.contains("ADAN")).take(6) {
|
||||
let short = u
|
||||
.trim_start_matches("UN_")
|
||||
.split('_')
|
||||
.skip(1)
|
||||
.collect::<Vec<_>>()
|
||||
.join("_");
|
||||
let rosters=gd::load_unit_rosters(&pak);
|
||||
for r in rosters.iter().filter(|r|r.stage.is_some()).take(4){
|
||||
println!("\n{} — {} combatants:", r.stage.as_deref().unwrap(), r.units.len());
|
||||
for u in r.units.iter().filter(|u|u.contains("ADAN")).take(6){
|
||||
let short=u.trim_start_matches("UN_").split('_').skip(1).collect::<Vec<_>>().join("_");
|
||||
println!(" {short:32} {}", hp(u));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,22 +8,13 @@ fn main() {
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let names = sylpheed_formats::mesh::xbg7_resource_names(&bytes);
|
||||
let ids: Vec<String> = {
|
||||
let mut v: Vec<String> = names
|
||||
.iter()
|
||||
.filter(|n| is_base_part(n))
|
||||
.filter_map(|n| ship_id_of(n).map(|s| s.to_string()))
|
||||
.collect();
|
||||
v.sort();
|
||||
v.dedup();
|
||||
v.truncate(5);
|
||||
v
|
||||
let mut v: Vec<String> = names.iter().filter(|n| is_base_part(n))
|
||||
.filter_map(|n| ship_id_of(n).map(|s| s.to_string())).collect();
|
||||
v.sort(); v.dedup(); v.truncate(5); v
|
||||
};
|
||||
for id in &ids {
|
||||
let want: HashSet<String> = names
|
||||
.iter()
|
||||
.filter(|n| ship_id_of(n) == Some(id.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let want: HashSet<String> = names.iter()
|
||||
.filter(|n| ship_id_of(n) == Some(id.as_str())).cloned().collect();
|
||||
let t = Instant::now();
|
||||
let got = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
println!("{id}: {} models in {:?}", got.len(), t.elapsed());
|
||||
|
||||
@@ -28,10 +28,7 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
};
|
||||
let (vc, ic) = markers[0];
|
||||
println!(
|
||||
"{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}",
|
||||
markers.len()
|
||||
);
|
||||
println!("{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}", markers.len());
|
||||
|
||||
// Where did the decoder put it?
|
||||
let ours = Xbg7Model::stage_models(&bytes)
|
||||
@@ -41,10 +38,7 @@ fn main() {
|
||||
println!("our anchor: {ours:?}");
|
||||
|
||||
let starts = debug_vertex_run_starts(&bytes, stride);
|
||||
println!(
|
||||
"{} candidate vertex-run starts for stride {stride}",
|
||||
starts.len()
|
||||
);
|
||||
println!("{} candidate vertex-run starts for stride {stride}", starts.len());
|
||||
|
||||
// Score every (start, pad): degenerate triangles, winding against the stored
|
||||
// normals, and whether the run covers the pool exactly.
|
||||
@@ -73,15 +67,12 @@ fn main() {
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
if pos
|
||||
.iter()
|
||||
.any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6))
|
||||
{
|
||||
if pos.iter().any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6)) {
|
||||
continue;
|
||||
}
|
||||
let mut degen = 0usize;
|
||||
let (mut agree, mut counted) = (0usize, 0usize);
|
||||
for t in idx.as_chunks::<3>().0 {
|
||||
for t in idx.chunks_exact(3) {
|
||||
let (x, y, z) = (t[0] as usize, t[1] as usize, t[2] as usize);
|
||||
if x == y || y == z || x == z {
|
||||
degen += 1;
|
||||
@@ -91,16 +82,8 @@ fn main() {
|
||||
// stand-in so this stays declaration-agnostic: a consistent mesh
|
||||
// has all faces pointing away from the centroid on a convex-ish
|
||||
// hull. Weak, so degeneracy leads the sort.
|
||||
let e1 = [
|
||||
pos[y][0] - pos[x][0],
|
||||
pos[y][1] - pos[x][1],
|
||||
pos[y][2] - pos[x][2],
|
||||
];
|
||||
let e2 = [
|
||||
pos[z][0] - pos[x][0],
|
||||
pos[z][1] - pos[x][1],
|
||||
pos[z][2] - pos[x][2],
|
||||
];
|
||||
let e1 = [pos[y][0] - pos[x][0], pos[y][1] - pos[x][1], pos[y][2] - pos[x][2]];
|
||||
let e2 = [pos[z][0] - pos[x][0], pos[z][1] - pos[x][1], pos[z][2] - pos[x][2]];
|
||||
let f = [
|
||||
e1[1] * e2[2] - e1[2] * e2[1],
|
||||
e1[2] * e2[0] - e1[0] * e2[2],
|
||||
@@ -121,19 +104,12 @@ fn main() {
|
||||
agree += 1;
|
||||
}
|
||||
}
|
||||
let w = if counted == 0 {
|
||||
0.0
|
||||
} else {
|
||||
agree as f32 / counted as f32
|
||||
};
|
||||
let w = if counted == 0 { 0.0 } else { agree as f32 / counted as f32 };
|
||||
rows.push((degen, w.max(1.0 - w), vb, pad, max_idx, max_idx + 1 == vc));
|
||||
}
|
||||
}
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.total_cmp(&a.1)));
|
||||
println!(
|
||||
"\n{} in-range candidates; best 12 by (degenerate, winding):",
|
||||
rows.len()
|
||||
);
|
||||
println!("\n{} in-range candidates; best 12 by (degenerate, winding):", rows.len());
|
||||
for (d, w, vb, pad, mx, cov) in rows.iter().take(12) {
|
||||
let mark = if Some(*vb) == ours { " <-- ours" } else { "" };
|
||||
println!(
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//! Dump one BGM bank's waves as RIFF/XMA so they can be decoded and compared
|
||||
//! against a capture of the running game.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example bgm_dump -- BGM_103.slb OUTDIR
|
||||
use sylpheed_formats::media::{self, DirectorySource};
|
||||
use sylpheed_formats::slb;
|
||||
|
||||
fn main() {
|
||||
let name = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "BGM_103.slb".into());
|
||||
let out = std::env::args().nth(2).expect("OUTDIR");
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
std::fs::create_dir_all(&out).expect("outdir");
|
||||
let h = sylpheed_formats::hash::name_hash(&name);
|
||||
let bytes = media::read_sound_bank(&src, h).expect("bank");
|
||||
println!("{name}: {} B", bytes.len());
|
||||
for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() {
|
||||
let p = format!("{out}/{}_{i}.xma", name.trim_end_matches(".slb"));
|
||||
std::fs::write(&p, r).expect("write");
|
||||
println!(
|
||||
" wave {i}: {} B (byte_size {}) -> {p}",
|
||||
r.len(),
|
||||
r.len() - 60
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//! Does a screen declare its own OPAQUE BLACK backdrop? Disc-wide.
|
||||
//!
|
||||
//! `sylpheed-port` observed that the splash builds declare `palogo_eff0.prm` as a
|
||||
//! full-screen primitive at t=0 with `fade_argb 0xff000000` -- alpha 255 over RGB
|
||||
//! 000000 -- and turned it into a candidate predicate: a declared opaque-black
|
||||
//! backdrop separates STANDALONE screens from COMPOSITED ones. On their sixteen
|
||||
//! exported screens it splits 12 / 4, with all four exceptions independently known
|
||||
//! to be composited (the two `press_start` plates, and two loading builds that
|
||||
//! carry the `pgloading_*` set without its backdrop).
|
||||
//!
|
||||
//! That matters because the corpus previously told them "no content rule exists,
|
||||
//! take the entry index" -- correct for the question asked (recognise the splash),
|
||||
//! but this is a content rule for a different and useful question. They asked for
|
||||
//! it to be tested against an archive they do not have. This is that test.
|
||||
//!
|
||||
//! CONTROL: it must reproduce the 12/4 split on GP_TITLE's sixteen composable
|
||||
//! bundles before its disc-wide numbers mean anything.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example black_backdrop_predicate
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// A screen declares its own backdrop if some `.prm` primitive holds
|
||||
/// `fade == 0xff000000` at t = 0: full alpha over black.
|
||||
fn has_black_backdrop(b: &ui_layout::UiBuild) -> Option<String> {
|
||||
for el in &b.elements {
|
||||
if !el.name.ends_with(".prm") {
|
||||
continue;
|
||||
}
|
||||
if let Some(k) = el.keyframes.iter().find(|k| k.time == Some(0)) {
|
||||
if k.fade == 0xff00_0000 {
|
||||
return Some(el.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
|
||||
println!("== CONTROL: GP_TITLE's 16 composable bundles (port reports 12 with, 4 without)");
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let (mut y, mut n) = (0, 0);
|
||||
for e in 0..16usize {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
match has_black_backdrop(&b) {
|
||||
Some(nm) => {
|
||||
y += 1;
|
||||
println!(" entry {e:>2} YES {nm}")
|
||||
}
|
||||
None => {
|
||||
n += 1;
|
||||
println!(" entry {e:>2} no")
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" -> {y} with, {n} without\n");
|
||||
|
||||
println!("== DISC-WIDE, over every screen build");
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut tot, mut with) = (0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
let (mut t, mut w) = (0usize, 0usize);
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ui_layout::is_build(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
t += 1;
|
||||
if has_black_backdrop(&b).is_some() {
|
||||
w += 1
|
||||
}
|
||||
}
|
||||
if t > 0 {
|
||||
println!("{name:30} {w:4} / {t:<4} declare a black backdrop");
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
tot += t;
|
||||
with += w;
|
||||
}
|
||||
println!(
|
||||
"\n{with} of {tot} screen builds disc-wide declare an opaque-black backdrop \
|
||||
({:.1} %)",
|
||||
100.0 * with as f64 / tot as f64
|
||||
);
|
||||
println!("--- END ---");
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
//! Control for the new public accessor `ui_layout::sprite_blend_additive`.
|
||||
//!
|
||||
//! `blend_vs_t8ad_bit` established the field by reading the `T8aD` header inline.
|
||||
//! The exporter cannot do that — `Element` exposed nothing at `+0x04`, which is
|
||||
//! why a blend map keyed by SCREEN NAME had to be authored, and why the Japanese
|
||||
//! menus were being asserted-by-omission to blend differently from the English
|
||||
//! ones. This checks the accessor the exporter will actually call, against the
|
||||
//! same 35 oracle rows, so a later refactor cannot silently change the field.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example blend_api_check
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// (build entry, sprite, measured additive?) — from `data/blend-bit-vs-oracle.txt`,
|
||||
/// every label an `RB_BLENDCONTROL0` value read out of the guest command stream.
|
||||
const MEASURED: &[(usize, &str, bool)] = &[
|
||||
(4, "ptbase2.t32", false),
|
||||
(4, "ptlogo1.t32", false),
|
||||
(4, "ptlogo2.t32", false),
|
||||
(4, "ptlogo_tm.t32", false),
|
||||
(4, "ptcopyright.t32", false),
|
||||
(4, "ptlogo_back2.t32", false),
|
||||
(4, "ptlogo_back2eff.t32", false),
|
||||
(2, "ptbtn00.t32", false),
|
||||
(2, "ptbtn00f.t32", true),
|
||||
(5, "ptbase.t32", false),
|
||||
(5, "ptmsg.t32", false),
|
||||
(5, "ptbtn01f.t32", false),
|
||||
(5, "ptbtneff01.t32", false),
|
||||
(5, "pteff10.t32", true),
|
||||
(5, "pteff12.t32", true),
|
||||
(6, "pteff21.t32", true),
|
||||
(6, "pteff22.t32", true),
|
||||
(6, "pteff23.t32", true),
|
||||
(6, "ptframe3.t32", true),
|
||||
(6, "ptframe4.t32", true),
|
||||
(6, "pteff03.t32", true),
|
||||
(6, "pteff03a.t32", true),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let (mut ok, mut bad, mut missing) = (0, 0, 0);
|
||||
println!(
|
||||
"{:<7} {:<22} {:<10} {:<10} ",
|
||||
"entry", "sprite", "expected", "accessor"
|
||||
);
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let by = ar.read(&ar.entries()[e]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for &(oe, name, additive) in MEASURED {
|
||||
if oe != e {
|
||||
continue;
|
||||
}
|
||||
// Prefer the Element accessor; fall back to the by-name one for
|
||||
// focused variants, which are reached through `opt ` and carry no
|
||||
// top-level element of their own.
|
||||
let got = b
|
||||
.elements
|
||||
.iter()
|
||||
.find(|x| x.sprite.as_deref() == Some(name))
|
||||
.and_then(|el| ui_layout::sprite_blend_additive(&b, &by, el))
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, &by, name));
|
||||
if got.is_none() {
|
||||
println!("{e:<7} {name:<22} {additive:<10} {:<10} MISSING", "-");
|
||||
missing += 1;
|
||||
continue;
|
||||
}
|
||||
match got {
|
||||
Some(g) if g == additive => {
|
||||
ok += 1;
|
||||
println!("{e:<7} {name:<22} {additive:<10} {g:<10} OK");
|
||||
}
|
||||
other => {
|
||||
bad += 1;
|
||||
println!("{e:<7} {name:<22} {additive:<10} {other:?} MISMATCH");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n{ok} agree, {bad} mismatched, {missing} not found (of {})",
|
||||
MEASURED.len()
|
||||
);
|
||||
// The control that removes the test's own subject: the accessor must also
|
||||
// report a MIX. An accessor stuck at one value would pass every `false` row.
|
||||
let by = ar.read(&ar.entries()[6]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let add = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| ui_layout::sprite_blend_additive(&b, &by, e) == Some(true))
|
||||
.count();
|
||||
let over = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| ui_layout::sprite_blend_additive(&b, &by, e) == Some(false))
|
||||
.count();
|
||||
println!("control -- entry 6 must report BOTH values: additive={add} alpha-over={over}");
|
||||
assert!(add > 0 && over > 0, "accessor is not discriminating");
|
||||
assert_eq!(
|
||||
bad, 0,
|
||||
"the public accessor disagrees with the committed oracle"
|
||||
);
|
||||
println!("PASS");
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
//! A PREDICTION, written before the capture that tests it.
|
||||
//!
|
||||
//! `blend_vs_t8ad_bit` finds that `T8aD +0x04` bit `0x02` separates additive from
|
||||
//! alpha-over on all 35 elements whose blend has been measured off the GPU, and
|
||||
//! that no other bit of the 48-byte header does. That is a fit to three screens.
|
||||
//!
|
||||
//! The developer splash (`GP_TITLE` entries 10 and 13) has **never been captured**
|
||||
//! and is one of the five screens the port ships. This prints what the bit says
|
||||
//! its elements should be, so the capture can falsify it rather than confirm it.
|
||||
//!
|
||||
//! Takes an archive and a build list, so the prediction can be written for any
|
||||
//! screen -- including one in a DIFFERENT pak, which is the sharper test: the
|
||||
//! splash predicts alpha-over for both its elements and so can only fail, never
|
||||
//! discriminate, while a screen with a predicted MIX can do both.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example blend_prediction_splash
|
||||
//! cargo run -p sylpheed-formats --example blend_prediction_splash -- GP_OPTIONS 0 1 2
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = args
|
||||
.iter()
|
||||
.find(|a| a.parse::<usize>().is_err())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "GP_TITLE".to_string());
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
let builds: Vec<usize> = args.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
let builds = if builds.is_empty() {
|
||||
(0..ar.entries().len()).collect()
|
||||
} else {
|
||||
builds
|
||||
};
|
||||
for e in builds {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("=== {pak} entry {e} ===");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 8 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
let w = u32::from_be_bytes([s[4], s[5], s[6], s[7]]);
|
||||
println!(
|
||||
"{n:<26} +0x04 = {w:08X} bit 0x02 {} PREDICT {}",
|
||||
if w & 2 != 0 { "SET " } else { "clear" },
|
||||
if w & 2 != 0 { "ADDITIVE" } else { "alpha-over" }
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
//! Does `T8aD +0x04` bit `0x02` predict the blend the GAME uses?
|
||||
//!
|
||||
//! ⚠️ **`REFUTED.md` kills this claim**: *"`T8aD +0x04` bit `0x02` selects an
|
||||
//! additive blend" → mine, and refuted. Blending those sprites additively
|
||||
//! worsens every measure against the capture.* That refutation rests entirely on
|
||||
//! **our renderer** — it is a claim about our renderer, and the corpus's own rule
|
||||
//! says so. Since it was written, the blend has been measured off the GPU per
|
||||
//! draw on three screens (`structures/ui-blend-mode-measured.md`), so the claim
|
||||
//! can now be tested against the oracle instead of against a render.
|
||||
//!
|
||||
//! The labels below are **not** from a render. Every one is a
|
||||
//! `RB_BLENDCONTROL0` value read out of the guest command stream and attributed
|
||||
//! to an element by quad size:
|
||||
//! `data/ui-blend-mode-measured.txt`, `data/ui-blend-title-and-replication.txt`,
|
||||
//! `data/ui-blend-extras-complete.txt`.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example blend_vs_t8ad_bit
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// (build entry, sprite, measured additive?) — the oracle's verdicts, verbatim.
|
||||
const MEASURED: &[(usize, &str, bool)] = &[
|
||||
// --- GP_TITLE entry 4 + 2, the live title -------------------------------
|
||||
(4, "ptbase2.t32", false),
|
||||
(4, "ptlogo1.t32", false),
|
||||
(4, "ptlogo2.t32", false),
|
||||
(4, "ptlogo_tm.t32", false),
|
||||
(4, "ptcopyright.t32", false),
|
||||
(4, "ptlogo_back2.t32", false),
|
||||
(4, "ptlogo_back2eff.t32", false),
|
||||
(2, "ptbtn00.t32", false),
|
||||
(2, "ptbtn00f.t32", true),
|
||||
// --- entry 5, the main menu ---------------------------------------------
|
||||
(5, "ptbase.t32", false),
|
||||
(5, "ptmsg.t32", false),
|
||||
(5, "ptbtn01f.t32", false),
|
||||
(5, "ptbtneff01.t32", false),
|
||||
(5, "pteff10.t32", true),
|
||||
(5, "pteff12.t32", true),
|
||||
(5, "ptframe1.t32", true),
|
||||
(5, "ptframe2.t32", true),
|
||||
(5, "pteff03.t32", true), // the rotated sweep strips, via ptloop01/02
|
||||
(5, "pteff03a.t32", true),
|
||||
// --- entry 6, EXTRAS ------------------------------------------------------
|
||||
(6, "ptbase.t32", false),
|
||||
(6, "ptmsg2.t32", false),
|
||||
(6, "pttitle.t32", false),
|
||||
(6, "ptbtn11f.t32", false),
|
||||
(6, "ptbtn12.t32", false),
|
||||
(6, "ptbtn13.t32", false),
|
||||
(6, "ptbtneff02.t32", false),
|
||||
(6, "pteff10.t32", true),
|
||||
(6, "pteff20.t32", true),
|
||||
(6, "pteff21.t32", true),
|
||||
(6, "pteff22.t32", true),
|
||||
(6, "pteff23.t32", true),
|
||||
(6, "ptframe3.t32", true),
|
||||
(6, "ptframe4.t32", true),
|
||||
(6, "pteff03.t32", true),
|
||||
(6, "pteff03a.t32", true),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|a| a == "decl") {
|
||||
decl_rivals();
|
||||
return;
|
||||
}
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let mut hdr: BTreeMap<(usize, String), u32> = BTreeMap::new();
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let by = ar.read(&ar.entries()[e]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for (n, &(off, size)) in &b.sprites {
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 8 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
hdr.insert((e, n.clone()), u32::from_be_bytes([s[4], s[5], s[6], s[7]]));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:<10} {:<22} {:<10} {:>10} measured blend",
|
||||
"entry", "sprite", "+0x04", "bit 0x02"
|
||||
);
|
||||
let (mut tp, mut tn, mut fp, mut fnn, mut missing) = (0, 0, 0, 0, 0);
|
||||
for &(e, n, additive) in MEASURED {
|
||||
let Some(&w) = hdr.get(&(e, n.to_string())) else {
|
||||
println!(
|
||||
"{e:<10} {n:<22} {:<10} {:>10} {}",
|
||||
"MISSING",
|
||||
"-",
|
||||
if additive { "ADDITIVE" } else { "alpha-over" }
|
||||
);
|
||||
missing += 1;
|
||||
continue;
|
||||
};
|
||||
let bit = w & 0x02 != 0;
|
||||
match (bit, additive) {
|
||||
(true, true) => tp += 1,
|
||||
(false, false) => tn += 1,
|
||||
(true, false) => fp += 1,
|
||||
(false, true) => fnn += 1,
|
||||
}
|
||||
println!(
|
||||
"{e:<10} {n:<22} {:08X} {:>10} {}{}",
|
||||
w,
|
||||
bit,
|
||||
if additive { "ADDITIVE" } else { "alpha-over" },
|
||||
if bit == additive {
|
||||
""
|
||||
} else {
|
||||
" <== DISAGREES"
|
||||
}
|
||||
);
|
||||
}
|
||||
println!("\nbit set & additive {tp}");
|
||||
println!("bit clear & alpha-over {tn}");
|
||||
println!("bit set & alpha-over {fp} <- false positives");
|
||||
println!("bit clear & additive {fnn} <- false negatives");
|
||||
println!("sprite not found {missing}");
|
||||
println!(
|
||||
"\n{}",
|
||||
if fp == 0 && fnn == 0 && missing == 0 {
|
||||
"PERFECT PARTITION on every element whose blend was measured."
|
||||
} else {
|
||||
"THE BIT DOES NOT PREDICT THE MEASURED BLEND."
|
||||
}
|
||||
);
|
||||
|
||||
// ── THE CONTROL THAT MATTERS ────────────────────────────────────────────
|
||||
// A perfect partition is worthless if half the header partitions equally
|
||||
// well: then the sample is too small to single out a field, and picking
|
||||
// `+0x04` bit 0x02 out of the tie is the same mistake as picking `+0x08`
|
||||
// 0x8050 was. So: how many OTHER bits of the first 12 header words separate
|
||||
// the same 35 elements without error?
|
||||
let mut rivals: Vec<String> = Vec::new();
|
||||
let mut words: BTreeMap<(usize, String), Vec<u32>> = BTreeMap::new();
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let by = ar.read(&ar.entries()[e]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for (n, &(off, size)) in &b.sprites {
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 48 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
words.insert(
|
||||
(e, n.clone()),
|
||||
(0..12)
|
||||
.map(|k| {
|
||||
u32::from_be_bytes([s[k * 4], s[k * 4 + 1], s[k * 4 + 2], s[k * 4 + 3]])
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for w in 0..12 {
|
||||
for bit in 0..32 {
|
||||
let mut ok = true;
|
||||
let mut set_seen = false;
|
||||
let mut clear_seen = false;
|
||||
for &(e, n, additive) in MEASURED {
|
||||
let Some(v) = words.get(&(e, n.to_string())) else {
|
||||
ok = false;
|
||||
break;
|
||||
};
|
||||
let on = (v[w] >> bit) & 1 == 1;
|
||||
if on {
|
||||
set_seen = true
|
||||
} else {
|
||||
clear_seen = true
|
||||
}
|
||||
if on != additive {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A constant bit trivially "agrees" with nothing; require both sides.
|
||||
if ok && set_seen && clear_seen {
|
||||
rivals.push(format!("+0x{:02X} bit {bit} (0x{:X})", w * 4, 1u32 << bit));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\nRIVAL FIELDS — other bits of the first 12 header words that separate");
|
||||
println!("the same 35 elements with zero errors: {}", rivals.len());
|
||||
for r in &rivals {
|
||||
println!(" {r}");
|
||||
}
|
||||
if rivals.len() == 1 {
|
||||
println!(" -> the sample singles out ONE field. Nothing else in the header does it.");
|
||||
} else {
|
||||
println!(
|
||||
" -> the sample does NOT single out a field; {} candidates tie.",
|
||||
rivals.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── An integrity check the published decode did NOT do ──────────────────────
|
||||
// The rival sweep above covers the 48-byte T8aD header. It does NOT cover the
|
||||
// 60-byte DECLARATION entry, and the earlier declaration hunt was run with
|
||||
// labels taken from the port's RENDER -- which put pteff10, pteff12, pteff20 and
|
||||
// pteff21..23 on the alpha-over side, where the oracle says all six are
|
||||
// additive. So the declaration has never been swept with correct labels, and if
|
||||
// one of its words also partitions the 35 without error, "the field is the T8aD
|
||||
// bit" is underdetermined.
|
||||
//
|
||||
// Run as: cargo run -p sylpheed-formats --example blend_vs_t8ad_bit -- decl
|
||||
#[allow(dead_code)]
|
||||
fn decl_rivals() {
|
||||
use std::collections::BTreeMap;
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
const AT: usize = 0x20;
|
||||
const STRIDE: usize = 60;
|
||||
let mut decl: BTreeMap<(usize, String), Vec<u32>> = BTreeMap::new();
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
if by.len() < 0x18 {
|
||||
continue;
|
||||
}
|
||||
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
||||
for i in 0..count {
|
||||
let at = AT + i * STRIDE;
|
||||
if at + STRIDE > by.len() {
|
||||
break;
|
||||
}
|
||||
let end = by[at..at + 12].iter().position(|&c| c == 0).unwrap_or(12);
|
||||
let name = String::from_utf8_lossy(&by[at..at + end]).to_string();
|
||||
decl.insert(
|
||||
(e, name),
|
||||
(0..15)
|
||||
.map(|k| {
|
||||
u32::from_be_bytes([
|
||||
by[at + k * 4],
|
||||
by[at + k * 4 + 1],
|
||||
by[at + k * 4 + 2],
|
||||
by[at + k * 4 + 3],
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut missing: Vec<String> = Vec::new();
|
||||
for &(e, n, _) in MEASURED {
|
||||
if !decl.contains_key(&(e, n.to_string())) {
|
||||
missing.push(format!("entry {e} {n}"));
|
||||
}
|
||||
}
|
||||
println!("\n=== DECLARATION-ENTRY RIVAL SWEEP ===");
|
||||
println!(
|
||||
"measured elements with NO declaration entry of their own: {} of {}",
|
||||
missing.len(),
|
||||
MEASURED.len()
|
||||
);
|
||||
for m in &missing {
|
||||
println!(" {m}");
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
println!(" -> no declaration field can select the blend for these, because they");
|
||||
println!(" have no declaration entry. The header is the only per-sprite home.");
|
||||
}
|
||||
let labelled: Vec<&(usize, &str, bool)> = MEASURED
|
||||
.iter()
|
||||
.filter(|(e, n, _)| decl.contains_key(&(*e, n.to_string())))
|
||||
.collect();
|
||||
let mut rivals = 0;
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for w in 0..15 {
|
||||
for bit in 0..32 {
|
||||
let (mut ok, mut s, mut c) = (true, false, false);
|
||||
for &&(e, n, additive) in &labelled {
|
||||
let on = (decl[&(e, n.to_string())][w] >> bit) & 1 == 1;
|
||||
if on {
|
||||
s = true
|
||||
} else {
|
||||
c = true
|
||||
}
|
||||
if on != additive {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ok && s && c {
|
||||
println!(" RIVAL: declaration +0x{:02X} bit {bit}", w * 4);
|
||||
rivals += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"declaration bits that separate the {} labellable elements: {rivals}",
|
||||
labelled.len()
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
//! those float values.
|
||||
//!
|
||||
//! Usage: bounds_in_descriptor <container.xpr> <resource>...
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model};
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
@@ -24,9 +24,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else {
|
||||
continue;
|
||||
};
|
||||
let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else { continue };
|
||||
println!(
|
||||
"{} descriptor 0x{d0:x}..0x{d1:x} ({} bytes), box lo{:?} hi{:?}",
|
||||
m.name,
|
||||
@@ -36,12 +34,8 @@ fn main() {
|
||||
);
|
||||
// Where in the descriptor does each bound value appear (±0.01)?
|
||||
let targets: Vec<(&str, f32)> = vec![
|
||||
("lo.x", lo[0]),
|
||||
("lo.y", lo[1]),
|
||||
("lo.z", lo[2]),
|
||||
("hi.x", hi[0]),
|
||||
("hi.y", hi[1]),
|
||||
("hi.z", hi[2]),
|
||||
("lo.x", lo[0]), ("lo.y", lo[1]), ("lo.z", lo[2]),
|
||||
("hi.x", hi[0]), ("hi.y", hi[1]), ("hi.z", hi[2]),
|
||||
];
|
||||
for (label, v) in targets {
|
||||
let mut at: Vec<usize> = Vec::new();
|
||||
@@ -53,10 +47,7 @@ fn main() {
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
println!(
|
||||
" {label:5} {v:10.3} at descriptor offsets {:x?}",
|
||||
&at[..at.len().min(6)]
|
||||
);
|
||||
println!(" {label:5} {v:10.3} at descriptor offsets {:x?}", &at[..at.len().min(6)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text = TextIndex::build(&pak);
|
||||
let mut stages = game_data::load_stages(&pak);
|
||||
stages.retain(|s| {
|
||||
s.id.starts_with('S')
|
||||
&& s.id.len() == 3
|
||||
&& s.id[1..].parse::<u32>().map(|n| n <= 16).unwrap_or(false)
|
||||
});
|
||||
stages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text=TextIndex::build(&pak);
|
||||
let mut stages=game_data::load_stages(&pak);
|
||||
stages.retain(|s|s.id.starts_with('S') && s.id.len()==3 && s.id[1..].parse::<u32>().map(|n|n<=16).unwrap_or(false));
|
||||
stages.sort_by(|a,b|a.id.cmp(&b.id));
|
||||
println!("═══ CAMPAIGN (S01–S16) ═══");
|
||||
for s in &stages {
|
||||
let obj = text.objectives(&s.id, 1);
|
||||
for s in &stages{
|
||||
let obj=text.objectives(&s.id,1);
|
||||
println!("\n{} · {}", s.id, s.location.as_deref().unwrap_or("?"));
|
||||
for o in obj.iter().take(2) {
|
||||
println!(" ▸ {o}");
|
||||
}
|
||||
for o in obj.iter().take(2){ println!(" ▸ {o}"); }
|
||||
}
|
||||
// roster with real names
|
||||
let mut chars = game_data::load_characters(&pak);
|
||||
chars.retain(|c| {
|
||||
c.faction.as_deref() == Some("TCAF") && c.unique == Some(true) && c.faces.len() >= 4
|
||||
});
|
||||
let mut chars=game_data::load_characters(&pak);
|
||||
chars.retain(|c|c.faction.as_deref()==Some("TCAF") && c.unique==Some(true) && c.faces.len()>=4);
|
||||
println!("\n═══ PRINCIPAL CAST (TCAF, named) ═══");
|
||||
for c in &chars {
|
||||
let id = c.id.as_deref().unwrap_or("");
|
||||
let name = text
|
||||
.character_name(id.trim_start_matches("Character"))
|
||||
.or_else(|| c.name_key.as_deref().and_then(|k| text.get(k)))
|
||||
.unwrap_or("?");
|
||||
println!(
|
||||
" {name:12} ({} portraits) [{}]",
|
||||
c.faces.len(),
|
||||
id.trim_start_matches("Character")
|
||||
);
|
||||
for c in &chars{
|
||||
let id=c.id.as_deref().unwrap_or("");
|
||||
let name=text.character_name(id.trim_start_matches("Character")).or_else(||c.name_key.as_deref().and_then(|k|text.get(k))).unwrap_or("?");
|
||||
println!(" {name:12} ({} portraits) [{}]", c.faces.len(), id.trim_start_matches("Character"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --release --example capture_ib_truth -- <Stage_SNN.xpr> <capture.log>...
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn q(v: f32) -> i64 {
|
||||
(v as f64 * 1e4).round() as i64
|
||||
@@ -60,13 +60,7 @@ fn main() {
|
||||
let mut o = 0usize;
|
||||
while o + 12 <= bytes.len() {
|
||||
let (x, y, z) = (be(o), be(o + 4), be(o + 8));
|
||||
if x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& z.is_finite()
|
||||
&& x.abs() < 1e6
|
||||
&& y.abs() < 1e6
|
||||
&& z.abs() < 1e6
|
||||
{
|
||||
if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 {
|
||||
index.entry((q(x), q(y), q(z))).or_default().push(o as u32);
|
||||
}
|
||||
o += 4;
|
||||
@@ -78,9 +72,7 @@ fn main() {
|
||||
for dx in -1..=1i64 {
|
||||
for dy in -1..=1i64 {
|
||||
for dz in -1..=1i64 {
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue };
|
||||
for &off in cands {
|
||||
for stride in (12..=64).step_by(4) {
|
||||
let ok = (1..4).all(|j| {
|
||||
@@ -104,9 +96,7 @@ fn main() {
|
||||
eprintln!("no draw could be placed in this container");
|
||||
std::process::exit(1);
|
||||
};
|
||||
println!(
|
||||
"container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)"
|
||||
);
|
||||
println!("container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)");
|
||||
|
||||
// ── Our decoder's view of the same container.
|
||||
let models = Xbg7Model::stage_models(&bytes);
|
||||
@@ -114,19 +104,14 @@ fn main() {
|
||||
for m in &models {
|
||||
for sm in &m.meshes {
|
||||
if let Some(off) = sm.vbuf_offset {
|
||||
by_off.entry(off).or_default().push((
|
||||
m.name.clone(),
|
||||
sm.positions.len(),
|
||||
sm.indices.len(),
|
||||
));
|
||||
by_off
|
||||
.entry(off)
|
||||
.or_default()
|
||||
.push((m.name.clone(), sm.positions.len(), sm.indices.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"decoded {} resources, {} distinct vertex offsets\n",
|
||||
models.len(),
|
||||
by_off.len()
|
||||
);
|
||||
println!("decoded {} resources, {} distinct vertex offsets\n", models.len(), by_off.len());
|
||||
|
||||
// Declared-but-not-decoded resources, indexed by their first marker's
|
||||
// (vertex, index) counts. A drawn buffer our decoder cannot name is the one
|
||||
@@ -148,21 +133,13 @@ fn main() {
|
||||
}
|
||||
|
||||
// ── The report: one row per drawn BUFFER, aggregating its index batches.
|
||||
let mut per_buf: HashMap<
|
||||
u32,
|
||||
(
|
||||
usize,
|
||||
Vec<sylpheed_formats::ship_capture::CapturedIndexBuffer>,
|
||||
u32,
|
||||
),
|
||||
> = HashMap::new();
|
||||
let mut per_buf: HashMap<u32, (usize, Vec<sylpheed_formats::ship_capture::CapturedIndexBuffer>, u32)> =
|
||||
HashMap::new();
|
||||
for (delta, voff, d) in &hits {
|
||||
if *delta != base_delta {
|
||||
continue;
|
||||
}
|
||||
let e = per_buf
|
||||
.entry(d.vbase)
|
||||
.or_insert((*voff, Vec::new(), d.vcount));
|
||||
let e = per_buf.entry(d.vbase).or_insert((*voff, Vec::new(), d.vcount));
|
||||
let ib = d.ib.unwrap();
|
||||
if !e.1.contains(&ib) {
|
||||
e.1.push(ib);
|
||||
@@ -170,10 +147,9 @@ fn main() {
|
||||
}
|
||||
|
||||
let (mut pad0, mut pad_small, mut pad_off, mut unnamed) = (0usize, 0usize, 0usize, 0usize);
|
||||
let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) =
|
||||
(0usize, 0usize, 0usize, 0usize);
|
||||
let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) = (0usize, 0usize, 0usize, 0usize);
|
||||
let mut rows: Vec<(usize, String)> = Vec::new();
|
||||
for (voff, ibs, vcount) in per_buf.values() {
|
||||
for (_, (voff, ibs, vcount)) in per_buf.iter() {
|
||||
let batches = ibs.len();
|
||||
let total: u32 = ibs.iter().map(|i| i.icount).sum();
|
||||
let lo = ibs.iter().map(|i| i.ibase).min().unwrap() as i64 - base_delta;
|
||||
@@ -181,11 +157,8 @@ fn main() {
|
||||
let umax = ibs.iter().map(|i| i.imax).max().unwrap();
|
||||
let gap = *voff as i64 - hi; // bytes from the end of the index data to the vertex buffer
|
||||
let names = by_off.get(voff);
|
||||
let dec_idx = names.and_then(|v| {
|
||||
v.iter()
|
||||
.find(|(_, p, _)| *p as u32 == *vcount)
|
||||
.map(|(_, _, i)| *i as u32)
|
||||
});
|
||||
let dec_idx = names
|
||||
.and_then(|v| v.iter().find(|(_, p, _)| *p as u32 == *vcount).map(|(_, _, i)| *i as u32));
|
||||
// The decoder's assumption, scored: it expects the whole index buffer at
|
||||
// `vb - 2*idx_count - pad`, pad ≤ 3.
|
||||
let dec_pad = dec_idx.map(|i| *voff as i64 - (i as i64) * 2 - lo);
|
||||
@@ -245,7 +218,5 @@ fn main() {
|
||||
println!(
|
||||
"index extent: our idx_count == sum of captured batches for {idx_equal} buffers, differs for {idx_partial}"
|
||||
);
|
||||
println!(
|
||||
"vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}"
|
||||
);
|
||||
println!("vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}");
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --release --example capture_index_bytes -- <Stage_SNN.xpr> <capture.log>...
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn q(v: f32) -> i64 {
|
||||
(v as f64 * 1e4).round() as i64
|
||||
@@ -34,10 +34,7 @@ fn main() {
|
||||
let text = std::fs::read_to_string(log).expect("log");
|
||||
for d in parse_capture(&text) {
|
||||
let k = d.ib.map(|i| (i.ibase, i.icount)).unwrap_or((0, 0));
|
||||
if d.ib.is_some_and(|i| i.head_len > 0)
|
||||
&& d.pos.len() >= 4
|
||||
&& seen.insert((log.clone(), d.vbase, k))
|
||||
{
|
||||
if d.ib.map_or(false, |i| i.head_len > 0) && d.pos.len() >= 4 && seen.insert((log.clone(), d.vbase, k)) {
|
||||
draws.push(d);
|
||||
}
|
||||
}
|
||||
@@ -51,13 +48,7 @@ fn main() {
|
||||
let mut o = 0usize;
|
||||
while o + 12 <= bytes.len() {
|
||||
let (x, y, z) = (be(o), be(o + 4), be(o + 8));
|
||||
if x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& z.is_finite()
|
||||
&& x.abs() < 1e6
|
||||
&& y.abs() < 1e6
|
||||
&& z.abs() < 1e6
|
||||
{
|
||||
if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 {
|
||||
index.entry((q(x), q(y), q(z))).or_default().push(o as u32);
|
||||
}
|
||||
o += 4;
|
||||
@@ -69,9 +60,7 @@ fn main() {
|
||||
for dx in -1..=1i64 {
|
||||
for dy in -1..=1i64 {
|
||||
for dz in -1..=1i64 {
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue };
|
||||
for &off in cands {
|
||||
for stride in (12..=64).step_by(4) {
|
||||
let ok = (1..4).all(|j| {
|
||||
@@ -103,11 +92,10 @@ fn main() {
|
||||
for m in &models {
|
||||
for sm in &m.meshes {
|
||||
if let Some(off) = sm.vbuf_offset {
|
||||
by_off.entry(off).or_default().push((
|
||||
m.name.clone(),
|
||||
sm.positions.len(),
|
||||
sm.indices.clone(),
|
||||
));
|
||||
by_off
|
||||
.entry(off)
|
||||
.or_default()
|
||||
.push((m.name.clone(), sm.positions.len(), sm.indices.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,10 +112,7 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
let ibase = d.ib.unwrap().ibase as i64;
|
||||
ib_start
|
||||
.entry(d.vbase)
|
||||
.and_modify(|e| *e = (*e).min(ibase))
|
||||
.or_insert(ibase);
|
||||
ib_start.entry(d.vbase).and_modify(|e| *e = (*e).min(ibase)).or_insert(ibase);
|
||||
}
|
||||
|
||||
let (mut agree, mut disagree, mut unmatched, mut nooverlap) = (0usize, 0usize, 0usize, 0usize);
|
||||
|
||||
@@ -1,37 +1,21 @@
|
||||
//! Identify which stage + ship a capture log came from: match the capture's
|
||||
//! draw vertex counts against every resource (incl. LODs) of every Stage_SNN.xpr.
|
||||
//! cargo run --release --example capture_match -- <capture.log> <resource3d dir>
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let text = std::fs::read_to_string(&a[1]).unwrap();
|
||||
let mut draws = parse_capture(&text);
|
||||
if draws.is_empty() {
|
||||
draws = parse_drawlog(&text);
|
||||
}
|
||||
let caps: HashSet<u32> = draws
|
||||
.iter()
|
||||
.map(|d| d.vcount)
|
||||
.filter(|v| *v >= 100)
|
||||
.collect();
|
||||
eprintln!(
|
||||
"{} draws, {} distinct vcounts>=100",
|
||||
draws.len(),
|
||||
caps.len()
|
||||
);
|
||||
if draws.is_empty() { draws = parse_drawlog(&text); }
|
||||
let caps: HashSet<u32> = draws.iter().map(|d| d.vcount).filter(|v| *v >= 100).collect();
|
||||
eprintln!("{} draws, {} distinct vcounts>=100", draws.len(), caps.len());
|
||||
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&a[2])
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
let n = e.file_name().to_string_lossy().to_string();
|
||||
n.starts_with("Stage_") && n.ends_with(".xpr")
|
||||
})
|
||||
.map(|e| e.path())
|
||||
.collect();
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&a[2]).unwrap().filter_map(|e| e.ok())
|
||||
.filter(|e| { let n = e.file_name().to_string_lossy().to_string(); n.starts_with("Stage_") && n.ends_with(".xpr") })
|
||||
.map(|e| e.path()).collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
@@ -44,18 +28,12 @@ fn main() {
|
||||
let vc: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||
if vc >= 100 && caps.contains(&(vc as u32)) {
|
||||
let id = m.name.get(..4).unwrap_or("?").to_string();
|
||||
hits.entry(id)
|
||||
.or_default()
|
||||
.push((m.name.clone(), vc as u32));
|
||||
hits.entry(id).or_default().push((m.name.clone(), vc as u32));
|
||||
}
|
||||
}
|
||||
let total: usize = hits.values().map(|v| v.len()).sum();
|
||||
if total >= 3 {
|
||||
println!(
|
||||
"== {} : {} matching resources ==",
|
||||
path.file_name().unwrap().to_string_lossy(),
|
||||
total
|
||||
);
|
||||
println!("== {} : {} matching resources ==", path.file_name().unwrap().to_string_lossy(), total);
|
||||
for (id, v) in &hits {
|
||||
let s: Vec<String> = v.iter().map(|(n, c)| format!("{n}({c})")).collect();
|
||||
println!(" {id}: {}", s.join(" "));
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
//! `Stage_S01` gave us.
|
||||
//!
|
||||
//! Usage: capture_truth_scan <resource3d_dir> <capture.log>...
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn q(v: f32) -> i64 {
|
||||
(v as f64 * 1e4).round() as i64
|
||||
@@ -47,9 +47,7 @@ fn main() {
|
||||
|
||||
let mut placed = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
// Index quantised position triples. Junk floats (NaN/huge) are skipped,
|
||||
// which prunes most of a texture-heavy container.
|
||||
let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||
@@ -57,12 +55,7 @@ fn main() {
|
||||
let mut o = 0usize;
|
||||
while o + 12 <= bytes.len() {
|
||||
let (x, y, z) = (be(o), be(o + 4), be(o + 8));
|
||||
if x.is_finite()
|
||||
&& y.is_finite()
|
||||
&& z.is_finite()
|
||||
&& x.abs() < 1e6
|
||||
&& y.abs() < 1e6
|
||||
&& z.abs() < 1e6
|
||||
if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6
|
||||
{
|
||||
index.entry((q(x), q(y), q(z))).or_default().push(o as u32);
|
||||
}
|
||||
@@ -84,8 +77,9 @@ fn main() {
|
||||
let ok = (1..4).all(|j| {
|
||||
let at = off as usize + j * stride;
|
||||
at + 12 <= bytes.len()
|
||||
&& (0..3)
|
||||
.all(|c| (be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4)
|
||||
&& (0..3).all(|c| {
|
||||
(be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4
|
||||
})
|
||||
});
|
||||
if ok {
|
||||
deltas
|
||||
|
||||
@@ -71,27 +71,15 @@ fn main() {
|
||||
for p in &base_parts {
|
||||
let mut vcounts = Vec::new();
|
||||
let mut pos = Vec::new();
|
||||
for cand in [
|
||||
p.clone(),
|
||||
format!("{p}_m"),
|
||||
format!("{p}_l"),
|
||||
format!("{p}_d"),
|
||||
] {
|
||||
for cand in [p.clone(), format!("{p}_m"), format!("{p}_l"), format!("{p}_d")] {
|
||||
if let Some(m) = models.iter().find(|m| m.name == cand) {
|
||||
let mp: Vec<[f32; 3]> = m
|
||||
.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.collect();
|
||||
let mp: Vec<[f32; 3]> =
|
||||
m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect();
|
||||
vcounts.push(mp.len() as u32);
|
||||
pos.extend(mp);
|
||||
}
|
||||
}
|
||||
parts.push(Part {
|
||||
base: p.clone(),
|
||||
vcounts,
|
||||
pos,
|
||||
});
|
||||
parts.push(Part { base: p.clone(), vcounts, pos });
|
||||
}
|
||||
|
||||
// Validate a draw against a part (direct or X-mirrored), like ship_capture.
|
||||
@@ -100,9 +88,7 @@ fn main() {
|
||||
return None;
|
||||
}
|
||||
let near = |a: &[f32; 3], b: &[f32; 3]| {
|
||||
(a[0] - b[0]).abs() <= 1e-2
|
||||
&& (a[1] - b[1]).abs() <= 1e-2
|
||||
&& (a[2] - b[2]).abs() <= 1e-2
|
||||
(a[0] - b[0]).abs() <= 1e-2 && (a[1] - b[1]).abs() <= 1e-2 && (a[2] - b[2]).abs() <= 1e-2
|
||||
};
|
||||
let (mut direct, mut mirror) = (0usize, 0usize);
|
||||
for q in &d.pos {
|
||||
@@ -158,11 +144,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"{log}: {} validated draws, {} bdy_04 refs",
|
||||
labeled.len(),
|
||||
refs.len()
|
||||
);
|
||||
eprintln!("{log}: {} validated draws, {} bdy_04 refs", labeled.len(), refs.len());
|
||||
}
|
||||
|
||||
// Cluster per part (greedy, 8-unit radius), print clusters with ≥3 samples.
|
||||
@@ -183,7 +165,7 @@ fn main() {
|
||||
None => clusters.push((*s, 1)),
|
||||
}
|
||||
}
|
||||
clusters.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
clusters.sort_by(|x, y| y.1.cmp(&x.1));
|
||||
let tops: Vec<String> = clusters
|
||||
.iter()
|
||||
.filter(|(_, n)| *n >= 3)
|
||||
|
||||
@@ -22,19 +22,14 @@ fn main() {
|
||||
let mut rows = vec![];
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
if o.schema_hash != 0x3c9ae32e {
|
||||
continue;
|
||||
}
|
||||
let t = o.tokens();
|
||||
let stage = t
|
||||
.iter()
|
||||
.find_map(|s| {
|
||||
s.strip_prefix("EnumUnit_")
|
||||
.map(|x| x.trim_end_matches(".tbl").to_string())
|
||||
})
|
||||
.find_map(|s| s.strip_prefix("EnumUnit_").map(|x| x.trim_end_matches(".tbl").to_string()))
|
||||
.unwrap_or("?".into());
|
||||
let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string();
|
||||
rows.push((stage, bg, t.len()));
|
||||
@@ -52,20 +47,11 @@ fn main() {
|
||||
let mut all_tokens: Vec<(u32, Vec<String>)> = vec![];
|
||||
for e in ch.entries() {
|
||||
let Ok(b) = ch.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
*by_schema.entry(o.schema_hash).or_default() += 1;
|
||||
all_tokens.push((
|
||||
o.schema_hash,
|
||||
o.tokens().iter().map(|s| s.to_string()).collect(),
|
||||
));
|
||||
all_tokens.push((o.schema_hash, o.tokens().iter().map(|s| s.to_string()).collect()));
|
||||
}
|
||||
println!(
|
||||
" {} entries, {} IDXD objects",
|
||||
ch.entries().len(),
|
||||
all_tokens.len()
|
||||
);
|
||||
println!(" {} entries, {} IDXD objects", ch.entries().len(), all_tokens.len());
|
||||
for (h, n) in &by_schema {
|
||||
println!(" schema {h:08x} x{n}");
|
||||
}
|
||||
@@ -80,16 +66,10 @@ fn main() {
|
||||
let mut hits: std::collections::BTreeSet<String> = Default::default();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
for t in o.tokens() {
|
||||
let l = t.to_ascii_lowercase();
|
||||
if l.contains("challenge")
|
||||
|| t.ends_with("_EX")
|
||||
|| t.contains("_EX4")
|
||||
|| t.contains("_EX5")
|
||||
{
|
||||
if l.contains("challenge") || t.ends_with("_EX") || t.contains("_EX4") || t.contains("_EX5") {
|
||||
hits.insert(format!("{:08x} {t}", o.schema_hash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,7 @@ fn main() {
|
||||
paks.sort();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(arc) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if KEYS.iter().filter(|k| find(&b, k.as_bytes())).count() < KEYS.len() {
|
||||
|
||||
@@ -15,9 +15,7 @@ fn main() {
|
||||
files.sort();
|
||||
let (mut total, mut composite, mut composite_named) = (0usize, 0usize, 0usize);
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
total += 1;
|
||||
let has_nodes = !scene_world_nodes(&bytes, &m.name).is_empty();
|
||||
|
||||
@@ -8,11 +8,8 @@
|
||||
//! shift chain after the exact-coverage fix.
|
||||
//!
|
||||
//! Usage: consensus_check <resource3d_dir> [--list]
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
/// Every place one model name was seen: (container, verts, tris, span).
|
||||
type Sightings = BTreeMap<String, Vec<(String, usize, usize, [i64; 3])>>;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -26,11 +23,9 @@ fn main() {
|
||||
files.sort();
|
||||
|
||||
// name -> [(container, verts, tris, span)]
|
||||
let mut seen: Sightings = BTreeMap::new();
|
||||
let mut seen: BTreeMap<String, Vec<(String, usize, usize, [i64; 3])>> = BTreeMap::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let where_ = f.file_name().unwrap().to_string_lossy().to_string();
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
@@ -52,9 +47,7 @@ fn main() {
|
||||
(hi[1] - lo[1]).round() as i64,
|
||||
(hi[2] - lo[2]).round() as i64,
|
||||
];
|
||||
seen.entry(m.name.clone())
|
||||
.or_default()
|
||||
.push((where_.clone(), v, t, span));
|
||||
seen.entry(m.name.clone()).or_default().push((where_.clone(), v, t, span));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,5 +81,7 @@ fn main() {
|
||||
println!("{r}");
|
||||
}
|
||||
}
|
||||
println!("{minority} minority decodes across {resources} resources that have a majority");
|
||||
println!(
|
||||
"{minority} minority decodes across {resources} resources that have a majority"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,18 +16,19 @@
|
||||
//! Usage:
|
||||
//! SYLPHEED_ISO=... cargo run --release --example correlate_capture -- \
|
||||
//! <capture.log> <Stage_SNN> <ship_id> [ref_part_substr] [--emit]
|
||||
//! e.g. SYLPHEED_ISO="/path/to/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso" \
|
||||
//! e.g. SYLPHEED_ISO="/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of
|
||||
//! Deception (USA, Europe) (En,Ja).iso" \
|
||||
//! cargo run --release --example correlate_capture -- \
|
||||
//! xenia_ship_capture.log Stage_S01 e106 bdy_04 --emit
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||
use sylpheed_formats::ship_capture::{
|
||||
correlate, parse_capture, parse_drawlog, serialize_table, PartKey,
|
||||
};
|
||||
use sylpheed_formats::xiso::open_iso;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
@@ -57,15 +58,10 @@ fn main() {
|
||||
// Decode the ship's base parts AND their LOD copies (vcount + leading
|
||||
// positions are the match keys; LODs share the base part's local frame).
|
||||
let bytes = {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
|
||||
rt.block_on(async {
|
||||
let mut r = open_iso(Path::new(&iso)).await.unwrap();
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr"))
|
||||
.await
|
||||
.unwrap()
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap()
|
||||
})
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
@@ -87,12 +83,7 @@ fn main() {
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
let positions_of = |name: &str| -> Option<Vec<[f32; 3]>> {
|
||||
let m = models.iter().find(|m| m.name == name)?;
|
||||
Some(
|
||||
m.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.collect(),
|
||||
)
|
||||
Some(m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect())
|
||||
};
|
||||
|
||||
// One PartKey per (part, variant-vcount present in the capture) — correlate
|
||||
@@ -103,33 +94,18 @@ fn main() {
|
||||
// variant is the same part in the same local frame.
|
||||
let mut keys: Vec<PartKey> = Vec::new();
|
||||
for part in &base_parts {
|
||||
let variants = [
|
||||
part.clone(),
|
||||
format!("{part}_m"),
|
||||
format!("{part}_l"),
|
||||
format!("{part}_d"),
|
||||
];
|
||||
let union: Vec<[f32; 3]> = variants
|
||||
.iter()
|
||||
.filter_map(|v| positions_of(v))
|
||||
.flatten()
|
||||
.collect();
|
||||
let variants =
|
||||
[part.clone(), format!("{part}_m"), format!("{part}_l"), format!("{part}_d")];
|
||||
let union: Vec<[f32; 3]> =
|
||||
variants.iter().filter_map(|v| positions_of(v)).flatten().collect();
|
||||
let mut any = false;
|
||||
for cand in &variants {
|
||||
if let Some(pos) = positions_of(cand) {
|
||||
let vcount = pos.len() as u32;
|
||||
if draws.iter().any(|d| d.vcount == vcount) {
|
||||
let lod = if cand == part {
|
||||
"full"
|
||||
} else {
|
||||
cand.rsplit('_').next().unwrap_or("?")
|
||||
};
|
||||
let lod = if cand == part { "full" } else { cand.rsplit('_').next().unwrap_or("?") };
|
||||
eprintln!(" {part:20} try vcount={vcount:6} [{lod}]");
|
||||
keys.push(PartKey {
|
||||
part: part.clone(),
|
||||
vcount,
|
||||
ref_pos: union.clone(),
|
||||
});
|
||||
keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() });
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
@@ -144,15 +120,9 @@ fn main() {
|
||||
return;
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"\nreference = {} → ship-relative placement:",
|
||||
ship.reference
|
||||
);
|
||||
eprintln!("\nreference = {} → ship-relative placement:", ship.reference);
|
||||
for p in &ship.parts {
|
||||
eprintln!(
|
||||
" {:18} T=[{:9.1}{:9.1}{:9.1}]",
|
||||
p.part, p.t[0], p.t[1], p.t[2]
|
||||
);
|
||||
eprintln!(" {:18} T=[{:9.1}{:9.1}{:9.1}]", p.part, p.t[0], p.t[1], p.t[2]);
|
||||
}
|
||||
for part in &base_parts {
|
||||
if !ship.parts.iter().any(|p| &p.part == part) {
|
||||
|
||||
@@ -16,23 +16,19 @@
|
||||
//! SYLPHEED_ISO=... cargo run --release --example correlate_frames -- \
|
||||
//! <capture.log> <Stage_SNN> <ship_id> [ref_part_substr] [--min-parts N]
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::Path;
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||
use sylpheed_formats::ship_capture::{
|
||||
correlate, parse_capture, parse_drawlog, segment_frames, PartKey,
|
||||
};
|
||||
use sylpheed_formats::xiso::open_iso;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
fn median(mut v: Vec<f32>) -> f32 {
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let n = v.len();
|
||||
if n % 2 == 1 {
|
||||
v[n / 2]
|
||||
} else {
|
||||
0.5 * (v[n / 2 - 1] + v[n / 2])
|
||||
}
|
||||
if n % 2 == 1 { v[n / 2] } else { 0.5 * (v[n / 2 - 1] + v[n / 2]) }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -58,22 +54,13 @@ fn main() {
|
||||
draws = parse_drawlog(&text);
|
||||
}
|
||||
let frames = segment_frames(&draws);
|
||||
println!(
|
||||
"{} draws → {} camera-consistent blocks",
|
||||
draws.len(),
|
||||
frames.len()
|
||||
);
|
||||
println!("{} draws → {} camera-consistent blocks", draws.len(), frames.len());
|
||||
|
||||
let bytes = {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
|
||||
rt.block_on(async {
|
||||
let mut r = open_iso(Path::new(&iso)).await.unwrap();
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr"))
|
||||
.await
|
||||
.unwrap()
|
||||
r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap()
|
||||
})
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
@@ -94,12 +81,7 @@ fn main() {
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
let positions_of = |name: &str| -> Option<Vec<[f32; 3]>> {
|
||||
let m = models.iter().find(|m| m.name == name)?;
|
||||
Some(
|
||||
m.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.iter().copied())
|
||||
.collect(),
|
||||
)
|
||||
Some(m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect())
|
||||
};
|
||||
|
||||
// part -> [T per frame], and how many frames placed it at all.
|
||||
@@ -109,33 +91,20 @@ fn main() {
|
||||
for (fi, fr) in frames.iter().enumerate() {
|
||||
let mut keys: Vec<PartKey> = Vec::new();
|
||||
for part in &base_parts {
|
||||
let variants = [
|
||||
part.clone(),
|
||||
format!("{part}_m"),
|
||||
format!("{part}_l"),
|
||||
format!("{part}_d"),
|
||||
];
|
||||
let union: Vec<[f32; 3]> = variants
|
||||
.iter()
|
||||
.filter_map(|v| positions_of(v))
|
||||
.flatten()
|
||||
.collect();
|
||||
let variants =
|
||||
[part.clone(), format!("{part}_m"), format!("{part}_l"), format!("{part}_d")];
|
||||
let union: Vec<[f32; 3]> =
|
||||
variants.iter().filter_map(|v| positions_of(v)).flatten().collect();
|
||||
for cand in &variants {
|
||||
if let Some(pos) = positions_of(cand) {
|
||||
let vcount = pos.len() as u32;
|
||||
if fr.iter().any(|d| d.vcount == vcount) {
|
||||
keys.push(PartKey {
|
||||
part: part.clone(),
|
||||
vcount,
|
||||
ref_pos: union.clone(),
|
||||
});
|
||||
keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(ship) = correlate(id, fr, &keys, ref_sub) else {
|
||||
continue;
|
||||
};
|
||||
let Some(ship) = correlate(id, fr, &keys, ref_sub) else { continue };
|
||||
if ship.parts.len() < min_parts {
|
||||
continue;
|
||||
}
|
||||
@@ -145,10 +114,7 @@ fn main() {
|
||||
// Averaging them together is what makes an otherwise clean result look
|
||||
// like it disagrees by exactly the distance between the two references.
|
||||
if !ship.reference.contains(ref_sub) {
|
||||
println!(
|
||||
" block {fi:2}: skipped — reference fell back to {}",
|
||||
ship.reference
|
||||
);
|
||||
println!(" block {fi:2}: skipped — reference fell back to {}", ship.reference);
|
||||
continue;
|
||||
}
|
||||
used_frames += 1;
|
||||
@@ -207,8 +173,7 @@ fn main() {
|
||||
let spread: Vec<f32> = (0..3)
|
||||
.map(|a| {
|
||||
let v: Vec<f32> = cl.iter().map(|t| t[a]).collect();
|
||||
v.iter().cloned().fold(f32::MIN, f32::max)
|
||||
- v.iter().cloned().fold(f32::MAX, f32::min)
|
||||
v.iter().cloned().fold(f32::MIN, f32::max) - v.iter().cloned().fold(f32::MAX, f32::min)
|
||||
})
|
||||
.collect();
|
||||
let verdict = if cl.len() < 2 {
|
||||
@@ -222,21 +187,14 @@ fn main() {
|
||||
// (turret aiming, engine gimballing) does not — which is what separates
|
||||
// "the assembler has the rotation wrong" from "the part moved".
|
||||
let all_ms = rots.get(part).cloned().unwrap_or_default();
|
||||
let ms: Vec<[[f32; 3]; 3]> = cl_idx
|
||||
.iter()
|
||||
.filter_map(|&i| all_ms.get(i).copied())
|
||||
.collect();
|
||||
let ms: Vec<[[f32; 3]; 3]> =
|
||||
cl_idx.iter().filter_map(|&i| all_ms.get(i).copied()).collect();
|
||||
// Keep a rotation from INSIDE the cluster: the first sample overall can
|
||||
// belong to another instance, and diffing static against that reads as a
|
||||
// rotation error that is really an instance mix-up.
|
||||
consensus.insert(
|
||||
part.clone(),
|
||||
(
|
||||
med,
|
||||
ms.first()
|
||||
.copied()
|
||||
.unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
|
||||
),
|
||||
(med, ms.first().copied().unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])),
|
||||
);
|
||||
let rot_var = ms
|
||||
.iter()
|
||||
@@ -261,12 +219,8 @@ fn main() {
|
||||
// re-expressed in the reference part's frame before comparing — and the
|
||||
// rotation is compared too, because "wrong orientation" is half of the
|
||||
// reported viewer symptom and a translation-only check cannot see it.
|
||||
let Some(si) = args.iter().position(|a| a == "--static") else {
|
||||
return;
|
||||
};
|
||||
let Some(spath) = args.get(si + 1) else {
|
||||
return;
|
||||
};
|
||||
let Some(si) = args.iter().position(|a| a == "--static") else { return };
|
||||
let Some(spath) = args.get(si + 1) else { return };
|
||||
let sbytes = std::fs::read(spath).expect("read stage container");
|
||||
// `include_external = true` — the engine cluster, the bridge and cross-id
|
||||
// turrets live in SEPARATE composites (`e_rou_e106_eng`, 3 nodes) that the
|
||||
@@ -315,19 +269,12 @@ fn main() {
|
||||
.fold(0.0f32, f32::max);
|
||||
worst_t = worst_t.max(dtm);
|
||||
worst_r = worst_r.max(drm);
|
||||
let mark = if dtm < 1.0 && drm < 0.02 {
|
||||
"MATCH"
|
||||
} else {
|
||||
"DIFFERS"
|
||||
};
|
||||
let mark = if dtm < 1.0 && drm < 0.02 { "MATCH" } else { "DIFFERS" };
|
||||
println!(
|
||||
" {part:18} static=[{:9.1}{:9.1}{:9.1}] dT={dtm:7.2} dR={drm:6.3} {mark}",
|
||||
r[0], r[1], r[2]
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"\nworst dT={worst_t:.2} worst dR={worst_r:.3} ({} static parts, {} captured)",
|
||||
scene.len(),
|
||||
samples.len()
|
||||
);
|
||||
println!("\nworst dT={worst_t:.2} worst dR={worst_r:.3} ({} static parts, {} captured)",
|
||||
scene.len(), samples.len());
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! is well founded. This measures the slack on every block that DOES decode: if
|
||||
//! real geometry always covers its pool, under-coverage is good evidence of a
|
||||
//! wrong candidate and the gate stands.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -22,9 +22,7 @@ fn main() {
|
||||
let mut hist: BTreeMap<i64, usize> = BTreeMap::new();
|
||||
let mut worst: Vec<(i64, String)> = Vec::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
for sub in &m.meshes {
|
||||
if sub.positions.is_empty() || sub.indices.is_empty() {
|
||||
@@ -34,21 +32,14 @@ fn main() {
|
||||
let slack = sub.positions.len() as i64 - 1 - max_idx;
|
||||
*hist.entry(slack.min(20)).or_default() += 1;
|
||||
if slack > 4 {
|
||||
worst.push((
|
||||
slack,
|
||||
format!("{} in {}", m.name, f.file_name().unwrap().to_string_lossy()),
|
||||
));
|
||||
worst.push((slack, format!("{} in {}", m.name, f.file_name().unwrap().to_string_lossy())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("unreferenced tail vertices (vtx_count − 1 − max index), over decoded sub-meshes:");
|
||||
for (slack, n) in &hist {
|
||||
println!(
|
||||
" {:>3}{} : {n}",
|
||||
slack,
|
||||
if *slack == 20 { "+" } else { " " }
|
||||
);
|
||||
println!(" {:>3}{} : {n}", slack, if *slack == 20 { "+" } else { " " });
|
||||
}
|
||||
worst.sort_by_key(|(s, _)| std::cmp::Reverse(*s));
|
||||
for (s, w) in worst.iter().take(5) {
|
||||
|
||||
@@ -12,13 +12,7 @@ use sylpheed_formats::{movie_subtitle, PakArchive};
|
||||
fn movie_secs(path: &str) -> Option<f32> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
path,
|
||||
"-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path,
|
||||
])
|
||||
.output()
|
||||
.ok()?;
|
||||
@@ -33,9 +27,7 @@ fn main() {
|
||||
let mut over = 0;
|
||||
let mut worst: Vec<(String, f32, f32)> = Vec::new();
|
||||
let dir = format!("{disc}/dat/movie");
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else {
|
||||
return;
|
||||
};
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { return };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.extension().is_none_or(|x| x != "wmv") {
|
||||
@@ -47,9 +39,7 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max);
|
||||
let Some(secs) = movie_secs(&p.to_string_lossy()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(secs) = movie_secs(&p.to_string_lossy()) else { continue };
|
||||
checked += 1;
|
||||
if last > secs {
|
||||
over += 1;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
//! Does a textured element's declaration carry anything that distinguishes the
|
||||
//! two FRAME elements from every other element on the main menu?
|
||||
//!
|
||||
//! `sylpheed-port` asks for the blend/alpha mode of `ptframe1`/`ptframe2`. Prior
|
||||
//! work is on `.prm` PRIMITIVES (`ui-prm-blend-mode.md`, undecodable with reach —
|
||||
//! no field, the declaration words are constant) and on a refuted `T8aD +0x04`
|
||||
//! bit. Neither covers a `.t32` element's own declaration entry, which is 60
|
||||
//! bytes and mostly unread.
|
||||
//!
|
||||
//! This dumps every declaration entry on the menu and reports, per 4-byte word,
|
||||
//! whether the two frames share a value that no other element has. A word that
|
||||
//! separates exactly those two is a candidate; one that does not, is not.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_entry_diff
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const AT: usize = 0x20;
|
||||
const N: usize = 60;
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let names: Vec<String> = b.elements.iter().map(|e| e.name.clone()).collect();
|
||||
let frames: Vec<usize> = names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| n.starts_with("ptframe"))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
println!("{} elements; frames at indices {:?}", names.len(), frames);
|
||||
|
||||
let word = |i: usize, w: usize| -> u32 {
|
||||
let o = AT + i * N + w * 4;
|
||||
u32::from_be_bytes([by[o], by[o + 1], by[o + 2], by[o + 3]])
|
||||
};
|
||||
println!("\nper-word: does a value separate EXACTLY the two frames?");
|
||||
for w in 0..N / 4 {
|
||||
let fv: Vec<u32> = frames.iter().map(|&i| word(i, w)).collect();
|
||||
let same_in_frames = fv.windows(2).all(|p| p[0] == p[1]);
|
||||
let others: Vec<u32> = (0..names.len())
|
||||
.filter(|i| !frames.contains(i))
|
||||
.map(|i| word(i, w))
|
||||
.collect();
|
||||
let unique = same_in_frames && !others.contains(&fv[0]);
|
||||
let distinct = {
|
||||
let mut v: Vec<u32> = (0..names.len()).map(|i| word(i, w)).collect();
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
v.len()
|
||||
};
|
||||
println!(
|
||||
" +0x{:02X} frames {:?} distinct values {distinct:2}{}",
|
||||
w * 4,
|
||||
fv.iter().map(|v| format!("{v:08X}")).collect::<Vec<_>>(),
|
||||
if unique {
|
||||
" <- SEPARATES THE FRAMES"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//! Which elements carry which values at the low-cardinality declaration words?
|
||||
//!
|
||||
//! `decl_entry_diff` found nothing separating `ptframe1`/`ptframe2` except their
|
||||
//! NAME — +0x00 and +0x08 are the name string ("ptfr", ".t32"), so those two hits
|
||||
//! are a false positive of that test, not a field.
|
||||
//!
|
||||
//! The remaining candidates for a per-element mode flag are the words with few
|
||||
//! distinct values: +0x28 (3) and +0x2C (6). This prints who has what.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_flag_words
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const AT: usize = 0x20;
|
||||
const N: usize = 60;
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let w = |i: usize, off: usize| -> u32 {
|
||||
let o = AT + i * N + off;
|
||||
u32::from_be_bytes([by[o], by[o + 1], by[o + 2], by[o + 3]])
|
||||
};
|
||||
println!(
|
||||
"{:<22} {:>10} {:>10} {:>10} {:>10}",
|
||||
"element", "+0x28", "+0x2C", "+0x34", "kind"
|
||||
);
|
||||
for (i, e) in b.elements.iter().enumerate() {
|
||||
let mark = if e.name.starts_with("ptframe") {
|
||||
" <- FRAME"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
"{:<22} {:>10} {:>10} {:>10} {:>#10x}{mark}",
|
||||
e.name,
|
||||
w(i, 0x28),
|
||||
w(i, 0x2C) as i32,
|
||||
w(i, 0x34),
|
||||
e.kind
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,12 @@ use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
const OFFS: [usize; 4] = [28, 36, 44, 56];
|
||||
|
||||
fn be32(b: &[u8], o: usize) -> u32 {
|
||||
if o + 4 > b.len() {
|
||||
return 0;
|
||||
}
|
||||
if o + 4 > b.len() { return 0; }
|
||||
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args()
|
||||
.nth(1)
|
||||
.expect("usage: decl_word_probe <pak> [entry]");
|
||||
let path = std::env::args().nth(1).expect("usage: decl_word_probe <pak> [entry]");
|
||||
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
@@ -33,62 +29,34 @@ fn main() {
|
||||
let mut hit = [0usize; 4];
|
||||
let mut tot = 0usize;
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
if want.is_some_and(|w| w != i) {
|
||||
continue;
|
||||
}
|
||||
if want.is_some_and(|w| w != i) { continue; }
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Some(kids) = ratc::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
// Index space to test against: the T8aD children, in child order.
|
||||
let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect();
|
||||
if build
|
||||
.elements
|
||||
.iter()
|
||||
.all(|el| el.sprite.is_some() || el.kind & 0x10 != 0)
|
||||
{
|
||||
if build.elements.iter().all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) {
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
"== entry {i} ({} elements, {} T8aD children)",
|
||||
build.elements.len(),
|
||||
t8.len()
|
||||
);
|
||||
for (n, c) in t8.iter().enumerate() {
|
||||
println!(" child[{n:2}] {}", c.name);
|
||||
}
|
||||
println!("== entry {i} ({} elements, {} T8aD children)", build.elements.len(), t8.len());
|
||||
for (n, c) in t8.iter().enumerate() { println!(" child[{n:2}] {}", c.name); }
|
||||
for el in &build.elements {
|
||||
if el.kind & 0x10 != 0 {
|
||||
continue;
|
||||
}
|
||||
if el.kind & 0x10 != 0 { continue; }
|
||||
let d = &bytes[0x20 + el.index * 60..0x20 + (el.index + 1) * 60];
|
||||
let words: Vec<u32> = OFFS.iter().map(|&o| be32(d, o)).collect();
|
||||
// The control: for a RESOLVED element, which T8aD child is it?
|
||||
let truth = el
|
||||
.sprite
|
||||
.as_ref()
|
||||
let truth = el.sprite.as_ref()
|
||||
.and_then(|s| t8.iter().position(|c| &c.name == s));
|
||||
if let Some(t) = truth {
|
||||
tot += 1;
|
||||
for (k, w) in words.iter().enumerate() {
|
||||
if *w as usize == t {
|
||||
hit[k] += 1;
|
||||
}
|
||||
if *w as usize == t { hit[k] += 1; }
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" [{:2}] {:26} sprite={:?} child={:?} +28={} +36={} +44={} +56={}",
|
||||
el.index,
|
||||
el.name,
|
||||
el.sprite,
|
||||
truth,
|
||||
words[0] as i32,
|
||||
words[1] as i32,
|
||||
words[2] as i32,
|
||||
words[3] as i32
|
||||
el.index, el.name, el.sprite, truth,
|
||||
words[0] as i32, words[1] as i32, words[2] as i32, words[3] as i32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +1,59 @@
|
||||
use sylpheed_formats::{ratc, idxd::IdxdObject, PakArchive};
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{idxd::IdxdObject, ratc, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
// 1. RATC child-type census across the big RATC paks
|
||||
let mut childtypes: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut ratc_unparsed = 0u64;
|
||||
for pk in [
|
||||
"GP_READY_ROOM",
|
||||
"GP_MOVIE_THEATER",
|
||||
"GP_DIALOG",
|
||||
"GP_DEBRIEFING_PILOTLOG",
|
||||
"GP_TITLE",
|
||||
"GP_BUNK",
|
||||
] {
|
||||
let Ok(arc) = PakArchive::open(format!("{disc}/dat/{pk}.pak")) else {
|
||||
continue;
|
||||
};
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if !ratc::is_ratc(&b) {
|
||||
continue;
|
||||
}
|
||||
match ratc::parse(&b) {
|
||||
Some(kids) => {
|
||||
for k in kids {
|
||||
let ext = k.name.rsplit('.').next().unwrap_or("?").to_lowercase();
|
||||
*childtypes.entry(ext).or_default() += 1;
|
||||
}
|
||||
}
|
||||
None => ratc_unparsed += 1,
|
||||
let mut childtypes:BTreeMap<String,u64>=BTreeMap::new();
|
||||
let mut ratc_unparsed=0u64;
|
||||
for pk in ["GP_READY_ROOM","GP_MOVIE_THEATER","GP_DIALOG","GP_DEBRIEFING_PILOTLOG","GP_TITLE","GP_BUNK"]{
|
||||
let Ok(arc)=PakArchive::open(format!("{disc}/dat/{pk}.pak")) else{continue};
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
if !ratc::is_ratc(&b){continue;}
|
||||
match ratc::parse(&b){
|
||||
Some(kids)=> for k in kids{
|
||||
let ext=k.name.rsplit('.').next().unwrap_or("?").to_lowercase();
|
||||
*childtypes.entry(ext).or_default()+=1;
|
||||
},
|
||||
None=>ratc_unparsed+=1,
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("=== RATC child-type census (big RATC paks) ===");
|
||||
let mut cv: Vec<_> = childtypes.into_iter().collect();
|
||||
cv.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
for (e, c) in &cv {
|
||||
println!(" .{e:8} ×{c}");
|
||||
}
|
||||
let mut cv:Vec<_>=childtypes.into_iter().collect(); cv.sort_by_key(|x|std::cmp::Reverse(x.1));
|
||||
for (e,c) in &cv{ println!(" .{e:8} ×{c}"); }
|
||||
println!(" (RATC bundles that failed to parse: {ratc_unparsed})");
|
||||
|
||||
// 2. The 00000002 mystery format (GP_MAIN_GAME_E)
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if b.len() >= 4 && b[0..4] == [0, 0, 0, 2] {
|
||||
let hx: String = b[..48.min(b.len())]
|
||||
.iter()
|
||||
.map(|x| format!("{x:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let asc: String = b[..64.min(b.len())]
|
||||
.iter()
|
||||
.map(|&x| {
|
||||
if (0x20..0x7f).contains(&x) {
|
||||
x as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
"\n=== 00000002 format sample ({}B) ===\n{hx}\n{asc}",
|
||||
b.len()
|
||||
);
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
for e in arc.entries(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
if b.len()>=4 && b[0..4]==[0,0,0,2]{
|
||||
let hx:String=b[..48.min(b.len())].iter().map(|x|format!("{x:02x}")).collect::<Vec<_>>().join(" ");
|
||||
let asc:String=b[..64.min(b.len())].iter().map(|&x|if(0x20..0x7f).contains(&x){x as char}else{'.'}).collect();
|
||||
println!("\n=== 00000002 format sample ({}B) ===\n{hx}\n{asc}",b.len());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sample the top undecoded IDXD schemas: first tokens (guess semantics)
|
||||
println!("\n=== top undecoded IDXD schemas — sample tokens (semantic hints) ===");
|
||||
let targets: [u32; 6] = [
|
||||
0xb412e6d8, 0x026379ab, 0x43faa517, 0x3c5b0549, 0x6ab4825a, 0x0426e81d,
|
||||
];
|
||||
for want in targets {
|
||||
let mut shown = false;
|
||||
for e in arc.entries() {
|
||||
if shown {
|
||||
break;
|
||||
}
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if b.len() < 12 || &b[0..4] != b"IDXD" {
|
||||
continue;
|
||||
}
|
||||
let s = u32::from_be_bytes([b[8], b[9], b[10], b[11]]);
|
||||
if s != want {
|
||||
continue;
|
||||
}
|
||||
if let Ok(o) = IdxdObject::parse(&b) {
|
||||
let t = o.tokens();
|
||||
let sample: Vec<String> = t
|
||||
.iter()
|
||||
.take(14)
|
||||
.map(|s| {
|
||||
let s = s.chars().take(18).collect::<String>();
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
let targets:[u32;6]=[0xb412e6d8,0x026379ab,0x43faa517,0x3c5b0549,0x6ab4825a,0x0426e81d];
|
||||
for want in targets{
|
||||
let mut shown=false;
|
||||
for e in arc.entries(){
|
||||
if shown{break;}
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
if b.len()<12 || &b[0..4]!=b"IDXD"{continue;}
|
||||
let s=u32::from_be_bytes([b[8],b[9],b[10],b[11]]);
|
||||
if s!=want{continue;}
|
||||
if let Ok(o)=IdxdObject::parse(&b){
|
||||
let t=o.tokens();
|
||||
let sample:Vec<String>=t.iter().take(14).map(|s|{let s=s.chars().take(18).collect::<String>();s}).collect();
|
||||
println!(" {want:08x}: {sample:?}");
|
||||
shown = true;
|
||||
shown=true;
|
||||
}
|
||||
}
|
||||
if !shown {
|
||||
println!(" {want:08x}: (parse failed / not found)");
|
||||
}
|
||||
if !shown{ println!(" {want:08x}: (parse failed / not found)"); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,8 @@ fn is_number(s: &str) -> bool {
|
||||
}
|
||||
fn is_key(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
&& s.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
&& s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
&& !is_number(s)
|
||||
}
|
||||
fn is_value(s: &str) -> bool {
|
||||
@@ -25,20 +22,14 @@ fn is_value(s: &str) -> bool {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// No hardcoded fallback: it made SYLPHEED_DISC look like a control while
|
||||
// one machine's directory layout decided the outcome (#16).
|
||||
let Ok(root) = std::env::var("SYLPHEED_DISC") else {
|
||||
eprintln!("set SYLPHEED_DISC to the extracted disc root");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let root = std::env::var("SYLPHEED_DISC")
|
||||
.unwrap_or_else(|_| "/home/fabi/RE - Project Sylpheed/sylph_extract".into());
|
||||
let wanted: Vec<String> = std::env::args().skip(1).collect();
|
||||
let arc = PakArchive::open(std::path::Path::new(&root).join("dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
|
||||
for e in arc.entries() {
|
||||
let Ok(bytes) = arc.read(e) else { continue };
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
|
||||
let toks = obj.tokens().to_vec();
|
||||
let mut hits: Vec<String> = vec![];
|
||||
for (i, t) in toks.iter().enumerate() {
|
||||
@@ -53,12 +44,7 @@ fn main() {
|
||||
});
|
||||
}
|
||||
if !hits.is_empty() {
|
||||
println!(
|
||||
"0x{:08x} {:<44} {}",
|
||||
obj.schema_hash,
|
||||
obj.identity(),
|
||||
hits.join(" ")
|
||||
);
|
||||
println!("0x{:08x} {:<44} {}", obj.schema_hash, obj.identity(), hits.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ fn is_key(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
&& s.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
&& s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
|
||||
&& !is_number(s)
|
||||
}
|
||||
|
||||
@@ -31,14 +29,9 @@ fn is_value(s: &str) -> bool {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// argv[1], else SYLPHEED_DISC. No hardcoded fallback -- see #16.
|
||||
let root = std::env::args()
|
||||
.nth(1)
|
||||
.or_else(|| std::env::var("SYLPHEED_DISC").ok())
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("usage: defaulted_fields <disc-root> [paks...] (or set SYLPHEED_DISC)");
|
||||
std::process::exit(2);
|
||||
});
|
||||
.unwrap_or_else(|| "/home/fabi/RE - Project Sylpheed/sylph_extract".into());
|
||||
let paks: Vec<String> = std::env::args().skip(2).collect();
|
||||
let paks = if paks.is_empty() {
|
||||
vec![
|
||||
@@ -78,11 +71,7 @@ fn main() {
|
||||
}
|
||||
let prev = if i == 0 { None } else { Some(&toks[i - 1]) };
|
||||
let valued = prev.map(|p| is_value(p)).unwrap_or(false);
|
||||
let v = if valued {
|
||||
Some(toks[i - 1].clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let v = if valued { Some(toks[i - 1].clone()) } else { None };
|
||||
seen_here.entry(t.clone()).or_insert(v);
|
||||
}
|
||||
for (k, v) in seen_here {
|
||||
@@ -118,14 +107,17 @@ fn main() {
|
||||
0xb412_e6d8 => "MESSAGE",
|
||||
_ => "?",
|
||||
};
|
||||
let defaulted: Vec<_> = keys.iter().filter(|(_, s)| s.1 > 0).collect();
|
||||
let defaulted: Vec<_> = keys
|
||||
.iter()
|
||||
.filter(|(_, s)| s.1 > 0)
|
||||
.collect();
|
||||
if defaulted.is_empty() {
|
||||
continue;
|
||||
}
|
||||
println!("\n--- schema 0x{schema:08x} {name} ({n_obj} objects) ---");
|
||||
println!(
|
||||
"{:<30} {:>5} {:>5} values seen (≤12) | owners defaulting",
|
||||
"KEY", "set", "dflt"
|
||||
"{:<30} {:>5} {:>5} {}",
|
||||
"KEY", "set", "dflt", "values seen (≤12) | owners defaulting"
|
||||
);
|
||||
for (k, (n_set, n_def, vals, owners)) in defaulted {
|
||||
let vv: Vec<&str> = vals.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
@@ -49,16 +49,12 @@ fn main() {
|
||||
if r + 12 > desc.len() {
|
||||
break;
|
||||
}
|
||||
let (o, code, usage) =
|
||||
(be32(desc, r), be32(desc, r + 4), be32(desc, r + 8) >> 16);
|
||||
let (o, code, usage) = (be32(desc, r), be32(desc, r + 4), be32(desc, r + 8) >> 16);
|
||||
if o == 0x00FF_0000 || code == 0xFFFF_FFFF || o > 0x1000 {
|
||||
println!(" end marker @0x{r:X}: off=0x{o:X} code=0x{code:X}");
|
||||
break;
|
||||
}
|
||||
println!(
|
||||
" off {o:>3} code 0x{:06X} usage {usage}",
|
||||
code & 0xFF_FFFF
|
||||
);
|
||||
println!(" off {o:>3} code 0x{:06X} usage {usage}", code & 0xFF_FFFF);
|
||||
stride = stride.max(o + 4);
|
||||
r += 12;
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
//! How often is a screen's design size READ, and how often is it FABRICATED?
|
||||
//!
|
||||
//! `ui_layout.rs` scans the `.rat` records for a `(w,h)` at `+0x18`/`+0x1c` and,
|
||||
//! finding none, falls back to `(DESIGN_W, DESIGN_H)` = 1280x720. Its own comment
|
||||
//! says "every screen seen is 1280x720, **which is also the fallback**" -- which
|
||||
//! is precisely the problem: the fabricated value equals the expected one, so no
|
||||
//! output of the parser can distinguish a read design size from an invented one.
|
||||
//! The port sizes its screens off this number.
|
||||
//!
|
||||
//! This replicates the scan through the public RATC API and counts.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example design_size_fallback
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
|
||||
|
||||
fn be32(b: &[u8], o: usize) -> u32 {
|
||||
if o + 4 > b.len() {
|
||||
return 0;
|
||||
}
|
||||
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut read, mut fell_back, mut nonstd) = (0usize, 0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
let (mut r, mut f) = (0usize, 0usize);
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ui_layout::is_build(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(kids) = ratc::parse(&by) else {
|
||||
continue;
|
||||
};
|
||||
// the same predicate ui_layout uses, over the same records
|
||||
// ⚠️ A first version took EVERY RATC child and failed its control:
|
||||
// it reported all 965 builds stating a non-1280x720 size, where
|
||||
// `screen list` prints 1280x720 for every one. `records` in
|
||||
// ui_layout is the `.rat` children only; a T8aD sprite header read
|
||||
// at +0x18 is garbage that passes the range test.
|
||||
let found = kids
|
||||
.iter()
|
||||
.filter(|k| k.kind == "RATC" || k.name.ends_with(".rat"))
|
||||
.find_map(|k| {
|
||||
let rec = &by[k.offset..(k.offset + k.size).min(by.len())];
|
||||
let (w, h) = (be32(rec, 0x18), be32(rec, 0x1c));
|
||||
(w > 0 && h > 0 && w <= 8192 && h <= 8192).then_some((w, h))
|
||||
});
|
||||
match found {
|
||||
Some((w, h)) => {
|
||||
r += 1;
|
||||
if (w, h) != (1280, 720) {
|
||||
nonstd += 1;
|
||||
println!(" {name} : a build states a NON-standard design size {w}x{h}");
|
||||
}
|
||||
}
|
||||
None => f += 1,
|
||||
}
|
||||
}
|
||||
if r + f > 0 {
|
||||
println!("{name:30} {r:5} read {f:5} FABRICATED");
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
read += r;
|
||||
fell_back += f;
|
||||
}
|
||||
println!("\n{read} builds state a design size, {fell_back} get the 1280x720 FALLBACK");
|
||||
println!("{nonstd} builds state something other than 1280x720");
|
||||
println!("--- END ---");
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
//! Is `GP_DIALOG` entry 2 the `DLG_SELECT_DIFFICULTY` screen?
|
||||
//!
|
||||
//! The image lists `DLG_SELECT_DIFFICULTY` among the `DLG_*` names at
|
||||
//! `0x820A41BB`, so DIFFICULTY is a DIALOG, not a GamePart screen with its own
|
||||
//! pak — which is why a search for an 8-record `btn` build in a difficulty-named
|
||||
//! archive found nothing. `GP_DIALOG` entry 2 carries `pcbtn00`..`03`: four
|
||||
//! buttons, matching EASY / NORMAL / HARD / BACK.
|
||||
//!
|
||||
//! CONTROL: `GP_TITLE` entry 5's five buttons must come back at the rows the disc
|
||||
//! is independently known to place them (162/242/322/401/482, spacing 80).
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_button_rows
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn rows(ar: &PakArchive, entry: usize, what: &str) {
|
||||
let Ok(by) = ar.read(&ar.entries()[entry]) else {
|
||||
return;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
return;
|
||||
};
|
||||
let mut v: Vec<(i32, String)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
let n = &e.name;
|
||||
(n.starts_with("pcbtn") || n.starts_with("ptbtn")) && !n.contains('f')
|
||||
})
|
||||
.map(|e| {
|
||||
(
|
||||
e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32),
|
||||
e.name.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
v.sort_by_key(|r| r.0);
|
||||
println!("\n{what} (entry {entry}):");
|
||||
for (y, n) in &v {
|
||||
println!(" y {y:5} {n}");
|
||||
}
|
||||
if v.len() > 1 {
|
||||
let sp: Vec<i32> = v.windows(2).map(|w| w[1].0 - w[0].0).collect();
|
||||
println!(" spacing {sp:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let t = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
rows(
|
||||
&t,
|
||||
5,
|
||||
"CONTROL: GP_TITLE main menu (must be 162/242/322/401/482)",
|
||||
);
|
||||
let d = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
rows(&d, 2, "GP_DIALOG candidate for DLG_SELECT_DIFFICULTY");
|
||||
rows(&d, 3, "GP_DIALOG entry 3 (the pair)");
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
//! Are the 37 equal-button-count `GP_DIALOG` pairs language pairs, or not?
|
||||
//!
|
||||
//! 26 of 65 adjacent pairs differ in BUTTON COUNT — two languages cannot, so those
|
||||
//! are unrelated dialogs. For the rest my language reading was left UNSUPPORTED
|
||||
//! rather than refuted, and both agents observed that nothing rewards closing it.
|
||||
//!
|
||||
//! A language pair must share its BUTTON NAMES and ROWS exactly (a locale changes
|
||||
//! glyphs, not layout) and differ only elsewhere. An unrelated pair will differ in
|
||||
//! button names or rows too.
|
||||
//!
|
||||
//! CONTROL: entries 2/3 (identical element sets) must come out as "buttons match".
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_37
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn sig(ar: &PakArchive, i: usize) -> Option<(Vec<(String, i32)>, usize)> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
let mut v: Vec<(String, i32)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| e.name.contains("btn"))
|
||||
.map(|e| {
|
||||
(
|
||||
e.name.clone(),
|
||||
e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
v.sort();
|
||||
let n = v.len();
|
||||
Some((v, n))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let (mut match_btn, mut differ_btn, mut ctrl) = (0, 0, false);
|
||||
let mut examples = 0;
|
||||
for k in (0..n).step_by(2) {
|
||||
let (Some((a, na)), Some((b, nb))) = (sig(&ar, k), sig(&ar, k + 1)) else {
|
||||
continue;
|
||||
};
|
||||
if na != nb {
|
||||
continue; // the 26 already settled
|
||||
}
|
||||
if a == b {
|
||||
match_btn += 1;
|
||||
if k == 2 {
|
||||
ctrl = true
|
||||
}
|
||||
} else {
|
||||
differ_btn += 1;
|
||||
if examples < 5 {
|
||||
println!(" entries {k:3}/{:<3} buttons DIFFER", k + 1);
|
||||
println!(" {:?}", a.iter().map(|x| &x.0).collect::<Vec<_>>());
|
||||
println!(" {:?}", b.iter().map(|x| &x.0).collect::<Vec<_>>());
|
||||
examples += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\nequal-button-count pairs whose button NAMES+ROWS match : {match_btn}");
|
||||
println!("equal-button-count pairs whose buttons DIFFER : {differ_btn}");
|
||||
println!(
|
||||
"control (entries 2/3 counted as matching): {}",
|
||||
if ctrl { "PASSED" } else { "FAILED" }
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//! Do adjacent `GP_DIALOG` entries differ in BUTTON COUNT?
|
||||
//!
|
||||
//! I offered an untested reading for why 63 of 65 adjacent pairs have different
|
||||
//! element sets: dialog text baked into language-specific sprites, so EN/JP
|
||||
//! entries differ by construction. `sylpheed-port` refuted it with a count — two
|
||||
//! languages of one dialog cannot differ in how many buttons they have. This
|
||||
//! re-derives that with my own reader before I record the refutation.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_button_counts
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn btns(ar: &PakArchive, i: usize) -> Option<usize> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().filter(|e| e.name.contains("btn")).count())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let (mut diff, mut same, mut skip) = (0, 0, 0);
|
||||
let mut show = 0;
|
||||
for k in (0..n).step_by(2) {
|
||||
match (btns(&ar, k), btns(&ar, k + 1)) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a != b {
|
||||
diff += 1;
|
||||
if show < 6 {
|
||||
println!(" entries {k:3}/{:<3} button counts {a} vs {b}", k + 1);
|
||||
show += 1;
|
||||
}
|
||||
} else {
|
||||
same += 1
|
||||
}
|
||||
}
|
||||
_ => skip += 1,
|
||||
}
|
||||
}
|
||||
println!("\nadjacent pairs differing in BUTTON COUNT: {diff}");
|
||||
println!("adjacent pairs with equal button counts : {same}");
|
||||
println!("unreadable : {skip}");
|
||||
println!("\ntwo languages of one dialog cannot differ in button count.");
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//! What differs between equal-button-count `GP_DIALOG` adjacent pairs?
|
||||
//!
|
||||
//! All 39 share button names and rows. That is consistent with a LANGUAGE PAIR and
|
||||
//! equally with TWO DIALOGS SHARING A BUTTON TEMPLATE (two yes/no boxes differing
|
||||
//! only in their message sprite). The difference is in what else differs: a
|
||||
//! language pair should differ in the SAME slots with locale-marked names.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_diffs
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn names(ar: &PakArchive, i: usize) -> Option<BTreeSet<String>> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|e| e.name.clone()).collect())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let mut shown = 0;
|
||||
for k in (0..n).step_by(2) {
|
||||
let (Some(a), Some(b)) = (names(&ar, k), names(&ar, k + 1)) else {
|
||||
continue;
|
||||
};
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
let oa: Vec<&String> = a.difference(&b).collect();
|
||||
let ob: Vec<&String> = b.difference(&a).collect();
|
||||
// only the equal-button-count ones
|
||||
let ba = a.iter().filter(|x| x.contains("btn")).count();
|
||||
let bb = b.iter().filter(|x| x.contains("btn")).count();
|
||||
if ba != bb {
|
||||
continue;
|
||||
}
|
||||
if shown < 6 {
|
||||
println!(
|
||||
"entries {k:3}/{:<3} shared {} only-in-{k}: {:?} only-in-{}: {:?}",
|
||||
k + 1,
|
||||
a.intersection(&b).count(),
|
||||
oa,
|
||||
k + 1,
|
||||
ob
|
||||
);
|
||||
shown += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//! Are `GP_DIALOG` entries 0/1 and 2/3 a LANGUAGE PAIR or a DUPLICATE?
|
||||
//!
|
||||
//! They are the only two adjacent pairs in that archive with identical element
|
||||
//! sets; every other adjacent pair is two unrelated dialogs. Left open as
|
||||
//! "untested" — identical element names are equally consistent with a language
|
||||
//! pair (same layout, different glyphs baked into the textures) and with a
|
||||
//! byte-for-byte duplicate.
|
||||
//!
|
||||
//! The bytes decide it: identical entries are a duplicate; entries that share
|
||||
//! every element name but differ in payload are a language pair.
|
||||
//!
|
||||
//! CONTROL: entries 10/11, known to be two DIFFERENT dialogs (stage 10 vs stage
|
||||
//! 02), must come out as differing — and by a lot. A comparator that cannot
|
||||
//! separate two unrelated dialogs cannot judge two similar ones.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_identity
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::pak::PakArchive;
|
||||
|
||||
fn cmp(ar: &PakArchive, a: usize, b: usize, what: &str) {
|
||||
let (Ok(x), Ok(y)) = (ar.read(&ar.entries()[a]), ar.read(&ar.entries()[b])) else {
|
||||
println!("{what}: unreadable");
|
||||
return;
|
||||
};
|
||||
let same_len = x.len() == y.len();
|
||||
let n = x.len().min(y.len());
|
||||
let diff = (0..n).filter(|&i| x[i] != y[i]).count();
|
||||
let first = (0..n).find(|&i| x[i] != y[i]);
|
||||
println!("{what}");
|
||||
println!(
|
||||
" sizes {} / {} ({})",
|
||||
x.len(),
|
||||
y.len(),
|
||||
if same_len { "equal" } else { "DIFFER" }
|
||||
);
|
||||
println!(
|
||||
" differing bytes over the common prefix: {diff} / {n} ({:.2}%)",
|
||||
100.0 * diff as f64 / n as f64
|
||||
);
|
||||
match first {
|
||||
None if same_len => println!(" => BYTE-IDENTICAL — a duplicate"),
|
||||
None => println!(" => one is a prefix of the other"),
|
||||
Some(o) => println!(" => first difference at offset 0x{o:X}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
cmp(
|
||||
&ar,
|
||||
10,
|
||||
11,
|
||||
"CONTROL: entries 10/11 — known two different dialogs",
|
||||
);
|
||||
cmp(&ar, 0, 1, "entries 0/1");
|
||||
cmp(&ar, 2, 3, "entries 2/3 — the DIFFICULTY build");
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//! Are `GP_DIALOG`'s 140 entries adjacent EN/JP pairs, one per dialog record?
|
||||
//!
|
||||
//! The dialog table has 70 records and the archive has 140 entries. If the
|
||||
//! pairing is adjacent — (0,1), (2,3), … — then dialog index = entry / 2, and the
|
||||
//! unbound id-to-entry join becomes an ordering question rather than a search.
|
||||
//!
|
||||
//! Test: for each pair, compare the SET of element names. GP_TITLE's EN/JP pairs
|
||||
//! share their sprite sets exactly except for the title art (4/7), so identical
|
||||
//! sets are the signature of a language pair.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pairing
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn names(ar: &PakArchive, i: usize) -> Option<BTreeSet<String>> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|e| e.name.clone()).collect())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let (mut adj_same, mut adj_diff, mut skipped) = (0, 0, 0);
|
||||
for k in (0..n).step_by(2) {
|
||||
match (names(&ar, k), names(&ar, k + 1)) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a == b {
|
||||
adj_same += 1;
|
||||
println!(" identical pair: entries {k}/{}", k + 1)
|
||||
} else {
|
||||
adj_diff += 1
|
||||
}
|
||||
}
|
||||
_ => skipped += 1,
|
||||
}
|
||||
}
|
||||
println!("ADJACENT pairing (2k, 2k+1): identical {adj_same} differing {adj_diff} unreadable {skipped}");
|
||||
// rival hypothesis: halves, (i, i+70)
|
||||
let (mut h_same, mut h_diff, mut h_skip) = (0, 0, 0);
|
||||
for k in 0..n / 2 {
|
||||
match (names(&ar, k), names(&ar, k + n / 2)) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a == b {
|
||||
h_same += 1
|
||||
} else {
|
||||
h_diff += 1
|
||||
}
|
||||
}
|
||||
_ => h_skip += 1,
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"HALVES pairing (i, i+70): identical {h_same} differing {h_diff} unreadable {h_skip}"
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//! How is `GP_DIALOG.pak` organised? Testing whether dialog id maps positionally.
|
||||
//!
|
||||
//! The dialog table gives name -> id (70 records, `DLG_SELECT_DIFFICULTY` = 2000)
|
||||
//! and the disc gives a unique four-button build at GP_DIALOG entries 2/3. Nothing
|
||||
//! joins them. If the archive were laid out in table order, or in id order, the
|
||||
//! join would be positional — this checks.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pak_shape
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let mut builds = 0;
|
||||
let mut with_btn = 0;
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if let Some(b) = ui_layout::parse_build(&by) {
|
||||
builds += 1;
|
||||
if b.elements.iter().any(|el| el.name.contains("btn")) {
|
||||
with_btn += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("entries {n}, parse as builds {builds}, of those with a btn element {with_btn}");
|
||||
println!("dialog table has 70 records; 70 x 2 (EN/JP) = 140");
|
||||
}
|
||||
@@ -1,30 +1,13 @@
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text = TextIndex::build(&pak);
|
||||
let msgs = game_data::load_demo_messages(&pak);
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text=TextIndex::build(&pak);
|
||||
let msgs=game_data::load_demo_messages(&pak);
|
||||
println!("{} dialogue lines total\n", msgs.len());
|
||||
for m in msgs
|
||||
.iter()
|
||||
.filter(|m| m.character.is_some() && !m.page_keys.is_empty())
|
||||
.take(8)
|
||||
{
|
||||
let who = m
|
||||
.character
|
||||
.as_deref()
|
||||
.unwrap_or("?")
|
||||
.trim_start_matches("Character");
|
||||
let line: String = m
|
||||
.page_keys
|
||||
.iter()
|
||||
.filter_map(|k| text.get(k))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
println!(
|
||||
" {who:10} [{}] “{}”",
|
||||
m.voice_clip.as_deref().unwrap_or("-"),
|
||||
line.chars().take(64).collect::<String>()
|
||||
);
|
||||
for m in msgs.iter().filter(|m|m.character.is_some()&&!m.page_keys.is_empty()).take(8){
|
||||
let who=m.character.as_deref().unwrap_or("?").trim_start_matches("Character");
|
||||
let line:String=m.page_keys.iter().filter_map(|k|text.get(k)).collect::<Vec<_>>().join(" ");
|
||||
println!(" {who:10} [{}] “{}”", m.voice_clip.as_deref().unwrap_or("-"), line.chars().take(64).collect::<String>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,29 @@
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let tables: [(u32, &str); 4] = [
|
||||
(0x0426e81d, "Player / physics+scoring"),
|
||||
(0x6ab4825a, "Weapon"),
|
||||
(0x43faa517, "Unit / craft"),
|
||||
(0x3c5b0549, "Vessel / capital ship"),
|
||||
];
|
||||
for (want, label) in tables {
|
||||
use std::collections::BTreeMap;
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let tables:[(u32,&str);4]=[(0x0426e81d,"Player / physics+scoring"),(0x6ab4825a,"Weapon"),(0x43faa517,"Unit / craft"),(0x3c5b0549,"Vessel / capital ship")];
|
||||
for (want,label) in tables{
|
||||
// collect all records of this schema
|
||||
let mut recs: Vec<IdxdObject> = vec![];
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
if o.schema_hash == want {
|
||||
recs.push(o);
|
||||
}
|
||||
}
|
||||
let mut recs:Vec<IdxdObject>=vec![];
|
||||
for e in arc.entries(){ let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; if o.schema_hash==want{recs.push(o);} }
|
||||
// field-union (explicit-valued keys), with occurrence count
|
||||
let mut cols: BTreeMap<String, u32> = BTreeMap::new();
|
||||
for o in &recs {
|
||||
for (k, _) in o.resolved_fields() {
|
||||
*cols.entry(k.into()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let mut cols:BTreeMap<String,u32>=BTreeMap::new();
|
||||
for o in &recs{ for (k,_) in o.resolved_fields(){ *cols.entry(k.into()).or_default()+=1; } }
|
||||
println!("\n╔══ {label} [{want:08x}] {} records ══", recs.len());
|
||||
let mut cv: Vec<_> = cols.into_iter().collect();
|
||||
cv.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
let colstr: String = cv
|
||||
.iter()
|
||||
.take(28)
|
||||
.map(|(k, c)| format!("{k}({c})"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut cv:Vec<_>=cols.into_iter().collect(); cv.sort_by_key(|x|std::cmp::Reverse(x.1));
|
||||
let colstr:String=cv.iter().take(28).map(|(k,c)|format!("{k}({c})")).collect::<Vec<_>>().join(" ");
|
||||
println!("║ numeric/enum fields: {colstr}");
|
||||
// dump 2 sample records: identity + explicit fields
|
||||
for o in recs.iter().take(2) {
|
||||
let id = o.get_raw("ID").unwrap_or("?");
|
||||
let name = o.get_raw("Name").unwrap_or("");
|
||||
for o in recs.iter().take(2){
|
||||
let id=o.get_raw("ID").unwrap_or("?");
|
||||
let name=o.get_raw("Name").unwrap_or("");
|
||||
print!("║ • {id}");
|
||||
if !name.is_empty() {
|
||||
print!(" «{name}»");
|
||||
}
|
||||
if !name.is_empty(){print!(" «{name}»");}
|
||||
println!();
|
||||
let fields: Vec<String> = o
|
||||
.resolved_fields()
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}={v}"))
|
||||
.collect();
|
||||
for chunk in fields.chunks(5) {
|
||||
println!("║ {}", chunk.join(" "));
|
||||
}
|
||||
let fields:Vec<String>=o.resolved_fields().iter().map(|(k,v)|format!("{k}={v}")).collect();
|
||||
for chunk in fields.chunks(5){ println!("║ {}", chunk.join(" ")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{game_data, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let chars = game_data::load_characters(&pak);
|
||||
let mut byfac: BTreeMap<String, Vec<String>> = Default::default();
|
||||
for c in &chars {
|
||||
byfac
|
||||
.entry(c.faction.clone().unwrap_or("·".into()))
|
||||
.or_default()
|
||||
.push(format!(
|
||||
"{}({})",
|
||||
c.id.clone()
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches("Character"),
|
||||
c.faces.len()
|
||||
));
|
||||
}
|
||||
println!(
|
||||
"{} characters across {} factions:",
|
||||
chars.len(),
|
||||
byfac.len()
|
||||
);
|
||||
for (f, mut v) in byfac {
|
||||
v.sort();
|
||||
println!(" [{f}] {}", v.join(" "));
|
||||
}
|
||||
use std::collections::BTreeMap;
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let chars=game_data::load_characters(&pak);
|
||||
let mut byfac:BTreeMap<String,Vec<String>>=Default::default();
|
||||
for c in &chars{ byfac.entry(c.faction.clone().unwrap_or("·".into())).or_default().push(format!("{}({})",c.id.clone().unwrap_or_default().trim_start_matches("Character"),c.faces.len())); }
|
||||
println!("{} characters across {} factions:",chars.len(),byfac.len());
|
||||
for (f,mut v) in byfac{ v.sort(); println!(" [{f}] {}", v.join(" ")); }
|
||||
}
|
||||
|
||||
@@ -1,83 +1,14 @@
|
||||
use sylpheed_formats::{idxd::IdxdObject, PakArchive};
|
||||
fn short(s: &str) -> String {
|
||||
s.trim_start_matches("Weapon_")
|
||||
.trim_start_matches("UN_")
|
||||
.trim_start_matches("UnitName_UN_")
|
||||
.into()
|
||||
}
|
||||
fn g(o: &IdxdObject, k: &str) -> String {
|
||||
o.get_f32(k)
|
||||
.map(|v| {
|
||||
if v == v.trunc() {
|
||||
format!("{}", v as i64)
|
||||
} else {
|
||||
format!("{v}")
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
o.get_raw(k)
|
||||
.filter(|x| {
|
||||
x.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.unwrap_or("·".into())
|
||||
}
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let recs = |want: u32| -> Vec<IdxdObject> {
|
||||
arc.entries()
|
||||
.iter()
|
||||
.filter_map(|e| arc.read(e).ok())
|
||||
.filter_map(|b| IdxdObject::parse(&b).ok())
|
||||
.filter(|o| o.schema_hash == want)
|
||||
.collect()
|
||||
};
|
||||
fn short(s:&str)->String{ s.trim_start_matches("Weapon_").trim_start_matches("UN_").trim_start_matches("UnitName_UN_").into() }
|
||||
fn g<'a>(o:&'a IdxdObject,k:&str)->String{ o.get_f32(k).map(|v|{if v==v.trunc(){format!("{}",v as i64)}else{format!("{v}")}}).or_else(||o.get_raw(k).filter(|x|x.chars().next().map(|c|c.is_ascii_digit()).unwrap_or(false)).map(|s|s.to_string())).unwrap_or("·".into()) }
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let recs=|want:u32|->Vec<IdxdObject>{ arc.entries().iter().filter_map(|e|arc.read(e).ok()).filter_map(|b|IdxdObject::parse(&b).ok()).filter(|o|o.schema_hash==want).collect() };
|
||||
println!("### WEAPONS (name | target | load | int | power | vel | range | trig)");
|
||||
for o in recs(0x6ab4825a).iter().take(16) {
|
||||
println!(
|
||||
"{:<34}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
short(o.get_raw("ID").unwrap_or("?")),
|
||||
o.get_raw("TargetType").unwrap_or("·"),
|
||||
g(o, "LoadingCount"),
|
||||
g(o, "Interval"),
|
||||
g(o, "Power"),
|
||||
g(o, "Velocity"),
|
||||
g(o, "MaximumRange"),
|
||||
g(o, "TriggerShotCount")
|
||||
);
|
||||
}
|
||||
for o in recs(0x6ab4825a).iter().take(16){ println!("{:<34}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), o.get_raw("TargetType").unwrap_or("·"), g(o,"LoadingCount"),g(o,"Interval"),g(o,"Power"),g(o,"Velocity"),g(o,"MaximumRange"),g(o,"TriggerShotCount")); }
|
||||
println!("\n### UNITS/CRAFT (name | HP | cruise | accel | radar | turrets | score)");
|
||||
for o in recs(0x43faa517).iter().take(16) {
|
||||
println!(
|
||||
"{:<38}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
short(o.get_raw("ID").unwrap_or("?")),
|
||||
g(o, "HP"),
|
||||
g(o, "CruisingVelocity"),
|
||||
g(o, "Acceleration"),
|
||||
g(o, "RadarRange"),
|
||||
g(o, "TurretCount"),
|
||||
g(o, "ScorePoint")
|
||||
);
|
||||
}
|
||||
for o in recs(0x43faa517).iter().take(16){ println!("{:<38}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), g(o,"HP"),g(o,"CruisingVelocity"),g(o,"Acceleration"),g(o,"RadarRange"),g(o,"TurretCount"),g(o,"ScorePoint")); }
|
||||
println!("\n### VESSELS/CAPITAL SHIPS (name | HP | Sz_X | Sz_Z | radar | turrets | bridges | hatches | shieldgen | thrusters)");
|
||||
for o in recs(0x3c5b0549).iter() {
|
||||
println!(
|
||||
"{:<30}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
short(o.get_raw("ID").unwrap_or("?")),
|
||||
g(o, "HP"),
|
||||
g(o, "Size_X"),
|
||||
g(o, "Size_Z"),
|
||||
g(o, "RadarRange"),
|
||||
g(o, "TurretCount"),
|
||||
g(o, "BridgeCount"),
|
||||
g(o, "HatchCount"),
|
||||
g(o, "ShieldGeneratorCount"),
|
||||
g(o, "ThrusterCount")
|
||||
);
|
||||
}
|
||||
for o in recs(0x3c5b0549).iter(){ println!("{:<30}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), g(o,"HP"),g(o,"Size_X"),g(o,"Size_Z"),g(o,"RadarRange"),g(o,"TurretCount"),g(o,"BridgeCount"),g(o,"HatchCount"),g(o,"ShieldGeneratorCount"),g(o,"ThrusterCount")); }
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use sylpheed_formats::{localization::TextIndex, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text = TextIndex::build(&pak);
|
||||
for n in 1..=16u32 {
|
||||
let sid = format!("S{n:02}");
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, PakArchive};
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let text=TextIndex::build(&pak);
|
||||
for n in 1..=16u32{
|
||||
let sid=format!("S{n:02}");
|
||||
// primary objective across phases (first that resolves)
|
||||
let obj: Vec<String> = (1..=3)
|
||||
.flat_map(|p| text.objectives(&sid, p))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let lose: Vec<String> = (1..=3)
|
||||
.flat_map(|p| text.lose_conditions(&sid, p))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let full_obj = obj.join(" ");
|
||||
let full_lose = lose.into_iter().take(2).collect::<Vec<_>>().join(" ");
|
||||
let obj:Vec<String>=(1..=3).flat_map(|p|text.objectives(&sid,p)).map(|s|s.to_string()).collect();
|
||||
let lose:Vec<String>=(1..=3).flat_map(|p|text.lose_conditions(&sid,p)).map(|s|s.to_string()).collect();
|
||||
let full_obj=obj.join(" ");
|
||||
let full_lose=lose.into_iter().take(2).collect::<Vec<_>>().join(" ");
|
||||
println!("{sid}\t{full_obj}\t{full_lose}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! `XBG7_EDGE_CAP` sets the cap; this reports, for one setting, how much
|
||||
//! geometry decodes and how self-consistent it is across containers — the two
|
||||
//! numbers any change to the cap has to trade off. Run it once per cap value.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
@@ -20,9 +20,7 @@ fn main() {
|
||||
let mut seen: BTreeMap<String, Vec<([i64; 3], usize, usize)>> = BTreeMap::new();
|
||||
let (mut models, mut verts) = (0usize, 0usize);
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for s in &m.meshes {
|
||||
@@ -66,7 +64,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
let (mut shared, mut inconsistent) = (0usize, 0usize);
|
||||
for list in seen.values() {
|
||||
for (_, list) in &seen {
|
||||
if list.len() < 2 || !list.iter().all(|e| e.1 == list[0].1 && e.2 == list[0].2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
//! Every record reachable from an element — leaf **and** `focus_link` — plus a
|
||||
//! disc-wide census of how many elements have more than one.
|
||||
//!
|
||||
//! ⚠️ WHY. I claimed `ptbtn00f`'s peak alpha of 80 was undeclared, having read
|
||||
//! `ptbtn00.rat` (the leaf, flat 255) and stopped. The pulse is in
|
||||
//! `ptbtn00f.rat`, the focus record. `focus_link` was already parsed and
|
||||
//! `ui_layout.rs` already documented it: the format was known and I did not
|
||||
//! consult it. An absence claim is only as good as its enumeration, so this
|
||||
//! enumerates rather than asking the reader to remember.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example element_records -- GP_TITLE ptbtn00
|
||||
//! cargo run -p sylpheed-formats --example element_records -- --census
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
if argv.iter().any(|a| a == "--census") {
|
||||
let (mut els, mut linked, mut screens_with) = (0usize, 0usize, 0usize);
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for ent in ar.entries() {
|
||||
let Ok(by) = ar.read(ent) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let n = b.elements.iter().filter(|e| e.focus_link.is_some()).count();
|
||||
els += b.elements.len();
|
||||
linked += n;
|
||||
if n > 0 {
|
||||
screens_with += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("elements disc-wide : {els}");
|
||||
println!(
|
||||
"with a focus_link record : {linked} ({:.1}%)",
|
||||
100.0 * linked as f64 / els as f64
|
||||
);
|
||||
println!("builds containing at least 1: {screens_with}");
|
||||
println!("\nEach of those carries a SECOND record whose keyframes are invisible");
|
||||
println!("to anyone who looks up the leaf by name and stops.");
|
||||
return;
|
||||
}
|
||||
|
||||
let pak = argv.first().cloned().unwrap_or_else(|| "GP_TITLE".into());
|
||||
let want = argv.get(1).cloned();
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
for (i, ent) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(ent) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if let Some(w) = &want {
|
||||
if !el.name.starts_with(w.as_str()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if el.focus_link.is_none() && want.is_none() {
|
||||
continue;
|
||||
}
|
||||
println!("entry {i}: {}", el.name);
|
||||
let stem = el.name.trim_end_matches(".rat");
|
||||
for (tag, rec) in [
|
||||
("leaf", format!("{stem}.rat")),
|
||||
("focus", el.focus_link.clone().unwrap_or_default()),
|
||||
] {
|
||||
if rec.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match b.records.get(rec.as_str()) {
|
||||
Some(&(lo, ls)) => {
|
||||
let bytes = &by[lo..(lo + ls).min(by.len())];
|
||||
let loop_u = ui_layout::loop_length_units(bytes);
|
||||
let kf: Vec<String> = ui_layout::parse_build(bytes)
|
||||
.map(|lb| {
|
||||
lb.elements
|
||||
.iter()
|
||||
.map(|e| {
|
||||
format!(
|
||||
"{} [{} keys, peak a{}]",
|
||||
e.name,
|
||||
e.keyframes.len(),
|
||||
e.keyframes
|
||||
.iter()
|
||||
.map(|k| k.fade >> 24)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
println!(" {tag:<6} {rec:<20} loop {:?} {}", loop_u, kf.join(", "));
|
||||
}
|
||||
None => println!(" {tag:<6} {rec:<20} (no such record)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,13 @@
|
||||
//! however big, is part of the silhouette.
|
||||
//!
|
||||
//! Usage: envelope_screen <resource3d_dir> [protrusion_fraction]
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship::{assemble_ship, ship_id_of};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let limit: f32 = std::env::args()
|
||||
.nth(2)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0.35);
|
||||
let limit: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(0.35);
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
@@ -28,9 +25,7 @@ fn main() {
|
||||
|
||||
let mut flagged = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let ids: BTreeSet<String> = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false)
|
||||
.iter()
|
||||
.filter_map(|m| ship_id_of(&m.name).map(|s| s.to_string()))
|
||||
@@ -45,9 +40,7 @@ fn main() {
|
||||
// World box per placement.
|
||||
let mut boxes: Vec<(String, [f32; 3], [f32; 3])> = Vec::new();
|
||||
for p in &placed {
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else {
|
||||
continue;
|
||||
};
|
||||
let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue };
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for s in &m.meshes {
|
||||
for q in &s.positions {
|
||||
@@ -109,8 +102,5 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{flagged} parts protrude more than {:.0}% of their ship's size",
|
||||
100.0 * limit
|
||||
);
|
||||
println!("{flagged} parts protrude more than {:.0}% of their ship's size", 100.0 * limit);
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//! Is `ptbtn11` the TOP item of the `EXTRAS` screen?
|
||||
//!
|
||||
//! `sylpheed-port` authors `extras/initial_focus: ptbtn11` and states it is
|
||||
//! correct under the surviving reading — "a submenu resets to the item it opens
|
||||
//! on". The oracle shows EXTRAS opening on `MISSION SELECT`, the first of
|
||||
//! MISSION SELECT / MOVIE THEATER / BACK. So their value is right only if
|
||||
//! `ptbtn11` is that first item. This checks it against the disc.
|
||||
//!
|
||||
//! CONTROL: the same read on the MAIN MENU build, whose five buttons have a known
|
||||
//! top-to-bottom order (NEW GAME first). If the ordering rule cannot reproduce a
|
||||
//! known screen it cannot be trusted on an unknown one.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example extras_button_order
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn report(ar: &PakArchive, entry: usize, what: &str) {
|
||||
let Ok(by) = ar.read(&ar.entries()[entry]) else {
|
||||
return;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
return;
|
||||
};
|
||||
let mut rows: Vec<(i32, String, u32)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| e.name.starts_with("ptbtn") && !e.name.contains('f'))
|
||||
.map(|e| {
|
||||
let y = e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32);
|
||||
(y, e.name.clone(), e.kind)
|
||||
})
|
||||
.collect();
|
||||
rows.sort_by_key(|r| r.0);
|
||||
println!("\n{what} (entry {entry}) — buttons top to bottom:");
|
||||
for (y, n, k) in &rows {
|
||||
println!(" y {y:5} {n:14} kind 0x{k:04x}");
|
||||
}
|
||||
if let Some((_, first, _)) = rows.first() {
|
||||
println!(" => TOP item is {first}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
report(&ar, 5, "CONTROL: main menu (NEW GAME must be top)");
|
||||
report(&ar, 6, "EXTRAS");
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
//! assembler and the viewer) can reach a different answer from a whole-container
|
||||
//! decode. This measures that directly.
|
||||
//! Usage: filter_consistency <container.xpr> [resource...]
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::HashSet;
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).expect("container");
|
||||
@@ -27,16 +27,9 @@ fn main() {
|
||||
if off(f) != off(s) {
|
||||
differ += 1;
|
||||
if differ <= 10 {
|
||||
println!(
|
||||
"{n}: full decode at 0x{:x}, filtered at 0x{:x}",
|
||||
off(f),
|
||||
off(s)
|
||||
);
|
||||
println!("{n}: full decode at 0x{:x}, filtered at 0x{:x}", off(f), off(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{differ} of {} resources decode differently when filtered",
|
||||
names.len()
|
||||
);
|
||||
println!("{differ} of {} resources decode differently when filtered", names.len());
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
//! Where does the `DIFFICULTY` screen live?
|
||||
//!
|
||||
//! `boot-config-and-gamepart-registry.md` records a count-match — "Ⓑ = event 0,
|
||||
//! four menu items load an external archive, EXTRAS stays inside GP_TITLE" —
|
||||
//! explicitly as an observation, not a decode. The disc can test half of it:
|
||||
//! OPTIONS, LOAD GAME and TUTORIAL have their own paks, and EXTRAS' two items
|
||||
//! have GP_MISSION_SELECT / GP_MOVIE_THEATER while EXTRAS itself is GP_TITLE
|
||||
//! entries 6/9. NEW GAME is the fourth, and there is no GP_DIFFICULTY.pak.
|
||||
//!
|
||||
//! So: which archive holds a build with EASY / NORMAL / HARD buttons?
|
||||
//!
|
||||
//! CONTROL: the same scan must find the EXTRAS build in GP_TITLE, whose location
|
||||
//! is independently known (entries 6/9, buttons ptbtn11/12/13).
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_difficulty_build
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let dat = root.join("dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dat)
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let mut found_extras = false;
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let btns: Vec<&String> = b
|
||||
.records
|
||||
.keys()
|
||||
.filter(|n| n.starts_with("ptbtn") || n.contains("btn"))
|
||||
.collect();
|
||||
if btns.len() != 8 {
|
||||
continue;
|
||||
}
|
||||
let name = p.file_name().unwrap().to_string_lossy();
|
||||
// CONTROL: the known MAIN MENU build (11 records, so this control is now vacuous) must show up.
|
||||
if name == "GP_TITLE.pak" && (i == 5 || i == 8) {
|
||||
found_extras = true;
|
||||
println!("CONTROL {name} entry {i}: {} button records — the known MAIN MENU build (11 records, so this control is now vacuous)",
|
||||
btns.len());
|
||||
}
|
||||
// any build outside GP_TITLE with a small button set is a candidate
|
||||
if name != "GP_TITLE.pak" {
|
||||
let mut names: Vec<String> = btns.iter().map(|s| (*s).clone()).collect();
|
||||
names.sort();
|
||||
println!(
|
||||
" {name:28} entry {i:3} {} buttons {:?}",
|
||||
btns.len(),
|
||||
&names[..names.len().min(8)]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\ncontrol {} — the known MAIN MENU build (11 records, so this control is now vacuous) was {}found",
|
||||
if found_extras { "PASSED" } else { "FAILED" },
|
||||
if found_extras { "" } else { "NOT " });
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
//! Where is the `DIFFICULTY` screen? Search by NAME, not by structure.
|
||||
//!
|
||||
//! A previous pass searched every pak for a build with exactly 8 `btn`-named
|
||||
//! records, on the assumption that DIFFICULTY's four items (EASY / NORMAL / HARD
|
||||
//! / BACK) pair with `f` focus variants the way GP_TITLE's screens do. Nothing
|
||||
//! plausible turned up, and the assumption was mine — recorded as a negative
|
||||
//! narrower than "not found" (data/gp-title-holds-three-button-screens.txt).
|
||||
//!
|
||||
//! This drops the structural assumption and looks for the words instead, across
|
||||
//! every sprite AND record name in every build on the disc.
|
||||
//!
|
||||
//! CONTROL: the same scan must find `ptbtn11` in GP_TITLE — a name whose home is
|
||||
//! independently known — when asked for it. A name scan that finds nothing
|
||||
//! proves nothing unless it can find something.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_difficulty_names
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const WANTED: &[&str] = &["easy", "normal", "hard", "diff", "level", "rank"];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let mut control = false;
|
||||
let mut hits = 0usize;
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let pname = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let all: Vec<String> = b.sprites.keys().chain(b.records.keys()).cloned().collect();
|
||||
if pname == "GP_TITLE.pak" && all.iter().any(|n| n.contains("ptbtn11")) {
|
||||
control = true;
|
||||
}
|
||||
let m: Vec<&String> = all
|
||||
.iter()
|
||||
.filter(|n| {
|
||||
let l = n.to_lowercase();
|
||||
WANTED.iter().any(|w| l.contains(w))
|
||||
})
|
||||
.collect();
|
||||
if !m.is_empty() {
|
||||
hits += 1;
|
||||
let mut s: Vec<String> = m.iter().map(|x| (*x).clone()).collect();
|
||||
s.sort();
|
||||
s.dedup();
|
||||
println!(" {pname:28} entry {i:3} {:?}", &s[..s.len().min(6)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\ncontrol (found ptbtn11 in GP_TITLE): {}",
|
||||
if control { "PASSED" } else { "FAILED" }
|
||||
);
|
||||
println!("{hits} build(s) carried a difficulty-ish name");
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
//! mirrored pair.
|
||||
//!
|
||||
//! Usage: find_mirror <container.xpr> <resource>...
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
@@ -18,12 +18,7 @@ fn main() {
|
||||
let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||
|
||||
for m in &models {
|
||||
let pos: Vec<[f32; 3]> = m
|
||||
.meshes
|
||||
.iter()
|
||||
.flat_map(|s| s.positions.clone())
|
||||
.take(8)
|
||||
.collect();
|
||||
let pos: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).take(8).collect();
|
||||
if pos.len() < 8 {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
//! Which cue owns an XMA stream of a given payload size?
|
||||
//!
|
||||
//! A boot with `--xma_param_probe` logs each decoded stream's `byte_size`. Three
|
||||
//! of the five on the take-2 `ADV` boot are that movie's own streams; two —
|
||||
//! 1 150 976 and 1 269 760 B — belong to something unidentified. The probe gives
|
||||
//! a size and nothing else, so the disc has to be asked which cue has a stream
|
||||
//! that long.
|
||||
//!
|
||||
//! Searches every inter-descriptor span of the continuous voice stream, and
|
||||
//! every `sound.pak` entry, for a stream whose payload matches.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_stream_by_size -- <disc> <bytes>…
|
||||
use sylpheed_formats::media::{DirectorySource, DiscSource};
|
||||
use sylpheed_formats::{slb, PakArchive};
|
||||
|
||||
const DESC_MARK: u32 = 0x11;
|
||||
const DESC_REPEAT: usize = 0x800;
|
||||
const ID_MAX: u32 = 0x1_0000;
|
||||
|
||||
fn all_descriptors(buf: &[u8]) -> Vec<(usize, u32)> {
|
||||
let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]);
|
||||
let mut out = Vec::new();
|
||||
if buf.len() < DESC_REPEAT + 8 {
|
||||
return out;
|
||||
}
|
||||
let end = buf.len() - (DESC_REPEAT + 4);
|
||||
let mut o = 0;
|
||||
while o <= end {
|
||||
let id = be(o);
|
||||
if (1..ID_MAX).contains(&id) && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
|
||||
out.push((o, id));
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let disc = args
|
||||
.next()
|
||||
.expect("usage: find_stream_by_size <disc> <bytes>…");
|
||||
let wanted: Vec<usize> = args.filter_map(|a| a.parse().ok()).collect();
|
||||
assert!(!wanted.is_empty(), "give at least one payload size");
|
||||
// A `to_xma_riffs` chunk is the payload plus a 60-byte RIFF wrapper.
|
||||
let want_riff: Vec<usize> = wanted.iter().map(|w| w + 60).collect();
|
||||
println!("looking for payloads {wanted:?} (riff sizes {want_riff:?})\n");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
|
||||
// --- 1. the continuous movie-voice stream, span by span
|
||||
let tpak = src.open_pak("dat/tables.pak").expect("tables.pak");
|
||||
let marker = "eng\\Movie\\VOICE_ADV.slb";
|
||||
let registry = tpak
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|e| {
|
||||
tpak.read(e)
|
||||
.ok()
|
||||
.filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes()))
|
||||
})
|
||||
.expect("registry");
|
||||
let ids = sylpheed_formats::movie_voice::registry_voice_ids(®istry);
|
||||
let name_of: std::collections::HashMap<u32, String> =
|
||||
ids.iter().map(|(n, &i)| (i, n.clone())).collect();
|
||||
let win_start: u64 = 421_739_888 & !3;
|
||||
let buf = src
|
||||
.read_segment_range("dat/sound", win_start, 116_300_000)
|
||||
.expect("window");
|
||||
let descs = all_descriptors(&buf);
|
||||
println!("voice stream: {} descriptors", descs.len());
|
||||
let mut hits = 0;
|
||||
for w in descs.windows(2) {
|
||||
let (a, b) = (w[0].0, w[1].0);
|
||||
if b <= a || b - a < 4096 {
|
||||
continue;
|
||||
}
|
||||
for (i, r) in slb::to_xma_riffs(&buf[a..b]).iter().enumerate() {
|
||||
if want_riff.contains(&r.len()) {
|
||||
let name = name_of
|
||||
.get(&w[1].1)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("id{}", w[1].1));
|
||||
println!(
|
||||
" ✅ cue {name} (id {}) stream {i}: payload {} B",
|
||||
w[1].1,
|
||||
r.len() - 60
|
||||
);
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" {hits} hit(s) in the voice stream\n");
|
||||
|
||||
// --- 2. every sound.pak entry
|
||||
let stoc = src.read_file("dat/sound.pak").expect("sound.pak toc");
|
||||
let entries = PakArchive::parse_toc(&stoc).expect("toc");
|
||||
println!("sound.pak: {} entries", entries.len());
|
||||
let mut phits = 0;
|
||||
let mut scanned = 0usize;
|
||||
for e in &entries {
|
||||
// Only entries big enough to hold the target.
|
||||
let need = wanted.iter().copied().min().unwrap_or(0) as u32;
|
||||
if e.comp_size < need {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = src.read_segment_range("dat/sound", e.offset as u64, e.comp_size as usize)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
scanned += 1;
|
||||
for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() {
|
||||
if want_riff.contains(&r.len()) {
|
||||
println!(
|
||||
" ✅ sound.pak entry hash {:08x} offset {} size {} — stream {i}: payload {} B",
|
||||
e.name_hash,
|
||||
e.offset,
|
||||
e.comp_size,
|
||||
r.len() - 60
|
||||
);
|
||||
phits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" scanned {scanned} entries large enough; {phits} hit(s)");
|
||||
}
|
||||
@@ -1,22 +1,16 @@
|
||||
use sylpheed_formats::{game_data as gd, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap();
|
||||
let rosters = gd::load_pilot_rosters(&pak);
|
||||
fn main(){
|
||||
let disc=std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak=PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap();
|
||||
let rosters=gd::load_pilot_rosters(&pak);
|
||||
// distinct rosters by their pilot set
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut shown = 0;
|
||||
let mut seen=std::collections::BTreeSet::new(); let mut shown=0;
|
||||
println!("{} pilot-roster configs; distinct line-ups:", rosters.len());
|
||||
for r in &rosters {
|
||||
let key: String = r
|
||||
.pilots()
|
||||
.iter()
|
||||
.map(|(c, p)| format!("{c}:{p}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
if seen.insert(key) && shown < 8 {
|
||||
shown += 1;
|
||||
let flt: Vec<String> = r.pilots().iter().map(|(c, p)| format!("{c}={p}")).collect();
|
||||
for r in &rosters{
|
||||
let key:String=r.pilots().iter().map(|(c,p)|format!("{c}:{p}")).collect::<Vec<_>>().join(",");
|
||||
if seen.insert(key) && shown<8 {
|
||||
shown+=1;
|
||||
let flt:Vec<String>=r.pilots().iter().map(|(c,p)|format!("{c}={p}")).collect();
|
||||
println!(" {}", flt.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
//! Which focus records have a VARYING alpha — disc-wide, not export-wide?
|
||||
//!
|
||||
//! `rest()` returns an element's last hold keyframe. For a constant-alpha
|
||||
//! element that is harmless. For one that pulses it returns the PEAK, which is
|
||||
//! the `ui-settle-time.md` pathology: the plate's `ptbtn00f` ramps 0→80→0 and
|
||||
//! `rest()` reports 80, its maximum.
|
||||
//!
|
||||
//! The port censused this over its own export (34 records, 2 varying) and
|
||||
//! concluded there is nothing to fix. That conclusion is only as wide as the
|
||||
//! export. This asks the same question of the whole disc.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root =
|
||||
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut n_rec, mut n_elem, mut varying) = (0usize, 0usize, 0usize);
|
||||
let (mut at_peak, mut mid_ramp) = (0usize, 0usize);
|
||||
let mut by_pak: std::collections::BTreeMap<String, usize> = Default::default();
|
||||
let mut hits: Vec<String> = Vec::new();
|
||||
for p in &paks {
|
||||
let pn = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (ei, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (rn, &(o, s)) in &b.records {
|
||||
// A focus record is one whose name is another record's plus `f`.
|
||||
let Some(stem) = rn.strip_suffix("f.rat") else {
|
||||
continue;
|
||||
};
|
||||
if !b.records.contains_key(&format!("{stem}.rat")) {
|
||||
continue;
|
||||
}
|
||||
if o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
n_rec += 1;
|
||||
for el in &lb.elements {
|
||||
if el.keyframes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
n_elem += 1;
|
||||
let a: Vec<u32> = el.keyframes.iter().map(|k| k.fade >> 24).collect();
|
||||
let (lo, hi) = (*a.iter().min().unwrap(), *a.iter().max().unwrap());
|
||||
if lo == hi {
|
||||
continue;
|
||||
}
|
||||
varying += 1;
|
||||
let rest = el.rest().map(|k| k.fade >> 24).unwrap_or(0);
|
||||
if rest == hi {
|
||||
at_peak += 1
|
||||
} else {
|
||||
mid_ramp += 1
|
||||
}
|
||||
*by_pak.entry(pn.clone()).or_default() += 1;
|
||||
hits.push(format!(
|
||||
"{pn} [{ei}] {rn}::{} alpha {lo}..{hi} rest()={rest}{}",
|
||||
el.name,
|
||||
if rest == hi { " 🔴 == PEAK" } else { "" }
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("focus records disc-wide : {n_rec}");
|
||||
println!(" their timed elements : {n_elem}");
|
||||
println!(" with a VARYING alpha : {varying}");
|
||||
println!(" of which rest() == the PEAK : {at_peak} <- burns bright forever");
|
||||
println!(" of which rest() is MID-RAMP : {mid_ramp} <- neither extreme; looks plausible");
|
||||
println!("\nby pak:");
|
||||
for (k, v) in &by_pak {
|
||||
println!(" {k:<34} {v}")
|
||||
}
|
||||
println!("\nevery varying one:");
|
||||
hits.sort();
|
||||
hits.dedup();
|
||||
for h in &hits {
|
||||
println!(" {h}")
|
||||
}
|
||||
println!("\n({} distinct)", hits.len());
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//! Reconcile two ink counts for one screen that were never counting the same pixels.
|
||||
//!
|
||||
//! The port agent double-witnessed the pixel-cost claim in Godot — a renderer
|
||||
//! sharing no code with `compose` — and got `GP_TITLE` entry 12 at **59 530 px**
|
||||
//! ink above threshold 0 and **48 368** above 1. This crate reported **49 771**.
|
||||
//! Neither is wrong; the question is which convention each was using, and on a
|
||||
//! mostly-dark frame the answer moves thousands of pixels.
|
||||
//!
|
||||
//! So: count the same composite every way, and print the family. Whichever row
|
||||
//! the port's numbers land in is the convention, and then the two renderers can be
|
||||
//! compared on purpose rather than by coincidence.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_ink_thresholds
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use ui_layout::ComposeOptions;
|
||||
|
||||
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
|
||||
idx.sort_by_key(|&i| {
|
||||
let el = &build.elements[i];
|
||||
(
|
||||
ui_layout::sprite_layer_key(build, bundle, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name))
|
||||
.unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
});
|
||||
idx
|
||||
}
|
||||
|
||||
fn counts(rgba: &[u8], t: u8) -> (usize, usize) {
|
||||
let rgb = rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|p| p[0] > t || p[1] > t || p[2] > t)
|
||||
.count();
|
||||
let alpha = rgba.as_chunks::<4>().0.iter().filter(|p| p[3] > t).count();
|
||||
(rgb, alpha)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
|
||||
for entry in [12usize, 15] {
|
||||
let by = ar.read(&ar.entries()[entry]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("parse");
|
||||
for (label, opts) in [
|
||||
(
|
||||
"primitives on (what the cost run used)",
|
||||
ComposeOptions {
|
||||
include_primitives: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"primitives+focus+animated",
|
||||
ComposeOptions {
|
||||
include_primitives: true,
|
||||
include_focus: true,
|
||||
include_animated: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
] {
|
||||
let with = ui_layout::derived_paint_order(&b, &by);
|
||||
let without = order_without_rule(&b, &by);
|
||||
let a = ui_layout::compose_with_order(&b, &by, opts, None, Some(&with));
|
||||
let c = ui_layout::compose_with_order(&b, &by, opts, None, Some(&without));
|
||||
println!(
|
||||
"\n== GP_TITLE entry {entry} — {label} ({}x{})",
|
||||
a.width, a.height
|
||||
);
|
||||
println!(
|
||||
" threshold | RGB>t with rule | A>t with rule | RGB>t WITHOUT | A>t WITHOUT"
|
||||
);
|
||||
for t in [0u8, 1, 2, 4, 8, 16] {
|
||||
let (r1, a1) = counts(&a.rgba, t);
|
||||
let (r0, a0) = counts(&c.rgba, t);
|
||||
println!(" >{t:<8} | {r1:>16} | {a1:>14} | {r0:>13} | {a0:>11}");
|
||||
}
|
||||
let changed = a
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(c.rgba.as_chunks::<4>().0.iter())
|
||||
.filter(|(x, y)| x != y)
|
||||
.count();
|
||||
println!(" exact-RGBA changed pixels between the two orders: {changed}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
//! Of the forced instances the rule merely CONFIRMS, how many have a key READ
|
||||
//! FROM THE FILE, and how many an IMPLIED key that is itself a measurement?
|
||||
//!
|
||||
//! `forced_backdrop_necessity.rs` asked only whether an element had *a* key,
|
||||
//! collapsing `sprite_layer_key` (a `u16` read out of the `T8aD` header — decoded)
|
||||
//! with `implied_layer_key` (this crate's per-name table of positions **measured
|
||||
//! in the running game**). For counting whether the rule moves anything that is
|
||||
//! the right question. For describing what a confirmation is *made of*, it is not:
|
||||
//! "the file already settles it" and "another measurement already settles it" are
|
||||
//! different claims, and a reader who sees "own key" will take the first.
|
||||
//!
|
||||
//! Raised by the port agent 2026-08-30.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_key_source
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let (mut read, mut implied, mut none) = (0usize, 0usize, 0usize);
|
||||
println!("# archive entry element key_source key");
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if !ui_layout::forced_backdrop(&b, el) {
|
||||
continue;
|
||||
}
|
||||
let (src, key) = match ui_layout::sprite_layer_key(&b, &by, el) {
|
||||
Some(k) => {
|
||||
read += 1;
|
||||
("read_T8aD", Some(k))
|
||||
}
|
||||
None => match ui_layout::implied_layer_key(&el.name) {
|
||||
Some(k) => {
|
||||
implied += 1;
|
||||
("implied_MEASURED", Some(k))
|
||||
}
|
||||
None => {
|
||||
none += 1;
|
||||
("none", None)
|
||||
}
|
||||
},
|
||||
};
|
||||
println!(
|
||||
" {name} {i} {} {src} {}",
|
||||
el.name,
|
||||
key.map(|k| format!("0x{k:08X}")).unwrap_or("-".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n# forced instances by key source:");
|
||||
println!("# read from the T8aD header (decoded): {read}");
|
||||
println!("# implied — this crate's MEASURED name table: {implied}");
|
||||
println!("# none — only forced_backdrop can speak: {none}");
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
//! Which screens does `forced_backdrop` DECIDE, and which does it merely agree with?
|
||||
//!
|
||||
//! Every check this corpus has run on the rule measured its **stability** — that
|
||||
//! no verdict moved when something else changed. That is a different property
|
||||
//! from **necessity**: an element whose position is already fixed by a read or an
|
||||
//! implied key is confirmed by the rule, not decided by it.
|
||||
//!
|
||||
//! So: compute `derived_paint_order` with the rule, and again with the
|
||||
//! `forced_backdrop` fallback removed, and report every entry whose order moves.
|
||||
//! Where nothing moves, the rule is decorative on that screen; where it moves,
|
||||
//! the rule is the only thing holding the order up.
|
||||
//!
|
||||
//! Raised by the port agent 2026-08-30. Reach note in
|
||||
//! `docs/re/structures/ui-forced-backdrop.md`.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_necessity -- [pak...]
|
||||
//!
|
||||
//! 🔴 With no argument this used to default to `GP_TITLE` alone, so a bare run
|
||||
//! reported **6 instances, not 80** — a thirteenth of the census, printed in the
|
||||
//! same format and reading like the whole thing. The port agent hit it and nearly
|
||||
//! filed the discrepancy back at me. It now walks every `dat/*.pak` by default and
|
||||
//! says on stderr how many archives it opened, because "I ran your instrument" has
|
||||
//! to mean the same thing to both of us.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
|
||||
idx.sort_by_key(|&i| {
|
||||
let el = &build.elements[i];
|
||||
(
|
||||
ui_layout::sprite_layer_key(build, bundle, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name))
|
||||
.unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
});
|
||||
idx
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let (mut total_decides, mut total_agrees) = (0usize, 0usize);
|
||||
let mut paks: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
|
||||
if paks.is_empty() {
|
||||
let mut all: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
all.sort();
|
||||
paks = all;
|
||||
}
|
||||
eprintln!("# scanning {} archive(s)", paks.len());
|
||||
for path in &paks {
|
||||
let ar = PakArchive::open(path).expect("pak");
|
||||
println!("# {}", path.display());
|
||||
println!("# entry forced decides elements note");
|
||||
|
||||
let mut decides = Vec::new();
|
||||
let mut agrees = Vec::new();
|
||||
#[allow(unused)]
|
||||
let _ = (&decides, &agrees);
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let with = ui_layout::derived_paint_order(&b, &by);
|
||||
let without = order_without_rule(&b, &by);
|
||||
|
||||
// Which elements does the rule fire on, and of those, which have no key
|
||||
// of their own to fall back on?
|
||||
let mut forced = Vec::new();
|
||||
let mut keyless = Vec::new();
|
||||
for el in &b.elements {
|
||||
if !ui_layout::forced_backdrop(&b, el) {
|
||||
continue;
|
||||
}
|
||||
forced.push(el.name.clone());
|
||||
let own = ui_layout::sprite_layer_key(&b, &by, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name));
|
||||
if own.is_none() {
|
||||
keyless.push(el.name.clone());
|
||||
}
|
||||
}
|
||||
if forced.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let moved = with != without;
|
||||
if moved {
|
||||
decides.push(i);
|
||||
} else {
|
||||
agrees.push(i);
|
||||
}
|
||||
println!(
|
||||
" {i:>5} {:>6} {:>7} {:>8} forced=[{}] keyless=[{}]",
|
||||
forced.len(),
|
||||
if moved { "YES" } else { "no" },
|
||||
b.elements.len(),
|
||||
forced.join(","),
|
||||
keyless.join(","),
|
||||
);
|
||||
}
|
||||
println!("# rule DECIDES the order on entries {decides:?}");
|
||||
println!("# rule merely AGREES on entries {agrees:?}\n");
|
||||
total_decides += decides.len();
|
||||
total_agrees += agrees.len();
|
||||
}
|
||||
println!(
|
||||
"# TOTAL over {} archive(s): {total_decides} deciding entries, \
|
||||
{total_agrees} agreeing",
|
||||
paks.len()
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
//! What does `forced_backdrop` cost IN PIXELS on the screens it decides?
|
||||
//!
|
||||
//! `forced_backdrop_necessity.rs` answers "does the derived ORDER move", which is
|
||||
//! a property of the sort. The port agent then pointed out — correctly — that its
|
||||
//! re-run of that probe was **my code executed twice**, not a second witness, so
|
||||
//! the disc-wide 62 has one measurement behind it and only `GP_TITLE` has two.
|
||||
//!
|
||||
//! This does not fix that (it is still this crate), but it moves the question to a
|
||||
//! **different layer**: render each deciding build twice, once in the order
|
||||
//! `compose` derives and once with the `forced_backdrop` fallback removed, and
|
||||
//! count the pixels that differ. "The order moved" and "the picture moved" are not
|
||||
//! the same claim, and the second is the one anybody cares about — the tie-break
|
||||
//! work already found overlapping reorders that cost exactly zero pixels.
|
||||
//!
|
||||
//! Each entry carries its own CONTROL: the pixel count of the composite itself.
|
||||
//! If a build renders empty, its zero means the instrument saw nothing, not that
|
||||
//! the rule is free.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_pixel_cost -- [pak...]
|
||||
//!
|
||||
//! With no argument it walks **every `dat/*.pak`** — the necessity probe defaulted
|
||||
//! to `GP_TITLE`, which made a bare run report a thirteenth of the census and read
|
||||
//! like the whole thing.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use ui_layout::ComposeOptions;
|
||||
|
||||
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
|
||||
idx.sort_by_key(|&i| {
|
||||
let el = &build.elements[i];
|
||||
(
|
||||
ui_layout::sprite_layer_key(build, bundle, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name))
|
||||
.unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
});
|
||||
idx
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
|
||||
if paks.is_empty() {
|
||||
let mut all: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
all.sort();
|
||||
paks = all;
|
||||
}
|
||||
eprintln!("# scanning {} archive(s)", paks.len());
|
||||
|
||||
let opts = ComposeOptions {
|
||||
include_primitives: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
println!("# archive entry element changed_px total_px ink_px(control) pct");
|
||||
let (mut decided, mut zero_cost, mut blind) = (0usize, 0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let with = ui_layout::derived_paint_order(&b, &by);
|
||||
let without = order_without_rule(&b, &by);
|
||||
if with == without {
|
||||
continue;
|
||||
}
|
||||
let forced: Vec<&str> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| ui_layout::forced_backdrop(&b, el))
|
||||
.map(|el| el.name.as_str())
|
||||
.collect();
|
||||
|
||||
let a = ui_layout::compose_with_order(&b, &by, opts, None, Some(&with));
|
||||
let c = ui_layout::compose_with_order(&b, &by, opts, None, Some(&without));
|
||||
let n = a
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(c.rgba.as_chunks::<4>().0.iter())
|
||||
.filter(|(x, y)| x != y)
|
||||
.count();
|
||||
// Control: does this build put any ink down at all, against the bare
|
||||
// backdrop? A build that renders to nothing cannot show a reorder.
|
||||
let ink = a
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|p| p[..3] != [0, 0, 0])
|
||||
.count();
|
||||
let total = a.rgba.len() / 4;
|
||||
|
||||
decided += 1;
|
||||
if ink == 0 {
|
||||
blind += 1;
|
||||
} else if n == 0 {
|
||||
zero_cost += 1;
|
||||
}
|
||||
println!(
|
||||
" {name} {i} {} {n} {total} {ink} {:.2}%",
|
||||
forced.join(","),
|
||||
100.0 * n as f64 / total as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("\n# builds whose ORDER the rule decides: {decided}");
|
||||
println!("# of those, costing ZERO pixels: {zero_cost}");
|
||||
println!("# of those, BLIND (build renders no ink, control fails): {blind}");
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//! Is `GP_DIALOG` 2/3 the ONLY build on the disc with four buttons at 259/329/399/469?
|
||||
//!
|
||||
//! Both agents recorded the same reach on the DIFFICULTY identification: entries
|
||||
//! 2/3 are picked out by button count and geometry, not by a binding from
|
||||
//! `DLG_SELECT_DIFFICULTY` to a pak entry, so "another four-button dialog with the
|
||||
//! same rows would be indistinguishable". This tests whether such a rival exists.
|
||||
//!
|
||||
//! CONTROL: the scan must find GP_DIALOG 2 and 3 themselves. A rival-search that
|
||||
//! cannot find the incumbent proves nothing.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example four_button_row_rivals
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const WANT: [i32; 4] = [259, 329, 399, 469];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut incumbent, mut rivals) = (0, 0);
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let pname = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut ys: Vec<i32> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.name.contains("btn") && !el.name.contains('f'))
|
||||
.map(|el| el.rest().map(|k| k.y).unwrap_or(el.pivot_y as i32))
|
||||
.collect();
|
||||
ys.sort();
|
||||
ys.dedup();
|
||||
if ys.len() != 4 {
|
||||
continue;
|
||||
}
|
||||
let close = ys.iter().zip(WANT.iter()).all(|(a, b)| (a - b).abs() <= 6);
|
||||
if close {
|
||||
let is_inc = pname == "GP_DIALOG.pak" && (i == 2 || i == 3);
|
||||
if is_inc {
|
||||
incumbent += 1
|
||||
} else {
|
||||
rivals += 1
|
||||
}
|
||||
println!(
|
||||
" {}{pname:24} entry {i:4} rows {ys:?}",
|
||||
if is_inc { "INCUMBENT " } else { "RIVAL " }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\ncontrol: found {incumbent} incumbent build(s) (want 2) — {}",
|
||||
if incumbent == 2 { "PASSED" } else { "FAILED" }
|
||||
);
|
||||
println!("{rivals} rival build(s) elsewhere on the disc");
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
//! The main menu's sprites, by their ALPHA channel — is `ptframe1`/`ptframe2`'s
|
||||
//! "no fully-opaque pixel" a property of the artwork, and does any alpha value
|
||||
//! look like a scale the game expands (e.g. 0..128) rather than 0..255?
|
||||
//!
|
||||
//! The port measures both frames as rendering too DARK against the capture, with
|
||||
//! the shortfall correlating with the BACKGROUND. Two different causes predict
|
||||
//! that: a background-scaling blend selected in code, or an alpha that is too
|
||||
//! LOW in our decode. This example tests the second, which is on the disc.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example frame_alpha_census
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, t8ad, ui_layout};
|
||||
|
||||
fn census(name: &str, img: &t8ad::T8adImage) {
|
||||
let n = (img.width * img.height) as usize;
|
||||
let mut hist = [0usize; 256];
|
||||
for p in 0..n {
|
||||
hist[img.rgba[p * 4 + 3] as usize] += 1;
|
||||
}
|
||||
let zero = hist[0];
|
||||
let full = hist[255];
|
||||
let max = (0..256).rev().find(|&a| hist[a] > 0).unwrap_or(0);
|
||||
let nonzero = n - zero;
|
||||
// the top five alpha values that actually occur, by population
|
||||
let mut top: Vec<(usize, usize)> = (1..256)
|
||||
.map(|a| (hist[a], a))
|
||||
.filter(|&(c, _)| c > 0)
|
||||
.collect();
|
||||
top.sort_unstable_by_key(|a| std::cmp::Reverse(a.0));
|
||||
let top5: Vec<String> = top
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|&(c, a)| format!("{a}x{c}"))
|
||||
.collect();
|
||||
println!(
|
||||
"{name:<16} {}x{:<4} px={n:<8} a=0:{:5.1}% a=255:{:5.1}% max={max:<3} \
|
||||
partial(1..254)/nonzero={:5.1}% top:[{}]",
|
||||
img.width,
|
||||
img.height,
|
||||
100.0 * zero as f64 / n as f64,
|
||||
100.0 * full as f64 / n as f64,
|
||||
if nonzero > 0 {
|
||||
100.0 * (nonzero - full) as f64 / nonzero as f64
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
top5.join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = argv
|
||||
.iter()
|
||||
.find(|a| a.parse::<usize>().is_err())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "GP_TITLE".to_string());
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
// Builds default to the two the port ships and can be overridden, so the
|
||||
// same census serves the title (4) and the `PRESS (A)` plate (2).
|
||||
let args: Vec<usize> = argv.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
let builds: Vec<usize> = if args.is_empty() { vec![5, 6] } else { args };
|
||||
for build in builds {
|
||||
let Ok(by) = ar.read(&ar.entries()[build]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("=== {pak} build {build} ===");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
match t8ad::parse(s) {
|
||||
Some(img) => census(n, &img),
|
||||
None => println!("{n:<16} (not a T8aD / failed to parse)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//! The keyframe record's two unexplained words (`+4`, `+8`) and the fade/tint —
|
||||
//! do any of them separate the four elements the port measures as rendering too
|
||||
//! dark (`ptframe1`/`2`, `ptframe3`/`4`) from the ones it measures as accurate?
|
||||
//!
|
||||
//! The T8aD header does not: no word and no bit of `+0x04`/`+0x08` puts the four
|
||||
//! frames on one side and `pteff10` (max alpha 130, wholly semi-transparent, and
|
||||
//! rendered nearly exact) on the other. The keyframe is the other place a
|
||||
//! per-element draw mode could live.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example frame_keyframe_unknowns
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
println!(
|
||||
"{:<22} {:>5} {:>3} {:>10} {:>10} {:>8} {:>8} {:>8}",
|
||||
"element", "build", "kf", "unknown_4", "unknown_8", "fade", "tint", "rot"
|
||||
);
|
||||
let mut frame_sets: Vec<(String, i32, i32, u32, u32, i32)> = Vec::new();
|
||||
let mut other_sets: Vec<(String, i32, i32, u32, u32, i32)> = Vec::new();
|
||||
for build in [5usize, 6] {
|
||||
let by = ar.read(&ar.entries()[build]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for e in &b.elements {
|
||||
for (i, k) in e.keyframes.iter().enumerate() {
|
||||
println!(
|
||||
"{:<22} {build:>5} {i:>3} {:>10} {:>10} {:08X} {:08X} {:>8}",
|
||||
e.name, k.unknown_4, k.unknown_8, k.fade, k.tint, k.rotation_deg
|
||||
);
|
||||
let row = (
|
||||
e.name.clone(),
|
||||
k.unknown_4,
|
||||
k.unknown_8,
|
||||
k.fade,
|
||||
k.tint,
|
||||
k.rotation_deg,
|
||||
);
|
||||
if e.name.contains("frame") {
|
||||
frame_sets.push(row)
|
||||
} else {
|
||||
other_sets.push(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\nframe keyframes: {} other keyframes: {}",
|
||||
frame_sets.len(),
|
||||
other_sets.len()
|
||||
);
|
||||
for (label, get) in [
|
||||
("unknown_4", 0usize),
|
||||
("unknown_8", 1),
|
||||
("fade", 2),
|
||||
("tint", 3),
|
||||
("rotation", 4),
|
||||
] {
|
||||
let val = |r: &(String, i32, i32, u32, u32, i32)| -> i64 {
|
||||
match get {
|
||||
0 => r.1 as i64,
|
||||
1 => r.2 as i64,
|
||||
2 => r.3 as i64,
|
||||
3 => r.4 as i64,
|
||||
_ => r.5 as i64,
|
||||
}
|
||||
};
|
||||
let fv: std::collections::BTreeSet<i64> = frame_sets.iter().map(val).collect();
|
||||
let ov: std::collections::BTreeSet<i64> = other_sets.iter().map(val).collect();
|
||||
let only_frames: Vec<&i64> = fv.iter().filter(|v| !ov.contains(v)).collect();
|
||||
println!(
|
||||
"{label:<10} frames take {:?} others take {} distinct values; frame-only values: {:?}",
|
||||
fv,
|
||||
ov.len(),
|
||||
only_frames
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
//! Which T8aD header word, if any, separates the FOUR elements the port measures
|
||||
//! as rendering too dark (`ptframe1`/`2` on the main menu, `ptframe3`/`4` on
|
||||
//! `EXTRAS`) from the elements on the same two screens it measures as accurate?
|
||||
//!
|
||||
//! The control that matters: `pteff10` has max alpha 130 and no fully-opaque
|
||||
//! pixel — the same "wholly semi-transparent" property the port proposed as the
|
||||
//! reason the frames are special — and it renders nearly exact. So the separator
|
||||
//! must put `pteff10` on the ACCURATE side.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example frame_vs_accurate_words
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let mut rows: Vec<(String, usize, Vec<u32>)> = Vec::new();
|
||||
for build in [5usize, 6] {
|
||||
let by = ar.read(&ar.entries()[build]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 48 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
let ws: Vec<u32> = (0..12)
|
||||
.map(|k| u32::from_be_bytes([s[k * 4], s[k * 4 + 1], s[k * 4 + 2], s[k * 4 + 3]]))
|
||||
.collect();
|
||||
rows.push((n.clone(), build, ws));
|
||||
}
|
||||
}
|
||||
let is_frame = |n: &str| n.starts_with("ptframe");
|
||||
println!(
|
||||
"{:<16} {:>5} +0x04 +0x08 +0x1C +0x2C",
|
||||
"sprite", "build"
|
||||
);
|
||||
for (n, b, w) in &rows {
|
||||
println!(
|
||||
"{n:<16} {b:>5} {:08X} {:08X} {:08X} {:08X}{}",
|
||||
w[1],
|
||||
w[2],
|
||||
w[7],
|
||||
w[11],
|
||||
if is_frame(n) { " <- TOO DARK" } else { "" }
|
||||
);
|
||||
}
|
||||
println!("\nwords where every ptframe* agrees and NO other sprite takes that value:");
|
||||
let frames: Vec<&(String, usize, Vec<u32>)> =
|
||||
rows.iter().filter(|(n, _, _)| is_frame(n)).collect();
|
||||
let mut any = false;
|
||||
for k in 0..12 {
|
||||
let v = frames[0].2[k];
|
||||
if !frames.iter().all(|r| r.2[k] == v) {
|
||||
continue;
|
||||
}
|
||||
if rows.iter().any(|(n, _, w)| !is_frame(n) && w[k] == v) {
|
||||
continue;
|
||||
}
|
||||
println!(" word {k} (+0x{:02X}) = {v:08X}", k * 4);
|
||||
any = true;
|
||||
}
|
||||
if !any {
|
||||
println!(" NONE — no header word separates the four frames from the rest");
|
||||
}
|
||||
println!(
|
||||
"\nper-bit check on +0x04 and +0x08 (a bit that is 1 on all frames, 0 on all others):"
|
||||
);
|
||||
let mut anyb = false;
|
||||
for &k in &[1usize, 2] {
|
||||
for bit in 0..32 {
|
||||
let on = |v: u32| (v >> bit) & 1 == 1;
|
||||
if frames.iter().all(|r| on(r.2[k]))
|
||||
&& rows.iter().all(|(n, _, w)| is_frame(n) || !on(w[k]))
|
||||
{
|
||||
println!(" +0x{:02X} bit {bit} (0x{:X})", k * 4, 1u32 << bit);
|
||||
anyb = true;
|
||||
}
|
||||
if frames.iter().all(|r| !on(r.2[k]))
|
||||
&& rows.iter().all(|(n, _, w)| is_frame(n) || on(w[k]))
|
||||
{
|
||||
println!(
|
||||
" +0x{:02X} bit {bit} (0x{:X}) INVERTED",
|
||||
k * 4,
|
||||
1u32 << bit
|
||||
);
|
||||
anyb = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !anyb {
|
||||
println!(" NONE");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
//! Which gate stops the resources that never decode?
|
||||
//! Usage: gate_histogram <resource3d_dir> [max_resources]
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let cap: usize = std::env::args()
|
||||
.nth(2)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(usize::MAX);
|
||||
let cap: usize = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX);
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
@@ -23,9 +20,7 @@ fn main() {
|
||||
if done >= cap {
|
||||
break;
|
||||
}
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
if names.is_empty() {
|
||||
continue;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//! Every button record in `GP_TITLE.pak`, per entry.
|
||||
//!
|
||||
//! Testing half of the count-match in boot-config-and-gamepart-registry.md:
|
||||
//! "four menu items load an external archive, EXTRAS stays inside GP_TITLE".
|
||||
//! If DIFFICULTY (NEW GAME's destination, EASY/NORMAL/HARD/BACK) is also inside
|
||||
//! GP_TITLE, then NEW GAME loads nothing external and that reading is wrong.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example gp_title_buttons
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut btns: Vec<String> = b
|
||||
.records
|
||||
.keys()
|
||||
.filter(|n| n.starts_with("ptbtn"))
|
||||
.cloned()
|
||||
.collect();
|
||||
btns.sort();
|
||||
if btns.is_empty() {
|
||||
continue;
|
||||
}
|
||||
println!("entry {i:2} {:2} button records {:?}", btns.len(), btns);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").unwrap());
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).unwrap();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else {
|
||||
println!("{i:2} <unreadable>");
|
||||
continue;
|
||||
};
|
||||
let names: Vec<String> = ui_layout::parse_build(&by)
|
||||
.map(|b| b.sprites.keys().take(2).cloned().collect())
|
||||
.unwrap_or_default();
|
||||
let rec: Vec<String> = ui_layout::parse_build(&by)
|
||||
.map(|b| b.records.keys().take(2).cloned().collect())
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
"{i:2} {} B sprites {:?} records {:?}",
|
||||
by.len(),
|
||||
names,
|
||||
rec
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! Is `GP_TITLE.pak` really "8 screens shipped twice, EN/JP"?
|
||||
//!
|
||||
//! The Q2 headline says each screen appears twice. The entry dump raised a
|
||||
//! doubt: entry 11 shows `palogo_gamearts` and entry 14 shows `palogo_seta`,
|
||||
//! which are different studios, not a language pair. If the two halves of a
|
||||
//! "pair" declare different sprites, "shipped twice" is the wrong description of
|
||||
//! at least that pair.
|
||||
//!
|
||||
//! CONTROL: a pair known to be a real EN/JP pair must come out as matching. 2/3
|
||||
//! (the PRESS Ⓐ plate) is byte-identical in size and is the control.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example gp_title_pair_check
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn sprites(ar: &PakArchive, i: usize) -> BTreeSet<String> {
|
||||
let Ok(by) = ar.read(&ar.entries()[i]) else {
|
||||
return BTreeSet::new();
|
||||
};
|
||||
ui_layout::parse_build(&by)
|
||||
.map(|b| b.sprites.keys().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let pairs = [
|
||||
(0, 1, "loading plain"),
|
||||
(2, 3, "PRESS (A) plate [CONTROL]"),
|
||||
(4, 7, "title art"),
|
||||
(5, 8, "main menu"),
|
||||
(6, 9, "EXTRAS"),
|
||||
(10, 13, "publisher splash"),
|
||||
(11, 14, "developer splash"),
|
||||
(12, 15, "loading dressed"),
|
||||
];
|
||||
for (a, b, what) in pairs {
|
||||
let (sa, sb) = (sprites(&ar, a), sprites(&ar, b));
|
||||
let only_a: Vec<_> = sa.difference(&sb).cloned().collect();
|
||||
let only_b: Vec<_> = sb.difference(&sa).cloned().collect();
|
||||
let shared = sa.intersection(&sb).count();
|
||||
let verdict = if only_a.is_empty() && only_b.is_empty() {
|
||||
"IDENTICAL SET"
|
||||
} else {
|
||||
"DIFFERS"
|
||||
};
|
||||
println!("\n{a:2}/{b:<2} {what:26} {shared:3} shared {verdict}");
|
||||
if !only_a.is_empty() {
|
||||
println!(" only in {a}: {only_a:?}");
|
||||
}
|
||||
if !only_b.is_empty() {
|
||||
println!(" only in {b}: {only_b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,2 @@
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let b = std::fs::read(&a[1]).unwrap();
|
||||
for l in sylpheed_formats::mesh::debug_grouped_report(&b, &a[2], a[3].parse().unwrap()) {
|
||||
println!("{l}")
|
||||
}
|
||||
}
|
||||
fn main(){let a:Vec<String>=std::env::args().collect();let b=std::fs::read(&a[1]).unwrap();
|
||||
for l in sylpheed_formats::mesh::debug_grouped_report(&b,&a[2],a[3].parse().unwrap()){println!("{l}")}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user