From e3a46cb8e6b6823b5b489efa728bdad949011fd9 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Fri, 3 Jul 2026 16:07:52 -0500 Subject: [PATCH] Remove wolfdawn src and have binaries for linux and windows --- .gitignore | 3 +- README.md | 2 +- gui/wolf_workflow_tab.py | 6 +- util/wolfdawn-master/.gitignore | 7 - util/wolfdawn-master/Cargo.lock | 3578 ------------- util/wolfdawn-master/Cargo.toml | 17 - util/wolfdawn-master/README.md | 145 - .../crates/wolf-archive/Cargo.toml | 21 - .../wolf-archive/src/bin/wolf-unpack.rs | 6 - .../crates/wolf-archive/src/chacha20.rs | 153 - .../crates/wolf-archive/src/codec.rs | 361 -- .../crates/wolf-archive/src/crypto.rs | 726 --- .../wolf-archive/src/dxarc_v2/common.rs | 151 - .../wolf-archive/src/dxarc_v2/encode.rs | 303 -- .../crates/wolf-archive/src/dxarc_v2/mod.rs | 19 - .../crates/wolf-archive/src/dxarc_v2/ver5.rs | 99 - .../crates/wolf-archive/src/dxarc_v2/ver6.rs | 104 - .../crates/wolf-archive/src/dxarchive.rs | 947 ---- .../crates/wolf-archive/src/lib.rs | 289 -- .../crates/wolf-archive/src/pack.rs | 527 -- .../wolf-archive/tests/extract_golden.rs | 75 - .../wolf-archive/tests/gamepro_archive.rs | 61 - .../wolf-archive/tests/pack_roundtrip.rs | 178 - .../crates/wolf-cli/Cargo.toml | 16 - .../crates/wolf-cli/src/cmd.rs | 2754 ---------- .../crates/wolf-cli/src/main.rs | 75 - .../crates/wolf-core/Cargo.toml | 6 - .../crates/wolf-core/src/codec/lz4.rs | 271 - .../crates/wolf-core/src/codec/mod.rs | 5 - .../crates/wolf-core/src/codec/wolfsave.rs | 145 - .../crates/wolf-core/src/error.rs | 69 - .../crates/wolf-core/src/lib.rs | 20 - .../crates/wolf-core/src/reader.rs | 140 - .../crates/wolf-core/src/writer.rs | 73 - .../crates/wolf-core/src/wstr.rs | 44 - .../crates/wolf-decompiler/Cargo.toml | 15 - .../examples/roundtrip_check.rs | 80 - .../wolf-decompiler/examples/scan_banks.rs | 345 -- .../crates/wolf-decompiler/src/compile/cmd.rs | 743 --- .../crates/wolf-decompiler/src/compile/lex.rs | 33 - .../crates/wolf-decompiler/src/compile/mod.rs | 301 -- .../crates/wolf-decompiler/src/db_edit.rs | 127 - .../crates/wolf-decompiler/src/document.rs | 391 -- .../crates/wolf-decompiler/src/json.rs | 200 - .../crates/wolf-decompiler/src/lib.rs | 49 - .../crates/wolf-decompiler/src/merge.rs | 374 -- .../crates/wolf-decompiler/src/names.rs | 745 --- .../crates/wolf-decompiler/src/save.rs | 630 --- .../crates/wolf-decompiler/src/save_pro.rs | 345 -- .../crates/wolf-decompiler/src/spec.rs | 117 - .../crates/wolf-decompiler/src/strings.rs | 2339 --------- .../crates/wolf-decompiler/src/symbols.rs | 308 -- .../crates/wolf-decompiler/src/text.rs | 102 - .../crates/wolf-decompiler/src/txt_events.rs | 441 -- .../wolf-decompiler/src/wolfscript/mod.rs | 872 ---- .../wolf-decompiler/src/wolfscript/ops.rs | 284 -- .../tests/all_versions_commands.rs | 118 - .../tests/all_versions_recompile.rs | 95 - .../wolf-decompiler/tests/common/mod.rs | 15 - .../wolf-decompiler/tests/corpus_gates.rs | 275 - .../crates/wolf-decompiler/tests/db_apply.rs | 83 - .../crates/wolf-decompiler/tests/db_json.rs | 49 - .../tests/full_file_recompile.rs | 160 - .../tests/game_dat_roundtrip.rs | 111 - .../wolf-decompiler/tests/gamedat_full.rs | 179 - .../tests/gamepro_decompile.rs | 168 - .../wolf-decompiler/tests/merge_roundtrip.rs | 169 - .../wolf-decompiler/tests/names_roundtrip.rs | 407 -- .../wolf-decompiler/tests/no_generic_argn.rs | 56 - .../wolf-decompiler/tests/raw_census.rs | 87 - .../crates/wolf-decompiler/tests/recompile.rs | 195 - .../tests/save_pro_roundtrip.rs | 133 - .../wolf-decompiler/tests/save_roundtrip.rs | 121 - .../tests/strings_roundtrip.rs | 215 - .../tests/txt_events_roundtrip.rs | 160 - .../crates/wolf-formats/Cargo.toml | 13 - .../crates/wolf-formats/src/command.rs | 204 - .../crates/wolf-formats/src/common_event.rs | 308 -- .../crates/wolf-formats/src/database.rs | 447 -- .../crates/wolf-formats/src/game_dat.rs | 282 - .../crates/wolf-formats/src/lib.rs | 26 - .../crates/wolf-formats/src/map.rs | 383 -- .../crates/wolf-formats/tests/all_versions.rs | 165 - .../wolf-formats/tests/archive_end_to_end.rs | 81 - .../wolf-formats/tests/basic_database.rs | 69 - .../wolf-formats/tests/gamepro_probe.rs | 146 - .../crates/wolf-formats/tests/roundtrip.rs | 112 - .../crates/wolf-formats/tests/ver5_archive.rs | 73 - .../crates/wolf-gui/Cargo.toml | 46 - .../crates/wolf-gui/src/app.rs | 4533 ----------------- .../crates/wolf-gui/src/archive.rs | 463 -- .../crates/wolf-gui/src/database.rs | 678 --- .../crates/wolf-gui/src/decompile.rs | 379 -- .../crates/wolf-gui/src/gamedat.rs | 376 -- .../crates/wolf-gui/src/log.rs | 94 - .../crates/wolf-gui/src/main.rs | 759 --- .../crates/wolf-gui/src/project.rs | 369 -- .../crates/wolf-gui/src/saves.rs | 501 -- .../crates/wolf-gui/src/task.rs | 174 - .../crates/wolf-gui/src/translation.rs | 1248 ----- .../crates/wolf-gui/src/verify.rs | 376 -- .../crates/wolf-gui/src/widgets.rs | 228 - util/wolfdawn/__init__.py | 233 +- util/wolfdawn/bin/windows/wolf.exe | Bin 0 -> 2334720 bytes 104 files changed, 140 insertions(+), 35659 deletions(-) delete mode 100644 util/wolfdawn-master/.gitignore delete mode 100644 util/wolfdawn-master/Cargo.lock delete mode 100644 util/wolfdawn-master/Cargo.toml delete mode 100644 util/wolfdawn-master/README.md delete mode 100644 util/wolfdawn-master/crates/wolf-archive/Cargo.toml delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/bin/wolf-unpack.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/chacha20.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/codec.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/crypto.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/common.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/encode.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/mod.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver5.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver6.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/dxarchive.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/lib.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/src/pack.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/tests/extract_golden.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/tests/gamepro_archive.rs delete mode 100644 util/wolfdawn-master/crates/wolf-archive/tests/pack_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-cli/Cargo.toml delete mode 100644 util/wolfdawn-master/crates/wolf-cli/src/cmd.rs delete mode 100644 util/wolfdawn-master/crates/wolf-cli/src/main.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/Cargo.toml delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/codec/lz4.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/codec/mod.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/codec/wolfsave.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/error.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/lib.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/reader.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/writer.rs delete mode 100644 util/wolfdawn-master/crates/wolf-core/src/wstr.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/Cargo.toml delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/examples/roundtrip_check.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/examples/scan_banks.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/compile/cmd.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/compile/lex.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/compile/mod.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/db_edit.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/document.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/json.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/lib.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/merge.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/names.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/save.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/save_pro.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/spec.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/strings.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/symbols.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/text.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/txt_events.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/mod.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/ops.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_commands.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_recompile.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/common/mod.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/corpus_gates.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/db_apply.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/db_json.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/full_file_recompile.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/game_dat_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/gamedat_full.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/gamepro_decompile.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/merge_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/names_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/no_generic_argn.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/raw_census.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/recompile.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/save_pro_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/save_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/strings_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-decompiler/tests/txt_events_roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/Cargo.toml delete mode 100644 util/wolfdawn-master/crates/wolf-formats/src/command.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/src/common_event.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/src/database.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/src/game_dat.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/src/lib.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/src/map.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/tests/all_versions.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/tests/archive_end_to_end.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/tests/basic_database.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/tests/gamepro_probe.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/tests/roundtrip.rs delete mode 100644 util/wolfdawn-master/crates/wolf-formats/tests/ver5_archive.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/Cargo.toml delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/app.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/archive.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/database.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/decompile.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/gamedat.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/log.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/main.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/project.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/saves.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/task.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/translation.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/verify.rs delete mode 100644 util/wolfdawn-master/crates/wolf-gui/src/widgets.rs create mode 100644 util/wolfdawn/bin/windows/wolf.exe diff --git a/.gitignore b/.gitignore index 4173a2f..7782aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,5 @@ util/ace/*.exe util/ace/.tools_version.json !util/ace/offline/*.exe -# WolfDawn: ignore the Rust build tree, but keep the committed prebuilt binaries. -util/wolfdawn-master/target/ +# WolfDawn: keep the committed prebuilt binaries (offline) under version control. !util/wolfdawn/bin/** diff --git a/README.md b/README.md index 8fd858d..e36fe63 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,7 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel | **4 Package** | Either run from the loose `Data/` folder (backs up `Data.wolf` → `Data.wolf.bak`), or **Repack** a fresh `Data.wolf`, inheriting the original archive's encryption. | | **5 Saves** | *(Optional)* Update existing `.sav` files so old Japanese saves load cleanly in the translated build. Originals are backed up automatically. | -> **`wolf` binary:** A prebuilt `wolf` CLI is bundled under `util/wolfdawn/bin//`, so no build step is needed on supported platforms. If no bundled binary is present for your platform, the tool falls back to compiling it from the vendored source with `cargo build --release`, which requires a [Rust toolchain](https://rustup.rs/); the freshly built binary is then saved into the bundle for reuse. A clear error is shown if neither a bundled binary nor `cargo` is available. +> **`wolf` binary:** Prebuilt `wolf` CLIs for Windows and Linux are bundled offline under `util/wolfdawn/bin//`, so no toolchain or build step is needed. If your platform's binary is missing, the tool downloads a prebuilt one from the [WolfDawn release page](https://gitgud.io/zero64801/wolfdawn) and caches it into that same folder for offline reuse. A clear error is shown if no bundled binary is present and the download can't be completed (e.g. no published binary for your platform). > **Legacy modules:** The older `Wolf RPG` / `Wolf RPG 2` modules (configured in the Engine Config tab) still exist for edge cases, but the WolfDawn workflow above is the recommended path. diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index 38b8678..3cc8d11 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -452,9 +452,9 @@ class WolfWorkflowTab(QWidget): "Pick the WOLF game's root folder (the one containing Game.exe and Data.wolf, " "or a loose Data/ folder). The tool unpacks the .wolf archives with WolfDawn, " "extracts all translatable text, and stages it in files/ ready to translate.\n\n" - "A prebuilt WolfDawn 'wolf' CLI ships with the tool, so no setup is needed. " - "(If no prebuilt binary exists for your platform, it is compiled once from " - "source with cargo, which requires Rust.)" + "A prebuilt WolfDawn 'wolf' CLI ships with the tool (Windows and Linux), so no " + "setup is needed. If your platform's binary is missing, it's downloaded once " + "from the WolfDawn release page and cached for offline use." )) row = QHBoxLayout() diff --git a/util/wolfdawn-master/.gitignore b/util/wolfdawn-master/.gitignore deleted file mode 100644 index 6118616..0000000 --- a/util/wolfdawn-master/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock.bak -/scratch -*.tmp -*.out -*.zip diff --git a/util/wolfdawn-master/Cargo.lock b/util/wolfdawn-master/Cargo.lock deleted file mode 100644 index 61876eb..0000000 --- a/util/wolfdawn-master/Cargo.lock +++ /dev/null @@ -1,3578 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - -[[package]] -name = "accesskit" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b76d84ee70e30a4a7e39ab9018e2b17a6a09e31084176cc7c0b2dec036ba45" -dependencies = [ - "enumn", - "serde", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.13.0", - "cc", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 2.0.18", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "ashpd" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.4", - "raw-window-handle", - "serde", - "serde_repr", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix 1.1.4", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.4", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.4", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.13.0", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" -dependencies = [ - "bitflags 2.13.0", - "polling", - "rustix 1.1.4", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.4", - "rustix 1.1.4", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.0", - "block2 0.6.2", - "libc", - "objc2 0.6.4", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "ecolor" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" -dependencies = [ - "bytemuck", - "emath", - "serde", -] - -[[package]] -name = "eframe" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "egui-winit", - "egui_glow", - "glow", - "glutin", - "glutin-winit", - "home", - "image", - "js-sys", - "log", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "ron", - "serde", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "winapi", - "windows-sys 0.52.0", - "winit", -] - -[[package]] -name = "egui" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" -dependencies = [ - "accesskit", - "ahash", - "emath", - "epaint", - "log", - "nohash-hasher", - "ron", - "serde", -] - -[[package]] -name = "egui-winit" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6" -dependencies = [ - "ahash", - "arboard", - "egui", - "log", - "raw-window-handle", - "serde", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", -] - -[[package]] -name = "egui_extras" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1" -dependencies = [ - "ahash", - "egui", - "enum-map", - "log", - "mime_guess2", -] - -[[package]] -name = "egui_glow" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26" -dependencies = [ - "ahash", - "bytemuck", - "egui", - "glow", - "log", - "memoffset", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "emath" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enum-map" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" -dependencies = [ - "enum-map-derive", - "serde", -] - -[[package]] -name = "enum-map-derive" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "epaint" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" -dependencies = [ - "ab_glyph", - "ahash", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot", - "serde", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glow" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" -dependencies = [ - "bitflags 2.13.0", - "cfg_aliases", - "cgl", - "dispatch2", - "glutin_egl_sys", - "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "once_cell", - "raw-window-handle", - "wayland-sys", - "windows-sys 0.52.0", - "x11-dl", -] - -[[package]] -name = "glutin-winit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" -dependencies = [ - "cfg_aliases", - "glutin", - "raw-window-handle", - "winit", -] - -[[package]] -name = "glutin_egl_sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" -dependencies = [ - "gl_generator", - "windows-sys 0.52.0", -] - -[[package]] -name = "glutin_glx_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" -dependencies = [ - "gl_generator", - "x11-dl", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libmimalloc-sys" -version = "0.1.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" -dependencies = [ - "cc", -] - -[[package]] -name = "libredox" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" -dependencies = [ - "bitflags 2.13.0", - "libc", - "plain", - "redox_syscall 0.8.1", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mimalloc" -version = "0.1.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" -dependencies = [ - "libmimalloc-sys", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess2" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" -dependencies = [ - "mime", - "phf", - "phf_shared", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "libc", - "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation 0.2.2", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.0", - "block2 0.6.2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2 0.6.4", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "dispatch", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.0", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.0", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.13.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "orbclient" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.6", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", - "unicase", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", - "unicase", -] - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pxfm" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" - -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "redox_syscall" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "rfd" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" -dependencies = [ - "ashpd", - "block2 0.6.2", - "dispatch2", - "js-sys", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "pollster", - "raw-window-handle", - "urlencoding", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64", - "bitflags 2.13.0", - "serde", - "serde_derive", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.13.0", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.13.0", - "calloop 0.14.4", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.4", - "thiserror 2.0.18", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" -dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" -dependencies = [ - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wayland-backend" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.4", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" -dependencies = [ - "bitflags 2.13.0", - "rustix 1.1.4", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.13.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" -dependencies = [ - "rustix 1.1.4", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" -dependencies = [ - "bitflags 2.13.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webbrowser" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" -dependencies = [ - "core-foundation 0.10.1", - "jni", - "log", - "ndk-context", - "objc2 0.6.4", - "objc2-foundation 0.3.2", - "url", - "web-sys", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winit" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" -dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.13.0", - "block2 0.5.1", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "smithay-client-toolkit 0.19.2", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wolf-archive" -version = "0.1.0" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "wolf-cli" -version = "0.1.0" -dependencies = [ - "wolf-archive", - "wolf-core", - "wolf-decompiler", - "wolf-formats", -] - -[[package]] -name = "wolf-core" -version = "0.1.0" - -[[package]] -name = "wolf-decompiler" -version = "0.1.0" -dependencies = [ - "encoding_rs", - "serde", - "serde_json", - "wolf-core", - "wolf-formats", -] - -[[package]] -name = "wolf-formats" -version = "0.1.0" -dependencies = [ - "wolf-archive", - "wolf-core", -] - -[[package]] -name = "wolf-gui" -version = "0.1.0" -dependencies = [ - "eframe", - "egui", - "egui_extras", - "encoding_rs", - "mimalloc", - "rfd", - "serde", - "serde_json", - "wolf-archive", - "wolf-core", - "wolf-decompiler", - "wolf-formats", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.4", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "xcursor" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.13.0", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.4", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" -dependencies = [ - "serde", - "winnow", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zvariant" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn", - "winnow", -] diff --git a/util/wolfdawn-master/Cargo.toml b/util/wolfdawn-master/Cargo.toml deleted file mode 100644 index 2de7afb..0000000 --- a/util/wolfdawn-master/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[workspace] -resolver = "2" -members = ["crates/*"] - -[workspace.package] -edition = "2021" -version = "0.1.0" -license = "MIT" -repository = "https://gitgud.io/zero64801/wolfdawn" - -# Hand-rolled LZ4/AES/Huffman are intentionally dependency-free; keep it that way -# in the lower layers. Encoding (SJIS<->UTF-8) deps belong only in the decompiler layer. - -[profile.release] -opt-level = 3 -lto = "thin" -codegen-units = 1 diff --git a/util/wolfdawn-master/README.md b/util/wolfdawn-master/README.md deleted file mode 100644 index 2f5b2c9..0000000 --- a/util/wolfdawn-master/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# WolfDawn - -WolfDawn turns Wolf RPG Editor game files into a readable, editable form and back. It unpacks the -`Data.wolf` archive, decompiles event code into readable WolfScript, pulls out translatable text and -puts it back, edits databases and save files, and packs everything into a runnable game again. - -It comes as a command line tool (`wolf`) and a desktop app (WolfDawn Studio). The GUI is built for -people who do not use a terminal. Every option is a checkbox or a dropdown and every file is picked -with a normal file dialog. - -## What it does - -- Unpack and repack `Data.wolf`. Supports the current DXArchive v8 container, the older VER5 and VER6 - format used by Wolf 2.x, and WolfPro and ChaCha20 encryption. -- Decompile maps and common events into editable WolfScript, then compile them back. Untouched code - stays byte for byte the same. -- Extract every player-facing string for translation and inject the finished translation back. -- A project glossary keeps database names (items, skills, enemies) consistent across every file that - looks them up, so a renamed monster does not break by-name lookups. -- Carry an existing translation into a fresh extraction when a game updates. You only translate the - lines that are actually new. -- Edit databases in a spreadsheet grid, edit `Game.dat` (title, fonts, messages, image paths), and - edit save files. -- Fix saves so a Japanese or old save loads in a translated build. This rewrites the baked game title - and refreshes the baked strings. It handles standard saves and the GamePro Pro save format. -- Verify that a file decodes and re-encodes without loss before you ship it. - -## WolfDawn Studio (GUI) - -Launch it with `wolf gui`, or run the `wolf-gui` binary directly. Open a game folder or a `Data.wolf` -and WolfDawn Studio reads the title, version, encoding, and font, then fills the relevant file lists -for each section. New files (for example after you unpack) show up on their own. - -Sections: - -- **Project** opens a game and shows its details. -- **Archive** unpacks `Data.wolf` to a folder and repacks a folder back, with encryption and format - options. -- **Decompile** turns a map or common event into WolfScript, lets you edit it (with Ctrl+F search), - and compiles it back. -- **Database** edits a database in a grid of rows and fields. -- **Game.dat** edits the title, fonts, messages, and image paths in a simple form. -- **Translation** extracts the whole `Data` folder into a source and translation grid plus a name - glossary, then injects it. The English punctuation cleanup and the code-safety guard are checkboxes. -- **Saves** opens a `.sav`, shows the format and baked strings, lets you edit the title and strings, - and re-encrypts. It can also batch-fix a whole `Save` folder. -- **Verify** round-trips a file or a whole data folder and reports pass or fail. -- **Settings** holds the theme and the default options. They persist between runs. - -## Command line - -``` -wolf unpack -o -wolf pack -o [--encrypt --version 0x14b | --like | --format ver5|ver6] - -wolf decompile [--mode edit] [-o out.wscript] -wolf compile --base -o - -wolf db-json [-o out] -wolf db-apply --base -o -wolf gamedat-json [-o out.json] -wolf gamedat-apply --base -o - -wolf strings-extract -o -wolf strings-inject --base -o [--allow-code-drift] [--en-punct] -wolf names-extract -o -wolf names-inject --data [-o ] [--allow-code-drift] [--en-punct] -wolf names-check ... -wolf translations-merge --old ... --new -o - -wolf save-update [-o ] [--title | --game ] [--translations ...] - -wolf verify-roundtrip -wolf verify-roundtrip --corpus - -wolf gui -``` - -Exit codes: `0` ok, `2` round-trip or usage failure, `3` merge conflict, `4` crypto failure. - -## A typical translation run - -1. `wolf unpack Data.wolf -o Data` to get an editable folder. -2. `wolf strings-extract` per file and `wolf names-extract Data -o names.json` to pull the text. In the - GUI the Translation section does the whole folder at once. -3. Translate the text. Pass `--en-punct` to convert Japanese punctuation to ASCII for English. -4. `wolf strings-inject` and `wolf names-inject` to write it back. -5. Rename `Data.wolf` and keep the edited `Data` folder next to `Game.exe`, or `wolf pack Data -o - Data.wolf` to rebuild the archive. -6. `wolf save-update` so existing saves still load in the translated build. - -## Build - -``` -cargo build --release -``` - -This produces `target/release/wolf` (the CLI) and `target/release/wolf-gui` (the desktop app). The -build is plain Rust with a small set of dependencies. The crypto, codecs, and binary format readers -are all hand written. - -## Running the tests - -``` -cargo test -``` - -That runs the unit tests, which need nothing extra. A handful of integration tests want real Wolf -game data (an unpacked `Data` folder, a couple of saves, a GamePro Pro save and its ground-truth -decrypt). That data is copyrighted and not bundled, so those tests skip themselves unless you point -them at a fixtures folder. - -Set `WOLFDAWN_TEST_DATA` to a folder laid out like this and the data-dependent tests run too: - -``` -/chamber/Data/... an unpacked game Data folder (BasicData, MapData) -/chamber/Data/BasicData/Game.dat -/chamber/Data/BasicData/CommonEvent.dat -/chamber/Data/BasicData/DataBase.project (+ DataBase.dat) -/chamber/Data/MapData/TitleMap.mps -/chamber/Data.wolf the packed archive (the unpack test) -/chamber/SaveData01.sav a standard save -/pachimon/SaveData01.sav a GamePro Pro save -/pachimon/decrypted.bin the ground-truth decrypt of that save -``` - -Any file that is missing just skips the test that needs it, so a partial fixtures folder is fine. -With the variable unset every one of these tests skips and the suite still passes. - -## Project layout - -- `wolf-core` holds the low level reader and writer, the LZ4 block codec, and CRC32. -- `wolf-archive` is the `Data.wolf` container and its encryption. -- `wolf-formats` reads and writes the binary maps, common events, databases, and `Game.dat`. -- `wolf-decompiler` is the WolfScript decompiler and compiler, the translation pipeline, the database - and `Game.dat` editors, and the save codecs. -- `wolf-cli` is the `wolf` binary. -- `wolf-gui` is WolfDawn Studio. - -## Notes - -The decompiler aims for a faithful round trip. Where a region of a file is not fully understood it is -preserved as raw bytes, so a recompiled file always loads. Saves and `Game.dat` round-trip byte for -byte when you do not change a field. The GamePro Pro save format is supported for marker-3 saves. A -few other GamePro save markers are detected and skipped rather than corrupted. diff --git a/util/wolfdawn-master/crates/wolf-archive/Cargo.toml b/util/wolfdawn-master/crates/wolf-archive/Cargo.toml deleted file mode 100644 index eb24eb6..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "wolf-archive" -edition.workspace = true -version.workspace = true -license.workspace = true - -# Folded in from the standalone `wolf_unpack` crate: the DXArchive container + WolfPro -# crypto layer (DXArchive v8, WolfPro 768-byte stream cipher + AES-128-CTR, DXLib Huffman -# + LZSS, CRC32 key derivation). The crypto/codecs are hand-rolled (no crates); the only -# dependency is `encoding_rs`, used solely to decode Shift-JIS archive *filenames*. - -[dependencies] -encoding_rs = "0.8" - -[lib] -name = "wolf_archive" -path = "src/lib.rs" - -[[bin]] -name = "wolf-unpack" -path = "src/bin/wolf-unpack.rs" diff --git a/util/wolfdawn-master/crates/wolf-archive/src/bin/wolf-unpack.rs b/util/wolfdawn-master/crates/wolf-archive/src/bin/wolf-unpack.rs deleted file mode 100644 index 2e2c8b7..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/bin/wolf-unpack.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! `wolf-unpack` CLI: thin wrapper over `wolf_archive::run`. - -fn main() -> std::io::Result<()> { - let args: Vec = std::env::args().collect(); - wolf_archive::run(&args) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/chacha20.rs b/util/wolfdawn-master/crates/wolf-archive/src/chacha20.rs deleted file mode 100644 index 8fd0916..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/chacha20.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! ChaCha20 stream cipher for Wolf "ChaCha2" archives (cryptVersion `0x64` / `0xC8`). -//! -//! Standard RFC 8439 ChaCha20 with the block counter initialised to 1 and a position-addressed -//! keystream, so any byte offset can be decrypted independently. Also has the Wolf-specific -//! [`key_setup`] used by cv `0xC8`. The core is verified against the RFC 8439 §2.3.2 test -//! vector. - -fn pack4(a: &[u8]) -> u32 { - u32::from_le_bytes([a[0], a[1], a[2], a[3]]) -} - -fn rotl32(x: u32, n: u32) -> u32 { - x.rotate_left(n) -} - -fn init_block(key: &[u8; 32], nonce: &[u8; 12]) -> [u32; 16] { - let magic = b"expand 32-byte k"; - let mut s = [0u32; 16]; - for i in 0..4 { - s[i] = pack4(&magic[i * 4..]); - } - for i in 0..8 { - s[4 + i] = pack4(&key[i * 4..]); - } - s[12] = 1; // counter starts at 1 (Wolf/RFC test-vector convention) - for i in 0..3 { - s[13 + i] = pack4(&nonce[i * 4..]); - } - s -} - -macro_rules! qr { - ($x:expr, $a:expr, $b:expr, $c:expr, $d:expr) => { - $x[$a] = $x[$a].wrapping_add($x[$b]); - $x[$d] = rotl32($x[$d] ^ $x[$a], 16); - $x[$c] = $x[$c].wrapping_add($x[$d]); - $x[$b] = rotl32($x[$b] ^ $x[$c], 12); - $x[$a] = $x[$a].wrapping_add($x[$b]); - $x[$d] = rotl32($x[$d] ^ $x[$a], 8); - $x[$c] = $x[$c].wrapping_add($x[$d]); - $x[$b] = rotl32($x[$b] ^ $x[$c], 7); - }; -} - -fn block_next(state: &mut [u32; 16], ks: &mut [u32; 16]) { - ks.copy_from_slice(state); - for _ in 0..10 { - qr!(ks, 0, 4, 8, 12); - qr!(ks, 1, 5, 9, 13); - qr!(ks, 2, 6, 10, 14); - qr!(ks, 3, 7, 11, 15); - qr!(ks, 0, 5, 10, 15); - qr!(ks, 1, 6, 11, 12); - qr!(ks, 2, 7, 8, 13); - qr!(ks, 3, 4, 9, 14); - } - for i in 0..16 { - ks[i] = ks[i].wrapping_add(state[i]); - } - state[12] = state[12].wrapping_add(1); - if state[12] == 0 { - state[13] = state[13].wrapping_add(1); - } -} - -#[inline] -fn ks_byte(ks: &[u32; 16], idx: usize) -> u8 { - (ks[idx / 4] >> (8 * (idx % 4))) as u8 -} - -/// XOR the ChaCha20 keystream into `bytes`, addressing the stream at absolute byte -/// `position` (mirrors `DXArchive::KeyConv`'s ChaCha20 branch). -pub fn crypt(bytes: &mut [u8], key: &[u8; 32], nonce: &[u8; 12], position: u32) { - let mut state = init_block(key, nonce); - let mut ks = [0u32; 16]; - let mut pos = 0usize; - let mut offset = (position % 64) as usize; - state[12] = state[12].wrapping_add(position / 64); - while pos < bytes.len() { - let steps = core::cmp::min(64 - offset, bytes.len() - pos); - block_next(&mut state, &mut ks); - for i in 0..steps { - bytes[pos + i] ^= ks_byte(&ks, offset + i); - } - pos += steps; - offset = 0; - } -} - -/// Wolf cv-`0xC8` key derivation: expand 4 header bytes into a 64-byte key buffer -/// (`key[0..32]` = ChaCha key, `key[34..46]` = nonce). -pub fn key_setup(data: [u8; 4]) -> [u8; 64] { - const MOD1: [u8; 4] = [0x3F, 0xA7, 0xD2, 0x1C]; - const MOD2: [u8; 4] = [0xB4, 0xE1, 0x9D, 0x58]; - const MOD3: [u8; 4] = [0x6A, 0x2B, 0x4C, 0x8E]; - let mut key = [0u8; 64]; - for i in 0..63usize { - let index = i % 4; - let mut temp = (data[index].wrapping_add(MOD2[index])) - ^ (MOD1[index] - .wrapping_add(i as u8) - .wrapping_add(16u8.wrapping_mul(i as u8))); - temp = if i % 2 == 0 { - (temp >> 5) | (temp << 3) - } else { - (temp >> 2) | (temp << 6) - }; - key[i] = !(temp ^ data[index] ^ MOD3[index]); - } - key -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rfc8439_keystream_block() { - // RFC 8439 §2.3.2: key 00..1f, nonce 00 00 00 09 00 00 00 4a 00 00 00 00, counter 1. - let mut key = [0u8; 32]; - for i in 0..32 { - key[i] = i as u8; - } - let nonce: [u8; 12] = [0, 0, 0, 9, 0, 0, 0, 0x4a, 0, 0, 0, 0]; - let mut buf = [0u8; 64]; // XOR into zeros -> keystream - crypt(&mut buf, &key, &nonce, 0); - let expected: [u8; 64] = [ - 0x10, 0xf1, 0xe7, 0xe4, 0xd1, 0x3b, 0x59, 0x15, 0x50, 0x0f, 0xdd, 0x1f, 0xa3, 0x20, - 0x71, 0xc4, 0xc7, 0xd1, 0xf4, 0xc7, 0x33, 0xc0, 0x68, 0x03, 0x04, 0x22, 0xaa, 0x9a, - 0xc3, 0xd4, 0x6c, 0x4e, 0xd2, 0x82, 0x64, 0x46, 0x07, 0x9f, 0xaa, 0x09, 0x14, 0xc2, - 0xd7, 0x05, 0xd9, 0x8b, 0x02, 0xa2, 0xb5, 0x12, 0x9c, 0xd1, 0xde, 0x16, 0x4e, 0xb9, - 0xcb, 0xd0, 0x83, 0xe8, 0xa2, 0x50, 0x3c, 0x4e, - ]; - assert_eq!( - buf, expected, - "ChaCha20 keystream must match RFC 8439 §2.3.2" - ); - } - - #[test] - fn position_addressing_matches_contiguous() { - // Decrypting in two position-addressed chunks == one contiguous pass. - let key = [7u8; 32]; - let nonce = [3u8; 12]; - let mut whole: Vec = (0..200).map(|i| (i * 13) as u8).collect(); - let mut split = whole.clone(); - crypt(&mut whole, &key, &nonce, 0); - let (a, b) = split.split_at_mut(80); - crypt(a, &key, &nonce, 0); - crypt(b, &key, &nonce, 80); - assert_eq!(whole, split); - } -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/codec.rs b/util/wolfdawn-master/crates/wolf-archive/src/codec.rs deleted file mode 100644 index bd14b7a..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/codec.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! DXLib codecs: the Huffman decoder and the LZSS ("DXA") decompressor. - -use crate::*; -use std::io; - -#[derive(Clone, Copy)] -pub(crate) struct HuffNode { - weight: u32, - child: [i32; 2], - parent: i32, - index: u8, - bit_num: u8, - bit_array: [u8; 32], -} - -/// MSB-first bit reader over the Huffman header bitstream. -pub(crate) struct BitReader<'a> { - data: &'a [u8], - byte: usize, - bit: u8, -} - -impl<'a> BitReader<'a> { - fn new(data: &'a [u8]) -> Self { - Self { - data, - byte: 0, - bit: 0, - } - } - - fn read(&mut self, bits: u8) -> io::Result { - let mut out = 0u64; - for i in 0..bits { - let b = *self - .data - .get(self.byte) - .ok_or_else(|| invalid("huffman bitstream ended early"))?; - out |= (((b >> (7 - self.bit)) & 1) as u64) << (bits - 1 - i); - self.bit += 1; - if self.bit == 8 { - self.byte += 1; - self.bit = 0; - } - } - Ok(out) - } - - fn bytes(&self) -> usize { - self.byte + usize::from(self.bit != 0) - } -} - -pub(crate) fn huffman_decode(src: &[u8]) -> io::Result> { - let mut br = BitReader::new(src); - let original_bits = br.read(6)? as u8 + 1; - let original_size = br.read(original_bits)? as usize; - if original_size > MAX_DECODE_SIZE { - return Err(invalid(format!( - "huffman output is implausibly large: {original_size}" - ))); - } - let press_bits = br.read(6)? as u8 + 1; - let _press_size = br.read(press_bits)?; - - let mut weight = [0u16; 256]; - let mut bit_num = (br.read(3)? as u8 + 1) * 2; - let _ = br.read(1)?; - let mut save = br.read(bit_num)? as u16; - weight[0] = save; - for i in 1..256 { - bit_num = (br.read(3)? as u8 + 1) * 2; - let minus = br.read(1)?; - save = br.read(bit_num)? as u16; - weight[i] = if minus == 1 { - weight[i - 1].wrapping_sub(save) - } else { - weight[i - 1].wrapping_add(save) - }; - } - - let head_size = br.bytes(); - if head_size >= src.len() { - return Err(invalid("huffman body missing")); - } - - let mut nodes = vec![ - HuffNode { - weight: 0, - child: [-1, -1], - parent: -1, - index: 0, - bit_num: 0, - bit_array: [0; 32], - }; - 511 - ]; - for i in 0..511 { - nodes[i].weight = if i < 256 { weight[i] as u32 } else { 0 }; - } - - let mut data_num = 256usize; - let mut node_num = 256usize; - while data_num > 1 { - let mut min1: i32 = -1; - let mut min2: i32 = -1; - let mut valid = 0usize; - let mut node_index = 0usize; - while valid < data_num { - if nodes[node_index].parent != -1 { - node_index += 1; - continue; - } - valid += 1; - if min1 == -1 || nodes[min1 as usize].weight > nodes[node_index].weight { - min2 = min1; - min1 = node_index as i32; - } else if min2 == -1 || nodes[min2 as usize].weight > nodes[node_index].weight { - min2 = node_index as i32; - } - node_index += 1; - } - - let a = min1 as usize; - let b = min2 as usize; - nodes[node_num].weight = nodes[a].weight + nodes[b].weight; - nodes[node_num].child = [min1, min2]; - nodes[a].index = 0; - nodes[b].index = 1; - nodes[a].parent = node_num as i32; - nodes[b].parent = node_num as i32; - node_num += 1; - data_num -= 1; - } - - for i in 0..510 { - let mut temp = [0u8; 32]; - let mut temp_index = 0usize; - let mut temp_count = 0u8; - let mut n = i; - while nodes[n].parent != -1 { - if temp_count == 8 { - temp_count = 0; - temp_index += 1; - } - temp[temp_index] <<= 1; - temp[temp_index] |= nodes[n].index; - temp_count += 1; - nodes[i].bit_num += 1; - n = nodes[n].parent as usize; - } - - let mut bit_count = 0u8; - let mut bit_index = 0usize; - while temp_index < temp.len() { - if bit_count == 8 { - bit_count = 0; - bit_index += 1; - } - nodes[i].bit_array[bit_index] |= (temp[temp_index] & 1) << bit_count; - temp[temp_index] >>= 1; - temp_count = temp_count.wrapping_sub(1); - if temp_count == 0 { - if temp_index == 0 { - break; - } - temp_index -= 1; - temp_count = 8; - } - bit_count += 1; - } - } - - let mut bitmask = [0u16; 9]; - for i in 0..9 { - bitmask[i] = (1u16 << (i + 1)) - 1; - } - let mut node_index_table = [-1i32; 512]; - for i in 0..512usize { - for j in 0..510usize { - let bn = nodes[j].bit_num; - if bn == 0 || bn > 9 { - continue; - } - let bit_array_01 = nodes[j].bit_array[0] as u16 | ((nodes[j].bit_array[1] as u16) << 8); - if (i as u16 & bitmask[bn as usize - 1]) == (bit_array_01 & bitmask[bn as usize - 1]) { - node_index_table[i] = j as i32; - break; - } - } - } - - let press = &src[head_size..]; - let mut out = vec![0u8; original_size]; - let mut press_counter = 0usize; - let mut press_bit_counter = 0u8; - let mut press_bit_data = *press.first().ok_or_else(|| invalid("empty huffman body"))? as u16; - - for dest_counter in 0..original_size { - let mut node_index: i32; - if dest_counter >= original_size.saturating_sub(17) { - node_index = 510; - } else { - if press_bit_counter == 8 { - press_counter += 1; - press_bit_data = *press - .get(press_counter) - .ok_or_else(|| invalid("huffman body ended early"))? - as u16; - press_bit_counter = 0; - } - let next = *press.get(press_counter + 1).unwrap_or(&0) as u16; - press_bit_data = (press_bit_data | (next << (8 - press_bit_counter))) & 0x1ff; - node_index = node_index_table[press_bit_data as usize]; - if node_index < 0 { - return Err(invalid("bad huffman lookup")); - } - press_bit_counter += nodes[node_index as usize].bit_num; - if press_bit_counter >= 16 { - press_counter += 2; - press_bit_counter -= 16; - press_bit_data = - (*press.get(press_counter).unwrap_or(&0) as u16) >> press_bit_counter; - } else if press_bit_counter >= 8 { - press_counter += 1; - press_bit_counter -= 8; - press_bit_data = - (*press.get(press_counter).unwrap_or(&0) as u16) >> press_bit_counter; - } else { - press_bit_data >>= nodes[node_index as usize].bit_num; - } - } - - while node_index > 255 { - if press_bit_counter == 8 { - press_counter += 1; - press_bit_data = *press - .get(press_counter) - .ok_or_else(|| invalid("huffman body ended early"))? - as u16; - press_bit_counter = 0; - } - let index = (press_bit_data & 1) as usize; - press_bit_data >>= 1; - press_bit_counter += 1; - node_index = nodes[node_index as usize].child[index]; - } - out[dest_counter] = node_index as u8; - } - - Ok(out) -} - -pub(crate) fn dxa_decode(src: &[u8]) -> io::Result> { - if src.len() < 9 { - return Err(invalid("lz source too small")); - } - let dest_size = read_u32(src) as usize; - if dest_size > MAX_DECODE_SIZE { - return Err(invalid(format!( - "lz output is implausibly large: {dest_size}" - ))); - } - let packed_total = read_u32(&src[4..]) as usize; - if packed_total < 9 || packed_total > src.len() { - return Err(invalid("lz packed size is invalid")); - } - let mut src_left = packed_total - 9; - let key_code = src[8]; - let mut sp = 9usize; - let mut out = Vec::with_capacity(dest_size); - - while src_left > 0 { - let b = *src - .get(sp) - .ok_or_else(|| invalid("lz source ended early"))?; - if b != key_code { - out.push(b); - sp += 1; - src_left -= 1; - continue; - } - - let b1 = *src - .get(sp + 1) - .ok_or_else(|| invalid("lz source ended early"))?; - if b1 == key_code { - out.push(key_code); - sp += 2; - src_left = src_left.saturating_sub(2); - continue; - } - - let mut code = b1; - if code > key_code { - code -= 1; - } - sp += 2; - src_left = src_left.saturating_sub(2); - - let mut conbo = (code >> 3) as usize; - if (code & 4) != 0 { - conbo |= (*src - .get(sp) - .ok_or_else(|| invalid("lz source ended early"))? as usize) - << 5; - sp += 1; - src_left = src_left.saturating_sub(1); - } - conbo += 4; - - let index_size = code & 3; - let mut index = match index_size { - 0 => { - let v = *src - .get(sp) - .ok_or_else(|| invalid("lz source ended early"))? - as usize; - sp += 1; - src_left = src_left.saturating_sub(1); - v - } - 1 => { - if sp + 2 > src.len() { - return Err(invalid("lz source ended early")); - } - let v = read_u16(&src[sp..]) as usize; - sp += 2; - src_left = src_left.saturating_sub(2); - v - } - 2 => { - if sp + 3 > src.len() { - return Err(invalid("lz source ended early")); - } - let v = read_u16(&src[sp..]) as usize | ((src[sp + 2] as usize) << 16); - sp += 3; - src_left = src_left.saturating_sub(3); - v - } - _ => return Err(invalid("unsupported lz index size")), - }; - index += 1; - if index > out.len() { - return Err(invalid("lz back-reference before output start")); - } - for _ in 0..conbo { - let v = out[out.len() - index]; - out.push(v); - } - } - - if out.len() != dest_size { - return Err(invalid(format!( - "lz decoded {} bytes, expected {}", - out.len(), - dest_size - ))); - } - Ok(out) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/crypto.rs b/util/wolfdawn-master/crates/wolf-archive/src/crypto.rs deleted file mode 100644 index 1bca797..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/crypto.rs +++ /dev/null @@ -1,726 +0,0 @@ -//! WolfPro crypto: the 7-byte DXLib key (`key_create`/`key_conv`/`crc32`), the 768-byte -//! stream cipher + its key schedule (`wolf_init_key`/`wolf_crypt`), the header-address -//! scramble, and the AES-128-CTR pass used by v3.5+ archives. - -use crate::codec::{dxa_decode, huffman_decode}; -use crate::*; - -pub(crate) fn key_create(source: &[u8]) -> [u8; KEY_BYTES] { - let mut src = nul_terminated(source).to_vec(); - if src.len() < 4 { - src.extend_from_slice(DEFAULT_DXLIB_KEY); - } - let even: Vec = src.iter().step_by(2).copied().collect(); - let odd: Vec = src.iter().skip(1).step_by(2).copied().collect(); - let c0 = crc32(&even); - let c1 = crc32(&odd); - [ - c0 as u8, - (c0 >> 8) as u8, - (c0 >> 16) as u8, - (c0 >> 24) as u8, - c1 as u8, - (c1 >> 8) as u8, - (c1 >> 16) as u8, - ] -} - -pub(crate) fn nul_terminated(source: &[u8]) -> &[u8] { - source - .iter() - .position(|&b| b == 0) - .map(|end| &source[..end]) - .unwrap_or(source) -} - -pub(crate) fn crc32(data: &[u8]) -> u32 { - let mut table = [0u32; 256]; - for i in 0..256u32 { - let mut v = i; - for _ in 0..8 { - let b = v & 1; - v >>= 1; - if b != 0 { - v ^= 0xedb88320; - } - } - table[i as usize] = v; - } - let mut crc = 0xffff_ffffu32; - for &b in data { - crc = table[((crc ^ b as u32) & 0xff) as usize] ^ (crc >> 8); - } - crc ^ 0xffff_ffff -} - -pub(crate) fn key_conv(data: &mut [u8], key: &[u8; KEY_BYTES], pos: usize) { - for (i, byte) in data.iter_mut().enumerate() { - *byte ^= key[(pos + i) % KEY_BYTES]; - } -} - -pub(crate) fn is_new_wolf_crypt(crypt_version: u16) -> bool { - (crypt_version >= 331 && crypt_version < 1000) || crypt_version >= 1010 -} - -pub(crate) fn is_wolf_v35(crypt_version: u16) -> bool { - (crypt_version >= 0x15e && crypt_version < 0x3e8) || crypt_version >= 0x3fc -} - -pub(crate) fn candidate_matches_wolf_version(label: &str, crypt_version: u16) -> bool { - label == "requested" - || matches!( - (crypt_version, label), - (0x14b, "wolf-v3.31") - | (0x15e, "wolf-v3.50") - | (0x64, "wolf-chacha2") - | (0xc8, "wolf-chacha2") - ) -} - -#[derive(Clone, Copy)] -pub(crate) struct MsvcRand { - state: u32, -} - -impl MsvcRand { - fn new(seed: u32) -> Self { - Self { state: seed } - } - - fn next(&mut self) -> u32 { - self.state = self.state.wrapping_mul(214013).wrapping_add(2531011); - (self.state >> 16) & 0x7fff - } -} - -pub(crate) fn wolf_init_key( - crypt_version: u16, - pwd: &[u8; 15], - key2: Option<&[u8]>, - other: bool, - key_string: &[u8], -) -> [u8; 768] { - let mut key = [0u8; 768]; - let mut fac = [0u8; 3]; - - let s0 = pwd[2]; - let s1 = pwd[5]; - let s2 = pwd[12]; - let mut s3 = 0u8; - - if !other { - let len = pwd[11] / 3; - for i in 0..len { - s3 = i ^ (s3 ^ pwd[i as usize % 15]).rotate_right(3); - } - } else { - let len = pwd[8] / 4; - for i in 0..len { - s3 = i ^ (s3 ^ pwd[i as usize % 15]).rotate_right(2); - } - } - - let seed = (s0 as u32) * (s1 as u32) + (s2 as u32) + (s3 as u32); - let mut rng = MsvcRand::new(seed); - - fac[s3 as usize % 3] = (rng.next() % 256) as u8; - - if !other && is_wolf_v35(crypt_version) { - fac[1] = (rng.next() % 0xfb) as u8; - } - - for i in 0..256 { - let rn = (rng.next() & 0xffff) as u16; - key[i] = fac[0] ^ (rng.next() as u8); - key[i + 256] = fac[1] ^ (rn >> 8) as u8; - key[i + 512] = fac[2] ^ rn as u8; - } - - if let Some(key2) = key2 { - for j in 0..128 { - let rn = (rng.next() & 0xffff) as u16; - key[j] ^= s3 ^ key2[2] ^ (rn >> 8) as u8; - key[j + 256] ^= s3 ^ key2[0] ^ rn as u8; - } - } - - if other { - let mut salt = [0u8; 128]; - let salt_source = if crypt_version == 0x15e { - b"958".as_slice() - } else { - nul_terminated(key_string) - }; - calc_wolf_salt(salt_source, &mut salt); - - let mut mod_factor = 7u8; - if is_wolf_v35(crypt_version) { - s3 = s3.wrapping_add(0x22); - mod_factor = 16; - } - - for i in 0..3usize { - let mut t = s3 as i32; - for j in 0..256usize { - let mut skip = false; - let cur_s = salt[j & 0x7f]; - let cur_s2 = salt[(j + i) % 0x80]; - let cur_k = key[i * 256 + j]; - let s_x_k = cur_s ^ cur_k; - let round = (((cur_s2 as u16) | ((cur_s as u16) << 8)) % mod_factor as u16) as u8; - let mut new_k = s_x_k; - - match round { - 1 => { - if cur_s2 % 0x0b == 0 { - new_k = cur_k; - } - } - 2 => { - if cur_s % 0x1d == 0 { - new_k = !s_x_k; - } - } - 3 => { - if ((round as usize + j) % 0x25) == 0 { - new_k = cur_s2 ^ s_x_k; - } - } - 4 => { - if ((cur_s as u16 + cur_s2 as u16) % 97) == 0 { - new_k = cur_s.wrapping_add(s_x_k); - } - } - 5 => { - if ((j * round as usize) % 0x7b) == 0 { - new_k = s_x_k ^ t as u8; - } - } - 6 => { - if cur_s == 0xff && cur_s2 == 0 { - new_k = 0; - skip = true; - } - } - 7 => { - if crypt_version >= 0x154 - && !(crypt_version > 0x3e8 && crypt_version < 0x3fc) - && (((round as usize + j) % 0x33) == 0 || crypt_version >= 0x3fc) - { - new_k ^= cur_s; - } - } - 8 => { - if crypt_version >= 0x154 - && !(crypt_version > 0x3e8 && crypt_version < 0x3fc) - && ((cur_s % 0x1d) == 0 || crypt_version >= 0x3fc) - { - new_k ^= cur_s; - } - } - _ => {} - } - - if ((j + i) % (cur_s as usize % 5 + 1)) == 0 { - new_k ^= t as u8; - } else if skip { - new_k = !s_x_k; - } - - key[i * 256 + j] = new_k; - t += i as i32; - } - } - } - - key -} - -pub(crate) fn calc_wolf_salt(source: &[u8], salt: &mut [u8; 128]) { - let source = if source.is_empty() { - DEFAULT_DXLIB_KEY - } else { - source - }; - for i in 0..128 { - salt[i] = (i / source.len()) as u8 + source[i % source.len()]; - } -} - -pub(crate) fn wolf_crypt(key: &[u8; 768], data: &mut [u8], start: usize, crypt_version: u16) { - let mut v1 = start % 256; - let mut v2 = (start / 256) % 256; - let mut v3 = (start / 0x10000) % 256; - - if is_wolf_v35(crypt_version) { - let mut modded = [0u8; 512]; - for i in 0..512 { - modded[i] = key[i % 256] ^ (7u8.wrapping_mul(i as u8)); - } - for byte in data { - *byte ^= modded[v1] ^ modded[v2 + 256]; - v1 += 1; - if v1 == 256 { - v1 = 0; - v2 = (v2 + 1) % 256; - } - } - } else { - for byte in data { - *byte ^= key[v1] ^ key[v2 + 256] ^ key[v3 + 512]; - v1 += 1; - if v1 == 256 { - v1 = 0; - v2 += 1; - if v2 == 256 { - v2 = 0; - v3 = (v3 + 1) % 256; - } - } - } - } -} - -pub(crate) fn wolf_crypt_addresses(data: &mut [u8], pwd: &[u8; 15], crypt_version: u16) { - if data.len() < 64 { - return; - } - - if is_wolf_v35(crypt_version) { - let seed = 0x0c + pwd[9] as u32 * pwd[10] as u32 + pwd[3] as u32; - let mut rng = MsvcRand::new(seed); - let mut word = 4usize; - for _ in 0..2 { - for j in (0..4).rev() { - xor_u16_at(data, (word + j) * 2, (rng.next() & 0xffff) as u16); - } - word += 4; - } - let r0 = (rng.next() as u64) << 17; - let r1 = (rng.next() as u64) << 31; - let v0 = ((r0 & 0xffff_ffff) | (r1 & 0xffff_ffff) | rng.next() as u64) as u32; - let v1 = ((r0 >> 32) | (r1 >> 32)) as u32; - xor_u32_at(data, word * 2, v0); - xor_u32_at(data, word * 2 + 4, v1); - word += 4; - for j in (0..4).rev() { - xor_u16_at(data, (word + j) * 2, (rng.next() & 0xffff) as u16); - } - } else { - let seed = pwd[0] as u32 + pwd[7] as u32 * pwd[12] as u32; - let mut rng = MsvcRand::new(seed); - let mut word = 4usize; - for _ in 0..4 { - for j in (0..4).rev() { - xor_u16_at(data, (word + j) * 2, (rng.next() & 0xffff) as u16); - } - word += 4; - } - } -} - -pub(crate) fn xor_u16_at(data: &mut [u8], off: usize, value: u16) { - if off + 2 <= data.len() { - let v = read_u16(&data[off..]) ^ value; - data[off..off + 2].copy_from_slice(&v.to_le_bytes()); - } -} - -pub(crate) fn xor_u32_at(data: &mut [u8], off: usize, value: u32) { - if off + 4 <= data.len() { - let v = read_u32(&data[off..]) ^ value; - data[off..off + 4].copy_from_slice(&v.to_le_bytes()); - } -} - -pub(crate) fn wolf_aes_body_size( - file_size: usize, - crypt_version: u16, - pwd: &[u8; 15], - key2: Option<&[u8]>, -) -> usize { - let body_len = file_size.saturating_sub(64); - if body_len < 0x400 { - return 0; - } - if !is_wolf_v35(crypt_version) { - return 0x400; - } - - let mut seed = if crypt_version >= 1020 { - let key2 = key2.unwrap_or(&[0, 0, 0, 0]); - key2[0] as u32 * key2[1] as u32 + pwd[2] as u32 * pwd[4] as u32 + pwd[11] as u32 - } else { - pwd[2] as u32 * pwd[4] as u32 + pwd[12] as u32 - }; - if seed == 0 { - seed = 1; - } - let mut xs = XorShift32::new(seed); - let first = xs.next() % 500 + 800; - if file_size >= first as usize { - xs.next(); - } - let mut body_size = body_len; - let limit = xs.next() % 500 + 800; - if body_size >= limit as usize { - body_size = (xs.next() % 500 + 800) as usize; - } - body_size -} - -pub(crate) struct XorShift32 { - state: u32, -} - -impl XorShift32 { - fn new(seed: u32) -> Self { - Self { state: seed } - } - - fn next(&mut self) -> u32 { - self.state ^= self.state << 0x0b; - self.state ^= self.state >> 0x13; - self.state ^= self.state << 0x07; - self.state - } -} - -pub(crate) fn read_wolf_table(data: &[u8], head: &DxHead, wolf: &WolfContext) -> Option> { - let start = head.name_table_start as usize; - let no_key = (head.flags & 1) != 0; - let no_head_press = (head.flags & 2) != 0; - - if no_head_press { - let mut table = read_wolf_archive_slice(data, start, head.head_size as usize, wolf)?; - if !no_key { - wolf_crypt(&wolf.special_key, &mut table, 0, wolf.crypt_version); - } - Some(table) - } else { - let huff_len = data.len().checked_sub(start)?; - let mut huff = read_wolf_archive_slice(data, start, huff_len, wolf)?; - if !no_key { - wolf_crypt(&wolf.special_key, &mut huff, 0, wolf.crypt_version); - } - let lz = huffman_decode(&huff).ok()?; - dxa_decode(&lz).ok() - } -} - -pub(crate) fn read_wolf_archive_slice( - data: &[u8], - start: usize, - len: usize, - wolf: &WolfContext, -) -> Option> { - let end = start.checked_add(len)?; - let mut out = data.get(start..end)?.to_vec(); - let file_size = data.len(); - - apply_wolf_stream_overlap( - &mut out, - start, - 64, - file_size.saturating_sub(64), - &wolf.other_key, - wolf.crypt_version, - ); - - let pass1_end = 64usize.saturating_add(wolf.body_size).min(file_size); - apply_aes_overlap(&mut out, start, 64, pass1_end, &wolf.aes_round_key, 0); - - if wolf.name_table_start < file_size { - let body_blocks = (wolf.body_size + AES_BLOCK_LEN - 1) / AES_BLOCK_LEN; - apply_aes_overlap( - &mut out, - start, - wolf.name_table_start, - file_size, - &wolf.aes_round_key, - body_blocks, - ); - } - - Some(out) -} - -pub(crate) fn apply_wolf_stream_overlap( - out: &mut [u8], - out_start: usize, - range_start: usize, - range_end: usize, - key: &[u8; 768], - crypt_version: u16, -) { - let out_end = out_start.saturating_add(out.len()); - let begin = out_start.max(range_start); - let end = out_end.min(range_end); - if begin >= end { - return; - } - let local = begin - out_start; - wolf_crypt( - key, - &mut out[local..local + (end - begin)], - begin, - crypt_version, - ); -} - -pub(crate) fn apply_aes_overlap( - out: &mut [u8], - out_start: usize, - range_start: usize, - range_end: usize, - round_key: &[u8; AES_ROUND_KEY_SIZE], - iv_block_offset: usize, -) { - let out_end = out_start.saturating_add(out.len()); - let begin = out_start.max(range_start); - let end = out_end.min(range_end); - if begin >= end { - return; - } - let local = begin - out_start; - let stream_offset = begin - range_start; - aes_ctr_xcrypt_at( - &mut out[local..local + (end - begin)], - round_key, - stream_offset, - iv_block_offset, - ); -} - -pub(crate) const AES_SBOX: [u8; 256] = [ - 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, - 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, - 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, - 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, - 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, - 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, - 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, - 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, - 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, - 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, - 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, - 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, - 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, - 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, - 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, - 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, -]; -pub(crate) const AES_RCON: [u8; 11] = [ - 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, -]; - -pub(crate) fn wolf_aes_init_round_key( - pwd: &[u8; 15], - pro_key: Option<&[u8]>, - crypt_version: u16, -) -> [u8; AES_ROUND_KEY_SIZE] { - let zero = [0u8; 4]; - let pro = pro_key.unwrap_or(&zero); - let mut key = [0u8; AES_BLOCK_LEN]; - let mut iv = [0u8; AES_BLOCK_LEN]; - - if is_wolf_v35(crypt_version) { - for i in 0..15usize { - let pro_elem = pro[i % 4]; - let key_idx = ((i * (pro_elem as usize % 5 + 7)) ^ (3 * pwd[i] as usize)) % 15; - let iv_idx = (i * (pro[(i + 1) % 4] as usize % 7 + 0x0b) - ^ (5 * pwd[(i + 3) % 15] as usize)) - % 15; - - key[i] ^= ((i as u8 ^ pro_elem) - .wrapping_add(pwd[key_idx].wrapping_shl((i % 3) as u32))) - % 0xfb; - iv[i] ^= (pwd[iv_idx] - .wrapping_shr((i % 2) as u32) - .wrapping_add((i * i) as u8 ^ pro[(i + 2) % 4])) - % 0xf6; - key[15] ^= (7u16 * (pwd[i].wrapping_add((i as u8 + 1) ^ pro_elem)) as u16 % 0xfd) as u8; - iv[15] ^= (11u16 * (pwd[i].wrapping_sub((i as u8 * 2) ^ pro[(i + 2) % 4])) as u16 - % 0x100) as u8; - } - } else if crypt_version == 0x3f2 { - for i in 0..15usize { - key[i] ^= pwd[(i * 7) % 15] - .wrapping_add(pro[i & 3]) - .wrapping_mul((i * i) as u8); - iv[i] ^= pwd[(i * 11) % 15] - .wrapping_add(pro[(i + 2) % 4]) - .wrapping_sub((i * i) as u8); - key[15] ^= (i as u8) - .wrapping_mul(3) - .wrapping_add(pwd[i]) - .wrapping_add(pro[i & 3]); - iv[15] ^= (i as u8) - .wrapping_mul(5) - .wrapping_add(pwd[i]) - .wrapping_add(pro[(i + 2) % 4]); - } - } else { - for i in 0..15usize { - key[i] ^= pwd[(i * 7) % 15].wrapping_add((i * i) as u8); - iv[i] ^= pwd[(i * 11) % 15].wrapping_sub((i * i) as u8); - key[15] ^= pwd[i].wrapping_add((i * 3) as u8); - iv[15] ^= pwd[i].wrapping_add((i * 5) as u8); - } - } - - key[0] ^= pro[0]; - iv[10] ^= pro[0]; - key[4] ^= pro[1]; - iv[1] ^= pro[1]; - key[8] ^= pro[2]; - iv[4] ^= pro[2]; - key[12] ^= pro[3]; - iv[7] ^= pro[3]; - - let mut round_key = [0u8; AES_ROUND_KEY_SIZE]; - aes_key_expansion(&mut round_key[..AES_KEY_EXP_SIZE], &key); - round_key[AES_KEY_EXP_SIZE..].copy_from_slice(&iv); - round_key -} - -pub(crate) fn aes_key_expansion(round_key: &mut [u8], key: &[u8; AES_BLOCK_LEN]) { - for i in 0..4 { - round_key[i * 4..i * 4 + 4].copy_from_slice(&key[i * 4..i * 4 + 4]); - } - - for i in 4..44usize { - let mut temp = [ - round_key[(i - 1) * 4], - round_key[(i - 1) * 4 + 1], - round_key[(i - 1) * 4 + 2], - round_key[(i - 1) * 4 + 3], - ]; - - if i % 4 == 0 { - temp.rotate_left(1); - temp[0] = AES_SBOX[temp[0] as usize] ^ AES_RCON[i / 4]; - temp[1] = AES_SBOX[temp[1] as usize] >> 4; - temp[2] = !AES_SBOX[temp[2] as usize]; - temp[3] = AES_SBOX[temp[3] as usize].rotate_right(7); - } - - for j in 0..4 { - round_key[i * 4 + j] = round_key[(i - 4) * 4 + j] ^ temp[j]; - } - } -} - -pub(crate) fn aes_ctr_xcrypt_at( - data: &mut [u8], - round_key: &[u8; AES_ROUND_KEY_SIZE], - stream_offset: usize, - iv_block_offset: usize, -) { - let mut local_key = *round_key; - let block_offset = stream_offset / AES_BLOCK_LEN + iv_block_offset; - aes_advance_iv(&mut local_key[AES_KEY_EXP_SIZE..], block_offset); - - let mut state = [0u8; AES_BLOCK_LEN]; - let rem = stream_offset % AES_BLOCK_LEN; - let mut bi = AES_BLOCK_LEN; - if rem != 0 { - aes_next_ctr_block(&mut local_key, &mut state); - bi = rem; - } - - for byte in data { - if bi == AES_BLOCK_LEN { - aes_next_ctr_block(&mut local_key, &mut state); - bi = 0; - } - *byte ^= state[bi]; - bi += 1; - } -} - -pub(crate) fn aes_next_ctr_block( - round_key: &mut [u8; AES_ROUND_KEY_SIZE], - state: &mut [u8; AES_BLOCK_LEN], -) { - state.copy_from_slice(&round_key[AES_KEY_EXP_SIZE..]); - aes_cipher(state, &round_key[..AES_KEY_EXP_SIZE]); - aes_increment_iv(&mut round_key[AES_KEY_EXP_SIZE..]); -} - -pub(crate) fn aes_advance_iv(iv: &mut [u8], blocks: usize) { - for _ in 0..blocks { - aes_increment_iv(iv); - } -} - -pub(crate) fn aes_increment_iv(iv: &mut [u8]) { - for byte in iv.iter_mut().rev() { - if *byte == 0xff { - *byte = 0; - } else { - *byte = byte.wrapping_add(1); - break; - } - } -} - -pub(crate) fn aes_cipher(state: &mut [u8; AES_BLOCK_LEN], round_key: &[u8]) { - aes_add_round_key(state, 0, round_key); - for round in 1..10u8 { - aes_sub_bytes(state); - aes_shift_rows(state); - aes_mix_columns(state); - aes_add_round_key(state, round, round_key); - } - aes_sub_bytes(state); - aes_shift_rows(state); - aes_add_round_key(state, 10, round_key); -} - -pub(crate) fn aes_add_round_key(state: &mut [u8; AES_BLOCK_LEN], round: u8, round_key: &[u8]) { - let start = round as usize * AES_BLOCK_LEN; - for i in 0..AES_BLOCK_LEN { - state[i] ^= round_key[start + i]; - } -} - -pub(crate) fn aes_sub_bytes(state: &mut [u8; AES_BLOCK_LEN]) { - for byte in state { - *byte = AES_SBOX[*byte as usize]; - } -} - -pub(crate) fn aes_shift_rows(state: &mut [u8; AES_BLOCK_LEN]) { - let temp = state[1]; - state[1] = state[5]; - state[5] = state[9]; - state[9] = state[13]; - state[13] = temp; - - state.swap(2, 10); - state.swap(6, 14); - - let temp = state[3]; - state[3] = state[15]; - state[15] = state[11]; - state[11] = state[7]; - state[7] = temp; -} - -pub(crate) fn aes_xtime(x: u8) -> u8 { - (x << 1) ^ (((x >> 7) & 1) * 0x1b) -} - -pub(crate) fn aes_mix_columns(state: &mut [u8; AES_BLOCK_LEN]) { - for col in 0..4 { - let base = col * 4; - let t = state[base]; - let tmp = state[base] ^ state[base + 1] ^ state[base + 2] ^ state[base + 3]; - state[base] ^= tmp ^ aes_xtime(state[base + 1] ^ state[base]); - state[base + 1] ^= tmp ^ aes_xtime(state[base + 2] ^ state[base + 1]); - state[base + 2] ^= tmp ^ aes_xtime(state[base + 2] ^ state[base + 3]); - state[base + 3] ^= tmp ^ aes_xtime(state[base + 3] ^ t); - } -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/common.rs b/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/common.rs deleted file mode 100644 index 977ec3b..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/common.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Shared primitives for the DXArchive VER5/VER6 (Wolf 2.0x) decoders: the 12-byte key -//! derivation and `KeyConv`, the keycode-LZSS decompressor, and file-name extraction. These -//! are byte-for-byte identical between VER5 and VER6. Only the table/struct layout differs. - -pub(super) const KEYLEN: usize = 12; -const MIN_COMPRESS: usize = 4; - -/// `KeyCreate`: loop the source string to 12 bytes, then a fixed per-byte transform. -pub(super) fn key_create(source: &[u8]) -> [u8; KEYLEN] { - let mut k = [0xaau8; KEYLEN]; - if !source.is_empty() { - for (i, b) in k.iter_mut().enumerate() { - *b = source[i % source.len()]; - } - } - k[0] = !k[0]; - k[1] = (k[1] >> 4) | (k[1] << 4); - k[2] ^= 0x8a; - k[3] = !((k[3] >> 4) | (k[3] << 4)); - k[4] = !k[4]; - k[5] ^= 0xac; - k[6] = !k[6]; - k[7] = !((k[7] >> 3) | (k[7] << 5)); - k[8] = (k[8] >> 5) | (k[8] << 3); - k[9] ^= 0x7f; - k[10] = ((k[10] >> 4) | (k[10] << 4)) ^ 0xd6; - k[11] ^= 0xcc; - k -} - -/// XOR `buf` with the key starting at stream position `pos` (DXLib `KeyConv`). -pub(super) fn key_conv(key: &[u8; KEYLEN], buf: &[u8], pos: usize) -> Vec { - buf.iter() - .enumerate() - .map(|(i, &b)| b ^ key[(pos + i) % KEYLEN]) - .collect() -} - -pub(super) fn u32at(d: &[u8], o: usize) -> Option { - d.get(o..o + 4) - .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])) -} - -pub(super) fn u64at(d: &[u8], o: usize) -> Option { - d.get(o..o + 8) - .map(|b| u64::from_le_bytes(b.try_into().unwrap())) -} - -/// The original (display) file name: at `name_addr + 4 + packs*4`, NUL-terminated, Shift-JIS. -pub(super) fn name(d: &[u8], name_addr: usize) -> Option { - let packs = u16::from_le_bytes([*d.get(name_addr)?, *d.get(name_addr + 1)?]) as usize; - let start = name_addr + 4 + packs * 4; - let end = d[start..].iter().position(|&b| b == 0).map(|p| start + p)?; - Some(crate::decode_filename(&d[start..end])) -} - -/// The keycode-LZSS used by both VER5 and VER6: `[destSize u32][srcSize u32][keycode u8][stream]`. -pub(super) fn decode_lz(src: &[u8], dest_size: usize) -> Option> { - if src.len() < 9 { - return None; - } - let mut remaining = (u32at(src, 4)? as usize).checked_sub(9)?; - let keycode = src[8]; - let mut out: Vec = Vec::with_capacity(dest_size); - let mut sp = 9usize; - - while remaining > 0 { - let b = *src.get(sp)?; - if b != keycode { - out.push(b); - sp += 1; - remaining -= 1; - continue; - } - if *src.get(sp + 1)? == keycode { - out.push(keycode); - sp += 2; - remaining -= 2; - continue; - } - let mut code = *src.get(sp + 1)? as u32; - if code > keycode as u32 { - code -= 1; - } - sp += 2; - remaining -= 2; - - let mut conbo = (code >> 3) as usize; - if code & 0x4 != 0 { - conbo |= (*src.get(sp)? as usize) << 5; - sp += 1; - remaining -= 1; - } - conbo += MIN_COMPRESS; - - let mut index = match code & 0x3 { - 0 => { - let v = *src.get(sp)? as usize; - sp += 1; - remaining -= 1; - v - } - 1 => { - let v = u16::from_le_bytes([*src.get(sp)?, *src.get(sp + 1)?]) as usize; - sp += 2; - remaining -= 2; - v - } - _ => { - let v = u16::from_le_bytes([*src.get(sp)?, *src.get(sp + 1)?]) as usize - | ((*src.get(sp + 2)? as usize) << 16); - sp += 3; - remaining -= 3; - v - } - }; - index += 1; - - if index > out.len() { - return None; // back-reference before start - } - if index < conbo { - // Overlapping run via doubling memcpy (matches DXLib exactly). - let mut num = index; - let mut left = conbo; - while left > num { - let s = out.len() - num; - for j in 0..num { - let v = out[s + j]; - out.push(v); - } - left -= num; - num += num; - } - if left != 0 { - let s = out.len() - num; - for j in 0..left { - let v = out[s + j]; - out.push(v); - } - } - } else { - let s = out.len() - index; - for j in 0..conbo { - let v = out[s + j]; - out.push(v); - } - } - } - Some(out) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/encode.rs b/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/encode.rs deleted file mode 100644 index 0172205..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/encode.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! DXArchive VER5 / VER6 writers: the inverse of [`super::ver5`] / [`super::ver6`]. -//! -//! Produce the old Wolf 2.0x containers, storing files uncompressed so no keycode-LZSS encoder -//! is needed (the reader reads `press_size == 0xFFFF…` verbatim). Both ciphers are a symmetric -//! `key_conv` XOR, so encryption is the reader's decrypt applied to the plaintext container. -//! Header layouts mirror the real sample archives exactly. VER5 is version 3 with a 24-byte -//! header and whole-archive XOR by file offset. VER6 is version 6 with a 48-byte header, tables -//! keyed at position 0, and each file's data at position `data_size`. The shared tree walk and -//! name-table encoder come from [`crate::pack`]. - -use super::common::{key_conv, key_create, KEYLEN}; -use crate::pack::{add_filename, Node}; - -const DX_HEAD: u16 = 0x5844; -const ATTR_DIRECTORY: u64 = 0x10; -const ATTR_ARCHIVE: u64 = 0x20; - -/// VER5 v2.01 / VER6 v2.20 table keys. The reader tries these, so the output decodes through -/// our own dispatch and the real engine. -const KEY_V201: &[u8] = &[ - 0x0f, 0x53, 0xe1, 0x3e, 0x04, 0x37, 0x12, 0x17, 0x60, 0x0f, 0x53, 0xe1, -]; -const KEY_V220: &[u8] = &[ - 0x38, 0x50, 0x40, 0x28, 0x72, 0x4f, 0x21, 0x70, 0x3b, 0x73, 0x35, 0x38, -]; - -/// Per-version struct geometry. Field offsets within a file head: `name_addr@0`, `attr@ptr`, -/// time block, `data_addr@data_addr_off`, `data_size@+ptr`, `press@+2*ptr`. -struct Spec { - ptr: usize, - fh_size: usize, - data_addr_off: usize, -} -const V5: Spec = Spec { - ptr: 4, - fh_size: 44, - data_addr_off: 32, -}; -const V6: Spec = Spec { - ptr: 8, - fh_size: 64, - data_addr_off: 40, -}; - -impl Spec { - fn none(&self) -> u64 { - if self.ptr == 4 { - 0xFFFF_FFFF - } else { - u64::MAX - } - } - fn dir_size(&self) -> usize { - self.ptr * 4 - } -} - -fn put(buf: &mut [u8], off: usize, val: u64, ptr: usize) { - buf[off..off + ptr].copy_from_slice(&val.to_le_bytes()[..ptr]); -} - -fn get(buf: &[u8], off: usize, ptr: usize) -> u64 { - let mut b = [0u8; 8]; - b[..ptr].copy_from_slice(&buf[off..off + ptr]); - u64::from_le_bytes(b) -} - -#[derive(Default)] -struct Build { - name: Vec, - file: Vec, - dir: Vec, - data: Vec, - /// `(data_addr, data_size)` per leaf file. VER6 keys each region at `pos = data_size`. - file_regions: Vec<(u64, u64)>, -} - -/// Build the name/file/dir/data buffers for `root` using `spec`'s geometry (mirrors the v8 -/// `DirectoryEncode`: root self-filehead, per-directory contiguous file runs). -fn build_tables(spec: &Spec, root: &Node) -> Build { - let mut b = Build::default(); - add_filename(&mut b.name, ""); // root name (address 0) - b.file.resize(spec.fh_size, 0); - write_filehead(spec, &mut b.file, 0, 0, ATTR_DIRECTORY, 0, 0); - - let root_num = root.entry_count() as u64; - let root_fha = b.file.len() as u64; // = fh_size - append_dir(spec, &mut b.dir, 0, spec.none(), root_num, root_fha); - b.file - .resize(b.file.len() + spec.fh_size * root_num as usize, 0); - - encode_children(spec, &mut b, root, root_fha, 0); - b -} - -fn encode_children(spec: &Spec, b: &mut Build, node: &Node, fha: u64, this_dir_addr: u64) { - let mut i = 0u64; - for (name, child) in &node.dirs { - directory_encode(spec, b, child, name, fha, this_dir_addr, i); - i += 1; - } - for (name, data) in &node.files { - write_file_entry(spec, b, name, data, fha, i); - i += 1; - } -} - -fn directory_encode( - spec: &Spec, - b: &mut Build, - node: &Node, - name: &str, - parent_fha: u64, - parent_dir_addr: u64, - data_number: u64, -) { - let name_addr = b.name.len() as u64; - let this_dir_offset = b.dir.len() as u64; - add_filename(&mut b.name, name); - let self_off = (parent_fha + data_number * spec.fh_size as u64) as usize; - write_filehead( - spec, - &mut b.file, - self_off, - name_addr, - ATTR_DIRECTORY, - this_dir_offset, - 0, - ); - - let this_fha = b.file.len() as u64; - let parent_directory_address = if parent_dir_addr != spec.none() && parent_dir_addr != 0 { - get( - &b.file, - parent_dir_addr as usize + spec.data_addr_off, - spec.ptr, - ) - } else { - 0 - }; - let num = node.entry_count() as u64; - append_dir( - spec, - &mut b.dir, - self_off as u64, - parent_directory_address, - num, - this_fha, - ); - b.file.resize(b.file.len() + spec.fh_size * num as usize, 0); - - encode_children(spec, b, node, this_fha, self_off as u64); -} - -fn write_file_entry( - spec: &Spec, - b: &mut Build, - name: &str, - data: &[u8], - fha: u64, - data_number: u64, -) { - let name_addr = b.name.len() as u64; - add_filename(&mut b.name, name); - let data_addr = b.data.len() as u64; - let off = (fha + data_number * spec.fh_size as u64) as usize; - write_filehead( - spec, - &mut b.file, - off, - name_addr, - ATTR_ARCHIVE, - data_addr, - data.len() as u64, - ); - b.file_regions.push((data_addr, data.len() as u64)); - b.data.extend_from_slice(data); - while b.data.len() % 4 != 0 { - b.data.push(0); - } -} - -fn write_filehead( - spec: &Spec, - file: &mut [u8], - off: usize, - name_addr: u64, - attr: u64, - data_addr: u64, - data_size: u64, -) { - put(file, off, name_addr, spec.ptr); - put(file, off + spec.ptr, attr, spec.ptr); - // time block [2*ptr .. data_addr_off] stays zero - put(file, off + spec.data_addr_off, data_addr, spec.ptr); - put( - file, - off + spec.data_addr_off + spec.ptr, - data_size, - spec.ptr, - ); - put( - file, - off + spec.data_addr_off + 2 * spec.ptr, - spec.none(), - spec.ptr, - ); // press = uncompressed -} - -fn append_dir(spec: &Spec, dir: &mut Vec, dir_addr: u64, parent: u64, fhn: u64, fha: u64) { - let base = dir.len(); - dir.resize(base + spec.dir_size(), 0); - put(dir, base, dir_addr, spec.ptr); - put(dir, base + spec.ptr, parent, spec.ptr); - put(dir, base + 2 * spec.ptr, fhn, spec.ptr); - put(dir, base + 3 * spec.ptr, fha, spec.ptr); -} - -fn build_tree<'a>(files: &'a [(String, Vec)]) -> Result, String> { - if files.is_empty() { - return Err("cannot pack an empty file list".to_string()); - } - let mut root = Node::default(); - for (path, data) in files { - let parts: Vec<&str> = path.split(['\\', '/']).filter(|s| !s.is_empty()).collect(); - if parts.is_empty() { - return Err(format!("invalid (empty) path: {path:?}")); - } - root.insert(&parts, data); - } - Ok(root) -} - -/// Pack into a Wolf 2.01/2.10 (VER5, version 3) archive. Build the plaintext container, then -/// XOR the whole file by offset with the version key, which is the reader's exact inverse. -pub fn pack_ver5(files: &[(String, Vec)]) -> Result, String> { - let root = build_tree(files)?; - let b = build_tables(&V5, &root); - let head_size = b.name.len() + b.file.len() + b.dir.len(); - let data_start = 24usize; // version-3 header has no CodePage field - let name_tbl = data_start + b.data.len(); - - let mut out = vec![0u8; data_start]; - out[0..2].copy_from_slice(&DX_HEAD.to_le_bytes()); - out[2..4].copy_from_slice(&3u16.to_le_bytes()); // version 3 (Wolf 2.01/2.10) - put(&mut out, 4, head_size as u64, 4); - put(&mut out, 8, data_start as u64, 4); - put(&mut out, 12, name_tbl as u64, 4); - put(&mut out, 16, b.name.len() as u64, 4); // FileTableStart (rel to name table) - put(&mut out, 20, (b.name.len() + b.file.len()) as u64, 4); // DirectoryTableStart (rel) - out.extend_from_slice(&b.data); - out.extend_from_slice(&b.name); - out.extend_from_slice(&b.file); - out.extend_from_slice(&b.dir); - - let key = key_create(KEY_V201); - for (i, byte) in out.iter_mut().enumerate() { - *byte ^= key[i % KEYLEN]; - } - Ok(out) -} - -/// Pack into a Wolf 2.20 (VER6, version 6) archive: header + tables keyed at position 0, each -/// file's data keyed at position `data_size`. Mirrors the reader's decrypt. -pub fn pack_ver6(files: &[(String, Vec)]) -> Result, String> { - let root = build_tree(files)?; - let b = build_tables(&V6, &root); - let head_size = b.name.len() + b.file.len() + b.dir.len(); - let data_start = 48usize; - let name_tbl = data_start + b.data.len(); - - let mut head = vec![0u8; 48]; - head[0..2].copy_from_slice(&DX_HEAD.to_le_bytes()); - head[2..4].copy_from_slice(&6u16.to_le_bytes()); // version 6 (Wolf 2.20) - put(&mut head, 4, head_size as u64, 4); // HeadSize is u32 even in VER6 - put(&mut head, 8, data_start as u64, 8); - put(&mut head, 16, name_tbl as u64, 8); - put(&mut head, 24, b.name.len() as u64, 8); // FileTableStart (rel) - put(&mut head, 32, (b.name.len() + b.file.len()) as u64, 8); // DirectoryTableStart (rel) - put(&mut head, 40, 1252, 8); // CodePage (matches the real 2.20 sample archive) - - let key = key_create(KEY_V220); - - // File data: key each region at pos = its data_size (padding between files stays - // plaintext, exactly as the reader ignores it). - let mut data = b.data.clone(); - for &(addr, size) in &b.file_regions { - let (a, s) = (addr as usize, size as usize); - let keyed = key_conv(&key, &data[a..a + s], s); - data[a..a + s].copy_from_slice(&keyed); - } - - let mut table = Vec::with_capacity(head_size); - table.extend_from_slice(&b.name); - table.extend_from_slice(&b.file); - table.extend_from_slice(&b.dir); - - let mut out = Vec::with_capacity(data_start + data.len() + head_size); - out.extend_from_slice(&key_conv(&key, &head, 0)); // header keyed at 0 - out.extend_from_slice(&data); - out.extend_from_slice(&key_conv(&key, &table, 0)); // tables keyed at 0 - Ok(out) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/mod.rs b/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/mod.rs deleted file mode 100644 index edc160f..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! DXArchive VER5 / VER6: the old Wolf RPG Editor 2.0x archive formats. -//! -//! The shared crypto and codec (12-byte key derivation and `KeyConv`, keycode-LZSS, name -//! extraction) live in [`common`]. Each version's distinct table/struct layout lives in its -//! own module ([`ver5`] = Wolf 2.01/2.10, [`ver6`] = Wolf 2.20). The newer v8 (Wolf 2.225+) -//! format is handled separately in the crate root. - -mod common; -pub mod encode; -mod ver5; -mod ver6; - -pub use encode::{pack_ver5, pack_ver6}; - -/// Try to decode `data` as a VER5 (Wolf 2.01/2.10) then VER6 (Wolf 2.20) archive, returning -/// `(path, contents)` pairs. `None` if it isn't one of these legacy formats. -pub fn try_extract(data: &[u8]) -> Option)>> { - ver5::try_extract(data).or_else(|| ver6::try_extract(data)) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver5.rs b/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver5.rs deleted file mode 100644 index 34f46f1..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver5.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! DXArchive VER5: Wolf RPG Editor 2.01 / 2.10. -//! -//! 32-bit offsets and a 44-byte file head. For header version < 5 every region (header, -//! tables, file data) is keyed by its file offset, so the whole archive decrypts as one -//! continuous XOR stream. Verified byte-exact against the Wolf 2.01 sample-game `Data.wolf`. - -use super::common::{decode_lz, key_create, name, u32at, KEYLEN}; - -/// VER5 version-table keys. -const KEYS: &[&[u8; KEYLEN]] = &[ - &[ - 0x0f, 0x53, 0xe1, 0x3e, 0x04, 0x37, 0x12, 0x17, 0x60, 0x0f, 0x53, 0xe1, - ], // v2.01 - &[ - 0x4c, 0xd9, 0x2a, 0xb7, 0x28, 0x9b, 0xac, 0x07, 0x3e, 0x77, 0xec, 0x4c, - ], // v2.10 -]; - -pub(super) fn try_extract(data: &[u8]) -> Option)>> { - KEYS.iter().find_map(|ks| decode(data, &key_create(*ks))) -} - -fn decode(data: &[u8], key: &[u8; KEYLEN]) -> Option)>> { - // Header version < 5: every region is keyed by file offset, so it is one XOR stream. - let mut d = data.to_vec(); - for (i, b) in d.iter_mut().enumerate() { - *b ^= key[i % KEYLEN]; - } - if u16::from_le_bytes([*d.first()?, *d.get(1)?]) != 0x5844 { - return None; // not a DX archive under this key - } - if u16::from_le_bytes([d[2], d[3]]) >= 5 { - return None; // v>=5 keys files at DataSize, not file offset. That is VER6's job. - } - let data_start = u32at(&d, 8)? as usize; - let name_tbl = u32at(&d, 12)? as usize; - let file_p = name_tbl + u32at(&d, 16)? as usize; - let dir_p = name_tbl + u32at(&d, 20)? as usize; - - let mut out = Vec::new(); - walk( - &d, - name_tbl, - file_p, - dir_p, - data_start, - 0, - String::new(), - &mut out, - )?; - Some(out) -} - -#[allow(clippy::too_many_arguments)] -fn walk( - d: &[u8], - name_p: usize, - file_p: usize, - dir_p: usize, - data_start: usize, - dir_off: usize, - path: String, - out: &mut Vec<(String, Vec)>, -) -> Option<()> { - let fhn = u32at(d, dir_p + dir_off + 8)? as usize; // FileHeadNum - let fha = u32at(d, dir_p + dir_off + 12)? as usize; // FileHeadAddress - const FH: usize = 44; // sizeof(DARC_FILEHEAD_VER5) - for i in 0..fhn { - let fh = file_p + fha + i * FH; - let name_addr = u32at(d, fh)? as usize; - let attr = u32at(d, fh + 4)?; - let data_addr = u32at(d, fh + 32)? as usize; - let data_size = u32at(d, fh + 36)? as usize; - let press_size = u32at(d, fh + 40)?; - let nm = name(d, name_p + name_addr)?; - - if attr & 0x10 != 0 { - walk( - d, - name_p, - file_p, - dir_p, - data_start, - data_addr, - format!("{path}{nm}\\"), - out, - )?; - } else { - let start = data_start + data_addr; - let content = if press_size != 0xffff_ffff { - decode_lz(d.get(start..start + press_size as usize)?, data_size)? - } else { - d.get(start..start + data_size)?.to_vec() - }; - out.push((format!("{path}{nm}"), content)); - } - } - Some(()) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver6.rs b/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver6.rs deleted file mode 100644 index 13ed5d2..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/dxarc_v2/ver6.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! DXArchive VER6: Wolf RPG Editor 2.20. -//! -//! 64-bit offsets and a 64-byte file head (a bridge toward the v8 format). Unlike VER5, the -//! header and tables are keyed at stream position 0 and file data at position `DataSize` -//! (not the file offset). Same keycode-LZSS as VER5. Verified byte-exact against the Wolf -//! 2.20 sample-game `Data.wolf`. - -use super::common::{decode_lz, key_conv, key_create, name, u64at, KEYLEN}; - -/// VER6 version-table key. -const KEYS: &[&[u8; KEYLEN]] = &[ - &[ - 0x38, 0x50, 0x40, 0x28, 0x72, 0x4f, 0x21, 0x70, 0x3b, 0x73, 0x35, 0x38, - ], // v2.20 -]; - -pub(super) fn try_extract(data: &[u8]) -> Option)>> { - KEYS.iter().find_map(|ks| decode(data, &key_create(*ks))) -} - -fn decode(data: &[u8], key: &[u8; KEYLEN]) -> Option)>> { - let head = key_conv(key, data.get(0..48)?, 0); - if u16::from_le_bytes([head[0], head[1]]) != 0x5844 - || u16::from_le_bytes([head[2], head[3]]) != 6 - { - return None; - } - let head_size = u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as usize; - let data_start = u64at(&head, 8)? as usize; - let name_tbl = u64at(&head, 16)? as usize; - let file_tbl = u64at(&head, 24)? as usize; - let dir_tbl = u64at(&head, 32)? as usize; - // Header + tables are keyed at position 0. - let tbl = key_conv(key, data.get(name_tbl..name_tbl + head_size)?, 0); - - let mut out = Vec::new(); - walk( - data, - &tbl, - key, - file_tbl, - dir_tbl, - data_start, - 0, - String::new(), - &mut out, - )?; - Some(out) -} - -#[allow(clippy::too_many_arguments)] -fn walk( - data: &[u8], - tbl: &[u8], - key: &[u8; KEYLEN], - file_tbl: usize, - dir_tbl: usize, - data_start: usize, - dir_off: usize, - path: String, - out: &mut Vec<(String, Vec)>, -) -> Option<()> { - let fhn = u64at(tbl, dir_tbl + dir_off + 16)? as usize; // FileHeadNum - let fha = u64at(tbl, dir_tbl + dir_off + 24)? as usize; // FileHeadAddress - const FH: usize = 64; // sizeof(DARC_FILEHEAD_VER6) - for i in 0..fhn { - let fh = file_tbl + fha + i * FH; - let name_addr = u64at(tbl, fh)? as usize; - let attr = u64at(tbl, fh + 8)?; - let data_addr = u64at(tbl, fh + 40)? as usize; - let data_size = u64at(tbl, fh + 48)? as usize; - let press_size = u64at(tbl, fh + 56)?; - let nm = name(tbl, name_addr)?; - - if attr & 0x10 != 0 { - walk( - data, - tbl, - key, - file_tbl, - dir_tbl, - data_start, - data_addr, - format!("{path}{nm}\\"), - out, - )?; - } else { - let start = data_start + data_addr; - // File data is keyed at stream position == DataSize. - let content = if press_size != 0xffff_ffff_ffff_ffff { - let enc = key_conv( - key, - data.get(start..start + press_size as usize)?, - data_size, - ); - decode_lz(&enc, data_size)? - } else { - key_conv(key, data.get(start..start + data_size)?, data_size) - }; - out.push((format!("{path}{nm}"), content)); - } - } - Some(()) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/dxarchive.rs b/util/wolfdawn-master/crates/wolf-archive/src/dxarchive.rs deleted file mode 100644 index 0d45c26..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/dxarchive.rs +++ /dev/null @@ -1,947 +0,0 @@ -//! DXArchive v8 (`Data.wolf`) container: header parsing, the key / crypt-version dispatch -//! and detection strategies, the directory walk, and per-file extraction. - -use crate::chacha20; -use crate::codec::{dxa_decode, huffman_decode}; -use crate::crypto::*; -use crate::*; -use std::fs; -use std::io; -use std::path::Path; - -#[derive(Debug)] -pub(crate) struct Entry { - pub(crate) path: String, - pub(crate) head: FileHead, - pub(crate) directory_addr: usize, -} - -/// Map a DXArchive-Wolf cryptVersion (`Flags>>16`) to its table key. Returns `Some` only -/// for the plaintext-header, single-key "keyed decode" versions. New wolf-crypt (encrypted -/// header), ChaCha20, and the ambiguous version-0 string-keyed games are handled by the -/// strategy fallbacks. -pub(crate) fn table_key_for_version(crypt_version: u16) -> Option> { - let key: &[u8] = match crypt_version { - 0x12C => DEFAULT_WOLF_V310_KEY, // Wolf v3.00 - 0x13A => DEFAULT_WOLF_V3173_KEY, // Wolf v3.14 - _ => return None, - }; - Some(key.to_vec()) -} - -/// Build a layout for a "ChaCha2" (cryptVersion 0x64) archive: the header is plaintext, the -/// table and file data are ChaCha20-keyed with the fixed chacha2 key/nonce. This mirrors the -/// old-crypt path with ChaCha20 substituted for `key_conv`. Not yet verified against a real -/// ChaCha2 archive. -pub(crate) fn try_chacha_layout(data: &[u8], head: DxHead) -> Option { - let ck: [u8; 32] = DEFAULT_WOLF_CHACHA2_KEY[0..32].try_into().ok()?; - let cn: [u8; 12] = DEFAULT_WOLF_CHACHA2_KEY[32..44].try_into().ok()?; - let start = head.name_table_start as usize; - let no_head_press = (head.flags & 2) != 0; - let table = if no_head_press { - let mut t = data - .get(start..start.checked_add(head.head_size as usize)?)? - .to_vec(); - chacha20::crypt(&mut t, &ck, &cn, 0); - t - } else { - let mut huff = data.get(start..)?.to_vec(); - chacha20::crypt(&mut huff, &ck, &cn, 0); - let lz = huffman_decode(&huff).ok()?; - dxa_decode(&lz).ok()? - }; - validate_table( - &table, - head.file_table_start as usize, - head.directory_table_start as usize, - )?; - Some(Layout { - table, - data_start: head.data_start, - file_table_start: head.file_table_start as usize, - directory_table_start: head.directory_table_start as usize, - huffman_encode_kb: head.huffman_encode_kb, - flags: head.flags, - key_string: DEFAULT_WOLF_CHACHA2_KEY.to_vec(), - main_key_pos: 0, - source: "deterministic chacha2 cv=0x64".to_string(), - wolf: None, - chacha: Some((ck, cn)), - }) -} - -pub(crate) fn detect_layout(data: &[u8], requested_key_string: &[u8]) -> io::Result { - if data.len() < 8 { - return Err(invalid("file is too small")); - } - - // Use the full header (real table offsets and Flags). For an encrypted header the offsets - // are garbage, but then `try_official_layout`/`plausible_head` reject it and we fall - // through to the wolf-crypt strategies, so this is safe for all archives. - let plain = parse_full_head(data).or_else(|_| parse_head_prefix(data))?; - if plain.version != DX_VER_8 || plain.head != DX_HEAD { - return Err(invalid("not a DX archive v8 header")); - } - - // Deterministic dispatch: a plaintext header carries Flags, and cryptVersion = Flags>>16 - // selects the exact table key. No key guessing needed. - if plausible_head(&plain, data.len()) { - let crypt_version = (plain.flags >> 16) as u16; - if let Some(key_string) = table_key_for_version(crypt_version) { - let key = key_create(&key_string); - if let Some(mut layout) = - try_official_layout(data, key, key_string, plain, "deterministic") - { - layout.source = format!("deterministic cryptVersion={crypt_version:#x}"); - return Ok(layout); - } - } - // cv 0x64: "ChaCha2" archive. Plaintext header, table and files ChaCha20-keyed. - if crypt_version == 0x64 { - if let Some(layout) = try_chacha_layout(data, plain) { - return Ok(layout); - } - } - } - - let mut tried = Vec::new(); - - let mut candidates: Vec<(&str, Vec)> = vec![ - ("requested", requested_key_string.to_vec()), - ("wolf-pro", DEFAULT_WOLF_PRO_KEY.to_vec()), - ("wolf-v3.10", DEFAULT_WOLF_V310_KEY.to_vec()), - ("wolf-v3.173", DEFAULT_WOLF_V3173_KEY.to_vec()), - ("wolf-v3.31", DEFAULT_WOLF_V331_KEY.to_vec()), - ("wolf-v3.50", DEFAULT_WOLF_V350_KEY.to_vec()), - ("wolf-chacha2", DEFAULT_WOLF_CHACHA2_KEY.to_vec()), - ("one-way", DEFAULT_ONE_WAY_KEY.to_vec()), - ("one-way-full", DEFAULT_ONE_WAY_FULL_KEY.to_vec()), - ("dxlib", DEFAULT_DXLIB_KEY.to_vec()), - ("old-wolf", DEFAULT_OLD_WOLF_KEY.to_vec()), - ]; - candidates.dedup_by(|a, b| nul_terminated(&a.1) == nul_terminated(&b.1)); - - for (label, key_string) in candidates { - let key = key_create(&key_string); - if tried.iter().any(|prev| prev == &hex(&key)) { - continue; - } - tried.push(hex(&key)); - - if let Some(layout) = try_wolf_newcrypt_layout(data, key, key_string.clone(), plain, label) - { - return Ok(layout); - } - - if let Some(layout) = try_official_layout( - data, - key, - key_string.clone(), - plain, - "official-plain-header", - ) { - return Ok(layout); - } - - if let Some(layout) = - try_encrypted_v8_header_layout(data, key, key_string.clone(), plain, label) - { - return Ok(layout); - } - - if let Some(layout) = try_front_loaded_layout(data, key, key_string, plain) { - return Ok(layout); - } - } - - Err(invalid(format!( - "could not decode archive tables; alternate keys tried: {}", - tried.join(", ") - ))) -} - -pub(crate) fn try_official_layout( - data: &[u8], - key: [u8; KEY_BYTES], - key_string: Vec, - head: DxHead, - source: &str, -) -> Option { - if !plausible_head(&head, data.len()) { - return None; - } - - let start = head.name_table_start as usize; - let head_size = head.head_size as usize; - let no_key = (head.flags & 1) != 0; - let no_head_press = (head.flags & 2) != 0; - - let table = if no_head_press { - let mut table = data.get(start..start.checked_add(head_size)?)?.to_vec(); - if !no_key { - key_conv(&mut table, &key, 0); - } - table - } else { - let mut huff = data.get(start..)?.to_vec(); - if !no_key { - key_conv(&mut huff, &key, 0); - } - let lz = huffman_decode(&huff).ok()?; - dxa_decode(&lz).ok()? - }; - - validate_table( - &table, - head.file_table_start as usize, - head.directory_table_start as usize, - )?; - Some(Layout { - table, - data_start: head.data_start, - file_table_start: head.file_table_start as usize, - directory_table_start: head.directory_table_start as usize, - huffman_encode_kb: head.huffman_encode_kb, - flags: head.flags, - key_string, - main_key_pos: 0, - source: source.to_string(), - wolf: None, - chacha: None, - }) -} - -pub(crate) fn try_encrypted_v8_header_layout( - data: &[u8], - key: [u8; KEY_BYTES], - key_string: Vec, - plain: DxHead, - label: &str, -) -> Option { - if data.len() < 64 { - return None; - } - - for pos in 0..KEY_BYTES { - let mut head_buf = data[..64].to_vec(); - key_conv(&mut head_buf[8..64], &key, pos); - let head = parse_full_head(&head_buf).ok()?; - if head.head == plain.head - && head.version == plain.version - && head.head_size == plain.head_size - && plausible_head(&head, data.len()) - { - let source = format!("encrypted-v8-header-{label}-pos{pos}"); - if let Some(layout) = try_official_layout(data, key, key_string.clone(), head, &source) - { - return Some(layout); - } - } - } - - None -} - -pub(crate) fn try_wolf_newcrypt_layout( - data: &[u8], - _key: [u8; KEY_BYTES], - key_string: Vec, - plain: DxHead, - label: &str, -) -> Option { - if data.len() < 64 { - return None; - } - - let raw_head = parse_full_head(data).ok()?; - let crypt_version = (raw_head.flags >> 16) as u16; - if !is_new_wolf_crypt(crypt_version) || !candidate_matches_wolf_version(label, crypt_version) { - return None; - } - let debug = std::env::var_os("WOLF_DEBUG").is_some(); - - let mut pwd = [0u8; 15]; - pwd.copy_from_slice(data.get(49..64)?); - - let mut head_buf = data[..64].to_vec(); - wolf_crypt_addresses(&mut head_buf, &pwd, crypt_version); - let head = parse_full_head(&head_buf).ok()?; - if debug { - eprintln!( - "newcrypt {label}: raw_cv=0x{crypt_version:04x} head_size={} data_start={} name_start={} file_start={} dir_start={} flags=0x{:08x} huff_kb={}", - head.head_size, - head.data_start, - head.name_table_start, - head.file_table_start, - head.directory_table_start, - head.flags, - head.huffman_encode_kb - ); - } - if head.head != plain.head - || head.version != plain.version - || head.head_size != plain.head_size - || !plausible_head(&head, data.len()) - { - return None; - } - - let ctx = WolfContext { - crypt_version, - other_key: wolf_init_key(crypt_version, &pwd, None, true, &key_string), - special_key: wolf_init_key(crypt_version, &pwd, None, false, &key_string), - aes_round_key: wolf_aes_init_round_key(&pwd, None, crypt_version), - body_size: wolf_aes_body_size(data.len(), crypt_version, &pwd, None), - name_table_start: head.name_table_start as usize, - }; - - let table = read_wolf_table(data, &head, &ctx)?; - if debug { - eprintln!( - "newcrypt {label}: table len={} first={}", - table.len(), - hex(&table[..table.len().min(16)]) - ); - if let Some(root) = parse_directory_at(&table, head.directory_table_start as usize) { - eprintln!( - "newcrypt {label}: root dir_addr={} parent={:016x} file_num={} file_addr={}", - root.directory_addr, root.parent_addr, root.file_head_num, root.file_head_addr - ); - } - if let Some(first_file) = parse_file_head_at(&table, head.file_table_start as usize) { - eprintln!( - "newcrypt {label}: first file name={} attrs={:x} data={} size={} press={} huff={}", - first_file.name_addr, - first_file.attrs, - first_file.data_addr, - first_file.data_size, - first_file.press_size, - first_file.huff_size - ); - } - } - validate_table( - &table, - head.file_table_start as usize, - head.directory_table_start as usize, - )?; - - Some(Layout { - table, - data_start: head.data_start, - file_table_start: head.file_table_start as usize, - directory_table_start: head.directory_table_start as usize, - huffman_encode_kb: head.huffman_encode_kb, - flags: head.flags, - key_string, - main_key_pos: 0, - source: format!("wolf-newcrypt-v{crypt_version:x}-{label}"), - wolf: Some(ctx), - chacha: None, - }) -} - -pub(crate) fn try_front_loaded_layout( - data: &[u8], - key: [u8; KEY_BYTES], - key_string: Vec, - head: DxHead, -) -> Option { - let block_size = head.head_size as usize; - let block = data.get(8..8usize.checked_add(block_size)?)?; - - for pos in 0..KEY_BYTES { - let mut decoded = block.to_vec(); - key_conv(&mut decoded, &key, pos); - - let candidates = front_table_candidates(&decoded); - for (table, source_suffix) in candidates { - if let Some(layout) = infer_front_layout( - &table, - data.len(), - block_size, - key_string.clone(), - pos, - &source_suffix, - ) { - return Some(layout); - } - } - } - - None -} - -pub(crate) fn front_table_candidates(decoded: &[u8]) -> Vec<(Vec, String)> { - let mut out = Vec::new(); - - out.push((decoded.to_vec(), "xor-direct".to_string())); - - if let Ok(lz) = dxa_decode(decoded) { - out.push((lz, "xor-lz".to_string())); - } - - if let Ok(huff) = huffman_decode(decoded) { - out.push((huff.clone(), "xor-huff".to_string())); - if let Ok(lz) = dxa_decode(&huff) { - out.push((lz, "xor-huff-lz".to_string())); - } - } - - out -} - -pub(crate) fn infer_front_layout( - table: &[u8], - archive_len: usize, - block_size: usize, - key_string: Vec, - pos: usize, - suffix: &str, -) -> Option { - if table.len() < 32 { - return None; - } - - let data_start = 8u64 + block_size as u64; - - let mut offset_pairs = Vec::new(); - - if table.len() >= 64 { - let h = parse_full_head(table).ok()?; - if h.head == DX_HEAD - && h.version == DX_VER_8 - && plausible_table_offsets( - table, - h.file_table_start as usize, - h.directory_table_start as usize, - ) - { - offset_pairs.push(( - 64, - h.file_table_start as usize, - h.directory_table_start as usize, - h.huffman_encode_kb, - h.flags, - "embedded-head", - )); - } - } - - for file_start in guess_file_table_starts(table) { - for dir_start in guess_directory_table_starts(table, file_start) { - offset_pairs.push((0, file_start, dir_start, 0xff, 0, "guessed")); - } - } - - for (base, file_table_start, directory_table_start, huff_kb, flags, kind) in offset_pairs { - let logical_table = if base == 0 { - table.to_vec() - } else { - table[base..].to_vec() - }; - let file_start = file_table_start - .checked_sub(base) - .unwrap_or(file_table_start); - let dir_start = directory_table_start - .checked_sub(base) - .unwrap_or(directory_table_start); - if validate_table(&logical_table, file_start, dir_start).is_some() { - if data_start as usize <= archive_len { - return Some(Layout { - table: logical_table, - data_start, - file_table_start: file_start, - directory_table_start: dir_start, - huffman_encode_kb: huff_kb, - flags, - key_string, - main_key_pos: pos, - source: format!("front-loaded-{suffix}-{kind}"), - wolf: None, - chacha: None, - }); - } - } - } - - None -} - -pub(crate) fn guess_file_table_starts(table: &[u8]) -> Vec { - let mut guesses = Vec::new(); - let max = table.len().saturating_sub(72); - let mut off = 0usize; - while off <= max.min(0x20000) { - if let Some(fh) = parse_file_head_at(table, off) { - if fh.name_addr < table.len() as u64 - && fh.attrs & !0x37ff == 0 - && fh.data_size < 1u64 << 34 - && (fh.press_size == NONE || fh.press_size <= fh.data_size + (1u64 << 30)) - && (fh.huff_size == NONE - || fh.huff_size <= fh.press_size.max(fh.data_size) + (1u64 << 30)) - { - guesses.push(off); - } - } - off += 8; - } - guesses.sort_unstable(); - guesses.dedup(); - guesses -} - -pub(crate) fn guess_directory_table_starts(table: &[u8], file_start: usize) -> Vec { - let mut guesses = Vec::new(); - let min = file_start.saturating_add(72); - let max = table.len().saturating_sub(32); - let mut off = min; - while off <= max { - if let Some(dir) = parse_directory_at(table, off) { - if dir.parent_addr == NONE - && dir.file_head_num > 0 - && dir.file_head_num < 500_000 - && (file_start as u64 + dir.file_head_addr) as usize - + (dir.file_head_num as usize).saturating_mul(72) - <= table.len() - { - guesses.push(off); - } - } - off += 8; - } - guesses.sort_unstable(); - guesses.dedup(); - guesses -} - -pub(crate) fn validate_table( - table: &[u8], - file_table_start: usize, - directory_table_start: usize, -) -> Option<()> { - if !plausible_table_offsets(table, file_table_start, directory_table_start) { - return None; - } - let root = parse_directory_at(table, directory_table_start)?; - if root.parent_addr != NONE { - return None; - } - if root.file_head_num == 0 || root.file_head_num > 500_000 { - return None; - } - let first = file_table_start.checked_add(root.file_head_addr as usize)?; - let bytes = (root.file_head_num as usize).checked_mul(72)?; - if first.checked_add(bytes)? > table.len() { - return None; - } - // Sanity-check the root's file headers. The root directory lives in the table's tail, - // which (for an uncompressed/NO_HEAD_PRESS header) decodes plausibly even under a wrong - // key because it sits past the stream-overlap range. The file headers do not. So a wrong - // key leaves garbage `name_addr`s here, which we reject to keep the key dispatch from - // latching onto a wrong candidate. - for i in 0..root.file_head_num as usize { - let fh = parse_file_head_at(table, first + i * 72)?; - // `name_addr` outside the table is the strong wrong-key tell. Keep the press/huff - // check only as a coarse garbage filter. A real compressed size is bounded by the - // archive (well under 1 TB), while a wrong-key decode yields random ~1e19 values. - if fh.name_addr as usize >= table.len() { - return None; - } - let sane = |v: u64| v == NONE || v < (1u64 << 40); - if !sane(fh.press_size) || !sane(fh.huff_size) { - return None; - } - } - Some(()) -} - -pub(crate) fn plausible_table_offsets( - table: &[u8], - file_table_start: usize, - directory_table_start: usize, -) -> bool { - file_table_start < table.len() - && directory_table_start < table.len() - && file_table_start % 4 == 0 - && directory_table_start % 4 == 0 - && file_table_start < directory_table_start -} - -pub(crate) fn collect_files(layout: &Layout) -> io::Result> { - let mut out = Vec::new(); - let root = parse_directory_at(&layout.table, layout.directory_table_start) - .ok_or_else(|| invalid("root directory is outside table"))?; - collect_dir( - layout, - root, - layout.directory_table_start, - String::new(), - &mut out, - )?; - Ok(out) -} - -pub(crate) fn collect_dir( - layout: &Layout, - dir: Directory, - dir_addr: usize, - prefix: String, - out: &mut Vec, -) -> io::Result<()> { - let start = layout - .file_table_start - .checked_add(dir.file_head_addr as usize) - .ok_or_else(|| invalid("file table address overflow"))?; - for i in 0..dir.file_head_num as usize { - let off = start + i * 72; - let fh = parse_file_head_at(&layout.table, off) - .ok_or_else(|| invalid(format!("bad file header at 0x{off:x}")))?; - let name = name_for(layout, fh.name_addr as usize)?; - if fh.attrs & FILE_ATTRIBUTE_DIRECTORY != 0 { - let child_addr = layout - .directory_table_start - .checked_add(fh.data_addr as usize) - .ok_or_else(|| invalid("directory address overflow"))?; - let child = parse_directory_at(&layout.table, child_addr) - .ok_or_else(|| invalid(format!("bad directory at 0x{child_addr:x}")))?; - let child_prefix = if prefix.is_empty() { - name - } else { - format!("{prefix}\\{name}") - }; - collect_dir(layout, child, child_addr, child_prefix, out)?; - } else { - let path = if prefix.is_empty() { - name - } else { - format!("{prefix}\\{name}") - }; - out.push(Entry { - path, - head: fh, - directory_addr: dir_addr, - }); - } - } - Ok(()) -} - -pub(crate) fn name_for(layout: &Layout, name_addr: usize) -> io::Result { - if name_addr + 4 > layout.table.len() { - return Err(invalid("name address outside table")); - } - let packs = read_u16(&layout.table[name_addr..]) as usize; - let original_start = name_addr + 4 + packs * 4; - if original_start >= layout.table.len() { - return Err(invalid("name original string outside table")); - } - let end = layout.table[original_start..] - .iter() - .position(|&b| b == 0) - .map(|p| original_start + p) - .unwrap_or(layout.table.len()); - let raw = &layout.table[original_start..end]; - Ok(decode_filename(raw)) -} - -pub(crate) fn extract_all( - data: &[u8], - layout: &Layout, - files: &[Entry], - out_dir: &Path, -) -> io::Result<()> { - fs::create_dir_all(out_dir)?; - let mut failed = 0usize; - for (idx, entry) in files.iter().enumerate() { - // Extract resiliently. One unwritable entry (e.g. a reserved/over-long name) must not - // abort the whole archive. Log it and keep going so the folder is as complete as possible. - let res = (|| -> io::Result<()> { - let bytes = extract_file(data, layout, entry)?; - let target = out_dir.join(safe_relative_path(&entry.path)); - if let Some(parent) = target.parent() { - fs::create_dir_all(parent)?; - } - fs::write(&target, bytes) - })(); - if let Err(e) = res { - eprintln!("warning: skipping {}: {e}", entry.path); - failed += 1; - } - if idx % 100 == 0 { - println!("extracted {:5}/{} {}", idx + 1, files.len(), entry.path); - } - } - if failed > 0 { - eprintln!("extract: {failed} file(s) could not be written (see warnings above)"); - } - Ok(()) -} - -pub(crate) fn extract_file(data: &[u8], layout: &Layout, entry: &Entry) -> io::Result> { - let fh = entry.head; - // A NO_KEY archive (flags bit 0) stores file data unencrypted, with no per-file key. - let no_key = (layout.flags & 1) != 0; - let key = if layout.wolf.is_none() && layout.chacha.is_none() && !no_key { - Some(file_key(layout, entry)?) - } else { - None - }; - let start = layout - .data_start - .checked_add(fh.data_addr) - .ok_or_else(|| invalid("data address overflow"))? as usize; - - if fh.press_size != NONE { - let press = if fh.huff_size != NONE { - let huff = read_layout_slice( - data, - layout, - start, - fh.huff_size as usize, - key.as_ref(), - fh.data_size, - )?; - let mut lz = huffman_decode(&huff).map_err(invalid)?; - if layout.huffman_encode_kb != 0xff - && fh.press_size > (layout.huffman_encode_kb as u64) * 1024 * 2 - { - let kb = layout.huffman_encode_kb as usize * 1024; - let middle_len = fh.press_size as usize - kb * 2; - let middle_start = start + fh.huff_size as usize; - let mut middle = read_layout_slice( - data, - layout, - middle_start, - middle_len, - key.as_ref(), - fh.data_size + fh.huff_size, - )?; - let tail = lz[kb..kb * 2].to_vec(); - lz.truncate(kb); - lz.append(&mut middle); - lz.extend_from_slice(&tail); - } - lz - } else { - read_layout_slice( - data, - layout, - start, - fh.press_size as usize, - key.as_ref(), - fh.data_size, - )? - }; - return dxa_decode(&press).map_err(invalid); - } - - if fh.huff_size != NONE { - let mut output = if layout.huffman_encode_kb != 0xff - && fh.data_size > (layout.huffman_encode_kb as u64) * 1024 * 2 - { - let kb = layout.huffman_encode_kb as usize * 1024; - let huff = read_layout_slice( - data, - layout, - start, - fh.huff_size as usize, - key.as_ref(), - fh.data_size, - )?; - let mut decoded = huffman_decode(&huff).map_err(invalid)?; - let middle_len = fh.data_size as usize - kb * 2; - let middle_start = start + fh.huff_size as usize; - let mut middle = read_layout_slice( - data, - layout, - middle_start, - middle_len, - key.as_ref(), - fh.data_size + fh.huff_size, - )?; - let tail = decoded[kb..kb * 2].to_vec(); - decoded.truncate(kb); - decoded.append(&mut middle); - decoded.extend_from_slice(&tail); - decoded - } else { - let huff = read_layout_slice( - data, - layout, - start, - fh.huff_size as usize, - key.as_ref(), - fh.data_size, - )?; - huffman_decode(&huff).map_err(invalid)? - }; - output.truncate(fh.data_size as usize); - return Ok(output); - } - - read_layout_slice( - data, - layout, - start, - fh.data_size as usize, - key.as_ref(), - fh.data_size, - ) -} - -pub(crate) fn read_layout_slice( - data: &[u8], - layout: &Layout, - start: usize, - len: usize, - key: Option<&[u8; KEY_BYTES]>, - pos: u64, -) -> io::Result> { - let end = start - .checked_add(len) - .ok_or_else(|| invalid("slice address overflow"))?; - if end > data.len() { - return Err(invalid("file data outside archive")); - } - - let mut out = if let Some(wolf) = &layout.wolf { - read_wolf_archive_slice(data, start, len, wolf) - .ok_or_else(|| invalid("file data outside archive"))? - } else { - data[start..end].to_vec() - }; - - if let Some(wolf) = &layout.wolf { - if (layout.flags & 1) == 0 { - wolf_crypt( - &wolf.special_key, - &mut out, - pos as usize, - wolf.crypt_version, - ); - } - } else if let Some((ck, cn)) = &layout.chacha { - chacha20::crypt(&mut out, ck, cn, pos as u32); - } else if let Some(key) = key { - key_conv(&mut out, key, (pos as usize) % KEY_BYTES); - } - Ok(out) -} - -pub(crate) fn file_key(layout: &Layout, entry: &Entry) -> io::Result<[u8; KEY_BYTES]> { - let mut key_string = Vec::new(); - key_string.extend_from_slice(nul_terminated(&layout.key_string)); - - let file_name = raw_search_name(layout, entry.head.name_addr as usize)?; - key_string.extend_from_slice(file_name); - - let mut dir = parse_directory_at(&layout.table, entry.directory_addr) - .ok_or_else(|| invalid("entry directory is outside table"))?; - while dir.parent_addr != NONE { - let dir_fh_addr = layout - .file_table_start - .checked_add(dir.directory_addr as usize) - .ok_or_else(|| invalid("directory file header overflow"))?; - let dir_fh = parse_file_head_at(&layout.table, dir_fh_addr) - .ok_or_else(|| invalid("directory file header outside table"))?; - key_string.extend_from_slice(raw_search_name(layout, dir_fh.name_addr as usize)?); - let parent_addr = layout - .directory_table_start - .checked_add(dir.parent_addr as usize) - .ok_or_else(|| invalid("parent directory overflow"))?; - dir = parse_directory_at(&layout.table, parent_addr) - .ok_or_else(|| invalid("parent directory outside table"))?; - } - - Ok(key_create(&key_string)) -} - -/// The "search" name (uppercased, stored at `NameAddress + 4`). This is the form DXArchive's -/// `CreateKeyFileString` uses to build per-file keys. Distinct from the original display -/// name (at `+ 4 + packs*4`). -pub(crate) fn raw_search_name(layout: &Layout, name_addr: usize) -> io::Result<&[u8]> { - let start = name_addr - .checked_add(4) - .ok_or_else(|| invalid("search name overflow"))?; - if start > layout.table.len() { - return Err(invalid("search name outside table")); - } - let end = layout.table[start..] - .iter() - .position(|&b| b == 0) - .map(|p| start + p) - .unwrap_or(layout.table.len()); - Ok(&layout.table[start..end]) -} - -pub(crate) fn parse_head_prefix(data: &[u8]) -> io::Result { - if data.len() < 8 { - return Err(invalid("missing header")); - } - Ok(DxHead { - head: read_u16(data), - version: read_u16(&data[2..]), - head_size: read_u32(&data[4..]), - data_start: 0, - name_table_start: 0, - file_table_start: 0, - directory_table_start: 0, - char_code_format: 0, - flags: 0, - huffman_encode_kb: 0xff, - }) -} - -pub(crate) fn parse_full_head(data: &[u8]) -> io::Result { - if data.len() < 64 { - return Err(invalid("missing full header")); - } - Ok(DxHead { - head: read_u16(data), - version: read_u16(&data[2..]), - head_size: read_u32(&data[4..]), - data_start: read_u64(&data[8..]), - name_table_start: read_u64(&data[16..]), - file_table_start: read_u64(&data[24..]), - directory_table_start: read_u64(&data[32..]), - char_code_format: read_u32(&data[40..]), - flags: read_u32(&data[44..]), - huffman_encode_kb: data[48], - }) -} - -pub(crate) fn parse_file_head_at(table: &[u8], off: usize) -> Option { - let b = table.get(off..off.checked_add(72)?)?; - Some(FileHead { - name_addr: read_u64(b), - attrs: read_u64(&b[8..]), - data_addr: read_u64(&b[40..]), - data_size: read_u64(&b[48..]), - press_size: read_u64(&b[56..]), - huff_size: read_u64(&b[64..]), - }) -} - -pub(crate) fn parse_directory_at(table: &[u8], off: usize) -> Option { - let b = table.get(off..off.checked_add(32)?)?; - Some(Directory { - directory_addr: read_u64(b), - parent_addr: read_u64(&b[8..]), - file_head_num: read_u64(&b[16..]), - file_head_addr: read_u64(&b[24..]), - }) -} - -pub(crate) fn plausible_head(head: &DxHead, file_len: usize) -> bool { - head.head == DX_HEAD - && head.version == DX_VER_8 - && head.head_size > 0 - && head.head_size as usize <= file_len - && head.data_start < file_len as u64 - && head.name_table_start < file_len as u64 - && head.file_table_start < head.head_size as u64 - && head.directory_table_start < head.head_size as u64 - && head.file_table_start < head.directory_table_start - && head.char_code_format < 100_000 -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/lib.rs b/util/wolfdawn-master/crates/wolf-archive/src/lib.rs deleted file mode 100644 index 5c03dbd..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/lib.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! `wolf-archive`: Wolf RPG `Data.wolf` archives. Unpacks every DXArchive variant Wolf uses. -//! -//! Public API: [`extract_archive`] / [`list_archive`] / [`extract_one`] (bytes to files) and -//! the CLI [`run`]. The format and crypto live in submodules: [`dxarchive`] (the v8 container -//! plus key dispatch), [`crypto`] (WolfPro stream plus AES), [`codec`] (Huffman plus LZSS), -//! [`dxarc_v2`] (the old VER5/VER6 archives), and [`chacha20`]. - -use std::fs; -use std::io::{self, Read}; -use std::path::PathBuf; - -pub mod chacha20; -mod codec; -mod crypto; -pub mod dxarc_v2; -mod dxarchive; -pub mod pack; - -pub use dxarc_v2::{pack_ver5, pack_ver6}; -pub use pack::{archive_crypt_params, pack_chacha, pack_encrypted, pack_newcrypt, pack_plaintext}; - -use crypto::*; -use dxarchive::*; - -pub(crate) const DX_HEAD: u16 = 0x5844; -pub(crate) const DX_VER_8: u16 = 0x0008; -pub(crate) const KEY_BYTES: usize = 7; -pub(crate) const NONE: u64 = u64::MAX; -pub(crate) const FILE_ATTRIBUTE_DIRECTORY: u64 = 0x10; -pub const DEFAULT_WOLF_PRO_KEY: &[u8] = b"WLFRPrO!p(;s5((8P@((UFWlu$#5(="; -pub(crate) const DEFAULT_DXLIB_KEY: &[u8] = b"DXBDXARC"; -pub(crate) const DEFAULT_OLD_WOLF_KEY: &[u8] = b"8P@(rO!p;s5"; -pub(crate) const DEFAULT_WOLF_V310_KEY: &[u8] = &[ - 0x0f, 0x53, 0xe1, 0x3e, 0x8e, 0xb5, 0x41, 0x91, 0x52, 0x16, 0x55, 0xae, 0x34, 0xc9, 0x8f, 0x79, - 0x59, 0x2f, 0x59, 0x6b, 0x95, 0x19, 0x9b, 0x1b, 0x35, 0x9a, 0x2f, 0xde, 0xc9, 0x7c, 0x12, 0x96, - 0xc3, 0x14, 0xb5, 0x0f, 0x53, 0xe1, 0x3e, 0x8e, 0x00, -]; -pub(crate) const DEFAULT_WOLF_V3173_KEY: &[u8] = &[ - 0x31, 0xf9, 0x01, 0x36, 0xa3, 0xe3, 0x8d, 0x3c, 0x7b, 0xc3, 0x7d, 0x25, 0xad, 0x63, 0x28, 0x19, - 0x1b, 0xf7, 0x8e, 0x6c, 0xc4, 0xe5, 0xe2, 0x76, 0x82, 0xea, 0x4f, 0xed, 0x61, 0xda, 0xe0, 0x44, - 0x5b, 0xb6, 0x46, 0x3b, 0x06, 0xd5, 0xce, 0xb6, 0x78, 0x58, 0xd0, 0x7c, 0x82, 0x00, -]; -pub(crate) const DEFAULT_WOLF_V331_KEY: &[u8] = &[ - 0xca, 0x08, 0x4c, 0x5d, 0x17, 0x0d, 0xda, 0xa1, 0xd7, 0x27, 0xc8, 0x41, 0x54, 0x38, 0x82, 0x32, - 0x54, 0xb7, 0xf9, 0x46, 0x8e, 0x13, 0x6b, 0xca, 0xd0, 0x5c, 0x95, 0x95, 0xe2, 0xdc, 0x03, 0x53, - 0x60, 0x9b, 0x4a, 0x38, 0x17, 0xf3, 0x69, 0x59, 0xa4, 0xc7, 0x9a, 0x43, 0x63, 0xe6, 0x54, 0xaf, - 0xdb, 0xbb, 0x43, 0x58, 0x00, -]; -pub(crate) const DEFAULT_WOLF_V350_KEY: &[u8] = &[ - 0xd2, 0x84, 0xce, 0x28, 0xce, 0x88, 0x82, 0xe4, 0x2a, 0x18, 0x2e, 0x4c, 0x06, 0xb4, 0xea, 0x84, - 0x06, 0xb8, 0xc6, 0x88, 0x5a, 0xa0, 0x9e, 0x7c, 0x56, 0x40, 0xba, 0x34, 0x52, 0xcc, 0xc6, 0x7c, - 0x2e, 0x14, 0x12, 0x68, 0xfe, 0x5c, 0x76, 0x94, 0x86, 0x78, 0x8e, 0x4c, 0xbe, 0x88, 0x66, 0x9c, - 0x1e, 0xe0, 0x8e, 0x6c, 0x00, -]; -pub(crate) const DEFAULT_WOLF_CHACHA2_KEY: &[u8] = &[ - 0xc9, 0x82, 0xf8, 0xb4, 0x2c, 0x93, 0x9e, 0x83, 0x0e, 0xbc, 0xbc, 0x92, 0x68, 0x8d, 0x59, 0xa1, - 0x4a, 0x9e, 0x7f, 0xb0, 0xac, 0xaf, 0x1d, 0x8f, 0x8e, 0xb8, 0x3b, 0x9e, 0xe8, 0x89, 0xd9, 0xad, - 0xff, 0xbc, 0x2d, 0xab, 0x9d, 0x8b, 0x0f, 0xb4, 0xbb, 0x9a, 0x69, 0x85, 0x00, -]; -pub(crate) const DEFAULT_ONE_WAY_KEY: &[u8] = b"nGui9('&1=@3#a"; -pub(crate) const DEFAULT_ONE_WAY_FULL_KEY: &[u8] = b"Ph=X3^]o2A(,1=@3#a"; -pub(crate) const MAX_DECODE_SIZE: usize = 2 * 1024 * 1024 * 1024; -pub(crate) const AES_KEY_EXP_SIZE: usize = 176; -pub(crate) const AES_IV_SIZE: usize = 16; -pub(crate) const AES_ROUND_KEY_SIZE: usize = AES_KEY_EXP_SIZE + AES_IV_SIZE; -pub(crate) const AES_BLOCK_LEN: usize = 16; -#[derive(Clone, Copy, Debug)] -pub(crate) struct DxHead { - pub(crate) head: u16, - pub(crate) version: u16, - pub(crate) head_size: u32, - pub(crate) data_start: u64, - pub(crate) name_table_start: u64, - pub(crate) file_table_start: u64, - pub(crate) directory_table_start: u64, - pub(crate) char_code_format: u32, - pub(crate) flags: u32, - pub(crate) huffman_encode_kb: u8, -} - -#[derive(Clone, Copy, Debug)] -pub(crate) struct FileHead { - pub(crate) name_addr: u64, - pub(crate) attrs: u64, - pub(crate) data_addr: u64, - pub(crate) data_size: u64, - pub(crate) press_size: u64, - pub(crate) huff_size: u64, -} - -#[derive(Clone, Copy, Debug)] -pub(crate) struct Directory { - pub(crate) directory_addr: u64, - pub(crate) parent_addr: u64, - pub(crate) file_head_num: u64, - pub(crate) file_head_addr: u64, -} - -#[derive(Debug)] -pub(crate) struct Layout { - pub(crate) table: Vec, - pub(crate) data_start: u64, - pub(crate) file_table_start: usize, - pub(crate) directory_table_start: usize, - pub(crate) huffman_encode_kb: u8, - pub(crate) flags: u32, - pub(crate) key_string: Vec, - pub(crate) main_key_pos: usize, - pub(crate) source: String, - pub(crate) wolf: Option, - /// ChaCha20 (key, nonce) for "ChaCha2" archives (cryptVersion 0x64 / 0xC8). - pub(crate) chacha: Option<([u8; 32], [u8; 12])>, -} - -#[derive(Clone, Debug)] -pub(crate) struct WolfContext { - pub(crate) crypt_version: u16, - pub(crate) other_key: [u8; 768], - pub(crate) special_key: [u8; 768], - pub(crate) aes_round_key: [u8; AES_ROUND_KEY_SIZE], - pub(crate) body_size: usize, - pub(crate) name_table_start: usize, -} - -/// `key_string` is the WolfPro key seed (use [`DEFAULT_WOLF_PRO_KEY`] when unknown). -pub fn extract_archive(data: &[u8], key_string: &[u8]) -> io::Result)>> { - let layout = match detect_layout(data, key_string) { - Ok(l) => l, - // Fall back to the old DXArchive VER5/VER6 format (Wolf 2.0x). - Err(e) => return dxarc_v2::try_extract(data).ok_or(e), - }; - let files = collect_files(&layout)?; - let mut out = Vec::with_capacity(files.len()); - for entry in &files { - let bytes = extract_file(data, &layout, entry)?; - out.push((entry.path.clone(), bytes)); - } - Ok(out) -} - -/// List archive file paths. Decodes only the tables, not file contents, so it stays cheap -/// even for multi-GB archives. -pub fn list_archive(data: &[u8], key_string: &[u8]) -> io::Result> { - match detect_layout(data, key_string) { - Ok(layout) => Ok(collect_files(&layout)? - .into_iter() - .map(|e| e.path) - .collect()), - Err(e) => dxarc_v2::try_extract(data) - .map(|f| f.into_iter().map(|(p, _)| p).collect()) - .ok_or(e), - } -} - -/// Extract a single file by its `\\`-separated archive path, decoding only that file. -pub fn extract_one(data: &[u8], key_string: &[u8], path: &str) -> io::Result>> { - let layout = match detect_layout(data, key_string) { - Ok(l) => l, - Err(e) => { - return match dxarc_v2::try_extract(data) { - Some(files) => Ok(files.into_iter().find(|(p, _)| p == path).map(|(_, c)| c)), - None => Err(e), - } - } - }; - let files = collect_files(&layout)?; - match files.iter().find(|e| e.path == path) { - Some(e) => Ok(Some(extract_file(data, &layout, e)?)), - None => Ok(None), - } -} - -/// CLI entry point: `wolf-unpack [output_dir] [key_string]`. Traces the head-table -/// decode with `key_string` and optionally extracts. -pub fn run(args: &[String]) -> io::Result<()> { - if args.len() < 2 { - eprintln!("usage: wolf-unpack [output_dir] [key_string]"); - std::process::exit(2); - } - - let archive_path = PathBuf::from(&args[1]); - let out_dir = args.get(2).map(PathBuf::from); - let key_string = args - .get(3) - .map(|s| s.as_bytes().to_vec()) - .unwrap_or_else(|| DEFAULT_WOLF_PRO_KEY.to_vec()); - - let mut data = Vec::new(); - fs::File::open(&archive_path)?.read_to_end(&mut data)?; - - let key = key_create(&key_string); - println!("archive: {}", archive_path.display()); - println!("key: {}", hex(&key)); - - let layout = detect_layout(&data, &key_string)?; - println!("layout: {}", layout.source); - println!( - "table={} data_start={} file_table={} dir_table={} huff_kb={} flags=0x{:08x} key_pos={}", - layout.table.len(), - layout.data_start, - layout.file_table_start, - layout.directory_table_start, - layout.huffman_encode_kb, - layout.flags, - layout.main_key_pos - ); - - let files = collect_files(&layout)?; - println!("files: {}", files.len()); - for (i, entry) in files.iter().take(20).enumerate() { - println!( - "{:5} {:10} {:10} {:10} {}", - i, - entry.head.data_size, - display_opt_size(entry.head.press_size), - display_opt_size(entry.head.huff_size), - entry.path - ); - } - - if let Some(out_dir) = out_dir { - extract_all(&data, &layout, &files, &out_dir)?; - println!("extracted to {}", out_dir.display()); - } - - Ok(()) -} -pub(crate) fn read_u16(b: &[u8]) -> u16 { - u16::from_le_bytes([b[0], b[1]]) -} - -pub(crate) fn read_u32(b: &[u8]) -> u32 { - u32::from_le_bytes([b[0], b[1], b[2], b[3]]) -} - -pub(crate) fn read_u64(b: &[u8]) -> u64 { - u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) -} - -pub(crate) fn decode_filename(raw: &[u8]) -> String { - // Archive filenames are ASCII/UTF-8 in newer (3.x) Wolf builds and Shift-JIS in older - // Japanese ones. A lossy UTF-8 decode turns SJIS lead bytes into U+FFFD while leaving - // their trailing bytes (e.g. 0x7C `|`, 0x3C `<`) as literal ASCII. Those are invalid in - // Windows paths (ERROR_INVALID_NAME) and abort the unpack. Decode as Shift-JIS whenever - // the bytes are not already valid UTF-8. - let s = match std::str::from_utf8(raw) { - Ok(s) => std::borrow::Cow::Borrowed(s), - Err(_) => encoding_rs::SHIFT_JIS.decode(raw).0, - }; - s.replace('/', "\\") -} - -pub(crate) fn safe_relative_path(path: &str) -> PathBuf { - let mut out = PathBuf::new(); - for part in path.split(['\\', '/']) { - if part.is_empty() || part == "." || part == ".." { - continue; - } - let cleaned: String = part - .chars() - .map(|c| match c { - '<' | '>' | ':' | '"' | '|' | '?' | '*' => '_', - _ => c, - }) - .collect(); - out.push(cleaned); - } - out -} - -pub(crate) fn display_opt_size(v: u64) -> String { - if v == NONE { - "-".to_string() - } else { - v.to_string() - } -} - -pub(crate) fn hex(bytes: &[u8]) -> String { - bytes - .iter() - .map(|b| format!("{b:02X}")) - .collect::>() - .join("") -} - -pub(crate) fn invalid(msg: E) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, msg.to_string()) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/src/pack.rs b/util/wolfdawn-master/crates/wolf-archive/src/pack.rs deleted file mode 100644 index 7d38fd5..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/src/pack.rs +++ /dev/null @@ -1,527 +0,0 @@ -//! DXArchive (VER8) writer: the inverse of [`crate::dxarchive`]. -//! -//! Produces a `.wolf` the engine loads (DXLib's `NO_HEAD_PRESS` mode, plus `NO_KEY` for the -//! plaintext variant) that our own extractor round-trips byte-for-byte. Follows DXLib's -//! `EncodeArchive`/`DirectoryEncode` address semantics (root self-filehead, per-directory -//! contiguous file runs, name-table parity), so the container is a real DXArchive. Re-encryption -//! per `cryptVersion` layers on top of the plaintext build, reusing the reader's symmetric -//! decrypt as the encrypt. -//! -//! ## Known deviations from an editor-produced archive (do not affect loading) -//! * Filenames are stored as UTF-8 bytes with the header declaring codepage 932. For the -//! all-ASCII paths Wolf uses internally (`Game.dat`, `MapData/Map001.mps`, …) UTF-8 == SJIS, -//! so this is exact. A non-ASCII filename would need Shift-JIS encoding to match the -//! engine's `packNum`/parity file search, which is not yet done (this crate is -//! zero-dependency). -//! * Inter-file 4-byte alignment padding is written as plaintext zeros (the reader reads -//! exactly `data_size` and ignores it). DXLib keys the padded length. So the archive -//! round-trips through our reader and loads, but is not byte-identical to the editor's. - -use std::collections::BTreeMap; - -use crate::crypto::{ - key_conv, key_create, read_wolf_archive_slice, wolf_aes_body_size, wolf_aes_init_round_key, - wolf_crypt, wolf_crypt_addresses, wolf_init_key, -}; -use crate::dxarchive::{collect_files, file_key, parse_full_head}; -use crate::{ - Layout, WolfContext, DEFAULT_WOLF_V310_KEY, DEFAULT_WOLF_V3173_KEY, DEFAULT_WOLF_V331_KEY, - DEFAULT_WOLF_V350_KEY, KEY_BYTES, -}; - -const HEADER_SIZE: usize = 64; -const FILEHEAD_SIZE: usize = 72; -const NONE: u64 = u64::MAX; - -const DX_HEAD: u16 = 0x5844; // *(u16*)"DX" -const DX_VER_8: u16 = 0x0008; -const FLAG_NO_KEY: u32 = 0x01; -const FLAG_NO_HEAD_PRESS: u32 = 0x02; -const CHAR_CODE_SJIS: u32 = 0x3A4; // codepage 932 -const ATTR_DIRECTORY: u64 = 0x10; -const ATTR_ARCHIVE: u64 = 0x20; - -/// A directory node built from the flat path list (references the caller's file bytes). -/// Shared with the VER5/VER6 encoder ([`crate::dxarc_v2::encode`]). -#[derive(Default)] -pub(crate) struct Node<'a> { - pub(crate) dirs: BTreeMap>, - pub(crate) files: BTreeMap, -} - -impl<'a> Node<'a> { - pub(crate) fn entry_count(&self) -> usize { - self.dirs.len() + self.files.len() - } - - pub(crate) fn insert(&mut self, parts: &[&str], data: &'a [u8]) { - match parts { - [] => {} - [name] => { - self.files.insert((*name).to_string(), data); - } - [dir, rest @ ..] => { - self.dirs - .entry((*dir).to_string()) - .or_default() - .insert(rest, data); - } - } - } -} - -/// The four growing sections of a DXArchive (mirrors DXLib's `SIZESAVE` + buffers). -#[derive(Default)] -struct Build { - name: Vec, - file: Vec, - dir: Vec, - data: Vec, -} - -/// Pack a flat `(path, bytes)` list into an unencrypted, uncompressed `.wolf` archive. -/// Paths may use `/` or `\` separators. Returns the archive bytes. Errors only if the list -/// is empty (DXArchive requires at least one entry under the root). -pub fn pack_plaintext(files: &[(String, Vec)]) -> Result, String> { - if files.is_empty() { - return Err("cannot pack an empty file list".to_string()); - } - - let mut root = Node::default(); - for (path, data) in files { - let parts: Vec<&str> = path.split(['\\', '/']).filter(|s| !s.is_empty()).collect(); - if parts.is_empty() { - return Err(format!("invalid (empty) path in file list: {path:?}")); - } - root.insert(&parts, data); - } - - let mut b = Build::default(); - - // Root self-filehead (empty name) at file-table offset 0, root DARC_DIRECTORY at dir - // offset 0 (parent = NONE). Mirrors DXLib's EncodeArchive root setup. - add_filename(&mut b.name, ""); // name_address 0 - b.file.resize(FILEHEAD_SIZE, 0); - write_filehead(&mut b.file, 0, 0, ATTR_DIRECTORY, 0, 0); - - let root_num = root.entry_count() as u64; - let root_fha = b.file.len() as u64; // = 72 - append_directory(&mut b.dir, 0, NONE, root_num, root_fha); - b.file - .resize(b.file.len() + FILEHEAD_SIZE * root_num as usize, 0); - - encode_children(&mut b, &root, root_fha, 0); - - // Assemble: HEADER | file data | name table | file table | directory table. - let head_size = b.name.len() + b.file.len() + b.dir.len(); - let mut out = Vec::with_capacity(HEADER_SIZE + b.data.len() + head_size); - write_header( - &mut out, - head_size as u32, - (HEADER_SIZE + b.data.len()) as u64, // name_table_start - b.name.len() as u64, // file_table_start (rel to name table) - (b.name.len() + b.file.len()) as u64, // directory_table_start (rel) - ); - out.extend_from_slice(&b.data); - out.extend_from_slice(&b.name); - out.extend_from_slice(&b.file); - out.extend_from_slice(&b.dir); - Ok(out) -} - -/// Re-encrypt a directory into a keyed VER8 archive for a known cryptVersion (v3.00 = `0x12C`, -/// v3.14 = `0x13A`). Every Wolf cipher is symmetric (XOR-stream), so we build the plaintext -/// container then apply the same `key_conv` the reader undoes. The 64-byte header stays -/// plaintext (only `Flags` is patched), the table block and each file's data are keyed, and -/// `NO_HEAD_PRESS` keeps the header uncompressed so no Huffman encoder is needed. The output -/// decodes through our own deterministic dispatch (see the module-level note for the minor -/// deviations from an editor-produced archive). -pub fn pack_encrypted(files: &[(String, Vec)], crypt_version: u16) -> Result, String> { - let key_string: &[u8] = match crypt_version { - 0x12C => DEFAULT_WOLF_V310_KEY, // Wolf v3.00 - 0x13A => DEFAULT_WOLF_V3173_KEY, // Wolf v3.14 - _ => { - return Err(format!( - "unsupported encrypt cryptVersion {crypt_version:#x} \ - (supported: 0x12c=v3.00, 0x13a=v3.14)" - )) - } - }; - - let mut out = pack_plaintext(files)?; - let head = parse_full_head(&out).map_err(|e| e.to_string())?; - let name_start = head.name_table_start as usize; - let head_size = head.head_size as usize; - let data_start = head.data_start; - - // A reader Layout over the RAW (still-plaintext) table, so per-file keys are computed - // from the same name table the reader will see after it decrypts. - let layout = Layout { - table: out[name_start..name_start + head_size].to_vec(), - data_start, - file_table_start: head.file_table_start as usize, - directory_table_start: head.directory_table_start as usize, - huffman_encode_kb: head.huffman_encode_kb, - flags: head.flags, - key_string: key_string.to_vec(), - main_key_pos: 0, - source: "encrypt".to_string(), - wolf: None, - chacha: None, - }; - let entries = collect_files(&layout).map_err(|e| e.to_string())?; - - // Encrypt each file's data with its per-file key, at the same start offset the reader - // uses (`data_size % KEY_BYTES`). - for entry in &entries { - let key = file_key(&layout, entry).map_err(|e| e.to_string())?; - let start = (data_start + entry.head.data_addr) as usize; - let len = entry.head.data_size as usize; - key_conv(&mut out[start..start + len], &key, len % KEY_BYTES); - } - - // Encrypt the table block (header stays plaintext) and patch Flags (clears NO_KEY). - key_conv( - &mut out[name_start..name_start + head_size], - &key_create(key_string), - 0, - ); - let flags = ((crypt_version as u32) << 16) | FLAG_NO_HEAD_PRESS; - out[44..48].copy_from_slice(&flags.to_le_bytes()); - - Ok(out) -} - -/// Re-encrypt into a "ChaCha2" archive (cryptVersion `0x64`). The header stays plaintext, the -/// table and each file's data are ChaCha20-keyed with the fixed chacha2 key/nonce (position 0 -/// for the table, `data_size` for each file, matching our decoder). ChaCha20 is symmetric, so -/// this is again the reader's decode applied to plaintext. Self-consistent with our extractor. -/// The ChaCha2 decode path itself is unverified against a real ChaCha2 game (none in the -/// corpus). -pub fn pack_chacha(files: &[(String, Vec)]) -> Result, String> { - let mut out = pack_plaintext(files)?; - let head = parse_full_head(&out).map_err(|e| e.to_string())?; - let name_start = head.name_table_start as usize; - let head_size = head.head_size as usize; - let data_start = head.data_start; - - let ck: [u8; 32] = crate::DEFAULT_WOLF_CHACHA2_KEY[0..32] - .try_into() - .map_err(|_| "chacha key")?; - let cn: [u8; 12] = crate::DEFAULT_WOLF_CHACHA2_KEY[32..44] - .try_into() - .map_err(|_| "chacha nonce")?; - - // Enumerate files from the plaintext table. - let layout = Layout { - table: out[name_start..name_start + head_size].to_vec(), - data_start, - file_table_start: head.file_table_start as usize, - directory_table_start: head.directory_table_start as usize, - huffman_encode_kb: head.huffman_encode_kb, - flags: head.flags, - key_string: Vec::new(), - main_key_pos: 0, - source: "chacha-encode".to_string(), - wolf: None, - chacha: Some((ck, cn)), - }; - let entries = collect_files(&layout).map_err(|e| e.to_string())?; - for entry in &entries { - let start = (data_start + entry.head.data_addr) as usize; - let len = entry.head.data_size as usize; - crate::chacha20::crypt(&mut out[start..start + len], &ck, &cn, len as u32); - } - // Table at position 0. - crate::chacha20::crypt(&mut out[name_start..name_start + head_size], &ck, &cn, 0); - - let flags = (0x64u32 << 16) | FLAG_NO_HEAD_PRESS; - out[44..48].copy_from_slice(&flags.to_le_bytes()); - Ok(out) -} - -/// Read the cryptVersion (`Flags>>16`) and the embedded 15-byte password from an existing -/// archive header (both live plaintext in the header). Feed these to [`pack_newcrypt`] to -/// repack a directory under the same encryption an original `Data.wolf` uses. -pub fn archive_crypt_params(data: &[u8]) -> Option<(u16, [u8; 15])> { - if data.len() < 64 { - return None; - } - let flags = u32::from_le_bytes(data[44..48].try_into().ok()?); - let mut pwd = [0u8; 15]; - pwd.copy_from_slice(&data[49..64]); - Some(((flags >> 16) as u16, pwd)) -} - -/// Re-encrypt into a WolfPro "new-crypt" archive (v3.31 = `0x14B`, v3.50 / v3.5 = `0x15E`). -/// The whole new-crypt transform (768-byte stream, AES-CTR, header address scramble) is -/// XOR-based, hence an involution, so encryption is exactly the reader's decrypt applied to -/// the plaintext container, reusing the tested primitives. The 15-byte `pwd` is embedded in the -/// header (bytes 49..64) just as the engine stores it. Re-encrypting GamePro with its own -/// embedded password yields a game-loadable archive (the Game.dat hash still validates). -/// `NO_HEAD_PRESS` keeps the header uncompressed (no Huffman encoder needed). -pub fn pack_newcrypt( - files: &[(String, Vec)], - crypt_version: u16, - pwd: &[u8; 15], -) -> Result, String> { - let key_string: &[u8] = match crypt_version { - 0x14B => DEFAULT_WOLF_V331_KEY, // Wolf Pro v3.31 - 0x15E => DEFAULT_WOLF_V350_KEY, // Wolf Pro v3.50 / v3.5 - _ => { - return Err(format!( - "unsupported new-crypt cryptVersion {crypt_version:#x} \ - (supported: 0x14b=v3.31, 0x15e=v3.50)" - )) - } - }; - - let mut out = pack_plaintext(files)?; - let head = parse_full_head(&out).map_err(|e| e.to_string())?; - let name_start = head.name_table_start as usize; - let head_size = head.head_size as usize; - let data_start = head.data_start; - - // Patch the header: embed the password, set new-crypt flags (cv<<16 | NO_HEAD_PRESS). - out[49..64].copy_from_slice(pwd); - let flags = ((crypt_version as u32) << 16) | FLAG_NO_HEAD_PRESS; - out[44..48].copy_from_slice(&flags.to_le_bytes()); - - // Build the context exactly as the reader will recompute it (file_size + embedded pwd). - let file_size = out.len(); - let ctx = WolfContext { - crypt_version, - other_key: wolf_init_key(crypt_version, pwd, None, true, key_string), - special_key: wolf_init_key(crypt_version, pwd, None, false, key_string), - aes_round_key: wolf_aes_init_round_key(pwd, None, crypt_version), - body_size: wolf_aes_body_size(file_size, crypt_version, pwd, None), - name_table_start: name_start, - }; - - // Enumerate files from the still-plaintext table (collect_files only walks the table). - let layout = Layout { - table: out[name_start..name_start + head_size].to_vec(), - data_start, - file_table_start: head.file_table_start as usize, - directory_table_start: head.directory_table_start as usize, - huffman_encode_kb: head.huffman_encode_kb, - flags, - key_string: key_string.to_vec(), - main_key_pos: 0, - source: "newcrypt-encode".to_string(), - wolf: Some(ctx.clone()), - chacha: None, - }; - let entries = collect_files(&layout).map_err(|e| e.to_string())?; - - // Encrypt each file's data: ciphertext = wolf_crypt_special(S(plaintext)), i.e. the - // reader's decrypt path, which is its own inverse. File regions are disjoint, so order - // is irrelevant and reading one never depends on another. - for entry in &entries { - let start = (data_start + entry.head.data_addr) as usize; - let len = entry.head.data_size as usize; - let mut slice = read_wolf_archive_slice(&out, start, len, &ctx) - .ok_or("file data slice out of range")?; - wolf_crypt(&ctx.special_key, &mut slice, len, crypt_version); - out[start..start + len].copy_from_slice(&slice); - } - - // Encrypt the table block (still plaintext at this point). - let mut table = read_wolf_archive_slice(&out, name_start, head_size, &ctx) - .ok_or("table slice out of range")?; - wolf_crypt(&ctx.special_key, &mut table, 0, crypt_version); - out[name_start..name_start + head_size].copy_from_slice(&table); - - // Encrypt the header address fields (bytes 8..40). The password at 49..64 stays plaintext. - let mut header = out[..64].to_vec(); - wolf_crypt_addresses(&mut header, pwd, crypt_version); - out[..64].copy_from_slice(&header); - - Ok(out) -} - -/// Write the `data_number`-th entry slots of a directory whose children block starts at -/// `fha`. Subdirectories recurse (each writes its own self-filehead into this block). Files -/// write a leaf filehead and append their (4-aligned) data. `this_dir_addr` is this -/// directory's own DirectoryAddress (used by children for ParentDirectoryAddress). -fn encode_children(b: &mut Build, node: &Node, fha: u64, this_dir_addr: u64) { - let mut i = 0u64; - for (name, child) in &node.dirs { - directory_encode(b, child, name, fha, this_dir_addr, i); - i += 1; - } - for (name, data) in &node.files { - write_file_entry(b, name, data, fha, i); - i += 1; - } -} - -/// Encode one subdirectory: its self-filehead goes into the parent's reserved slot -/// (`parent_fha + data_number*72`). Its own DARC_DIRECTORY and children block follow. -fn directory_encode( - b: &mut Build, - node: &Node, - name: &str, - parent_fha: u64, - parent_dir_addr: u64, - data_number: u64, -) { - // This dir's self-filehead (written into the parent's file run). - let name_addr = b.name.len() as u64; - let this_dir_offset = b.dir.len() as u64; - add_filename(&mut b.name, name); - let self_fh_off = (parent_fha + data_number * FILEHEAD_SIZE as u64) as usize; - write_filehead( - &mut b.file, - self_fh_off, - name_addr, - ATTR_DIRECTORY, - this_dir_offset, - 0, - ); - - let this_fha = b.file.len() as u64; - // ParentDirectoryAddress = DataAddress field of the parent's self-filehead (= parent's - // dir-table offset), or 0 for children of the root (whose DirectoryAddress is 0). - let parent_directory_address = if parent_dir_addr != NONE && parent_dir_addr != 0 { - read_u64(&b.file, parent_dir_addr as usize + 40) - } else { - 0 - }; - let num = node.entry_count() as u64; - append_directory( - &mut b.dir, - self_fh_off as u64, - parent_directory_address, - num, - this_fha, - ); - b.file - .resize(b.file.len() + FILEHEAD_SIZE * num as usize, 0); - - encode_children(b, node, this_fha, self_fh_off as u64); -} - -/// Write a leaf file: its filehead into slot `data_number` of the run at `fha`, its bytes -/// (4-aligned) into the data section. -fn write_file_entry(b: &mut Build, name: &str, data: &[u8], fha: u64, data_number: u64) { - let name_addr = b.name.len() as u64; - add_filename(&mut b.name, name); - let data_addr = b.data.len() as u64; - let off = (fha + data_number * FILEHEAD_SIZE as u64) as usize; - write_filehead( - &mut b.file, - off, - name_addr, - ATTR_ARCHIVE, - data_addr, - data.len() as u64, - ); - b.data.extend_from_slice(data); - while b.data.len() % 4 != 0 { - b.data.push(0); - } -} - -/// Append a DXArchive name-table entry: `[u16 packNum][u16 parity][UPPER name][original name]`. -/// `packNum = ceil((len+1)/4)` and parity = sum of the uppercased name bytes. The engine's file -/// search matches on packNum and parity, so both must be exact. The name-table format is -/// identical across v8 and VER5/VER6, so the VER5/VER6 encoder ([`crate::dxarc_v2::encode`]) -/// reuses this. -pub(crate) fn add_filename(buf: &mut Vec, name: &str) { - let s = name.as_bytes(); - if s.is_empty() { - buf.extend_from_slice(&[0, 0, 0, 0]); // packNum 0, parity 0 - return; - } - let length = s.len() + 1; // include the trailing NUL - let pack_num = (length + 3) / 4; - let entry_size = pack_num * 4 * 2 + 4; - let start = buf.len(); - buf.resize(start + entry_size, 0); - - let upper_off = start + 4; - let orig_off = start + 4 + pack_num * 4; - buf[orig_off..orig_off + s.len()].copy_from_slice(s); // original name (NUL already 0) - - let mut parity: u32 = 0; - let mut i = 0; - while i < s.len() { - let lead = s[i]; - // Shift-JIS lead bytes carry a 2-byte char copied verbatim (no upper-casing). - if (0x81..=0x9F).contains(&lead) || (0xE0..=0xFC).contains(&lead) { - if i + 1 < s.len() { - buf[upper_off + i] = s[i]; - buf[upper_off + i + 1] = s[i + 1]; - parity += s[i] as u32 + s[i + 1] as u32; - i += 2; - continue; - } - } - let up = if lead.is_ascii_lowercase() { - lead - b'a' + b'A' - } else { - lead - }; - buf[upper_off + i] = up; - parity += up as u32; - i += 1; - } - buf[start + 2..start + 4].copy_from_slice(&(parity as u16).to_le_bytes()); - buf[start..start + 2].copy_from_slice(&(pack_num as u16).to_le_bytes()); -} - -fn write_filehead( - file: &mut [u8], - off: usize, - name_addr: u64, - attrs: u64, - data_addr: u64, - data_size: u64, -) { - let b = &mut file[off..off + FILEHEAD_SIZE]; - b[0..8].copy_from_slice(&name_addr.to_le_bytes()); - b[8..16].copy_from_slice(&attrs.to_le_bytes()); - // Time (create / last-access / last-write) left zero. - b[40..48].copy_from_slice(&data_addr.to_le_bytes()); - b[48..56].copy_from_slice(&data_size.to_le_bytes()); - b[56..64].copy_from_slice(&NONE.to_le_bytes()); // PressDataSize: not compressed - b[64..72].copy_from_slice(&NONE.to_le_bytes()); // HuffPressDataSize: not compressed -} - -fn append_directory( - dir: &mut Vec, - directory_addr: u64, - parent_addr: u64, - file_head_num: u64, - file_head_addr: u64, -) { - dir.extend_from_slice(&directory_addr.to_le_bytes()); - dir.extend_from_slice(&parent_addr.to_le_bytes()); - dir.extend_from_slice(&file_head_num.to_le_bytes()); - dir.extend_from_slice(&file_head_addr.to_le_bytes()); -} - -fn write_header( - out: &mut Vec, - head_size: u32, - name_table_start: u64, - file_table_start: u64, - directory_table_start: u64, -) { - out.extend_from_slice(&DX_HEAD.to_le_bytes()); - out.extend_from_slice(&DX_VER_8.to_le_bytes()); - out.extend_from_slice(&head_size.to_le_bytes()); - out.extend_from_slice(&(HEADER_SIZE as u64).to_le_bytes()); // DataStartAddress - out.extend_from_slice(&name_table_start.to_le_bytes()); - out.extend_from_slice(&file_table_start.to_le_bytes()); - out.extend_from_slice(&directory_table_start.to_le_bytes()); - out.extend_from_slice(&CHAR_CODE_SJIS.to_le_bytes()); - out.extend_from_slice(&(FLAG_NO_KEY | FLAG_NO_HEAD_PRESS).to_le_bytes()); // cryptVersion 0 - out.push(0); // HuffmanEncodeKB (unused: nothing is compressed) - out.extend_from_slice(&[0u8; 15]); // Reserve -} - -fn read_u64(b: &[u8], off: usize) -> u64 { - u64::from_le_bytes(b[off..off + 8].try_into().unwrap()) -} diff --git a/util/wolfdawn-master/crates/wolf-archive/tests/extract_golden.rs b/util/wolfdawn-master/crates/wolf-archive/tests/extract_golden.rs deleted file mode 100644 index 443e180..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/tests/extract_golden.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Golden gate: extracting `Data.wolf` must reproduce the loose `Data/` tree byte-for-byte. -//! Guards the archive decoder against any future crypto refactor. Skips gracefully when the -//! 74 MB archive isn't present. - -use std::path::PathBuf; - -fn root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") -} - -#[test] -fn data_wolf_extracts_byte_identical() { - // Heavy (74 MB, decode attempts). Opt-in via WOLF_GOLDEN=1. - if std::env::var_os("WOLF_GOLDEN").is_none() { - eprintln!("set WOLF_GOLDEN=1 to run the Data.wolf golden extraction test"); - return; - } - let archive = root().join("Data.wolf"); - let Ok(bytes) = std::fs::read(&archive) else { - eprintln!("Data.wolf not present, skipping golden extraction test"); - return; - }; - - // This particular Data.wolf is a Wolf v3.x container that needs the deterministic - // `Flags>>16` version dispatch. Until that lands the heuristic decoder can't decode it, - // so a decode failure is reported, not asserted. - let files = match wolf_archive::extract_archive(&bytes, wolf_archive::DEFAULT_WOLF_PRO_KEY) { - Ok(f) => f, - Err(e) => { - eprintln!("archive decode not yet supported for this Data.wolf (pending version dispatch): {e}"); - return; - } - }; - eprintln!("extracted {} files from Data.wolf", files.len()); - assert!( - files.len() > 100, - "expected a populated archive, got {}", - files.len() - ); - - // Compare every extracted file that also exists loose under Data/, byte-for-byte. - let loose_root = root().join("Data"); - let mut compared = 0usize; - let mut mismatched = Vec::new(); - for (path, contents) in &files { - let loose = loose_root.join(path.replace('\\', "/")); - let Ok(disk) = std::fs::read(&loose) else { - continue; - }; - compared += 1; - if &disk != contents { - mismatched.push(path.clone()); - } - } - let matched = compared - mismatched.len(); - eprintln!("compared {compared} files against loose Data/, {matched} byte-exact, {} differ (build diff)", - mismatched.len()); - - // The archive decode is byte-perfect: files unchanged between the archived build and the - // loose tree extract byte-identical. The loose Data/ here is a slightly different/older - // build, so its editable BasicData/MapData files legitimately differ. Those bytes are - // proven valid and complete by the `archive_end_to_end` round-trip gate in wolf-formats. - assert!( - files.len() > 2000, - "expected a fully-populated archive, got {}", - files.len() - ); - assert!( - matched >= 800, - "too few byte-exact matches ({matched}); decode likely regressed" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-archive/tests/gamepro_archive.rs b/util/wolfdawn-master/crates/wolf-archive/tests/gamepro_archive.rs deleted file mode 100644 index 6c2c0f8..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/tests/gamepro_archive.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! New wolf-crypt (v3.31) decode against the real GameProData.wolf (1.26 GB). -//! WOLF_GOLDEN=1 cargo test -p wolf-archive --test gamepro_archive -- --nocapture -use std::path::PathBuf; - -#[test] -fn gamepro_archive_decodes() { - if std::env::var_os("WOLF_GOLDEN").is_none() { - eprintln!("set WOLF_GOLDEN=1 to run the (heavy) GameProData.wolf test"); - return; - } - let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("GameProData.wolf"); - let Ok(data) = std::fs::read(&p) else { - eprintln!("no GameProData.wolf"); - return; - }; - eprintln!("read {} bytes", data.len()); - - let paths = wolf_archive::list_archive(&data, wolf_archive::DEFAULT_WOLF_PRO_KEY) - .expect("new-crypt (v3.31) table decode"); - eprintln!("new-crypt table decode OK: {} files", paths.len()); - assert!(paths.len() > 1000, "expected a populated Pro archive"); - - // The loose GamePro_Data/ tree is this archive's extraction. Verify a sample of files - // are byte-identical (proves the new wolf-crypt decode is byte-perfect). - let loose = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("GamePro_Data"); - let samples = [ - "BasicData\\CommonEvent.dat", - "BasicData\\DataBase.dat", - "BasicData\\CDataBase.dat", - "BasicData\\SysDatabase.dat", - "BasicData\\Game.dat", - "BasicData\\TileSetData.dat", - "MapData\\Map001.mps", - ]; - let mut checked = 0; - for s in samples { - let Some(ext) = wolf_archive::extract_one(&data, wolf_archive::DEFAULT_WOLF_PRO_KEY, s) - .expect("extract") - else { - continue; - }; - let Ok(disk) = std::fs::read(loose.join(s.replace('\\', "/"))) else { - continue; - }; - assert_eq!( - ext, disk, - "{s} must extract byte-identical to loose GamePro_Data" - ); - eprintln!(" byte-exact: {s} ({} bytes)", ext.len()); - checked += 1; - } - assert!(checked > 0, "no sample files compared"); -} diff --git a/util/wolfdawn-master/crates/wolf-archive/tests/pack_roundtrip.rs b/util/wolfdawn-master/crates/wolf-archive/tests/pack_roundtrip.rs deleted file mode 100644 index 78813e3..0000000 --- a/util/wolfdawn-master/crates/wolf-archive/tests/pack_roundtrip.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Plaintext packer gate: `extract(pack(files)) == files`. -//! -//! Proves the VER8 writer produces a container our own extractor reads back with identical -//! paths and bytes, across a synthetic nested tree and a real Wolf `Data/` subtree. -//! cargo test -p wolf-archive --test pack_roundtrip -- --nocapture - -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use wolf_archive::{extract_archive, pack_chacha, pack_encrypted, pack_newcrypt, pack_plaintext}; - -/// Normalize a path to forward slashes for order-independent comparison. -fn norm(p: &str) -> String { - p.replace('\\', "/") -} - -fn roundtrip(files: &[(String, Vec)]) { - let archive = pack_plaintext(files).expect("pack"); - let extracted = extract_archive(&archive, b"").expect("extract our own archive"); - - let want: BTreeMap> = files.iter().map(|(p, d)| (norm(p), d.clone())).collect(); - let got: BTreeMap> = - extracted.into_iter().map(|(p, d)| (norm(&p), d)).collect(); - - assert_eq!( - got.keys().collect::>(), - want.keys().collect::>(), - "path set differs" - ); - for (p, data) in &want { - assert_eq!(got.get(p), Some(data), "bytes differ for {p}"); - } -} - -#[test] -fn synthetic_tree_round_trips() { - let files = vec![ - ("BasicData/CommonEvent.dat".to_string(), vec![1, 2, 3, 4, 5]), - ("BasicData/Game.dat".to_string(), b"hello game".to_vec()), - ("MapData/Map001.mps".to_string(), vec![0xAA; 1000]), - ("MapData/Map002.mps".to_string(), vec![]), // empty file - ("MapData/sub/Deep.mps".to_string(), vec![7; 3]), // odd length (alignment) - ("top.txt".to_string(), b"a top-level file".to_vec()), - ]; - roundtrip(&files); -} - -#[test] -fn encrypted_round_trips_through_our_reader() { - // pack_encrypted(cv) -> extract_archive (auto-detects cryptVersion = Flags>>16) -> files. - // Proves the symmetric re-encryption decodes through our deterministic dispatch. - let files = vec![ - ( - "BasicData/Game.dat".to_string(), - vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9], - ), - ("BasicData/CommonEvent.dat".to_string(), vec![0xCD; 777]), - ("MapData/Map001.mps".to_string(), b"map data here".to_vec()), - ("MapData/sub/Deep.mps".to_string(), vec![9, 8, 7]), - ]; - let want: BTreeMap> = files.iter().map(|(p, d)| (norm(p), d.clone())).collect(); - - for cv in [0x12Cu16, 0x13A] { - let archive = pack_encrypted(&files, cv).expect("encrypt"); - // The archive must NOT be plaintext: data should be transformed. - let extracted = extract_archive(&archive, b"").expect("extract encrypted"); - let got: BTreeMap> = - extracted.into_iter().map(|(p, d)| (norm(&p), d)).collect(); - assert_eq!(got, want, "cryptVersion {cv:#x} did not round-trip"); - } -} - -#[test] -fn wolfpro_newcrypt_round_trips_through_our_reader() { - // pack_newcrypt(cv, pwd) -> extract_archive (recomputes the context from the embedded - // password) -> files. Proves the symmetric WolfPro stream + AES-CTR + address scramble - // encode is the exact inverse of our verified decrypt path. - let files = vec![ - ("BasicData/Game.dat".to_string(), vec![0x11u8; 2048]), - ("BasicData/CommonEvent.dat".to_string(), vec![0x42; 5000]), - ("MapData/Map001.mps".to_string(), b"hello wolf pro".to_vec()), - ("MapData/sub/Deep.mps".to_string(), (0..255u8).collect()), - ]; - let want: BTreeMap> = files.iter().map(|(p, d)| (norm(p), d.clone())).collect(); - - let pwd: [u8; 15] = [ - 0x37, 0x9a, 0x14, 0xc2, 0x5b, 0x08, 0xe1, 0x4f, 0x2d, 0x71, 0xbc, 0x33, 0x96, 0x6a, 0xd8, - ]; - for cv in [0x14Bu16, 0x15E] { - let archive = pack_newcrypt(&files, cv, &pwd).expect("newcrypt encode"); - let extracted = extract_archive(&archive, b"").expect("decode newcrypt"); - let got: BTreeMap> = - extracted.into_iter().map(|(p, d)| (norm(&p), d)).collect(); - assert_eq!(got, want, "WolfPro cryptVersion {cv:#x} did not round-trip"); - } -} - -#[test] -fn chacha2_round_trips_through_our_reader() { - let files = vec![ - ("BasicData/Game.dat".to_string(), vec![0x5Au8; 4096]), - ( - "MapData/Map001.mps".to_string(), - b"chacha20 round trip".to_vec(), - ), - ("MapData/sub/Deep.mps".to_string(), (0..200u8).collect()), - ]; - let want: BTreeMap> = files.iter().map(|(p, d)| (norm(p), d.clone())).collect(); - let archive = pack_chacha(&files).expect("chacha encode"); - let extracted = extract_archive(&archive, b"").expect("decode chacha2"); - let got: BTreeMap> = - extracted.into_iter().map(|(p, d)| (norm(&p), d)).collect(); - assert_eq!(got, want, "ChaCha2 did not round-trip"); -} - -#[test] -fn real_data_subtree_round_trips() { - // Pack a real BasicData folder (ASCII paths, real binary content) and read it back. - let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/BasicData"); - if !base.exists() { - eprintln!("no Data/BasicData corpus, skipping"); - return; - } - - let mut files = Vec::new(); - collect(&base, &base, &mut files); - files.sort(); - if files.is_empty() { - eprintln!("no files found, skipping"); - return; - } - eprintln!("packing {} real files", files.len()); - roundtrip(&files); -} - -fn collect(base: &Path, dir: &Path, out: &mut Vec<(String, Vec)>) { - let Ok(rd) = std::fs::read_dir(dir) else { - return; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - collect(base, &p, out); - } else if let Ok(bytes) = std::fs::read(&p) { - let rel = p - .strip_prefix(base) - .unwrap() - .to_string_lossy() - .replace('\\', "/"); - out.push((format!("BasicData/{rel}"), bytes)); - } - } -} - -#[test] -fn ver5_ver6_round_trip_through_our_reader() { - use wolf_archive::{pack_ver5, pack_ver6}; - let files = vec![ - ("BasicData/Game.dat".to_string(), vec![0x3Cu8; 1234]), - ( - "BasicData/CommonEvent.dat".to_string(), - b"old wolf 2.0x".to_vec(), - ), - ("MapData/Map001.mps".to_string(), (0..250u8).collect()), - ("MapData/sub/Deep.mps".to_string(), vec![5, 4, 3, 2, 1]), - ]; - let want: BTreeMap> = files.iter().map(|(p, d)| (norm(p), d.clone())).collect(); - - for (label, archive) in [ - ("VER5", pack_ver5(&files).expect("ver5 encode")), - ("VER6", pack_ver6(&files).expect("ver6 encode")), - ] { - let extracted = extract_archive(&archive, b"").expect("decode legacy archive"); - let got: BTreeMap> = - extracted.into_iter().map(|(p, d)| (norm(&p), d)).collect(); - assert_eq!(got, want, "{label} did not round-trip"); - } -} diff --git a/util/wolfdawn-master/crates/wolf-cli/Cargo.toml b/util/wolfdawn-master/crates/wolf-cli/Cargo.toml deleted file mode 100644 index e9f2774..0000000 --- a/util/wolfdawn-master/crates/wolf-cli/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "wolf-cli" -edition.workspace = true -version.workspace = true -license.workspace = true -description = "The `wolf` command-line front-end for the WolfDawn toolchain." - -[[bin]] -name = "wolf" -path = "src/main.rs" - -[dependencies] -wolf-core = { path = "../wolf-core" } -wolf-formats = { path = "../wolf-formats" } -wolf-decompiler = { path = "../wolf-decompiler" } -wolf-archive = { path = "../wolf-archive" } diff --git a/util/wolfdawn-master/crates/wolf-cli/src/cmd.rs b/util/wolfdawn-master/crates/wolf-cli/src/cmd.rs deleted file mode 100644 index a1adbe0..0000000 --- a/util/wolfdawn-master/crates/wolf-cli/src/cmd.rs +++ /dev/null @@ -1,2754 +0,0 @@ -//! Subcommand implementations for the `wolf` CLI. - -use std::path::{Path, PathBuf}; -use std::process::ExitCode; - -use wolf_decompiler::{ - apply_database_edit, apply_game_dat, apply_memory, audit_common_events_to_json, - audit_db_to_json, audit_map_to_json, build_memory, check_name_conflicts, - compile_common_events_edit, compile_map_edit, database_to_json, db_strings_to_json, - decompile_common_events, decompile_common_events_edit_annotated, decompile_map, - decompile_map_edit_annotated, dropped_sources, dump_game_dat, extract_common_events, - extract_db_strings, extract_game_dat, extract_map, extract_names, extract_txt_events, - inject_common_events, inject_db_strings, inject_game_dat, inject_map, inject_names, - inject_txt_events, scenes_to_json, update_save, InjectOptions, InjectStats, MergeStats, - SymbolTable, -}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::game_dat::GameDat; -use wolf_formats::map::Map; - -pub(crate) fn cmd_decompile(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut edit = false; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "--mode" => edit = it.next().map(String::as_str) == Some("edit"), - "--edit" => edit = true, - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!("decompile: missing input file"); - return ExitCode::from(64); - }; - - let result = if edit { - decompile_edit_path(&input) - } else { - decompile_path(&input) - }; - let text = match result { - Ok(t) => t, - Err(e) => { - eprintln!("decompile failed for {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - - match output { - Some(out) => { - if let Err(e) = std::fs::write(&out, text) { - eprintln!("could not write {}: {e}", out.display()); - return ExitCode::from(4); - } - eprintln!("wrote {}", out.display()); - } - None => { - // Ignore a broken pipe (e.g. piping into `head`) instead of panicking. - use std::io::Write; - let _ = std::io::stdout().write_all(text.as_bytes()); - } - } - ExitCode::SUCCESS -} - -/// `wolf export-names `. Dumps the deduplicated **engine/structural** text -/// (variable names, DB type/field names, system-table row names) for building the -/// engine-text glossary. Excludes player-facing content (item/skill/actor display names). -pub(crate) fn cmd_export_names(args: &[String]) -> ExitCode { - use std::collections::BTreeSet; - - let dir = PathBuf::from(args.first().map(String::as_str).unwrap_or(".")); - let basic = if dir.join("CommonEvent.dat").exists() { - dir.clone() - } else if dir.join("BasicData").join("CommonEvent.dat").exists() { - dir.join("BasicData") - } else { - dir.clone() - }; - - let mut symbols = SymbolTable::new(); - if let Ok(b) = std::fs::read(basic.join("CommonEvent.dat")) { - if let Ok(ce) = CommonEventsFile::read(&b) { - symbols.add_common_events(&ce); - } - } - add_databases(&mut symbols, Some(&basic)); - - fn consider(set: &mut BTreeSet, s: &str) { - let t = s.trim(); - let has_jp = t.chars().any(|c| c as u32 >= 0x3000); // CJK/kana - needs translating - // Exclude markup/format-code chars only. `/`, `=`, `(`, `)` are legitimate in stock - // engine variable/field names (e.g. "[読]ネット/状態 -1失敗 0通信中 1終了"), so they stay. - let noisy = t.contains(['\\', '<', '>', '▼', '★', '※']); - if !t.is_empty() && t.chars().count() <= 60 && has_jp && !noisy { - set.insert(t.to_string()); - } - } - - let mut set = BTreeSet::new(); - for ce in symbols.common_events.values() { - for v in ce.self_vars.values() { - consider(&mut set, v); - } - for v in &ce.inputs { - consider(&mut set, v); - } - } - for v in symbols - .globals - .normal - .values() - .chain(symbols.globals.string.values()) - .chain(symbols.globals.system.values()) - { - consider(&mut set, v); - } - for db in &symbols.databases { - for t in &db.types { - consider(&mut set, &t.name); - for f in &t.fields { - consider(&mut set, f); - } - // System/config row names only (SysDB rows + the Basic-System CDB tables); - // User-DB content rows (item/skill names) are player-facing game text. - let system_rows = db.kind == "SDB" || (db.kind == "CDB" && t.name.contains("基本")); - if system_rows { - for d in &t.data { - consider(&mut set, d); - } - } - } - } - - for s in &set { - println!("{s}"); - } - eprintln!("{} unique engine-text strings", set.len()); - ExitCode::SUCCESS -} - -/// `wolf xref `. Finds every write/read of a variable/reference across -/// all maps and common events. Answers "where is this value set?". -pub(crate) fn cmd_xref(args: &[String]) -> ExitCode { - let Some(reff) = args.first() else { - eprintln!("xref: usage: wolf xref \"\" "); - return ExitCode::from(64); - }; - let dir = PathBuf::from(args.get(1).map(String::as_str).unwrap_or(".")); - - let mut files: Vec = Vec::new(); - collect(&dir, &mut files); - files.sort(); - - let mut writes = 0usize; - let mut reads = 0usize; - for path in &files { - if !matches!(classify(path), FileKind::Map | FileKind::CommonEvent) { - continue; - } - let Ok(text) = decompile_path(path) else { - continue; - }; - let rel = rel(&dir, path); - for (i, line) in text.lines().enumerate() { - if !line.contains(reff.as_str()) { - continue; - } - let t = line.trim_start(); - let is_write = is_write_to(t, reff); - if is_write { - writes += 1; - } else { - reads += 1; - } - let tag = if is_write { "SET " } else { "read" }; - println!("{tag} {rel}:{} {t}", i + 1); - } - } - println!("\n{reff}: {writes} write(s), {reads} read(s)"); - ExitCode::SUCCESS -} - -/// True if the decompiled line assigns to `reff` (a write site). -pub(crate) fn is_write_to(line: &str, reff: &str) -> bool { - line.starts_with(&format!("SetVariable {reff} ")) - || line.starts_with(&format!("SetString {reff} =")) - || line.starts_with(&format!("{reff} =")) // DB-read / GetXXX-into form -} - -/// Debug: dump raw (cid, indent, int args, string count) per command. Ground truth for -/// validating the decompiler's argument decoding. -pub(crate) fn cmd_raw(args: &[String]) -> ExitCode { - let Some(input) = args.first() else { - eprintln!("raw: missing input file"); - return ExitCode::from(64); - }; - let path = PathBuf::from(input); - let bytes = match std::fs::read(&path) { - Ok(b) => b, - Err(e) => { - eprintln!("read failed: {e}"); - return ExitCode::from(4); - } - }; - // CommonEvent: dump the per-event metadata strings (name/description/self-var-name - // candidates) to see what naming info WOLF stores. - if matches!(classify(&path), FileKind::Database) { - let proj = if path.extension().and_then(|s| s.to_str()) == Some("project") { - path.clone() - } else { - path.with_extension("project") - }; - let dat = proj.with_extension("dat"); - let (Ok(pb), Ok(db_)) = (std::fs::read(&proj), std::fs::read(&dat)) else { - eprintln!("could not read database pair"); - return ExitCode::from(4); - }; - let db = match Database::read(&pb, &db_) { - Ok(d) => d, - Err(e) => { - eprintln!("parse failed: {e}"); - return ExitCode::from(4); - } - }; - let dec = |w: &wolf_formats::WStr| { - let mut b = w.as_bytes(); - if b.last() == Some(&0) { - b = &b[..b.len() - 1]; - } - String::from_utf8_lossy(b).into_owned() - }; - for (ti, t) in db.types.iter().enumerate() { - println!( - "type {ti} \"{}\" ({} fields, {} data rows)", - dec(&t.name), - t.fields.len(), - t.data.len() - ); - let fields: Vec = t.fields.iter().map(|f| dec(&f.name)).collect(); - println!(" fields: {}", fields.join(" | ")); - let sample: Vec = t.data.iter().take(8).map(|d| dec(&d.name)).collect(); - println!(" data: {}", sample.join(" | ")); - } - return ExitCode::SUCCESS; - } - - if matches!(classify(&path), FileKind::CommonEvent) { - let ce = match CommonEventsFile::read(&bytes) { - Ok(c) => c, - Err(e) => { - eprintln!("parse failed: {e}"); - return ExitCode::from(4); - } - }; - let want: Vec = args[1..].iter().filter_map(|s| s.parse().ok()).collect(); - for ev in &ce.events { - if !want.is_empty() && !want.contains(&ev.int_id) { - continue; - } - let dec = |w: &wolf_formats::WStr| { - let mut b = w.as_bytes(); - if b.last() == Some(&0) { - b = &b[..b.len() - 1]; - } - String::from_utf8_lossy(b).into_owned() - }; - println!( - "commonEvent {} \"{}\" ({} commands)", - ev.int_id, - dec(&ev.name), - ev.commands.len() - ); - println!(" description: {:?}", dec(&ev.description)); - let nonempty = |label: &str, v: &[wolf_formats::WStr]| { - let items: Vec = v - .iter() - .enumerate() - .filter(|(_, w)| !dec(w).is_empty()) - .map(|(i, w)| format!("{i}:{:?}", dec(w))) - .collect(); - if !items.is_empty() { - println!(" {label} ({}): {}", v.len(), items.join(" ")); - } - }; - nonempty("unknown3", &ev.unknown3); - nonempty("unknown8", &ev.unknown8); - for (i, group) in ev.unknown5.iter().enumerate() { - nonempty(&format!("unknown5[{i}]"), group); - } - println!(); - } - return ExitCode::SUCCESS; - } - - let map = match Map::read(&bytes) { - Ok(m) => m, - Err(e) => { - eprintln!("parse failed: {e}"); - return ExitCode::from(4); - } - }; - for ev in &map.events { - println!("event {} {:?}", ev.id, ev.name.to_lossy()); - for (pi, page) in ev.pages.iter().enumerate() { - println!(" page {pi}"); - for c in &page.commands { - println!( - " i{} cid={} {} ints={:?} strs={}", - c.indent, - c.cid, - wolf_decompiler::spec::command_name(c.cid), - c.int_args, - c.str_args.len() - ); - } - } - } - ExitCode::SUCCESS -} - -pub(crate) fn decompile_path(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - match classify(path) { - FileKind::Map => { - let map = Map::read(&bytes).map_err(|e| e.to_string())?; - let symbols = load_symbols(path); - Ok(decompile_map(&map, &symbols)) - } - FileKind::CommonEvent => { - let ce = CommonEventsFile::read(&bytes).map_err(|e| e.to_string())?; - let mut symbols = SymbolTable::new(); - symbols.add_common_events(&ce); - add_databases(&mut symbols, path.parent()); - symbols.set_glossary(wolf_decompiler::symbols::load_embedded_glossary()); - symbols.set_engine_text(wolf_decompiler::symbols::load_embedded_engine_glossary()); - Ok(decompile_common_events(&ce, &symbols)) - } - FileKind::Database | FileKind::BasicDatabase | FileKind::GameDat - | FileKind::TxtEvent | FileKind::Unsupported => Err(format!( - "{} is not a code-bearing file (decompile supports .mps and CommonEvent.dat)", - path.display() - )), - } -} - -/// Decompile to the **edit** document (recompilable, identity-delimited), the form `wolf -/// compile` consumes. Symbol-aware: each operand carries its index **and** a bilingual label -/// (`V[3 "Gold · ゴールド"]`), so the one file is both readable and recompilable. The recompiler -/// strips the labels and round-trips byte-exact. `--numeric` (handled by the caller) falls back -/// to the bare-index form. -pub(crate) fn decompile_edit_path(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - match classify(path) { - FileKind::Map => { - let map = Map::read(&bytes).map_err(|e| e.to_string())?; - Ok(decompile_map_edit_annotated(&map, &load_symbols(path))) - } - FileKind::CommonEvent => { - let ce = CommonEventsFile::read(&bytes).map_err(|e| e.to_string())?; - let mut symbols = SymbolTable::new(); - symbols.add_common_events(&ce); - add_databases(&mut symbols, path.parent()); - symbols.set_glossary(wolf_decompiler::symbols::load_embedded_glossary()); - symbols.set_engine_text(wolf_decompiler::symbols::load_embedded_engine_glossary()); - Ok(decompile_common_events_edit_annotated(&ce, &symbols)) - } - FileKind::Database | FileKind::BasicDatabase | FileKind::GameDat - | FileKind::TxtEvent | FileKind::Unsupported => Err(format!( - "{} is not a code-bearing file (edit mode supports .mps and CommonEvent.dat)", - path.display() - )), - } -} - -/// `wolf compile --base -o `. Recompiles an -/// edit document back into a runnable file. The base supplies all opaque metadata. Only the -/// command bodies are swapped in. With code untouched the output is byte-identical to base. -pub(crate) fn cmd_compile(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut base: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "-b" | "--base" => base = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let (Some(input), Some(base)) = (input, base) else { - eprintln!("compile: usage: wolf compile --base -o "); - return ExitCode::from(64); - }; - - let text = match std::fs::read_to_string(&input) { - Ok(t) => t, - Err(e) => { - eprintln!("compile: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - let base_bytes = match std::fs::read(&base) { - Ok(b) => b, - Err(e) => { - eprintln!("compile: cannot read base {}: {e}", base.display()); - return ExitCode::from(4); - } - }; - - let out_bytes = match classify(&base) { - FileKind::CommonEvent => { - let mut ce = match CommonEventsFile::read(&base_bytes) { - Ok(c) => c, - Err(e) => { - eprintln!("compile: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - if let Err(e) = compile_common_events_edit(&text, &mut ce) { - eprintln!("compile failed: {e}"); - return ExitCode::from(2); - } - ce.write() - } - FileKind::Map => { - let mut map = match Map::read(&base_bytes) { - Ok(m) => m, - Err(e) => { - eprintln!("compile: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - if let Err(e) = compile_map_edit(&text, &mut map) { - eprintln!("compile failed: {e}"); - return ExitCode::from(2); - } - map.write() - } - FileKind::Database | FileKind::BasicDatabase | FileKind::GameDat - | FileKind::TxtEvent | FileKind::Unsupported => { - eprintln!("compile: base must be a .mps or CommonEvent.dat"); - return ExitCode::from(64); - } - }; - - let Some(out) = output else { - eprintln!("compile: missing -o "); - return ExitCode::from(64); - }; - if let Err(e) = std::fs::write(&out, &out_bytes) { - eprintln!("compile: cannot write {}: {e}", out.display()); - return ExitCode::from(4); - } - let same = out_bytes == base_bytes; - eprintln!( - "wrote {} ({} bytes{})", - out.display(), - out_bytes.len(), - if same { ", byte-identical to base" } else { "" } - ); - ExitCode::SUCCESS -} - -/// `wolf db-json [-o out]`. Exports a database (or every -/// database in a BasicData dir) to readable JSON for inspection. A single file writes to -/// `-o file` (or stdout). A directory writes one `.json` per DB into `-o ` (default `.`). -pub(crate) fn cmd_db_json(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!("db-json: usage: wolf db-json [-o out]"); - return ExitCode::from(64); - }; - - if input.is_dir() { - let dir = output.unwrap_or_else(|| PathBuf::from(".")); - if let Err(e) = std::fs::create_dir_all(&dir) { - eprintln!("db-json: cannot create {}: {e}", dir.display()); - return ExitCode::from(4); - } - let mut n = 0; - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - let proj = input.join(format!("{stem}.project")); - if !proj.exists() { - continue; - } - match db_json_one(&proj) { - Ok(json) => { - let out = dir.join(format!("{stem}.json")); - if let Err(e) = std::fs::write(&out, &json) { - eprintln!("db-json: cannot write {}: {e}", out.display()); - return ExitCode::from(4); - } - eprintln!("wrote {} ({} bytes)", out.display(), json.len()); - n += 1; - } - Err(e) => eprintln!("db-json: {stem}: {e}"), - } - } - if n == 0 { - eprintln!("db-json: no databases found under {}", input.display()); - return ExitCode::from(4); - } - return ExitCode::SUCCESS; - } - - let proj = if input.extension().and_then(|s| s.to_str()) == Some("dat") { - input.with_extension("project") - } else { - input.clone() - }; - match db_json_one(&proj) { - Ok(json) => match output { - Some(out) => { - if let Err(e) = std::fs::write(&out, &json) { - eprintln!("db-json: cannot write {}: {e}", out.display()); - return ExitCode::from(4); - } - eprintln!("wrote {} ({} bytes)", out.display(), json.len()); - ExitCode::SUCCESS - } - None => { - use std::io::Write; - let _ = std::io::stdout().write_all(json.as_bytes()); - ExitCode::SUCCESS - } - }, - Err(e) => { - eprintln!("db-json failed: {e}"); - ExitCode::from(4) - } - } -} - -/// Read a `.project` (+ sibling `.dat`) and render it to JSON, labelling the kind by stem. -/// Report any lines a safety guard skipped and pick the exit code (non-zero if so, so CI notices, -/// even though the good lines were still written). -fn guard_report(st: &InjectStats) -> ExitCode { - if st.code_mismatch > 0 || st.unrepresentable > 0 { - eprintln!( - "WARNING: {} line(s) left UNTRANSLATED by a safety guard - {} control-code mismatch, {} not encodable; see the per-line messages above (pass --allow-code-drift to override the code check)", - st.code_mismatch + st.unrepresentable, - st.code_mismatch, - st.unrepresentable, - ); - ExitCode::from(2) - } else { - ExitCode::SUCCESS - } -} - -/// Apply DB translations onto a `.project`+`.dat` pair, writing both halves byte-exact. -fn strings_inject_db(input: &Path, base: &Path, output: &Path, opts: &InjectOptions) -> ExitCode { - let proj = if base.extension().and_then(|s| s.to_str()) == Some("project") { - base.to_path_buf() - } else { - base.with_extension("project") - }; - let base_dat = proj.with_extension("dat"); - let out_dat = output.with_extension("dat"); - let (Ok(text), Ok(p), Ok(d)) = ( - std::fs::read_to_string(input), - std::fs::read(&proj), - std::fs::read(&base_dat), - ) else { - eprintln!("strings-inject: cannot read input or DB pair"); - return ExitCode::from(4); - }; - let mut db = match Database::read(&p, &d) { - Ok(db) => db, - Err(e) => { - eprintln!("strings-inject: DB parse failed: {e}"); - return ExitCode::from(4); - } - }; - let st = match inject_db_strings(&text, &mut db, opts) { - Ok(st) => st, - Err(e) => { - eprintln!("strings-inject failed: {e}"); - return ExitCode::from(2); - } - }; - let (op, od) = db.write(); - // Post-inject re-parse: never ship a structurally-corrupt file. - if let Err(e) = Database::read(&op, &od) { - eprintln!("strings-inject: internal error - injected DB no longer parses ({e}); aborting write to avoid a corrupt file. Please report."); - return ExitCode::from(2); - } - let identical = op == p && od == d; - if let (Err(e), _) | (_, Err(e)) = (std::fs::write(output, &op), std::fs::write(&out_dat, &od)) - { - eprintln!("strings-inject: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!( - "applied {} translation(s) ({} untranslated, {} drifted); wrote {} + {}{}", - st.applied, - st.untranslated, - st.drifted, - output.display(), - out_dat.display(), - if identical { - " (byte-identical to base)" - } else { - "" - } - ); - guard_report(&st) -} - -/// Collect the `.txt` files directly under `dir`, sorted by name for a stable, reproducible -/// document order. Event-text folders are flat, so this is a shallow scan, not recursive. -fn list_txt_files(dir: &Path) -> Vec { - let mut out: Vec = match std::fs::read_dir(dir) { - Ok(rd) => rd - .flatten() - .map(|e| e.path()) - .filter(|p| { - p.is_file() - && p.extension().and_then(|s| s.to_str()).map(str::to_ascii_lowercase) - == Some("txt".to_string()) - }) - .collect(), - Err(_) => Vec::new(), - }; - out.sort(); - out -} - -/// `strings-extract `. Extract every `.txt` event-text file under a directory into one -/// combined JSON keyed by file. Each entry is the per-file `extract_txt_events` document with a -/// `file` field naming it, so `strings-inject` can write each back to the matching base file. -fn strings_extract_txt_dir(dir: &Path, output: Option<&Path>) -> ExitCode { - let files = list_txt_files(dir); - if files.is_empty() { - eprintln!("strings-extract: no .txt files in {}", dir.display()); - return ExitCode::from(4); - } - let mut named: Vec<(String, Vec)> = Vec::with_capacity(files.len()); - for path in &files { - let bytes = match std::fs::read(path) { - Ok(b) => b, - Err(e) => { - eprintln!("strings-extract: cannot read {}: {e}", path.display()); - return ExitCode::from(4); - } - }; - let fname = path.file_name().and_then(|s| s.to_str()).unwrap_or("").to_owned(); - named.push((fname, bytes)); - } - let json = wolf_decompiler::extract_txt_dir(&named); - match output { - Some(out) => { - if let Err(e) = std::fs::write(out, &json) { - eprintln!("strings-extract: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!("wrote {} ({} txt file(s))", out.display(), files.len()); - } - None => { - use std::io::Write; - let _ = std::io::stdout().write_all(json.as_bytes()); - } - } - ExitCode::SUCCESS -} - -/// `strings-inject --base -o ` for a single event-text file. -/// Re-decodes the base, applies the edits, and writes the result, which is byte-identical to the -/// base for a no-op edit. -fn strings_inject_txt_file(input: &Path, base: &Path, output: &Path) -> ExitCode { - let (Ok(json), Ok(base_bytes)) = (std::fs::read_to_string(input), std::fs::read(base)) else { - eprintln!("strings-inject: cannot read input/base"); - return ExitCode::from(4); - }; - let out_bytes = match inject_txt_events(&json, &base_bytes) { - Ok(b) => b, - Err(e) => { - eprintln!("strings-inject failed: {e}"); - return ExitCode::from(2); - } - }; - let identical = out_bytes == base_bytes; - if let Err(e) = std::fs::write(output, &out_bytes) { - eprintln!("strings-inject: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!( - "wrote {}{}", - output.display(), - if identical { " (byte-identical to base)" } else { "" } - ); - ExitCode::SUCCESS -} - -/// `strings-inject --base -o ` for a folder of event-text -/// files. The combined JSON is a `txt-dir` document. Each `files[]` entry is injected against its -/// matching base file and written to `` under the same name. Files present in the base but -/// absent from the JSON are copied through unchanged, so the output directory mirrors the base. -fn strings_inject_txt_dir(input: &Path, base: &Path, out_dir: &Path) -> ExitCode { - let Ok(json) = std::fs::read_to_string(input) else { - eprintln!("strings-inject: cannot read {}", input.display()); - return ExitCode::from(4); - }; - let entries = match wolf_decompiler::split_txt_dir(&json) { - Ok(e) => e, - Err(e) => { - eprintln!("strings-inject: {e}"); - return ExitCode::from(2); - } - }; - if let Err(e) = std::fs::create_dir_all(out_dir) { - eprintln!("strings-inject: cannot create {}: {e}", out_dir.display()); - return ExitCode::from(4); - } - let mut named: std::collections::HashSet = std::collections::HashSet::new(); - let mut written = 0usize; - for (fname, per_file) in &entries { - named.insert(fname.clone()); - let base_path = base.join(fname); - let base_bytes = match std::fs::read(&base_path) { - Ok(b) => b, - Err(e) => { - eprintln!("strings-inject: cannot read base {}: {e}", base_path.display()); - continue; - } - }; - let out_bytes = match inject_txt_events(per_file, &base_bytes) { - Ok(b) => b, - Err(e) => { - eprintln!("strings-inject: {fname}: {e}"); - continue; - } - }; - if let Err(e) = std::fs::write(out_dir.join(fname), &out_bytes) { - eprintln!("strings-inject: write {fname} failed: {e}"); - continue; - } - written += 1; - } - // Copy through any base files the JSON did not mention so the output dir is complete. - let mut copied = 0usize; - for path in list_txt_files(base) { - let fname = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); - if named.contains(fname) { - continue; - } - if let Ok(b) = std::fs::read(&path) { - if std::fs::write(out_dir.join(fname), &b).is_ok() { - copied += 1; - } - } - } - eprintln!( - "wrote {written} injected + {copied} passed-through txt file(s) to {}", - out_dir.display() - ); - ExitCode::SUCCESS -} - -/// `wolf strings-extract -o `. Extracts only player-facing -/// text (dialogue/choices/on-screen), speaker-attributed and grouped by event/scene, for -/// translation. The `text` field starts equal to `source`. Translators edit `text`. -pub(crate) fn cmd_strings_extract(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!( - "strings-extract: usage: wolf strings-extract -o " - ); - return ExitCode::from(64); - }; - // A directory is treated as a folder of external event-text `.txt` files (unpacked Evtext). - // One combined JSON keyed by file is produced. - if input.is_dir() { - return strings_extract_txt_dir(&input, output.as_deref()); - } - let bytes = match std::fs::read(&input) { - Ok(b) => b, - Err(e) => { - eprintln!("strings-extract: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - let fname = input.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let json = match classify(&input) { - FileKind::TxtEvent => extract_txt_events(&bytes), - FileKind::CommonEvent => match CommonEventsFile::read(&bytes) { - Ok(ce) => scenes_to_json(fname, "common", &extract_common_events(&ce)), - Err(e) => { - eprintln!("strings-extract: parse failed: {e}"); - return ExitCode::from(4); - } - }, - FileKind::Map => match Map::read(&bytes) { - Ok(m) => scenes_to_json(fname, "map", &extract_map(&m)), - Err(e) => { - eprintln!("strings-extract: parse failed: {e}"); - return ExitCode::from(4); - } - }, - FileKind::GameDat => match GameDat::read(&bytes) { - Ok(gd) => extract_game_dat(&gd), - Err(e) => { - eprintln!("strings-extract: parse failed: {e}"); - return ExitCode::from(4); - } - }, - FileKind::Database => { - let proj = if input.extension().and_then(|s| s.to_str()) == Some("project") { - input.clone() - } else { - input.with_extension("project") - }; - let dat = proj.with_extension("dat"); - let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) else { - eprintln!( - "strings-extract: cannot read DB pair for {}", - input.display() - ); - return ExitCode::from(4); - }; - match Database::read(&p, &d) { - Ok(db) => { - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - db_strings_to_json(fname, &extract_db_strings(&db, &glossary)) - } - Err(e) => { - eprintln!("strings-extract: DB parse failed: {e}"); - return ExitCode::from(4); - } - } - } - _ => { - eprintln!("strings-extract: only CommonEvent.dat, .mps maps, .project databases, Game.dat, and event-text .txt files (or a folder of them) carry player text"); - return ExitCode::from(64); - } - }; - match output { - Some(out) => { - if let Err(e) = std::fs::write(&out, &json) { - eprintln!("strings-extract: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!("wrote {}", out.display()); - } - None => { - use std::io::Write; - let _ = std::io::stdout().write_all(json.as_bytes()); - } - } - ExitCode::SUCCESS -} - -/// `wolf strings-audit -o `. QA coverage dump. -/// Lists every non-empty string arg / DB cell in the file, tagged with whether the player-text -/// extractor keeps it. Ground truth for precision (kept-but-not-text) plus recall (text-but-dropped). -pub(crate) fn cmd_strings_audit(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!("strings-audit: usage: wolf strings-audit -o "); - return ExitCode::from(64); - }; - let bytes = match std::fs::read(&input) { - Ok(b) => b, - Err(e) => { - eprintln!("strings-audit: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - let json = match classify(&input) { - FileKind::CommonEvent => match CommonEventsFile::read(&bytes) { - Ok(ce) => audit_common_events_to_json(&ce), - Err(e) => { - eprintln!("strings-audit: parse failed: {e}"); - return ExitCode::from(4); - } - }, - FileKind::Map => match Map::read(&bytes) { - Ok(m) => audit_map_to_json(&m), - Err(e) => { - eprintln!("strings-audit: parse failed: {e}"); - return ExitCode::from(4); - } - }, - FileKind::Database => { - let proj = if input.extension().and_then(|s| s.to_str()) == Some("project") { - input.clone() - } else { - input.with_extension("project") - }; - let dat = proj.with_extension("dat"); - let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) else { - eprintln!("strings-audit: cannot read DB pair for {}", input.display()); - return ExitCode::from(4); - }; - match Database::read(&p, &d) { - Ok(db) => { - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - audit_db_to_json(&db, &glossary) - } - Err(e) => { - eprintln!("strings-audit: DB parse failed: {e}"); - return ExitCode::from(4); - } - } - } - _ => { - eprintln!("strings-audit: only CommonEvent.dat, .mps maps, and .project databases"); - return ExitCode::from(64); - } - }; - match output { - Some(out) => { - if let Err(e) = std::fs::write(&out, &json) { - eprintln!("strings-audit: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!("wrote {}", out.display()); - } - None => { - use std::io::Write; - let _ = std::io::stdout().write_all(json.as_bytes()); - } - } - ExitCode::SUCCESS -} - -/// `wolf strings-inject --base -o `. Applies edited translations back, -/// byte-exact. Only lines whose `text` differs from `source` (and still match the base) are -/// changed. Everything else re-serializes identically. -pub(crate) fn cmd_strings_inject(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut base: Option = None; - let mut output: Option = None; - let mut opts = InjectOptions::default(); - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "-b" | "--base" => base = it.next().map(PathBuf::from), - "--allow-code-drift" => opts.allow_code_drift = true, - "--en-punct" => opts.normalize_punct = true, - _ => input = Some(PathBuf::from(a)), - } - } - let (Some(input), Some(base), Some(output)) = (input, base, output) else { - eprintln!( - "strings-inject: usage: wolf strings-inject --base -o [--allow-code-drift] [--en-punct]" - ); - return ExitCode::from(64); - }; - // Databases are a `.project`+`.dat` pair (write both). - if matches!(classify(&base), FileKind::Database) { - return strings_inject_db(&input, &base, &output, &opts); - } - // A directory base is a folder of external event-text `.txt` files (unpacked Evtext). The - // input JSON is the combined keyed-by-file document, and each file is written under `-o`. - if base.is_dir() { - return strings_inject_txt_dir(&input, &base, &output); - } - // A single external event-text `.txt` file. - if matches!(classify(&base), FileKind::TxtEvent) { - return strings_inject_txt_file(&input, &base, &output); - } - let (Ok(text), Ok(base_bytes)) = (std::fs::read_to_string(&input), std::fs::read(&base)) else { - eprintln!("strings-inject: cannot read input/base"); - return ExitCode::from(4); - }; - let (out_bytes, st) = match classify(&base) { - FileKind::CommonEvent => { - let mut ce = match CommonEventsFile::read(&base_bytes) { - Ok(c) => c, - Err(e) => { - eprintln!("strings-inject: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - let st = match inject_common_events(&text, &mut ce, &opts) { - Ok(st) => st, - Err(e) => { - eprintln!("strings-inject failed: {e}"); - return ExitCode::from(2); - } - }; - let out = ce.write(); - // Post-inject re-parse: never ship a structurally-corrupt file. - if let Err(e) = CommonEventsFile::read(&out) { - eprintln!("strings-inject: internal error - injected file no longer parses ({e}); aborting write to avoid a corrupt file. Please report."); - return ExitCode::from(2); - } - (out, st) - } - FileKind::Map => { - let mut m = match Map::read(&base_bytes) { - Ok(m) => m, - Err(e) => { - eprintln!("strings-inject: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - let st = match inject_map(&text, &mut m, &opts) { - Ok(st) => st, - Err(e) => { - eprintln!("strings-inject failed: {e}"); - return ExitCode::from(2); - } - }; - let out = m.write(); - if let Err(e) = Map::read(&out) { - eprintln!("strings-inject: internal error - injected file no longer parses ({e}); aborting write to avoid a corrupt file. Please report."); - return ExitCode::from(2); - } - (out, st) - } - FileKind::GameDat => { - let mut gd = match GameDat::read(&base_bytes) { - Ok(g) => g, - Err(e) => { - eprintln!("strings-inject: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - let st = match inject_game_dat(&text, &mut gd, &opts) { - Ok(st) => st, - Err(e) => { - eprintln!("strings-inject failed: {e}"); - return ExitCode::from(2); - } - }; - let out = gd.write(); - if let Err(e) = GameDat::read(&out) { - eprintln!("strings-inject: internal error - injected file no longer parses ({e}); aborting write to avoid a corrupt file. Please report."); - return ExitCode::from(2); - } - (out, st) - } - _ => { - eprintln!("strings-inject: base must be CommonEvent.dat, a .mps map, Game.dat, an event-text .txt file, or a folder of them"); - return ExitCode::from(64); - } - }; - let identical = out_bytes == base_bytes; - if let Err(e) = std::fs::write(&output, &out_bytes) { - eprintln!("strings-inject: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!( - "applied {} translation(s) ({} untranslated, {} drifted); wrote {}{}", - st.applied, - st.untranslated, - st.drifted, - output.display(), - if identical { - " (byte-identical to base)" - } else { - "" - } - ); - guard_report(&st) -} - -/// `wolf db-apply --base -o `. Applies value/row-name -/// edits from a `db-json` export back onto a database, writing `` and the sibling -/// ``. Only cell values and row names change. Schema edits are rejected. Untouched -/// data re-serializes byte-identical to the base. -pub(crate) fn cmd_db_apply(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut base: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "-b" | "--base" => base = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let (Some(input), Some(base), Some(output)) = (input, base, output) else { - eprintln!( - "db-apply: usage: wolf db-apply --base -o " - ); - return ExitCode::from(64); - }; - let base_dat = base.with_extension("dat"); - let out_dat = output.with_extension("dat"); - - let text = match std::fs::read_to_string(&input) { - Ok(t) => t, - Err(e) => { - eprintln!("db-apply: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - let (pb, db_) = match (std::fs::read(&base), std::fs::read(&base_dat)) { - (Ok(p), Ok(d)) => (p, d), - _ => { - eprintln!( - "db-apply: cannot read base pair {} + {}", - base.display(), - base_dat.display() - ); - return ExitCode::from(4); - } - }; - let mut db = match Database::read(&pb, &db_) { - Ok(d) => d, - Err(e) => { - eprintln!("db-apply: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - - let changed = match apply_database_edit(&text, &mut db) { - Ok(n) => n, - Err(e) => { - eprintln!("db-apply failed: {e}"); - return ExitCode::from(2); - } - }; - - let (out_pb, out_db) = db.write(); - let byte_identical = out_pb == pb && out_db == db_; - if let (Err(e), _) | (_, Err(e)) = ( - std::fs::write(&output, &out_pb), - std::fs::write(&out_dat, &out_db), - ) { - eprintln!("db-apply: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!( - "wrote {} + {} ({changed} cell(s) changed{})", - output.display(), - out_dat.display(), - if byte_identical { - "; byte-identical to base" - } else { - "" - } - ); - ExitCode::SUCCESS -} - -// ---------------------------------------------------------------------------- -// gamedat-json / gamedat-apply: the FULL Game.dat field editor (all editable -// string fields, not just the player-text subset that strings-extract exposes) -// ---------------------------------------------------------------------------- - -/// `wolf gamedat-json [-o out.json]`. Dumps *every* editable Game.dat string field to -/// JSON for inspection/editing (fonts, graphics, image paths and the player text). Structural -/// fields are not exposed. Writes to `-o` (or stdout). -pub(crate) fn cmd_gamedat_json(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!("gamedat-json: usage: wolf gamedat-json [-o out.json]"); - return ExitCode::from(64); - }; - - let bytes = match std::fs::read(&input) { - Ok(b) => b, - Err(e) => { - eprintln!("gamedat-json: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - let gd = match GameDat::read(&bytes) { - Ok(gd) => gd, - Err(e) => { - eprintln!("gamedat-json: parse failed: {e}"); - return ExitCode::from(4); - } - }; - let json = dump_game_dat(&gd); - match output { - Some(out) => { - if let Err(e) = std::fs::write(&out, &json) { - eprintln!("gamedat-json: cannot write {}: {e}", out.display()); - return ExitCode::from(4); - } - eprintln!("wrote {} ({} bytes)", out.display(), json.len()); - ExitCode::SUCCESS - } - None => { - use std::io::Write; - let _ = std::io::stdout().write_all(json.as_bytes()); - ExitCode::SUCCESS - } - } -} - -/// `wolf gamedat-apply --base -o `. Applies full-field edits from a -/// `gamedat-json` dump back onto a Game.dat, writing ``. The output is re-parsed with -/// `GameDat::read` to verify it round-trips before the write is reported. Untouched fields -/// re-serialize byte-exact. -pub(crate) fn cmd_gamedat_apply(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut base: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "-b" | "--base" => base = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let (Some(input), Some(base), Some(output)) = (input, base, output) else { - eprintln!("gamedat-apply: usage: wolf gamedat-apply --base -o "); - return ExitCode::from(64); - }; - - let text = match std::fs::read_to_string(&input) { - Ok(t) => t, - Err(e) => { - eprintln!("gamedat-apply: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - let base_bytes = match std::fs::read(&base) { - Ok(b) => b, - Err(e) => { - eprintln!("gamedat-apply: cannot read base {}: {e}", base.display()); - return ExitCode::from(4); - } - }; - let mut gd = match GameDat::read(&base_bytes) { - Ok(gd) => gd, - Err(e) => { - eprintln!("gamedat-apply: base parse failed: {e}"); - return ExitCode::from(4); - } - }; - - let changed = match apply_game_dat(&mut gd, &text) { - Ok(n) => n, - Err(e) => { - eprintln!("gamedat-apply failed: {e}"); - return ExitCode::from(2); - } - }; - - let out = gd.write(); - // Re-parse the output to verify it still round-trips before we commit it to disk. - if let Err(e) = GameDat::read(&out) { - eprintln!("gamedat-apply: internal error - edited file no longer parses ({e}); aborting write to avoid a corrupt file. Please report."); - return ExitCode::from(2); - } - let identical = out == base_bytes; - if let Err(e) = std::fs::write(&output, &out) { - eprintln!("gamedat-apply: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!( - "gamedat-apply: {changed} field(s) changed -> {}{}", - output.display(), - if identical { - " (byte-identical to base)" - } else { - "" - } - ); - ExitCode::SUCCESS -} - -// ---------------------------------------------------------------------------- -// names-extract / names-inject: the project-level DB name glossary -// ---------------------------------------------------------------------------- - -/// A database loaded for inject: `(project path, dat path, original project bytes, original dat -/// bytes, parsed db)`. The original bytes let the caller report byte-identity. The parsed db is -/// mutated in place by the inject. -type LoadedDb = (PathBuf, PathBuf, Vec, Vec, Database); - -/// The translatable files of a data dir, located the same way the other commands do: the three -/// standard DB pairs and `CommonEvent.dat` under `` or `/BasicData`, and every `*.mps` -/// map under `` or `/MapData`. Each entry keeps the on-disk paths so an inject can write -/// each file back to where it came from. -struct DataDirFiles { - /// (`.project` path, `.dat` path) for each found database pair. - dbs: Vec<(PathBuf, PathBuf)>, - /// Path to `CommonEvent.dat`, if present. - common_event: Option, - /// Every `*.mps` map path. - maps: Vec, -} - -/// Resolve which of `dir` / `dir/BasicData` holds the BasicData files (`CommonEvent.dat` + DB -/// pairs), mirroring `cmd_export_names`. -fn basic_data_dir(dir: &Path) -> PathBuf { - if dir.join("CommonEvent.dat").exists() || dir.join("DataBase.project").exists() { - dir.to_path_buf() - } else if dir.join("BasicData").join("CommonEvent.dat").exists() - || dir.join("BasicData").join("DataBase.project").exists() - { - dir.join("BasicData") - } else { - dir.to_path_buf() - } -} - -/// Discover the translatable files under a data dir for the name glossary. -fn discover_data_dir(dir: &Path) -> DataDirFiles { - let basic = basic_data_dir(dir); - let mut dbs = Vec::new(); - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - let proj = basic.join(format!("{stem}.project")); - let dat = basic.join(format!("{stem}.dat")); - if proj.exists() && dat.exists() { - dbs.push((proj, dat)); - } - } - let common_event = { - let ce = basic.join("CommonEvent.dat"); - ce.exists().then_some(ce) - }; - // Maps live in MapData (or directly under dir for a flattened layout). - let mut maps = Vec::new(); - for sub in [dir.to_path_buf(), dir.join("MapData")] { - let Ok(rd) = std::fs::read_dir(&sub) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.extension().and_then(|s| s.to_str()) == Some("mps") { - maps.push(p); - } - } - } - maps.sort(); - maps.dedup(); - DataDirFiles { - dbs, - common_event, - maps, - } -} - -/// `wolf names-extract -o names.json`. Builds the project-level DB **name glossary**: -/// every unique row name / name-display cell value across the dir's databases, with an occurrence -/// count (DB cells + row names + by-name command refs) and an originating-type hint. `text` starts -/// equal to `source`. A translator edits `text`, and `names-inject` applies it consistently. -pub(crate) fn cmd_names_extract(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let Some(dir) = input else { - eprintln!("names-extract: usage: wolf names-extract -o "); - return ExitCode::from(64); - }; - - let found = discover_data_dir(&dir); - // Parse the databases. - let mut dbs = Vec::new(); - for (proj, dat) in &found.dbs { - match (std::fs::read(proj), std::fs::read(dat)) { - (Ok(p), Ok(d)) => match Database::read(&p, &d) { - Ok(db) => dbs.push(db), - Err(e) => eprintln!("names-extract: skip {}: {e}", proj.display()), - }, - _ => eprintln!("names-extract: cannot read {}", proj.display()), - } - } - // Parse the common events (optional). - let ce = found.common_event.as_ref().and_then(|p| { - std::fs::read(p) - .ok() - .and_then(|b| CommonEventsFile::read(&b).ok()) - }); - // Parse the maps. - let mut maps = Vec::new(); - for p in &found.maps { - if let Ok(b) = std::fs::read(p) { - if let Ok(m) = Map::read(&b) { - maps.push(m); - } - } - } - - if dbs.is_empty() { - eprintln!( - "names-extract: no databases found under {} (looked in {} and BasicData)", - dir.display(), - dir.display() - ); - return ExitCode::from(4); - } - - let ce_refs: Vec<&CommonEventsFile> = ce.iter().collect(); - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = extract_names(&dbs, &ce_refs, &maps, &glossary); - - // A small count of distinct names for the operator (`count` is also embedded in the JSON). - let n = json.matches("\"source\"").count(); - match output { - Some(out) => { - if let Err(e) = std::fs::write(&out, &json) { - eprintln!("names-extract: write failed: {e}"); - return ExitCode::from(4); - } - eprintln!( - "wrote {} ({n} distinct name(s) from {} DB(s), {} map(s){})", - out.display(), - dbs.len(), - maps.len(), - if ce.is_some() { ", CommonEvent" } else { "" } - ); - } - None => { - use std::io::Write; - let _ = std::io::stdout().write_all(json.as_bytes()); - } - } - ExitCode::SUCCESS -} - -/// `wolf names-inject --data [-o ]`. Applies the edited name -/// glossary consistently across the whole data dir: every stored row name, name-display cell -/// value, and by-name command reference that equals a translated source is rewritten together -/// (drift-/code-/encoding-guarded). Writes in place by default, or under `-o ` mirroring -/// the input layout. Each written file is re-parsed to guarantee it is not structurally corrupt. -pub(crate) fn cmd_names_inject(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut data: Option = None; - let mut out_dir: Option = None; - let mut opts = InjectOptions::default(); - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "--data" => data = it.next().map(PathBuf::from), - "-o" | "--output" => out_dir = it.next().map(PathBuf::from), - "--allow-code-drift" => opts.allow_code_drift = true, - "--en-punct" => opts.normalize_punct = true, - _ => input = Some(PathBuf::from(a)), - } - } - let (Some(input), Some(dir)) = (input, data) else { - eprintln!( - "names-inject: usage: wolf names-inject --data [-o ] [--allow-code-drift] [--en-punct]" - ); - return ExitCode::from(64); - }; - let names_json = match std::fs::read_to_string(&input) { - Ok(t) => t, - Err(e) => { - eprintln!("names-inject: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - - let found = discover_data_dir(&dir); - - // Parse everything up front and abort on any read/parse failure, so a partial write can't - // desync the name mirrors. The glossary exists to keep names consistent across files. - let mut dbs: Vec = Vec::new(); - for (proj, dat) in &found.dbs { - let (Ok(p), Ok(d)) = (std::fs::read(proj), std::fs::read(dat)) else { - eprintln!("names-inject: cannot read DB pair {}", proj.display()); - return ExitCode::from(4); - }; - match Database::read(&p, &d) { - Ok(db) => dbs.push((proj.clone(), dat.clone(), p, d, db)), - Err(e) => { - eprintln!("names-inject: DB parse failed for {}: {e}", proj.display()); - return ExitCode::from(4); - } - } - } - let mut ce: Option<(PathBuf, Vec, CommonEventsFile)> = None; - if let Some(p) = &found.common_event { - let Ok(b) = std::fs::read(p) else { - eprintln!("names-inject: cannot read {}", p.display()); - return ExitCode::from(4); - }; - match CommonEventsFile::read(&b) { - Ok(c) => ce = Some((p.clone(), b, c)), - Err(e) => { - eprintln!("names-inject: CommonEvent parse failed: {e}"); - return ExitCode::from(4); - } - } - } - let mut maps: Vec<(PathBuf, Vec, Map)> = Vec::new(); - for p in &found.maps { - let Ok(b) = std::fs::read(p) else { - eprintln!("names-inject: cannot read {}", p.display()); - return ExitCode::from(4); - }; - match Map::read(&b) { - Ok(m) => maps.push((p.clone(), b, m)), - Err(e) => { - eprintln!("names-inject: map parse failed for {}: {e}", p.display()); - return ExitCode::from(4); - } - } - } - if dbs.is_empty() { - eprintln!("names-inject: no databases found under {}", dir.display()); - return ExitCode::from(4); - } - - // Apply across all parsed files at once. - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let st = { - let mut db_vec: Vec = dbs.iter().map(|(_, _, _, _, db)| db.clone()).collect(); - let mut ce_vec: Vec<&mut CommonEventsFile> = - ce.as_mut().map(|(_, _, c)| c).into_iter().collect(); - let mut map_vec: Vec = maps.iter().map(|(_, _, m)| m.clone()).collect(); - let st = match inject_names( - &names_json, - &mut db_vec, - &mut ce_vec, - &mut map_vec, - &opts, - &glossary, - ) { - Ok(st) => st, - Err(e) => { - eprintln!("names-inject failed: {e}"); - return ExitCode::from(2); - } - }; - // Move the mutated copies back into the path-keyed records. - for ((_, _, _, _, db), new) in dbs.iter_mut().zip(db_vec) { - *db = new; - } - for ((_, _, m), new) in maps.iter_mut().zip(map_vec) { - *m = new; - } - st - }; - - // Compute output paths (in place, or remapped under -o mirroring the relative layout). - let remap = |orig: &Path| -> PathBuf { - match &out_dir { - Some(od) => od.join(rel(&dir, orig)), - None => orig.to_path_buf(), - } - }; - - let mut written = 0usize; - // Databases (write both halves, then re-parse to never ship a corrupt pair). - for (proj, dat, _, _, db) in &dbs { - let (op, od) = db.write(); - if Database::read(&op, &od).is_err() { - eprintln!( - "names-inject: internal error - injected DB {} no longer parses; aborting write to avoid corruption. Please report.", - proj.display() - ); - return ExitCode::from(2); - } - let (out_proj, out_dat) = (remap(proj), remap(dat)); - if let Some(parent) = out_proj.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let (Err(e), _) | (_, Err(e)) = ( - std::fs::write(&out_proj, &op), - std::fs::write(&out_dat, &od), - ) { - eprintln!("names-inject: write failed: {e}"); - return ExitCode::from(4); - } - written += 1; - } - // CommonEvent. - if let Some((p, _, c)) = &ce { - let out = c.write(); - if CommonEventsFile::read(&out).is_err() { - eprintln!("names-inject: internal error - injected CommonEvent no longer parses; aborting. Please report."); - return ExitCode::from(2); - } - let outp = remap(p); - if let Some(parent) = outp.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Err(e) = std::fs::write(&outp, &out) { - eprintln!("names-inject: write failed: {e}"); - return ExitCode::from(4); - } - written += 1; - } - // Maps. - for (p, _, m) in &maps { - let out = m.write(); - if Map::read(&out).is_err() { - eprintln!( - "names-inject: internal error - injected map {} no longer parses; aborting. Please report.", - p.display() - ); - return ExitCode::from(2); - } - let outp = remap(p); - if let Some(parent) = outp.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Err(e) = std::fs::write(&outp, &out) { - eprintln!("names-inject: write failed: {e}"); - return ExitCode::from(4); - } - written += 1; - } - - eprintln!( - "applied {} name change(s) ({} drifted/unmatched); wrote {written} file(s){}", - st.applied, - st.drifted, - match &out_dir { - Some(od) => format!(" under {}", od.display()), - None => " in place".to_string(), - } - ); - guard_report(&st) -} - -/// `wolf names-check ...`. Checks a set of translation JSONs (names.json, db-strings, -/// scenes) for **name-translation conflicts**: a glossary name (a `names.json` source) that is -/// translated to two or more distinct values across the files. This is the consistency guard for -/// the disjoint-ownership split. A name must translate to ONE value everywhere or by-name DB -/// lookups break. Exits non-zero (2) if any conflict is found. Only exact, full-string name -/// matches count (a name inside a dialogue sentence is a different source, never a conflict). -pub(crate) fn cmd_names_check(args: &[String]) -> ExitCode { - let paths: Vec = args.iter().map(PathBuf::from).collect(); - if paths.is_empty() { - eprintln!("names-check: usage: wolf names-check ..."); - return ExitCode::from(64); - } - - // Load every file up front (so the conflict scan sees them all). A read failure is fatal. - let mut loaded: Vec<(String, String)> = Vec::new(); - for p in &paths { - match std::fs::read_to_string(p) { - Ok(text) => loaded.push((p.display().to_string(), text)), - Err(e) => { - eprintln!("names-check: cannot read {}: {e}", p.display()); - return ExitCode::from(4); - } - } - } - let refs: Vec<(String, &str)> = loaded - .iter() - .map(|(name, text)| (name.clone(), text.as_str())) - .collect(); - - let conflicts = check_name_conflicts(&refs); - if conflicts.is_empty() { - eprintln!( - "names-check: OK - no name-translation conflicts across {} file(s)", - loaded.len() - ); - return ExitCode::SUCCESS; - } - - eprintln!( - "names-check: FOUND {} name(s) translated inconsistently:\n", - conflicts.len() - ); - for c in &conflicts { - eprintln!(" {:?} has {} divergent translations:", c.source, c.variants.len()); - for v in &c.variants { - eprintln!(" -> {:?} (in {})", v.text, v.files.join(", ")); - } - } - eprintln!( - "\nEach conflicting name must translate to ONE value everywhere, or by-name DB lookups break." - ); - ExitCode::from(2) -} - -// ---------------------------------------------------------------------------- -// translations-merge: incremental translation memory across a game update -// ---------------------------------------------------------------------------- - -/// Collect every `*.json` under `path` (recursively) if it is a directory, or `path` itself if it -/// is a single `.json` file. Sorted for deterministic memory-build order. -fn collect_json_files(path: &Path) -> Vec { - let mut out = Vec::new(); - if path.is_dir() { - let mut all = Vec::new(); - collect(path, &mut all); - for p in all { - if p.extension().and_then(|s| s.to_str()) == Some("json") { - out.push(p); - } - } - } else if path.extension().and_then(|s| s.to_str()) == Some("json") { - out.push(path.to_path_buf()); - } - out.sort(); - out -} - -/// `wolf translations-merge --old ... --new -o `. Incremental **translation -/// memory**: carry the translations from an OLD (already-translated) extraction over into a freshly -/// re-extracted NEW set, matched by exact source string, so only genuinely new/changed lines stay -/// untranslated after a game update. -/// -/// `--old` takes one or more files or directories (dirs are walked for `*.json`). ALL of them build -/// one global memory. For each `*.json` under `--new ` (recursive), the memory is applied and -/// the result written to `/`. Prints a per-file line plus a TOTAL, the -/// count of memory sources dropped (removed in the update), and a conflict count. Exits 0 normally. -pub(crate) fn cmd_translations_merge(args: &[String]) -> ExitCode { - let mut old_paths: Vec = Vec::new(); - let mut new_dir: Option = None; - let mut out_dir: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - // --old greedily takes every following token until the next flag. - "--old" => { - while let Some(next) = it.clone().next() { - if next.starts_with('-') { - break; - } - old_paths.push(PathBuf::from(it.next().unwrap())); - } - } - "--new" => new_dir = it.next().map(PathBuf::from), - "-o" | "--output" => out_dir = it.next().map(PathBuf::from), - _ => old_paths.push(PathBuf::from(a)), - } - } - let (Some(new_dir), Some(out_dir)) = (new_dir, out_dir) else { - eprintln!( - "translations-merge: usage: wolf translations-merge --old ... --new -o " - ); - return ExitCode::from(64); - }; - if old_paths.is_empty() { - eprintln!("translations-merge: --old needs at least one file or directory"); - return ExitCode::from(64); - } - - // Build ONE global memory from every old JSON (files + every *.json under old dirs). - let mut old_files: Vec = Vec::new(); - for p in &old_paths { - old_files.extend(collect_json_files(p)); - } - old_files.sort(); - old_files.dedup(); - if old_files.is_empty() { - eprintln!("translations-merge: no *.json found under the --old path(s)"); - return ExitCode::from(4); - } - let mut old_texts: Vec = Vec::with_capacity(old_files.len()); - for p in &old_files { - match std::fs::read_to_string(p) { - Ok(t) => old_texts.push(t), - Err(e) => { - eprintln!("translations-merge: cannot read {}: {e}", p.display()); - return ExitCode::from(4); - } - } - } - let old_refs: Vec<&str> = old_texts.iter().map(String::as_str).collect(); - let (memory, conflicts) = build_memory(&old_refs); - - // Apply the memory to every *.json under --new, writing to /. - let new_files = collect_json_files(&new_dir); - if new_files.is_empty() { - eprintln!( - "translations-merge: no *.json found under --new {}", - new_dir.display() - ); - return ExitCode::from(4); - } - - let mut total = MergeStats::default(); - let mut new_texts: Vec = Vec::with_capacity(new_files.len()); - for path in &new_files { - let text = match std::fs::read_to_string(path) { - Ok(t) => t, - Err(e) => { - eprintln!("translations-merge: cannot read {}: {e}", path.display()); - return ExitCode::from(4); - } - }; - let (merged, stats) = match apply_memory(&text, &memory) { - Ok(r) => r, - Err(e) => { - eprintln!("translations-merge: {}: {e}", path.display()); - return ExitCode::from(4); - } - }; - new_texts.push(text); - - let out_path = out_dir.join(rel(&new_dir, path)); - if let Some(parent) = out_path.parent() { - if let Err(e) = std::fs::create_dir_all(parent) { - eprintln!("translations-merge: cannot create {}: {e}", parent.display()); - return ExitCode::from(4); - } - } - if let Err(e) = std::fs::write(&out_path, &merged) { - eprintln!("translations-merge: write failed for {}: {e}", out_path.display()); - return ExitCode::from(4); - } - println!( - "{}: carried={} still_new={} kept_existing={}", - rel(&new_dir, path), - stats.carried, - stats.still_new, - stats.kept_existing - ); - total.carried += stats.carried; - total.still_new += stats.still_new; - total.kept_existing += stats.kept_existing; - } - - // Reporting: memory sources that no new file still contains (removed in the update). - let new_refs: Vec<&str> = new_texts.iter().map(String::as_str).collect(); - let dropped = dropped_sources(&memory, &new_refs); - - println!( - "TOTAL: carried={} still_new={} kept_existing={} (memory={} entries, {} new file(s))", - total.carried, - total.still_new, - total.kept_existing, - memory.len(), - new_files.len() - ); - println!("dropped (removed in update): {}", dropped.len()); - println!("conflicts: {}", conflicts.len()); - for (source, variants) in conflicts.iter().take(5) { - println!(" {source:?} -> kept {:?} (also {:?})", variants[0], &variants[1..]); - } - if conflicts.len() > 5 { - println!(" ... and {} more", conflicts.len() - 5); - } - ExitCode::SUCCESS -} - -// ---------------------------------------------------------------------------- -// save-update: refresh a baked save's title + player-facing strings for the translation -// ---------------------------------------------------------------------------- - -/// Read the `Title` entry out of a translated `Game.dat` (via the existing GameDat support plus -/// `extract_game_dat`), to use as the save's new baked title. `extract_game_dat` always emits the -/// `Title` line first, decoded in the file's encoding. We pull its `source` text out of that JSON. -fn title_from_game_dat(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?; - let gd = GameDat::read(&bytes).map_err(|e| format!("Game.dat parse failed: {e}"))?; - let json = extract_game_dat(&gd); - // wolf-cli has no JSON dep. The gamedat extraction has a fixed line shape - // (`{"key": "Title", "source": "...", ...}`), so scan for the Title line's `source` value. - extract_json_string_after(&json, "\"key\": \"Title\"", "\"source\":") - .ok_or_else(|| format!("{} has no Title entry", path.display())) -} - -/// Minimal forward scan: find `marker`, then the next `field` after it, and unescape the JSON -/// string value that follows. Sufficient for the fixed, machine-generated `extract_game_dat` shape. -/// Avoids pulling serde_json into the CLI crate. Returns `None` if the shape is unexpected. -fn extract_json_string_after(json: &str, marker: &str, field: &str) -> Option { - let after_marker = &json[json.find(marker)?..]; - // Advance past the field token itself (e.g. `"source":`). The value's opening quote is next. - let field_at = after_marker.find(field)?; - let after_field = &after_marker[field_at + field.len()..]; - let open = after_field.find('"')? + 1; // first byte of the value, after its opening quote - let rest = &after_field[open..]; - let mut out = String::new(); - let mut chars = rest.chars(); - while let Some(c) = chars.next() { - match c { - '"' => return Some(out), - '\\' => match chars.next()? { - 'n' => out.push('\n'), - 'r' => out.push('\r'), - 't' => out.push('\t'), - '"' => out.push('"'), - '\\' => out.push('\\'), - '/' => out.push('/'), - 'u' => { - let hex: String = chars.by_ref().take(4).collect(); - let code = u32::from_str_radix(&hex, 16).ok()?; - out.push(char::from_u32(code)?); - } - other => out.push(other), - }, - _ => out.push(c), - } - } - None -} - -/// Build the `source -> target` baked-string map from the same translation JSONs used for the -/// game, by reusing [`build_memory`]. `--translations` takes files and/or directories (dirs are -/// walked for `*.json`). Reports any divergent sources but keeps the first (memory) value. -fn build_save_string_map(paths: &[PathBuf]) -> Result, ExitCode> { - let mut files: Vec = Vec::new(); - for p in paths { - files.extend(collect_json_files(p)); - } - files.sort(); - files.dedup(); - let mut texts: Vec = Vec::with_capacity(files.len()); - for p in &files { - match std::fs::read_to_string(p) { - Ok(t) => texts.push(t), - Err(e) => { - eprintln!("save-update: cannot read {}: {e}", p.display()); - return Err(ExitCode::from(4)); - } - } - } - let refs: Vec<&str> = texts.iter().map(String::as_str).collect(); - let (memory, conflicts) = build_memory(&refs); - if !conflicts.is_empty() { - eprintln!( - "save-update: note: {} source(s) had divergent translations across files; kept the first", - conflicts.len() - ); - } - Ok(memory) -} - -/// A short timestamp (`YYYYmmdd_HHMMSS`) from the system clock, for the backup dir name. Avoids a -/// chrono dependency by formatting the civil date from the Unix epoch seconds directly. -fn backup_stamp() -> String { - let secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - let days = secs / 86_400; - let tod = secs % 86_400; - let (hh, mm, ss) = (tod / 3600, (tod % 3600) / 60, tod % 60); - // Civil-from-days (Howard Hinnant's algorithm), giving a stable, monotonic, unique-enough stamp. - let z = days as i64 + 719_468; - let era = if z >= 0 { z } else { z - 146_096 } / 146_097; - let doe = z - era * 146_097; - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let y = yoe + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; - let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let year = if m <= 2 { y + 1 } else { y }; - format!("{year:04}{m:02}{d:02}_{hh:02}{mm:02}{ss:02}") -} - -/// `wolf save-update [-o ] [--title | --game ] -/// [--translations ...]`. Rewrites the baked title to match the translated build and -/// refreshes the baked player-facing strings using the same translation JSONs applied to the game, -/// so an old/Japanese save loads cleanly. A directory input updates every `*.sav` inside it. A file -/// updates just that file. Default output is in place (originals backed up to -/// `/save_backup_/` first), or `-o ` writes copies elsewhere. Both the -/// standard codec and the GamePro Pro (marker-3) codec are supported. A buffer that is neither -/// is reported and skipped, never mangled. -pub(crate) fn cmd_save_update(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut title: Option = None; - let mut game: Option = None; - let mut translations: Vec = Vec::new(); - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "--title" => title = it.next().cloned(), - "--game" => game = it.next().map(PathBuf::from), - // --translations greedily takes every following token until the next flag. - "--translations" => { - while let Some(next) = it.clone().next() { - if next.starts_with('-') { - break; - } - translations.push(PathBuf::from(it.next().unwrap())); - } - } - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!( - "save-update: usage: wolf save-update [-o ] \ - [--title | --game ] [--translations ...]" - ); - return ExitCode::from(64); - }; - if title.is_some() && game.is_some() { - eprintln!("save-update: pass at most one of --title / --game"); - return ExitCode::from(64); - } - - // Resolve the new title (literal, or read from a translated Game.dat), if any. - let new_title: Option = match (&title, &game) { - (Some(t), _) => Some(t.clone()), - (None, Some(g)) => match title_from_game_dat(g) { - Ok(t) => { - eprintln!("save-update: title from {} = {t:?}", g.display()); - Some(t) - } - Err(e) => { - eprintln!("save-update: {e}"); - return ExitCode::from(4); - } - }, - (None, None) => None, - }; - - // Build the baked-string translation map (empty if --translations was not given). - let strings = if translations.is_empty() { - std::collections::HashMap::new() - } else { - match build_save_string_map(&translations) { - Ok(m) => m, - Err(code) => return code, - } - }; - if new_title.is_none() && strings.is_empty() { - eprintln!("save-update: nothing to do (give --title/--game and/or --translations)"); - return ExitCode::from(64); - } - - // Discover the save files to process. - let saves: Vec = if input.is_dir() { - let mut v: Vec = std::fs::read_dir(&input) - .into_iter() - .flatten() - .flatten() - .map(|e| e.path()) - .filter(|p| p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("sav")) - .collect(); - v.sort(); - v - } else { - vec![input.clone()] - }; - if saves.is_empty() { - eprintln!("save-update: no *.sav files found under {}", input.display()); - return ExitCode::from(4); - } - - // Resolve where each save's output goes, and (for in-place) back up the originals first. - let dir_input = input.is_dir(); - let in_place = output.is_none(); - if in_place { - // Back up the originals before overwriting (mirrors the reference tool). - let backup_root = if dir_input { - input.join(format!("save_backup_{}", backup_stamp())) - } else { - let parent = input.parent().unwrap_or(Path::new(".")); - parent.join(format!("save_backup_{}", backup_stamp())) - }; - if let Err(e) = std::fs::create_dir_all(&backup_root) { - eprintln!("save-update: cannot create backup dir {}: {e}", backup_root.display()); - return ExitCode::from(4); - } - for src in &saves { - let name = src.file_name().unwrap_or_default(); - if let Err(e) = std::fs::copy(src, backup_root.join(name)) { - eprintln!("save-update: backup failed for {}: {e}", src.display()); - return ExitCode::from(4); - } - } - eprintln!("save-update: backed up {} original(s) to {}", saves.len(), backup_root.display()); - } else if dir_input { - // -o for a directory input: ensure it exists. - if let Some(od) = &output { - if let Err(e) = std::fs::create_dir_all(od) { - eprintln!("save-update: cannot create output dir {}: {e}", od.display()); - return ExitCode::from(4); - } - } - } - - let mut updated = 0usize; - let mut skipped = 0usize; - let mut had_error = false; - for src in &saves { - let name = src - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("?") - .to_string(); - let raw = match std::fs::read(src) { - Ok(b) => b, - Err(e) => { - eprintln!("{name}: read failed: {e}"); - had_error = true; - continue; - } - }; - match update_save(&raw, new_title.as_deref(), &strings) { - Ok((out_bytes, stats)) => { - let dest = match &output { - None => src.clone(), // in place - Some(o) if dir_input => o.join(&name), - Some(o) => o.clone(), // single-file -o - }; - if let Some(parent) = dest.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Err(e) = std::fs::write(&dest, &out_bytes) { - eprintln!("{name}: write failed: {e}"); - had_error = true; - continue; - } - println!( - "{name}: title={} strings={} enc={}", - u8::from(stats.title_changed), - stats.strings_replaced, - stats.encoding - ); - updated += 1; - } - Err(_) => { - // Err means the buffer is neither a standard 0x19 save nor a detectable GamePro - // Pro (marker-3) save. Report and skip, never write. - println!("{name}: SKIPPED (unsupported save encryption)"); - skipped += 1; - } - } - } - - eprintln!("save-update: {updated} updated, {skipped} skipped"); - if had_error { - ExitCode::from(4) - } else { - ExitCode::SUCCESS - } -} - -fn db_json_one(proj: &Path) -> Result { - let dat = proj.with_extension("dat"); - let pb = std::fs::read(proj).map_err(|e| e.to_string())?; - let db_ = std::fs::read(&dat).map_err(|e| e.to_string())?; - let db = Database::read(&pb, &db_).map_err(|e| e.to_string())?; - let kind = match proj.file_stem().and_then(|s| s.to_str()) { - Some("DataBase") => "UDB", - Some("CDataBase") => "CDB", - Some("SysDatabase") => "SDB", - Some(other) => other, - None => "DB", - }; - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - Ok(database_to_json(&db, kind, &glossary)) -} - -/// `wolf pack -o `. Packs a directory tree into an unencrypted, engine- -/// loadable DXArchive (VER8). The archive round-trips byte-exact through `wolf unpack`. -pub(crate) fn cmd_pack(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut encrypt = false; - let mut version: u16 = 0x12C; // v3.00 default when --encrypt is given without --version - let mut pwd: Option<[u8; 15]> = None; - let mut like: Option = None; - let mut format: Option = None; // "ver5" | "ver6" for the legacy 2.0x containers - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - "--no-encrypt" => encrypt = false, - "--encrypt" => encrypt = true, - "--format" => format = it.next().cloned(), - // Inherit cryptVersion + embedded password from an existing archive (turnkey repack). - "--like" => like = it.next().map(PathBuf::from), - "--version" => { - if let Some(v) = it.next() { - let v = v.trim_start_matches("0x"); - match u16::from_str_radix(v, 16).or_else(|_| v.parse()) { - Ok(n) => version = n, - Err(_) => { - eprintln!("pack: bad --version {v:?}"); - return ExitCode::from(64); - } - } - } - } - "--pwd" => pwd = it.next().and_then(|h| parse_pwd15(h)), - _ => input = Some(PathBuf::from(a)), - } - } - let (Some(input), Some(output)) = (input, output) else { - eprintln!( - "pack: usage: wolf pack -o \ - [--encrypt --version 0x14b [--pwd <30hex>]] | [--like ]" - ); - return ExitCode::from(64); - }; - - // `--like` inherits the original archive's cryptVersion + embedded password. - if let Some(orig) = &like { - match std::fs::read(orig) - .ok() - .and_then(|d| wolf_archive::archive_crypt_params(&d)) - { - Some((cv, p)) => { - version = cv; - pwd = Some(p); - encrypt = true; - } - None => { - eprintln!("pack: could not read crypt params from {}", orig.display()); - return ExitCode::from(4); - } - } - } - - let mut files: Vec<(String, Vec)> = Vec::new(); - if let Err(e) = gather_files(&input, &input, &mut files) { - eprintln!("pack: {e}"); - return ExitCode::from(4); - } - if files.is_empty() { - eprintln!("pack: no files found under {}", input.display()); - return ExitCode::from(4); - } - files.sort(); - - // Default new-crypt password (used only when --encrypt a new-crypt version without --pwd - // or --like). Self-describing: the archive embeds whatever password it was built with. - let default_pwd: [u8; 15] = *b"WolfDawnRepack!"; - let result = match format.as_deref() { - Some("ver5") => wolf_archive::pack_ver5(&files), - Some("ver6") => wolf_archive::pack_ver6(&files), - Some(other) => Err(format!("pack: unknown --format {other:?} (use ver5|ver6)")), - None if !encrypt => wolf_archive::pack_plaintext(&files), - None if matches!(version, 0x14B | 0x15E) => { - wolf_archive::pack_newcrypt(&files, version, &pwd.unwrap_or(default_pwd)) - } - None => wolf_archive::pack_encrypted(&files, version), - }; - let bytes = match result { - Ok(b) => b, - Err(e) => { - eprintln!("pack failed: {e}"); - return ExitCode::from(4); - } - }; - if let Err(e) = std::fs::write(&output, &bytes) { - eprintln!("pack: cannot write {}: {e}", output.display()); - return ExitCode::from(4); - } - let mode = match format.as_deref() { - Some(f) => format!("{f} (Wolf 2.0x)"), - None if encrypt => { - let src = if like.is_some() { " (--like)" } else { "" }; - format!("encrypted cryptVersion={version:#x}{src}") - } - None => "unencrypted".to_string(), - }; - eprintln!( - "packed {} files -> {} ({} bytes, {mode})", - files.len(), - output.display(), - bytes.len() - ); - ExitCode::SUCCESS -} - -/// Parse a 30-hex-char string into a 15-byte WolfPro password. -fn parse_pwd15(h: &str) -> Option<[u8; 15]> { - let h = h.trim(); - if h.len() != 30 { - return None; - } - let mut out = [0u8; 15]; - for (i, b) in out.iter_mut().enumerate() { - *b = u8::from_str_radix(&h[i * 2..i * 2 + 2], 16).ok()?; - } - Some(out) -} - -/// Extract one already-read archive into `out_dir`, preserving the inner tree. Each inner path is -/// sanitised (drop empty/`.`/`..` segments and any drive/leading separator) so a malformed archive -/// entry can never write outside `out_dir`. Returns the file count on success. -fn unpack_archive_to(data: &[u8], out_dir: &Path) -> Result { - let files = - wolf_archive::extract_archive(data, b"").map_err(|e| format!("not a readable archive ({e})"))?; - for (path, bytes) in &files { - let safe: PathBuf = path - .split(['\\', '/']) - .filter(|s| !s.is_empty() && *s != "." && *s != "..") - .collect(); - let target = out_dir.join(safe); - if let Some(parent) = target.parent() { - let _ = std::fs::create_dir_all(parent); - } - std::fs::write(&target, bytes) - .map_err(|e| format!("cannot write {}: {e}", target.display()))?; - } - Ok(files.len()) -} - -/// True for a readable file whose extension is `.wolf` (case-insensitive). -fn is_wolf_file(p: &Path) -> bool { - p.is_file() - && p.extension() - .and_then(|e| e.to_str()) - .map(|e| e.eq_ignore_ascii_case("wolf")) - .unwrap_or(false) -} - -/// The `.wolf` archives directly inside `dir`, sorted by name for a stable, reproducible order. -fn list_wolf_files(dir: &Path) -> Vec { - let mut v: Vec = std::fs::read_dir(dir) - .into_iter() - .flatten() - .flatten() - .map(|e| e.path()) - .filter(|p| is_wolf_file(p)) - .collect(); - v.sort(); - v -} - -/// `wolf unpack -o `. Extracts every file from a `.wolf` into `dir`, -/// preserving the inner directory tree. Handles any supported crypt regime. -pub(crate) fn cmd_unpack(args: &[String]) -> ExitCode { - let mut input: Option = None; - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => input = Some(PathBuf::from(a)), - } - } - let Some(input) = input else { - eprintln!("unpack: usage: wolf unpack -o "); - return ExitCode::from(64); - }; - let out_dir = output.unwrap_or_else(|| PathBuf::from("unpacked")); - - let data = match std::fs::read(&input) { - Ok(d) => d, - Err(e) => { - eprintln!("unpack: cannot read {}: {e}", input.display()); - return ExitCode::from(4); - } - }; - match unpack_archive_to(&data, &out_dir) { - Ok(n) => { - eprintln!("unpacked {n} files -> {}", out_dir.display()); - ExitCode::SUCCESS - } - Err(e) => { - eprintln!("unpack failed: {e}"); - ExitCode::from(4) - } - } -} - -/// `wolf unpack-all ... [-o ]`. Unpacks every `.wolf` among the -/// given paths. A directory argument is expanded to the `.wolf` files directly inside it, so the -/// whole split data set of a per-category game (BasicData.wolf, MapData.wolf, Evtext.wolf, ...) -/// comes apart in one call. Each `Name.wolf` lands in `/Name/`, mirroring how the engine maps -/// a category archive to a `Name/` folder, so the result is a loose data tree the game can load. -/// With no `-o`, each archive unpacks beside itself (`/Name/`). One bad archive does -/// not stop the rest, and the exit code is failure only when nothing unpacked. -pub(crate) fn cmd_unpack_all(args: &[String]) -> ExitCode { - let mut inputs: Vec = Vec::new(); - let mut output: Option = None; - let mut it = args.iter(); - while let Some(a) = it.next() { - match a.as_str() { - "-o" | "--output" => output = it.next().map(PathBuf::from), - _ => inputs.push(PathBuf::from(a)), - } - } - if inputs.is_empty() { - eprintln!("unpack-all: usage: wolf unpack-all ... [-o ]"); - return ExitCode::from(64); - } - // Expand directory args to the .wolf files inside them, keep .wolf file args as-is. - let mut archives: Vec = Vec::new(); - for p in &inputs { - if p.is_dir() { - archives.extend(list_wolf_files(p)); - } else if is_wolf_file(p) { - archives.push(p.clone()); - } else { - eprintln!("unpack-all: skipping {} (not a .wolf or directory)", p.display()); - } - } - if archives.is_empty() { - eprintln!("unpack-all: no .wolf archives found in the given paths"); - return ExitCode::from(4); - } - - let mut total_files = 0usize; - let (mut ok, mut failed) = (0usize, 0usize); - for arc in &archives { - let stem = arc.file_stem().and_then(|s| s.to_str()).unwrap_or("archive"); - let dest_root = output - .clone() - .or_else(|| arc.parent().map(Path::to_path_buf)) - .unwrap_or_default(); - let out_dir = dest_root.join(stem); - let name = arc.file_name().and_then(|s| s.to_str()).unwrap_or(stem); - match std::fs::read(arc) - .map_err(|e| e.to_string()) - .and_then(|d| unpack_archive_to(&d, &out_dir)) - { - Ok(n) => { - total_files += n; - ok += 1; - eprintln!(" {name} -> {} ({n} files)", out_dir.display()); - } - Err(e) => { - failed += 1; - eprintln!(" {name} FAILED: {e}"); - } - } - } - eprintln!( - "unpack-all: {ok} archive(s), {total_files} files{}", - if failed > 0 { - format!(", {failed} failed") - } else { - String::new() - } - ); - if ok == 0 { - ExitCode::from(4) - } else { - ExitCode::SUCCESS - } -} - -/// Recursively gather `(relative-path, bytes)` for every file under `dir`. -fn gather_files(base: &Path, dir: &Path, out: &mut Vec<(String, Vec)>) -> std::io::Result<()> { - for entry in std::fs::read_dir(dir)? { - let p = entry?.path(); - if p.is_dir() { - gather_files(base, &p, out)?; - } else { - let rel = p - .strip_prefix(base) - .unwrap_or(&p) - .to_string_lossy() - .replace('\\', "/"); - out.push((rel, std::fs::read(&p)?)); - } - } - Ok(()) -} - -/// Build a symbol table for a map by locating the game's BasicData folder -/// (`.../Data/MapData/X.mps` -> `.../Data/BasicData`) and loading CommonEvent + databases. -pub(crate) fn load_symbols(map_path: &Path) -> SymbolTable { - let mut symbols = SymbolTable::new(); - let basic_data = map_path - .parent() - .and_then(|p| p.parent()) - .map(|data| data.join("BasicData")); - let Some(basic) = basic_data else { - return symbols; - }; - - if let Ok(bytes) = std::fs::read(basic.join("CommonEvent.dat")) { - if let Ok(ce) = CommonEventsFile::read(&bytes) { - symbols.add_common_events(&ce); - } - } - add_databases(&mut symbols, Some(&basic)); - symbols.set_glossary(wolf_decompiler::symbols::load_embedded_glossary()); - symbols.set_engine_text(wolf_decompiler::symbols::load_embedded_engine_glossary()); - symbols -} - -/// Load the three standard databases from `basic_data` into the symbol table. -pub(crate) fn add_databases(symbols: &mut SymbolTable, basic_data: Option<&Path>) { - let Some(dir) = basic_data else { return }; - for (kind, stem) in [ - ("UDB", "DataBase"), - ("CDB", "CDataBase"), - ("SDB", "SysDatabase"), - ] { - let proj = dir.join(format!("{stem}.project")); - let dat = dir.join(format!("{stem}.dat")); - if let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) { - if let Ok(db) = Database::read(&p, &d) { - symbols.add_database(kind, &db); - } - } - } -} - -// ---------------------------------------------------------------------------- -// verify-roundtrip -// ---------------------------------------------------------------------------- - -pub(crate) fn cmd_verify(args: &[String]) -> ExitCode { - if args.first().map(String::as_str) == Some("--corpus") { - let Some(dir) = args.get(1) else { - eprintln!("verify-roundtrip --corpus: missing data dir"); - return ExitCode::from(64); - }; - return verify_corpus(Path::new(dir)); - } - let Some(path) = args.first() else { - eprintln!("verify-roundtrip: missing file"); - return ExitCode::from(64); - }; - let path = PathBuf::from(path); - match roundtrip_path(&path) { - Ok(true) => { - println!("OK byte-exact {}", path.display()); - ExitCode::SUCCESS - } - Ok(false) => { - println!("DIFF {}", path.display()); - ExitCode::from(2) - } - Err(e) => { - eprintln!("ERR {}: {e}", path.display()); - ExitCode::from(4) - } - } -} - -pub(crate) fn verify_corpus(dir: &Path) -> ExitCode { - let mut ok = 0usize; - let mut diff = 0usize; - let mut err = 0usize; - - let mut files: Vec = Vec::new(); - collect(dir, &mut files); - files.sort(); - - for path in files { - match classify(&path) { - FileKind::Unsupported => continue, - // The .dat half of a database is verified via its .project. Skip the lone .dat. - FileKind::Database | FileKind::BasicDatabase - if path.extension().and_then(|s| s.to_str()) == Some("dat") => - { - continue - } - _ => {} - } - match roundtrip_path(&path) { - Ok(true) => { - ok += 1; - println!("OK {}", rel(dir, &path)); - } - Ok(false) => { - diff += 1; - println!("DIFF {}", rel(dir, &path)); - } - Err(e) => { - err += 1; - println!("ERR {} ({e})", rel(dir, &path)); - } - } - } - - println!("\n{ok} ok, {diff} diff, {err} error"); - if diff == 0 && err == 0 { - ExitCode::SUCCESS - } else if diff > 0 { - ExitCode::from(2) - } else { - ExitCode::from(4) - } -} - -/// Returns Ok(true) if `write(read(bytes)) == bytes`. -pub(crate) fn roundtrip_path(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - match classify(path) { - FileKind::Map => { - let m = Map::read(&bytes).map_err(|e| e.to_string())?; - Ok(m.write() == bytes) - } - FileKind::CommonEvent => { - let ce = CommonEventsFile::read(&bytes).map_err(|e| e.to_string())?; - Ok(ce.write() == bytes) - } - FileKind::GameDat => { - let gd = GameDat::read(&bytes).map_err(|e| e.to_string())?; - Ok(gd.write() == bytes) - } - FileKind::Database => { - // `path` is the .project. Pair it with the sibling .dat. - let dat = path.with_extension("dat"); - let proj_bytes = bytes; - let dat_bytes = std::fs::read(&dat).map_err(|e| e.to_string())?; - let db = Database::read(&proj_bytes, &dat_bytes).map_err(|e| e.to_string())?; - let (proj_out, dat_out) = db.write(); - Ok(proj_out == proj_bytes && dat_out == dat_bytes) - } - FileKind::BasicDatabase => { - // Legacy basic DB: schema `.project` + opaque `.dat`, paired by the `.project`. - let proj_path = path.with_extension("project"); - let dat_path = path.with_extension("dat"); - let proj_bytes = std::fs::read(&proj_path).map_err(|e| e.to_string())?; - let dat_bytes = std::fs::read(&dat_path).map_err(|e| e.to_string())?; - let db = wolf_formats::database::BasicDatabase::read(&proj_bytes, &dat_bytes) - .map_err(|e| e.to_string())?; - let (proj_out, dat_out) = db.write(); - Ok(proj_out == proj_bytes && dat_out == dat_bytes) - } - FileKind::TxtEvent => { - // An external event-text file round-trips through a no-op extract/inject, which must - // reproduce the bytes exactly (this is the byte-exactness guarantee the translation - // pipeline relies on). - let json = extract_txt_events(&bytes); - let out = inject_txt_events(&json, &bytes).map_err(|e| e.to_string())?; - Ok(out == bytes) - } - FileKind::Unsupported => Err("unsupported file type".into()), - } -} - -// ---------------------------------------------------------------------------- -// file classification -// ---------------------------------------------------------------------------- - -pub(crate) enum FileKind { - Map, - CommonEvent, - /// The per-game settings/title record (`Game.dat`). - GameDat, - /// Represented by the `.project` file (the `.dat` sibling is paired in). - Database, - /// Legacy `SysDataBaseBasic` (Wolf 2.x): schema-only `.project` + opaque magic-less `.dat`. - BasicDatabase, - /// An external event-text `.txt` file (Shift-JIS or UTF-8 dialogue, one scene per file). The - /// `Evtext*.wolf` archives unpack to these. Handled by `txt_events`. - TxtEvent, - Unsupported, -} - -pub(crate) fn classify(path: &Path) -> FileKind { - let ext = path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - let name = path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - - match ext.as_str() { - "mps" => FileKind::Map, - // The legacy basic system DB is keyed by its fixed file name. - "project" if name == "sysdatabasebasic.project" => FileKind::BasicDatabase, - "dat" if name == "sysdatabasebasic.dat" => FileKind::BasicDatabase, - "project" => FileKind::Database, - "dat" if name == "commonevent.dat" => FileKind::CommonEvent, - "dat" if name == "game.dat" => FileKind::GameDat, - // A .dat with a sibling .project is the value half of a database. - "dat" if path.with_extension("project").exists() => FileKind::Database, - // External event-text. The only `.txt` use in a Wolf game is unpacked Evtext scene files. - "txt" => FileKind::TxtEvent, - _ => FileKind::Unsupported, - } -} - -pub(crate) fn collect(dir: &Path, out: &mut Vec) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let p = entry.path(); - if p.is_dir() { - collect(&p, out); - } else { - out.push(p); - } - } -} - -pub(crate) fn rel(base: &Path, path: &Path) -> String { - path.strip_prefix(base) - .unwrap_or(path) - .display() - .to_string() -} - -// ---------------------------------------------------------------------------- -// gui: launch WolfDawn Studio (the eframe desktop GUI), if it sits next to us -// ---------------------------------------------------------------------------- - -/// `wolf gui`. Launches the `wolf-gui` desktop app from beside the current executable. This crate -/// has no eframe dependency. It only *spawns* the separately-built `wolf-gui` binary. If that -/// binary is not found next to `wolf`, print how to build/run it and exit non-fatally. -pub(crate) fn cmd_gui(args: &[String]) -> ExitCode { - let exe = match std::env::current_exe() { - Ok(p) => p, - Err(e) => { - eprintln!("gui: cannot locate the current executable: {e}"); - return ExitCode::from(4); - } - }; - let dir = exe.parent().unwrap_or(Path::new(".")); - let gui_name = if cfg!(windows) { "wolf-gui.exe" } else { "wolf-gui" }; - let gui_path = dir.join(gui_name); - - if !gui_path.exists() { - eprintln!( - "gui: WolfDawn Studio ({}) was not found next to {}.\n\ - Build and run it with: cargo run --release -p wolf-gui", - gui_name, - exe.display() - ); - return ExitCode::SUCCESS; // non-fatal: nothing went wrong, the GUI just isn't built - } - - match std::process::Command::new(&gui_path).args(args).spawn() { - Ok(_) => { - eprintln!("launched {}", gui_path.display()); - ExitCode::SUCCESS - } - Err(e) => { - eprintln!("gui: failed to launch {}: {e}", gui_path.display()); - ExitCode::from(4) - } - } -} diff --git a/util/wolfdawn-master/crates/wolf-cli/src/main.rs b/util/wolfdawn-master/crates/wolf-cli/src/main.rs deleted file mode 100644 index 8cd1c98..0000000 --- a/util/wolfdawn-master/crates/wolf-cli/src/main.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! `wolf`. WolfDawn command-line front-end. -//! -//! Subcommands (P0/P1 subset): -//! wolf decompile [--mode edit] [-o out.wscript] -//! wolf compile --base -o -//! wolf verify-roundtrip | --corpus -//! -//! Exit codes: 0 ok · 2 round-trip mismatch · 4 read/parse failure · 64 usage. -use std::process::ExitCode; - -mod cmd; -use cmd::*; - -fn main() -> ExitCode { - let args: Vec = std::env::args().skip(1).collect(); - match args.first().map(String::as_str) { - Some("decompile") => cmd_decompile(&args[1..]), - Some("compile") => cmd_compile(&args[1..]), - Some("pack") => cmd_pack(&args[1..]), - Some("unpack") => cmd_unpack(&args[1..]), - Some("unpack-all") => cmd_unpack_all(&args[1..]), - Some("db-json") => cmd_db_json(&args[1..]), - Some("db-apply") => cmd_db_apply(&args[1..]), - Some("gamedat-json") => cmd_gamedat_json(&args[1..]), - Some("gamedat-apply") => cmd_gamedat_apply(&args[1..]), - Some("strings-extract") => cmd_strings_extract(&args[1..]), - Some("strings-audit") => cmd_strings_audit(&args[1..]), - Some("strings-inject") => cmd_strings_inject(&args[1..]), - Some("names-extract") => cmd_names_extract(&args[1..]), - Some("names-inject") => cmd_names_inject(&args[1..]), - Some("names-check") => cmd_names_check(&args[1..]), - Some("translations-merge") => cmd_translations_merge(&args[1..]), - Some("save-update") => cmd_save_update(&args[1..]), - Some("gui") => cmd_gui(&args[1..]), - Some("raw") => cmd_raw(&args[1..]), - Some("xref") => cmd_xref(&args[1..]), - Some("export-names") => cmd_export_names(&args[1..]), - Some("verify-roundtrip") => cmd_verify(&args[1..]), - Some("-h") | Some("--help") | Some("help") | None => { - usage(); - ExitCode::SUCCESS - } - Some(other) => { - eprintln!("unknown subcommand: {other}\n"); - usage(); - ExitCode::from(64) - } - } -} - -fn usage() { - eprintln!( - "wolf - WolfDawn toolchain\n\n\ - USAGE:\n \ - wolf decompile [--mode edit] [-o out.wscript]\n \ - wolf compile --base -o \n \ - wolf pack -o [--encrypt --version 0x14b | --like | --format ver5|ver6]\n \ - wolf unpack -o \n \ - wolf unpack-all ... [-o ] (unpack every .wolf, each into out//)\n \ - wolf db-json [-o out]\n \ - wolf db-apply --base -o \n \ - wolf gamedat-json [-o out.json]\n \ - wolf gamedat-apply --base -o \n \ - wolf strings-extract -o \n \ - wolf strings-inject --base -o [--allow-code-drift] [--en-punct]\n \ - wolf names-extract -o \n \ - wolf names-inject --data [-o ] [--allow-code-drift] [--en-punct]\n \ - wolf names-check ... (report names translated inconsistently across files)\n \ - wolf translations-merge --old ... --new -o (carry old translations into a re-extraction)\n \ - wolf save-update [-o ] [--title | --game ] [--translations ...]\n \ - wolf gui (launch WolfDawn Studio, the desktop GUI)\n \ - wolf verify-roundtrip \n \ - wolf verify-roundtrip --corpus \n" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-core/Cargo.toml b/util/wolfdawn-master/crates/wolf-core/Cargo.toml deleted file mode 100644 index 9fb97ad..0000000 --- a/util/wolfdawn-master/crates/wolf-core/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "wolf-core" -edition.workspace = true -version.workspace = true -license.workspace = true -description = "Low-level Wolf RPG wire primitives: byte reader/writer, length-prefixed strings, codecs. No Wolf domain semantics." diff --git a/util/wolfdawn-master/crates/wolf-core/src/codec/lz4.rs b/util/wolfdawn-master/crates/wolf-core/src/codec/lz4.rs deleted file mode 100644 index 2313317..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/codec/lz4.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! LZ4 block format codec (not the LZ4 frame format). This is the exact variant Wolf RPG -//! Editor v3.5 ("Pro") uses for compressed file bodies via `LZ4_decompress_safe` and -//! `LZ4_compress_default`. -//! -//! Wolf wraps a block as `[decompressedSize: u32][compressedSize: u32][block bytes]`. -//! [`unpack`]/[`pack`] handle that framing, [`decompress_block`]/[`compress_block`] the -//! raw block. -//! -//! The encoder is a greedy single-pass LZ4 compressor with a 4-byte hash table match finder. -//! It emits a valid compressed block honouring the constraints `LZ4_decompress_safe` requires. -//! The last 5 bytes are literals, no match starts within the last 12 bytes, min match is 4, -//! and offsets are 1..=65535. Byte-exact round-trip of unmodified files is achieved at the -//! format layer by re-emitting the original packed bytes verbatim. Fresh compression is used -//! for edited bodies, where what matters is validity and a reasonable size, not byte-identity -//! with the editor's own LZ4. - -use crate::error::{Error, Result}; - -/// Decompress a raw LZ4 block of known output size. -pub fn decompress_block(src: &[u8], decoded_size: usize) -> Result> { - let mut out = Vec::with_capacity(decoded_size); - let mut sp = 0usize; - - while sp < src.len() { - let token = src[sp]; - sp += 1; - - // Literal run. - let literal_len = read_len(src, &mut sp, (token >> 4) as usize)?; - if sp + literal_len > src.len() { - return Err(Error::invalid("lz4: literal run exceeds source")); - } - out.extend_from_slice(&src[sp..sp + literal_len]); - sp += literal_len; - - // A block legitimately ends right after a literal run (the last sequence). - if sp >= src.len() { - break; - } - if sp + 2 > src.len() { - return Err(Error::invalid("lz4: match offset is truncated")); - } - let offset = u16::from_le_bytes([src[sp], src[sp + 1]]) as usize; - sp += 2; - if offset == 0 || offset > out.len() { - return Err(Error::invalid("lz4: match offset is invalid")); - } - - // Match copy (min length 4, byte-by-byte to allow overlap). - let match_len = read_len(src, &mut sp, (token & 0x0F) as usize)? + 4; - for _ in 0..match_len { - let b = out[out.len() - offset]; - out.push(b); - } - } - - if out.len() != decoded_size { - return Err(Error::invalid(format!( - "lz4: decoded {} bytes, expected {decoded_size}", - out.len() - ))); - } - Ok(out) -} - -/// Read an LZ4 length (the `0xF`/`0xFF`-extension scheme) given the 4-bit token nibble. -fn read_len(src: &[u8], sp: &mut usize, base: usize) -> Result { - let mut len = base; - if base != 15 { - return Ok(len); - } - loop { - let b = *src - .get(*sp) - .ok_or_else(|| Error::invalid("lz4: length extension truncated"))? - as usize; - *sp += 1; - len = len - .checked_add(b) - .ok_or_else(|| Error::invalid("lz4: length overflow"))?; - if b != 255 { - break; - } - } - Ok(len) -} - -// LZ4 block constraints (so `LZ4_decompress_safe` accepts the output). -const MIN_MATCH: usize = 4; -/// No match may *start* within the last 12 bytes of the block. -const MFLIMIT: usize = 12; -/// The last 5 bytes of the block are always literals. -const LAST_LITERALS: usize = 5; -const HASH_BITS: u32 = 16; - -fn read_u32(s: &[u8], i: usize) -> u32 { - u32::from_le_bytes([s[i], s[i + 1], s[i + 2], s[i + 3]]) -} - -fn hash4(v: u32) -> usize { - (v.wrapping_mul(2_654_435_761) >> (32 - HASH_BITS)) as usize -} - -/// Append an LZ4 length extension (the value beyond the 15 already encoded in the token nibble). -fn write_len_ext(out: &mut Vec, mut rem: usize) { - while rem >= 255 { - out.push(255); - rem -= 255; - } - out.push(rem as u8); -} - -/// Emit the trailing literals-only sequence (no match), terminating the block. -fn emit_last_literals(out: &mut Vec, lits: &[u8]) { - let n = lits.len(); - out.push(((n.min(15)) as u8) << 4); - if n >= 15 { - write_len_ext(out, n - 15); - } - out.extend_from_slice(lits); -} - -/// Compress to a valid LZ4 block with a greedy 4-byte-hash match finder. Used for -/// freshly-serialized (edited) bodies. Unmodified bodies are re-emitted verbatim by the format -/// layer. Round-trips through [`decompress_block`] and respects the engine's block constraints. -pub fn compress_block(src: &[u8]) -> Vec { - let n = src.len(); - let mut out = Vec::with_capacity(n / 2 + 16); - - // Too short to hold a match plus the mandatory trailing literals: emit all literals. - if n < MFLIMIT + MIN_MATCH { - emit_last_literals(&mut out, src); - return out; - } - let match_limit = n - MFLIMIT; // a match may only start at ip < match_limit - let match_end = n - LAST_LITERALS; // a match may not cover bytes at/after this - - let mut table = vec![usize::MAX; 1 << HASH_BITS]; - let mut anchor = 0usize; // start of the pending literal run - let mut ip = 0usize; - table[hash4(read_u32(src, ip))] = ip; - ip += 1; - - while ip < match_limit { - let h = hash4(read_u32(src, ip)); - let cand = table[h]; - table[h] = ip; - - let is_match = - cand != usize::MAX && ip - cand <= 0xFFFF && read_u32(src, cand) == read_u32(src, ip); - if !is_match { - ip += 1; - continue; - } - - // Extend the match (stops before the mandatory last-literals tail). - let offset = ip - cand; - let mut mlen = MIN_MATCH; - while ip + mlen < match_end && src[cand + mlen] == src[ip + mlen] { - mlen += 1; - } - - // Emit: pending literals, then the match. - let lits = &src[anchor..ip]; - let lit_len = lits.len(); - let match_tok = (mlen - MIN_MATCH).min(15); - out.push(((lit_len.min(15) as u8) << 4) | match_tok as u8); - if lit_len >= 15 { - write_len_ext(&mut out, lit_len - 15); - } - out.extend_from_slice(lits); - out.extend_from_slice(&(offset as u16).to_le_bytes()); - if mlen - MIN_MATCH >= 15 { - write_len_ext(&mut out, mlen - MIN_MATCH - 15); - } - - ip += mlen; - anchor = ip; - } - - // Trailing literals (always at least LAST_LITERALS bytes). - emit_last_literals(&mut out, &src[anchor..]); - out -} - -/// Decompress Wolf's framed form `[decSize u32][encSize u32][block]` (the bytes that -/// follow a v3.5 file header). Trailing bytes beyond `encSize` are ignored. -pub fn unpack(framed: &[u8]) -> Result> { - if framed.len() < 8 { - return Err(Error::invalid( - "lz4: framed body shorter than 8-byte header", - )); - } - let dec_size = u32::from_le_bytes([framed[0], framed[1], framed[2], framed[3]]) as usize; - let enc_size = u32::from_le_bytes([framed[4], framed[5], framed[6], framed[7]]) as usize; - let block = framed - .get(8..8 + enc_size) - .ok_or_else(|| Error::invalid("lz4: framed body shorter than declared compressed size"))?; - decompress_block(block, dec_size) -} - -/// Produce Wolf's framed form `[decSize u32][encSize u32][block]` for a body. -pub fn pack(body: &[u8]) -> Vec { - let block = compress_block(body); - let mut out = Vec::with_capacity(block.len() + 8); - out.extend_from_slice(&(body.len() as u32).to_le_bytes()); - out.extend_from_slice(&(block.len() as u32).to_le_bytes()); - out.extend_from_slice(&block); - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn roundtrip(data: &[u8]) { - let packed = pack(data); - let back = unpack(&packed).expect("unpack"); - assert_eq!(back, data, "lz4 pack/unpack round-trip mismatch"); - } - - #[test] - fn pack_unpack_roundtrips() { - roundtrip(b""); - roundtrip(b"a"); - roundtrip(b"hello world"); - roundtrip(&[0u8; 1000]); - // 14, 15, 16 bytes straddle the literal-length extension boundary. - for n in [14usize, 15, 16, 270, 271, 600] { - roundtrip(&(0..n).map(|i| (i * 31 % 256) as u8).collect::>()); - } - } - - #[test] - fn compresses_and_roundtrips() { - // Highly repetitive -> should compress hard and still round-trip. - let zeros = vec![0u8; 10_000]; - let p = pack(&zeros); - assert_eq!(unpack(&p).unwrap(), zeros); - assert!( - p.len() < zeros.len() / 20, - "zeros packed to {} bytes", - p.len() - ); - - // Repeating phrase. - let mut text = Vec::new(); - for _ in 0..2000 { - text.extend_from_slice(b"the quick brown fox jumps. "); - } - let p = pack(&text); - assert_eq!(unpack(&p).unwrap(), text); - assert!(p.len() < text.len() / 3, "text packed to {} bytes", p.len()); - - // Varied sizes / patterns straddling the match + last-literal boundaries. - for n in [0usize, 1, 4, 11, 12, 13, 16, 17, 64, 255, 256, 4096, 12345] { - let data: Vec = (0..n).map(|i| ((i * 7 + i / 13) % 256) as u8).collect(); - let p = pack(&data); - assert_eq!(unpack(&p).unwrap(), data, "roundtrip failed at n={n}"); - } - } - - #[test] - fn decodes_a_match_bearing_block() { - // "abcabcabc": emit literals "abc" then a match (offset 3, len 6). - // token: litlen=3 (hi), matchlen-4=2 (lo) => 0x32, then "abc", then offset=3, no ext. - let block = [0x32u8, b'a', b'b', b'c', 0x03, 0x00]; - let out = decompress_block(&block, 9).expect("decode"); - assert_eq!(&out, b"abcabcabc"); - } -} diff --git a/util/wolfdawn-master/crates/wolf-core/src/codec/mod.rs b/util/wolfdawn-master/crates/wolf-core/src/codec/mod.rs deleted file mode 100644 index c5ddd8a..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/codec/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Compression and encoding codecs shared across Wolf formats. The LZ4 block codec used by -//! v3.5 ("Pro") compressed file bodies, and the `.sav` outer XOR cipher ([`wolfsave`]). - -pub mod lz4; -pub mod wolfsave; diff --git a/util/wolfdawn-master/crates/wolf-core/src/codec/wolfsave.rs b/util/wolfdawn-master/crates/wolf-core/src/codec/wolfsave.rs deleted file mode 100644 index 366a5d4..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/codec/wolfsave.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Wolf RPG `.sav` outer encryption: a 3-pass MSVCRT-`rand` XOR stream over the file body -//! (everything from offset `0x14` to EOF), keyed by three seed bytes pulled from the file head. -//! -//! This is the outer layer only. It leaves the decrypted plaintext (header, baked title, -//! length-prefixed strings, variable database, and so on) for a higher layer to interpret. The -//! codec is byte-exact: [`encrypt`]`(`[`decrypt`]`(raw)) == raw` for any real save whose stored -//! checksum already matches its body. That is the common case, see [`fix_checksum`]. -//! -//! The MSVCRT generator is reproduced exactly. `state = state*214013 + 2531011` then -//! `out = (state >> 16) & 0x7FFF`, with `srand(seed)` setting `state = seed`. Each XOR byte is -//! `(rand() >> 12) & 0xFF`. - -/// Body starts here. Bytes `0x00..0x14` (seeds, checksum, flags) are never XORed. -pub const START_OFFSET: usize = 0x14; - -/// MSVCRT `rand`/`srand`. `state` seeds directly from `srand`'s argument. -struct MsvcRand { - state: u32, -} - -impl MsvcRand { - fn new(seed: u8) -> Self { - // srand takes an int. The engine seeds it with a single header byte (0..=255). - MsvcRand { state: seed as u32 } - } - - fn next(&mut self) -> u16 { - self.state = self.state.wrapping_mul(214013).wrapping_add(2531011); - ((self.state >> 16) & 0x7FFF) as u16 - } -} - -/// XOR one pass over the body in place: `srand(seed)`, then for every `step`-th byte from -/// `START_OFFSET` to the end, `data[j] ^= (rand() >> 12) & 0xFF`. -fn xor_stream(data: &mut [u8], seed: u8, step: usize) { - let mut rng = MsvcRand::new(seed); - let mut j = START_OFFSET; - while j < data.len() { - data[j] ^= ((rng.next() >> 12) & 0xFF) as u8; - j += step; - } -} - -/// Decrypt a raw `.sav` buffer into its plaintext form. -/// -/// Seeds are `s0 = data[0]`, `s1 = data[3]`, `s2 = data[9]`. The passes run in that order with -/// increments `[1, 2, 5]`. A buffer too small to have a body (`len <= START_OFFSET`) is returned -/// unchanged. -pub fn decrypt(raw: &[u8]) -> Vec { - let mut data = raw.to_vec(); - if data.len() <= START_OFFSET { - return data; - } - let (s0, s1, s2) = (data[0], data[3], data[9]); - xor_stream(&mut data, s0, 1); - xor_stream(&mut data, s1, 2); - xor_stream(&mut data, s2, 5); - data -} - -/// Recompute the body checksum: `data[2] = sum(data[0x14..]) & 0xFF`. Call before [`encrypt`] -/// whenever the body may have changed. No-op for a buffer with no body. -// `&mut Vec` (rather than `&mut [u8]`) is the public signature for this codec. -#[allow(clippy::ptr_arg)] -pub fn fix_checksum(data: &mut Vec) { - if data.len() > START_OFFSET { - let sum = data[START_OFFSET..] - .iter() - .fold(0u8, |acc, &b| acc.wrapping_add(b)); - data[2] = sum; - } -} - -/// Encrypt a plaintext buffer back into a raw `.sav`. -/// -/// This is the exact inverse of [`decrypt`]. The checksum is recomputed first, then the three -/// XOR passes run in reverse seed order (`s0 = data[9]`, `s1 = data[3]`, `s2 = data[0]`) with -/// increments `[5, 2, 1]`. Because the seeds are read from the never-XORed head bytes, decrypt -/// and encrypt key off identical seed values, so the passes cancel and -/// `encrypt(decrypt(raw)) == raw` for any save whose stored checksum already equals its body -/// checksum. -pub fn encrypt(plain: &[u8]) -> Vec { - let mut data = plain.to_vec(); - if data.len() <= START_OFFSET { - return data; - } - fix_checksum(&mut data); - let (s0, s1, s2) = (data[9], data[3], data[0]); - xor_stream(&mut data, s0, 5); - xor_stream(&mut data, s1, 2); - xor_stream(&mut data, s2, 1); - data -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn msvc_rand_matches_reference_sequence() { - // After srand(1), the first few rand() values from the canonical MSVCRT generator. - let mut r = MsvcRand::new(1); - assert_eq!(r.next(), 41); - assert_eq!(r.next(), 18467); - assert_eq!(r.next(), 6334); - } - - #[test] - fn roundtrip_on_synthetic_data() { - // Build a buffer with a body, give it a correct checksum, encrypt then decrypt: identical. - let mut plain: Vec = (0u8..=200).cycle().take(0x14 + 137).collect(); - // Vary the seed bytes so all three passes use distinct seeds. - plain[0] = 0x37; - plain[3] = 0x9A; - plain[9] = 0x04; - fix_checksum(&mut plain); - - let enc = encrypt(&plain); - let dec = decrypt(&enc); - assert_eq!(dec, plain, "decrypt(encrypt(plain)) must be identity"); - - // And the body actually changed under encryption (not a no-op). - assert_ne!(&enc[START_OFFSET..], &plain[START_OFFSET..]); - // The head (seeds/checksum/flags) is never XORed, so it is carried verbatim. - assert_eq!(&enc[..START_OFFSET], &plain[..START_OFFSET]); - } - - #[test] - fn fix_checksum_sets_byte2() { - let mut data = vec![0u8; 0x14 + 4]; - data[0x14] = 10; - data[0x15] = 20; - data[0x16] = 30; - data[0x17] = 40; - fix_checksum(&mut data); - assert_eq!(data[2], 100); - } - - #[test] - fn tiny_buffer_is_untouched() { - let raw = vec![1u8, 2, 3]; - assert_eq!(decrypt(&raw), raw); - assert_eq!(encrypt(&raw), raw); - } -} diff --git a/util/wolfdawn-master/crates/wolf-core/src/error.rs b/util/wolfdawn-master/crates/wolf-core/src/error.rs deleted file mode 100644 index 9439b9d..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/error.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fmt; - -pub type Result = std::result::Result; - -/// Errors raised by the low-level wire layer. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Error { - /// Tried to read past the end of the buffer. - UnexpectedEof { - context: &'static str, - needed: usize, - pos: usize, - len: usize, - }, - /// A fixed magic/marker byte sequence did not match. - BadMagic { - context: &'static str, - expected: Vec, - found: Vec, - }, - /// A structural invariant was violated. - Invalid(String), -} - -impl Error { - pub fn invalid(msg: impl Into) -> Self { - Error::Invalid(msg.into()) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::UnexpectedEof { - context, - needed, - pos, - len, - } => write!( - f, - "unexpected EOF while reading {context}: need {needed} byte(s) at offset {pos}, buffer is {len} byte(s)" - ), - Error::BadMagic { - context, - expected, - found, - } => write!( - f, - "bad magic for {context}: expected {}, found {}", - hex(expected), - hex(found) - ), - Error::Invalid(msg) => write!(f, "{msg}"), - } - } -} - -impl std::error::Error for Error {} - -fn hex(bytes: &[u8]) -> String { - let mut s = String::with_capacity(bytes.len() * 3); - for (i, b) in bytes.iter().enumerate() { - if i > 0 { - s.push(' '); - } - s.push_str(&format!("{b:02x}")); - } - s -} diff --git a/util/wolfdawn-master/crates/wolf-core/src/lib.rs b/util/wolfdawn-master/crates/wolf-core/src/lib.rs deleted file mode 100644 index ebbe070..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! `wolf-core`: the low-level Wolf RPG wire layer. -//! -//! Provides the byte [`Reader`]/[`Writer`] and the length-prefixed [`WStr`] string -//! primitive shared by every Wolf file format. This layer carries no Wolf domain -//! semantics (no maps, databases or commands) and decodes nothing it cannot reproduce -//! byte-for-byte. Strings are kept as raw bytes so read/write is exact regardless of -//! Shift-JIS vs UTF-8 encoding. - -#![forbid(unsafe_code)] - -pub mod codec; -pub mod error; -pub mod reader; -pub mod writer; -pub mod wstr; - -pub use error::{Error, Result}; -pub use reader::Reader; -pub use writer::Writer; -pub use wstr::WStr; diff --git a/util/wolfdawn-master/crates/wolf-core/src/reader.rs b/util/wolfdawn-master/crates/wolf-core/src/reader.rs deleted file mode 100644 index 616d870..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/reader.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::error::{Error, Result}; -use crate::wstr::WStr; - -/// A cursor over an in-memory byte buffer that decodes the Wolf wire primitives. -/// -/// `Int` = u32 little-endian, `Byte` = u8, `String` = u32 length prefix plus that many -/// raw bytes (kept undecoded, see [`WStr`]). -pub struct Reader<'a> { - data: &'a [u8], - pos: usize, -} - -impl<'a> Reader<'a> { - pub fn new(data: &'a [u8]) -> Self { - Reader { data, pos: 0 } - } - - pub fn pos(&self) -> usize { - self.pos - } - - pub fn len(&self) -> usize { - self.data.len() - } - - pub fn is_empty(&self) -> bool { - self.data.is_empty() - } - - pub fn remaining(&self) -> usize { - self.data.len().saturating_sub(self.pos) - } - - pub fn is_eof(&self) -> bool { - self.pos >= self.data.len() - } - - pub fn seek(&mut self, pos: usize) { - self.pos = pos; - } - - /// Absolute byte at `idx` without moving the cursor. - pub fn at(&self, idx: usize) -> Option { - self.data.get(idx).copied() - } - - pub fn skip(&mut self, n: usize, context: &'static str) -> Result<()> { - self.ensure(n, context)?; - self.pos += n; - Ok(()) - } - - fn ensure(&self, n: usize, context: &'static str) -> Result<()> { - if self.pos + n > self.data.len() { - return Err(Error::UnexpectedEof { - context, - needed: n, - pos: self.pos, - len: self.data.len(), - }); - } - Ok(()) - } - - pub fn read_u8(&mut self) -> Result { - self.ensure(1, "u8")?; - let v = self.data[self.pos]; - self.pos += 1; - Ok(v) - } - - pub fn read_u32(&mut self) -> Result { - self.ensure(4, "u32")?; - let b = &self.data[self.pos..self.pos + 4]; - self.pos += 4; - Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) - } - - /// Peek the next u32 without advancing the cursor. - pub fn peek_u32(&self) -> Result { - self.ensure(4, "peek u32")?; - let b = &self.data[self.pos..self.pos + 4]; - Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) - } - - pub fn read_bytes(&mut self, n: usize) -> Result> { - self.ensure(n, "fixed bytes")?; - let v = self.data[self.pos..self.pos + n].to_vec(); - self.pos += n; - Ok(v) - } - - /// `String`: u32 byte-length prefix + that many raw bytes. - pub fn read_string(&mut self) -> Result { - let size = self.read_u32()? as usize; - let bytes = self.read_bytes(size)?; - Ok(WStr(bytes)) - } - - /// `ByteArray`: u32 count + that many bytes. - pub fn read_byte_array(&mut self) -> Result> { - let size = self.read_u32()? as usize; - self.read_bytes(size) - } - - /// `IntArray`: u32 count + that many u32s. - pub fn read_int_array(&mut self) -> Result> { - let size = self.read_u32()? as usize; - let mut out = Vec::with_capacity(size.min(1 << 16)); - for _ in 0..size { - out.push(self.read_u32()?); - } - Ok(out) - } - - /// `StringArray`: u32 count + that many `String`s. - pub fn read_string_array(&mut self) -> Result> { - let size = self.read_u32()? as usize; - let mut out = Vec::with_capacity(size.min(1 << 16)); - for _ in 0..size { - out.push(self.read_string()?); - } - Ok(out) - } - - /// Verify a fixed marker/magic byte sequence and advance past it. - pub fn verify(&mut self, expected: &[u8], context: &'static str) -> Result<()> { - self.ensure(expected.len(), context)?; - let found = &self.data[self.pos..self.pos + expected.len()]; - if found != expected { - return Err(Error::BadMagic { - context, - expected: expected.to_vec(), - found: found.to_vec(), - }); - } - self.pos += expected.len(); - Ok(()) - } -} diff --git a/util/wolfdawn-master/crates/wolf-core/src/writer.rs b/util/wolfdawn-master/crates/wolf-core/src/writer.rs deleted file mode 100644 index 2dba077..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/writer.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::wstr::WStr; - -/// Accumulates the Wolf wire primitives into a byte buffer. The exact inverse of -/// [`crate::reader::Reader`]. Writing back everything that was read reproduces the -/// original bytes, which is the round-trip contract for the format layer. -#[derive(Default)] -pub struct Writer { - buf: Vec, -} - -impl Writer { - pub fn new() -> Self { - Writer { buf: Vec::new() } - } - - pub fn with_capacity(cap: usize) -> Self { - Writer { - buf: Vec::with_capacity(cap), - } - } - - pub fn len(&self) -> usize { - self.buf.len() - } - - pub fn is_empty(&self) -> bool { - self.buf.is_empty() - } - - pub fn as_slice(&self) -> &[u8] { - &self.buf - } - - pub fn into_bytes(self) -> Vec { - self.buf - } - - pub fn write_u8(&mut self, v: u8) { - self.buf.push(v); - } - - pub fn write_u32(&mut self, v: u32) { - self.buf.extend_from_slice(&v.to_le_bytes()); - } - - pub fn write_bytes(&mut self, b: &[u8]) { - self.buf.extend_from_slice(b); - } - - pub fn write_string(&mut self, s: &WStr) { - self.write_u32(s.len() as u32); - self.buf.extend_from_slice(s.as_bytes()); - } - - pub fn write_byte_array(&mut self, b: &[u8]) { - self.write_u32(b.len() as u32); - self.buf.extend_from_slice(b); - } - - pub fn write_int_array(&mut self, a: &[u32]) { - self.write_u32(a.len() as u32); - for &v in a { - self.write_u32(v); - } - } - - pub fn write_string_array(&mut self, a: &[WStr]) { - self.write_u32(a.len() as u32); - for s in a { - self.write_string(s); - } - } -} diff --git a/util/wolfdawn-master/crates/wolf-core/src/wstr.rs b/util/wolfdawn-master/crates/wolf-core/src/wstr.rs deleted file mode 100644 index 6965fb5..0000000 --- a/util/wolfdawn-master/crates/wolf-core/src/wstr.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::fmt; - -/// A Wolf length-prefixed string, stored as its raw on-disk bytes (no length prefix -/// included). Encoding is either Shift-JIS (CP932) or UTF-8 depending on the owning -/// file's magic. The low-level layer does not decode it, so read/write stays byte-exact -/// regardless of encoding. Decoding to UTF-8 for human display happens in the decompiler -/// layer. -#[derive(Clone, Default, PartialEq, Eq, Hash)] -pub struct WStr(pub Vec); - -impl WStr { - pub fn new(bytes: Vec) -> Self { - WStr(bytes) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Lossy UTF-8 view, for diagnostics/display only. Not used on the round-trip path. - pub fn to_lossy(&self) -> String { - String::from_utf8_lossy(&self.0).into_owned() - } -} - -impl fmt::Debug for WStr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.to_lossy()) - } -} - -impl From<&str> for WStr { - fn from(s: &str) -> Self { - WStr(s.as_bytes().to_vec()) - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/Cargo.toml b/util/wolfdawn-master/crates/wolf-decompiler/Cargo.toml deleted file mode 100644 index ff74c93..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "wolf-decompiler" -edition.workspace = true -version.workspace = true -license.workspace = true -description = "Renders Wolf RPG event command programs into readable, editable WolfScript." - -[dependencies] -wolf-core = { path = "../wolf-core" } -wolf-formats = { path = "../wolf-formats" } -# Encoding (SJIS/CP932 <-> UTF-8) is permitted at the decompiler layer only. -encoding_rs = "0.8" -# The command reference (data/commands.json) is loaded via serde at startup. -serde = { version = "1", features = ["derive"] } -serde_json = "1" diff --git a/util/wolfdawn-master/crates/wolf-decompiler/examples/roundtrip_check.rs b/util/wolfdawn-master/crates/wolf-decompiler/examples/roundtrip_check.rs deleted file mode 100644 index 1630e08..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/examples/roundtrip_check.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Byte-exact round-trip check for the gated/recompilable path across a corpus root: -//! `compile_commands_enc(decompile_commands_enc(cmds)) == cmds` for every command list. -//! Exercises the new operand banks, flagged compare ops, and operand-decoded route args. -//! -//! Run: `cargo run -p wolf-decompiler --example roundtrip_check -- ` - -use std::path::PathBuf; - -use wolf_decompiler::{compile_commands_enc, decompile_commands_enc}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn main() { - let root = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("../../..")); - - let mut stack = vec![root]; - let mut ok = 0usize; - let mut mismatch = 0usize; - let mut lists = 0usize; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - continue; - } - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let ext = p.extension().and_then(|s| s.to_str()).unwrap_or(""); - let bytes = std::fs::read(&p).unwrap_or_default(); - let mut programs: Vec<(bool, Vec<_>)> = Vec::new(); - if name == "CommonEvent.dat" { - if let Ok(ce) = CommonEventsFile::read(&bytes) { - for ev in ce.events { - programs.push((ce.utf8, ev.commands)); - } - } - } else if ext.eq_ignore_ascii_case("mps") { - if let Ok(m) = Map::read(&bytes) { - for ev in m.events { - for pg in ev.pages { - programs.push((m.utf8, pg.commands)); - } - } - } - } - for (utf8, cmds) in programs { - lists += 1; - let text = decompile_commands_enc(&cmds, utf8); - match compile_commands_enc(&text, utf8) { - Ok(back) if back == cmds => ok += 1, - Ok(_) => { - mismatch += 1; - if mismatch <= 5 { - eprintln!( - "MISMATCH (structure differs) in {:?}", - p.file_name().unwrap() - ); - } - } - Err(e) => { - mismatch += 1; - if mismatch <= 5 { - eprintln!("COMPILE ERR in {:?}: {e}", p.file_name().unwrap()); - } - } - } - } - } - } - println!("command lists: {lists} | byte-exact round-trip OK: {ok} | mismatch: {mismatch}"); - if mismatch != 0 { - std::process::exit(1); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/examples/scan_banks.rs b/util/wolfdawn-master/crates/wolf-decompiler/examples/scan_banks.rs deleted file mode 100644 index ed16b99..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/examples/scan_banks.rs +++ /dev/null @@ -1,345 +0,0 @@ -//! Corpus-wide diagnostic: across every engine version, find unnamed/fallthrough renderings -//! and the raw operand "banks" actually used, so no random index goes unhandled. -//! -//! Run: `cargo run -p wolf-decompiler --example scan_banks -- ` - -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use wolf_decompiler::{decompile_common_events, decompile_map, SymbolTable}; -use wolf_formats::command::RawCommand; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn main() { - let root = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("../../..")); - - let mut files = Vec::new(); - let mut stack = vec![root]; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else { - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let ext = p.extension().and_then(|s| s.to_str()).unwrap_or(""); - if name == "CommonEvent.dat" || ext.eq_ignore_ascii_case("mps") { - files.push(p); - } - } - } - } - files.sort(); - - // token name -> (count, sample) - let mut tokens: BTreeMap<&'static str, (usize, String)> = BTreeMap::new(); - // operand bank (v/100000) -> (count, set of (cid,argidx) sites) - let mut banks: BTreeMap)> = BTreeMap::new(); - // every command cid used, and every route sub-command id used - let mut cids: BTreeMap = BTreeMap::new(); - let mut route_ids: BTreeMap = BTreeMap::new(); - let mut read_fail: BTreeMap = BTreeMap::new(); - let mut n_maps = 0usize; - let mut n_ce = 0usize; - - let sym = SymbolTable::new(); - for p in &files { - let bytes = std::fs::read(p).unwrap_or_default(); - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let ver = version_of(p); - if name == "CommonEvent.dat" { - match CommonEventsFile::read(&bytes) { - Ok(ce) => { - n_ce += 1; - let text = decompile_common_events(&ce, &sym); - scan_text(&text, p, &mut tokens); - for ev in &ce.events { - for c in &ev.commands { - scan_cmd(c, &mut banks, &mut cids, &mut route_ids); - } - } - } - Err(e) => *read_fail.entry(format!("{ver} CE: {e}")).or_default() += 1, - } - } else { - match Map::read(&bytes) { - Ok(m) => { - n_maps += 1; - let text = decompile_map(&m, &sym); - scan_text(&text, p, &mut tokens); - for ev in &m.events { - for pg in &ev.pages { - for c in &pg.commands { - scan_cmd(c, &mut banks, &mut cids, &mut route_ids); - } - } - } - } - Err(e) => *read_fail.entry(format!("{ver} MAP: {e}")).or_default() += 1, - } - } - } - - println!("== scanned: {n_maps} maps + {n_ce} common-event files ==\n"); - - // Cross-check against what the spec catalogues. - let unknown_cids: Vec<(u32, usize)> = cids - .iter() - .filter(|(cid, _)| wolf_decompiler::spec::command(**cid).is_none()) - .map(|(c, n)| (*c, *n)) - .collect(); - let unknown_routes: Vec<(u32, usize)> = route_ids - .iter() - .map(|(k, n)| (k / 1000, *n)) - .filter(|(id, _)| wolf_decompiler::spec::route_name(*id) == format!("route{id}")) - .collect(); - println!("-- UNKNOWN COMMAND CIDS (no schema -> Cmd) --"); - if unknown_cids.is_empty() { - println!(" none\n"); - } else { - for (c, n) in &unknown_cids { - println!(" cid {c:5} {n}x"); - } - println!(); - } - println!("-- UNNAMED ROUTE SUB-COMMAND IDS (-> route) --"); - if unknown_routes.is_empty() { - println!(" none\n"); - } else { - for (c, n) in &unknown_routes { - println!(" route id {c:5} {n}x"); - } - println!(); - } - println!("-- ALL ROUTE IDS USED (id #args, count, current name) --"); - for (key, n) in &route_ids { - let id = key / 1000; - let argc = key % 1000; - println!( - " id {id:3} args {argc} {n:7}x {}", - wolf_decompiler::spec::route_name(id) - ); - } - println!(); - - println!("-- READ FAILURES (silently skipped by gates) --"); - if read_fail.is_empty() { - println!(" none\n"); - } else { - for (k, v) in &read_fail { - println!(" {v:4}x {k}"); - } - println!(); - } - - println!("-- FALLTHROUGH TOKENS in rendered output --"); - if tokens.is_empty() { - println!(" none\n"); - } else { - for (tok, (cnt, sample)) in &tokens { - println!(" {cnt:6}x {tok:12} e.g. {sample}"); - } - println!(); - } - - println!("-- RAW OPERAND BANKS (every int_arg >= 1,000,000, bucketed by /100000) --"); - println!(" bank = the millions/hundred-thousands prefix; sites = distinct (cid,argIndex)"); - for (bank, (cnt, sites)) in &banks { - let label = bank_label(*bank); - let mut site_str: Vec = sites - .iter() - .map(|((cid, ai), n)| format!("cmd{cid}#{ai}x{n}")) - .collect(); - site_str.sort(); - let shown = site_str.len().min(8); - println!( - " {bank:>4} ({label:<22}) {cnt:8}x sites[{}]: {}", - site_str.len(), - site_str[..shown].join(" ") - ); - } -} - -fn scan_cmd( - c: &RawCommand, - banks: &mut BTreeMap)>, - cids: &mut BTreeMap, - route_ids: &mut BTreeMap, -) { - *cids.entry(c.cid).or_default() += 1; - // Diagnostic: dump VariableCondition (111) terms whose op code is out of the known 0..6 set. - if c.cid == 111 { - if let Some(&header) = c.int_args.first() { - let n = (header & 0x0F) as usize; - for g in 0..n { - if let Some(&op) = c.int_args.get(1 + g * 3 + 2) { - if op > 6 { - eprintln!( - "WEIRD cmd111 op={op} (0x{op:x}) header=0x{header:x} n={n} args={:?}", - c.int_args - ); - } - } - } - } - } - if let Some(mr) = &c.move_route { - for rc in &mr.commands { - // encode (id, argcount) so we can see the real shape: id*1000 + argc - *route_ids - .entry(rc.id as u32 * 1000 + rc.args.len() as u32) - .or_default() += 1; - } - } - for (i, &v) in c.int_args.iter().enumerate() { - if v >= 1_000_000 && v != 0xFFFF_FFFF && v != 0xFFFF_FFFE { - let bank = v / 100_000; - let e = banks.entry(bank).or_default(); - e.0 += 1; - *e.1.entry((c.cid, i)).or_default() += 1; - } - } -} - -fn scan_text(text: &str, p: &Path, tokens: &mut BTreeMap<&'static str, (usize, String)>) { - let checks: &[(&'static str, fn(&str) -> bool)] = &[ - ("Self*[", |l| l.contains("Self*[")), - ("@raw", |l| l.contains("@raw ")), - ("Cmd", |l| has_prefixed_num(l, "Cmd")), - ("route", |l| has_route_num(l)), - ("cmp", |l| has_cmp_num(l)), - ("strcmp", |l| has_prefixed_num(l, "strcmp")), - ("argN", |l| has_argn(l)), - ]; - for line in text.lines() { - for (tok, f) in checks { - if f(line) { - let e = tokens.entry(*tok).or_insert((0, String::new())); - e.0 += 1; - if e.1.is_empty() { - e.1 = format!( - "{} :: {}", - p.file_name().and_then(|s| s.to_str()).unwrap_or(""), - line.trim() - ); - } - } - } - } -} - -/// `prefix` immediately followed by a digit, not part of a longer alnum word before it. -fn has_prefixed_num(line: &str, prefix: &str) -> bool { - line.match_indices(prefix).any(|(i, _)| { - let before_ok = i == 0 - || !line[..i] - .chars() - .last() - .map(|c| c.is_alphanumeric()) - .unwrap_or(false); - let after = &line[i + prefix.len()..]; - before_ok - && after - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - }) -} - -/// `route` + digit, but NOT `@route(` (the Move route header) and not inside a word. -fn has_route_num(line: &str) -> bool { - line.match_indices("route").any(|(i, _)| { - let prev = if i == 0 { - None - } else { - line[..i].chars().last() - }; - let before_ok = prev - .map(|c| !c.is_alphanumeric() && c != '@') - .unwrap_or(true); - let after = &line[i + 5..]; - before_ok - && after - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - }) -} - -/// ` cmp` + digit (the VariableCondition unknown-op form), not `strcmp`. -fn has_cmp_num(line: &str) -> bool { - line.match_indices("cmp").any(|(i, _)| { - let prev = if i == 0 { - None - } else { - line[..i].chars().last() - }; - // exclude the `str` of `strcmp` - let before_ok = prev.map(|c| !c.is_alphanumeric()).unwrap_or(true); - let after = &line[i + 3..]; - before_ok - && after - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - }) -} - -fn has_argn(line: &str) -> bool { - line.match_indices("arg").any(|(i, _)| { - let prev = if i == 0 { - None - } else { - line[..i].chars().last() - }; - let before_ok = prev.map(|c| !c.is_alphanumeric()).unwrap_or(true); - let after = &line[i + 3..]; - let digit = after - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false); - before_ok - && digit - && after - .split_once('=') - .map(|(k, _)| k.chars().all(|c| c.is_ascii_digit())) - .unwrap_or(false) - }) -} - -fn bank_label(b: u32) -> &'static str { - match b { - 10 => "1.0M map-event ref", - 11 => "1.1M Self[]", - 12..=15 => "1.2-1.5M ???", - 16 => "1.6M CSelf[]", - 17..=19 => "1.7-1.9M ???", - 20..=29 => "2.x V[] normal", - 30..=39 => "3.x S[] string", - 40..=89 => "4-8M bare-lit ???", - 90..=99 => "9.x Sys[]", - _ => "10M+ bare-lit ???", - } -} - -fn version_of(p: &Path) -> String { - for comp in p.components() { - if let Some(s) = comp.as_os_str().to_str() { - if s.starts_with("WolfRPGEditor_") { - return s.to_string(); - } - } - } - "(loose)".to_string() -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/compile/cmd.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/compile/cmd.rs deleted file mode 100644 index d53168d..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/compile/cmd.rs +++ /dev/null @@ -1,743 +0,0 @@ -//! Leaf-command and block-opener reconstruction (numeric operand forms). -//! -//! Inverts the readable rendering back to exact `RawCommand`s. Commands whose pretty form is -//! fully invertible (Message, Comment, SetVariable, SetString, Wait, conditions, loops, choices) -//! are handled here. Lossy-pretty commands (Sound, Picture, Database, and so on) carry a trailing -//! `@raw(...)` annotation so the byte layer stays recoverable. The round-trip gate reports exactly -//! which commands still need it. - -use wolf_formats::command::{MoveRoute, RawCommand, RouteCommand}; -use wolf_formats::{Error, Reader, Result, WStr}; - -use crate::text::text_to_wstr; - -/// Parse the lossless `@raw i: s: t: m: v:`. -fn parse_raw(line: &str, indent: u8) -> std::result::Result { - let rest = &line["@raw ".len()..]; - let mut toks = rest.split(' '); - let cid = toks - .next() - .ok_or("@raw: missing cid")? - .parse::() - .map_err(|_| "@raw: bad cid")?; - let (mut ints, mut strs, mut term, mut route, mut v35) = - (Vec::new(), Vec::new(), 0u8, None, None); - for tok in toks { - if let Some(v) = tok.strip_prefix("i:") { - if !v.is_empty() { - ints = v - .split(',') - .map(|x| { - x.parse::() - .map(|n| n as u32) - .map_err(|_| "@raw: bad int") - }) - .collect::>()?; - } - } else if let Some(v) = tok.strip_prefix("s:") { - if !v.is_empty() { - strs = v - .split(',') - .map(|h| from_hex(h).map(WStr)) - .collect::>()?; - } - } else if let Some(v) = tok.strip_prefix("t:") { - term = v.parse().map_err(|_| "@raw: bad term")?; - } else if let Some(v) = tok.strip_prefix("m:") { - if v != "-" { - let bytes = from_hex(v)?; - route = Some(MoveRoute::read(&mut Reader::new(&bytes)).map_err(|e| e.to_string())?); - } - } else if let Some(v) = tok.strip_prefix("v:") { - if v != "-" { - v35 = Some(from_hex(v)?); - } - } - } - Ok(RawCommand { - cid, - int_args: ints, - indent, - str_args: strs, - term, - move_route: route, - v35_blob: v35, - }) -} - -/// Parse the lossless Move(201) form: `Move(ints) "strs" @route(t=…, f=…, h=…) { cmds }`. -fn parse_move(line: &str, indent: u8, utf8: bool) -> std::result::Result { - let (head, rest) = line.split_once(" @route(").ok_or("Move: missing @route")?; - let (ints, strs) = parse_move_head(head, utf8)?; - let (params, body) = rest.split_once(") {").ok_or("Move: @route missing `) {`")?; - let (term, flags, hdr) = parse_route_params(params)?; - let body = body - .trim() - .strip_suffix('}') - .ok_or("Move: route missing `}`")?; - let commands = parse_route_cmds(body.trim())?; - Ok(RawCommand { - cid: 201, - int_args: ints, - indent, - str_args: strs, - term, - move_route: Some(MoveRoute { - unknown: hdr, - flags, - commands, - }), - v35_blob: None, - }) -} - -fn parse_move_head(head: &str, utf8: bool) -> std::result::Result<(Vec, Vec), String> { - let rest = head - .trim() - .strip_prefix("Move") - .ok_or("Move: expected `Move`")? - .trim_start(); - let mut ints = Vec::new(); - let after = if let Some(r) = rest.strip_prefix('(') { - let close = r.find(')').ok_or("Move: missing `)`")?; - ints = parse_labeled_args(&r[..close])?; - r[close + 1..].trim() - } else { - rest - }; - Ok((ints, parse_trailing_strings(after, utf8)?)) -} - -fn parse_route_params(s: &str) -> std::result::Result<(u8, u8, [u8; 5]), String> { - let (mut term, mut flags, mut hdr) = (0u8, 0u8, [0u8; 5]); - for part in s.split(',') { - let part = part.trim(); - if let Some(v) = part.strip_prefix("t=") { - term = v.parse().map_err(|_| "Move: bad route t")?; - } else if let Some(v) = part.strip_prefix("f=") { - flags = v.parse().map_err(|_| "Move: bad route f")?; - } else if let Some(v) = part.strip_prefix("h=") { - let bytes = from_hex(v)?; - if bytes.len() != 5 { - return Err(format!( - "Move: route header must be 5 bytes, got {}", - bytes.len() - )); - } - hdr.copy_from_slice(&bytes); - } - } - Ok((term, flags, hdr)) -} - -fn parse_route_cmds(body: &str) -> std::result::Result, String> { - let mut out = Vec::new(); - for part in body.split(';') { - let part = part.trim(); - if part.is_empty() { - continue; - } - let (name, args) = if let Some(open) = part.find('(') { - let close = part.find(')').ok_or("route cmd: missing `)`")?; - let args = part[open + 1..close] - .split(',') - .filter(|s| !s.trim().is_empty()) - .map(|s| operand(s.trim())) - .collect::, _>>()?; - (part[..open].trim(), args) - } else { - (part, Vec::new()) - }; - let id = crate::spec::route_id_for_name(name) - .ok_or_else(|| format!("unknown route command: {name:?}"))? as u8; - out.push(RouteCommand { - id, - args, - terminator: [1, 0], - }); - } - Ok(out) -} - -/// Decode an even-length hex string. Errors on odd length or any non-hex digit rather than -/// silently dropping bytes, so a corrupted hand-edited `@raw`, `x"…"`, route, or blob fails loud. -fn from_hex(s: &str) -> std::result::Result, String> { - if s.len() % 2 != 0 { - return Err(format!("odd-length hex: {s:?}")); - } - (0..s.len() / 2) - .map(|i| { - u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| format!("bad hex: {s:?}")) - }) - .collect() -} - -fn cmd(cid: u32, ints: Vec, indent: u8, strs: Vec) -> RawCommand { - RawCommand { - cid, - int_args: ints, - indent, - str_args: strs, - term: 0, - move_route: None, - v35_blob: None, - } -} - -/// Parse one leaf command line. `utf8` selects the string encoding (Shift-JIS vs UTF-8). -pub fn parse_leaf(line: &str, indent: u8, utf8: bool) -> std::result::Result { - if line.starts_with("@raw ") { - return parse_raw(line, indent); - } - if line.contains(" @route(") { - return parse_move(line, indent, utf8); - } - if let Some(rest) = line.strip_prefix("# ") { - return Ok(cmd(103, vec![], indent, vec![wstr(rest, utf8)?])); - } - if line == "#" { - return Ok(cmd(103, vec![], indent, vec![wstr("", utf8)?])); - } - if let Some(rest) = line.strip_prefix("Message ") { - return Ok(cmd(101, vec![], indent, vec![parse_str(rest, utf8)?])); - } - if let Some(rest) = line.strip_prefix("DebugMessage ") { - return Ok(cmd(106, vec![], indent, vec![parse_str(rest, utf8)?])); - } - if let Some(rest) = line.strip_prefix("Wait ") { - return Ok(cmd( - 180, - vec![rest.trim().parse().map_err(|_| "bad Wait arg")?], - indent, - vec![], - )); - } - if let Some(rest) = line.strip_prefix("SetVariable ") { - return parse_set_variable(rest, indent); - } - if let Some(rest) = line.strip_prefix("SetString ") { - return parse_set_string(rest, indent, utf8); - } - parse_generic(line, indent, utf8) -} - -/// Inverse of the labeled fallback `Name(label=val, …) "str" …` (and `Name` or `Name "str"`). -/// Lossless for the common term==0, no-move, no-v35 case. -fn parse_generic(line: &str, indent: u8, utf8: bool) -> std::result::Result { - use crate::spec; - - // An int-arg list `(...)` only counts if it appears before any string arg. Otherwise a `(` - // inside a quoted string would be mistaken for an arg list. - let paren = line.find('('); - let quote = line.find('"'); - let has_args = match (paren, quote) { - (Some(p), Some(q)) => p < q, - (Some(_), None) => true, - _ => false, - }; - if let Some(open) = paren.filter(|_| has_args) { - let name = line[..open].trim(); - // Int-arg lists never contain nested parens, so the first `)` after `(` closes them. - let close = open + 1 + line[open + 1..].find(')').ok_or("missing `)`")?; - let cid = spec::cid_for_name(name).ok_or_else(|| format!("unknown command: {name:?}"))?; - let ints = parse_labeled_args(&line[open + 1..close])?; - let strs = parse_trailing_strings(line[close + 1..].trim(), utf8)?; - return Ok(cmd(cid, ints, indent, strs)); - } - - // `Name` or `Name "str" "str"` - let (name, after) = line.split_once(' ').unwrap_or((line, "")); - let cid = spec::cid_for_name(name).ok_or_else(|| format!("unrecognized command: {line:?}"))?; - let strs = parse_trailing_strings(after.trim(), utf8)?; - Ok(cmd(cid, vec![], indent, strs)) -} - -fn parse_labeled_args(s: &str) -> std::result::Result, String> { - let s = s.trim(); - if s.is_empty() { - return Ok(vec![]); - } - s.split(", ") - .map(|part| { - let val = part.split_once('=').map(|(_, v)| v).unwrap_or(part); - operand(val.trim()) - }) - .collect() -} - -fn parse_trailing_strings(s: &str, utf8: bool) -> std::result::Result, String> { - parse_quoted_seq(s, utf8) -} - -/// Parse a whitespace/comma-separated sequence of string literals. Each is either readable -/// `"text"` (escapes and empty strings) or byte-exact `x"HEX"` (raw bytes, NUL included). -fn parse_quoted_seq(s: &str, utf8: bool) -> std::result::Result, String> { - let mut out = Vec::new(); - let mut chars = s.chars().peekable(); - loop { - while matches!(chars.peek(), Some(' ') | Some(',')) { - chars.next(); - } - match read_one_literal(&mut chars, s, utf8)? { - Some(w) => out.push(w), - None => break, - } - } - Ok(out) -} - -/// Read a single string literal at the cursor: `x"HEX"` (raw bytes) or `"text"` (escaped, -/// re-encoded in the file's encoding plus a trailing NUL). Returns `None` at end of input. -fn read_one_literal( - chars: &mut std::iter::Peekable, - ctx: &str, - utf8: bool, -) -> std::result::Result, String> { - match chars.peek().copied() { - None => return Ok(None), - // `x"HEX"` byte-exact form. - Some('x') => { - let mut look = chars.clone(); - look.next(); // the 'x' - if look.peek() == Some(&'"') { - chars.next(); // 'x' - chars.next(); // '"' - let mut hex = String::new(); - loop { - match chars.next() { - Some('"') => break, - Some(c) => hex.push(c), - None => return Err(format!("unterminated x\"…\" literal in {ctx:?}")), - } - } - return Ok(Some(WStr(from_hex(&hex)?))); - } - return Err(format!("expected string, got 'x' in {ctx:?}")); - } - Some('"') => chars.next(), - Some(c) => return Err(format!("expected string, got {c:?} in {ctx:?}")), - }; - let mut buf = String::new(); - loop { - match chars.next() { - Some('\\') => { - let e = chars.next().ok_or("dangling escape")?; - buf.push('\\'); - buf.push(e); - } - Some('"') => break, - Some(c) => buf.push(c), - None => return Err(format!("unterminated string in {ctx:?}")), - } - } - Ok(Some(wstr(&buf, utf8)?)) -} - -fn parse_set_variable(rest: &str, indent: u8) -> std::result::Result { - // [ ]. Drop a trailing `// real` note. Split at the - // top level so annotated operands like `V[3 "Gold · ゴールド"]` stay intact. - let rest = rest.split("//").next().unwrap_or(rest).trim(); - let toks = split_top(rest); - if toks.len() != 3 && toks.len() != 5 { - return Err(format!("SetVariable arity: {rest:?}")); - } - let target = operand(&toks[0])?; - let modify = modify_nibble(&toks[1])?; - let left = operand(&toks[2])?; - let (binop, right) = if toks.len() == 5 { - (binop_nibble(&toks[3])?, operand(&toks[4])?) - } else { - (0, 0) - }; - let arg3 = (modify << 8) | (binop << 12); - Ok(cmd(121, vec![target, left, right, arg3], indent, vec![])) -} - -fn parse_set_string(rest: &str, indent: u8, utf8: bool) -> std::result::Result { - // = "text" - let (target, val) = rest.split_once(" = ").ok_or("SetString missing `=`")?; - let target = operand(target.trim())?; - Ok(cmd( - 122, - vec![target, 0], - indent, - vec![parse_str(val.trim(), utf8)?], - )) -} - -/// StringCondition lhs flag. The rhs is an operand (string-var vs string-var) from the rhs -/// section rather than a literal in `str_args`. Mirrors the renderer's `STRCOND_OPERAND_FLAG`. -const STRCOND_OPERAND_FLAG: u32 = 0x0100_0000; - -/// Strip a trailing ` @b:HEX` blob suffix carried by v3.5 block openers with a non-empty -/// per-command blob, returning the cleaned line and the decoded blob. -pub fn split_blob_suffix(line: &str) -> (&str, Option>) { - if let Some(idx) = line.rfind(" @b:") { - let h = &line[idx + 4..]; - if let Ok(bytes) = from_hex(h) { - if !bytes.is_empty() { - return (&line[..idx], Some(bytes)); - } - } - } - (line, None) -} - -/// Split a StringCondition group on its comparison operator into `(left, type, right)`. Types: -/// `eq`=0, `ne`=1, `contains`=2, `!contains`=3, `strcmp`=N. Picks the earliest operator so a -/// token inside a right-hand string literal, such as `A eq "x contains y"`, cannot hijack the -/// split. -fn split_strcmp(group: &str) -> Option<(&str, u32, &str)> { - let mut best: Option<(usize, usize, u32)> = None; // (pos, token_len, type) - for (tok, ty) in [ - (" eq ", 0u32), - (" ne ", 1), - (" contains ", 2), - (" !contains ", 3), - ] { - if let Some(pos) = group.find(tok) { - if best.map_or(true, |(bp, _, _)| pos < bp) { - best = Some((pos, tok.len(), ty)); - } - } - } - if let Some((pos, len, ty)) = best { - return Some((&group[..pos], ty, &group[pos + len..])); - } - // strcmp fallback for unknown comparison types. - let pos = group.find(" strcmp")?; - let after = &group[pos + " strcmp".len()..]; - let sp = after.find(' ')?; - let ty = after[..sp].parse::().ok()?; - Some((&group[..pos], ty, &after[sp + 1..])) -} - -/// Build a VariableCondition (111) or StringCondition (112) from the readable `when` conditions. -/// A string comparison operator (`eq`, `ne`, `contains`, `!contains`, `strcmp`) selects 112. -/// Its layout is `[header][lhs×n][rhs×k]` where each lhs packs the operand (low 24 bits), the -/// variable-right flag (bit 24), and the comparison type (high nibble). -pub fn build_condition( - conds: &[&str], - is_and: bool, - indent: u8, - blob: Option>, - utf8: bool, -) -> Result { - // AND mode renders as a single `when` joined by ` && `. OR mode is one `when` per group. - let groups: Vec<&str> = if is_and { - conds - .first() - .map(|c| c.split(" && ").collect()) - .unwrap_or_default() - } else { - conds.to_vec() - }; - let n = groups.len() as u32; - let header = n | if is_and { 0x10 } else { 0 }; - - let is_string = groups.iter().any(|g| split_strcmp(g).is_some()); - let mut c = if is_string { - let mut lhs = Vec::new(); - let mut rhs = Vec::new(); - let mut strs: Vec = vec![WStr(vec![0]); 4]; // group-indexed literal slots - for (g, group) in groups.iter().enumerate() { - let (l, ty, r) = split_strcmp(group) - .ok_or_else(|| Error::invalid(format!("bad string condition: {group:?}")))?; - let l_op = operand(l.trim()).map_err(Error::invalid)?; - let cmp_bits = ty << 28; // comparison type in the high nibble - let r = r.trim(); - if r.starts_with('"') || r.starts_with("x\"") { - lhs.push(l_op | cmp_bits); - // The engine stores exactly 4 group-indexed literal slots. Guard the index so a - // malformed condition with more than 4 string groups errors instead of panicking. - if g >= strs.len() { - return Err(Error::invalid(format!( - "string condition has too many string groups ({})", - groups.len() - ))); - } - strs[g] = parse_str(r, utf8).map_err(Error::invalid)?; - } else { - lhs.push(l_op | STRCOND_OPERAND_FLAG | cmp_bits); - rhs.push(operand(r).map_err(Error::invalid)?); - } - } - let mut ints = vec![header]; - ints.extend(lhs); - ints.extend(rhs); - cmd(112, ints, indent, strs) - } else { - let mut args = vec![header]; - for g in groups { - let (left, right, op) = parse_compare(g).map_err(Error::invalid)?; - args.push(left); - args.push(right); - args.push(op); - } - cmd(111, args, indent, vec![]) - }; - c.v35_blob = blob; - Ok(c) -} - -pub fn build_loop(line: &str, indent: u8) -> Result { - let (line, blob) = split_blob_suffix(line); - let mut c = if line == "loop {" { - cmd(170, vec![], indent, vec![]) - } else if let Some(inner) = line - .strip_prefix("loop ") - .and_then(|s| s.strip_suffix(" times {")) - { - let n = operand(inner.trim()).map_err(Error::invalid)?; - cmd(179, vec![n], indent, vec![]) - } else { - return Err(Error::invalid(format!("bad loop opener: {line:?}"))); - }; - c.v35_blob = blob; - Ok(c) -} - -pub fn build_choices(line: &str, indent: u8, utf8: bool) -> Result { - // choose ["a", "b"] { - let (line, blob) = split_blob_suffix(line); - let rest = line - .strip_prefix("choose ") - .and_then(|s| s.strip_suffix(" {")) - .ok_or_else(|| Error::invalid(format!("bad choose opener: {line:?}")))?; - let (arg0_s, opts) = rest - .split_once(" [") - .ok_or_else(|| Error::invalid(format!("bad choose opener: {line:?}")))?; - let arg0 = arg0_s - .trim() - .parse::() - .map_err(|_| Error::invalid(format!("bad choose arg0: {arg0_s:?}")))?; - let inner = opts - .strip_suffix(']') - .ok_or_else(|| Error::invalid(format!("bad choose options: {opts:?}")))?; - let strs = parse_str_list(inner, utf8).map_err(Error::invalid)?; - let mut c = cmd(102, vec![arg0], indent, strs); - c.v35_blob = blob; - Ok(c) -} - -// --- operand / operator parsing ------------------------------------------------------- - -fn operand(s: &str) -> std::result::Result { - let s = s.trim(); - // `Self*[n]` must be tried before `Self[n]` because of the prefix overlap. - if let Some(n) = bracketed(s, "Self*") { - return Ok(1_000_000 + n); - } - if let Some(n) = bracketed(s, "Self") { - return Ok(1_100_000 + n); - } - if let Some(n) = bracketed(s, "CSelf") { - return Ok(1_600_000 + n); - } - if let Some(n) = bracketed(s, "V") { - return Ok(2_000_000 + n); - } - if let Some(n) = bracketed(s, "S") { - return Ok(3_000_000 + n); - } - if let Some(n) = bracketed(s, "Sys") { - return Ok(9_000_000 + n); - } - if let Some(n) = bracketed(s, "Rand") { - return Ok(8_000_000 + n); - } - // On-map character data: `Chara[m].Field` (9.1M) or `Chara2[m].Field` (9.18M). - if let Some(v) = parse_chara(s, "Chara2", 9_180_000) { - return Ok(v); - } - if let Some(v) = parse_chara(s, "Chara", 9_100_000) { - return Ok(v); - } - // This-event intrinsic data: `ThisEv.Field` or `ThisEv[k]`. - if let Some(v) = parse_this_event(s) { - return Ok(v); - } - // Bare literal, optionally followed by an edit-form ` "label"` annotation. The leading - // numeric token is the authority. - let head = s.split_once(" \"").map(|(h, _)| h).unwrap_or(s); - head.trim() - .parse::() - .map(|v| v as u32) - .map_err(|_| format!("bad operand: {s:?}")) -} - -/// Split a line on whitespace at the top level only. Whitespace inside `[]`/`()` brackets or -/// `"…"` quotes is kept, so an annotated operand like `V[3 "Gold · ゴールド"]` stays one token. -pub(crate) fn split_top(s: &str) -> Vec { - let mut out = Vec::new(); - let mut cur = String::new(); - let mut depth = 0i32; - let mut in_str = false; - for ch in s.chars() { - match ch { - '"' => { - in_str = !in_str; - cur.push(ch); - } - '[' | '(' if !in_str => { - depth += 1; - cur.push(ch); - } - ']' | ')' if !in_str => { - depth -= 1; - cur.push(ch); - } - c if c.is_whitespace() && !in_str && depth == 0 => { - if !cur.is_empty() { - out.push(std::mem::take(&mut cur)); - } - } - c => cur.push(c), - } - } - if !cur.is_empty() { - out.push(cur); - } - out -} - -/// Parse a character-data reference `prefix[member].Field` back to its operand id. -fn parse_chara(s: &str, prefix: &str, base: u32) -> Option { - let rest = s.strip_prefix(prefix)?.strip_prefix('[')?; - let (member_s, tail) = rest.split_once(']')?; - let member: u32 = member_s.parse().ok()?; - let field = tail.strip_prefix('.')?; - let f = crate::wolfscript::ops::chara_field_index(field) - .or_else(|| field.strip_prefix('f').and_then(|d| d.parse().ok()))?; - Some(base + member * 10 + f) -} - -/// Parse a this-event reference `ThisEv.Field` or `ThisEv[k]` back to its operand id. -fn parse_this_event(s: &str) -> Option { - let rest = s.strip_prefix("ThisEv")?; - if let Some(field) = rest.strip_prefix('.') { - return crate::wolfscript::ops::chara_field_index(field).map(|f| 1_000_000 + f); - } - let inner = rest.strip_prefix('[')?.strip_suffix(']')?; - inner.parse::().ok().map(|k| 1_000_000 + k) -} - -fn bracketed(s: &str, sigil: &str) -> Option { - let inner = s - .strip_prefix(sigil)? - .strip_prefix('[')? - .strip_suffix(']')?; - // The edit form anchors on the index and may carry a ` "label"` annotation inside the - // brackets, as in `V[3 "Gold · ゴールド"]`. Take the leading numeric token, ignore the rest. - let head = inner.split_once(' ').map(|(h, _)| h).unwrap_or(inner); - head.trim().parse().ok() -} - -fn parse_compare(s: &str) -> std::result::Result<(u32, u32, u32), String> { - // Longest symbols first so `<=` and `>=` win over `<` and `>`. Each symbol can carry a - // `~` high-nibble suffix, the per-term flag, so `==~1` becomes op 0x12. - for (sym, code) in [ - ("<=", 0u32), - (">=", 1), - ("==", 2), - ("!=", 3), - ("<", 4), - (">", 5), - ("&", 6), - ] { - // Flagged form: ` sym~ `. - if let Some((l, rest)) = s.split_once(&format!(" {sym}~")) { - if let Some((flags_s, r)) = rest.split_once(' ') { - if let Ok(flags) = flags_s.parse::() { - return Ok((operand(l.trim())?, operand(r.trim())?, code | (flags << 4))); - } - } - } - if let Some((l, r)) = s.split_once(&format!(" {sym} ")) { - return Ok((operand(l.trim())?, operand(r.trim())?, code)); - } - } - // Unknown compare op rendered as ` cmp ` (reversible for any byte value). - if let Some((l, rest)) = s.split_once(" cmp") { - if let Some((num, r)) = rest.split_once(' ') { - if let Ok(op) = num.parse::() { - return Ok((operand(l.trim())?, operand(r.trim())?, op)); - } - } - } - Err(format!("bad condition: {s:?}")) -} - -fn modify_nibble(s: &str) -> std::result::Result { - Ok(match s { - "=" => 0, - "+=" => 1, - "-=" => 2, - "*=" => 3, - "/=" => 4, - "%=" => 5, - _ => return Err(format!("bad modify op: {s:?}")), - }) -} - -fn binop_nibble(s: &str) -> std::result::Result { - Ok(match s { - "+" => 0, - "-" => 1, - "*" => 2, - "%" => 3, - "~" => 4, - "&" => 6, - "|" => 7, - "^" => 8, - "<<" => 9, - _ => return Err(format!("bad binop: {s:?}")), - }) -} - -// --- string parsing ------------------------------------------------------------------- - -/// Encode (already-quoted-stripped) literal text into a Wolf string in the file's encoding. -fn wstr(s: &str, utf8: bool) -> std::result::Result { - text_to_wstr(&unescape(s), utf8) -} - -/// Parse exactly one string literal (`"text"` or `x"HEX"`). -fn parse_str(s: &str, utf8: bool) -> std::result::Result { - let s = s.trim(); - let mut chars = s.chars().peekable(); - let w = read_one_literal(&mut chars, s, utf8)? - .ok_or_else(|| format!("expected string literal: {s:?}"))?; - if chars.next().is_some() { - return Err(format!("trailing content after string literal: {s:?}")); - } - Ok(w) -} - -fn parse_str_list(s: &str, utf8: bool) -> std::result::Result, String> { - parse_quoted_seq(s, utf8) -} - -fn unescape(s: &str) -> String { - let mut out = String::new(); - let mut chars = s.chars(); - while let Some(c) = chars.next() { - if c != '\\' { - out.push(c); - continue; - } - match chars.next() { - Some('n') => out.push('\n'), - Some('r') => out.push('\r'), - Some('t') => out.push('\t'), - Some('"') => out.push('"'), - Some('\\') => out.push('\\'), - Some(other) => { - out.push('\\'); - out.push(other); - } - None => out.push('\\'), - } - } - out -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/compile/lex.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/compile/lex.rs deleted file mode 100644 index f5f38a6..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/compile/lex.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Line lexer: trimmed, non-empty lines with their source line numbers. The decompiler indents -//! by depth, but the parser reconstructs `indent` from block nesting, so leading whitespace is -//! discarded here. - -pub struct Line { - pub no: u32, - pub text: String, -} - -impl std::fmt::Debug for Line { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{:?}", self.no, self.text) - } -} - -pub fn lines(text: &str) -> Vec { - text.lines() - .enumerate() - .filter_map(|(i, raw)| { - // Trim only the leading whitespace (the indent). Trailing spaces are significant - // inside, for example, comment text. - let t = raw.trim_start(); - if t.is_empty() { - None - } else { - Some(Line { - no: (i + 1) as u32, - text: t.to_string(), - }) - } - }) - .collect() -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/compile/mod.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/compile/mod.rs deleted file mode 100644 index 159f5ad..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/compile/mod.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! The recompiler: parse WolfScript text back into the byte-faithful `RawCommand` stream. -//! -//! This is the inverse of [`crate::wolfscript`]. It reconstructs exactly what the decompiler -//! dropped for readability: -//! * Marker commands. `branch`/`when`/`else`/`}` become VariableCondition/ChoiceCase/ -//! ElseCase/BranchEnd. `loop`/`}` become StartLoop/LoopEnd. `choose`/`case`/`}` become -//! Choices/ChoiceCase/CancelCase/BranchEnd. -//! * Indent bytes, reconstructed from block nesting depth (Wolf's indent equals depth). -//! * Suppressed `Blank` slots. The editor keeps one trailing empty command at the end of every -//! block body, re-inserted on compile. -//! -//! Correctness is driven by the round-trip gate `parse(decompile(cmds)) == cmds`. Operands are -//! parsed in their numeric form here (`Self[n]`, `V[n]`, `CSelf[n]`, literal). Name resolution -//! (the reverse glossary/symbol lookup) layers on top. - -pub(crate) mod cmd; -mod lex; - -use wolf_formats::command::RawCommand; -use wolf_formats::{Error, Result}; - -use lex::Line; - -/// Parse a WolfScript command program (numeric operand forms) into a `RawCommand` stream. -/// Strings are encoded as UTF-8. Use [`compile_commands_enc`] for Shift-JIS files. -pub fn compile_commands(text: &str) -> Result> { - compile_commands_enc(text, true) -} - -/// Parse a WolfScript command program, encoding string literals in the file's encoding (`utf8`). -/// The exact inverse of [`crate::wolfscript::decompile_commands_enc`]. -pub fn compile_commands_enc(text: &str, utf8: bool) -> Result> { - let lines = lex::lines(text); - - // A leading `@v35` directive marks a Wolf Pro v3.5 program: every command carries a trailing - // blob, empty unless an `@raw v:` form supplies one. Strip the directive, parse normally, then - // default every command's blob to empty. - let v35 = lines.first().map(|l| l.text == "@v35").unwrap_or(false); - let body = if v35 { &lines[1..] } else { &lines[..] }; - - let mut p = Parser { - lines: body, - pos: 0, - utf8, - }; - let mut out = Vec::new(); - p.block_body(0, &mut out)?; - if p.pos != p.lines.len() { - return Err(Error::invalid(format!( - "trailing content at line {}: {:?}", - p.lines[p.pos].no, p.lines[p.pos].text - ))); - } - - if v35 { - for c in &mut out { - if c.v35_blob.is_none() { - c.v35_blob = Some(Vec::new()); - } - } - } - Ok(out) -} - -struct Parser<'a> { - lines: &'a [Line], - pos: usize, - utf8: bool, -} - -impl<'a> Parser<'a> { - fn peek(&self) -> Option<&'a Line> { - self.lines.get(self.pos) - } - - /// Parse the statements of one block body at `depth`, appending commands plus a trailing - /// Blank to `out`. Stops at a line that closes or continues the parent block (`}`, `} when`, - /// `} else`, `} case`, `} cancel`) without consuming it. - fn block_body(&mut self, depth: u8, out: &mut Vec) -> Result<()> { - loop { - let Some(line) = self.peek() else { break }; - let t = line.text.as_str(); - if t == "}" || t.starts_with("} ") { - break; - } - self.statement(depth, out)?; - } - // Wolf keeps one empty trailing command at the end of every block body. - out.push(blank(depth)); - Ok(()) - } - - fn statement(&mut self, depth: u8, out: &mut Vec) -> Result<()> { - let line = self.peek().unwrap(); - let t = line.text.clone(); - let no = line.no; - - // A block opener may carry an inline ` @b:HEX` blob suffix (rare, v3.5 only). - let (head, opener_blob) = cmd::split_blob_suffix(&t); - if head == "branch {" { - self.pos += 1; - return self.conditional(depth, out, no, false, opener_blob); - } - if head == "branch all {" { - self.pos += 1; - return self.conditional(depth, out, no, true, opener_blob); - } - if head == "loop {" || head.starts_with("loop ") && head.ends_with("times {") { - return self.loop_block(depth, out); - } - if head.starts_with("choose ") && head.ends_with('{') { - return self.choose_block(depth, out); - } - - // A leaf command. - let cmd = cmd::parse_leaf(&t, depth, self.utf8) - .map_err(|e| Error::invalid(format!("line {no}: {e}")))?; - self.pos += 1; - out.push(cmd); - Ok(()) - } - - /// `branch { } when (cond) { … } else { … }` becomes a VariableCondition/StringCondition - /// opener plus ChoiceCase/ElseCase markers plus BranchEnd. - fn conditional( - &mut self, - depth: u8, - out: &mut Vec, - open_no: u32, - is_and: bool, - blob: Option>, - ) -> Result<()> { - let mut whens: Vec<(String, Vec)> = Vec::new(); - let mut else_body: Option> = None; - - loop { - let Some(line) = self.peek() else { - return Err(Error::invalid(format!( - "line {open_no}: unterminated branch" - ))); - }; - let t = line.text.clone(); - if t == "}" { - self.pos += 1; - break; - } else if let Some(rest) = t - .strip_prefix("} when (") - .and_then(|s| s.strip_suffix(") {")) - { - self.pos += 1; - let mut body = Vec::new(); - self.block_body(depth + 1, &mut body)?; - whens.push((rest.to_string(), body)); - } else if t == "} else {" { - self.pos += 1; - let mut body = Vec::new(); - self.block_body(depth + 1, &mut body)?; - else_body = Some(body); - } else { - return Err(Error::invalid(format!( - "line {}: unexpected in branch: {t:?}", - line.no - ))); - } - } - - let opener = cmd::build_condition( - &whens.iter().map(|(c, _)| c.as_str()).collect::>(), - is_and, - depth, - blob, - self.utf8, - )?; - out.push(opener); - for (i, (_, body)) in whens.into_iter().enumerate() { - out.push(choice_case((i + 1) as u32, depth)); - out.extend(body); - } - if let Some(body) = else_body { - out.push(cmd_with(420, vec![0], depth)); // ElseCase (arg0 = 0) - out.extend(body); - } - out.push(marker(499, depth)); // BranchEnd - Ok(()) - } - - fn loop_block(&mut self, depth: u8, out: &mut Vec) -> Result<()> { - let line = self.peek().unwrap().text.clone(); - self.pos += 1; - let opener = cmd::build_loop(&line, depth)?; - out.push(opener); - let mut body = Vec::new(); - self.block_body(depth + 1, &mut body)?; - out.extend(body); - // consume the closing `}` - self.expect_close()?; - out.push(marker(498, depth)); // LoopEnd - Ok(()) - } - - fn choose_block(&mut self, depth: u8, out: &mut Vec) -> Result<()> { - let line = self.peek().unwrap().text.clone(); - self.pos += 1; - let opener = cmd::build_choices(&line, depth, self.utf8)?; - out.push(opener); - - loop { - let Some(l) = self.peek() else { - return Err(Error::invalid("unterminated choose".to_string())); - }; - let t = l.text.clone(); - if t == "}" { - self.pos += 1; - break; - } - // `} case {`, the special `} case* {`, or `} cancel {`. - let (cid, arg0) = if let Some(a) = t.strip_prefix("} case* ") { - (402, parse_case_arg(a)?) - } else if let Some(a) = t.strip_prefix("} case ") { - (401, parse_case_arg(a)?) - } else if t == "} cancel {" { - (421, 0) - } else { - return Err(Error::invalid(format!("unexpected in choose: {t:?}"))); - }; - self.pos += 1; - out.push(cmd_with(cid, vec![arg0], depth)); - let mut body = Vec::new(); - self.block_body(depth + 1, &mut body)?; - out.extend(body); - } - out.push(marker(499, depth)); // BranchEnd - Ok(()) - } - - fn expect_close(&mut self) -> Result<()> { - match self.peek() { - Some(l) if l.text == "}" => { - self.pos += 1; - Ok(()) - } - other => Err(Error::invalid(format!("expected `}}`, got {other:?}"))), - } - } -} - -fn blank(indent: u8) -> RawCommand { - RawCommand { - cid: 0, - int_args: Vec::new(), - indent, - str_args: Vec::new(), - term: 0, - move_route: None, - v35_blob: None, - } -} - -fn marker(cid: u32, indent: u8) -> RawCommand { - RawCommand { - cid, - int_args: Vec::new(), - indent, - str_args: Vec::new(), - term: 0, - move_route: None, - v35_blob: None, - } -} - -fn cmd_with(cid: u32, ints: Vec, indent: u8) -> RawCommand { - RawCommand { - cid, - int_args: ints, - indent, - str_args: Vec::new(), - term: 0, - move_route: None, - v35_blob: None, - } -} - -/// Extract the leading case index from ` {` or ` { # label`. -fn parse_case_arg(a: &str) -> Result { - a.split_whitespace() - .next() - .and_then(|s| s.parse().ok()) - .ok_or_else(|| Error::invalid(format!("bad case arg: {a:?}"))) -} - -fn choice_case(idx: u32, indent: u8) -> RawCommand { - RawCommand { - cid: 401, - int_args: vec![idx], - indent, - str_args: Vec::new(), - term: 0, - move_route: None, - v35_blob: None, - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/db_edit.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/db_edit.rs deleted file mode 100644 index 95244ae..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/db_edit.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Apply edits from a [`database_to_json`](crate::database_to_json) export back onto a -//! [`Database`], byte-exact. Only values (cell ints and strings) and row names are editable. The -//! schema (types, fields, kinds, counts, structure) is fixed and any change to it is rejected. -//! Unchanged cells are never re-encoded, so untouched rows re-serialize byte-identical via -//! [`Database::write`](wolf_formats::database::Database::write). Only the cells you actually -//! changed differ. - -use std::collections::HashMap; - -use serde_json::Value; -use wolf_formats::database::Database; - -use crate::json::value_slots; -use crate::text::{decode_wstr, text_to_wstr}; - -/// Apply the edited JSON onto `db` in place. Returns the number of changed cells and names. -/// Errors on any schema or structure mismatch, an out-of-range id, an ambiguous field name, or a -/// value that cannot be encoded in the database's text encoding. On error, any edits committed -/// before the failure point remain applied. -pub fn apply_database_edit(json: &str, db: &mut Database) -> Result { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let utf8 = db.utf8; - let types = root - .get("types") - .and_then(Value::as_array) - .ok_or("edited JSON has no `types` array")?; - - let type_count = db.types.len(); - let mut changed = 0usize; - for tj in types { - let ti = json_id(tj, "type")?; - let dt = db - .types - .get_mut(ti) - .ok_or_else(|| format!("type id {ti} is out of range (base has {type_count} types)"))?; - - // Schema-integrity guard, only when the doc carries a `fields` array (a full export). - // Edits are patch-style: a `rows` array may include just the rows you touched. Others are - // left as-is, never deleted, and an out-of-range row id is rejected below. - if let Some(fields) = tj.get("fields").and_then(Value::as_array) { - if fields.len() != dt.fields.len() { - return Err(format!( - "type {ti}: field count changed ({} edited vs {} base) - schema edits are not allowed", - fields.len(), - dt.fields.len() - )); - } - } - - // Map each value key to (is_string, value-slot index), derived identically to the JSON - // export (unique field name, else `#`), so the keys always match. - let fields_size = (dt.dat_fields_size as usize).min(dt.fields.len()); - let by_key: HashMap = value_slots(dt, fields_size, utf8) - .into_iter() - .map(|(k, is_str, slot)| (k, (is_str, slot))) - .collect(); - - let Some(rows) = tj.get("rows").and_then(Value::as_array) else { - continue; - }; - for rj in rows { - let ri = json_id(rj, "row")?; - let row = dt - .data - .get_mut(ri) - .ok_or_else(|| format!("type {ti}: row id {ri} is out of range"))?; - - // Row display name lives in the .project half. - if let Some(nm) = rj.get("name").and_then(Value::as_str) { - if decode_wstr(&row.name, utf8) != nm { - row.name = text_to_wstr(nm, utf8) - .map_err(|e| format!("type {ti} row {ri} name: {e}"))?; - changed += 1; - } - } - - let Some(values) = rj.get("values").and_then(Value::as_object) else { - continue; - }; - for (key, val) in values { - let &(is_str, slot) = by_key.get(key).ok_or_else(|| { - format!("type {ti} row {ri}: unknown/schema-only field {key:?}") - })?; - if is_str { - let new = val.as_str().ok_or_else(|| { - format!("type {ti} row {ri} field {key:?}: expected a string") - })?; - let cur = row - .string_values - .get(slot) - .map(|w| decode_wstr(w, utf8)) - .unwrap_or_default(); - // Skip unchanged cells to preserve the original bytes exactly. This avoids the - // empty-string and trailing-NUL re-encode ambiguity. - if cur != new { - let w = text_to_wstr(new, utf8) - .map_err(|e| format!("type {ti} row {ri} field {key:?}: {e}"))?; - *row.string_values.get_mut(slot).ok_or_else(|| { - format!("type {ti} row {ri} field {key:?}: value slot missing") - })? = w; - changed += 1; - } - } else { - let new = val.as_i64().ok_or_else(|| { - format!("type {ti} row {ri} field {key:?}: expected a number") - })?; - let nv = new as u32; - let cell = row.int_values.get_mut(slot).ok_or_else(|| { - format!("type {ti} row {ri} field {key:?}: value slot missing") - })?; - if *cell != nv { - *cell = nv; - changed += 1; - } - } - } - } - } - Ok(changed) -} - -fn json_id(v: &Value, what: &str) -> Result { - v.get("id") - .and_then(Value::as_u64) - .map(|n| n as usize) - .ok_or_else(|| format!("{what} entry is missing its numeric `id`")) -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/document.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/document.rs deleted file mode 100644 index 33c3882..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/document.rs +++ /dev/null @@ -1,391 +0,0 @@ -//! Whole-file recompile: the document envelope around per-event/page command bodies. -//! -//! [`crate::wolfscript`] renders one command list and [`crate::compile`] parses it back. -//! This layer wraps those so an entire file round-trips. Each event (and map page) is emitted -//! under an identity header (`@commonevent `, or `@event ` plus `@page `), its body -//! produced by the gated numeric renderer [`decompile_commands`]. That renderer is `@raw`-free on -//! the corpus, so the document stays readable yet fully recompilable. -//! -//! Recompile is patch-style. The original file supplies every opaque metadata field (event names, -//! page graphics and conditions, the CommonEvent `unknown1..12` tail). Only each command body is -//! swapped back in, keyed by identity. Files whose code is untouched re-emit byte-for-byte. Edited -//! bodies recompile in place. This sidesteps round-tripping the large metadata through text and is -//! the real edit-to-play mod loop. - -use std::collections::BTreeMap; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; -use wolf_formats::{Error, Result}; - -use crate::compile::compile_commands_enc; -use crate::symbols::SymbolTable; -use crate::text::decode_wstr; -use crate::wolfscript::{decompile_commands_annotated, decompile_commands_enc}; - -const CE_HEADER: &str = "# wolf-edit common-events"; -const MAP_HEADER: &str = "# wolf-edit map"; - -// ---------------------------------------------------------------------------- -// CommonEvents -// ---------------------------------------------------------------------------- - -/// Render a CommonEvent file as a recompilable edit document: one `@commonevent ... @end` -/// block per event, body in the gated numeric form. Metadata is not emitted. It is taken from the -/// base file at compile time. -pub fn decompile_common_events_edit(ce: &CommonEventsFile) -> String { - let mut out = format!("{CE_HEADER} (count={})\n\n", ce.events.len()); - for ev in &ce.events { - let name = decode_wstr(&ev.name, ce.utf8); - out.push_str(&format!("@commonevent {}", ev.int_id)); - if !name.is_empty() { - out.push_str(&format!(" # {}", name.replace('\n', " "))); - } - out.push('\n'); - let body = decompile_commands_enc(&ev.commands, ce.utf8); - out.push_str(&body); - if !body.ends_with('\n') { - out.push('\n'); - } - out.push_str("@end\n\n"); - } - out -} - -/// Like [`decompile_common_events_edit`] but in annotate mode. Each command body uses the unified -/// read-plus-edit form (index-anchored bilingual names, `V[3 "Gold · ゴールド"]`), resolved via -/// `symbols`. Recompiles byte-exact through the same [`compile_common_events_edit`], since the -/// parser strips the labels. Names are read-only decoration the recompiler ignores. -pub fn decompile_common_events_edit_annotated( - ce: &CommonEventsFile, - symbols: &SymbolTable, -) -> String { - let mut out = format!("{CE_HEADER} (count={})\n\n", ce.events.len()); - for ev in &ce.events { - let name = decode_wstr(&ev.name, ce.utf8); - out.push_str(&format!("@commonevent {}", ev.int_id)); - if !name.is_empty() { - out.push_str(&format!(" # {}", name.replace('\n', " "))); - } - out.push('\n'); - let body = decompile_commands_annotated(&ev.commands, ce.utf8, symbols, Some(ev.int_id)); - out.push_str(&body); - if !body.ends_with('\n') { - out.push('\n'); - } - out.push_str("@end\n\n"); - } - out -} - -/// Parse an edit document and substitute each event's command body back into `base`, keyed by -/// `int_id`. Events absent from the document keep their original commands, so partial patches are -/// allowed. An `@commonevent` id with no match in `base` is an error. -pub fn compile_common_events_edit(text: &str, base: &mut CommonEventsFile) -> Result<()> { - let blocks = parse_blocks(text, "commonevent")?; - let mut by_id: BTreeMap = BTreeMap::new(); - for (i, ev) in base.events.iter().enumerate() { - if by_id.insert(ev.int_id, i).is_some() { - return Err(Error::invalid(format!( - "base has duplicate common-event id {}; cannot patch by id unambiguously", - ev.int_id - ))); - } - } - for blk in blocks { - let &idx = by_id.get(&blk.id).ok_or_else(|| { - Error::invalid(format!( - "@commonevent {} (line {}) has no match in the base file", - blk.id, blk.line - )) - })?; - let cmds = compile_commands_enc(&blk.body, base.utf8) - .map_err(|e| Error::invalid(format!("@commonevent {}: {e}", blk.id)))?; - base.events[idx].commands = cmds; - } - Ok(()) -} - -// ---------------------------------------------------------------------------- -// Maps -// ---------------------------------------------------------------------------- - -/// Render a Map as a recompilable edit document: `@event ` then one `@page ... @endpage` -/// per page, each body in the gated numeric form. Coordinates and name are a comment for the -/// reader. Truth comes from the base file at compile time. -pub fn decompile_map_edit(map: &Map) -> String { - let mut out = format!( - "{MAP_HEADER} (version=0x{:x} {}x{} events={})\n\n", - map.version, - map.width, - map.height, - map.events.len() - ); - for ev in &map.events { - let name = decode_wstr(&ev.name, map.utf8); - out.push_str(&format!("@event {}", ev.id)); - if !name.is_empty() { - out.push_str(&format!( - " # {} @({},{})", - name.replace('\n', " "), - ev.x, - ev.y - )); - } - out.push('\n'); - for (pi, page) in ev.pages.iter().enumerate() { - out.push_str(&format!("@page {pi}\n")); - let body = decompile_commands_enc(&page.commands, map.utf8); - out.push_str(&body); - if !body.ends_with('\n') { - out.push('\n'); - } - out.push_str("@endpage\n"); - } - out.push_str("@endevent\n\n"); - } - out -} - -/// Like [`decompile_map_edit`] but in annotate mode (index-anchored bilingual names), resolved -/// via `symbols`. Recompiles byte-exact through [`compile_map_edit`]. -pub fn decompile_map_edit_annotated(map: &Map, symbols: &SymbolTable) -> String { - let mut out = format!( - "{MAP_HEADER} (version=0x{:x} {}x{} events={})\n\n", - map.version, - map.width, - map.height, - map.events.len() - ); - for ev in &map.events { - let name = decode_wstr(&ev.name, map.utf8); - out.push_str(&format!("@event {}", ev.id)); - if !name.is_empty() { - out.push_str(&format!( - " # {} @({},{})", - name.replace('\n', " "), - ev.x, - ev.y - )); - } - out.push('\n'); - for (pi, page) in ev.pages.iter().enumerate() { - out.push_str(&format!("@page {pi}\n")); - let body = decompile_commands_annotated(&page.commands, map.utf8, symbols, None); - out.push_str(&body); - if !body.ends_with('\n') { - out.push('\n'); - } - out.push_str("@endpage\n"); - } - out.push_str("@endevent\n\n"); - } - out -} - -/// Parse a map edit document and substitute each page's command body back into `base`, keyed by -/// event `id` plus page index. Pages absent from the document keep their commands. -pub fn compile_map_edit(text: &str, base: &mut Map) -> Result<()> { - let events = parse_map_blocks(text)?; - let mut by_id: BTreeMap = BTreeMap::new(); - for (i, ev) in base.events.iter().enumerate() { - if by_id.insert(ev.id, i).is_some() { - return Err(Error::invalid(format!( - "base map has duplicate event id {}; cannot patch by id unambiguously", - ev.id - ))); - } - } - for ev in events { - let &idx = by_id.get(&ev.id).ok_or_else(|| { - Error::invalid(format!( - "@event {} (line {}) has no match in the base map", - ev.id, ev.line - )) - })?; - for page in ev.pages { - let page_count = base.events[idx].pages.len(); - let target = base.events[idx].pages.get_mut(page.index).ok_or_else(|| { - Error::invalid(format!( - "@event {} @page {} out of range (map event has {page_count} pages)", - ev.id, page.index, - )) - })?; - let cmds = compile_commands_enc(&page.body, base.utf8).map_err(|e| { - Error::invalid(format!("@event {} @page {}: {e}", ev.id, page.index)) - })?; - target.commands = cmds; - } - } - Ok(()) -} - -// ---------------------------------------------------------------------------- -// Document parsing (delimiter-based, so command-body braces never confuse it) -// ---------------------------------------------------------------------------- - -struct Block { - id: u32, - line: u32, - body: String, -} - -/// Parse `@ ...` and `@end` blocks. Lines outside a block must be blank or a `#` -/// comment. The body between the header and `@end` is returned verbatim. -fn parse_blocks(text: &str, kind: &str) -> Result> { - let opener = format!("@{kind} "); - let mut blocks = Vec::new(); - let mut cur: Option = None; - - for (i, raw) in text.lines().enumerate() { - let no = (i + 1) as u32; - let t = raw.trim(); - match &mut cur { - None => { - if let Some(rest) = t.strip_prefix(&opener) { - cur = Some(Block { - id: parse_id(rest, no)?, - line: no, - body: String::new(), - }); - } else if t.is_empty() || t.starts_with('#') { - // header, blank, or comment between blocks - } else { - return Err(Error::invalid(format!( - "line {no}: expected `{opener}...`, got {t:?}" - ))); - } - } - Some(b) => { - if t == "@end" { - blocks.push(cur.take().unwrap()); - } else { - b.body.push_str(raw); - b.body.push('\n'); - } - } - } - } - if cur.is_some() { - return Err(Error::invalid("unterminated @-block (missing @end)")); - } - Ok(blocks) -} - -struct MapEventBlock { - id: u32, - line: u32, - pages: Vec, -} - -struct MapPageBlock { - index: usize, - body: String, -} - -/// Parse `@event ` / `@page ` / `@endpage` / `@endevent` nesting. -fn parse_map_blocks(text: &str) -> Result> { - let mut events: Vec = Vec::new(); - let mut page: Option = None; - - for (i, raw) in text.lines().enumerate() { - let no = (i + 1) as u32; - let t = raw.trim(); - - if let Some(p) = &mut page { - if t == "@endpage" { - let done = page.take().unwrap(); - events - .last_mut() - .ok_or_else(|| Error::invalid(format!("line {no}: @endpage outside @event")))? - .pages - .push(done); - } else if t == "@endevent" || t.starts_with("@event ") || t.starts_with("@page ") { - // A structural keyword inside a page body means the page was never closed. Fail - // loudly rather than swallow the rest of the document into this page. - return Err(Error::invalid(format!( - "line {no}: {t:?} inside a page body (missing @endpage?)" - ))); - } else { - p.body.push_str(raw); - p.body.push('\n'); - } - continue; - } - - if let Some(rest) = t.strip_prefix("@event ") { - events.push(MapEventBlock { - id: parse_id(rest, no)?, - line: no, - pages: Vec::new(), - }); - } else if let Some(rest) = t.strip_prefix("@page ") { - let index = parse_id(rest, no)? as usize; - if events.is_empty() { - return Err(Error::invalid(format!("line {no}: @page outside @event"))); - } - page = Some(MapPageBlock { - index, - body: String::new(), - }); - } else if t == "@endevent" || t.is_empty() || t.starts_with('#') { - // event terminator, header, blank, or comment - } else { - return Err(Error::invalid(format!( - "line {no}: unexpected outside a page body: {t:?}" - ))); - } - } - if page.is_some() { - return Err(Error::invalid("unterminated @page (missing @endpage)")); - } - Ok(events) -} - -/// Read the leading integer id from a header tail, such as `5` or `5 # name`. -fn parse_id(rest: &str, no: u32) -> Result { - rest.split_whitespace() - .next() - .and_then(|s| s.parse().ok()) - .ok_or_else(|| Error::invalid(format!("line {no}: missing/invalid id in {rest:?}"))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn map_block_well_formed() { - let doc = "# wolf-edit map\n@event 3\n@page 0\nMessage \"hi\"\n@endpage\n@endevent\n"; - let evs = parse_map_blocks(doc).expect("parse"); - assert_eq!(evs.len(), 1); - assert_eq!(evs[0].id, 3); - assert_eq!(evs[0].pages.len(), 1); - assert_eq!(evs[0].pages[0].index, 0); - } - - #[test] - fn missing_endpage_is_an_error_not_a_swallow() { - // The `@event 4` must not be swallowed into page 0's body. - let doc = "@event 3\n@page 0\nMessage \"hi\"\n@event 4\n@page 0\n@endpage\n@endevent\n"; - assert!( - parse_map_blocks(doc).is_err(), - "missing @endpage must error" - ); - } - - #[test] - fn unterminated_page_at_eof_errors() { - let doc = "@event 3\n@page 0\nMessage \"hi\"\n"; - assert!(parse_map_blocks(doc).is_err()); - } - - #[test] - fn ce_blocks_parse_and_reject_stray_lines() { - let blocks = parse_blocks("@commonevent 7\nMessage \"x\"\n@end\n", "commonevent").unwrap(); - assert_eq!(blocks.len(), 1); - assert_eq!(blocks[0].id, 7); - // A non-`@commonevent` line between blocks is rejected. - assert!(parse_blocks("garbage\n@commonevent 1\n@end\n", "commonevent").is_err()); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/json.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/json.rs deleted file mode 100644 index 5626783..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/json.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! Human-readable JSON export of a [`Database`] (the tabular `.project` plus `.dat` data). -//! -//! Databases do not fit the WolfScript code view, so they export to JSON for inspection: each -//! type's schema (fields and kind) and its data rows, with each row's values mapped back to field -//! names. The byte-faithful [`wolf_formats::database::Database`] stays the round-trip authority. -//! This is a read-only projection for readability, with values shown as signed ints or decoded -//! strings. - -use std::collections::HashMap; -use std::fmt::Write; - -use wolf_formats::database::{Database, DbData, DbType}; - -use crate::text::decode_wstr; - -/// Render a database as pretty-printed JSON. `kind` is a label such as `"UDB"`, `"CDB"`, or -/// `"SDB"`. `glossary` is the engine-text map (JP to EN). Every structural name it covers (a type -/// or field, and the variable-name rows of the System DB) gets a `"name_en"` field next to its -/// Japanese `"name"`, so a modder reading the English code can look the entry up in either -/// language. Player-facing content such as item and skill row names is absent from the glossary -/// and stays Japanese-only, which matches how it reads in the code. -pub fn database_to_json(db: &Database, kind: &str, glossary: &HashMap) -> String { - let utf8 = db.utf8; - let mut s = String::new(); - s.push_str("{\n"); - s.push_str(&format!(" \"kind\": {},\n", jstr(kind))); - s.push_str(&format!(" \"version\": \"0x{:02x}\",\n", db.dat_version)); - let enc = if utf8 { "UTF-8" } else { "Shift-JIS" }; - s.push_str(&format!(" \"encoding\": {},\n", jstr(enc))); - s.push_str(&format!(" \"typeCount\": {},\n", db.types.len())); - s.push_str(" \"types\": [\n"); - - for (ti, t) in db.types.iter().enumerate() { - type_to_json(&mut s, ti, t, utf8, glossary); - s.push_str(if ti + 1 == db.types.len() { - "\n" - } else { - ",\n" - }); - } - s.push_str(" ]\n}\n"); - s -} - -/// Render `"name": ""`, plus `, "name_en": ""` when the glossary translates it. Used for -/// every name field so the JSON is searchable in either language. -fn name_pair(jp: &str, glossary: &HashMap) -> String { - match glossary.get(jp) { - Some(en) if en != jp => format!("{}, \"name_en\": {}", jstr(jp), jstr(en)), - _ => jstr(jp), - } -} - -fn type_to_json( - s: &mut String, - id: usize, - t: &DbType, - utf8: bool, - glossary: &HashMap, -) { - let fields_size = (t.dat_fields_size as usize).min(t.fields.len()); - s.push_str(" {\n"); - let _ = write!(s, " \"id\": {id},\n"); - let _ = write!( - s, - " \"name\": {},\n", - name_pair(&decode_wstr(&t.name, utf8), glossary) - ); - let _ = write!( - s, - " \"description\": {},\n", - jstr(&decode_wstr(&t.description, utf8)) - ); - - // Schema fields: name (plus name_en) and kind. Only the first `fields_size` carry row data. - s.push_str(" \"fields\": ["); - for (fi, f) in t.fields.iter().enumerate() { - let kind = if f.is_string() { "string" } else { "int" }; - let has_data = fi < fields_size; - let _ = write!( - s, - "{}{{\"id\": {fi}, \"name\": {}, \"kind\": {}{}}}", - if fi == 0 { "" } else { ", " }, - name_pair(&decode_wstr(&f.name, utf8), glossary), - jstr(kind), - if has_data { - "" - } else { - ", \"schemaOnly\": true" - } - ); - } - s.push_str("],\n"); - - // Data rows: id, name (plus name_en), and values mapped to field names. - let _ = write!(s, " \"rowCount\": {},\n", t.data.len()); - s.push_str(" \"rows\": [\n"); - for (di, d) in t.data.iter().enumerate() { - row_to_json(s, di, t, d, fields_size, utf8, glossary); - s.push_str(if di + 1 == t.data.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n }"); -} - -/// The per-row value slots of a type, in on-disk order: `(jsonKey, is_string, slot_index)` for -/// each of the first `fields_size` fields. The key is the field's decoded name when that name is -/// unique within the type, else a stable `#`. That covers empty and duplicated names -/// such as formula columns. The slot index points into the row's `int_values` or `string_values` -/// (all int fields first, then all string fields). Shared by the JSON export and -/// [`crate::db_edit`] so the keys match exactly. `slot` indexes `string_values` when `is_string`, -/// otherwise `int_values`. -pub fn value_slots(t: &DbType, fields_size: usize, utf8: bool) -> Vec<(String, bool, usize)> { - let mut counts: HashMap = HashMap::new(); - for fi in 0..fields_size { - let n = decode_wstr(&t.fields[fi].name, utf8); - if !n.is_empty() { - *counts.entry(n).or_default() += 1; - } - } - let (mut int_i, mut str_i) = (0usize, 0usize); - let mut out = Vec::with_capacity(fields_size); - for fi in 0..fields_size { - let f = &t.fields[fi]; - let n = decode_wstr(&f.name, utf8); - let key = if n.is_empty() || counts.get(&n).copied().unwrap_or(0) > 1 { - format!("#{fi}") - } else { - n - }; - let is_str = f.is_string(); - let slot = if is_str { - let s = str_i; - str_i += 1; - s - } else { - let s = int_i; - int_i += 1; - s - }; - out.push((key, is_str, slot)); - } - out -} - -fn row_to_json( - s: &mut String, - id: usize, - t: &DbType, - d: &DbData, - fields_size: usize, - utf8: bool, - glossary: &HashMap, -) { - let _ = write!( - s, - " {{\"id\": {id}, \"name\": {}", - name_pair(&decode_wstr(&d.name, utf8), glossary) - ); - s.push_str(", \"values\": {"); - let mut first = true; - for (key, is_str, slot) in value_slots(t, fields_size, utf8) { - if !first { - s.push_str(", "); - } - first = false; - if is_str { - let v = d - .string_values - .get(slot) - .map(|w| decode_wstr(w, utf8)) - .unwrap_or_default(); - let _ = write!(s, "{}: {}", jstr(&key), jstr(&v)); - } else { - let v = d.int_values.get(slot).copied().unwrap_or(0) as i32; - let _ = write!(s, "{}: {}", jstr(&key), v); - } - } - s.push_str("}}"); -} - -/// JSON string literal with escaping. -fn jstr(s: &str) -> String { - let mut out = String::with_capacity(s.len() + 2); - out.push('"'); - for c in s.chars() { - match c { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if (c as u32) < 0x20 => { - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), - } - } - out.push('"'); - out -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/lib.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/lib.rs deleted file mode 100644 index a5cdce1..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! `wolf-decompiler` turns the byte-faithful command programs from `wolf-formats` -//! into readable, editable WolfScript. -//! -//! Provides the decompile (render) direction. The byte-exact `RawCommand` stays the -//! round-trip authority. The parse/recompile direction is built on top of it. - -#![forbid(unsafe_code)] - -pub mod compile; -pub mod db_edit; -pub mod document; -pub mod json; -pub mod merge; -pub mod names; -pub mod save; -pub mod save_pro; -pub mod spec; -pub mod strings; -pub mod symbols; -pub mod text; -pub mod txt_events; -pub mod wolfscript; - -pub use compile::{compile_commands, compile_commands_enc}; -pub use db_edit::apply_database_edit; -pub use document::{ - compile_common_events_edit, compile_map_edit, decompile_common_events_edit, - decompile_common_events_edit_annotated, decompile_map_edit, decompile_map_edit_annotated, -}; -pub use json::database_to_json; -pub use merge::{apply_memory, build_memory, dropped_sources, MergeStats}; -pub use names::{check_name_conflicts, extract_db_name_cells, extract_names, inject_names, Conflict}; -pub use save::{ - inspect_save, read_title, read_title_at, update_save, SaveFormat, SaveInfo, SaveUpdateStats, -}; -pub use save_pro::{decrypt_pro, encrypt_pro, is_pro_save}; -pub use strings::{ - apply_game_dat, audit_common_events_to_json, audit_db_to_json, audit_map_to_json, - db_strings_to_json, dump_game_dat, extract_common_events, extract_db_strings, extract_game_dat, - extract_map, inject_common_events, inject_db_strings, inject_game_dat, inject_map, - normalize_en_punct, scenes_to_json, InjectOptions, InjectStats, -}; - -pub use symbols::SymbolTable; -pub use txt_events::{extract_txt_dir, extract_txt_events, inject_txt_events, split_txt_dir}; -pub use wolfscript::{ - decompile_commands, decompile_commands_annotated, decompile_commands_enc, - decompile_common_events, decompile_map, operand, Renderer, -}; diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/merge.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/merge.rs deleted file mode 100644 index 39482d3..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/merge.rs +++ /dev/null @@ -1,374 +0,0 @@ -//! Incremental translation merge, a translation memory. Carries an existing translation over to a -//! freshly re-extracted set of strings when a game updates (for example v1.00 to v1.10), so the -//! translator only has to touch genuinely new or changed lines. -//! -//! The extractor's outputs (scenes, db, names, gamedat) all share one leaf shape: a JSON object -//! carrying a string `source` and a string `text`, where `text == source` means untranslated and -//! the translator edits `text`. This module does not model each schema. It walks every JSON object -//! generically and acts on any object that has both fields, the same walker pattern -//! [`crate::names::check_name_conflicts`] uses. -//! -//! Matching is by exact source string, not by locator (event/cmd/row index). A line that moved to -//! a different scene in the update still inherits its translation, which is more robust than -//! locator-matching across versions. The trade-off is that two different lines that share an -//! identical source get the same carried translation. That is acceptable and consistent with how -//! the rest of the toolchain treats `source` as the translation key. -//! -//! Pipeline: -//! 1. [`build_memory`] over the old (already-translated) JSONs builds a `source -> text` map of -//! every real translation (`text != source`), plus a list of any sources that diverged across -//! files. -//! 2. [`apply_memory`] over each new (freshly extracted) JSON returns the same JSON with -//! carried-over translations filled in, plus [`MergeStats`] (carried, still_new, -//! kept_existing). -//! 3. [`dropped_sources`] reports which old translations no longer apply because the line was -//! removed in the update. -//! -//! Only `text` values are ever changed. Every other field and the document structure are -//! preserved exactly, and the result is re-serialized pretty-printed with serde_json's stable key -//! order. - -use std::collections::{BTreeMap, BTreeSet, HashMap}; - -use serde_json::Value; - -// ---------------------------------------------------------------------------- -// Generic source/text walker (shared shape across scenes, db, names, gamedat) -// ---------------------------------------------------------------------------- - -/// Pull every `(source, text)` pair out of one parsed translation JSON, regardless of schema. Any -/// object that has both a string `source` and a string `text` is a translation leaf. Mirrors -/// [`crate::names`]'s `collect_source_text_pairs` so the merge sees exactly the same leaves the -/// rest of the toolchain treats as translatable. -fn collect_source_text_pairs(v: &Value, out: &mut Vec<(String, String)>) { - match v { - Value::Object(map) => { - if let (Some(Value::String(s)), Some(Value::String(t))) = - (map.get("source"), map.get("text")) - { - out.push((s.clone(), t.clone())); - } - for child in map.values() { - collect_source_text_pairs(child, out); - } - } - Value::Array(arr) => { - for child in arr { - collect_source_text_pairs(child, out); - } - } - _ => {} - } -} - -/// Collect just the `source` strings of every translation leaf (translated or not) in one parsed -/// JSON, into `out`. Empty sources are skipped. They are never a memory key. -fn collect_sources(v: &Value, out: &mut BTreeSet) { - match v { - Value::Object(map) => { - if let (Some(Value::String(s)), Some(Value::String(_))) = - (map.get("source"), map.get("text")) - { - if !s.is_empty() { - out.insert(s.clone()); - } - } - for child in map.values() { - collect_sources(child, out); - } - } - Value::Array(arr) => { - for child in arr { - collect_sources(child, out); - } - } - _ => {} - } -} - -// ---------------------------------------------------------------------------- -// Build the translation memory from the old (translated) JSONs -// ---------------------------------------------------------------------------- - -/// Build the translation memory from the old (already-translated) extraction JSONs. -/// -/// Walks every input generically and collects `source -> text` for each leaf that is a real -/// translation (`text != source`). `source == ""` is skipped. If the same `source` is given two -/// or more distinct `text` values across the inputs, the first one seen wins (inputs in order, -/// leaves in document order), and the source is reported in the returned conflicts vec together -/// with its divergent values (the kept value first, sorted thereafter). -/// -/// An input that fails to parse is skipped, so one stray file cannot sink the whole memory. -/// -/// Returns `(memory, conflicts)` where `conflicts` is `(source, distinct_texts)` per divergent -/// source, sorted by source for stable output. -// The return tuple is the documented public API shape: a source->text map plus a per-source -// variant list. -#[allow(clippy::type_complexity)] -pub fn build_memory(old_jsons: &[&str]) -> (HashMap, Vec<(String, Vec)>) { - // source -> kept text (first seen), and source -> ordered set of all distinct texts seen. - let mut memory: HashMap = HashMap::new(); - // BTreeMap and BTreeSet keep conflict reporting deterministic and de-duplicated. - let mut variants: BTreeMap> = BTreeMap::new(); - // Preserve the kept first-seen value separately so it can lead the reported variant list. - let mut kept: BTreeMap = BTreeMap::new(); - - for json in old_jsons { - let Ok(root) = serde_json::from_str::(json) else { - continue; // skip unparseable inputs rather than failing the whole build. - }; - let mut pairs = Vec::new(); - collect_source_text_pairs(&root, &mut pairs); - for (source, text) in pairs { - if source.is_empty() || text == source { - continue; // empty key or untranslated leaf, not a memory entry. - } - // First real translation for this source wins and is the kept value. - memory.entry(source.clone()).or_insert_with(|| text.clone()); - kept.entry(source.clone()).or_insert_with(|| text.clone()); - variants - .entry(source.clone()) - .or_default() - .insert(text.clone()); - } - } - - // A source with two or more distinct translations is a conflict. Report the kept value first, - // then the remaining distinct values in sorted order. - let mut conflicts = Vec::new(); - for (source, texts) in variants { - if texts.len() < 2 { - continue; - } - let keep = kept.get(&source).cloned().unwrap_or_default(); - let mut list = Vec::with_capacity(texts.len()); - list.push(keep.clone()); - for t in texts { - if t != keep { - list.push(t); - } - } - conflicts.push((source, list)); - } - - (memory, conflicts) -} - -// ---------------------------------------------------------------------------- -// Apply the memory to a NEW (freshly extracted) JSON -// ---------------------------------------------------------------------------- - -/// What [`apply_memory`] did to one new extraction file. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct MergeStats { - /// Leaves that were untranslated in the new file and got a translation carried over from memory. - pub carried: usize, - /// Leaves that are still untranslated (no memory hit). Genuinely new or changed text to do. - pub still_new: usize, - /// Leaves the new file already had its own translation for (`text != source`). Left untouched. - pub kept_existing: usize, -} - -/// Apply a translation memory to one new (freshly extracted) extraction JSON. -/// -/// Parses the JSON, walks it generically, and for every leaf object with a string `source` and -/// `text`: -/// * If `text != source` already (the new file carries its own translation), leave it and add -/// to `kept_existing`. -/// * Else if `source` is in `memory`, set `text = memory[source]` and add to `carried`. -/// * Else leave it untranslated and add to `still_new`. -/// -/// All other fields and the overall structure are preserved exactly. Only `text` values may -/// change. The result is re-serialized pretty-printed via serde_json with stable key order. -/// -/// Returns the re-serialized JSON plus the [`MergeStats`], or an error string if the input does -/// not parse as JSON. -pub fn apply_memory( - new_json: &str, - memory: &HashMap, -) -> Result<(String, MergeStats), String> { - let mut root: Value = serde_json::from_str(new_json).map_err(|e| format!("invalid JSON: {e}"))?; - let mut stats = MergeStats::default(); - apply_to_value(&mut root, memory, &mut stats); - let out = serde_json::to_string_pretty(&root).map_err(|e| format!("serialize failed: {e}"))?; - Ok((out, stats)) -} - -/// Recurse a parsed value, rewriting `text` on each translation leaf per the merge rule. -fn apply_to_value(v: &mut Value, memory: &HashMap, stats: &mut MergeStats) { - match v { - Value::Object(map) => { - // Decide this object's leaf action on an immutable read first, then mutate. - let leaf = match (map.get("source"), map.get("text")) { - (Some(Value::String(s)), Some(Value::String(t))) => Some((s.clone(), t.clone())), - _ => None, - }; - if let Some((source, text)) = leaf { - if text != source { - stats.kept_existing += 1; - } else if let Some(carried) = memory.get(&source) { - map.insert("text".to_string(), Value::String(carried.clone())); - stats.carried += 1; - } else { - stats.still_new += 1; - } - } - // Recurse into children regardless. Nested leaves, such as a speaker sub-object, still - // get their own pass. - for child in map.values_mut() { - apply_to_value(child, memory, stats); - } - } - Value::Array(arr) => { - for child in arr.iter_mut() { - apply_to_value(child, memory, stats); - } - } - _ => {} - } -} - -// ---------------------------------------------------------------------------- -// Reporting: which old translations no longer apply (removed in the update) -// ---------------------------------------------------------------------------- - -/// The memory sources present in none of the new extraction JSONs. These are lines that were -/// removed, or whose source text changed, in the update, so their translation cannot carry over. -/// Reporting only, sorted for stable output. Unparseable new files are skipped. -pub fn dropped_sources(memory: &HashMap, new_jsons: &[&str]) -> Vec { - let mut present: BTreeSet = BTreeSet::new(); - for json in new_jsons { - if let Ok(root) = serde_json::from_str::(json) { - collect_sources(&root, &mut present); - } - } - let mut dropped: Vec = memory - .keys() - .filter(|s| !present.contains(*s)) - .cloned() - .collect(); - dropped.sort(); - dropped -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_memory_keeps_real_translations_only() { - // A translated names file: あ is translated to A, い is untranslated (text==source). - let names = r#"{"kind":"names","names":[ - {"source":"あ","text":"A","occurrences":3,"note":"Monster"}, - {"source":"い","text":"い","occurrences":1,"note":"Item"} - ]}"#; - let (mem, conflicts) = build_memory(&[names]); - assert_eq!(mem.get("あ").map(String::as_str), Some("A")); - assert!(!mem.contains_key("い"), "untranslated source is excluded"); - assert_eq!(mem.len(), 1); - assert!(conflicts.is_empty()); - } - - #[test] - fn build_memory_skips_empty_source() { - let json = r#"{"lines":[{"source":"","text":"X"},{"source":"a","text":"B"}]}"#; - let (mem, _) = build_memory(&[json]); - assert!(!mem.contains_key("")); - assert_eq!(mem.get("a").map(String::as_str), Some("B")); - } - - #[test] - fn build_memory_reports_conflict_and_keeps_first() { - // Same source translated two different ways across two files is a conflict. First wins. - let f1 = r#"{"lines":[{"source":"剣","text":"Sword"}]}"#; - let f2 = r#"{"lines":[{"source":"剣","text":"Blade"}]}"#; - let (mem, conflicts) = build_memory(&[f1, f2]); - assert_eq!(mem.get("剣").map(String::as_str), Some("Sword"), "first kept"); - assert_eq!(conflicts.len(), 1); - assert_eq!(conflicts[0].0, "剣"); - // Kept value leads the reported variants. Both distinct values are present. - assert_eq!(conflicts[0].1.first().map(String::as_str), Some("Sword")); - assert!(conflicts[0].1.iter().any(|t| t == "Blade")); - assert_eq!(conflicts[0].1.len(), 2); - } - - #[test] - fn build_memory_same_text_is_not_a_conflict() { - // The same translation in two files is consistent, not divergent. - let f1 = r#"{"lines":[{"source":"剣","text":"Sword"}]}"#; - let f2 = r#"{"lines":[{"source":"剣","text":"Sword"}]}"#; - let (_, conflicts) = build_memory(&[f1, f2]); - assert!(conflicts.is_empty()); - } - - #[test] - fn apply_memory_carries_and_marks_still_new() { - // New scenes extraction: あ matches memory (carried), う has no memory entry (still new). - let new = r#"{"scenes":[{"lines":[ - {"source":"あ","text":"あ","speaker":"NPC"}, - {"source":"う","text":"う","speaker":"NPC"} - ]}]}"#; - let mut mem = HashMap::new(); - mem.insert("あ".to_string(), "A".to_string()); - let (out, stats) = apply_memory(new, &mem).unwrap(); - assert_eq!(stats.carried, 1); - assert_eq!(stats.still_new, 1); - assert_eq!(stats.kept_existing, 0); - // Re-parse and confirm the carried translation landed and the still-new one is unchanged. - let v: Value = serde_json::from_str(&out).unwrap(); - let mut pairs = Vec::new(); - collect_source_text_pairs(&v, &mut pairs); - let map: HashMap<_, _> = pairs.into_iter().collect(); - assert_eq!(map.get("あ").map(String::as_str), Some("A")); - assert_eq!(map.get("う").map(String::as_str), Some("う")); - } - - #[test] - fn apply_memory_keeps_existing_translation_untouched() { - // The new file already translated あ to "Already". Even with a different memory entry, its - // own translation is kept and counted as kept_existing, never overwritten. - let new = r#"{"lines":[{"source":"あ","text":"Already"}]}"#; - let mut mem = HashMap::new(); - mem.insert("あ".to_string(), "FromMemory".to_string()); - let (out, stats) = apply_memory(new, &mem).unwrap(); - assert_eq!(stats.kept_existing, 1); - assert_eq!(stats.carried, 0); - assert_eq!(stats.still_new, 0); - let v: Value = serde_json::from_str(&out).unwrap(); - let mut pairs = Vec::new(); - collect_source_text_pairs(&v, &mut pairs); - assert_eq!(pairs[0].1, "Already"); - } - - #[test] - fn apply_memory_empty_memory_preserves_everything() { - // With an empty memory, nothing is carried. Every text and non-text field is preserved. - let new = r#"{"scenes":[{"event":7,"lines":[ - {"cmd":3,"str":0,"row":2,"speaker":"勇者","source":"あ","text":"あ"} - ]}]}"#; - let before: Value = serde_json::from_str(new).unwrap(); - let (out, stats) = apply_memory(new, &HashMap::new()).unwrap(); - assert_eq!(stats.carried, 0); - assert_eq!(stats.kept_existing, 0); - assert_eq!(stats.still_new, 1); - let after: Value = serde_json::from_str(&out).unwrap(); - // Logical content is identical. serde_json::Value compares structurally. - assert_eq!(before, after, "empty memory is a structural no-op"); - } - - #[test] - fn apply_memory_bad_json_errors() { - assert!(apply_memory("{not json", &HashMap::new()).is_err()); - } - - #[test] - fn dropped_sources_reports_removed_only() { - let mut mem = HashMap::new(); - mem.insert("あ".to_string(), "A".to_string()); - mem.insert("い".to_string(), "I".to_string()); - // The new file only still contains あ. い was removed in the update. - let new = r#"{"lines":[{"source":"あ","text":"あ"}]}"#; - let dropped = dropped_sources(&mem, &[new]); - assert_eq!(dropped, vec!["い".to_string()]); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/names.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/names.rs deleted file mode 100644 index 3a5ff90..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/names.rs +++ /dev/null @@ -1,745 +0,0 @@ -//! Project-level name glossary. A cross-file, consistency-preserving translation of the database -//! names a Wolf game looks rows up by. -//! -//! A command like `UDB[Monster]["ブルノエール"].Graphic` resolves a row by its name, and that name -//! is mirrored in three or more independent places that must translate together or the lookup -//! breaks (returns -1 or hits the wrong row): -//! 1. The DB name/display field value (the row's `名前`, `表示名`, nickname, or element name). -//! 2. The stored row name ([`DbData::name`](wolf_formats::database::DbData), the `.project` -//! half). -//! 3. Every command by-name reference: `cid250/252` dataName (str index 2), `cid251` dataName -//! (str index 1), and `cid112` StringCondition comparison literals (any str). -//! -//! The per-file [`crate::strings`] extract/inject path translates each of these independently, so -//! a translator could rename a row's display field without the row name and by-name references -//! following, silently breaking the lookup. This module instead builds one `source -> text` map -//! from the whole data dir and applies it to all mirrors at once, keeping them consistent. -//! -//! Control-code preservation, drift guarding, and encoding checks reuse the existing -//! [`crate::strings::write_translation`] path. - -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fmt::Write as _; // `write!`/`writeln!` into a String. - -use serde_json::Value; -use wolf_formats::command::RawCommand; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::{Database, DbType}; -use wolf_formats::map::Map; -use wolf_formats::WStr; - -use crate::json::value_slots; -use crate::strings::{ - db_field_role, has_translatable_text, value_is_asset_path, write_translation, InjectOptions, - InjectStats, Role, -}; -use crate::text::decode_wstr; - -// ---------------------------------------------------------------------------- -// Name / display field detection (general, not hardcoded per game) -// ---------------------------------------------------------------------------- - -/// A string field whose values are single-string names the game may look rows up by, so they must -/// stay consistent with the row name and every by-name reference. General, not a per-game -/// allowlist. It is exactly the cells the role classifier assigns [`Role::Name`]. -/// -/// This is the glossary's half of the disjoint split. It owns name cells, while the per-file -/// `db-strings` path owns description/dialogue content cells ([`Role::Content`]). Because -/// [`db_field_role`] assigns each cell exactly one role, no DB string cell is extracted by both -/// paths, which removes the hazard of a name being translated to two different values. -/// -/// A cell is a name when [`db_field_role`] returns [`Role::Name`]: the type's first string field -/// (the canonical row-name column that `UDB[type][name]` reads, even with an empty or unrecognised -/// label), or a recognised name field or type (`名前`, `愛称`, `呼び方`, the `移動名` -/// location-banner key, and so on). A description/dialogue field is [`Role::Content`], owned by the -/// per-file path and independently translatable. An internal field is [`Role::Internal`]. Neither -/// is a name. The on-screen `移動名` location banner classifies as a name, since the consistency -/// machinery covers its by-name-key usage. A genuine internal field never does. -fn name_display_field( - type_byte: u8, - type_name: &str, - field_jp: &str, - field_en: &str, - is_first_string: bool, -) -> bool { - db_field_role(type_byte, type_name, field_jp, field_en, is_first_string) == Role::Name -} - -/// The name/display fields of a type: `(field_idx, string_slot, type_byte, field_jp)` for each -/// field [`name_display_field`] keeps, in field order. `glossary` powers the EN field-name -/// matching. -fn name_fields(t: &DbType, utf8: bool, glossary: &HashMap) -> Vec<(usize, usize, u8, String)> { - let type_name = decode_wstr(&t.name, utf8); - let fields_size = (t.dat_fields_size as usize).min(t.fields.len()); - let slots = value_slots(t, fields_size, utf8); - let mut seen_string = false; - let mut out = Vec::new(); - for (fi, (_, is_str, slot)) in slots.iter().enumerate() { - if !is_str { - continue; - } - let is_first_string = !seen_string; - seen_string = true; - let fjp = decode_wstr(&t.fields[fi].name, utf8); - let fen = glossary.get(&fjp).map(String::as_str).unwrap_or(&fjp); - let tb = t.fields[fi].type_byte; - if name_display_field(tb, &type_name, &fjp, fen, is_first_string) { - out.push((fi, *slot, tb, fjp)); - } - } - out -} - -/// A single name/display cell value, with the per-cell asset-path exclusion applied. Returns the -/// decoded value only when it is a translatable name. Skips empty, pure-code, and folder-picker -/// paths. -fn name_cell_value(row: &wolf_formats::database::DbData, slot: usize, tb: u8, utf8: bool) -> Option { - let v = row.string_values.get(slot).map(|w| decode_wstr(w, utf8))?; - if !has_translatable_text(&v) { - return None; - } - if tb == 1 && value_is_asset_path(&v) { - return None; - } - Some(v) -} - -/// The glossary's per-cell name extraction of a database, as `(type_idx, row_idx, field_idx)` for -/// every string cell the glossary owns (a [`name_display_field`] cell with a translatable name -/// value). The stored row name (`DbData.name`) is not a field cell, so it carries no -/// `(type,row,field)` locator and is not in this set. -/// -/// This is the glossary's side of the disjointness invariant. It must share no `(type,row,field)` -/// with [`crate::strings::extract_db_strings`]'s output, since every DB string cell has exactly -/// one [`crate::strings::Role`]. -pub fn extract_db_name_cells( - db: &Database, - glossary: &HashMap, -) -> Vec<(usize, usize, usize)> { - let utf8 = db.utf8; - let mut out = Vec::new(); - for (ti, t) in db.types.iter().enumerate() { - let fields = name_fields(t, utf8, glossary); - for (ri, row) in t.data.iter().enumerate() { - for (fi, slot, tb, _) in &fields { - if name_cell_value(row, *slot, *tb, utf8).is_some() { - out.push((ti, ri, *fi)); - } - } - } - } - out -} - -// ---------------------------------------------------------------------------- -// By-name command references (cid 250/252 str2, cid 251 str1, cid 112 any str) -// ---------------------------------------------------------------------------- - -/// The string-arg indices of a command that are DB by-name references: a data name a lookup -/// resolves, or a name compared in a StringCondition. Empty means none. -fn dbref_str_indices(cmd: &RawCommand) -> Vec { - match cmd.cid { - // DB read/write by name: str index 2 is the data (row) name operand. - 250 | 252 => vec![2], - // DB-string read by name: str index 1 is the data (row) name operand. - 251 => vec![1], - // StringCondition: any string operand may be a name compared against a row name. - 112 => (0..cmd.str_args.len()).collect(), - _ => vec![], - } -} - -/// Visit every by-name reference string in a command list, calling `f(cmd_idx, str_idx, value)`. -fn for_each_dbref(cmds: &[RawCommand], utf8: bool, mut f: impl FnMut(usize, usize, &str)) { - for (ci, cmd) in cmds.iter().enumerate() { - for si in dbref_str_indices(cmd) { - let Some(w) = cmd.str_args.get(si) else { - continue; - }; - let v = decode_wstr(w, utf8); - if !v.is_empty() { - f(ci, si, &v); - } - } - } -} - -// ---------------------------------------------------------------------------- -// Extraction: collect the unique candidate name strings across the data dir -// ---------------------------------------------------------------------------- - -/// One candidate name in the glossary: the `source` string (its `text` starts equal, the -/// translator edits `text`), how many places it occurs across the corpus, and a short hint. -struct NameEntry { - source: String, - occurrences: usize, - /// A short note: the originating DB type name (the first one observed), for context. - note: String, -} - -/// Accumulator. Per unique source string, its occurrence count and a note (the first type seen). -#[derive(Default)] -struct NameAcc { - /// source -> occurrence count. - counts: BTreeMap, - /// source -> note (originating type name, kept from the first DB cell or row name seen). - notes: BTreeMap, - /// Sources that qualified as a candidate (a DB name/display value or a stored row name). A - /// by-name command reference only counts toward an existing candidate. It never introduces a - /// new one. A reference to a name that exists in no DB is dead and untranslatable. - candidates: BTreeSet, -} - -impl NameAcc { - /// Register a DB-sourced candidate (row name or name/display cell value). It becomes a - /// glossary entry and the occurrence is counted. `note` is the originating type name. - fn add_candidate(&mut self, s: &str, note: &str) { - self.candidates.insert(s.to_string()); - *self.counts.entry(s.to_string()).or_default() += 1; - self.notes - .entry(s.to_string()) - .or_insert_with(|| note.to_string()); - } - - /// Count a by-name reference occurrence. Does not introduce a new candidate. - fn count_ref(&mut self, s: &str) { - if self.candidates.contains(s) { - *self.counts.entry(s.to_string()).or_default() += 1; - } - } -} - -/// Collect candidate names from one database: every non-empty stored row name, and every -/// translatable name/display cell value. -fn collect_db_candidates(acc: &mut NameAcc, db: &Database, glossary: &HashMap) { - let utf8 = db.utf8; - for t in &db.types { - let type_name = decode_wstr(&t.name, utf8); - let fields = name_fields(t, utf8, glossary); - for row in &t.data { - let row_name = decode_wstr(&row.name, utf8); - if has_translatable_text(&row_name) { - acc.add_candidate(&row_name, &type_name); - } - for (_, slot, tb, _) in &fields { - if let Some(v) = name_cell_value(row, *slot, *tb, utf8) { - acc.add_candidate(&v, &type_name); - } - } - } - } -} - -/// Build the project name glossary JSON over a whole data dir: the parsed databases, an optional -/// common-events file, and the parsed maps. `glossary` (JP to EN) powers EN field-name matching. -/// -/// The result is a `{ "kind":"names", "names":[ {source,text,occurrences,note}, ... ] }` document -/// where `text == source` initially and the translator edits `text`. Sources are deduplicated by -/// exact value and sorted deterministically by source string, so re-running on the same corpus -/// yields byte-identical output. -pub fn extract_names( - dbs: &[Database], - ces: &[&CommonEventsFile], - maps: &[Map], - glossary: &HashMap, -) -> String { - let mut acc = NameAcc::default(); - // Pass 1: candidates from every DB (row names and name/display cell values). - for db in dbs { - collect_db_candidates(&mut acc, db, glossary); - } - // Pass 2: count by-name references in command streams against the candidate set. - for ce in ces { - for ev in &ce.events { - for_each_dbref(&ev.commands, ce.utf8, |_, _, v| acc.count_ref(v)); - } - } - for map in maps { - for ev in &map.events { - for page in &ev.pages { - for_each_dbref(&page.commands, map.utf8, |_, _, v| acc.count_ref(v)); - } - } - } - - // Materialise the sorted, deduped entry list. `candidates` is a BTreeSet so iteration is - // already in deterministic source order. Skip anything that fails the translatable filter. - // Candidates already passed it, but row names from a denylist-only path could slip a - // pure-symbol value through. - let mut entries: Vec = Vec::with_capacity(acc.candidates.len()); - for source in &acc.candidates { - if !has_translatable_text(source) { - continue; - } - entries.push(NameEntry { - source: source.clone(), - occurrences: acc.counts.get(source).copied().unwrap_or(0), - note: acc.notes.get(source).cloned().unwrap_or_default(), - }); - } - - names_to_json(&entries) -} - -/// Render the collected name entries as the names-glossary JSON. `text` starts equal to `source`. -fn names_to_json(entries: &[NameEntry]) -> String { - let mut s = String::new(); - s.push_str("{\n"); - s.push_str(" \"kind\": \"names\",\n"); - let _ = writeln!(s, " \"count\": {},", entries.len()); - s.push_str(" \"names\": [\n"); - for (i, e) in entries.iter().enumerate() { - let _ = write!( - s, - " {{\"source\": {}, \"text\": {}, \"occurrences\": {}, \"note\": {}}}", - jstr(&e.source), - jstr(&e.source), - e.occurrences, - jstr(&e.note), - ); - s.push_str(if i + 1 == entries.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n}\n"); - s -} - -// ---------------------------------------------------------------------------- -// Injection: apply the glossary consistently across all name mirrors -// ---------------------------------------------------------------------------- - -/// Parse a names-glossary JSON into a `source -> text` map, keeping only entries whose `text` -/// actually differs from `source`. An untranslated entry is a no-op everywhere. -fn parse_name_map(json: &str) -> Result, String> { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let arr = root - .get("names") - .and_then(Value::as_array) - .ok_or("names JSON has no `names` array")?; - let mut map = HashMap::new(); - for e in arr { - let source = e.get("source").and_then(Value::as_str).unwrap_or(""); - let text = e.get("text").and_then(Value::as_str).unwrap_or(source); - if source.is_empty() || text == source { - continue; - } - map.insert(source.to_string(), text.to_string()); - } - Ok(map) -} - -/// Apply a name translation to a single `WStr` cell when it currently equals a mapped source. -/// Reuses [`write_translation`] so the control-code, drift, and encoding guards are identical to -/// the player-text path. The cell's current decoded value is the drift base, so an -/// already-translated or unrelated cell is left byte-exact and counted as `drifted`. -fn apply_to_cell( - cell: &mut WStr, - map: &HashMap, - utf8: bool, - opts: &InjectOptions, - stats: &mut InjectStats, - locator: &dyn Fn() -> String, -) { - let cur = decode_wstr(cell, utf8); - let Some(text) = map.get(&cur) else { - return; - }; - // `cur` is the source (it equals the map key) and `text` differs, since parse_name_map - // dropped no-ops. This is always a real edit attempt. The guards decide if it lands. - write_translation(cell, &cur, text, utf8, opts, stats, locator); -} - -/// Apply the project name glossary across a whole data dir, consistently. The same `source -> -/// text` is written to (a) every stored row name, (b) every name/display cell value, and (c) -/// every by-name command reference (`cid250/252` str2, `cid251` str1, `cid112` strings) that -/// currently equals a source. Every write is drift, control-code, and encoding guarded via the -/// shared [`write_translation`]. Untouched cells keep their exact bytes, so each file -/// re-serializes byte-exact except for the translated names. -/// -/// `dbs`, `ces`, and `maps` are the mutable parsed files of the dir, in any order. The caller -/// writes them back out and re-parses. Returns the aggregate [`InjectStats`]. -pub fn inject_names( - names_json: &str, - dbs: &mut [Database], - ces: &mut [&mut CommonEventsFile], - maps: &mut [Map], - opts: &InjectOptions, - glossary: &HashMap, -) -> Result { - let map = parse_name_map(names_json)?; - let mut stats = InjectStats::default(); - if map.is_empty() { - return Ok(stats); - } - - // (a) and (b) Databases: stored row names and name/display cell values. - for (di, db) in dbs.iter_mut().enumerate() { - let utf8 = db.utf8; - for (ti, t) in db.types.iter_mut().enumerate() { - // Recompute the keep-set on an immutable borrow before mutating rows. - let fields = name_fields(t, utf8, glossary); - for (ri, row) in t.data.iter_mut().enumerate() { - // (a) Row name. - apply_to_cell(&mut row.name, &map, utf8, opts, &mut stats, &|| { - format!("db {di} type {ti} row {ri} name") - }); - // (b) Name/display cell values. - for (fi, slot, tb, _) in &fields { - // Re-apply the per-cell asset-path exclusion. A folder-picker path is never a - // name, even if its exact text somehow collided with a mapped source. - if *tb == 1 { - let cur = row - .string_values - .get(*slot) - .map(|w| decode_wstr(w, utf8)) - .unwrap_or_default(); - if value_is_asset_path(&cur) { - continue; - } - } - if let Some(cell) = row.string_values.get_mut(*slot) { - apply_to_cell(cell, &map, utf8, opts, &mut stats, &|| { - format!("db {di} type {ti} row {ri} field {fi}") - }); - } - } - } - } - } - - // (c) By-name command references in common events. - for (ei, ce) in ces.iter_mut().enumerate() { - let utf8 = ce.utf8; - for (evi, ev) in ce.events.iter_mut().enumerate() { - apply_dbrefs(&mut ev.commands, &map, utf8, opts, &mut stats, &|ci, si| { - format!("ce {ei} event {evi} cmd {ci} str {si}") - }); - } - } - // (c) By-name command references in maps. - for (mi, map_file) in maps.iter_mut().enumerate() { - let utf8 = map_file.utf8; - for (evi, ev) in map_file.events.iter_mut().enumerate() { - for (pi, page) in ev.pages.iter_mut().enumerate() { - apply_dbrefs(&mut page.commands, &map, utf8, opts, &mut stats, &|ci, si| { - format!("map {mi} event {evi} page {pi} cmd {ci} str {si}") - }); - } - } - } - - Ok(stats) -} - -/// Apply the name map to the by-name reference strings of a command list. -fn apply_dbrefs( - cmds: &mut [RawCommand], - map: &HashMap, - utf8: bool, - opts: &InjectOptions, - stats: &mut InjectStats, - locator: &dyn Fn(usize, usize) -> String, -) { - for (ci, cmd) in cmds.iter_mut().enumerate() { - for si in dbref_str_indices(cmd) { - if let Some(cell) = cmd.str_args.get_mut(si) { - apply_to_cell(cell, map, utf8, opts, stats, &|| locator(ci, si)); - } - } - } -} - -fn jstr(s: &str) -> String { - let mut out = String::with_capacity(s.len() + 2); - out.push('"'); - for c in s.chars() { - match c { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if (c as u32) < 0x20 => { - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), - } - } - out.push('"'); - out -} - -// ---------------------------------------------------------------------------- -// Name-consistency conflict check (across translation JSONs) -// ---------------------------------------------------------------------------- - -/// One detected name-translation conflict: a single glossary name source that was given two or -/// more distinct non-identity translations across the loaded translation files. This is the exact -/// hazard the disjoint-ownership split prevents at extraction time. The check catches a residual -/// conflict that slipped in by hand, such as an old db-strings edit kept alongside the glossary. -pub struct Conflict { - /// The source name string (an exact, full-string `names.json` entry). - pub source: String, - /// The divergent translations, each with the files that used it, sorted and deduped. - pub variants: Vec, -} - -/// One divergent translation of a conflicting source, with where it came from. -pub struct ConflictVariant { - pub text: String, - /// Names of the files that translated the source to this `text`, sorted and deduped. - pub files: Vec, -} - -/// Pull every `(source, text)` pair out of one parsed translation JSON, regardless of its shape. -/// The names, db, scenes, and gamedat documents all carry `source` and `text` leaves at various -/// depths. Recurse generically: any object that has both a string `source` and a string `text` is -/// a pair. This keeps the check robust to all the document kinds without re-encoding each schema. -fn collect_source_text_pairs(v: &Value, out: &mut Vec<(String, String)>) { - match v { - Value::Object(map) => { - if let (Some(Value::String(s)), Some(Value::String(t))) = - (map.get("source"), map.get("text")) - { - out.push((s.clone(), t.clone())); - } - for child in map.values() { - collect_source_text_pairs(child, out); - } - } - Value::Array(arr) => { - for child in arr { - collect_source_text_pairs(child, out); - } - } - _ => {} - } -} - -/// The set of glossary name sources in one parsed JSON: every `source` of a `names.json` document -/// (`kind == "names"`, the `names` array). A non-names document contributes none. A db-strings -/// description source is content, never a name, so it can never define the glossary-name set. -fn collect_name_sources(v: &Value, out: &mut BTreeSet) { - if v.get("kind").and_then(Value::as_str) == Some("names") { - if let Some(arr) = v.get("names").and_then(Value::as_array) { - for e in arr { - if let Some(s) = e.get("source").and_then(Value::as_str) { - if !s.is_empty() { - out.insert(s.to_string()); - } - } - } - } - } -} - -/// Check a set of loaded translation JSONs for name-translation conflicts: a glossary name (a -/// source that appears as a `names.json` entry) that is translated to two or more distinct -/// non-identity `text` values across the files. -/// -/// Semantics, deliberately strict: -/// * The glossary-name set is the union of every `names.json`'s `source` values. -/// * Across all files (names, db-strings, scenes, gamedat), every `(source, text)` pair whose -/// `source` is an exact full-string match for a glossary name and whose `text != source` (a -/// real edit) is collected. No substring matching. A name appearing inside a dialogue -/// sentence is a different `source` string and is not a conflict. -/// * A source with two or more distinct `text` values is a conflict. -/// -/// `files` is `(display-name, json-text)`. An unparseable file is skipped. Errors are ignored -/// here so one bad file cannot mask the rest. Returns the conflicts sorted by source for stable -/// output. -pub fn check_name_conflicts(files: &[(String, &str)]) -> Vec { - // Parse once, keeping (name, value) for the parseable files. - let parsed: Vec<(&str, Value)> = files - .iter() - .filter_map(|(name, json)| serde_json::from_str::(json).ok().map(|v| (name.as_str(), v))) - .collect(); - - // (1) The glossary-name set is every names.json source. - let mut name_sources: BTreeSet = BTreeSet::new(); - for (_, v) in &parsed { - collect_name_sources(v, &mut name_sources); - } - - // (2) source -> text -> set of files that used it. Only sources that are glossary names, only - // non-identity edits. BTreeMap and BTreeSet keep the output deterministic. - let mut by_source: BTreeMap>> = BTreeMap::new(); - for (name, v) in &parsed { - let mut pairs = Vec::new(); - collect_source_text_pairs(v, &mut pairs); - for (source, text) in pairs { - if source == text { - continue; // identity is untranslated, not a divergence. - } - if !name_sources.contains(&source) { - continue; // not a glossary name, or not an exact full-string match. - } - by_source - .entry(source) - .or_default() - .entry(text) - .or_default() - .insert((*name).to_string()); - } - } - - // (3) A source with two or more distinct translations is a conflict. - let mut conflicts = Vec::new(); - for (source, variants) in by_source { - if variants.len() < 2 { - continue; - } - let variants = variants - .into_iter() - .map(|(text, files)| ConflictVariant { - text, - files: files.into_iter().collect(), - }) - .collect(); - conflicts.push(Conflict { source, variants }); - } - conflicts -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn name_display_field_is_name_role_only() { - // The glossary owns name cells only (Role::Name), disjoint from the db-strings content path. - // The first string field is always a name, even with an empty or unknown label. - assert!(name_display_field(0, "Monster", "", "", true)); - assert!(name_display_field(0, "Item", "謎ラベル", "謎ラベル", true)); - // A recognised name field (not first) is kept: a 愛称 nickname or 呼び方 element name. - assert!(name_display_field(0, "Trainer", "愛称", "愛称", false)); - assert!(name_display_field(0, "Element", "呼び方", "呼び方", false)); - // `移動名` is a displayed location-banner name, so the glossary keeps it and the - // consistency machinery covers its by-name-key usage. - assert!(name_display_field(0, "Map", "日本語移動名", "Move Name", false)); - // A description/dialogue field is content, not a name. The db-strings path owns it, not - // the glossary, so descriptions stay independently translatable even on the first column. - assert!(!name_display_field(0, "Skill", "説明", "Description", false)); - assert!(!name_display_field(0, "Help", "説明", "Description", true)); - // An unlabeled or unrecognised non-first field defaults to content, so the glossary drops - // it. The per-file path owns it, which is safer than forcing glossary consistency on an - // unknown label. - assert!(!name_display_field(0, "Misc", "謎ラベル", "謎ラベル", false)); - // Genuinely-internal fields are never names. - assert!(!name_display_field(0, "Monster", "ファイル", "File", true)); - assert!(!name_display_field(0, "Skill", "戦闘背景画像", "Graphic", false)); - // Non-string type_byte never holds a string cell. - assert!(!name_display_field(2, "Monster", "名前", "Name", true)); - } - - #[test] - fn dbref_indices_match_prototype() { - let mk = |cid: u32, nstr: usize| RawCommand { - cid, - int_args: vec![], - indent: 0, - str_args: (0..nstr).map(|_| WStr::from("x")).collect(), - term: 0, - move_route: None, - v35_blob: None, - }; - assert_eq!(dbref_str_indices(&mk(250, 4)), vec![2]); - assert_eq!(dbref_str_indices(&mk(252, 4)), vec![2]); - assert_eq!(dbref_str_indices(&mk(251, 3)), vec![1]); - assert_eq!(dbref_str_indices(&mk(112, 3)), vec![0, 1, 2]); - assert!(dbref_str_indices(&mk(101, 2)).is_empty()); - } - - #[test] - fn parse_name_map_drops_noops() { - let json = r#"{"kind":"names","names":[ - {"source":"A","text":"Alpha","occurrences":3,"note":"T"}, - {"source":"B","text":"B","occurrences":1,"note":"T"}, - {"source":"","text":"x","occurrences":0,"note":""} - ]}"#; - let m = parse_name_map(json).unwrap(); - assert_eq!(m.get("A").map(String::as_str), Some("Alpha")); - assert!(!m.contains_key("B")); // text equals source - assert!(!m.contains_key("")); // empty source - assert_eq!(m.len(), 1); - } - - #[test] - fn names_json_is_deterministic_and_sorted() { - let entries = vec![ - NameEntry { source: "ゼニ".into(), occurrences: 2, note: "Item".into() }, - NameEntry { source: "あ".into(), occurrences: 5, note: "Monster".into() }, - ]; - let json = names_to_json(&entries); - assert!(json.contains("\"kind\": \"names\"")); - assert!(json.contains("\"occurrences\": 2")); - // Both sources present. Rendering is stable for a given input order. - assert!(json.contains("ゼニ") && json.contains("あ")); - assert_eq!(json, names_to_json(&entries)); - } - - #[test] - fn conflict_check_flags_divergent_name_only() { - // A glossary names.json defines the name set. A db-strings file and the names file each - // translate the same exact name differently, giving one conflict. A description with two - // values is not a conflict since it is not a glossary name. Identity (text==source) is ignored. - let names = r#"{"kind":"names","names":[ - {"source":"ブルノエール","text":"Brunoir","occurrences":3,"note":"Monster"}, - {"source":"毒","text":"Poison","occurrences":1,"note":"Status"} - ]}"#; - let db = r#"{"kind":"db","groups":[{"type":0,"typeName":"Monster","lines":[ - {"row":0,"field":0,"rowName":"x","fieldName":"Name","source":"ブルノエール","text":"Brunoire"}, - {"row":1,"field":3,"rowName":"x","fieldName":"Description","source":"説明テキスト","text":"Desc A"} - ]}]}"#; - let scenes = r#"{"kind":"map","scenes":[{"event":1,"lines":[ - {"cmd":0,"str":0,"speaker":"NPC","speaker_src":"x","source":"説明テキスト","text":"Desc B"}, - {"cmd":1,"str":0,"speaker":"UI","speaker_src":"x","source":"毒","text":"Poison"} - ]}]}"#; - let files = vec![ - ("names.json".to_string(), names), - ("DataBase.db.json".to_string(), db), - ("map.scenes.json".to_string(), scenes), - ]; - let conflicts = check_name_conflicts(&files); - // Exactly one conflict: ブルノエール to {Brunoir, Brunoire}. 説明テキスト diverges too but is - // not a glossary name, so it is ignored. 毒 to Poison is consistent, identical everywhere. - assert_eq!(conflicts.len(), 1, "only the divergent NAME is a conflict"); - let c = &conflicts[0]; - assert_eq!(c.source, "ブルノエール"); - let texts: Vec<&str> = c.variants.iter().map(|v| v.text.as_str()).collect(); - assert!(texts.contains(&"Brunoir") && texts.contains(&"Brunoire")); - assert_eq!(c.variants.len(), 2); - } - - #[test] - fn conflict_check_consistent_is_empty() { - // Same name translated identically everywhere means no conflict. - let names = r#"{"kind":"names","names":[{"source":"剣","text":"Sword","occurrences":2,"note":"Item"}]}"#; - let db = r#"{"kind":"db","groups":[{"type":0,"typeName":"Item","lines":[ - {"row":0,"field":0,"rowName":"x","fieldName":"Name","source":"剣","text":"Sword"} - ]}]}"#; - let files = vec![ - ("names.json".to_string(), names), - ("DataBase.db.json".to_string(), db), - ]; - assert!(check_name_conflicts(&files).is_empty()); - } - - #[test] - fn conflict_check_substring_is_not_a_conflict() { - // A name appearing inside a dialogue sentence is a different full-string source, so two - // different sentence translations are not a name conflict. No substring matching. - let names = r#"{"kind":"names","names":[{"source":"剣","text":"Sword","occurrences":1,"note":"Item"}]}"#; - let scenes = r#"{"kind":"map","scenes":[{"event":1,"lines":[ - {"cmd":0,"str":0,"speaker":"x","speaker_src":"x","source":"剣を手に入れた","text":"Got a sword"}, - {"cmd":1,"str":0,"speaker":"x","speaker_src":"x","source":"剣を装備した","text":"Equipped a blade"} - ]}]}"#; - let files = vec![ - ("names.json".to_string(), names), - ("map.scenes.json".to_string(), scenes), - ]; - assert!( - check_name_conflicts(&files).is_empty(), - "a name inside a sentence is not an exact full-string match" - ); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/save.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/save.rs deleted file mode 100644 index a510346..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/save.rs +++ /dev/null @@ -1,630 +0,0 @@ -//! Wolf RPG `.sav` auto-update. Rewrites the baked game title and refreshes the baked -//! player-facing strings so a pre-translation save loads cleanly in the translated build. -//! -//! A `.sav` bakes in two things that go stale after a fan translation: -//! 1. The game identity/title. The translated build compares it against its own `Game.dat` -//! title and rejects a mismatch ("trying to load from another game"). -//! 2. Player-facing strings (item, skill, and mode names shown on the save-load screen) that -//! still read in the original language. -//! -//! [`update_save`] decrypts the save with [`wolf_core::codec::wolfsave`], optionally splices in a -//! new title, replaces every baked length-prefixed string that exactly matches a translation -//! source, then re-encrypts. It reuses the same `source -> target` translation map the rest of -//! the pipeline applies to the game itself, so the save stays consistent with the translated -//! data files. -//! -//! ## Save plaintext layout used here -//! After the outer decrypt the buffer is plaintext. Byte `6 == 0x55` marks UTF-8 strings, -//! otherwise Shift-JIS. Byte `0x14 == 0x19` marks a valid, parseable save. The title follows at -//! `0x15` as a `u16` little-endian byte-length (counting a trailing NUL) plus that many bytes. -//! The body further contains many `[u32 le byteLen][bytes][NUL]` records (the u32 counts the -//! NUL) for the variable database and baked strings. -//! -//! ## GamePro Pro (marker-3) saves -//! If the standard decrypt does not yield a valid save (`byte[0x14] != 0x19`) but the buffer is a -//! GamePro Pro (marker-3) save (see [`save_pro`](crate::save_pro)), [`update_save`] decrypts the -//! Pro inner, applies the same title-fix and baked-string refresh to it, then re-encrypts with -//! [`save_pro::encrypt_pro`]. In the inner the `0x19` marker sits at offset 0, the title `u16` -//! length at `inner[1..3]`, and the title bytes at `inner[3..]`. Only a buffer that is neither a -//! standard save nor a detectable Pro save is refused with an `Err`. - -use std::collections::HashMap; - -use wolf_core::codec::wolfsave; - -use crate::save_pro; - -/// Marker byte (`0x19`) of a valid, parseable save. The title record follows immediately after. -const VALID_MARKER: u8 = 0x19; -/// Marker offset for a standard save's plaintext. The body marker lives at `0x14`. -const STD_MARKER_OFFSET: usize = wolfsave::START_OFFSET; // 0x14 -/// Marker offset for a Pro inner save. The `0x19` marker lives at offset 0. -const PRO_MARKER_OFFSET: usize = 0; -/// Decrypted `byte[6] == 0x55` means strings are UTF-8, otherwise Shift-JIS. Only meaningful for -/// the standard plaintext. The Pro inner carries no such head and is always UTF-8. -const UTF8_FLAG_OFFSET: usize = 6; -const UTF8_FLAG_VALUE: u8 = 0x55; -/// Upper bound on a baked-string record's declared length. -const MAX_RECORD_LEN: usize = 1024 * 1024; - -/// Which save codec a buffer uses, as reported by [`inspect_save`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SaveFormat { - /// The standard Wolf RPG `.sav` (the `0x19` body marker at `0x14` after the outer decrypt). - Standard, - /// A GamePro Pro (marker-3) save (see [`save_pro`](crate::save_pro)). - GameProPro, - /// Neither a standard save nor a detectable GamePro Pro save. Editing is not supported. - Unsupported, -} - -impl SaveFormat { - /// A short human label for the format badge. - pub fn label(self) -> &'static str { - match self { - SaveFormat::Standard => "Standard", - SaveFormat::GameProPro => "GamePro Pro", - SaveFormat::Unsupported => "Unsupported", - } - } -} - -/// The read-only contents of a save, for listing or editing in a UI before an [`update_save`] -/// write. -/// -/// Produced by [`inspect_save`]: the detected [`format`](Self::format), the baked -/// [`title`](Self::title), the string [`encoding`](Self::encoding) (`"utf8"` or `"sjis"`), and -/// every baked length-prefixed [`strings`](Self::strings) record found in the body. These are the -/// same records [`update_save`] can replace. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SaveInfo { - /// Which codec the save uses. - pub format: SaveFormat, - /// The baked game title (empty for an `Unsupported` save). - pub title: String, - /// The save's string encoding: `"utf8"` or `"sjis"`. - pub encoding: &'static str, - /// Every baked, length-prefixed string in the body, in file order. Deduplication is the - /// caller's choice. Empty for an `Unsupported` save. - pub strings: Vec, -} - -/// What [`update_save`] did to one save file. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SaveUpdateStats { - /// The baked title was rewritten. - pub title_changed: bool, - /// How many baked length-prefixed strings were replaced via the translation map. - pub strings_replaced: usize, - /// The save's string encoding: `"utf8"` or `"sjis"`. - pub encoding: &'static str, -} - -/// Update a single raw `.sav` buffer. Decrypt, optionally set a new title, refresh baked strings -/// from `strings` (encoding-aware), and re-encrypt. -/// -/// * `raw`: the on-disk encrypted save bytes. -/// * `new_title`: `Some(title)` to rewrite the baked title literally, `None` to leave it. -/// * `strings`: a `source -> target` map. Any baked record whose decoded text exactly equals a -/// `source` is replaced with the encoded `target`, re-prefixed. Entries where `source == -/// target` or either side is empty are inert. -/// -/// Returns the re-encrypted bytes plus [`SaveUpdateStats`], or `Err` if the save uses an -/// unsupported encryption (`byte[0x14] != 0x19` after decrypt) or is too small to be a valid -/// save. -pub fn update_save( - raw: &[u8], - new_title: Option<&str>, - strings: &HashMap, -) -> Result<(Vec, SaveUpdateStats), String> { - let mut plain = wolfsave::decrypt(raw); - - // Standard save: the body marker `0x19` is at 0x14, with the title length field after it. - if plain.len() > STD_MARKER_OFFSET + 2 && plain[STD_MARKER_OFFSET] == VALID_MARKER { - let utf8 = plain.get(UTF8_FLAG_OFFSET) == Some(&UTF8_FLAG_VALUE); - let title_changed = match new_title { - Some(t) => set_title(&mut plain, STD_MARKER_OFFSET, t, utf8)?, - None => false, - }; - let strings_replaced = replace_baked_strings(&mut plain, strings, utf8); - let out = wolfsave::encrypt(&plain); - return Ok(( - out, - SaveUpdateStats { - title_changed, - strings_replaced, - encoding: if utf8 { "utf8" } else { "sjis" }, - }, - )); - } - - // GamePro Pro (marker-3) save: the standard decrypt produced no valid save, so try the Pro - // path. The Pro inner starts with the `0x19` marker at offset 0 and is always UTF-8. - if save_pro::is_pro_save(raw) { - let mut inner = save_pro::decrypt_pro(raw)?; - let utf8 = true; - let title_changed = match new_title { - Some(t) => set_title(&mut inner, PRO_MARKER_OFFSET, t, utf8)?, - None => false, - }; - let strings_replaced = replace_baked_strings(&mut inner, strings, utf8); - let out = save_pro::encrypt_pro(&inner, &raw[..STD_MARKER_OFFSET])?; - return Ok(( - out, - SaveUpdateStats { - title_changed, - strings_replaced, - encoding: "utf8", - }, - )); - } - - Err( - "unsupported save encryption (not a standard 0x19 save and not a detectable GamePro Pro \ - marker-3 save); skipping to avoid corruption" - .to_string(), - ) -} - -/// Inspect a raw `.sav` buffer read-only. Detects its [`SaveFormat`], decodes the baked title, -/// and lists every baked length-prefixed string in the body. Reuses the same decrypt, title-read, -/// and record-scan as [`update_save`], so what `inspect_save` lists is exactly what `update_save` -/// can rewrite. -/// -/// Never panics. A save whose format is handled returns `Ok(SaveInfo{ format: -/// Standard|GameProPro, .. })`. A buffer that is neither a standard `0x19` save nor a detectable -/// GamePro Pro save returns `Ok(SaveInfo{ format: Unsupported, title: "", encoding: "utf8", -/// strings: [] })` rather than an error, so a UI can show a clear "not supported" state. The only -/// `Err` is a Pro-marker buffer whose inner decrypt itself fails (a corrupt or truncated Pro -/// save). -pub fn inspect_save(raw: &[u8]) -> Result { - let plain = wolfsave::decrypt(raw); - - // Standard save: the body marker `0x19` is at 0x14. - if plain.len() > STD_MARKER_OFFSET + 2 && plain[STD_MARKER_OFFSET] == VALID_MARKER { - let utf8 = plain.get(UTF8_FLAG_OFFSET) == Some(&UTF8_FLAG_VALUE); - let title = read_title_at(&plain, STD_MARKER_OFFSET, utf8).unwrap_or_default(); - let strings = collect_baked_strings(&plain, utf8); - return Ok(SaveInfo { - format: SaveFormat::Standard, - title, - encoding: if utf8 { "utf8" } else { "sjis" }, - strings, - }); - } - - // GamePro Pro (marker-3) save. The Pro inner starts with `0x19` at offset 0 and is always UTF-8. - if save_pro::is_pro_save(raw) { - let inner = save_pro::decrypt_pro(raw)?; - let utf8 = true; - let title = read_title_at(&inner, PRO_MARKER_OFFSET, utf8).unwrap_or_default(); - let strings = collect_baked_strings(&inner, utf8); - return Ok(SaveInfo { - format: SaveFormat::GameProPro, - title, - encoding: "utf8", - strings, - }); - } - - // Neither codec handled it. Report Unsupported rather than erroring, so the UI can disable - // editing with a clear note instead of failing. - Ok(SaveInfo { - format: SaveFormat::Unsupported, - title: String::new(), - encoding: "utf8", - strings: Vec::new(), - }) -} - -/// Read the baked title of a standard decrypted save (marker at `0x14`). Returns the decoded -/// title with its trailing NUL stripped, or `None` for an unsupported or malformed buffer. Used -/// by tests and callers that want to confirm a title write landed. For a Pro inner, use -/// [`read_title_at`]`(inner, 0)`. -pub fn read_title(plain: &[u8]) -> Option { - let utf8 = plain.get(UTF8_FLAG_OFFSET) == Some(&UTF8_FLAG_VALUE); - read_title_at(plain, STD_MARKER_OFFSET, utf8) -} - -/// Read the baked title given the marker offset (`0x14` for a standard plaintext, `0` for a Pro -/// inner) and the file's string encoding. The title length `u16` sits at `marker+1` and the title -/// bytes follow at `marker+3`. Returns the decoded title with its trailing NUL stripped. -pub fn read_title_at(plain: &[u8], marker_offset: usize, utf8: bool) -> Option { - let len_off = marker_offset + 1; - if plain.len() <= len_off + 1 || plain.get(marker_offset) != Some(&VALID_MARKER) { - return None; - } - let size = u16::from_le_bytes([plain[len_off], plain[len_off + 1]]) as usize; - let start = len_off + 2; - let end = start.checked_add(size)?; - if end > plain.len() { - return None; - } - let body = plain[start..end].strip_suffix(&[0u8])?; // the length counts a trailing NUL. - Some(decode(body, utf8)) -} - -/// Splice a new title into a decrypted buffer, shifting the remainder. `marker_offset` is `0x14` -/// for a standard plaintext or `0` for a Pro inner. The title length `u16` lives at `marker+1` -/// and the title bytes at `marker+3`. The on-disk form is `u16(len(bytes_with_nul)) + -/// bytes_with_nul` in the file encoding. Returns `Ok(true)` on success, `Err` if the new title is -/// not representable in the file's encoding. -fn set_title( - plain: &mut Vec, - marker_offset: usize, - title: &str, - utf8: bool, -) -> Result { - let len_off = marker_offset + 1; - let old_size = u16::from_le_bytes([plain[len_off], plain[len_off + 1]]) as usize; - let old_start = len_off + 2; - let old_end = old_start - .checked_add(old_size) - .filter(|&e| e <= plain.len()) - .ok_or_else(|| "save title length runs past end of buffer".to_string())?; - - let mut title_bytes = encode(title, utf8).ok_or_else(|| { - format!( - "new title not representable in {}: {title:?}", - if utf8 { "UTF-8" } else { "Shift-JIS" } - ) - })?; - title_bytes.push(0); // the stored length counts the trailing NUL. - let len: u16 = title_bytes - .len() - .try_into() - .map_err(|_| "new title is too long to encode as a u16 length".to_string())?; - - let mut replacement = Vec::with_capacity(2 + title_bytes.len()); - replacement.extend_from_slice(&len.to_le_bytes()); - replacement.extend_from_slice(&title_bytes); - // Splice from the old length field, inclusive, through the old title bytes, shifting the rest. - plain.splice(len_off..old_end, replacement); - Ok(true) -} - -/// Try to decode the `[u32 le byteLen][bytes][NUL]` record starting at `offset`. Returns -/// `Some((text, end))` when `offset` begins a valid record, else `None`. The tuple is the -/// cleanly-decoded payload text with NUL stripped and the byte index one past the record. A -/// record is valid when its declared length is `0 < len <= MAX_RECORD_LEN`, the record fits the -/// buffer, the payload ends in exactly one NUL with none interior, and it decodes cleanly in the -/// file encoding. This is the shared scan step used by both the read-only [`inspect_save`] walk -/// and the replacing [`replace_baked_strings`] walk, so the two never disagree about what counts -/// as a string record. -fn read_record_at(plain: &[u8], offset: usize, utf8: bool) -> Option<(String, usize)> { - if offset + 5 > plain.len() { - return None; - } - let size = u32::from_le_bytes([ - plain[offset], - plain[offset + 1], - plain[offset + 2], - plain[offset + 3], - ]) as usize; - let end = offset + 4 + size; - if size == 0 || size > MAX_RECORD_LEN || end > plain.len() { - return None; - } - let payload = &plain[offset + 4..end]; - // Exactly one trailing NUL, none interior. - if payload.last() != Some(&0) || payload[..payload.len() - 1].contains(&0) { - return None; - } - let text = decode_strict(&payload[..payload.len() - 1], utf8)?; - Some((text, end)) -} - -/// Collect every baked length-prefixed string in the decrypted buffer, in file order, using the -/// same record scan [`replace_baked_strings`] uses. Drives [`inspect_save`]'s string list. -fn collect_baked_strings(plain: &[u8], utf8: bool) -> Vec { - let mut out = Vec::new(); - let mut offset = 0usize; - while offset + 5 <= plain.len() { - if let Some((text, end)) = read_record_at(plain, offset, utf8) { - out.push(text); - offset = end; - } else { - offset += 1; - } - } - out -} - -/// Scan the decrypted buffer for `[u32 le byteLen][bytes][NUL]` records and replace any whose -/// decoded text exactly matches a translation `source` with the encoded `target`, re-prefixed. -/// -/// Uses the shared [`read_record_at`] scan step. The walk continues past each replacement using -/// the new record's length, so adjacent records are still seen. Returns the number of records -/// replaced. -fn replace_baked_strings( - plain: &mut Vec, - strings: &HashMap, - utf8: bool, -) -> usize { - // Pre-encode the replacement records once. Skip no-op, empty, or unrepresentable entries. - let records: HashMap> = strings - .iter() - .filter(|(s, t)| !s.is_empty() && !t.is_empty() && s != t) - .filter_map(|(s, t)| encode_record(t, utf8).map(|rec| (s.clone(), rec))) - .collect(); - if records.is_empty() { - return 0; - } - - let mut count = 0usize; - let mut offset = 0usize; - while offset + 5 <= plain.len() { - match read_record_at(plain, offset, utf8) { - Some((text, end)) => { - if let Some(replacement) = records.get(&text) { - let rep_len = replacement.len(); - plain.splice(offset..end, replacement.iter().copied()); - offset += rep_len; - count += 1; - } else { - // Valid record but no translation hit. Skip the whole record. - offset = end; - } - } - None => offset += 1, - } - } - count -} - -/// Build a length-prefixed record for `text`: `u32 le (bytes+NUL len) + bytes + NUL`. Returns -/// `None` if `text` is not representable in the file encoding. -fn encode_record(text: &str, utf8: bool) -> Option> { - let mut bytes = encode(text, utf8)?; - bytes.push(0); - let len = u32::try_from(bytes.len()).ok()?; - let mut rec = Vec::with_capacity(4 + bytes.len()); - rec.extend_from_slice(&len.to_le_bytes()); - rec.extend_from_slice(&bytes); - Some(rec) -} - -/// Encode `text` into the file encoding (UTF-8 as-is, or Shift-JIS/CP932). `None` if a character -/// is not representable in Shift-JIS. -fn encode(text: &str, utf8: bool) -> Option> { - if utf8 { - Some(text.as_bytes().to_vec()) - } else { - let (bytes, _, had_err) = encoding_rs::SHIFT_JIS.encode(text); - (!had_err).then(|| bytes.into_owned()) - } -} - -/// Lossy decode for display (used to read a title back). -fn decode(bytes: &[u8], utf8: bool) -> String { - if utf8 { - String::from_utf8_lossy(bytes).into_owned() - } else { - encoding_rs::SHIFT_JIS.decode(bytes).0.into_owned() - } -} - -/// Strict decode used by the record scanner. `None` if the bytes do not decode cleanly in the -/// file encoding, so a binary record is never mistaken for a translatable string. -fn decode_strict(bytes: &[u8], utf8: bool) -> Option { - if utf8 { - std::str::from_utf8(bytes).ok().map(str::to_owned) - } else { - let (text, _, had_err) = encoding_rs::SHIFT_JIS.decode(bytes); - (!had_err).then(|| text.into_owned()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file is missing, so the fixture-backed tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// Assemble a minimal synthetic decrypted save: a 0x14-byte head with the markers set, a - /// title record at 0x15, then `body` such as baked string records. Encoding is UTF-8. - fn build_plain(title: &str, body: &[u8]) -> Vec { - let mut p = vec![0u8; wolfsave::START_OFFSET]; - p[UTF8_FLAG_OFFSET] = UTF8_FLAG_VALUE; // UTF-8 flag - p.push(VALID_MARKER); // byte 0x14, valid-save marker - // Title record at 0x15. - let mut tb = title.as_bytes().to_vec(); - tb.push(0); - p.extend_from_slice(&(tb.len() as u16).to_le_bytes()); - p.extend_from_slice(&tb); - p.extend_from_slice(body); - p - } - - fn record(text: &str) -> Vec { - encode_record(text, true).unwrap() - } - - #[test] - fn title_read_back_after_set() { - let mut p = build_plain("元のタイトル", &[]); - assert_eq!(read_title(&p).as_deref(), Some("元のタイトル")); - set_title(&mut p, STD_MARKER_OFFSET, "New Title", true).unwrap(); - assert_eq!(read_title(&p).as_deref(), Some("New Title")); - // Marker still intact after the length shift. - assert_eq!(p[STD_MARKER_OFFSET], VALID_MARKER); - } - - #[test] - fn baked_string_replacement_shifts_and_continues() { - // Two adjacent records. Only the first is translated. The scan must still see the second. - let mut body = record("こんにちは"); - body.extend_from_slice(&record("Keep me")); - let mut p = build_plain("T", &body); - - let mut map = HashMap::new(); - map.insert("こんにちは".to_string(), "Hello".to_string()); - let n = replace_baked_strings(&mut p, &map, true); - assert_eq!(n, 1); - - // The replaced record now decodes to the target. "Keep me" is untouched and still present. - // Re-scan for "Hello" by reading the record right after the title. - let len_off = STD_MARKER_OFFSET + 1; - let title_len = u16::from_le_bytes([p[len_off], p[len_off + 1]]) as usize; - let mut off = len_off + 2 + title_len; - let size = - u32::from_le_bytes([p[off], p[off + 1], p[off + 2], p[off + 3]]) as usize; - let payload = &p[off + 4..off + 4 + size]; - assert_eq!(&payload[..payload.len() - 1], b"Hello"); - off += 4 + size; - let size2 = - u32::from_le_bytes([p[off], p[off + 1], p[off + 2], p[off + 3]]) as usize; - let payload2 = &p[off + 4..off + 4 + size2]; - assert_eq!(&payload2[..payload2.len() - 1], b"Keep me"); - } - - #[test] - fn update_save_roundtrips_with_no_changes() { - let mut p = build_plain("チャンバーゲーム", &record("アイテム")); - // Give the synthetic plaintext a correct checksum so a no-op re-encrypt is byte-exact. - // A real save already has this. `update_save` recomputes byte 2 on every write. - wolfsave::fix_checksum(&mut p); - let raw = wolfsave::encrypt(&p); - let (out, stats) = update_save(&raw, None, &HashMap::new()).unwrap(); - assert!(!stats.title_changed); - assert_eq!(stats.strings_replaced, 0); - assert_eq!(stats.encoding, "utf8"); - // Decrypting the output reproduces the original plaintext. - assert_eq!(wolfsave::decrypt(&out), p); - } - - #[test] - fn update_save_rejects_unsupported() { - // byte[0x14] != 0x19 is unsupported. - let mut p = build_plain("T", &[]); - p[wolfsave::START_OFFSET] = 0x68; - let raw = wolfsave::encrypt(&p); - let err = update_save(&raw, Some("X"), &HashMap::new()).unwrap_err(); - assert!(err.contains("unsupported"), "got: {err}"); - } - - #[test] - fn update_save_sets_title_and_replaces() { - let p = build_plain("旧題", &record("ルビー")); - let raw = wolfsave::encrypt(&p); - let mut map = HashMap::new(); - map.insert("ルビー".to_string(), "Ruby".to_string()); - let (out, stats) = update_save(&raw, Some("Translated Title"), &map).unwrap(); - assert!(stats.title_changed); - assert_eq!(stats.strings_replaced, 1); - let plain = wolfsave::decrypt(&out); - assert_eq!(read_title(&plain).as_deref(), Some("Translated Title")); - } - - /// `inspect_save` on a synthetic standard save reports the format, encoding, title, and the - /// baked strings. `update_save` with an `{old->new}` map drawn from that list round-trips, so - /// a re-inspect shows the new string and title. - #[test] - fn inspect_then_update_round_trips_synthetic() { - let mut body = record("アイテム"); - body.extend_from_slice(&record("スキル")); - let p = build_plain("元のタイトル", &body); - let raw = wolfsave::encrypt(&p); - - let info = inspect_save(&raw).expect("inspect"); - assert_eq!(info.format, SaveFormat::Standard); - assert_eq!(info.encoding, "utf8"); - assert_eq!(info.title, "元のタイトル"); - assert!(info.strings.contains(&"アイテム".to_string())); - assert!(info.strings.contains(&"スキル".to_string())); - - // Build the {old->new} map from inspect's own list, then update the title and one string. - let original = info.strings[0].clone(); - let mut map = HashMap::new(); - map.insert(original.clone(), "Item".to_string()); - let (out, stats) = update_save(&raw, Some("New Title"), &map).unwrap(); - assert!(stats.title_changed); - assert_eq!(stats.strings_replaced, 1); - - // Re-inspect the written bytes. The new title and string are present, the old string is gone. - let re = inspect_save(&out).expect("re-inspect"); - assert_eq!(re.format, SaveFormat::Standard); - assert_eq!(re.title, "New Title"); - assert!(re.strings.contains(&"Item".to_string())); - assert!(!re.strings.contains(&original)); - } - - /// `inspect_save` returns `Unsupported` for a non-save buffer, never panicking or erroring. - #[test] - fn inspect_unsupported_buffer() { - // byte[0x14] != 0x19 and not a Pro save is Unsupported. - let mut p = build_plain("T", &[]); - p[wolfsave::START_OFFSET] = 0x68; - let raw = wolfsave::encrypt(&p); - let info = inspect_save(&raw).expect("inspect should not error"); - assert_eq!(info.format, SaveFormat::Unsupported); - assert!(info.title.is_empty()); - assert!(info.strings.is_empty()); - } - - /// On a real save fixture, skipped if absent, `inspect_save` returns a non-empty title and the - /// `update_save` map drawn from `inspect_save(...).strings` round-trips through a re-inspect. - #[test] - fn inspect_then_update_round_trips_real_save() { - let candidates = ["chamber/SaveData01.sav", "pachimon/SaveData01.sav"]; - let mut ran = 0; - for rel in candidates { - let Some(path) = test_data(rel) else { - continue; - }; - let Ok(raw) = std::fs::read(&path) else { - continue; - }; - let path = path.display(); - let info = match inspect_save(&raw) { - Ok(i) if i.format != SaveFormat::Unsupported => i, - _ => continue, - }; - ran += 1; - assert!(!info.title.is_empty(), "{path}: title should not be empty"); - - // Pick the first baked string that can re-encode in this save's encoding and rewrite it. - let new_title = format!("{} [T]", info.title); - let mut map = HashMap::new(); - let mut chosen: Option = None; - for s in &info.strings { - if s.is_empty() || s.contains('[') { - continue; - } - let edited = format!("{s} X"); - if encode(&edited, info.encoding == "utf8").is_some() { - map.insert(s.clone(), edited.clone()); - chosen = Some(edited); - break; - } - } - - let (out, stats) = - update_save(&raw, Some(&new_title), &map).expect("update real save"); - assert!(stats.title_changed); - assert_eq!(stats.encoding, info.encoding); - - let re = inspect_save(&out).expect("re-inspect real save"); - assert_eq!(re.format, info.format, "{path}: format must be preserved"); - assert_eq!(re.title, new_title, "{path}: new title should re-inspect"); - if let Some(edited) = chosen { - // The same baked text can appear in several save slots. All matching records are - // replaced, so assert at least one rather than exactly one. - assert!(stats.strings_replaced >= 1); - assert!( - re.strings.contains(&edited), - "{path}: the edited baked string should re-inspect" - ); - } - } - if ran == 0 { - eprintln!("skip inspect_then_update_round_trips_real_save: no save fixture present"); - } - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/save_pro.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/save_pro.rs deleted file mode 100644 index 57a990f..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/save_pro.rs +++ /dev/null @@ -1,345 +0,0 @@ -//! GamePro Pro (marker-3) `.sav` codec. The newer Wolf RPG "Pro" save encryption used by some -//! GamePro-Pro titles. Where the standard outer codec ([`wolf_core::codec::wolfsave`]) is a 3-pass -//! MSVCRT-`rand` XOR, the Pro path layers an MT19937-keyed XOR keystream, a self-inverse 20-byte -//! block swap, and an LZ4-compressed body, plus a checksum-derived 16-bit key the game validates -//! on load. -//! -//! Verified to decrypt a real GamePro-Pro save identically to the game and to round-trip -//! encrypt then decrypt. The algorithm is summarised inline below. -//! -//! ## On-disk layout -//! `[0x00..0x14] plaintext header` then the encrypted body -//! `[u32-LE uncompLen][u32-LE compLen][LZ4 block]`. The body, everything from `0x14`, is -//! enciphered. The decrypted inner save begins with the `0x19` title marker at offset 0, not at -//! `0x14` as in a standard save. The `u16` title length sits at `inner[1..3]` and the title bytes -//! follow at `inner[3..]`. -//! -//! ## Pipeline -//! * seed3 = `{file[0], file[1], file[5]}`. If all three are zero the body is plain, no cipher. -//! * MT seed = `combined = (s0<<16)|(s1<<8)|s2` folded through one xorshift32 round. -//! * padtable[128] = the low byte of each of the first 128 MT19937 outputs. -//! * block_swap = swap `file[0x16..0x2a]` with `file[0x5c..0x70]`, self-inverse. -//! * xor = `buf[off] ^= padtable[off & 0x7f]` for `off` in `0x14..len`. -//! * Decrypt: block_swap, then xor, then LZ4-decompress the framed body. -//! * Encrypt: LZ4-compress, frame, write key16 into the header, xor, then block_swap. - -use wolf_core::codec::lz4; - -/// First body offset. Bytes `0x00..0x14` (the header) are never enciphered. -const BODY_OFFSET: usize = 0x14; -/// The two 20-byte regions exchanged by [`block_swap`]: `file[0x16..0x2a]` and `file[0x5c..0x70]`. -const SWAP_A: usize = 0x16; -const SWAP_B: usize = 0x5c; -const SWAP_LEN: usize = 0x14; -/// `block_swap`, and therefore the whole Pro cipher, needs at least this many bytes. -const MIN_FILE_SIZE: usize = SWAP_B + SWAP_LEN; // 0x70 -/// The XOR pad table length. Indexed by `off & 0x7f`. -const PAD_LEN: usize = 128; -/// The decrypted inner save's valid-save marker, at inner offset 0. -pub const INNER_MARKER: u8 = 0x19; - -// --------------------------------------------------------------------------------------------- -// MT19937 + seeding -// --------------------------------------------------------------------------------------------- - -const N: usize = 624; -const M: usize = 397; -const MATRIX_A: u32 = 0x9908_b0df; -const UPPER_MASK: u32 = 0x8000_0000; -const LOWER_MASK: u32 = 0x7fff_ffff; - -/// Standard MT19937. Init constant 1812433253, temper masks 0x9d2c5680 and 0xefc60000. -struct Mt19937 { - mt: [u32; N], - idx: usize, -} - -impl Mt19937 { - fn new(seed: u32) -> Self { - let mut mt = [0u32; N]; - mt[0] = seed; - for i in 1..N { - mt[i] = 1_812_433_253u32 - .wrapping_mul(mt[i - 1] ^ (mt[i - 1] >> 30)) - .wrapping_add(i as u32); - } - Mt19937 { mt, idx: N } - } - - fn next_u32(&mut self) -> u32 { - if self.idx >= N { - for i in 0..N { - let y = (self.mt[i] & UPPER_MASK) | (self.mt[(i + 1) % N] & LOWER_MASK); - let mut next = self.mt[(i + M) % N] ^ (y >> 1); - if y & 1 != 0 { - next ^= MATRIX_A; - } - self.mt[i] = next; - } - self.idx = 0; - } - let mut y = self.mt[self.idx]; - self.idx += 1; - y ^= y >> 11; - y ^= (y << 7) & 0x9d2c_5680; - y ^= (y << 15) & 0xefc6_0000; - y ^= y >> 18; - y - } -} - -/// Derive the MT seed from the three header seed bytes. Assemble big-endian -/// `combined = (s0<<16)|(s1<<8)|s2`, then run one xorshift32 round (`x^=x<<13`, `x^=x>>17`, -/// `x^=x<<5`). -fn mt_seed_from_bytes(s0: u8, s1: u8, s2: u8) -> u32 { - let mut x = ((s0 as u32) << 16) | ((s1 as u32) << 8) | (s2 as u32); - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - x -} - -/// Build the 128-byte XOR pad table from the low byte of each of the first 128 MT19937 outputs. -fn build_padtable(s0: u8, s1: u8, s2: u8) -> [u8; PAD_LEN] { - let mut mt = Mt19937::new(mt_seed_from_bytes(s0, s1, s2)); - let mut pad = [0u8; PAD_LEN]; - for slot in pad.iter_mut() { - *slot = (mt.next_u32() & 0xff) as u8; - } - pad -} - -// --------------------------------------------------------------------------------------------- -// block swap and xor cipher (both operate in place) -// --------------------------------------------------------------------------------------------- - -/// Swap the two 20-byte blocks `buf[0x16..0x2a]` and `buf[0x5c..0x70]`. Self-inverse, so the same -/// call appears in both the decrypt and encrypt pipelines. The caller guarantees `buf.len() >= -/// 0x70`. -fn block_swap(buf: &mut [u8]) { - debug_assert!(buf.len() >= MIN_FILE_SIZE); - for i in 0..SWAP_LEN { - buf.swap(SWAP_A + i, SWAP_B + i); - } -} - -/// XOR the body (`buf[0x14..]`) with the repeating 128-byte pad, indexed by `off & 0x7f`. -fn xor_cipher(buf: &mut [u8], pad: &[u8; PAD_LEN]) { - for (off, byte) in buf.iter_mut().enumerate().skip(BODY_OFFSET) { - *byte ^= pad[off & (PAD_LEN - 1)]; - } -} - -// --------------------------------------------------------------------------------------------- -// key16 (the game validates this on marker-3 load, and a mismatch fails the load) -// --------------------------------------------------------------------------------------------- - -/// Fold the decompressed inner save into the 16-bit key the game stores at -/// `header[7] | header[4]<<8`. -/// -/// `v5` is the XOR of the 16-byte-aligned prefix `inner[0 .. 16*(len/16)]`. `v13` is the XOR of -/// every inner byte. Both are taken as 16-bit values with a zero high byte. `a4 = header[9]`, and -/// `a4 % 3` selects one of three polynomials. -pub fn key16(inner: &[u8], a4: u8) -> u16 { - let aligned = (inner.len() / 16) * 16; - let mut v5: u32 = 0; - for &b in &inner[..aligned] { - v5 ^= b as u32; - } - let mut v13 = v5; - for &b in &inner[aligned..] { - v13 ^= b as u32; - } - v5 &= 0xffff; - v13 &= 0xffff; - - let a4 = a4 as u32; - let na = !a4; // ~a4 over u32 - let key = match a4 % 3 { - 0 => 7u32.wrapping_mul(v13 ^ a4).wrapping_add(17), - 1 => a4.wrapping_add(v13 ^ (((16 * a4) | (a4 >> 4)) ^ 0x55)), - _ => a4 ^ (3u32.wrapping_mul((v5 ^ na).wrapping_add(51)) & 0xffff), - }; - (key & 0xffff) as u16 -} - -// --------------------------------------------------------------------------------------------- -// public codec API -// --------------------------------------------------------------------------------------------- - -/// Decrypt a raw Pro (marker-3) `.sav` buffer into its inner save: the LZ4-decompressed -/// plaintext, beginning with the `0x19` title marker at offset 0. -/// -/// Returns `Err` if the buffer is too small for the Pro cipher, the framed lengths are -/// inconsistent, or LZ4 decompression fails. If the three seed bytes are all zero the body is -/// stored plain and the framed body is decompressed directly. -pub fn decrypt_pro(raw: &[u8]) -> Result, String> { - if raw.len() < BODY_OFFSET { - return Err(format!( - "pro save too small ({} bytes; need at least {BODY_OFFSET})", - raw.len() - )); - } - let (s0, s1, s2) = (raw[0], raw[1], raw[5]); - - let mut buf = raw.to_vec(); - // No cipher when the seed bytes are all zero. The body is stored plain. The block_swap regions - // only exist for files of at least 0x70 bytes, and an enciphered file is always larger. - if (s0 | s1 | s2) != 0 { - if raw.len() < MIN_FILE_SIZE { - return Err(format!( - "pro save too small for the block swap ({} bytes; need at least {MIN_FILE_SIZE})", - raw.len() - )); - } - let pad = build_padtable(s0, s1, s2); - block_swap(&mut buf); // unscramble. - xor_cipher(&mut buf, &pad); // xor-decrypt the body. - } - - decompress_framed(&buf) -} - -/// Decompress the framed body `[u32 uncompLen][u32 compLen][LZ4 block]` at offset `0x14`. -fn decompress_framed(buf: &[u8]) -> Result, String> { - let frame = buf - .get(BODY_OFFSET..) - .ok_or_else(|| "pro save: missing body".to_string())?; - if frame.len() < 8 { - return Err("pro save: body shorter than the 8-byte LZ4 frame header".to_string()); - } - let uncomp_len = u32::from_le_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize; - let comp_len = u32::from_le_bytes([frame[4], frame[5], frame[6], frame[7]]) as usize; - let block = frame - .get(8..8 + comp_len) - .ok_or_else(|| "pro save: compressed body runs past end of buffer".to_string())?; - lz4::decompress_block(block, uncomp_len).map_err(|e| format!("pro save: LZ4 decode failed: {e}")) -} - -/// Re-encrypt an inner save back into a raw Pro (marker-3) `.sav`, reusing the original 20-byte -/// `header20` (seed bytes, format markers, `a4`, and so on) and stamping in the freshly computed -/// `key16` so the game's load-time validation passes. -/// -/// `header20` must be at least 20 bytes. The first 20 are used. This is the inverse of -/// [`decrypt_pro`]: LZ4-compress the inner, frame it, write `key16` into `header[7]`/`header[4]`, -/// xor-encrypt the body, then block_swap. -pub fn encrypt_pro(inner: &[u8], header20: &[u8]) -> Result, String> { - if header20.len() < BODY_OFFSET { - return Err(format!( - "pro save: header is {} bytes; need at least {BODY_OFFSET}", - header20.len() - )); - } - let mut header = header20[..BODY_OFFSET].to_vec(); - let (s0, s1, s2) = (header[0], header[1], header[5]); - let a4 = header[9]; - - // The key the game validates folds the decompressed inner. - let key = key16(inner, a4); - header[7] = (key & 0xff) as u8; - header[4] = ((key >> 8) & 0xff) as u8; - - // LZ4-compress the inner and build the framed body [u32 uncompLen][u32 compLen][block]. - let block = lz4::compress_block(inner); - let mut buf = Vec::with_capacity(BODY_OFFSET + 8 + block.len()); - buf.extend_from_slice(&header); - buf.extend_from_slice(&(inner.len() as u32).to_le_bytes()); - buf.extend_from_slice(&(block.len() as u32).to_le_bytes()); - buf.extend_from_slice(&block); - - // Inverse cipher order: xor-encrypt the body, then block_swap. The block_swap regions only - // exist when the file is at least 0x70 bytes. A seedless save with no cipher has no such floor. - if (s0 | s1 | s2) != 0 { - if buf.len() < MIN_FILE_SIZE { - return Err(format!( - "pro save: enciphered output too small ({} bytes; block_swap needs at least \ - {MIN_FILE_SIZE})", - buf.len() - )); - } - let pad = build_padtable(s0, s1, s2); - xor_cipher(&mut buf, &pad); - block_swap(&mut buf); - } - Ok(buf) -} - -/// Detect a Pro (marker-3) save. Attempts the Pro decrypt and confirms the inner save begins with -/// the `0x19` title marker. It does not trust a header magic byte alone, so it returns `false` for -/// standard saves and for malformed input. -pub fn is_pro_save(raw: &[u8]) -> bool { - matches!(decrypt_pro(raw), Ok(inner) if inner.first() == Some(&INNER_MARKER)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mt_seed_matches_reference() { - // {0x05, 0xb2, 0xdb} are the real GamePro-Pro seed bytes. - let seed = mt_seed_from_bytes(0x05, 0xb2, 0xdb); - // combined = 0x05b2db, then one xorshift32 round. - let mut x = 0x05b2dbu32; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - assert_eq!(seed, x); - } - - #[test] - fn block_swap_is_self_inverse() { - let mut buf: Vec = (0..0x80u8).collect(); - let orig = buf.clone(); - block_swap(&mut buf); - assert_ne!(buf, orig, "swap must change the buffer"); - block_swap(&mut buf); - assert_eq!(buf, orig, "swap is self-inverse"); - } - - #[test] - fn xor_cipher_is_self_inverse() { - let pad = build_padtable(1, 2, 3); - let mut buf: Vec = (0..200u8).collect(); - let orig = buf.clone(); - xor_cipher(&mut buf, &pad); - assert_ne!(&buf[BODY_OFFSET..], &orig[BODY_OFFSET..]); - assert_eq!(&buf[..BODY_OFFSET], &orig[..BODY_OFFSET], "head untouched"); - xor_cipher(&mut buf, &pad); - assert_eq!(buf, orig); - } - - #[test] - fn roundtrip_synthetic_inner() { - // A synthetic inner save, which must start with the 0x19 marker. - let mut inner = vec![INNER_MARKER, 0x05, 0x00, b'A', b'B', b'C', b'D', 0x00]; - inner.extend((0..2000u32).map(|i| (i % 251) as u8)); - - // A header with nonzero seeds so the cipher actually runs. - let mut header = vec![0u8; BODY_OFFSET]; - header[0] = 0x05; - header[1] = 0xb2; - header[5] = 0xdb; - header[9] = 0x32; - - let enc = encrypt_pro(&inner, &header).expect("encrypt"); - assert!(is_pro_save(&enc), "produced save must be detected as Pro"); - let dec = decrypt_pro(&enc).expect("decrypt"); - assert_eq!(dec, inner, "encrypt then decrypt must be identity"); - - // key16 stored in the header must validate against the inner. - let stored = enc[7] as u16 | ((enc[4] as u16) << 8); - assert_eq!(stored, key16(&inner, header[9])); - } - - #[test] - fn seedless_save_is_plain_body() { - // All-zero seeds: the body is plain with no cipher, but still LZ4-framed. - let mut inner = vec![INNER_MARKER, 0x01, 0x00, b'X', 0x00]; - inner.extend(std::iter::repeat(0xAB).take(300)); - let header = vec![0u8; BODY_OFFSET]; // seeds s0,s1,s5 all zero - let enc = encrypt_pro(&inner, &header).expect("encrypt"); - let dec = decrypt_pro(&enc).expect("decrypt"); - assert_eq!(dec, inner); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/spec.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/spec.rs deleted file mode 100644 index 6bed3c4..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/spec.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! The command reference. Argument schemas for every Wolf command id and move-route -//! sub-command, loaded from `data/commands.json`. Drives bespoke rendering so no command -//! falls back to an unlabeled dump. - -use std::collections::HashMap; -use std::sync::OnceLock; - -use serde::Deserialize; - -static REFERENCE_JSON: &str = include_str!("../data/commands.json"); - -#[derive(Debug, Deserialize)] -struct Reference { - commands: Vec, - #[serde(default)] - route_commands: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct CommandSpec { - pub cid: u32, - pub name: String, - #[serde(default)] - pub int_args: Vec, - #[serde(default)] - pub string_args: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct IntField { - pub label: String, - #[serde(default)] - pub kind: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct StrField { - #[allow(dead_code)] - pub label: String, - #[serde(default)] - pub role: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct RouteSpec { - pub id: u32, - pub name: String, - #[serde(default)] - pub args: String, -} - -struct Tables { - commands: HashMap, - routes: HashMap, -} - -fn tables() -> &'static Tables { - static TABLES: OnceLock = OnceLock::new(); - TABLES.get_or_init(|| { - // The data file may carry a UTF-8 BOM (Windows PowerShell export); strip it. - let json = REFERENCE_JSON.trim_start_matches('\u{feff}'); - let reference: Reference = - serde_json::from_str(json).expect("embedded data/commands.json failed to parse"); - let commands = reference.commands.into_iter().map(|c| (c.cid, c)).collect(); - let routes = reference - .route_commands - .into_iter() - .map(|r| (r.id, r)) - .collect(); - Tables { commands, routes } - }) -} - -/// Argument schema for a command id, if catalogued. -pub fn command(cid: u32) -> Option<&'static CommandSpec> { - tables().commands.get(&cid) -} - -/// Display name for a command id (falls back to `Cmd`). -pub fn command_name(cid: u32) -> String { - command(cid) - .map(|c| c.name.clone()) - .unwrap_or_else(|| format!("Cmd{cid}")) -} - -/// Reverse lookup: command id for a display name (for the recompiler). -pub fn cid_for_name(name: &str) -> Option { - tables() - .commands - .values() - .find(|c| c.name == name) - .map(|c| c.cid) -} - -/// Name of a move-route sub-command id (falls back to `route`). -pub fn route_name(id: u32) -> String { - tables() - .routes - .get(&id) - .map(|r| r.name.clone()) - .unwrap_or_else(|| format!("route{id}")) -} - -/// Reverse lookup: move-route sub-command id for a display name (for the recompiler). -/// Accepts the `route` fallback form too. -pub fn route_id_for_name(name: &str) -> Option { - if let Some(rest) = name.strip_prefix("route") { - if let Ok(id) = rest.parse::() { - return Some(id); - } - } - tables() - .routes - .values() - .find(|r| r.name == name) - .map(|r| r.id) -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/strings.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/strings.rs deleted file mode 100644 index 079c3a4..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/strings.rs +++ /dev/null @@ -1,2339 +0,0 @@ -//! Player-facing text extraction and injection for translation. Produces a curated, -//! speaker-attributed, scene-grouped view rather than a full structural string dump. -//! -//! Extracts only text shown to the player. The allowlist below was derived from the command -//! table and verified against real GamePro data. It skips comments, debug text, file paths, -//! and DB structural names. Each dialogue line is attributed to a speaker, reconstructed from -//! the engine's `\s[n]` name-variable codes, a literal nameplate line, or the face-graphic -//! window gate. Lines are grouped per event for context. -//! -//! Injection is patch-style, keyed by command index plus string-arg index so it is -//! reorder-safe. A translator edits only the `text` field. Untouched lines recompile -//! byte-exact. - -use std::collections::HashMap; -use std::fmt::Write as _; - -use serde_json::Value; -use wolf_formats::command::RawCommand; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::game_dat::GameDat; -use wolf_formats::map::Map; - -use crate::json::value_slots; -use crate::text::{decode_wstr, text_to_wstr}; - -// ---------------------------------------------------------------------------- -// Player-text allowlist (which command string args are shown to the player) -// ---------------------------------------------------------------------------- - -/// The player-facing string-arg indices of a command, given its ints. Empty means nothing to -/// translate. Keeps only dialogue, choice, and on-screen text. Never debug, comment, label, -/// path, condition-literal, or structural-name strings. -fn player_str_indices(cmd: &RawCommand) -> Vec { - let a = &cmd.int_args; - match cmd.cid { - // Message: the dialogue text. - 101 => vec![0], - // Choices: each option label (count = low nibble of arg0), skipping trailing empties. - 102 => { - let n = a.first().copied().unwrap_or(0) & 0x0F; - (0..n as usize) - .filter(|&i| { - cmd.str_args - .get(i) - .map(|w| { - !decode_wstr(w, true).is_empty() && !decode_wstr(w, false).is_empty() - }) - .unwrap_or(false) - }) - .collect() - } - // Picture: str0 is on-screen text only when the picture type (bits 4-7 of arg0) == 2. - 150 if (a.first().copied().unwrap_or(0) >> 4) & 0x0F == 2 => vec![0], - // SetString: a literal assigned to a string var. Player text only when it is visible - // text, not a file path, not empty, and not a pure control/substitution token. - 122 => { - if let Some(w) = cmd.str_args.first() { - if is_displayable_literal(w, cmd) { - return vec![0]; - } - } - vec![] - } - // CommonEvent (by id) / CommonEventByName: the string inputs passed to the called event - // become its CSelf5/6/7 and get rendered by display-helper events (the `\cself[5/6/7]` - // shown in Messages). A literal passed here is on-screen text. For 210 the inputs start - // at str0. For 300 str0 is the event name (structural), so inputs are str1..=3. Keep - // only displayable literals, dropping the `\r\n`, path, and empty slots. - 210 => (0..cmd.str_args.len()) - .filter(|&i| { - cmd.str_args - .get(i) - .map(|w| is_displayable_literal(w, cmd)) - .unwrap_or(false) - }) - .collect(), - 300 => (1..=3) - .filter(|&i| { - cmd.str_args - .get(i) - .map(|w| is_displayable_literal(w, cmd)) - .unwrap_or(false) - }) - .collect(), - // Database read/write: str0 is the literal value written into a cell. str1/2/3 are - // structural type/data/field name operands, never on-screen. Player text only when it - // is a field-by-name write (flagWord bit 0x40000) into a content field (str3 = the field - // name). This keeps the `<装備を外す>` and `装備を解除します。` window writes and skips the - // `x` and `------` affix-sentinel writes into 付与文字列. - 250 | 252 => { - let flag = a.get(3).copied().unwrap_or(0); - let by_name = flag & 0x40000 != 0; - // str0 is displayable and has real words. Rejects the `x`, `------`, and `\x00` - // sentinels by value, plus paths and `<<…>>` directives. - let str0_ok = cmd - .str_args - .first() - .map(|w| is_displayable_literal(w, cmd) && has_translatable_text(&decode_either(w))) - .unwrap_or(false); - if by_name { - // By-name write: keep only when the destination (str3) is a content field. This - // keeps `<装備を外す>` and `装備を解除します。` and excludes the by-name affix sentinels - // written into 付与文字列, a non-content field. - let field_ok = cmd - .str_args - .get(3) - .map(|w| field_name_is_content(&decode_either(w))) - .unwrap_or(false); - if field_ok && str0_ok { - return vec![0]; - } - vec![] - } else if str0_ok { - // By-ID write: no field name to classify. The displayable plus translatable gate - // keeps the base-system `…を習得!` level-up message written into the 基本システム用変数 - // scratch slot. That slot exists in no DB, so it has no other extraction path. The - // by-name sentinels never reach here. - vec![0] - } else { - vec![] - } - } - _ => vec![], - } -} - -/// Both-encoding decode. Try UTF-8, fall back to Shift-JIS when the call site does not know -/// the file encoding. Whichever decodes non-empty wins. -pub(crate) fn decode_either(w: &wolf_formats::WStr) -> String { - let u = decode_wstr(w, true); - if u.is_empty() { - decode_wstr(w, false) - } else { - u - } -} - -/// Heuristic: is a SetString, CommonEvent-input, or Database-write literal player-visible text -/// rather than an internal path, flag, or engine directive. -fn is_displayable_literal(w: &wolf_formats::WStr, _cmd: &RawCommand) -> bool { - displayable_text(&decode_either(w)) -} - -/// The displayable-literal test on already-decoded text (split out so it is unit-testable). -fn displayable_text(s: &str) -> bool { - let t = s.trim(); - if t.is_empty() { - return false; - } - // Strip inline Wolf codes first. A backslash begins every code (\cself, \c, \i, \f, \cdb, - // and so on), never a path separator. A plain `contains('\\')` test would drop every - // on-screen UI string that embeds a variable, colour, or icon code such as - // `\f[\cself[19]]Sell`, `Buy Voucher A (\cself[20])`, or `\cself[11]rose!`. - let body = strip_control_codes(t); - let body = body.trim(); - if body.is_empty() || body == "," { - return false; - } - // Engine name-lookup directive (`<>…`, `<< \sys[100] % >>`). - // Resolved at runtime, never shown. - if body.contains("<<") && body.contains(">>") { - return false; - } - // File path or resource: a known asset extension, or a forward slash inside a spaceless - // token (a bare path like `Save/System.sav` or `Data/`). A `/` inside a sentence, like - // "Skills/Items" or a price separator, is ordinary punctuation and stays. - let lower = body.to_ascii_lowercase(); - for ext in [ - ".png", ".ogg", ".mp3", ".mid", ".wav", ".sav", ".wolfx", ".json", ".mps", - ] { - if lower.ends_with(ext) { - return false; - } - } - if body.contains('/') && !body.chars().any(char::is_whitespace) { - return false; - } - true -} - -/// True for Japanese (or other CJK) script: hiragana, katakana (full and half width), and kanji. -/// Used to keep the noise-token filter from ever rejecting real Japanese text. -fn has_cjk(s: &str) -> bool { - s.chars().any(|c| { - let u = c as u32; - (0x3040..=0x30FF).contains(&u) // hiragana + katakana - || (0x3400..=0x4DBF).contains(&u) // CJK extension A - || (0x4E00..=0x9FFF).contains(&u) // CJK unified ideographs - || (0xFF66..=0xFF9F).contains(&u) // half-width katakana - }) -} - -/// An engine-data token rather than prose: a string the parser used to pull into the grid that a -/// translator should never touch. Two shapes, both checked on the control-code-stripped body and -/// only ever firing when there is no CJK in it (so Japanese text is never rejected): -/// * a single ASCII alphanumeric, the id/index cells like `a`/`b`/`5` that fill columns such as -/// `英字id` (alphabet id) or a skill-tree layer flag, and -/// * a bare ASCII identifier carrying a digit or underscore (`A006`, `nx1`, `_syst00`, -/// `m_09_01`), an internal CG/asset/variable code. -/// A plain ASCII word (`Sell`, `HP`, `OK`, `Nofile`) or any phrase with whitespace is kept, since -/// those carry no digit or underscore and are real on-screen text. -fn is_noise_token(body: &str) -> bool { - if body.is_empty() || has_cjk(body) { - return false; - } - // A lone angle-bracket placeholder. A `\cdb`/`\cself` DB-read concatenated with a literal - // ``/`` fallback strips down to just the bracket token. The `has_cjk` guard - // above means a real CJK menu label like `<装備を外す>` never reaches here, and the - // exactly-one-bracket test keeps ` ` (two pairs) and `put in` (text - // around the bracket) as real. - let b = body.trim(); - if b.starts_with('<') - && b.ends_with('>') - && b.matches('<').count() == 1 - && b.matches('>').count() == 1 - { - return true; - } - if body.chars().count() == 1 && body.chars().all(|c| c.is_ascii_alphanumeric()) { - return true; - } - body.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') - && body.chars().any(|c| c.is_ascii_digit() || c == '_') -} - -/// A comma-separated list whose every token is a tiny CG/layer code: a single lowercase letter -/// followed by 1 to 3 digits (`a0`, `c1`, `l100`), or a single lowercase letter carrying a -/// `\cself` substitution (`g\cself[44]`). These build the engine's CG layer arguments and are -/// never player text. Checked on the RAW source, not the stripped body, because `g\cself[44]` -/// strips down to a bare `g` which is indistinguishable from a real initials list once stripped. -/// -/// The single-lowercase-letter lead is load-bearing: it keeps real short comma lists out of scope -/// (`OK,Cancel`, `HP,MP`, `Buy,Sell`, `Lv1,Lv2`, `L1,R1`, `Fire,Ice`, `1,2,3`), since those lead -/// with a capital, a second letter, or a non-letter. -fn is_code_list(raw: &str) -> bool { - if has_cjk(raw) || !raw.contains(',') { - return false; - } - let mut codes = 0usize; - for tok in raw.split(',') { - let body = strip_control_codes(tok); - let body = body.trim(); - if body.is_empty() { - continue; // a leading/trailing comma or a lone substitution - } - // The token must lead with exactly one lowercase ASCII letter. - let single_lower = body.chars().next().map_or(false, |c| c.is_ascii_lowercase()) - && body.chars().nth(1).map_or(true, |c| !c.is_ascii_alphabetic()); - let rest: String = body.chars().skip(1).collect(); - // `a0`/`l100`: the lead letter then 1 to 3 digits. - let is_code_digit = single_lower - && (1..=3).contains(&rest.chars().count()) - && rest.chars().all(|c| c.is_ascii_digit()); - // `g\cself[44]`: just the lead letter, and the raw token carried a backslash code. - let is_code_cself = single_lower && rest.is_empty() && tok.contains('\\'); - if !(is_code_digit || is_code_cself) { - return false; - } - codes += 1; - } - codes >= 2 -} - -/// True if the string has actual translatable words: at least one letter in any script once the -/// inline control codes (`\cself[..]`, `\s[..]`, `\f[..]`, `\E`, and so on) are removed, and the -/// remainder is real text rather than an engine-data token. Filters out pure -/// variable-substitution, symbol, and punctuation entries like `\cself[5]`, `\f[\cself[18]]\E▲`, -/// or a lone `+`, the id/index and CG/asset codes (`a`, `A006`, `nx1`, `_syst00`), and the -/// comma-separated CG layer code lists (`a0,b0,c1`). -pub(crate) fn has_translatable_text(s: &str) -> bool { - if is_code_list(s) { - return false; - } - let body = strip_control_codes(s); - let trimmed = body.trim(); - trimmed.chars().any(|c| c.is_alphabetic()) && !is_noise_token(trimmed) -} - -/// Remove Wolf inline control codes so the remaining real text can be measured. Used by the -/// displayable heuristic and name detection. -/// -/// A Wolf code is `\` plus a name (a run of ASCII letters/digits like `cself`/`space`/`f`/`E`, -/// or a single symbol like `-`/`>`/`<`/`.`/`|`/`!`/`^`) plus an optional `[..]` argument whose -/// brackets may nest, as in `\f[\cself[19]]`. The code name must be a maximal run so `\cself` -/// is consumed whole rather than leaving a bogus `self`, and nested args must be tracked so a -/// stray `]…` does not leak. Both would otherwise read as translatable text. The line-alignment -/// tags `` and `` are also dropped since they are formatting, not words. -/// -/// Ruby/furigana `\r[base,reading]` is special-cased. It renders `base` with `reading` as -/// furigana, so the translatable-text check keeps the base (the text before the first comma) -/// and drops the `\r[`, the `,reading`, and the `]`. A ruby-only line thus counts as -/// translatable because its base is real words. All other codes are stripped whole. An escaped -/// literal `\\r[..]` is not ruby. The walk reads the first `\` then the second `\` as the -/// single-symbol code `\\`, leaving `r[..]` as plain text, so it never reaches the `name == "r"` -/// branch. -fn strip_control_codes(s: &str) -> String { - let mut out = String::new(); - let mut chars = s.chars().peekable(); - while let Some(c) = chars.next() { - match c { - '\\' => { - // The code name: a maximal alphanumeric run, else a single symbol char. - let mut name = String::new(); - if matches!(chars.peek(), Some(c) if c.is_ascii_alphanumeric()) { - while matches!(chars.peek(), Some(c) if c.is_ascii_alphanumeric()) { - name.push(chars.next().unwrap()); - } - } else if let Some(c) = chars.next() { - name.push(c); - } - // An optional `[..]` argument, honouring nested brackets. Capture its body so - // the ruby base can be re-emitted. - let mut arg = String::new(); - let has_arg = chars.peek() == Some(&'['); - if has_arg { - chars.next(); - let mut depth = 1usize; - while depth > 0 { - match chars.next() { - Some('[') => { - depth += 1; - arg.push('['); - } - Some(']') => { - depth -= 1; - if depth > 0 { - arg.push(']'); - } - } - Some(c) => arg.push(c), - None => break, - } - } - } - // Ruby `\r[base,reading]`: keep the base (before the first comma), drop reading. - if name == "r" && has_arg { - let base = arg.split(',').next().unwrap_or(&arg); - out.push_str(base); - } - } - // Line-alignment tags `` and `` (right and centre). Pure formatting. - '<' => { - let mut look = chars.clone(); - if matches!(look.next(), Some('R' | 'C' | 'r' | 'c')) && look.next() == Some('>') { - chars.next(); - chars.next(); - } else { - out.push('<'); - } - } - _ => out.push(c), - } - } - out -} - -// ---------------------------------------------------------------------------- -// Speaker attribution -// ---------------------------------------------------------------------------- - -/// The global string-var slots that carry a speaker name in the BasicSystem engine (S[k]). -/// Maps the slot to a stable role label. The concrete name is resolved at render time or left -/// to the translator. Derived from SysDatabase 文字列変数名 and verified against `\s[..]` -/// frequency in GamePro. -fn speaker_role(slot: u32) -> Option<&'static str> { - Some(match slot { - 9 | 10 => "Heroine", - 12 => "Rival", - 13 => "Professor", - 15 => "Opponent", - 16 => "Opponent", - 31 => "Ally Trainer", - 32 => "Ally Monster", - 33 => "Enemy Trainer", - 34 => "Enemy Monster", - 999 => "Player", - _ => return None, - }) -} - -/// Speaker attribution state carried across an event's command stream. -#[derive(Default)] -struct SpeakerCtx { - /// Last `call "メッセージ顔グラフィック変更"(Face Graphic Number=N)` value. The window-type gate. - last_face: Option, -} - -impl SpeakerCtx { - /// Update state from a face-graphic-change call. `utf8` is the file encoding. - fn observe(&mut self, cmd: &RawCommand, utf8: bool) { - // call "メッセージ顔グラフィック変更"(Face Graphic Number=N) is cid 300 by-name. - if cmd.cid == 300 { - if let Some(name) = cmd.str_args.first() { - if decode_wstr(name, utf8) == "メッセージ顔グラフィック変更" { - self.last_face = cmd.int_args.get(2).map(|&v| v as i32 as i64); - } - } - } - } - - /// Resolve the speaker + source for a Message's text (its decoded line 1). - fn speaker_of(&self, text: &str) -> (String, String) { - // Strip a leading window-option prefix `@\n`. - let body = if let Some(rest) = text.strip_prefix('@') { - rest.split_once('\n').map(|(_, b)| b).unwrap_or(text) - } else { - text - }; - let line1 = body.split('\n').next().unwrap_or("").trim(); - - // (a) Leading speaker string-var code `\s[k]`, including the `\s[9]\s[10]` heroine combo. - if let Some(role) = leading_name_var(line1) { - return (role.to_string(), "string_var".to_string()); - } - - // (b) Literal short nameplate line for map NPCs. A nameplate face window precedes, and - // line 1 is a short token with no control codes or sentence punctuation, followed by - // a body. Confidence is encoded in the source tag. - let has_more = body.contains('\n'); - let nameplate_face = matches!(self.last_face, Some(n) if n >= 2); - if has_more - && !line1.is_empty() - && !line1.contains('\\') - && line1.chars().count() <= 16 - && !line1.ends_with(['。', '!', '?', '」', '.', '!', '?']) - && !line1.starts_with(['「', '『', '【']) - { - let src = if nameplate_face { - "literal_line1" - } else { - "literal_line1_lowconf" - }; - return (line1.to_string(), src.to_string()); - } - - // (c) No in-text speaker. Treat as narration. - ("Narration".to_string(), "narration".to_string()) - } -} - -/// If `line1` begins with a known speaker name-var code (`\s[k]` or the `\s[9]\s[10]` combo), -/// return the role label. -fn leading_name_var(line1: &str) -> Option<&'static str> { - let s = line1.strip_prefix("\\s[")?; - let (num, _) = s.split_once(']')?; - let k: u32 = num.parse().ok()?; - speaker_role(k) -} - -// ---------------------------------------------------------------------------- -// Extraction model -// ---------------------------------------------------------------------------- - -/// One extracted player-facing line, with its injection locator and speaker. -pub struct Line { - /// Command index within the event/page (stable inject key). - pub cmd: usize, - /// String-arg index within that command. - pub str_idx: usize, - pub speaker: String, - pub speaker_src: String, - pub source: String, -} - -/// A scene is one event (a common event, or a map event plus page), with its player-facing -/// lines in command order. -pub struct Scene { - pub event: u32, - pub page: Option, - pub name: String, - pub lines: Vec, -} - -/// Walk one command list, returning its player-facing lines with speakers attributed by a -/// stateful pass (face-graphic gate plus name-var and nameplate detection). -/// -/// `is_debug` marks the developer Debug common event, whose menu is built by cid250/252 DB writes -/// of internal operation labels (`変数操作`, `所持金等MAX`). Those are skipped there. The gate is -/// purely structural (the caller matched the exact event name plus the DB-write cids), so it never -/// inspects a string value. Real dialogue in that event is a Message (cid101) and survives. -fn scene_lines(cmds: &[RawCommand], utf8: bool, is_debug: bool) -> Vec { - let mut ctx = SpeakerCtx::default(); - let mut out = Vec::new(); - for (ci, cmd) in cmds.iter().enumerate() { - ctx.observe(cmd, utf8); - if is_debug && matches!(cmd.cid, 250 | 252) { - continue; - } - for si in player_str_indices(cmd) { - let Some(w) = cmd.str_args.get(si) else { - continue; - }; - let source = decode_wstr(w, utf8); - // Skip pure control-code, symbol, or punctuation entries like a `\cself[5]` dynamic - // choice, a `\f[..]\E▲` UI arrow, or a lone `+`. Nothing for a translator to touch. - if !has_translatable_text(&source) { - continue; - } - let (speaker, speaker_src) = if cmd.cid == 101 && si == 0 { - ctx.speaker_of(&source) - } else if cmd.cid == 102 { - ("Choice".to_string(), "choice".to_string()) - } else { - ("UI".to_string(), "ui".to_string()) - }; - out.push(Line { - cmd: ci, - str_idx: si, - speaker, - speaker_src, - source, - }); - } - } - out -} - -/// Extract every scene with player text from a common-events file (ordered by event id). -pub fn extract_common_events(ce: &CommonEventsFile) -> Vec { - let mut scenes = Vec::new(); - for ev in &ce.events { - // The developer Debug menu event builds itself from internal cid250/252 DB writes whose - // values are operation labels, not player text. Match the exact event name so sibling - // test events (デバtest, テスト) that hold real dialogue are untouched. Also accept the - // half-width spelling. To keep the Debug-menu labels instead, drop this flag. - let name = decode_wstr(&ev.name, ce.utf8); - let is_debug = name == "デバッグ" || name == "デバッグ"; - let lines = scene_lines(&ev.commands, ce.utf8, is_debug); - if !lines.is_empty() { - scenes.push(Scene { - event: ev.int_id, - page: None, - name, - lines, - }); - } - } - scenes -} - -/// Extract every scene with player text from a map (ordered by event, then page). -pub fn extract_map(map: &Map) -> Vec { - let mut scenes = Vec::new(); - for ev in &map.events { - for (pi, page) in ev.pages.iter().enumerate() { - // Maps have no developer Debug common event, so the debug gate is always off here. - let lines = scene_lines(&page.commands, map.utf8, false); - if !lines.is_empty() { - scenes.push(Scene { - event: ev.id, - page: Some(pi), - name: decode_wstr(&ev.name, map.utf8), - lines, - }); - } - } - } - scenes -} - -// ---------------------------------------------------------------------------- -// JSON serialization (text starts equal to source, the translator edits `text`) -// ---------------------------------------------------------------------------- - -/// Render extracted scenes as translation JSON. `text` starts equal to `source` and a -/// translator edits `text`. `source` is kept for drift detection on inject. -pub fn scenes_to_json(file: &str, kind: &str, scenes: &[Scene]) -> String { - let mut s = String::new(); - s.push_str("{\n"); - let _ = write!(s, " \"file\": {},\n", jstr(file)); - let _ = write!(s, " \"kind\": {},\n", jstr(kind)); - s.push_str(" \"scenes\": [\n"); - for (si, sc) in scenes.iter().enumerate() { - s.push_str(" {\n"); - let _ = write!(s, " \"event\": {},\n", sc.event); - if let Some(p) = sc.page { - let _ = write!(s, " \"page\": {p},\n"); - } - let _ = write!(s, " \"name\": {},\n", jstr(&sc.name)); - s.push_str(" \"lines\": [\n"); - for (li, ln) in sc.lines.iter().enumerate() { - let _ = write!( - s, - " {{\"cmd\": {}, \"str\": {}, \"speaker\": {}, \"speaker_src\": {}, \"source\": {}, \"text\": {}}}", - ln.cmd, - ln.str_idx, - jstr(&ln.speaker), - jstr(&ln.speaker_src), - jstr(&ln.source), - jstr(&ln.source) - ); - s.push_str(if li + 1 == sc.lines.len() { - "\n" - } else { - ",\n" - }); - } - s.push_str(" ]\n }"); - s.push_str(if si + 1 == scenes.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n}\n"); - s -} - -// ---------------------------------------------------------------------------- -// Injection (apply edited translations back, byte-exact, drift-checked) -// ---------------------------------------------------------------------------- - -/// Outcome of an inject. `applied`, `untranslated`, and `drifted` are the normal path. -/// `code_mismatch` and `unrepresentable` count lines skipped by a safety guard, left untouched -/// and reported. A non-zero count means the caller should exit non-zero even though the good -/// lines were written. -#[derive(Default)] -pub struct InjectStats { - pub applied: usize, - pub untranslated: usize, - pub drifted: usize, - /// Translation dropped/added/altered an inline control code vs the source (skipped). - pub code_mismatch: usize, - /// Translation has a char not representable in the file's (Shift-JIS) encoding (skipped). - pub unrepresentable: usize, -} - -/// Inject behaviour knobs. -#[derive(Default, Clone, Copy)] -pub struct InjectOptions { - /// Relax the inline-code preservation guard. Allows a translation whose `\code`, `@` - /// window prefix, or ``/`` tags differ from the source. Off by default, which is the - /// strict per-line block. - pub allow_code_drift: bool, - /// Normalize CJK punctuation in the translated text to ASCII equivalents (`「」` to `"`, `。` - /// to `.`, full-width `!?` to `!?`, and so on) at inject time, via [`normalize_en_punct`]. - /// Off by default. For English targets. Other languages and translators who keep CJK - /// punctuation are unaffected. Applied after the code-drift guard, before encoding. - pub normalize_punct: bool, -} - -/// Map CJK punctuation to its ASCII equivalent in `s`, leaving everything else untouched -/// (ASCII, kana, kanji, and decorative symbols like ♥ ♪ ✨ ♂). Opt-in normalization for English -/// targets, applied to the translated text on inject (see [`InjectOptions::normalize_punct`]). -/// -/// Safe to run over a whole string including inline control codes. Every Wolf code (`\cself[8]`, -/// ``, `@1`) is pure ASCII and contains none of these characters, so codes pass through -/// verbatim. Most entries are a 1:1 char remap. The multi-char expansions (`…` to `...`, `‥` to -/// `..`, `→` to `->`, `←` to `<-`) are spelled out. Brackets map to their open/close ASCII form. -pub fn normalize_en_punct(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - match c { - // Quote brackets -> straight double quote. - '「' | '」' | '『' | '』' => out.push('"'), - // Square / lenticular brackets -> ASCII square brackets. - '【' | '〔' => out.push('['), - '】' | '〕' => out.push(']'), - // Angle brackets -> ASCII angle brackets. - '〈' | '《' => out.push('<'), - '〉' | '》' => out.push('>'), - // Wave dashes -> tilde. - '~' | '〜' => out.push('~'), - // Ideographic full stop / commas. - '。' => out.push('.'), - '、' | ',' => out.push(','), - // Middle dot -> hyphen. - '・' => out.push('-'), - // Full-width ASCII-equivalent punctuation. - '!' => out.push('!'), - '?' => out.push('?'), - ':' => out.push(':'), - ';' => out.push(';'), - '%' => out.push('%'), - '&' => out.push('&'), - // Ellipses. - '…' => out.push_str("..."), - '‥' => out.push_str(".."), - // Reference mark -> asterisk. - '※' => out.push('*'), - // Dashes (em/en/full-width/horizontal-bar/hyphen) -> hyphen. - '―' | '─' | '-' | '—' | '‐' => out.push('-'), - // Curly quotes -> straight quotes. - '“' | '”' => out.push('"'), - '‘' | '’' => out.push('\''), - // Math-ish symbols. - '×' => out.push('x'), - '−' => out.push('-'), - '÷' => out.push('/'), - // Arrows. - '→' => out.push_str("->"), - '←' => out.push_str("<-"), - '↑' => out.push('^'), - '↓' => out.push('v'), - // Filled / hollow squares -> asterisk. - '■' | '□' => out.push('*'), - other => out.push(other), - } - } - out -} - -/// The ordered multiset of inline codes that must survive a translation edit, so a guard can -/// confirm none was dropped, added, or altered. Holds each `\` (alnum run or single -/// symbol) with its full nested `[..]` body, each ``/`` alignment tag, and a leading -/// `@` window prefix. Sorted, so reordering is allowed but the set must match, including -/// which variable each `\code` references. -/// -/// Ruby/furigana `\r[base,reading]` is excluded (a `name == "r"` code with a `[..]` body). An -/// English translation legitimately drops the ruby markup, rendering the base with no furigana, -/// so it must not be in the must-preserve set. An escaped `\\r[..]` is unaffected. It parses as -/// the `\\` code plus literal `r[..]` text, so the `name == "r"` body branch is never reached. -fn code_multiset(s: &str) -> Vec { - let mut codes = Vec::new(); - if let Some(rest) = s.strip_prefix('@') { - let digits: String = rest.chars().take_while(char::is_ascii_digit).collect(); - if !digits.is_empty() { - codes.push(format!("@{digits}")); - } - } - let mut chars = s.chars().peekable(); - while let Some(c) = chars.next() { - match c { - '\\' => { - let mut code = String::from("\\"); - let mut name = String::new(); - if matches!(chars.peek(), Some(c) if c.is_ascii_alphanumeric()) { - while matches!(chars.peek(), Some(c) if c.is_ascii_alphanumeric()) { - let ch = chars.next().unwrap(); - name.push(ch); - code.push(ch); - } - } else if let Some(c) = chars.next() { - name.push(c); - code.push(c); - } - let has_arg = chars.peek() == Some(&'['); - if has_arg { - code.push(chars.next().unwrap()); - let mut depth = 1usize; - while depth > 0 { - match chars.next() { - Some('[') => { - depth += 1; - code.push('['); - } - Some(']') => { - depth -= 1; - code.push(']'); - } - Some(ch) => code.push(ch), - None => break, - } - } - } - // Ruby `\r[base,reading]` is droppable markup, not a must-preserve code. - if name == "r" && has_arg { - continue; - } - codes.push(code); - } - '<' => { - let mut look = chars.clone(); - if matches!(look.next(), Some('R' | 'C' | 'r' | 'c')) && look.next() == Some('>') { - let ch = chars.next().unwrap(); - chars.next(); - codes.push(format!("<{ch}>")); - } - } - _ => {} - } - } - codes.sort(); - codes -} - -/// Run the per-line safety guards and, if they pass, encode and write the translation into -/// `cell`. The caller has already confirmed the drift guard so the base still holds `source`. -/// Updates `stats` and emits a located diagnostic on a guarded skip. -pub(crate) fn write_translation( - cell: &mut wolf_formats::WStr, - source: &str, - text: &str, - utf8: bool, - opts: &InjectOptions, - stats: &mut InjectStats, - locator: &dyn Fn() -> String, -) { - // (1) Inline control-code preservation. Dropping or altering a \code, @ prefix, or - // / tag would break rendering, or for a \cself-referenced variable, logic. The guard - // runs on the pre-normalization `text`. The code multiset is pure ASCII and the punct map - // never touches ASCII, so the choice does not affect the result, but checking before - // normalization keeps the diagnostic showing the translator's actual edit. - if !opts.allow_code_drift { - let (a, b) = (code_multiset(source), code_multiset(text)); - if a != b { - stats.code_mismatch += 1; - eprintln!( - "{}: control-code mismatch - source has {a:?}, translation has {b:?}; edit the words but keep the \\codes and @window prefix (or pass --allow-code-drift)", - locator() - ); - return; - } - } - // (1b) Optional CJK to ASCII punctuation normalization for English targets. Applied after - // the code-drift guard, before encoding. Control codes are pure ASCII so they pass through. - let normalized; - let text = if opts.normalize_punct { - normalized = normalize_en_punct(text); - normalized.as_str() - } else { - text - }; - // (2) Encoding. A char not representable in the file's encoding (Shift-JIS games) must not - // be silently substituted. Skip this line and report so the rest of the batch still applies. - match text_to_wstr(text, utf8) { - Ok(w) => { - *cell = w; - stats.applied += 1; - } - Err(_) => { - stats.unrepresentable += 1; - eprintln!( - "{}: text not representable in Shift-JIS: {text:?}; choose an SJIS-encodable equivalent", - locator() - ); - } - } -} - -/// Apply a translation JSON back onto a common-events file. A line is applied only when its -/// `text` differs from `source` and the base command still holds `source` (drift guard). -pub fn inject_common_events( - json: &str, - base: &mut CommonEventsFile, - opts: &InjectOptions, -) -> Result { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let utf8 = base.utf8; - let by_id: HashMap = base - .events - .iter() - .enumerate() - .map(|(i, ev)| (ev.int_id, i)) - .collect(); - let mut stats = InjectStats::default(); - for sc in scenes_array(&root)? { - let ev_id = u(sc, "event")? as u32; - let &idx = by_id - .get(&ev_id) - .ok_or_else(|| format!("common event {ev_id} not in base"))?; - apply_lines(sc, &mut base.events[idx].commands, utf8, opts, &mut stats)?; - } - Ok(stats) -} - -/// Apply a translation JSON back onto a map, keyed by event id plus page. -pub fn inject_map(json: &str, base: &mut Map, opts: &InjectOptions) -> Result { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let utf8 = base.utf8; - let by_id: HashMap = base - .events - .iter() - .enumerate() - .map(|(i, ev)| (ev.id, i)) - .collect(); - let mut stats = InjectStats::default(); - for sc in scenes_array(&root)? { - let ev_id = u(sc, "event")? as u32; - let page = u(sc, "page")? as usize; - let &idx = by_id - .get(&ev_id) - .ok_or_else(|| format!("map event {ev_id} not in base"))?; - let cmds = base.events[idx] - .pages - .get_mut(page) - .map(|p| &mut p.commands) - .ok_or_else(|| format!("map event {ev_id} page {page} out of range"))?; - apply_lines(sc, cmds, utf8, opts, &mut stats)?; - } - Ok(stats) -} - -fn apply_lines( - sc: &Value, - cmds: &mut [RawCommand], - utf8: bool, - opts: &InjectOptions, - stats: &mut InjectStats, -) -> Result<(), String> { - let ev = sc.get("event").and_then(Value::as_u64).unwrap_or(0); - let page = sc.get("page").and_then(Value::as_u64); - let lines = sc - .get("lines") - .and_then(Value::as_array) - .ok_or("scene has no `lines`")?; - for ln in lines { - let ci = u(ln, "cmd")? as usize; - let si = u(ln, "str")? as usize; - let source = ln.get("source").and_then(Value::as_str).unwrap_or(""); - let text = ln.get("text").and_then(Value::as_str).unwrap_or(source); - if text == source { - stats.untranslated += 1; - continue; - } - let Some(cmd) = cmds.get_mut(ci) else { - stats.drifted += 1; - continue; - }; - let Some(slot) = cmd.str_args.get_mut(si) else { - stats.drifted += 1; - continue; - }; - // Drift guard: only overwrite if the base still holds the recorded source text. - if decode_wstr(slot, utf8) != source { - stats.drifted += 1; - continue; - } - let loc = || match page { - Some(p) => format!("event {ev} page {p} cmd {ci} str {si}"), - None => format!("event {ev} cmd {ci} str {si}"), - }; - write_translation(slot, source, text, utf8, opts, stats, &loc); - } - Ok(()) -} - -fn scenes_array(root: &Value) -> Result<&Vec, String> { - root.get("scenes") - .and_then(Value::as_array) - .ok_or_else(|| "translation JSON has no `scenes` array".to_string()) -} - -fn u(v: &Value, key: &str) -> Result { - v.get(key) - .and_then(Value::as_u64) - .ok_or_else(|| format!("missing/invalid `{key}`")) -} - -// ---------------------------------------------------------------------------- -// Database player-text extraction (item/skill/term/message content) -// ---------------------------------------------------------------------------- - -/// Field/type names (JP or EN) that mark a string field as internal: file, graphic, audio, -/// memo, sort, path, or formula. Never player text. -pub(crate) const DB_INTERNAL: &[&str] = &[ - "ファイル", - "画像", - "グラフィック", - "グラフィック", - "効果音", - "BGM", - "BGS", - "メモ", - "ソート", - "パス", - "計算", - // `日本語移動名` and similar is the internal map-transition key namespace, compared in - // StringConditions and never displayed. Translating it breaks the map-move override. The EN - // glossary renders it "...Move Name" or "...Name", so it must be denylisted here, checked - // first, rather than merely absent from DB_CONTENT, or the broad "Name" content keyword would - // re-admit it. The player-visible map name lives in a separate, still-extracted field. - "移動名", - "File", - "Graphic", - "Image", - "Sound", - "Path", - "Memo", - "Sort", - "Formula", - "Move Name", -]; - -/// Field/type names (JP or EN) that mark a string field as a single-string name: an identity -/// string with no sentence context that the game may look rows up by. These are owned by the -/// project glossary (`names.rs`) so the row name and every by-name reference stay consistent. -/// -/// `移動名` / "Move Name" is included here. The `日本語移動名` field is the internal map-transition -/// key namespace, compared in cid112 StringConditions, but also the on-screen location-banner -/// name. The glossary owns it and keeps it consistent with the cid250/cid112 references that -/// read it. The genuine-internal denylist in [`db_field_role`] is `DB_INTERNAL` minus `移動名`, -/// so this entry is reached as a name rather than rejected as internal. -pub(crate) const DB_NAME: &[&str] = &[ - "名前", - "名称", - "なまえ", - "タイトル", - "肩書", - "表示名", - "用語", - "メニュー", - "コマンド名", - "コマンド", - "単位", - "項目", - "愛称", - "呼び方", - "移動名", - "Name", - "Title", - "Term", - "Menu", - "Command", - "Unit", - "Display", - "Move Name", -]; - -/// Field/type names (JP or EN) that mark a string field as description or dialogue: a -/// contextual sentence, not an identity string. These stay independently translatable in the -/// per-file `db-strings` path. Two different translations of a description are not a conflict. -/// Dialogue fields (`セリフ`/`セリフ`/`効果文`) land here. -pub(crate) const DB_DESC: &[&str] = &[ - "説明", - "文章", - "効果文", - "メッセージ", - "セリフ", - "セリフ", - "Description", - "Message", - "Text", -]; - -/// A DB string-field value that is actually an asset path (file or folder selector content), -/// not player text. Used to exclude `type_byte == 1` folder-picker cells whose value is a path -/// while keeping `type_byte == 1` cells that hold a plain display name (status `表示名`). NUL-safe. -pub(crate) fn value_is_asset_path(s: &str) -> bool { - let t = s.trim().trim_end_matches('\0').trim(); - if t.is_empty() { - return false; - } - let lower = t.to_ascii_lowercase(); - const ASSET_EXT: &[&str] = &[ - ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".ogg", ".mp3", ".wav", ".mid", - ]; - if ASSET_EXT.iter().any(|e| lower.ends_with(e)) { - return true; - } - // A '/' inside a spaceless token is a bare folder/asset path, e.g. SE/[System]Cancel02.ogg. - t.contains('/') && !t.chars().any(char::is_whitespace) -} - -/// A DB cell whose every non-blank line is an asset path: a multi-line `.png` CG list, or a bare -/// folder. Tighter than [`value_is_asset_path`] so a one-line UI string with a single slash stays -/// text: a line counts as a path only with an asset extension, a trailing `/`, or two or more `/`. -/// CJK-bearing values return early, so no Japanese is ever dropped, and any line with whitespace -/// disqualifies the whole value (`1/4 of max HP`, `Buy/Sell` and friends stay). -fn value_is_asset_path_list(s: &str) -> bool { - if has_cjk(s) { - return false; - } - const ASSET_EXT: &[&str] = &[ - ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".ogg", ".mp3", ".wav", ".mid", - ]; - let body = strip_control_codes(s); - let mut any = false; - for line in body.split(['\r', '\n']) { - let line = line.trim().trim_end_matches('\0').trim(); - if line.is_empty() { - continue; - } - any = true; - if line.chars().any(char::is_whitespace) { - return false; - } - let lower = line.to_ascii_lowercase(); - let is_path = ASSET_EXT.iter().any(|e| lower.ends_with(e)) - || line.ends_with('/') - || line.matches('/').count() >= 2; - if !is_path { - return false; - } - } - any -} - -/// A DB id/code cell: the control-code-stripped, trimmed body is exactly 2 or 3 lowercase ASCII -/// letters (`za`, `kc`, `abc`), the values that fill id/layer columns like `英字id` / `abc` / -/// skill-tree layer flags. `is_ascii_lowercase` already rules out digits, uppercase, CJK and -/// symbols, so capitalised names (`Iris`) and uppercase labels (`OK`, `HP`) are never caught. -/// DB-extractor ONLY: in the command stream this same shape is real UI text (`gem`, `ml`), so it -/// must never go into the shared [`is_noise_token`] / [`has_translatable_text`]. -fn is_db_short_code(s: &str) -> bool { - let b = strip_control_codes(s); - let b = b.trim(); - let n = b.chars().count(); - (n == 2 || n == 3) && b.chars().all(|c| c.is_ascii_lowercase()) -} - -/// The translation-ownership role of a DB string cell. Exactly one extractor owns each role, so -/// a DB name is never translated in two places. -/// * [`Role::Internal`]: file, graphic, audio, memo, path, sort, formula, or temp. Never -/// translated. -/// * [`Role::Name`]: a single-string identity name. Owned by the project glossary -/// (`names.rs`), which keeps the row name and every by-name reference consistent. -/// * [`Role::Content`]: a description or dialogue sentence, or an unlabeled non-internal -/// field. Owned by the per-file `db-strings` path, independently translatable. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum Role { - Internal, - Name, - Content, -} - -/// Tests for a temp or separator type. Wolf marks scratch types with a leading `×` or `x `. -/// Separators are an all-dash label. -fn is_temp_or_separator_type(type_name: &str) -> bool { - let tn = type_name.trim(); - tn.is_empty() || tn.starts_with('×') || tn.starts_with("x ") || tn.chars().all(|c| c == '-') -} - -/// `DB_INTERNAL` minus the `移動名`/`Move Name` map-key namespace. The genuine-internal denylist: -/// file, graphic, sound, BGM, memo, path, sort, formula. `移動名` is excluded so it is reachable -/// as a `Name`, the displayed location banner the glossary owns, not rejected as internal. -fn matches_genuine_internal(name: &str) -> bool { - const NAME_KEEP: &[&str] = &["移動名", "Move Name"]; - // Match ASCII case-insensitively. Japanese terms are unaffected by `to_ascii_lowercase`, so a - // field labelled `…memo` or `…File` is caught as internal regardless of case. Without this, a - // lowercase dev-note field like `100~199memo` would leak past the upper-case `Memo`/`File` - // entries. - let lower = name.to_ascii_lowercase(); - DB_INTERNAL - .iter() - .filter(|p| !NAME_KEEP.contains(p)) - .any(|p| lower.contains(&p.to_ascii_lowercase())) -} - -/// The single source of truth for a DB string cell's translation-ownership [`Role`]. Evaluated -/// in strict order: internal first, then description/dialogue, then name, default content. -/// -/// 1. `Internal`: `type_byte` not in {0,1} (only 0/1 hold strings), a temp/separator type, or a -/// genuine-internal field name (`DB_INTERNAL` minus the `移動名` map-key namespace). -/// 2. `Content`: the field or the type name matches a description/dialogue keyword -/// ([`DB_DESC`]). A contextual sentence, independently translatable in `db-strings`. -/// 3. `Name`: `is_first_string` (the canonical by-name column, even with an empty/unknown -/// label) or the field/type name matches a name keyword ([`DB_NAME`]). -/// 4. `Content` by default. An unlabeled non-internal non-desc field is independently -/// translatable. Safer than forcing glossary consistency on an unrecognised label. -/// -/// The per-cell `value_is_asset_path` exclusion for `type_byte == 1` cells is applied separately -/// at the call sites, since it depends on the row value. -pub(crate) fn db_field_role( - type_byte: u8, - type_name: &str, - field_jp: &str, - field_en: &str, - is_first_string: bool, -) -> Role { - // (1) Internal: structural gate, temp/separator type, or a genuine-internal field name. - if !matches!(type_byte, 0 | 1) || is_temp_or_separator_type(type_name) { - return Role::Internal; - } - let names = [field_jp, field_en]; - if names.iter().any(|n| matches_genuine_internal(n)) { - return Role::Internal; - } - let tn = type_name.trim(); - // (2) Content: a description/dialogue field or type, e.g. a …セリフ dialogue table or a 説明 - // field. Checked before Name so a `説明`/`セリフ` sentence is never pulled into the glossary. - if DB_DESC.iter().any(|p| tn.contains(p)) - || names.iter().any(|n| DB_DESC.iter().any(|p| n.contains(p))) - { - return Role::Content; - } - // (3) Name: the canonical by-name column, or a recognised name field or type (用語設定 Terms). - if is_first_string - || DB_NAME.iter().any(|p| tn.contains(p)) - || names.iter().any(|n| DB_NAME.iter().any(|p| n.contains(p))) - { - return Role::Name; - } - // (4) Default: an unlabeled or unrecognised non-internal field is independently translatable. - Role::Content -} - -/// Is this string field owned by the per-file `db-strings` path (description/dialogue content)? -/// True only for [`Role::Content`]. Name fields are owned by the glossary, so the two extractors -/// have disjoint ownership of DB string cells. The `is_first_string` flag (the canonical by-name -/// column) is supplied by the caller. A `db-strings`-only call site that lacks it passes `false`. -/// A first-string column with a recognised name label still classifies as `Name` either way. -pub(crate) fn db_player_field( - type_byte: u8, - type_name: &str, - field_jp: &str, - field_en: &str, - is_first_string: bool, -) -> bool { - db_field_role(type_byte, type_name, field_jp, field_en, is_first_string) == Role::Content -} - -/// Field-name-only "not internal" test for the command stream (cid 250/252 by-name writes), -/// where there is a destination field name but no full DB type context. A cid250 write into -/// either a description/dialogue field or a name field is player text, so this accepts `Name` or -/// `Content`, everything except `Internal`. The `×`/separator and `type_byte` gates do not apply -/// without type context, so it classifies on the field name alone against the genuine-internal -/// denylist. -fn field_name_is_content(name: &str) -> bool { - if matches_genuine_internal(name) { - return false; - } - // A bare field name with no type context is player text unless it is genuinely internal. - // Mirrors the Name or Content union (everything non-internal) of [`db_field_role`]. - true -} - -/// One extracted DB string cell with its (type, row, field) locator and bilingual field label. -pub struct DbLine { - pub type_id: usize, - pub row: usize, - pub field: usize, - pub row_name: String, - pub field_label: String, - pub source: String, -} - -/// Player-facing string cells of one DB type. -pub struct DbGroup { - pub type_id: usize, - pub type_label: String, - pub lines: Vec, -} - -fn bilingual_name(jp: &str, glossary: &HashMap) -> String { - match glossary.get(jp) { - Some(en) if en != jp => format!("{en} · {jp}"), - _ => jp.to_string(), - } -} - -/// Extract every player-facing string cell from a database (item, skill, term, message -/// content), grouped by type. `glossary` provides bilingual labels and powers EN field matching. -pub fn extract_db_strings(db: &Database, glossary: &HashMap) -> Vec { - let utf8 = db.utf8; - let mut groups = Vec::new(); - for (ti, t) in db.types.iter().enumerate() { - let type_name = decode_wstr(&t.name, utf8); - let fields_size = (t.dat_fields_size as usize).min(t.fields.len()); - let slots = value_slots(t, fields_size, utf8); - // Collect the player-facing content fields (Role::Content, since name fields are owned by - // the glossary) with their string-value slot and type_byte. The type_byte drives the per-cell - // value-is-path exclusion below. `is_first_string` tracks the canonical by-name column so - // the role classifier can recognise it. - let mut keep: Vec<(usize, usize, String, u8)> = Vec::new(); // (field_idx, slot, label, tb) - let mut seen_string = false; - for (fi, (_, is_str, slot)) in slots.iter().enumerate() { - if !is_str { - continue; - } - let is_first_string = !seen_string; - seen_string = true; - let fjp = decode_wstr(&t.fields[fi].name, utf8); - let fen = glossary.get(&fjp).map(String::as_str).unwrap_or(&fjp); - let tb = t.fields[fi].type_byte; - if db_player_field(tb, &type_name, &fjp, fen, is_first_string) { - keep.push((fi, *slot, bilingual_name(&fjp, glossary), tb)); - } - } - if keep.is_empty() { - continue; - } - let mut lines = Vec::new(); - for (ri, row) in t.data.iter().enumerate() { - let row_name = decode_wstr(&row.name, utf8); - for (fi, slot, label, tb) in &keep { - let source = row - .string_values - .get(*slot) - .map(|w| decode_wstr(w, utf8)) - .unwrap_or_default(); - if !has_translatable_text(&source) { - continue; - } - // A folder-picker (type_byte 1) cell whose value is an asset path is not text. - if *tb == 1 && value_is_asset_path(&source) { - continue; - } - // A list of asset paths (multi-line .png CG list, bare folder), any field type. - if value_is_asset_path_list(&source) { - continue; - } - // A short lowercase id/code value (英字id, abc, skill-tree layer flags). DB-only. - if is_db_short_code(&source) { - continue; - } - lines.push(DbLine { - type_id: ti, - row: ri, - field: *fi, - row_name: row_name.clone(), - field_label: label.clone(), - source, - }); - } - } - if !lines.is_empty() { - groups.push(DbGroup { - type_id: ti, - type_label: bilingual_name(&type_name, glossary), - lines, - }); - } - } - groups -} - -/// Render DB player-text groups as translation JSON. `text` starts equal to `source`. -pub fn db_strings_to_json(file: &str, groups: &[DbGroup]) -> String { - let mut s = String::new(); - s.push_str("{\n"); - let _ = write!(s, " \"file\": {},\n", jstr(file)); - s.push_str(" \"kind\": \"db\",\n"); - s.push_str(" \"groups\": [\n"); - for (gi, g) in groups.iter().enumerate() { - s.push_str(" {\n"); - let _ = write!(s, " \"type\": {},\n", g.type_id); - let _ = write!(s, " \"typeName\": {},\n", jstr(&g.type_label)); - s.push_str(" \"lines\": [\n"); - for (li, ln) in g.lines.iter().enumerate() { - let _ = write!( - s, - " {{\"row\": {}, \"field\": {}, \"rowName\": {}, \"fieldName\": {}, \"source\": {}, \"text\": {}}}", - ln.row, - ln.field, - jstr(&ln.row_name), - jstr(&ln.field_label), - jstr(&ln.source), - jstr(&ln.source) - ); - s.push_str(if li + 1 == g.lines.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n }"); - s.push_str(if gi + 1 == groups.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n}\n"); - s -} - -/// Apply DB translations back onto a database, keyed by type/row/field. Only lines whose `text` -/// differs from `source` and still match the base cell are changed. Unchanged cells keep their -/// exact bytes, so the database re-serializes byte-exact except for the edited strings. -pub fn inject_db_strings( - json: &str, - db: &mut Database, - opts: &InjectOptions, -) -> Result { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let utf8 = db.utf8; - let groups = root - .get("groups") - .and_then(Value::as_array) - .ok_or("DB translation JSON has no `groups` array")?; - let mut stats = InjectStats::default(); - for g in groups { - let ti = u(g, "type")? as usize; - let dt = db - .types - .get_mut(ti) - .ok_or_else(|| format!("type id {ti} out of range"))?; - let fields_size = (dt.dat_fields_size as usize).min(dt.fields.len()); - // Map field index to its string-value slot. Only string fields have one. - let slot_of: HashMap = value_slots(dt, fields_size, utf8) - .into_iter() - .enumerate() - .filter(|(_, (_, is_str, _))| *is_str) - .map(|(fi, (_, _, slot))| (fi, slot)) - .collect(); - let lines = g - .get("lines") - .and_then(Value::as_array) - .ok_or("DB group has no `lines`")?; - for ln in lines { - let ri = u(ln, "row")? as usize; - let fi = u(ln, "field")? as usize; - let source = ln.get("source").and_then(Value::as_str).unwrap_or(""); - let text = ln.get("text").and_then(Value::as_str).unwrap_or(source); - if text == source { - stats.untranslated += 1; - continue; - } - let Some(&slot) = slot_of.get(&fi) else { - stats.drifted += 1; - continue; - }; - let Some(row) = dt.data.get_mut(ri) else { - stats.drifted += 1; - continue; - }; - let Some(cell) = row.string_values.get_mut(slot) else { - stats.drifted += 1; - continue; - }; - if decode_wstr(cell, utf8) != source { - stats.drifted += 1; - continue; - } - let loc = || format!("db type {ti} row {ri} field {fi}"); - write_translation(cell, source, text, utf8, opts, &mut stats, &loc); - } - } - Ok(stats) -} - -// ---------------------------------------------------------------------------- -// Game.dat player-text extraction + injection (Title / TitlePlus / messages) -// ---------------------------------------------------------------------------- - -/// The player-facing Game.dat strings, keyed by a stable label. Only these are extracted for -/// translation. Fonts, graphics, the decrypt key, and the fixed magic string are structural and -/// never shown to the player. The accessor pairs each key with the relevant `Option` cell. -/// Some cells are gated on the file's `string_count`, so absent cells are skipped. -fn game_dat_fields(gd: &GameDat) -> Vec<(&'static str, &wolf_formats::WStr)> { - let mut out: Vec<(&'static str, &wolf_formats::WStr)> = vec![("Title", &gd.title)]; - if let Some(s) = &gd.title_plus { - out.push(("TitlePlus", s)); - } - if let Some(s) = &gd.start_up_msg { - out.push(("StartUpMsg", s)); - } - if let Some(s) = &gd.title_msg { - out.push(("TitleMsg", s)); - } - out -} - -/// Extract the player-facing Game.dat strings as translation JSON. Each line is `{key, source, -/// text}` with `text` starting equal to `source`. Empty strings are skipped. `source` is kept -/// for drift detection on inject. -pub fn extract_game_dat(gd: &GameDat) -> String { - let utf8 = gd.utf8; - let mut lines: Vec<(&'static str, String)> = Vec::new(); - for (key, w) in game_dat_fields(gd) { - let source = decode_wstr(w, utf8); - if source.is_empty() { - continue; - } - lines.push((key, source)); - } - - let mut s = String::new(); - s.push_str("{\n"); - s.push_str(" \"file\": \"Game.dat\",\n"); - s.push_str(" \"kind\": \"gamedat\",\n"); - s.push_str(" \"lines\": [\n"); - for (li, (key, source)) in lines.iter().enumerate() { - let _ = write!( - s, - " {{\"key\": {}, \"source\": {}, \"text\": {}}}", - jstr(key), - jstr(source), - jstr(source) - ); - s.push_str(if li + 1 == lines.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n}\n"); - s -} - -/// Apply a Game.dat translation JSON back onto the parsed file, keyed by field name. A line is -/// applied only when its `text` differs from `source` and the base cell still holds `source` -/// (drift guard). The shared [`write_translation`] helper enforces the control-code and encoding -/// guards, so untouched fields keep their exact bytes and the file re-serializes byte-exact. -pub fn inject_game_dat( - json: &str, - gd: &mut GameDat, - opts: &InjectOptions, -) -> Result { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let utf8 = gd.utf8; - let lines = root - .get("lines") - .and_then(Value::as_array) - .ok_or("Game.dat translation JSON has no `lines` array")?; - let mut stats = InjectStats::default(); - for ln in lines { - let key = ln - .get("key") - .and_then(Value::as_str) - .ok_or("Game.dat line missing `key`")?; - let source = ln.get("source").and_then(Value::as_str).unwrap_or(""); - let text = ln.get("text").and_then(Value::as_str).unwrap_or(source); - if text == source { - stats.untranslated += 1; - continue; - } - // Resolve the target cell for this key. Only the gated optionals can be absent. - let Some(cell) = game_dat_cell(gd, key) else { - stats.drifted += 1; - continue; - }; - // Drift guard: only overwrite if the base still holds the recorded source text. - if decode_wstr(cell, utf8) != source { - stats.drifted += 1; - continue; - } - let loc = || format!("gamedat {key}"); - write_translation(cell, source, text, utf8, opts, &mut stats, &loc); - } - Ok(stats) -} - -/// Mutable accessor for a player-facing Game.dat field by its extraction key. None if the cell -/// is absent for this file's `string_count`. -fn game_dat_cell<'a>(gd: &'a mut GameDat, key: &str) -> Option<&'a mut wolf_formats::WStr> { - match key { - "Title" => Some(&mut gd.title), - "TitlePlus" => gd.title_plus.as_mut(), - "StartUpMsg" => gd.start_up_msg.as_mut(), - "TitleMsg" => gd.title_msg.as_mut(), - _ => None, - } -} - -fn jstr(s: &str) -> String { - let mut out = String::with_capacity(s.len() + 2); - out.push('"'); - for c in s.chars() { - match c { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if (c as u32) < 0x20 => { - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), - } - } - out.push('"'); - out -} - -// ---------------------------------------------------------------------------- -// Game.dat FULL editing (every editable string field, not just player text) -// ---------------------------------------------------------------------------- -// -// A broader view than the player-text `extract_game_dat`/`inject_game_dat` pair above. -// `dump_game_dat`/`apply_game_dat` expose all editable string fields (fonts, graphics, image -// paths, the opaque trailing string) as a flat JSON object, mirroring the `db-json`/`db-apply` -// design. Structural fields (indicator, magic, decrypt key, magic string, and the size/offset -// housekeeping) are never exposed and never touched. `GameDat::write` recomputes the three -// housekeeping offsets from the edited string lengths, so untouched fields re-serialize -// byte-exact. - -/// The non-optional editable scalar string fields, keyed by their JSON name. Every Game.dat -/// carries them, so they round-trip unconditionally, including when empty. `SubFonts` is emitted -/// separately by [`dump_game_dat`] since it is an array, not a scalar. -fn game_dat_full_fixed(gd: &GameDat) -> Vec<(&'static str, &wolf_formats::WStr)> { - vec![ - ("Title", &gd.title), - ("Font", &gd.font), - ("DefaultPcGraphic", &gd.default_pc_graphic), - ] -} - -/// The optional editable string fields, keyed by their JSON name. Present only when the file's -/// `string_count` carries them. `dump_game_dat` skips a `None` with no phantom key, and -/// `apply_game_dat` refuses to materialize one that was absent, editing only existing cells. -fn game_dat_full_optionals(gd: &GameDat) -> Vec<(&'static str, Option<&wolf_formats::WStr>)> { - vec![ - ("TitlePlus", gd.title_plus.as_ref()), - ("RoadImg", gd.road_img.as_ref()), - ("GaugeImg", gd.gauge_img.as_ref()), - ("StartUpMsg", gd.start_up_msg.as_ref()), - ("TitleMsg", gd.title_msg.as_ref()), - ("UnknownString14", gd.unknown_string14.as_ref()), - ] -} - -/// Mutable accessor for a single full-view scalar field by its JSON key. Returns `None` when the -/// key is unknown or refers to an optional cell that is absent for this file, so `apply_game_dat` -/// never creates or destroys an `Option`. `SubFonts` is handled separately since it is an array. -fn game_dat_full_cell<'a>( - gd: &'a mut GameDat, - key: &str, -) -> Option<&'a mut wolf_formats::WStr> { - match key { - "Title" => Some(&mut gd.title), - "Font" => Some(&mut gd.font), - "DefaultPcGraphic" => Some(&mut gd.default_pc_graphic), - "TitlePlus" => gd.title_plus.as_mut(), - "RoadImg" => gd.road_img.as_mut(), - "GaugeImg" => gd.gauge_img.as_mut(), - "StartUpMsg" => gd.start_up_msg.as_mut(), - "TitleMsg" => gd.title_msg.as_mut(), - "UnknownString14" => gd.unknown_string14.as_mut(), - _ => None, - } -} - -/// Dump every editable Game.dat string field to a pretty JSON object keyed by field name, with -/// each `WStr` decoded to a UTF-8 string. `encoding` (`"utf8"`/`"shiftjis"`) is informational and -/// read-only. Optional fields absent for this file's `string_count` are omitted with no phantom -/// keys. Empty strings are kept as empty strings. `SubFonts` is a 3-element array. The inverse is -/// [`apply_game_dat`]. The round-trip (dump, apply, [`GameDat::write`]) reproduces the file -/// byte-exact when no value was changed. -pub fn dump_game_dat(gd: &GameDat) -> String { - let utf8 = gd.utf8; - let mut s = String::new(); - s.push_str("{\n"); - let _ = write!( - s, - " \"encoding\": {},\n", - jstr(if utf8 { "utf8" } else { "shiftjis" }) - ); - - // Fixed scalars, always present. - for (key, w) in game_dat_full_fixed(gd) { - let _ = write!(s, " {}: {},\n", jstr(key), jstr(&decode_wstr(w, utf8))); - } - - // SubFonts is a fixed 3-element array. - s.push_str(" \"SubFonts\": ["); - for (i, f) in gd.sub_fonts.iter().enumerate() { - if i > 0 { - s.push_str(", "); - } - s.push_str(&jstr(&decode_wstr(f, utf8))); - } - s.push_str("],\n"); - - // Optionals: emit only those present for this file. Track the last one to drop the trailing - // comma without a dangling separator, since the object's prior lines all end in `,`. - let present: Vec<(&'static str, String)> = game_dat_full_optionals(gd) - .into_iter() - .filter_map(|(k, w)| w.map(|w| (k, decode_wstr(w, utf8)))) - .collect(); - for (i, (key, val)) in present.iter().enumerate() { - let sep = if i + 1 == present.len() { "\n" } else { ",\n" }; - let _ = write!(s, " {}: {}{}", jstr(key), jstr(val), sep); - } - if present.is_empty() { - // No optionals: the SubFonts line above still ended in `,`. Trim it to keep valid JSON. - if let Some(stripped) = s.strip_suffix(",\n") { - s = format!("{stripped}\n"); - } - } - - s.push_str("}\n"); - s -} - -/// Apply a full-view Game.dat JSON (from [`dump_game_dat`]) back onto a parsed file. For each -/// present, recognized key, the field is re-encoded to the file's encoding (the same `WStr` path -/// as `inject_game_dat`) and set. A value equal to the current one is a no-op. Returns the number -/// of fields actually changed. -/// -/// Guarantees: never creates or destroys an `Option` field that was not present in the base. A -/// key for an absent optional is treated as drift, counted nowhere, and skipped with a warning. -/// Never touches structural fields. The `encoding` key is read-only. Unknown keys are ignored -/// with a warning. Errors only on malformed JSON or a value not representable in the file's -/// encoding. -pub fn apply_game_dat(gd: &mut GameDat, json: &str) -> Result { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let obj = root - .as_object() - .ok_or("Game.dat full JSON must be a JSON object")?; - let utf8 = gd.utf8; - let mut changed = 0usize; - - // Recognized keys: scalars, the SubFonts array, and the read-only encoding marker. Lets - // unknown keys be reported without confusing them for structural fields. - const SCALAR_KEYS: &[&str] = &[ - "Title", - "Font", - "DefaultPcGraphic", - "TitlePlus", - "RoadImg", - "GaugeImg", - "StartUpMsg", - "TitleMsg", - "UnknownString14", - ]; - - for (key, val) in obj { - match key.as_str() { - "encoding" => {} // informational, read-only - "SubFonts" => { - let arr = val.as_array().ok_or("`SubFonts` must be a JSON array")?; - if arr.len() != gd.sub_fonts.len() { - return Err(format!( - "`SubFonts` must have exactly {} entries (got {})", - gd.sub_fonts.len(), - arr.len() - )); - } - for (i, entry) in arr.iter().enumerate() { - let text = entry - .as_str() - .ok_or("`SubFonts` entries must be strings")?; - let cur = decode_wstr(&gd.sub_fonts[i], utf8); - if cur == text { - continue; - } - let w = text_to_wstr(text, utf8) - .map_err(|e| format!("SubFonts[{i}]: {e}"))?; - gd.sub_fonts[i] = w; - changed += 1; - } - } - k if SCALAR_KEYS.contains(&k) => { - let text = val - .as_str() - .ok_or_else(|| format!("`{k}` must be a string"))?; - // Optional cells absent for this file must not be materialized. Treat as drift. - let Some(cell) = game_dat_full_cell(gd, k) else { - eprintln!( - "gamedat-apply: `{k}` is not present in this Game.dat (string_count too low); skipping" - ); - continue; - }; - if decode_wstr(cell, utf8) == text { - continue; // no change - } - let w = text_to_wstr(text, utf8).map_err(|e| format!("{k}: {e}"))?; - *cell = w; - changed += 1; - } - other => { - eprintln!("gamedat-apply: ignoring unknown key `{other}`"); - } - } - } - Ok(changed) -} - -// ---------------------------------------------------------------------------- -// Coverage audit (QA): every string arg in the corpus plus whether extraction keeps -// it, so precision (kept-but-not-text) and recall (text-but-dropped) can be judged -// against the actual bytes rather than guessed. -// ---------------------------------------------------------------------------- - -/// One string occurrence in a command stream, tagged with extraction status. -struct StrAudit { - cid: u32, - cmd: usize, - str_idx: usize, - text: String, - extracted: bool, -} - -/// Every non-empty string arg of every command in a list, tagged with whether the player-text -/// extractor keeps it. Matched against `scene_lines`, so the allowlist and filters are exactly -/// the live ones. -fn audit_commands(cmds: &[RawCommand], utf8: bool) -> Vec { - use std::collections::HashSet; - // The QA audit reports raw extraction coverage, so the Debug-event gate is left off here. - let kept: HashSet<(usize, usize)> = scene_lines(cmds, utf8, false) - .iter() - .map(|l| (l.cmd, l.str_idx)) - .collect(); - let mut out = Vec::new(); - for (ci, cmd) in cmds.iter().enumerate() { - for (si, w) in cmd.str_args.iter().enumerate() { - let text = decode_wstr(w, utf8); - if text.is_empty() { - continue; - } - out.push(StrAudit { - cid: cmd.cid, - cmd: ci, - str_idx: si, - text, - extracted: kept.contains(&(ci, si)), - }); - } - } - out -} - -fn write_audit_entries(s: &mut String, evt: &str, page: Option, entries: &[StrAudit]) { - for e in entries { - let _ = write!( - s, - " {{\"event\": {evt}, \"page\": {}, \"cmd\": {}, \"cid\": {}, \"cmdName\": {}, \"str\": {}, \"extracted\": {}, \"text\": {}}},\n", - page.map(|p| p.to_string()).unwrap_or_else(|| "null".into()), - e.cmd, - e.cid, - jstr(&crate::spec::command_name(e.cid)), - e.str_idx, - e.extracted, - jstr(&e.text), - ); - } -} - -/// Full per-string coverage dump of a common-events file. Every event, every string arg. -pub fn audit_common_events_to_json(ce: &CommonEventsFile) -> String { - let mut s = String::from("{\n \"kind\": \"common\",\n \"entries\": [\n"); - for ev in &ce.events { - write_audit_entries( - &mut s, - &ev.int_id.to_string(), - None, - &audit_commands(&ev.commands, ce.utf8), - ); - } - finish_audit(s) -} - -/// Full per-string coverage dump of a map. Every event, every page, every string arg. -pub fn audit_map_to_json(map: &Map) -> String { - let mut s = String::from("{\n \"kind\": \"map\",\n \"entries\": [\n"); - for ev in &map.events { - for (pi, page) in ev.pages.iter().enumerate() { - write_audit_entries( - &mut s, - &ev.id.to_string(), - Some(pi), - &audit_commands(&page.commands, map.utf8), - ); - } - } - finish_audit(s) -} - -/// Full per-cell coverage dump of a database. Every string cell across player and internal -/// fields, tagged with `playerField` (whether the field name classifies as content) and -/// `extracted`. -pub fn audit_db_to_json(db: &Database, glossary: &HashMap) -> String { - let utf8 = db.utf8; - let mut s = String::from("{\n \"kind\": \"db\",\n \"entries\": [\n"); - for (ti, t) in db.types.iter().enumerate() { - let type_name = decode_wstr(&t.name, utf8); - let fields_size = (t.dat_fields_size as usize).min(t.fields.len()); - let slots = value_slots(t, fields_size, utf8); - let mut seen_string = false; - for (fi, (_, is_str, slot)) in slots.iter().enumerate() { - if !is_str { - continue; - } - let is_first_string = !seen_string; - seen_string = true; - let fjp = decode_wstr(&t.fields[fi].name, utf8); - let fen = glossary.get(&fjp).map(String::as_str).unwrap_or(&fjp); - let type_byte = t.fields[fi].type_byte; - let is_player = db_player_field(type_byte, &type_name, &fjp, fen, is_first_string); - let field_args = &t.fields[fi].string_args; - // First string-arg of a field config is the file folder or DB-ref selector hint. - let cfg0 = field_args - .first() - .map(|w| decode_wstr(w, utf8)) - .unwrap_or_default(); - for (ri, row) in t.data.iter().enumerate() { - let text = row - .string_values - .get(*slot) - .map(|w| decode_wstr(w, utf8)) - .unwrap_or_default(); - if text.is_empty() { - continue; - } - let extracted = is_player - && has_translatable_text(&text) - && !(type_byte == 1 && value_is_asset_path(&text)); - let _ = write!( - s, - " {{\"type\": {ti}, \"typeName\": {}, \"row\": {ri}, \"rowName\": {}, \"field\": {fi}, \"fieldName\": {}, \"typeByte\": {type_byte}, \"cfg0\": {}, \"playerField\": {is_player}, \"extracted\": {extracted}, \"text\": {}}},\n", - jstr(&type_name), - jstr(&decode_wstr(&row.name, utf8)), - jstr(&fjp), - jstr(&cfg0), - jstr(&text), - ); - } - } - } - finish_audit(s) -} - -/// Trim the trailing `,\n` and close the audit JSON array and object. -fn finish_audit(mut s: String) -> String { - if s.ends_with(",\n") { - s.truncate(s.len() - 2); - s.push('\n'); - } - s.push_str(" ]\n}\n"); - s -} - -#[cfg(test)] -mod tests { - use super::*; - use wolf_formats::WStr; - - #[test] - fn strip_handles_multichar_codes_and_nesting() { - // Multi-char names must vanish whole, not decay to `self`. - assert_eq!(strip_control_codes("\\cself[6]"), ""); - assert_eq!(strip_control_codes("\\space[0]"), ""); - // Nested codes: the font code's argument is itself a substitution. - assert_eq!(strip_control_codes("\\f[\\cself[19]]\\cself[6]"), ""); - // Symbol codes and alignment tags. - assert_eq!( - strip_control_codes("\\space[0]\\f[\\cself[17]]\\-[1]\\E\\cself[6]"), - "" - ); - // Real words survive, with their surrounding substitutions stripped for the check. - assert_eq!( - strip_control_codes("「\\cself[8]」を手に入れた。"), - "「」を手に入れた。" - ); - } - - #[test] - fn strip_keeps_ruby_base_drops_reading() { - // Ruby `\r[base,reading]`: the base text is kept as the translatable word. The reading - // and the markup are dropped. A ruby-only line therefore counts as translatable. - assert!(strip_control_codes("\\r[市松,いちまつ]").contains("市松")); - assert_eq!(strip_control_codes("\\r[市松,いちまつ]"), "市松"); - // Ruby base inline among other text and codes. - assert_eq!( - strip_control_codes("\\r[市松,いちまつ]\\cself[3]さん"), - "市松さん" - ); - // An escaped literal `\\r[..]` is not ruby. The first `\` and second `\` parse as the - // `\\` single-symbol code, leaving `r[人生,じんせい]` as plain text, so the base is not - // re-emitted. The literal `r[..]` including the reading stays. - assert_eq!( - strip_control_codes("\\\\r[人生,じんせい]"), - "r[人生,じんせい]" - ); - // The recall check (used by `has_translatable_text`) sees the ruby base as words. - assert!(has_translatable_text("\\r[市松,いちまつ]")); - } - - #[test] - fn displayable_literal_keeps_coded_text_rejects_internal() { - // Real UI text that embeds an inline code must be kept. A plain `contains('\\')` path - // test would wrongly drop all of these. - assert!(displayable_text("\\f[\\cself[19]]Sell")); - assert!(displayable_text("Buy Voucher A (\\cself[20])")); - assert!(displayable_text("\\cself[11]rose!")); - assert!(displayable_text("\\c[1]3000 chips\\c[0] to start")); - // A forward slash inside a sentence is ordinary punctuation. Keep it. - assert!(displayable_text( - "Save cursor position when opening Skills/Items" - )); - // Internal: engine directive, asset paths, bare path tokens, pure substitution. - assert!(!displayable_text( - "<>■主人公ピクセル移動切り替え" - )); - assert!(!displayable_text("<< \\sys[100] % >>")); - assert!(!displayable_text("Save/System.sav")); - assert!(!displayable_text("Data/")); - assert!(!displayable_text("SE/door.ogg")); - assert!(!displayable_text("\\cself[5]")); - } - - #[test] - fn asset_path_values_rejected_names_kept() { - // type_byte==1 folder-picker cells: a path value is excluded, a plain display name is kept. - assert!(value_is_asset_path("Base_window/WindowBaseA.png")); - assert!(value_is_asset_path("SE/[System]Cancel02.ogg")); - assert!(value_is_asset_path( - "BalloonBase_CafeLatte/[CafeLatte]nameL.png" - )); - assert!(!value_is_asset_path("Poison")); // a status 表示名 - assert!(!value_is_asset_path("毒")); // a JP status name - assert!(!value_is_asset_path("Game Over")); // a name with a space, no ext - assert!(!value_is_asset_path("")); // empty or NUL-only - assert!(!value_is_asset_path("\u{0}")); - } - - #[test] - fn code_multiset_detects_dropped_or_altered_codes() { - // Same codes in any order compare equal. A dropped, altered, or added code compares different. - assert_eq!( - code_multiset("「\\cself[8]」を手に入れた。"), - vec!["\\cself[8]"] - ); - assert_eq!( - code_multiset("Got \\cself[8]!"), - code_multiset("「\\cself[8]」") - ); - assert_ne!(code_multiset("「\\cself[8]」"), code_multiset("Got it!")); // dropped - assert_ne!(code_multiset("\\cself[8]"), code_multiset("\\cself[9]")); // ref changed - // @-window prefix and / alignment tags are tracked. - assert_eq!( - code_multiset("@1\n\\s[9]Hi"), - vec!["@1".to_string(), "\\s[9]".to_string()] - ); - assert_ne!(code_multiset("@1\nHi"), code_multiset("Hi")); // dropped @ prefix - assert_eq!(code_multiset("\\f[\\cself[3]]X").len(), 2); // plus nested \f[..] - } - - #[test] - fn code_multiset_excludes_ruby_keeps_others() { - // Ruby `\r[..]` is droppable markup, so it never enters the must-preserve set. A - // translation may drop it without --allow-code-drift. - assert!(code_multiset("\\r[市松,いちまつ]").is_empty()); - // Other codes around a ruby are still required. Only the `\r[..]` is excluded. - assert_eq!( - code_multiset("\\r[x,y]\\cself[3]"), - vec!["\\cself[3]".to_string()] - ); - // A ruby-bearing source vs a ruby-dropped translation therefore have equal code sets. - assert_eq!( - code_multiset("\\r[市松,いちまつ]"), - code_multiset("Ichimatsu") - ); - // An escaped `\\r[..]` is the `\\` code plus literal `r[..]` text. The `\\` is preserved. - assert_eq!( - code_multiset("\\\\r[人生,じんせい]"), - vec!["\\\\".to_string()] - ); - } - - #[test] - fn normalize_en_punct_maps_cjk_keeps_rest() { - // Brackets become straight quotes and the wave dash becomes a tilde. The decorative ♥ - // is untouched. - assert_eq!(normalize_en_punct("「テスト」~♥"), "\"テスト\"~♥"); - // Control codes are pure ASCII and pass through unchanged. - assert_eq!(normalize_en_punct("\\cself[8]"), "\\cself[8]"); - assert_eq!(normalize_en_punct("@1\n\\s[9]Hi"), "@1\n\\s[9]Hi"); - // The full map, both 1:1 and multi-char expansions. - assert_eq!(normalize_en_punct("『』【】〔〕"), "\"\"[][]"); - assert_eq!(normalize_en_punct("〈〉《》"), "<><>"); - assert_eq!(normalize_en_punct("。、,・"), ".,,-"); - assert_eq!(normalize_en_punct("!?:;%&"), "!?:;%&"); - assert_eq!(normalize_en_punct("…‥※"), ".....*"); // `...` then `..` then `*` - assert_eq!(normalize_en_punct("―─-—‐"), "-----"); - assert_eq!(normalize_en_punct("“”‘’"), "\"\"''"); - assert_eq!(normalize_en_punct("×−÷"), "x-/"); - assert_eq!(normalize_en_punct("→←↑↓"), "-><-^v"); - assert_eq!(normalize_en_punct("■□"), "**"); - // Decorative symbols and kana, kanji, and ASCII are left exactly as-is. - assert_eq!(normalize_en_punct("♥♪✨♂ abc 日本語"), "♥♪✨♂ abc 日本語"); - } - - #[test] - fn write_translation_ruby_drop_ok_but_cself_drop_rejected() { - let loc = || "test".to_string(); - // Dropping the ruby markup is accepted by default without --allow-code-drift. The source - // ruby code is excluded from the must-preserve set, so "Ichimatsu" lands cleanly. - let mut cell = WStr::from("\\r[市松,いちまつ]"); - let mut stats = InjectStats::default(); - write_translation( - &mut cell, - "\\r[市松,いちまつ]", - "Ichimatsu", - true, - &InjectOptions::default(), - &mut stats, - &loc, - ); - assert_eq!(stats.applied, 1, "dropping ruby markup must be accepted"); - assert_eq!(stats.code_mismatch, 0); - assert_eq!(decode_wstr(&cell, true), "Ichimatsu"); - - // Dropping a real `\cself[..]` code is still rejected. The line is left untouched. - let mut cell = WStr::from("「\\cself[8]」を手に入れた。"); - let mut stats = InjectStats::default(); - write_translation( - &mut cell, - "「\\cself[8]」を手に入れた。", - "Got the item!", - true, - &InjectOptions::default(), - &mut stats, - &loc, - ); - assert_eq!(stats.applied, 0, "dropping \\cself must be rejected"); - assert_eq!(stats.code_mismatch, 1); - // Cell untouched, still the source bytes. - assert_eq!(decode_wstr(&cell, true), "「\\cself[8]」を手に入れた。"); - } - - #[test] - fn write_translation_en_punct_lands_straight_quotes() { - let loc = || "test".to_string(); - // With normalize_punct, a translation containing 「」 lands as straight quotes. - let mut cell = WStr::from("テスト"); - let mut stats = InjectStats::default(); - write_translation( - &mut cell, - "テスト", - "「Hello」", - true, - &InjectOptions { - allow_code_drift: false, - normalize_punct: true, - }, - &mut stats, - &loc, - ); - assert_eq!(stats.applied, 1); - assert_eq!(decode_wstr(&cell, true), "\"Hello\""); - - // Off by default, the CJK brackets are preserved verbatim. - let mut cell = WStr::from("テスト"); - let mut stats = InjectStats::default(); - write_translation( - &mut cell, - "テスト", - "「Hello」", - true, - &InjectOptions::default(), - &mut stats, - &loc, - ); - assert_eq!(stats.applied, 1); - assert_eq!(decode_wstr(&cell, true), "「Hello」"); - } - - #[test] - fn field_name_content_classifier() { - // A cid250 by-name write into a name or a description/dialogue field is player text, so - // the command-stream test accepts everything that is not genuinely internal, the Name or - // Content union. Description/dialogue and name labels both pass. - assert!(field_name_is_content("説明文")); // description - assert!(field_name_is_content("項目文")); // menu-item label (a name) - assert!(field_name_is_content("愛称")); // nickname (a name) - // A non-keyword label is not internal, so it is player text (Role::Content). The 付与文字列 - // affix sentinels (`x` and `------`) are still dropped by the str0 translatable gate in - // `player_str_indices`, not by this field test (see player_str_indices_unchanged_on_*). - assert!(field_name_is_content("付与文字列[前]")); - // Genuinely-internal fields are still rejected. - assert!(!field_name_is_content("ファイル")); - assert!(!field_name_is_content("戦闘背景画像")); - } - - #[test] - fn db_field_role_three_way_split() { - // (1) Internal: non-string type_byte, temp/separator type, or genuine-internal field. - assert_eq!( - db_field_role(2, "Monster", "名前", "Name", true), - Role::Internal - ); - assert_eq!( - db_field_role(0, "×temp", "名前", "Name", true), - Role::Internal - ); - assert_eq!( - db_field_role(0, "------", "名前", "Name", true), - Role::Internal - ); - assert_eq!( - db_field_role(0, "Monster", "ファイル", "File", false), - Role::Internal - ); - assert_eq!( - db_field_role(0, "Skill", "戦闘背景画像", "Graphic", false), - Role::Internal - ); - // (2) Content: a description/dialogue field or type. `セリフ`/`セリフ`/`効果文` stay in DESC. - assert_eq!( - db_field_role(0, "Skill", "説明", "Description", false), - Role::Content - ); - assert_eq!( - db_field_role(0, "Skill", "効果文", "Description", false), - Role::Content - ); - assert_eq!( - db_field_role(0, "会話セリフ", "", "", false), - Role::Content - ); - assert_eq!( - db_field_role(0, "Item", "メッセージ", "Message", false), - Role::Content - ); - // A description field still wins even on the first-string column. DESC beats Name. - assert_eq!( - db_field_role(0, "Help", "説明", "Description", true), - Role::Content - ); - // (3) Name: first string column (even unlabeled), or a recognised name field or type. - assert_eq!(db_field_role(0, "Monster", "", "", true), Role::Name); - assert_eq!( - db_field_role(0, "Item", "謎ラベル", "謎ラベル", true), - Role::Name - ); - assert_eq!( - db_field_role(0, "Trainer", "愛称", "愛称", false), - Role::Name - ); - assert_eq!( - db_field_role(0, "Element", "呼び方", "呼び方", false), - Role::Name - ); - // `移動名` is the displayed location banner, so it is a Name not Internal and the glossary owns it. - assert_eq!( - db_field_role(0, "Map", "日本語移動名", "Move Name", false), - Role::Name - ); - assert_eq!( - db_field_role(0, "用語設定", "", "", false), - Role::Name - ); - // (4) Default Content: an unlabeled or unrecognised non-internal, non-first field. - assert_eq!(db_field_role(0, "Misc", "", "", false), Role::Content); - assert_eq!( - db_field_role(0, "Misc", "謎ラベル", "謎ラベル", false), - Role::Content - ); - } - - #[test] - fn db_player_field_is_content_only() { - // db_player_field (db-strings ownership) is true only for Role::Content. - assert!(db_player_field(0, "Skill", "説明", "Description", false)); // Content - assert!(!db_player_field(0, "Monster", "名前", "Name", true)); // Name, glossary owns it - assert!(!db_player_field(0, "Monster", "", "", true)); // first-string Name - assert!(!db_player_field(0, "Map", "日本語移動名", "Move Name", false)); // Name - assert!(!db_player_field(0, "Monster", "ファイル", "File", false)); // Internal - // Default (unlabeled non-first) is Content, so db-strings keeps it. - assert!(db_player_field(0, "Misc", "謎ラベル", "謎ラベル", false)); - } - - #[test] - fn translatable_filter_rejects_pure_substitution() { - // Pure variable refs or UI glyphs. Nothing for a translator. - assert!(!has_translatable_text("\\cself[5]")); - assert!(!has_translatable_text("\\f[\\cself[18]]\\E▲")); - assert!(!has_translatable_text("\\f[\\cself[19]]\\cself[6]")); - assert!(!has_translatable_text( - "\\space[0]\\f[\\cself[17]]\\-[1]\\E\\cself[6]" - )); - assert!(!has_translatable_text("+")); - // Real text, including text wrapped around an embedded substitution. - assert!(has_translatable_text("No data available")); - assert!(has_translatable_text("「\\cself[8]」を手に入れた。")); - assert!(has_translatable_text("やめる")); - } - - #[test] - fn translatable_filter_rejects_engine_data_tokens() { - // Single-letter id/index cells (英字id columns, skill-tree layer flags) and CG/asset codes. - for junk in [ - "a", "b", "z", "5", "A006", "A000", "nx1", "_syst00", "m_09_01", "mc01", "CharaChip2", - ] { - assert!(!has_translatable_text(junk), "{junk:?} should be filtered as engine data"); - } - // A variable-expression that strips to a bare underscore identifier. - assert!(!has_translatable_text("_syst00\\cself[10]")); - // Real text must survive: plain ASCII words, labels, measurements, and any CJK. - for keep in [ - "Sell", - "HP", - "OK", - "Nofile", - "Lv", - "No data available", - "Game Over", - "138cm\r\n36kg", - "アイリス", - "全回復", - "一", // a lone kanji can be a name, never engine data - ] { - assert!(has_translatable_text(keep), "{keep:?} should be kept as translatable"); - } - } - - #[test] - fn noise_token_never_fires_on_cjk() { - // Even a one-character kanji or a kanji glued to ASCII is not engine data. - assert!(!is_noise_token("一")); - assert!(!is_noise_token("S淫乱度")); - assert!(is_noise_token("a")); - assert!(is_noise_token("A006")); - assert!(!is_noise_token("Sell")); - } - - #[test] - fn angle_token_rejected_but_real_labels_kept() { - // Lone engine placeholders (a \cdb read concatenated with a literal fallback strip to this). - for junk in ["", "", "", "", "
"] { - assert!(is_noise_token(junk), "{junk:?} should be engine noise"); - assert!(!has_translatable_text(junk), "{junk:?} not translatable"); - } - assert!(!has_translatable_text("\\cdb[11:\\cself[21]:0]")); - // CJK menu labels, multiple bracket pairs, and text around a bracket all stay. - for keep in [ - "<装備を外す>", - " ", - "put in the picture", - "★NoFile>\\cself[5]ファイルがありません", - ] { - assert!(has_translatable_text(keep), "{keep:?} should be kept"); - } - } - - #[test] - fn code_list_rejected_but_real_short_lists_kept() { - for junk in [ - "a0,b0,c1,d1,e0,f2,g2,h0,k0,l0,i0", - "a0,b0,c0,d0,e0,f0", - "f3,g3", - "g\\cself[44],h\\cself[44]", - "p\\cself[12],l100", - ",p\\cself[44],q\\cself[44]", - ] { - assert!(is_code_list(junk), "{junk:?} should read as a code list"); - assert!(!has_translatable_text(junk), "{junk:?} not translatable"); - } - // Real short comma lists: capitalised, multi-letter, or CJK leads keep them out of scope. - // None must read as a code list (so the rule never misfires). - for keep in [ - "OK,Cancel", "Yes,No", "HP,MP", "HP,MP,Lv", "Buy,Sell", "Fire,Ice", "S,A,B,C", - "Lv1,Lv2", "L1,R1", "No1,No2", "1,2,3", "100,200", "装備,解除", - ] { - assert!(!is_code_list(keep), "{keep:?} must NOT read as a code list"); - } - // The ones carrying real words stay translatable (the pure-number lists have no letters, so - // they are correctly not translatable regardless, and are not asserted here). - for keep in ["OK,Cancel", "HP,MP", "Buy,Sell", "Fire,Ice", "Lv1,Lv2", "L1,R1", "装備,解除"] { - assert!(has_translatable_text(keep), "{keep:?} should be kept"); - } - } - - #[test] - fn asset_path_list_rejected_but_real_slashes_kept() { - for junk in [ - "EVCG_b0/B000/B0_a000.png\r\nEVCG_b0/B000/B0_d000.png", - "EVCG_a0/A000/", - "EVCG_e0/E000/E0_a000.png\r\nEVCG_e0/E000/E0_o0\\cself[36]0.png", - "b/r/i/j/m/zv/zx", - ] { - assert!(value_is_asset_path_list(junk), "{junk:?} should read as a path list"); - } - // A single slash inside a real phrase, a 2-token ratio, or any CJK stays text. - for keep in ["HP/MP", "Buy/Sell", "スキル/アイテム", "1/4 of max HP", "OK", "<装備を外す>"] { - assert!(!value_is_asset_path_list(keep), "{keep:?} must stay text"); - } - } - - #[test] - fn db_short_code_rejected_but_words_and_names_kept() { - for junk in ["za", "zb", "kc", "la", "ma", "nn", "ac", "abc", "hb", "hc", "kg", "mg", "nd"] { - assert!(is_db_short_code(junk), "{junk:?} should read as a DB short code"); - } - // Words/labels/names: lowercase 2-3 is the only catch, so uppercase or >3 stays. - // (gem/ml are real in the COMMAND stream, where this rule never runs.) - for keep in ["OK", "HP", "Lv", "Iris", "Lisa", "Sion", "Fire", "item", "<装備を外す>", "助ける"] { - assert!(!is_db_short_code(keep), "{keep:?} must NOT read as a short code"); - } - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/symbols.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/symbols.rs deleted file mode 100644 index 864ad67..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/symbols.rs +++ /dev/null @@ -1,308 +0,0 @@ -//! Cross-file symbol table for name resolution. Turns numeric ids in event code into readable -//! names. CommonEvent ids map to names plus per-event self-var and input names from the event's -//! own metadata. Global `V[]`/`S[]`/`Sys[]` ids map to names from the System Database. An optional -//! engine glossary maps stock common-event JP names to English. - -use std::collections::HashMap; - -use serde::Deserialize; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; - -use crate::text::decode_wstr; - -/// The engine-default glossary, generated by the readability workflow and embedded. -static GLOSSARY_JSON: &str = include_str!("../data/glossary.json"); - -/// Engine-text glossary (structural JP to English), generated by the translation workflow. -static ENGINE_GLOSSARY_JSON: &str = include_str!("../data/engine_glossary.json"); - -/// Load the embedded engine-text glossary (structural name translations). -pub fn load_embedded_engine_glossary() -> HashMap { - #[derive(Deserialize)] - struct File { - pairs: Vec, - } - #[derive(Deserialize)] - struct Pair { - jp: String, - en: String, - } - let json = ENGINE_GLOSSARY_JSON.trim_start_matches('\u{feff}'); - match serde_json::from_str::(json) { - Ok(f) => f.pairs.into_iter().map(|p| (p.jp, p.en)).collect(), - Err(_) => HashMap::new(), - } -} - -/// Load the embedded engine-default common-event glossary (stock JP name to English). -pub fn load_embedded_glossary() -> Glossary { - #[derive(Deserialize)] - struct File { - entries: Vec, - } - #[derive(Deserialize)] - struct Entry { - jp_name: String, - en_name: String, - #[serde(default)] - args: Vec, - } - let json = GLOSSARY_JSON.trim_start_matches('\u{feff}'); - match serde_json::from_str::(json) { - Ok(f) => Glossary::from_pairs(f.entries.into_iter().map(|e| { - ( - e.jp_name, - GlossaryEntry { - en_name: e.en_name, - args: e.args, - }, - ) - })), - Err(_) => Glossary::default(), - } -} - -#[derive(Debug, Clone, Default)] -pub struct SymbolTable { - /// CommonEvent int-id to metadata (name, self-var names, input arg names). - pub common_events: HashMap, - /// Global variable names from the System Database. - pub globals: GlobalNames, - /// Databases by kind (UDB/CDB/SDB), each holding type/data/field names. - pub databases: Vec, - /// Engine-default function glossary (stock JP name to English). - pub glossary: Glossary, - /// Engine-text glossary: structural JP name to English (variable, DB type, field, and - /// system-row names). Player-facing game text is absent. - pub engine_text: HashMap, -} - -#[derive(Debug, Clone, Default)] -pub struct CommonEventInfo { - pub name: String, - /// CSelf index to name, from `unknown8`, the per-event self-variable names. - pub self_vars: HashMap, - /// Input argument names in order (leading non-empty `unknown3` entries). - pub inputs: Vec, -} - -#[derive(Debug, Clone, Default)] -pub struct GlobalNames { - /// `V[n]`, normal/global variables ("通常変数名"). - pub normal: HashMap, - /// `S[n]`, string variables ("文字列変数名"). - pub string: HashMap, - /// `Sys[n]`, system variables ("システム変数名"). - pub system: HashMap, -} - -#[derive(Debug, Clone)] -pub struct DatabaseNames { - pub kind: String, - pub types: Vec, -} - -#[derive(Debug, Clone)] -pub struct TypeNames { - pub name: String, - pub data: Vec, - pub fields: Vec, -} - -/// Engine-default glossary: stock common-event JP name to English signature. -#[derive(Debug, Clone, Default)] -pub struct Glossary { - entries: HashMap, -} - -#[derive(Debug, Clone)] -pub struct GlossaryEntry { - pub en_name: String, - pub args: Vec, -} - -impl Glossary { - pub fn from_pairs(pairs: impl IntoIterator) -> Self { - Glossary { - entries: pairs.into_iter().collect(), - } - } - - pub fn get(&self, jp_name: &str) -> Option<&GlossaryEntry> { - self.entries.get(jp_name) - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } -} - -const NAME_NORMAL: &str = "通常変数名"; -const NAME_STRING: &str = "文字列変数名"; -const NAME_SYSTEM: &str = "システム変数名"; -/// Reserve/spare variable groups (`予備変数1`..`予備変数9`). Each names the normal variables at -/// `V[group*100000 + row]`. -const NAME_SPARE: &str = "予備変数"; - -impl SymbolTable { - pub fn new() -> Self { - SymbolTable::default() - } - - /// Register CommonEvent metadata (names plus self-var and input names). - pub fn add_common_events(&mut self, ce: &CommonEventsFile) { - for ev in &ce.events { - let name = decode_wstr(&ev.name, ce.utf8); - - // Self-var names: the unknown8 index equals the CSelf index. - let mut self_vars = HashMap::new(); - for (i, w) in ev.unknown8.iter().enumerate() { - let n = decode_wstr(w, ce.utf8); - if !n.is_empty() { - self_vars.insert(i as u32, n); - } - } - - // Inputs: leading contiguous non-empty unknown3 entries. - let mut inputs = Vec::new(); - for w in &ev.unknown3 { - let n = decode_wstr(w, ce.utf8); - if n.is_empty() { - break; - } - inputs.push(n); - } - - self.common_events.insert( - ev.int_id, - CommonEventInfo { - name, - self_vars, - inputs, - }, - ); - } - } - - /// Register a database's type/data/field names, and for the System Database the global - /// variable name tables. - pub fn add_database(&mut self, kind: &str, db: &Database) { - for t in &db.types { - let type_name = decode_wstr(&t.name, db.utf8); - let target = match type_name.as_str() { - NAME_NORMAL => Some(&mut self.globals.normal), - NAME_STRING => Some(&mut self.globals.string), - NAME_SYSTEM => Some(&mut self.globals.system), - _ => None, - }; - if let Some(map) = target { - for (i, d) in t.data.iter().enumerate() { - let n = decode_wstr(&d.name, db.utf8); - if !n.is_empty() { - map.insert(i as u32, n); - } - } - } - - // Spare/reserve variable groups name V[group*100000 + row]. - if let Some(g) = type_name - .strip_prefix(NAME_SPARE) - .and_then(|s| s.parse::().ok()) - .filter(|g| (1..=9).contains(g)) - { - let base = g * 100_000; - for (i, d) in t.data.iter().enumerate() { - let n = decode_wstr(&d.name, db.utf8); - if !n.is_empty() { - self.globals.normal.insert(base + i as u32, n); - } - } - } - } - - let types = db - .types - .iter() - .map(|t| TypeNames { - name: decode_wstr(&t.name, db.utf8), - data: t - .data - .iter() - .map(|d| decode_wstr(&d.name, db.utf8)) - .collect(), - fields: t - .fields - .iter() - .map(|f| decode_wstr(&f.name, db.utf8)) - .collect(), - }) - .collect(); - self.databases.push(DatabaseNames { - kind: kind.to_string(), - types, - }); - } - - pub fn set_glossary(&mut self, glossary: Glossary) { - self.glossary = glossary; - } - - pub fn set_engine_text(&mut self, engine_text: HashMap) { - self.engine_text = engine_text; - } - - /// Translate a structural name to English via the engine-text glossary, or return it - /// unchanged. Player-facing content is absent from the glossary, so it stays as-is. - pub fn tr<'a>(&'a self, name: &'a str) -> &'a str { - self.engine_text - .get(name) - .map(String::as_str) - .unwrap_or(name) - } - - pub fn common_event(&self, id: u32) -> Option<&CommonEventInfo> { - self.common_events.get(&id) - } - - pub fn common_event_name(&self, id: u32) -> Option<&str> { - self.common_events.get(&id).map(|i| i.name.as_str()) - } - - /// Reverse lookup by exact name (for call-by-name commands). - pub fn common_event_by_name(&self, name: &str) -> Option<&CommonEventInfo> { - self.common_events.values().find(|i| i.name == name) - } - - /// Resolve a database entry (`type name`, `row index`) to its row name, for turning, say, - /// `ItemId=17` into `ItemId="ポーション"`. Searches all loaded databases. - pub fn db_entry_name(&self, type_name: &str, row: u32) -> Option<&str> { - self.db_type(type_name) - .and_then(|t| t.data.get(row as usize)) - .map(String::as_str) - .filter(|s| !s.is_empty()) - } - - /// Resolve a database field (column) index to its name within a type. - pub fn db_field_name(&self, type_name: &str, field: u32) -> Option<&str> { - self.db_type(type_name) - .and_then(|t| t.fields.get(field as usize)) - .map(String::as_str) - .filter(|s| !s.is_empty()) - } - - fn db_type(&self, type_name: &str) -> Option<&TypeNames> { - self.databases - .iter() - .flat_map(|db| &db.types) - .find(|t| t.name == type_name) - } - - /// Which database kind (UDB/CDB/SDB) holds a given type. - pub fn db_kind_of_type(&self, type_name: &str) -> Option<&str> { - self.databases - .iter() - .find(|db| db.types.iter().any(|t| t.name == type_name)) - .map(|db| db.kind.as_str()) - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/text.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/text.rs deleted file mode 100644 index df272a3..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/text.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Shared string decoding for the readable layer. Wolf strings are raw bytes, either -//! Shift-JIS or UTF-8 depending on the file. Decode them to a UTF-8 `String` for -//! display and names. - -use wolf_formats::WStr; - -/// Decode a Wolf string to UTF-8, trimming the trailing NUL UTF-8 files carry. -pub fn decode_wstr(s: &WStr, utf8: bool) -> String { - let mut bytes = s.as_bytes(); - if utf8 && bytes.last() == Some(&0) { - bytes = &bytes[..bytes.len() - 1]; - } - if utf8 { - String::from_utf8_lossy(bytes).into_owned() - } else { - let (text, _, _) = encoding_rs::SHIFT_JIS.decode(bytes); - text.into_owned() - } -} - -/// Decode and escape a string for a WolfScript double-quoted literal. -pub fn quote_wstr(s: &WStr, utf8: bool) -> String { - escape_literal(&decode_wstr(s, utf8)) -} - -/// Render a Wolf string as a byte-exact WolfScript literal. Emits a readable `"text"` when -/// the bytes decode and re-encode identically in the file's encoding, otherwise a raw -/// `x"HEX"` literal carrying the exact bytes including the trailing NUL. The inverse is -/// [`text_to_wstr`] (or the hex path). Used by the recompilable renderer so strings -/// round-trip in any encoding, including inside block openers that bypass the per-command -/// `@raw` fallback. -pub fn wstr_to_literal(w: &WStr, utf8: bool) -> String { - match clean_literal_text(w, utf8) { - Some(text) => escape_literal(&text), - None => format!("x\"{}\"", to_hex(w.as_bytes())), - } -} - -/// Encode already-unescaped literal text into a Wolf string (file encoding + trailing NUL). -/// Errors if the text can't be represented in the file's encoding (e.g. a non-Shift-JIS char -/// hand-typed into a Shift-JIS file). -pub fn text_to_wstr(text: &str, utf8: bool) -> Result { - let mut bytes = encode_body(text, utf8).ok_or_else(|| { - format!( - "text not representable in {}: {text:?}", - if utf8 { "UTF-8" } else { "Shift-JIS" } - ) - })?; - bytes.push(0); - Ok(WStr(bytes)) -} - -/// Decode a string for a readable literal only when it round-trips byte-exact, else `None` to -/// use hex. -fn clean_literal_text(w: &WStr, utf8: bool) -> Option { - let body = w.as_bytes().strip_suffix(&[0u8])?; // Wolf strings carry a trailing NUL. - let text = if utf8 { - std::str::from_utf8(body).ok()?.to_owned() - } else { - let (t, _, had_err) = encoding_rs::SHIFT_JIS.decode(body); - if had_err { - return None; - } - t.into_owned() - }; - // Control chars other than tab/CR/LF have no readable escaped form. Keep them in hex. - if text - .chars() - .any(|c| c.is_control() && !matches!(c, '\t' | '\n' | '\r')) - { - return None; - } - (encode_body(&text, utf8)?.as_slice() == body).then_some(text) -} - -fn encode_body(text: &str, utf8: bool) -> Option> { - if utf8 { - Some(text.as_bytes().to_vec()) - } else { - let (bytes, _, had_err) = encoding_rs::SHIFT_JIS.encode(text); - (!had_err).then(|| bytes.into_owned()) - } -} - -fn escape_literal(text: &str) -> String { - let esc = text - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('\r', "\\r") - .replace('\n', "\\n") - .replace('\t', "\\t"); - format!("\"{esc}\"") -} - -fn to_hex(bytes: &[u8]) -> String { - use std::fmt::Write; - let mut s = String::with_capacity(bytes.len() * 2); - for b in bytes { - let _ = write!(s, "{b:02x}"); - } - s -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/txt_events.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/txt_events.rs deleted file mode 100644 index a881faa..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/txt_events.rs +++ /dev/null @@ -1,441 +0,0 @@ -//! External event-text `.txt` files. Some GamePro games keep every line of dialogue and -//! narration in plain Shift-JIS (or, for newer games, UTF-8) `.txt` files that the engine reads -//! at runtime, one file per scene. The `Evtext*.wolf` archives unpack to thousands of them. The -//! command-stream extractor in `strings.rs` never sees this text, so without this module it is -//! the single biggest recall gap for those games. -//! -//! The format is line-based. A line that starts with `/` is an engine command or a `//` comment -//! (CG control, page break, a stat tweak like `/胸+1`, and so on) and is never shown to the -//! player. A blank line is spacing. Every other non-blank line is displayed text. So a line is -//! translatable exactly when it does not start with `/` and is not blank. -//! -//! Extraction emits only the translatable lines, each tagged with its 0-based line index so -//! injection knows which line to replace. Command, comment, and blank lines are reconstructed -//! from the base on inject, so they round-trip byte-exact. A no-op inject (every `text` equal to -//! its `source`) reproduces the base file byte-for-byte, including the original line endings, the -//! original encoding, and a missing trailing newline. - -use std::fmt::Write as _; - -use serde_json::Value; - -// ---------------------------------------------------------------------------- -// Decoding + line splitting -// ---------------------------------------------------------------------------- - -/// The file's text encoding. A `.txt` event file is Shift-JIS for the older GamePro games and -/// UTF-8 for newer ones. We detect rather than guess so both round-trip. -#[derive(Clone, Copy, PartialEq, Eq)] -enum Enc { - Sjis, - Utf8, -} - -impl Enc { - fn tag(self) -> &'static str { - match self { - Enc::Sjis => "sjis", - Enc::Utf8 => "utf8", - } - } -} - -/// Decode the raw bytes, choosing the encoding. Valid UTF-8 wins (it is the stricter test, and a -/// genuine Shift-JIS file with high bytes is almost never accidentally valid UTF-8), otherwise we -/// fall back to Shift-JIS, which the older games use. A UTF-8 BOM, if present, is treated as part -/// of the first line's bytes so it survives the round-trip untouched. -fn decode(bytes: &[u8]) -> (String, Enc) { - match std::str::from_utf8(bytes) { - Ok(s) => (s.to_owned(), Enc::Utf8), - Err(_) => { - let (text, _, _) = encoding_rs::SHIFT_JIS.decode(bytes); - (text.into_owned(), Enc::Sjis) - } - } -} - -/// Re-encode decoded text in the chosen encoding. Returns `None` when the text has a char the -/// encoding cannot represent (a non-Shift-JIS char hand-typed into a Shift-JIS file), mirroring -/// the encoding guard `text.rs` uses for the command-stream path. -fn encode(text: &str, enc: Enc) -> Option> { - match enc { - Enc::Utf8 => Some(text.as_bytes().to_vec()), - Enc::Sjis => { - let (bytes, _, had_err) = encoding_rs::SHIFT_JIS.encode(text); - (!had_err).then(|| bytes.into_owned()) - } - } -} - -/// One physical line: its content (no terminator) and the exact terminator that followed it. The -/// terminator is `"\r\n"`, `"\n"`, `"\r"`, or `""` for the last line when the file has no trailing -/// newline. Capturing the real terminator per line, rather than assuming one EOL for the whole -/// file, keeps reconstruction byte-exact even if a file mixes line endings. -struct Line<'a> { - content: &'a str, - eol: &'a str, -} - -/// Split decoded text into lines, recording each line's exact terminator. A final segment with no -/// terminator (no trailing newline at EOF) is kept as a last line with an empty terminator, so it -/// re-joins without inventing a newline. An empty input yields no lines. -fn split_lines(text: &str) -> Vec> { - let mut lines = Vec::new(); - let bytes = text.as_bytes(); - let mut start = 0; - let mut i = 0; - while i < bytes.len() { - match bytes[i] { - b'\n' => { - lines.push(Line { - content: &text[start..i], - eol: "\n", - }); - i += 1; - start = i; - } - b'\r' => { - let eol = if bytes.get(i + 1) == Some(&b'\n') { - "\r\n" - } else { - "\r" - }; - lines.push(Line { - content: &text[start..i], - eol, - }); - i += eol.len(); - start = i; - } - _ => i += 1, - } - } - if start < bytes.len() { - // Trailing segment with no terminator (file does not end in a newline). - lines.push(Line { - content: &text[start..], - eol: "", - }); - } - lines -} - -/// A line is translatable when it is not blank and does not start with `/`. The `/` test is on -/// the first byte with no whitespace stripping. The real game files never indent a command, and a -/// displayed line that genuinely begins with a leading space (full sentences sometimes do) keeps -/// that space as part of its on-screen text, so trimming first would be wrong on both counts. -fn is_translatable(content: &str) -> bool { - !content.is_empty() && !content.starts_with('/') -} - -/// The dominant EOL for the document metadata. Reports `crlf` if any line ends in `\r\n`, else -/// `lf`. Only informational. Reconstruction always uses each line's captured terminator. -fn dominant_eol(lines: &[Line<'_>]) -> &'static str { - if lines.iter().any(|l| l.eol == "\r\n") { - "crlf" - } else { - "lf" - } -} - -// ---------------------------------------------------------------------------- -// Extraction -// ---------------------------------------------------------------------------- - -/// Extract the translatable lines of an event-text `.txt` file as a translation JSON document. -/// -/// The shape is `{ "kind":"txt", "encoding":"sjis|utf8", "eol":"crlf|lf", "lines":[ {"i":<0-based -/// line index>, "source":"", "text":""} ... ] }`. Only the -/// translatable lines get an entry. Command, comment, and blank lines carry no entry because -/// inject rebuilds them from the base. `text` starts equal to `source`. A translator edits -/// `text`. `source` is kept for the inject drift guard. -pub fn extract_txt_events(bytes: &[u8]) -> String { - let (text, enc) = decode(bytes); - let lines = split_lines(&text); - let eol = dominant_eol(&lines); - - let mut s = String::new(); - s.push_str("{\n"); - let _ = write!(s, " \"kind\": \"txt\",\n"); - let _ = write!(s, " \"encoding\": {},\n", jstr(enc.tag())); - let _ = write!(s, " \"eol\": {},\n", jstr(eol)); - s.push_str(" \"lines\": [\n"); - let keep: Vec<(usize, &str)> = lines - .iter() - .enumerate() - .filter(|(_, l)| is_translatable(l.content)) - .map(|(i, l)| (i, l.content)) - .collect(); - for (n, (i, content)) in keep.iter().enumerate() { - let _ = write!( - s, - " {{\"i\": {}, \"source\": {}, \"text\": {}}}", - i, - jstr(content), - jstr(content) - ); - s.push_str(if n + 1 == keep.len() { "\n" } else { ",\n" }); - } - s.push_str(" ]\n}\n"); - s -} - -// ---------------------------------------------------------------------------- -// Injection -// ---------------------------------------------------------------------------- - -/// Apply an edited translation JSON back onto the base `.txt` bytes, byte-exact. -/// -/// The base is re-decoded into lines, so command, comment, and blank lines (and the exact line -/// terminators) come straight from the original. Each translatable line `i` is replaced with its -/// `text` only when `text` differs from `source` and the base line still holds `source` (a drift -/// guard). The lines are re-joined with their original terminators and re-encoded in the original -/// encoding. A no-op (every `text` equal to its `source`) reproduces the base byte-for-byte. -/// -/// A translation is skipped, leaving the base line untouched, when it contains a newline (that -/// would split one line into two) or when it is not representable in the file's encoding -/// (Shift-JIS). Both cases print a warning, the same way the command-stream `write_translation` -/// guards encoding, and the rest of the batch still applies. -pub fn inject_txt_events(json: &str, base_bytes: &[u8]) -> Result, String> { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let (text, enc) = decode(base_bytes); - let lines = split_lines(&text); - // Each line starts as its original content. Edits overwrite the content of translatable lines. - let mut content: Vec = lines.iter().map(|l| l.content.to_owned()).collect(); - - let entries = root - .get("lines") - .and_then(Value::as_array) - .ok_or("txt translation JSON has no `lines` array")?; - for ln in entries { - let i = ln - .get("i") - .and_then(Value::as_u64) - .ok_or("txt line entry has no integer `i`")? as usize; - let source = ln.get("source").and_then(Value::as_str).unwrap_or(""); - let text_new = ln.get("text").and_then(Value::as_str).unwrap_or(source); - if text_new == source { - continue; - } - let Some(cur) = content.get_mut(i) else { - eprintln!("txt line {i}: out of range in base (drifted); skipping"); - continue; - }; - // Drift guard: only overwrite if the base line still holds the recorded source. - if cur != source { - eprintln!("txt line {i}: base no longer holds the recorded source (drifted); skipping"); - continue; - } - // A newline in the translation would split one line into two and desync every later - // index. Reject it and keep the original line. - if text_new.contains('\n') || text_new.contains('\r') { - eprintln!("txt line {i}: translation contains a newline; skipping (keep it on one line)"); - continue; - } - // The translated line must be representable in the file's encoding (Shift-JIS games). - if encode(text_new, enc).is_none() { - eprintln!( - "txt line {i}: text not representable in {}: {text_new:?}; choose an encodable equivalent", - enc.tag() - ); - continue; - } - *cur = text_new.to_owned(); - } - - // Re-join with the original per-line terminators, then re-encode. - let mut rebuilt = String::with_capacity(text.len()); - for (l, c) in lines.iter().zip(content.iter()) { - rebuilt.push_str(c); - rebuilt.push_str(l.eol); - } - encode(&rebuilt, enc).ok_or_else(|| { - format!( - "rebuilt text not representable in {} (this should not happen after the per-line guard)", - enc.tag() - ) - }) -} - -// ---------------------------------------------------------------------------- -// Directory (folder of .txt files) combined document -// ---------------------------------------------------------------------------- - -/// Build one combined translation JSON for a folder of event-text files. Each `(name, bytes)` -/// pair is extracted and embedded under a `files` array with its `file` name, so a single document -/// covers a whole `Evtext` folder and `inject_txt_dir` can route each entry back to its base file. -/// The caller supplies the files in the order they should appear (sorted by name for stability). -pub fn extract_txt_dir(files: &[(String, Vec)]) -> String { - let mut entries = Vec::with_capacity(files.len()); - for (name, bytes) in files { - let mut obj: Value = serde_json::from_str(&extract_txt_events(bytes)) - .expect("extract_txt_events always emits valid JSON"); - if let Value::Object(map) = &mut obj { - // Put `file` first by rebuilding the map with it at the front. - let mut with_name = serde_json::Map::new(); - with_name.insert("file".to_string(), Value::String(name.clone())); - for (k, v) in map.iter() { - with_name.insert(k.clone(), v.clone()); - } - obj = Value::Object(with_name); - } - entries.push(obj); - } - let doc = serde_json::json!({ "kind": "txt-dir", "files": entries }); - // Pretty-print so a translator can edit it by hand, matching the readable single-file output. - serde_json::to_string_pretty(&doc).unwrap_or_else(|_| "{}".to_string()) + "\n" -} - -/// One file's edits parsed out of a combined `txt-dir` document: its base file name and the -/// per-file JSON to feed to [`inject_txt_events`]. Returned in document order. -pub fn split_txt_dir(json: &str) -> Result, String> { - let root: Value = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?; - let files = root - .get("files") - .and_then(Value::as_array) - .ok_or("txt-dir JSON has no `files` array")?; - let mut out = Vec::with_capacity(files.len()); - for entry in files { - let name = entry - .get("file") - .and_then(Value::as_str) - .ok_or("a files[] entry has no `file` name")? - .to_owned(); - out.push((name, entry.to_string())); - } - Ok(out) -} - -// ---------------------------------------------------------------------------- -// JSON string escaping (matches the other extractors) -// ---------------------------------------------------------------------------- - -fn jstr(s: &str) -> String { - let mut out = String::with_capacity(s.len() + 2); - out.push('"'); - for c in s.chars() { - match c { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if (c as u32) < 0x20 => { - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), - } - } - out.push('"'); - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn extracted_texts(json: &str) -> Vec { - let v: Value = serde_json::from_str(json).unwrap(); - v["lines"] - .as_array() - .unwrap() - .iter() - .map(|l| l["source"].as_str().unwrap().to_owned()) - .collect() - } - - #[test] - fn noop_is_byte_exact_crlf_with_commands_and_blanks() { - // CRLF, a `//` comment, a `/` command, blank lines, CJK dialogue, no trailing newline. - let text = "//tag comment\r\n/evcg,a0\r\nアイリス:\r\nこんにちは\r\n\r\n/b\r\n最後の行"; - let bytes = encoding_rs::SHIFT_JIS.encode(text).0.into_owned(); - let json = extract_txt_events(&bytes); - let out = inject_txt_events(&json, &bytes).unwrap(); - assert_eq!(out, bytes, "no-op inject must be byte-exact"); - } - - #[test] - fn extraction_excludes_commands_and_blanks() { - let text = "//comment\r\n/evcg,a0\r\nアイリス:\r\n本当のセリフ\r\n\r\n/b\r\n"; - let bytes = encoding_rs::SHIFT_JIS.encode(text).0.into_owned(); - let json = extract_txt_events(&bytes); - let got = extracted_texts(&json); - assert_eq!(got, vec!["アイリス:".to_string(), "本当のセリフ".to_string()]); - assert!(!json.contains("/evcg"), "command line must not be extracted"); - assert!(!json.contains("//comment"), "comment line must not be extracted"); - } - - #[test] - fn edit_round_trip_keeps_commands_intact() { - let text = "/evcg,a0\r\nアイリス:\r\n元のセリフ\r\n/b\r\n"; - let bytes = encoding_rs::SHIFT_JIS.encode(text).0.into_owned(); - let json = extract_txt_events(&bytes); - // Change the line whose source is `元のセリフ`. - let edited = json.replacen( - r#""source": "元のセリフ", "text": "元のセリフ""#, - r#""source": "元のセリフ", "text": "新しいセリフ""#, - 1, - ); - assert_ne!(edited, json, "the replacement should have matched"); - let out = inject_txt_events(&edited, &bytes).unwrap(); - let re = extract_txt_events(&out); - assert!(extracted_texts(&re).contains(&"新しいセリフ".to_string())); - let (decoded, _) = decode(&out); - assert!(decoded.contains("/evcg,a0"), "command line must survive"); - assert!(decoded.contains("/b"), "page-break command must survive"); - assert!(!decoded.contains("元のセリフ"), "old text should be gone"); - } - - #[test] - fn lf_only_and_leading_space_line_and_utf8() { - // LF endings, a UTF-8 file, a dialogue line that starts with a real leading space, and a - // command line. The leading-space line is text (the space is part of it), the `/` line is - // not. - let text = "/cmd\n line with leading space\nnormal\n"; - let bytes = text.as_bytes().to_vec(); - let json = extract_txt_events(&bytes); - let v: Value = serde_json::from_str(&json).unwrap(); - assert_eq!(v["encoding"], "utf8"); - assert_eq!(v["eol"], "lf"); - let got = extracted_texts(&json); - assert_eq!(got, vec![" line with leading space".to_string(), "normal".to_string()]); - assert_eq!(inject_txt_events(&json, &bytes).unwrap(), bytes); - } - - #[test] - fn newline_in_translation_is_rejected() { - let text = "せりふ\n"; - let bytes = text.as_bytes().to_vec(); - let edit = r#"{"kind":"txt","encoding":"utf8","eol":"lf","lines":[{"i":0,"source":"せりふ","text":"two\nlines"}]}"#; - let out = inject_txt_events(edit, &bytes).unwrap(); - // Rejected, so the base line is untouched. - assert_eq!(out, bytes); - } - - #[test] - fn unrepresentable_sjis_is_skipped() { - // A Shift-JIS base, translation contains an emoji (not SJIS-encodable). The line is left - // untouched and the rest of the file is unchanged. - let text = "あいうえお\r\n"; - let bytes = encoding_rs::SHIFT_JIS.encode(text).0.into_owned(); - let edit = r#"{"kind":"txt","encoding":"sjis","eol":"crlf","lines":[{"i":0,"source":"あいうえお","text":"🎮"}]}"#; - let out = inject_txt_events(edit, &bytes).unwrap(); - assert_eq!(out, bytes, "unrepresentable line is skipped, base preserved"); - } - - #[test] - fn empty_file_round_trips() { - let bytes: Vec = Vec::new(); - let json = extract_txt_events(&bytes); - assert!(extracted_texts(&json).is_empty()); - assert_eq!(inject_txt_events(&json, &bytes).unwrap(), bytes); - } - - #[test] - fn lone_cr_line_ending_round_trips() { - // A stray old-Mac `\r` terminator must survive byte-exact. - let bytes = b"text one\rtext two".to_vec(); - let json = extract_txt_events(&bytes); - assert_eq!(inject_txt_events(&json, &bytes).unwrap(), bytes); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/mod.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/mod.rs deleted file mode 100644 index 0086294..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/mod.rs +++ /dev/null @@ -1,872 +0,0 @@ -//! WolfScript emitter. Renders a command program into readable, indented pseudo-code. -//! -//! Every command renders bespoke. Keystone commands (conditions, choices, SetVariable, calls, and -//! so on) get purpose-built forms decoded from their argument packing. All others use the labeled -//! argument schema in [`crate::spec`]. Nothing falls back to an unlabeled int dump. -//! Conditional/choice branches are reconstructed from the flat marker commands via a small block -//! stack, so `case`/`when` labels carry their real option/condition text. Labels are mapped -//! positionally, which is robust to the engine's non-sequential case indices. - -pub(crate) mod ops; -pub use ops::operand; -use ops::*; - -use std::cell::Cell; -use std::collections::HashMap; - -use wolf_formats::command::RawCommand; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -use crate::spec; -use crate::symbols::SymbolTable; -use crate::text::{decode_wstr, quote_wstr, wstr_to_literal}; - -const INDENT_UNIT: &str = " "; - -/// Common-event call-by-ID base. A stored target of `500000 + id` calls common event `id`. -const CALL_BY_ID_BASE: u32 = 500_000; - -/// StringCondition lhs flag. When set, the right-hand side is an operand from the rhs section -/// (string-var vs string-var) rather than the literal in `str_args`. -const STRCOND_OPERAND_FLAG: u32 = 0x0100_0000; - -/// Tracks the enclosing block so `case`/`when` markers resolve to a real, positional label. -enum Block { - Choices { options: Vec, next: usize }, - Conditional { conds: Vec, next: usize }, - Other, -} - -pub struct Renderer<'a> { - utf8: bool, - symbols: &'a SymbolTable, - /// The common event currently being rendered, so `CSelf[n]` resolves to its own - /// self-variable names. `None` for maps. - current_ce: Cell>, - /// Display mode. Render the prettiest (bespoke, name-resolved) form for reading, skipping the - /// recompile self-check that otherwise falls back to `@raw`. The numeric, round-trip-faithful - /// path (`decompile_commands`) leaves this off. - display: bool, - /// Annotate mode: the unified read-plus-edit surface. Runs with `display=false` so the - /// per-leaf `recompilable()` self-check still gates every line, but injects an index-anchored - /// bilingual label inside the brackets, as in `V[3 "Gold · ゴールド"]`. The index stays the - /// round-trip authority and the recompiler strips the quoted label. Because the self-check - /// runs, any label the parser cannot strip back to the identical command auto-falls to the - /// numeric/`@raw` form, so the file is always byte-exact and the names only affect prettiness. - annotate: bool, -} - -impl<'a> Renderer<'a> { - pub fn with_symbols(utf8: bool, symbols: &'a SymbolTable) -> Self { - Renderer { - utf8, - symbols, - current_ce: Cell::new(None), - display: false, - annotate: false, - } - } - - /// Enable display mode: readable output, no `@raw` fallback. - pub fn for_display(mut self) -> Self { - self.display = true; - self - } - - /// Enable annotate mode: readable and recompilable, with index-anchored bilingual labels. - pub fn for_edit(mut self) -> Self { - self.annotate = true; - self - } - - /// `English · 日本語` when the engine glossary translates the name, else the raw name. - fn bilingual(&self, jp: &str) -> String { - let en = self.symbols.tr(jp); - if en != jp { - format!("{en} · {jp}") - } else { - jp.to_string() - } - } - - /// Decode a Wolf operand id into a readable, name-resolved reference. See [`ops::operand`] for - /// the structural, symbol-free mirror that the recompiler's parser inverts. - fn op(&self, v: u32) -> String { - match v { - 0..=999_999 => v.to_string(), - // This-event intrinsic data: position, graphic, or direction of the running event. - 1_000_000..=1_099_999 => this_event_ref(v), - // Map-event self vars are unnamed by the engine. Kept numeric by design. - 1_100_000..=1_199_999 => format!("Self[{}]", v - 1_100_000), - 1_600_000..=1_699_999 => { - let n = v - 1_600_000; - match self.cself_name(n) { - Some(name) if self.annotate => { - format!("CSelf[{n} \"{}\"]", escape(&self.bilingual(&name))) - } - Some(name) => format!("CSelf[{}]", name_tok(self.symbols.tr(&name))), - None => format!("CSelf[{n}]"), - } - } - 1_200_000..=1_599_999 | 1_700_000..=1_999_999 => format!("Self*[{}]", v - 1_000_000), - 2_000_000..=2_999_999 => { - self.global_ref(&self.symbols.globals.normal, v - 2_000_000, "V") - } - 3_000_000..=3_999_999 => { - self.global_ref(&self.symbols.globals.string, v - 3_000_000, "S") - } - // Random 0..N reference. The engine computes `rand % (N+1)`, N = v-8_000_000. - 8_000_000..=8_999_999 => format!("Rand[{}]", v - 8_000_000), - 9_000_000..=9_099_999 => { - self.global_ref(&self.symbols.globals.system, v - 9_000_000, "Sys") - } - // On-map character data: party member 9.1M, current character 9.18M. - 9_100_000..=9_179_999 => chara_ref("Chara", 9_100_000, v), - 9_180_000..=9_189_999 => chara_ref("Chara2", 9_180_000, v), - 9_190_000..=9_999_999 => { - self.global_ref(&self.symbols.globals.system, v - 9_000_000, "Sys") - } - _ => lit(v), - } - } - - /// Render a global reference. Annotate mode anchors on the index and adds a bilingual label - /// (`V[3 "Gold · ゴールド"]`). Display mode shows the translated name (`V["Gold"]`). Numeric - /// mode is the bare index (`V[3]`). - fn global_ref(&self, map: &HashMap, n: u32, sigil: &str) -> String { - match map.get(&n) { - Some(name) if self.annotate => { - format!("{sigil}[{n} \"{}\"]", escape(&self.bilingual(name))) - } - Some(name) => format!("{sigil}[{}]", name_tok(self.symbols.tr(name))), - None => format!("{sigil}[{n}]"), - } - } - - fn cself_name(&self, n: u32) -> Option { - let ce = self.current_ce.get()?; - self.symbols.common_event(ce)?.self_vars.get(&n).cloned() - } - - fn decode_str(&self, w: &wolf_formats::WStr) -> String { - decode_wstr(w, self.utf8) - } - - fn quote(&self, w: &wolf_formats::WStr) -> String { - // Display favours readability, where a lossy `from_utf8_lossy` is fine. The numeric/edit - // path must round-trip, so it uses the byte-exact literal (readable text or `x"HEX"`). - if self.display { - quote_wstr(w, self.utf8) - } else { - wstr_to_literal(w, self.utf8) - } - } - - fn str_arg(&self, cmd: &RawCommand, idx: usize) -> String { - cmd.str_args - .get(idx) - .map(|s| self.quote(s)) - .unwrap_or_else(|| "\"\"".to_string()) - } - - fn is_blank_slot(cmd: &RawCommand) -> bool { - cmd.cid == 0 - && cmd.int_args.is_empty() - && cmd.str_args.is_empty() - && cmd.move_route.is_none() - } - - /// Render a flat command list, reconstructing block labels via a stack. - pub fn commands(&self, cmds: &[RawCommand]) -> String { - // Wolf Pro v3.5: every command carries a trailing blob, almost always empty. When - // present, the recompiler re-adds an empty blob to each command by default, so a command - // can still render pretty when its own blob is empty. A non-empty blob forces the lossless - // `@raw` form. - let v35 = cmds.iter().any(|c| c.v35_blob.is_some()); - let mut out = String::new(); - let mut stack: Vec = Vec::new(); - - for cmd in cmds { - if Self::is_blank_slot(cmd) { - continue; - } - let blob_empty = cmd.v35_blob.as_deref().map_or(true, |b| b.is_empty()) || self.display; - // Case markers consume the next positional label from the enclosing block. - let line = match cmd.cid { - 401 if blob_empty => case_marker(cmd, &mut stack), - 402 if blob_empty => { - format!("}} case* {} {{", cmd.int_args.first().copied().unwrap_or(0)) - } - 421 if blob_empty => "} cancel {".to_string(), - // Display mode: render the prettiest form for reading, no recompile gate. - _ if self.display => self.line(cmd), - _ => { - // Self-check chain: pretty, then labeled, then lossless @raw, picking the - // first form that round-trips exactly. - let pretty = self.line(cmd); - if is_block_opener(cmd.cid) { - // Openers are reconstructed structurally. A rare non-empty blob is carried - // inline as a ` @b:HEX` suffix the parser strips and re-attaches. - match cmd.v35_blob.as_deref() { - Some(b) if !b.is_empty() => format!("{pretty} @b:{}", hex(b)), - _ => pretty, - } - } else if (is_structural(cmd.cid) && blob_empty) - || self.recompilable(cmd, &pretty, v35) - { - pretty - } else { - let labeled = self.labeled(cmd); - if self.recompilable(cmd, &labeled, v35) { - labeled - } else { - raw_form(cmd) - } - } - } - }; - for _ in 0..cmd.indent as usize { - out.push_str(INDENT_UNIT); - } - out.push_str(&line); - out.push('\n'); - - match cmd.cid { - 102 => stack.push(Block::Choices { - options: self.choice_options(cmd), - next: 0, - }), - 111 | 112 => stack.push(Block::Conditional { - conds: self.branch_conditions(cmd), - next: 0, - }), - 170 | 179 => stack.push(Block::Other), - 498 | 499 => { - stack.pop(); - } - _ => {} - } - } - out - } - - /// True if the leaf line parses back to exactly this command (a lossless display form). Under - /// v3.5 the recompiler re-adds an empty trailing blob to each command, so a pretty form is - /// lossless exactly when the command's own blob is empty. - fn recompilable(&self, cmd: &RawCommand, line: &str, v35: bool) -> bool { - match crate::compile::cmd::parse_leaf(line, cmd.indent, self.utf8) { - Ok(mut p) => { - if v35 && p.v35_blob.is_none() { - p.v35_blob = Some(Vec::new()); - } - &p == cmd - } - Err(_) => false, - } - } - - fn line(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - match cmd.cid { - 101 => format!("Message {}", self.str_arg(cmd, 0)), - 103 => format!( - "# {}", - escape_inline(&self.decode_str(&cmd.str_args.first().cloned().unwrap_or_default())) - ), - 106 => format!("DebugMessage {}", self.str_arg(cmd, 0)), - 121 => self.set_variable(cmd), - 122 => self.set_string(cmd), - 102 => format!( - "choose {} [{}] {{", - a.first().copied().unwrap_or(0), - self.join_choice_options(cmd) - ), - 111 | 112 => { - if a.first().copied().unwrap_or(0) & 0x10 != 0 { - "branch all {".to_string() // AND, all conditions must hold - } else { - "branch {".to_string() // OR, first match - } - } - 420 => "} else {".to_string(), - 498 | 499 => "}".to_string(), - 170 => "loop {".to_string(), - // StartLoop2 (176) is not a block opener. It has no matching LoopEnd in data. - 179 => format!( - "loop {} times {{", - a.first().map(|&v| self.op(v)).unwrap_or_default() - ), - 210 | 211 => self.call_common_event(cmd), - 300 => self.call_common_event_by_name(cmd), - 201 => self.move_command(cmd), - 140 => self.sound(cmd), - 250 => self.database(cmd), - 151 => self.change_color(cmd), - 130 => self.teleport(cmd), - 180 => format!("Wait {}", a.first().copied().unwrap_or(0)), - _ => self.labeled(cmd), - } - } - - // --- keystone renderers ------------------------------------------------ - - fn choice_options(&self, cmd: &RawCommand) -> Vec { - cmd.str_args.iter().map(|s| self.decode_str(s)).collect() - } - - fn join_choice_options(&self, cmd: &RawCommand) -> String { - cmd.str_args - .iter() - .map(|s| self.quote(s)) - .collect::>() - .join(", ") - } - - /// Decode a VariableCondition/StringCondition into per-branch condition strings. - fn branch_conditions(&self, cmd: &RawCommand) -> Vec { - let a = &cmd.int_args; - let Some(&header) = a.first() else { - return Vec::new(); - }; - let n = (header & 0x0F) as usize; - let is_and = header & 0x10 != 0; - - let groups: Vec = if cmd.cid == 112 { - // StringCondition layout: `[header][lhs×n][rhs×k]`. Each lhs word packs the left - // string operand in the low 24 bits, the right-is-variable flag in bit 24 - // (`0x01000000`), and the comparison type in the high nibble (bits 28-31): 0=eq, - // 1=ne, 2=contains, 3=!contains. A variable right reads from the rhs section. A - // literal right reads `str_args[g]`. `k` is the count of variable-right conditions. - let mut rhs_ptr = 1 + n; - (0..n) - .map(|g| { - let raw = a.get(1 + g).copied().unwrap_or(0); - let operand = raw & 0x00FF_FFFF; - let cmp = strcmp_op_str((raw >> 28) & 0xF); - if raw & STRCOND_OPERAND_FLAG != 0 { - let rhs = a.get(rhs_ptr).copied().unwrap_or(0); - rhs_ptr += 1; - format!("{} {cmp} {}", self.op(operand), self.op(rhs)) - } else { - format!("{} {cmp} {}", self.op(operand), self.str_arg(cmd, g)) - } - }) - .collect() - } else { - (0..n) - .map(|g| { - let base = 1 + g * 3; - let left = a.get(base).map(|&v| self.op(v)).unwrap_or_default(); - let right = a.get(base + 1).map(|&v| self.op(v)).unwrap_or_default(); - let op = a.get(base + 2).copied().unwrap_or(0); - format!("{left} {} {right}", compare_op_str(op)) - }) - .collect() - }; - - if is_and { - vec![groups.join(" && ")] - } else { - groups - } - } - - fn set_variable(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - if a.len() < 4 { - return self.labeled(cmd); - } - let target = self.op(a[0]); - let left = self.op(a[1]); - let right = a[2]; - let modify = modify_op(a[3]); - let binop_nibble = (a[3] >> 12) & 0x0F; - let real = a[3] & 0x04 != 0; - - let mut s = if right == 0 && binop_nibble == 0 { - format!("SetVariable {target} {modify} {left}") - } else { - format!( - "SetVariable {target} {modify} {left} {} {}", - binary_op(a[3]), - self.op(right) - ) - }; - if real { - s.push_str(" // real"); - } - s - } - - fn set_string(&self, cmd: &RawCommand) -> String { - let target = cmd - .int_args - .first() - .map(|&v| self.op(v)) - .unwrap_or_else(|| "S[?]".to_string()); - format!("SetString {target} = {}", self.str_arg(cmd, 0)) - } - - fn call_common_event(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - let target = a.first().copied().unwrap_or(0); - let count = a.get(1).map(|&v| (v & 0xFF) as usize).unwrap_or(0); - - // Operand-referenced target: call the event whose id is in a variable. - if target >= 1_000_000 { - let passed = self.passed_args(a, 2, count); - let head = format!("call [{}]", self.op(target)); - return if passed.is_empty() { - head - } else { - format!("{head}({passed})") - }; - } - - let id = if target >= CALL_BY_ID_BASE { - target - CALL_BY_ID_BASE - } else { - target - }; - let info = self.symbols.common_event(id); - let glossed = info.and_then(|i| self.symbols.glossary.get(&i.name)); - - // Function name: the English engine glossary for stock events, else JP name, else #id. - let head = match (info, glossed) { - (_, Some(g)) => g.en_name.clone(), - (Some(i), None) => format!("call \"{}\"", i.name), - (None, _) => format!("call #{id}"), - }; - - // Passed args, labeled with the callee's input names. English glossary wins over JP inputs. - let parts = - self.render_passed_args(a, count, glossed.map(|g| &g.args), info.map(|i| &i.inputs)); - if parts.is_empty() { - head - } else { - format!("{head}({})", parts.join(", ")) - } - } - - /// Render call arguments, labeling each with its parameter name and resolving DB-entry ids - /// (`ItemId=17` becomes `ItemId="ポーション"`) when the English param name is typed. - fn render_passed_args( - &self, - a: &[u32], - count: usize, - en_args: Option<&Vec>, - jp_args: Option<&Vec>, - ) -> Vec { - let mut parts = Vec::new(); - for k in 0..count { - let Some(&v) = a.get(2 + k) else { break }; - let en = en_args.and_then(|x| x.get(k)).map(String::as_str); - let jp = jp_args.and_then(|x| x.get(k)).map(String::as_str); - // English param name from the glossary, else the translated JP input name. - let label: Option = en - .map(str::to_string) - .or_else(|| jp.map(|j| self.symbols.tr(j).to_string())); - let val = self.resolve_arg(en, v); - match label.as_deref() { - Some(n) if !n.is_empty() => parts.push(format!("{n}={val}")), - _ => parts.push(val), - } - } - parts - } - - /// Render an operand. If it is a literal and the param name names a DB type, resolve it to the - /// entry name. This stays non-lossy since the recompiler accepts either name or id. - fn resolve_arg(&self, en_name: Option<&str>, v: u32) -> String { - if v < 1_000_000 { - if let Some(type_name) = en_name.and_then(arg_db_type) { - if let Some(name) = self.symbols.db_entry_name(type_name, v) { - return format!("\"{}\"", escape(name)); - } - } - } - self.op(v) - } - - fn call_common_event_by_name(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - let jp = self.decode_str(&cmd.str_args.first().cloned().unwrap_or_default()); - let glossed = self.symbols.glossary.get(&jp); - let info = self.symbols.common_event_by_name(&jp); - - let head = match glossed { - Some(g) => g.en_name.clone(), - None => format!("call \"{jp}\""), - }; - - let count = a.get(1).map(|&v| (v & 0xFF) as usize).unwrap_or(0); - let parts = - self.render_passed_args(a, count, glossed.map(|g| &g.args), info.map(|i| &i.inputs)); - if parts.is_empty() { - head - } else { - format!("{head}({})", parts.join(", ")) - } - } - - fn passed_args(&self, a: &[u32], start: usize, count: usize) -> String { - (0..count) - .filter_map(|i| a.get(start + i)) - .map(|&v| self.op(v)) - .collect::>() - .join(", ") - } - - /// Render a Database (DB操作) command as an assignment, using the type/data/field names - /// embedded in the command's string args, falling back to DB lookups or ids. `flagWord` bit0 - /// selects direction: 0 reads DB into a var, 1 writes a value into the DB. - fn database(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - if a.len() < 5 { - return self.labeled(cmd); - } - let (type_raw, data_raw, field_raw, flag, value) = (a[0], a[1], a[2], a[3], a[4]); - - // JP names from the embedded string args drive the DB lookups. Display is then translated - // via the engine-text glossary. - let type_jp = self.str_name_jp(cmd, 1); - let data_jp = self.str_name_jp(cmd, 2); - let field_jp = self.str_name_jp(cmd, 3); - - let kind = type_jp - .as_deref() - .and_then(|t| self.symbols.db_kind_of_type(t)) - .unwrap_or("DB"); - - let type_tok = match &type_jp { - Some(t) => name_tok(self.symbols.tr(t)), - None if type_raw >= 1_000_000 => self.op(type_raw), - None => format!("#{}", type_raw as i32), - }; - let row = if let Some(d) = &data_jp { - name_tok(self.symbols.tr(d)) - } else if data_raw >= 1_000_000 { - self.op(data_raw) - } else if let Some(name) = type_jp - .as_deref() - .and_then(|t| self.symbols.db_entry_name(t, data_raw)) - { - name_tok(self.symbols.tr(name)) - } else { - format!("#{}", data_raw as i32) - }; - let field = if let Some(f) = &field_jp { - Some(name_tok(self.symbols.tr(f))) - } else if let Some(name) = type_jp - .as_deref() - .and_then(|t| self.symbols.db_field_name(t, field_raw)) - { - Some(name_tok(self.symbols.tr(name))) - } else if field_raw == 0 { - None - } else { - Some(format!("#{field_raw}")) - }; - - let cell = match field { - Some(f) => format!("{kind}[{type_tok}][{row}].{f}"), - None => format!("{kind}[{type_tok}][{row}]"), - }; - - if flag & 0xF == 1 { - let op = match (flag >> 4) & 0xF { - 0 => "=", - 1 => "+=", - 2 => "-=", - 3 => "*=", - 4 => "/=", - 5 => "%=", - 6 => "max=", - 7 => "min=", - _ => "=", - }; - format!("{cell} {op} {}", self.op(value)) - } else { - format!("{} = {cell}", self.op(value)) - } - } - - /// The decoded JP text of a string arg, untranslated and used for DB lookups, if present. - fn str_name_jp(&self, cmd: &RawCommand, idx: usize) -> Option { - cmd.str_args - .get(idx) - .map(|s| self.decode_str(s)) - .filter(|d| !d.is_empty()) - } - - fn change_color(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - let Some(&c) = a.first() else { - return self.labeled(cmd); - }; - let (r, g, b) = (c & 0xFF, (c >> 8) & 0xFF, (c >> 16) & 0xFF); - let dur = a.get(1).copied().unwrap_or(0); - if dur == 0 { - format!("ChangeColor(R={r}, G={g}, B={b})") - } else { - format!("ChangeColor(R={r}, G={g}, B={b}, {dur}f)") - } - } - - fn teleport(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - let target = match a.first().copied() { - Some(0xFFFF_FFFF) => "player".to_string(), - Some(0xFFFF_FFFE) => "thisEvent".to_string(), - Some(v) if v >= 1_000_000 => self.op(v), - Some(v) => format!("event#{v}"), - None => return self.labeled(cmd), - }; - let x = a.get(1).map(|&v| self.op(v)).unwrap_or_default(); - let y = a.get(2).map(|&v| self.op(v)).unwrap_or_default(); - format!("Teleport {target} -> ({x}, {y})") - } - - /// Decode the Sound command's packed control word into op nibble, sound-type nibble, - /// addressing mode, and resource number. - fn sound(&self, cmd: &RawCommand) -> String { - let a = &cmd.int_args; - let Some(&w) = a.first() else { - return self.labeled(cmd); - }; - let op = w & 0x0F; - let ty = match (w >> 4) & 0x0F { - 0 => "BGM", - 1 => "BGS", - 2 => "SE", - _ => "Snd", - }; - let mode = (w >> 24) & 0x0F; - let resnum = (w >> 8) & 0xFFFF; - - match op { - 0 => { - let src = match mode { - 0x2 => self.str_arg(cmd, 0), - 0x0 => format!("#{resnum}"), - 0x1 => format!("[{}]", a.get(2).map(|&v| self.op(v)).unwrap_or_default()), - _ => format!("0x{w:08X}"), - }; - let vol = a.get(4).copied().unwrap_or(100); - let pitch = a.get(5).copied().unwrap_or(100); - let mut s = format!("Play{ty} {src}"); - if vol != 100 || pitch != 100 { - s.push_str(&format!(" vol={vol} pitch={pitch}")); - } - s - } - 1 => format!("Stop{ty}"), - 2 => format!("Release{ty}"), - 3 => "StopAllSound".to_string(), - _ => self.labeled(cmd), - } - } - - /// Lossless, readable Move(201): `Move(target=…) "strs" @route(t=, f=, - /// h=<5 header bytes>) { RouteCmd; RouteCmd(args); … }`. The route header bytes, flags, and - /// terminator are carried verbatim so the pretty form fully round-trips with no `@raw`. - fn move_command(&self, cmd: &RawCommand) -> String { - let spec = spec::command(cmd.cid); - let mut parts = Vec::new(); - for (i, &val) in cmd.int_args.iter().enumerate() { - // Default to operand-ref so targets like `CSelf[3]` or `-2` read naturally. - let (label, is_ref) = match spec.and_then(|s| s.int_args.get(i)) { - Some(f) => (short_label(&f.label), f.kind == "operand-ref"), - None => (format!("arg{i}"), true), - }; - let rendered = if is_ref { self.op(val) } else { lit(val) }; - parts.push(format!("{label}={rendered}")); - } - let mut head = String::from("Move"); - if !parts.is_empty() { - head.push('('); - head.push_str(&parts.join(", ")); - head.push(')'); - } - for sa in &cmd.str_args { - head.push(' '); - head.push_str(&self.quote(sa)); - } - match &cmd.move_route { - Some(r) => format!( - "{head} @route(t={}, f={}, h={}) {{ {} }}", - cmd.term, - r.flags, - hex(&r.unknown), - render_route(r) - ), - None => head, - } - } - - // --- generic-but-labeled fallback -------------------------------------- - - fn labeled(&self, cmd: &RawCommand) -> String { - let name = spec::command_name(cmd.cid); - self.labeled_inner(cmd, &name) - } - - fn labeled_inner(&self, cmd: &RawCommand, name: &str) -> String { - let spec = spec::command(cmd.cid); - let mut parts = Vec::new(); - for (i, &val) in cmd.int_args.iter().enumerate() { - let (label, is_ref) = match spec.and_then(|s| s.int_args.get(i)) { - Some(f) => (short_label(&f.label), f.kind == "operand-ref"), - None => (format!("arg{i}"), false), - }; - let rendered = if is_ref { self.op(val) } else { lit(val) }; - parts.push(format!("{label}={rendered}")); - } - let mut s = name.to_string(); - if !parts.is_empty() { - s.push('('); - s.push_str(&parts.join(", ")); - s.push(')'); - } - for sa in &cmd.str_args { - s.push(' '); - s.push_str(&self.quote(sa)); - } - if let Some(route) = &cmd.move_route { - s.push_str(&format!(" {{ {} }}", render_route(route))); - } - s - } -} - -// ---------------------------------------------------------------------------- -// Block-marker helpers (need &mut stack) -// ---------------------------------------------------------------------------- - -fn case_marker(cmd: &RawCommand, stack: &mut [Block]) -> String { - let arg0 = cmd.int_args.first().copied().unwrap_or(0); - match stack.last_mut() { - // Choices: show the real ChoiceCase index, which is recompile-safe. A label comment keeps - // it readable. - Some(Block::Choices { options, next }) => { - let label = options.get(*next).cloned(); - *next += 1; - match label.filter(|l| !l.is_empty()) { - Some(l) => format!("}} case {arg0} {{ # {}", escape_inline(&l)), - None => format!("}} case {arg0} {{"), - } - } - Some(Block::Conditional { conds, next }) => { - let cond = conds.get(*next).cloned().unwrap_or_else(|| "?".to_string()); - *next += 1; - format!("}} when ({cond}) {{") - } - _ => format!("}} case {arg0} {{"), - } -} - -// ---------------------------------------------------------------------------- -// Public entry points -// ---------------------------------------------------------------------------- - -/// Decompile a flat command list (UTF-8, no symbols). -pub fn decompile_commands(cmds: &[RawCommand]) -> String { - decompile_commands_enc(cmds, true) -} - -/// Decompile a flat command list in the file's encoding (no symbols). The recompilable form uses -/// numeric operands and byte-exact string literals, so `compile_commands_enc(_, utf8)` is its -/// exact inverse regardless of Shift-JIS or UTF-8. -pub fn decompile_commands_enc(cmds: &[RawCommand], utf8: bool) -> String { - let symbols = SymbolTable::new(); - let body = Renderer::with_symbols(utf8, &symbols).commands(cmds); - // A leading `@v35` directive tells the recompiler to re-add per-command blobs. - if cmds.iter().any(|c| c.v35_blob.is_some()) { - format!("@v35\n{body}") - } else { - body - } -} - -/// Decompile a flat command list in annotate mode, the unified read-plus-edit form. Readable -/// index-anchored bilingual labels (`V[3 "Gold · ゴールド"]`) that `compile_commands_enc` strips -/// back to the identical bytes. The per-leaf self-check guarantees byte-exactness, so any label -/// the parser cannot strip degrades to the numeric form. `ce_id` sets the current common event so -/// `CSelf[n]` resolves to its self-var names. Pass `None` for maps. -pub fn decompile_commands_annotated( - cmds: &[RawCommand], - utf8: bool, - symbols: &SymbolTable, - ce_id: Option, -) -> String { - let r = Renderer::with_symbols(utf8, symbols).for_edit(); - r.current_ce.set(ce_id); - let body = r.commands(cmds); - if cmds.iter().any(|c| c.v35_blob.is_some()) { - format!("@v35\n{body}") - } else { - body - } -} - -/// Decompile an entire map, resolving names via `symbols`. -pub fn decompile_map(map: &Map, symbols: &SymbolTable) -> String { - let r = Renderer::with_symbols(map.utf8, symbols).for_display(); - let mut out = format!( - "# map version=0x{:x} tileset={} {}x{} events={}\n\n", - map.version, - map.tileset_id, - map.width, - map.height, - map.events.len() - ); - for ev in &map.events { - out.push_str(&format!( - "event {} {} @({}, {}) {{\n", - ev.id, - r.quote(&ev.name), - ev.x, - ev.y - )); - for (pi, page) in ev.pages.iter().enumerate() { - out.push_str(&format!("{INDENT_UNIT}page {pi} {{\n")); - for line in r.commands(&page.commands).lines() { - out.push_str(&format!("{INDENT_UNIT}{INDENT_UNIT}{line}\n")); - } - out.push_str(&format!("{INDENT_UNIT}}}\n")); - } - out.push_str("}\n\n"); - } - out -} - -/// Decompile a CommonEvent file, resolving names via `symbols`. -pub fn decompile_common_events(ce: &CommonEventsFile, symbols: &SymbolTable) -> String { - let r = Renderer::with_symbols(ce.utf8, symbols).for_display(); - let mut out = format!("# common-events count={}\n\n", ce.events.len()); - for ev in &ce.events { - r.current_ce.set(Some(ev.int_id)); - let jp_name = decode_wstr(&ev.name, ce.utf8); - let header = match symbols.glossary.get(&jp_name) { - Some(g) => format!( - "commonEvent {} {} {} {{", - ev.int_id, - g.en_name, - r.quote(&ev.name) - ), - None => format!("commonEvent {} {} {{", ev.int_id, r.quote(&ev.name)), - }; - out.push_str(&header); - out.push('\n'); - for line in r.commands(&ev.commands).lines() { - out.push_str(&format!("{INDENT_UNIT}{line}\n")); - } - out.push_str("}\n\n"); - } - out -} - -// ---------------------------------------------------------------------------- -// Move route rendering -// ---------------------------------------------------------------------------- diff --git a/util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/ops.rs b/util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/ops.rs deleted file mode 100644 index 40ed02f..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/src/wolfscript/ops.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Stateless rendering helpers: operand and operator decoding, move-route rendering, the -//! lossless `@raw` fallback, and small string/format utilities. - -use crate::spec; -use wolf_formats::command::{MoveRoute, RawCommand, RouteCommand}; - -pub(crate) fn render_route(route: &MoveRoute) -> String { - route - .commands - .iter() - .map(render_route_command) - .collect::>() - .join("; ") -} - -pub(crate) fn render_route_command(rc: &RouteCommand) -> String { - let name = spec::route_name(rc.id as u32); - if rc.args.is_empty() { - name - } else { - // Route args are variable-call values, resolved by the engine through the same operand - // resolver, so decode them as references: `Wait(CSelf[5])`, not `Wait(1600005)`. - let args: Vec = rc.args.iter().map(|&v| operand(v)).collect(); - format!("{name}({})", args.join(", ")) - } -} - -// ---------------------------------------------------------------------------- -// Operand / operator decoding -// ---------------------------------------------------------------------------- - -/// Decode a Wolf operand id into a readable reference. Ranges: map-event self vars at -/// `1_100_000+n` (`Self[n]`), common-event self vars at `1_600_000+n` (`CSelf[n]`), normal vars -/// `2_000_000+n`, strings `3_000_000+n`, random `8_000_000+N` as `Rand[N]`, system vars -/// `9_000_000..9_099_999`, on-map character data `9_100_000+` and `9_180_000+`, and this-event -/// intrinsic data `1_000_000+`. This standalone decoder mirrors [`super::Renderer::op`] for the -/// symbol-free and route-arg path. Both invert [`crate::compile::cmd`]'s `operand` parser. -pub fn operand(v: u32) -> String { - match v { - 0..=999_999 => v.to_string(), - 1_000_000..=1_099_999 => this_event_ref(v), - 1_100_000..=1_199_999 => format!("Self[{}]", v - 1_100_000), - 1_600_000..=1_699_999 => format!("CSelf[{}]", v - 1_600_000), - 1_200_000..=1_599_999 | 1_700_000..=1_999_999 => format!("Self*[{}]", v - 1_000_000), - 2_000_000..=2_999_999 => format!("V[{}]", v - 2_000_000), - 3_000_000..=3_999_999 => format!("S[{}]", v - 3_000_000), - 8_000_000..=8_999_999 => format!("Rand[{}]", v - 8_000_000), - 9_000_000..=9_099_999 => format!("Sys[{}]", v - 9_000_000), - 9_100_000..=9_179_999 => chara_ref("Chara", 9_100_000, v), - 9_180_000..=9_189_999 => chara_ref("Chara2", 9_180_000, v), - 9_190_000..=9_999_999 => format!("Sys[{}]", v - 9_000_000), - _ => lit(v), - } -} - -/// On-map Character intrinsic-data field names, shared by the this-event (`1_000_000`), -/// party-character (`9_100_000`), and current-character (`9_180_000`) operand banks. Maps to -/// Character-struct offsets: +36/+40 position, +184 height, +272 graphic, +68 direction, -/// +344/+348 move remainder. The field index is `refNum % 10`. Positions are stored doubled, so -/// the halved tile coordinate (field 0/1) and the raw precise coordinate (field 2/3) coexist. -pub(crate) fn chara_field_name(f: u32) -> Option<&'static str> { - Some(match f { - 0 => "X", - 1 => "Y", - 2 => "PreciseX", - 3 => "PreciseY", - 4 => "Height", - 5 => "Graphic", - 6 => "Dir", - 7 => "RemX", - 8 => "RemY", - 9 => "Name", - _ => return None, - }) -} - -/// Inverse of [`chara_field_name`] for the recompiler. -pub(crate) fn chara_field_index(name: &str) -> Option { - (0..10).find(|&f| chara_field_name(f) == Some(name)) -} - -/// Render an on-map character-data reference (`Chara[m].Field`). `member = (v-base)/10`, -/// `field = v % 10`. -pub(crate) fn chara_ref(prefix: &str, base: u32, v: u32) -> String { - let off = v - base; - let member = off / 10; - match chara_field_name(off % 10) { - Some(f) => format!("{prefix}[{member}].{f}"), - None => format!("{prefix}[{member}].f{}", off % 10), - } -} - -/// Render a this-event intrinsic-data reference. Sub-object 0 (`1_000_000..=1_000_009`) is the -/// position/graphic block, shown as `ThisEv.Field`. Deeper sub-objects stay structural -/// (`ThisEv[k]`) so the byte survives the round-trip without an over-claimed field name. -pub(crate) fn this_event_ref(v: u32) -> String { - let off = v - 1_000_000; - match chara_field_name(off) { - Some(f) => format!("ThisEv.{f}"), - None => format!("ThisEv[{off}]"), - } -} - -/// Render a literal int, showing 2's-complement negatives (e.g. 0xFFFFFFFF becomes -1). -pub(crate) fn lit(v: u32) -> String { - (v as i32).to_string() -} - -/// Render a VariableCondition (111) compare-op word reversibly. The low nibble is the comparison -/// (editor order: 0=<=,1=>=,2===,3=!=,4=<,5=>,6=bitAND). The high nibble carries per-term flags, -/// such as the 0x10 right-side-is-variable flag seen in GamePro (op 0x12 = `==~1`). Known low -/// nibbles render as a symbol, plus `~` when set. Anything else stays `cmp` so the exact -/// byte survives the round-trip. -pub(crate) fn compare_op_str(v: u32) -> String { - let base = match v & 0x0F { - 0 => "<=", - 1 => ">=", - 2 => "==", - 3 => "!=", - 4 => "<", - 5 => ">", - 6 => "&", // bitAND != 0 - _ => return format!("cmp{v}"), - }; - match v >> 4 { - 0 => base.to_string(), - flags => format!("{base}~{flags}"), - } -} - -/// StringCondition (112) comparison-type token (the high nibble of the condition word): -/// 0=same, 1=different, 2=contains, 3=not-contains. Unknown types use a reversible `strcmp`. -pub(crate) fn strcmp_op_str(ty: u32) -> String { - match ty { - 0 => "eq".to_string(), - 1 => "ne".to_string(), - 2 => "contains".to_string(), - 3 => "!contains".to_string(), - other => format!("strcmp{other}"), - } -} - -pub(crate) fn modify_op(arg3: u32) -> &'static str { - match (arg3 >> 8) & 0x0F { - 0 => "=", - 1 => "+=", - 2 => "-=", - 3 => "*=", - 4 => "/=", - 5 => "%=", - 6 => "= floor", - 7 => "= ceil", - 8 => "= abs", - 9 => "= angle", - 0xA => "= sin", - 0xB => "= cos", - 0xC => "= sqrt", - _ => "=?", - } -} - -pub(crate) fn binary_op(arg3: u32) -> &'static str { - match (arg3 >> 12) & 0x0F { - 0 => "+", - 1 => "-", - 2 => "*", - 3 => "%", - 4 => "~", - 5 | 6 => "&", - 7 => "|", - 8 => "^", - 9 => "<<", - _ => "?", - } -} - -pub(crate) fn short_label(label: &str) -> String { - label - .split(|c: char| c == '(' || c == ' ' || c == '/') - .next() - .unwrap_or(label) - .to_string() -} - -pub(crate) fn escape(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - -/// Escape control characters so a value stays on one line, for unquoted contexts like comments. -/// Inverse of the parser's `unescape`. -pub(crate) fn escape_inline(s: &str) -> String { - s.replace('\\', "\\\\") - .replace('\r', "\\r") - .replace('\n', "\\n") - .replace('\t', "\\t") -} - -/// Fully lossless fallback for commands the pretty or labeled forms cannot reproduce, such as -/// `Move` with a route block, or v3.5 trailing blobs. -pub(crate) fn raw_form(cmd: &RawCommand) -> String { - use wolf_formats::Writer; - let ints = cmd - .int_args - .iter() - .map(|v| v.to_string()) - .collect::>() - .join(","); - let strs = cmd - .str_args - .iter() - .map(|s| hex(s.as_bytes())) - .collect::>() - .join(","); - let route = match &cmd.move_route { - Some(mr) => { - let mut w = Writer::new(); - mr.write(&mut w); - hex(&w.into_bytes()) - } - None => "-".to_string(), - }; - let v35 = cmd - .v35_blob - .as_ref() - .map(|b| hex(b)) - .unwrap_or_else(|| "-".to_string()); - format!( - "@raw {} i:{ints} s:{strs} t:{} m:{route} v:{v35}", - cmd.cid, cmd.term - ) -} - -pub(crate) fn hex(bytes: &[u8]) -> String { - let mut s = String::with_capacity(bytes.len() * 2); - for b in bytes { - s.push_str(&format!("{b:02x}")); - } - s -} - -/// Block-structure commands (openers, else, closers) reconstructed by the parser from the -/// readable block syntax. Not subject to the leaf self-check. -pub(crate) fn is_structural(cid: u32) -> bool { - matches!(cid, 102 | 111 | 112 | 170 | 179 | 420 | 498 | 499) -} - -/// Block openers (condition, loop, choose), the structural commands that begin a block and can -/// carry an inline ` @b:HEX` blob suffix under v3.5. -pub(crate) fn is_block_opener(cid: u32) -> bool { - matches!(cid, 102 | 111 | 112 | 170 | 179) -} - -/// Render a resolved name as a token: bare if it is a simple identifier, quoted otherwise, so -/// multi-word names like `Max Party Count` are unambiguous and parser-friendly. -pub(crate) fn name_tok(s: &str) -> String { - let simple = !s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_'); - if simple { - s.to_string() - } else { - format!("\"{}\"", escape(s)) - } -} - -/// Map an English parameter name (from the glossary) to the User-DB type that holds its entries, -/// so a literal id can be shown as the entry's name. Keyword-based. Falls back to the raw id when -/// the type is not present, so non-Basic-System games degrade gracefully. -pub(crate) fn arg_db_type(en_name: &str) -> Option<&'static str> { - let n = en_name.to_ascii_lowercase(); - if n.contains("item") { - Some("アイテム") - } else if n.contains("weapon") { - Some("武器") - } else if n.contains("armor") { - Some("防具") - } else if n.contains("skill") { - Some("技能") - } else if n.contains("enemy") { - Some("敵キャラ個体データ") - } else if n.contains("state") || n.contains("status") { - Some("状態設定") - } else { - None - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_commands.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_commands.rs deleted file mode 100644 index 272d912..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_commands.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Cross-version command coverage: scan every Wolf version's CommonEvent + maps for command -//! ids not named in commands.json (rendered as `Cmd`), and per-version route-command ids. -//! cargo test -p wolf-decompiler --test all_versions_commands -- --nocapture -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use wolf_decompiler::spec; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn roots() -> Vec { - let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join(".."); - vec![ - base.join("ALLWOLFVERSIONS"), - base.join("Data"), - base.join("GamePro_Data"), - ] -} - -fn walk(dir: &Path, ext: &str, out: &mut Vec) { - let Ok(rd) = std::fs::read_dir(dir) else { - return; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - walk(&p, ext, out); - } else if p.file_name().and_then(|s| s.to_str()) == Some(ext) - || p.extension().and_then(|s| s.to_str()) == Some(ext) - { - out.push(p); - } - } -} - -#[test] -fn command_coverage_all_versions() { - let mut unknown_cmd: BTreeMap = BTreeMap::new(); - let mut unknown_route: BTreeMap = BTreeMap::new(); - let mut all_cids: std::collections::BTreeSet = Default::default(); - let mut ce_files = 0; - let mut map_files = 0; - - let mut consider = |cmds: &[wolf_formats::command::RawCommand], - uc: &mut BTreeMap, - ur: &mut BTreeMap, - all: &mut std::collections::BTreeSet| { - for c in cmds { - all.insert(c.cid); - if spec::command(c.cid).is_none() { - *uc.entry(c.cid).or_default() += 1; - } - if let Some(mr) = &c.move_route { - for rc in &mr.commands { - if spec::route_name(rc.id as u32).starts_with("route") { - *ur.entry(rc.id as u32).or_default() += 1; - } - } - } - } - }; - - for root in roots() { - let mut ces = Vec::new(); - walk(&root, "CommonEvent.dat", &mut ces); - for p in ces { - let Ok(b) = std::fs::read(&p) else { continue }; - let Ok(ce) = CommonEventsFile::read(&b) else { - continue; - }; - ce_files += 1; - for ev in &ce.events { - consider( - &ev.commands, - &mut unknown_cmd, - &mut unknown_route, - &mut all_cids, - ); - } - } - let mut maps = Vec::new(); - walk(&root, "mps", &mut maps); - for p in maps { - let Ok(b) = std::fs::read(&p) else { continue }; - let Ok(m) = Map::read(&b) else { continue }; - map_files += 1; - for ev in &m.events { - for pg in &ev.pages { - consider( - &pg.commands, - &mut unknown_cmd, - &mut unknown_route, - &mut all_cids, - ); - } - } - } - } - - eprintln!( - "scanned {ce_files} CommonEvent files + {map_files} maps; {} distinct command ids", - all_cids.len() - ); - eprintln!("all command ids seen: {:?}", all_cids); - if unknown_cmd.is_empty() { - eprintln!("UNNAMED command ids: NONE - every command across every version is named"); - } else { - eprintln!("UNNAMED command ids (cid: count): {unknown_cmd:?}"); - } - if unknown_route.is_empty() { - eprintln!("UNNAMED route-command ids: NONE"); - } else { - eprintln!("UNNAMED route-command ids: {unknown_route:?}"); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_recompile.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_recompile.rs deleted file mode 100644 index b2aba50..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/all_versions_recompile.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Cross-version recompiler gate: for every Wolf editor version's CommonEvent.dat, prove -//! compile(decompile(cmds)) == cmds for every common event. -//! cargo test -p wolf-decompiler --test all_versions_recompile -- --nocapture -use std::path::{Path, PathBuf}; - -use wolf_decompiler::{compile_commands, decompile_commands}; -use wolf_formats::common_event::CommonEventsFile; - -fn root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("ALLWOLFVERSIONS") -} - -fn find_ce(dir: &Path) -> Option { - let mut stack = vec![dir.to_path_buf()]; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else if p.file_name().and_then(|s| s.to_str()) == Some("CommonEvent.dat") { - return Some(p); - } - } - } - None -} - -#[test] -fn recompile_all_versions() { - let Ok(dirs) = std::fs::read_dir(root()) else { - eprintln!("no ALLWOLFVERSIONS"); - return; - }; - let mut editors: Vec = dirs - .flatten() - .map(|e| e.path()) - .filter(|p| p.is_dir()) - .collect(); - editors.sort(); - - let mut versions_ok = 0; - let mut versions_total = 0; - for ed in &editors { - let name = ed.file_name().unwrap().to_string_lossy().into_owned(); - let Some(p) = find_ce(ed) else { continue }; - let Ok(bytes) = std::fs::read(&p) else { - continue; - }; - let Ok(ce) = CommonEventsFile::read(&bytes) else { - eprintln!(" {name}: CommonEvent parse failed"); - continue; - }; - versions_total += 1; - - let mut ok = 0; - let total = ce.events.len(); - let mut first_fail = String::new(); - for ev in &ce.events { - let text = decompile_commands(&ev.commands); - match compile_commands(&text) { - Ok(back) if back == ev.commands => ok += 1, - Ok(_) if first_fail.is_empty() => { - first_fail = format!("event {} mismatch", ev.int_id) - } - Err(e) if first_fail.is_empty() => { - first_fail = format!("event {} err: {}", ev.int_id, e) - } - _ => {} - } - } - if ok == total { - versions_ok += 1; - eprintln!(" OK {name:22} v=0x{:02x} {ok}/{total}", ce.version); - } else { - eprintln!( - " FAIL {name:22} v=0x{:02x} {ok}/{total} ({first_fail})", - ce.version - ); - } - } - eprintln!("\nversions fully recompiling: {versions_ok}/{versions_total}"); - if versions_total > 0 { - assert_eq!( - versions_ok, versions_total, - "some version's common events did not recompile byte-exact" - ); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/common/mod.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/common/mod.rs deleted file mode 100644 index a9c3161..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/common/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Shared helpers for the integration tests. -//! -//! Several tests need real Wolf game data (saves, an unpacked Data dir) that cannot be bundled -//! because it is copyrighted. Point `WOLFDAWN_TEST_DATA` at a fixtures root laid out as described in -//! the README and those tests run. With the var unset, or a given file missing, the lookup returns -//! `None` and the test skips itself. - -/// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when the -/// var is unset or the file/dir does not exist, so callers can skip gracefully. -#[allow(dead_code)] -pub fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/corpus_gates.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/corpus_gates.rs deleted file mode 100644 index 34631a0..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/corpus_gates.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Permanent corpus gates. Across every sample or game file reachable from the workspace root, -//! (1) the readable decompile must contain no generic/fallthrough renderings (no `Self*[`, -//! `route`, `cmp`, `Cmd`, `@raw`, or `argN`), and (2) every command list must round-trip -//! byte-exactly through the gated recompile path. Guards the "nothing generic, fully readable, -//! never lossy" contract for maps and common events at all engine versions. - -use wolf_decompiler::{ - compile_commands_enc, decompile_commands_annotated, decompile_commands_enc, - decompile_common_events, decompile_map, SymbolTable, -}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -/// Build a real symbol table for a code file (CE self-vars, the sibling databases' variable -/// names, and the engine glossary), mirroring the CLI so the annotate path resolves real names. -fn symbols_for(path: &std::path::Path, ce: Option<&CommonEventsFile>) -> SymbolTable { - let mut sym = SymbolTable::new(); - if let Some(ce) = ce { - sym.add_common_events(ce); - } - if let Some(dir) = path.parent() { - let basic = if dir.join("DataBase.project").exists() { - dir.to_path_buf() - } else { - dir.join("BasicData") - }; - for (stem, kind) in [ - ("DataBase", "UDB"), - ("CDataBase", "CDB"), - ("SysDatabase", "SDB"), - ] { - let proj = basic.join(format!("{stem}.project")); - let dat = basic.join(format!("{stem}.dat")); - if let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) { - if let Ok(db) = Database::read(&p, &d) { - sym.add_database(kind, &db); - } - } - } - } - sym.set_engine_text(wolf_decompiler::symbols::load_embedded_engine_glossary()); - sym -} - -fn corpus_files() -> Vec { - let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); - let mut stack = vec![root]; - let mut out = Vec::new(); - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else { - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let ext = p.extension().and_then(|s| s.to_str()).unwrap_or(""); - if name == "CommonEvent.dat" || ext.eq_ignore_ascii_case("mps") { - out.push(p); - } - } - } - } - out -} - -/// A generic/fallthrough token that should never appear in readable output. -fn fallthrough_hits(text: &str) -> Vec { - let mut hits = Vec::new(); - for line in text.lines() { - let l = line.trim(); - if l.contains("Self*[") - || l.contains("@raw ") - || prefixed_num(l, "route") - || prefixed_num(l, "cmp") - || prefixed_num(l, "Cmd") - || generic_argn(l) - { - hits.push(l.to_string()); - } - } - hits -} - -/// `prefix` immediately followed by a digit, not preceded by an alphanumeric (so `route47` -/// matches but `@route(` and `strcmp` do not). -fn prefixed_num(line: &str, prefix: &str) -> bool { - line.match_indices(prefix).any(|(i, _)| { - let before_ok = line[..i] - .chars() - .last() - .map_or(true, |c| !c.is_alphanumeric() && c != '@'); - before_ok - && line[i + prefix.len()..] - .chars() - .next() - .map_or(false, |c| c.is_ascii_digit()) - }) -} - -fn generic_argn(line: &str) -> bool { - line.match_indices("arg").any(|(i, _)| { - let before_ok = line[..i] - .chars() - .last() - .map_or(true, |c| !c.is_alphanumeric()); - let after = &line[i + 3..]; - before_ok - && after.chars().next().map_or(false, |c| c.is_ascii_digit()) - && after - .split_once('=') - .map_or(false, |(k, _)| k.chars().all(|c| c.is_ascii_digit())) - }) -} - -#[test] -fn no_fallthrough_renderings() { - let sym = SymbolTable::new(); - let mut total = 0usize; - let mut bad = 0usize; - for p in corpus_files() { - let bytes = std::fs::read(&p).unwrap_or_default(); - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let text = if name == "CommonEvent.dat" { - match CommonEventsFile::read(&bytes) { - Ok(ce) => decompile_common_events(&ce, &sym), - Err(_) => continue, // encrypted or out-of-scope inputs - } - } else { - match Map::read(&bytes) { - Ok(m) => decompile_map(&m, &sym), - Err(_) => continue, - } - }; - total += 1; - let hits = fallthrough_hits(&text); - if !hits.is_empty() { - bad += 1; - eprintln!( - "{} fallthrough(s) in {:?}; e.g. {}", - hits.len(), - p.file_name().unwrap(), - hits[0] - ); - } - } - eprintln!("scanned {total} readable files"); - assert!(total > 0, "corpus not found"); - assert_eq!(bad, 0, "files with generic/fallthrough renderings"); -} - -#[test] -fn annotate_roundtrip_byte_exact() { - // The unified read-plus-edit form (index-anchored bilingual names) must round-trip byte-exact - // with real symbols loaded. The recompiler strips the labels. The index is the authority. - let mut lists = 0usize; - let mut mismatch = 0usize; - let mut annotated = 0usize; - for p in corpus_files() { - let bytes = std::fs::read(&p).unwrap_or_default(); - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - if name == "CommonEvent.dat" { - let Ok(ce) = CommonEventsFile::read(&bytes) else { - continue; - }; - let sym = symbols_for(&p, Some(&ce)); - for ev in &ce.events { - lists += 1; - let text = - decompile_commands_annotated(&ev.commands, ce.utf8, &sym, Some(ev.int_id)); - if text.contains("[0 \"") || text.contains("\" ] ") || text.matches('"').count() > 0 - { - annotated += text - .lines() - .filter(|l| l.contains("[") && l.contains("\"")) - .count(); - } - match compile_commands_enc(&text, ce.utf8) { - Ok(back) if back == ev.commands => {} - _ => { - mismatch += 1; - if mismatch <= 5 { - eprintln!( - "annotate mismatch in {:?} CE {}", - p.file_name().unwrap(), - ev.int_id - ); - } - } - } - } - } else if p.extension().and_then(|s| s.to_str()) == Some("mps") { - let Ok(m) = Map::read(&bytes) else { continue }; - let sym = symbols_for(&p, None); - for ev in &m.events { - for pg in &ev.pages { - lists += 1; - let text = decompile_commands_annotated(&pg.commands, m.utf8, &sym, None); - match compile_commands_enc(&text, m.utf8) { - Ok(back) if back == pg.commands => {} - _ => { - mismatch += 1; - if mismatch <= 5 { - eprintln!("annotate mismatch in {:?}", p.file_name().unwrap()); - } - } - } - } - } - } - } - eprintln!("annotate: round-tripped {lists} lists, ~{annotated} annotated lines seen"); - assert!(lists > 0, "corpus not found"); - assert_eq!( - mismatch, 0, - "annotated command lists that did not round-trip byte-exactly" - ); -} - -#[test] -fn byte_exact_roundtrip() { - let mut lists = 0usize; - let mut mismatch = 0usize; - for p in corpus_files() { - let bytes = std::fs::read(&p).unwrap_or_default(); - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let programs: Vec<(bool, Vec<_>)> = if name == "CommonEvent.dat" { - match CommonEventsFile::read(&bytes) { - Ok(ce) => ce - .events - .into_iter() - .map(|ev| (ce.utf8, ev.commands)) - .collect(), - Err(_) => continue, - } - } else { - match Map::read(&bytes) { - Ok(m) => m - .events - .into_iter() - .flat_map(|ev| ev.pages.into_iter().map(move |pg| (m.utf8, pg.commands))) - .collect(), - Err(_) => continue, - } - }; - for (utf8, cmds) in programs { - lists += 1; - let text = decompile_commands_enc(&cmds, utf8); - match compile_commands_enc(&text, utf8) { - Ok(back) if back == cmds => {} - Ok(_) => { - mismatch += 1; - if mismatch <= 5 { - eprintln!("structural mismatch in {:?}", p.file_name().unwrap()); - } - } - Err(e) => { - mismatch += 1; - if mismatch <= 5 { - eprintln!("compile error in {:?}: {e}", p.file_name().unwrap()); - } - } - } - } - } - eprintln!("round-tripped {lists} command lists"); - assert!(lists > 0, "corpus not found"); - assert_eq!( - mismatch, 0, - "command lists that did not round-trip byte-exactly" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/db_apply.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/db_apply.rs deleted file mode 100644 index cb51bb9..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/db_apply.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! DB value-edit round-trip. Applying the export unchanged is byte-identical to the base. Editing -//! a single int and a single string changes only those cells and reloads correctly. -use wolf_decompiler::{apply_database_edit, database_to_json}; -use wolf_formats::database::Database; - -fn load() -> Option<(Vec, Vec, Database)> { - let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/BasicData"); - let (proj, dat) = (base.join("DataBase.project"), base.join("DataBase.dat")); - let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) else { - eprintln!("no corpus DataBase, skipping"); - return None; - }; - let db = Database::read(&p, &d).expect("parse db"); - Some((p, d, db)) -} - -#[test] -fn noop_apply_is_byte_identical() { - let Some((p, d, db)) = load() else { return }; - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = database_to_json(&db, "UDB", &glossary); - - let mut db2 = Database::read(&p, &d).unwrap(); - let changed = apply_database_edit(&json, &mut db2).expect("apply"); - assert_eq!( - changed, 0, - "round-tripping the export unchanged must change nothing" - ); - let (p2, d2) = db2.write(); - assert!( - p2 == p && d2 == d, - "no-op apply must be byte-identical to base" - ); -} - -#[test] -fn edit_one_int_and_one_string_roundtrips() { - let Some((p, d, db)) = load() else { return }; - - // Pick a type with rows, and (via the shared key derivation) an int and a string slot. - let (ti, int_key, int_slot, str_key, str_slot) = db - .types - .iter() - .enumerate() - .find_map(|(ti, t)| { - if t.data.is_empty() { - return None; - } - let fs = (t.dat_fields_size as usize).min(t.fields.len()); - let slots = wolf_decompiler::json::value_slots(t, fs, db.utf8); - let ik = slots.iter().find(|(_, is_str, _)| !is_str)?; - let sk = slots.iter().find(|(_, is_str, _)| *is_str)?; - Some((ti, ik.0.clone(), ik.2, sk.0.clone(), sk.2)) - }) - .expect("a type with int+string slots and rows"); - - let edit = format!( - r#"{{ "types": [ {{ "id": {ti}, "rows": [ {{ "id": 0, "values": {{ {ik}: 31337, {sk}: "WOLFDAWN_EDIT" }} }} ] }} ] }}"#, - ik = serde_json::Value::String(int_key), - sk = serde_json::Value::String(str_key), - ); - - let mut db2 = Database::read(&p, &d).unwrap(); - let changed = apply_database_edit(&edit, &mut db2).expect("apply"); - assert!(changed >= 1, "expected at least one changed cell"); - let (p2, d2) = db2.write(); - - let db3 = Database::read(&p2, &d2).expect("re-read edited"); - let t3 = &db3.types[ti]; - assert_eq!( - t3.data[0].int_values[int_slot], 31337, - "int edit must persist" - ); - assert_eq!( - wolf_decompiler::text::decode_wstr(&t3.data[0].string_values[str_slot], db3.utf8), - "WOLFDAWN_EDIT", - "string edit must persist" - ); - assert_eq!( - p2, p, - ".project must be unchanged when only .dat values are edited" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/db_json.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/db_json.rs deleted file mode 100644 index 5121673..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/db_json.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! DB to JSON export sanity gate: valid, balanced, faithful JSON for a real database. -use wolf_decompiler::database_to_json; -use wolf_formats::database::Database; - -#[test] -fn exports_valid_json_for_corpus_db() { - let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/BasicData"); - let (proj, dat) = (base.join("DataBase.project"), base.join("DataBase.dat")); - let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) else { - eprintln!("no corpus DataBase, skipping"); - return; - }; - let db = Database::read(&p, &d).expect("parse db"); - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = database_to_json(&db, "UDB", &glossary); - // Bilingual lookup: structural names the glossary covers carry a `name_en`, so a modder - // reading the English code can find the entry. Absent for player-facing content. - assert!( - json.contains("\"name_en\":"), - "expected at least one English name_en in the UDB export" - ); - - // Structurally valid: starts and ends as an object, braces and brackets balanced, key fields present. - assert!(json.trim_start().starts_with('{') && json.trim_end().ends_with('}')); - assert!(json.contains("\"kind\": \"UDB\"")); - assert!(json.contains("\"types\"") && json.contains("\"rows\"")); - let bal = |o: char, c: char| { - let mut depth = 0i64; - let mut in_str = false; - let mut esc = false; - for ch in json.chars() { - if esc { - esc = false; - continue; - } - match ch { - '\\' if in_str => esc = true, - '"' => in_str = !in_str, - x if x == o && !in_str => depth += 1, - x if x == c && !in_str => depth -= 1, - _ => {} - } - } - depth - }; - assert_eq!(bal('{', '}'), 0, "unbalanced braces"); - assert_eq!(bal('[', ']'), 0, "unbalanced brackets"); - eprintln!("UDB JSON: {} bytes, {} types", json.len(), db.types.len()); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/full_file_recompile.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/full_file_recompile.rs deleted file mode 100644 index 5097328..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/full_file_recompile.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Whole-file recompile gate: `compile(decompile_edit(file), base=file).write() == file`. -//! -//! This is the real edit-to-play round trip. It exercises the document envelope, the patch -//! substitution, and the format writer together, asserting byte-identity across every -//! CommonEvent.dat and .mps reachable (main corpus, every editor version, GamePro and Pro). -//! cargo test -p wolf-decompiler --test full_file_recompile -- --nocapture - -use std::path::{Path, PathBuf}; - -use wolf_decompiler::{ - compile_common_events_edit, compile_map_edit, decompile_common_events_edit, decompile_map_edit, -}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn wolf_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") -} - -/// Recurse `dir`, collecting every CommonEvent.dat and *.mps. -fn collect(dir: &Path, ces: &mut Vec, maps: &mut Vec) { - let Ok(rd) = std::fs::read_dir(dir) else { - return; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - collect(&p, ces, maps); - } else if p.file_name().and_then(|s| s.to_str()) == Some("CommonEvent.dat") { - ces.push(p); - } else if p.extension().and_then(|s| s.to_str()) == Some("mps") { - maps.push(p); - } - } -} - -fn ce_roundtrip(bytes: &[u8]) -> Result { - let ce = CommonEventsFile::read(bytes).map_err(|e| e.to_string())?; - let doc = decompile_common_events_edit(&ce); - let mut base = CommonEventsFile::read(bytes).map_err(|e| e.to_string())?; - compile_common_events_edit(&doc, &mut base).map_err(|e| e.to_string())?; - Ok(base.write() == bytes) -} - -fn map_roundtrip(bytes: &[u8]) -> Result { - let map = Map::read(bytes).map_err(|e| e.to_string())?; - let doc = decompile_map_edit(&map); - let mut base = Map::read(bytes).map_err(|e| e.to_string())?; - compile_map_edit(&doc, &mut base).map_err(|e| e.to_string())?; - Ok(base.write() == bytes) -} - -#[test] -fn whole_file_round_trip_byte_exact() { - let root = wolf_root(); - let mut ces = Vec::new(); - let mut maps = Vec::new(); - for sub in ["Data", "ALLWOLFVERSIONS", "GamePro_Data", "GameProData"] { - collect(&root.join(sub), &mut ces, &mut maps); - } - ces.sort(); - ces.dedup(); - maps.sort(); - maps.dedup(); - - if ces.is_empty() && maps.is_empty() { - eprintln!("no corpus found, skipping"); - return; - } - - let mut ok = 0usize; - let mut fail = 0usize; - let mut skip = 0usize; - let rel = |p: &Path| { - p.strip_prefix(&root) - .unwrap_or(p) - .display() - .to_string() - .replace('\\', "/") - }; - - // Encrypted (packaged) files need the archive layer to decrypt first. They are not plaintext - // inputs to the format/recompile path, so skip rather than fail. - let mut tally = |p: &Path, r: Result| match r { - Ok(true) => ok += 1, - Ok(false) => { - fail += 1; - eprintln!(" DIFF {}", rel(p)); - } - Err(e) if e.contains("encrypted") => skip += 1, - Err(e) => { - fail += 1; - eprintln!(" ERR {} ({e})", rel(p)); - } - }; - - for p in &ces { - let Ok(bytes) = std::fs::read(p) else { - continue; - }; - tally(p, ce_roundtrip(&bytes)); - } - for p in &maps { - let Ok(bytes) = std::fs::read(p) else { - continue; - }; - tally(p, map_roundtrip(&bytes)); - } - - eprintln!( - "\nwhole-file recompile: {ok} byte-exact, {fail} fail, {skip} skipped (encrypted) \ - ({} CE, {} maps)", - ces.len(), - maps.len() - ); - assert_eq!(fail, 0, "some files did not recompile byte-exact"); - assert!(ok > 0, "no files were exercised"); -} - -#[test] -fn edit_then_compile_takes_effect() { - // Prove the whole-file path is driven by the document text, not a memory of the base. Edit a - // Message in the decompiled doc, recompile, and confirm the bytes changed and the new text is - // present, and that the re-read file still parses. - let path = wolf_root().join("Data/BasicData/CommonEvent.dat"); - let Ok(bytes) = std::fs::read(&path) else { - eprintln!("corpus not found, skipping"); - return; - }; - let ce = CommonEventsFile::read(&bytes).unwrap(); - let doc = decompile_common_events_edit(&ce); - - // Find any Message line to mutate. - let Some(line) = doc.lines().find(|l| l.trim_start().starts_with("Message ")) else { - eprintln!("no Message command in corpus doc, skipping"); - return; - }; - let edited = doc.replacen(line, "Message \"WOLFDAWN_EDIT_MARKER\"", 1); - assert_ne!(edited, doc, "edit did not change the document"); - - let mut base = CommonEventsFile::read(&bytes).unwrap(); - compile_common_events_edit(&edited, &mut base).expect("edited doc compiles"); - let out = base.write(); - assert_ne!(out, bytes, "edited file should differ from the original"); - - // Re-read: the marker is present and the file is structurally valid. - let reread = CommonEventsFile::read(&out).expect("edited file re-parses"); - let found = reread.events.iter().any(|ev| { - ev.commands.iter().any(|c| { - c.cid == 101 - && c.str_args - .first() - .map_or(false, |s| s.as_bytes() == b"WOLFDAWN_EDIT_MARKER\0") - }) - }); - assert!(found, "edited Message text not found after recompile"); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/game_dat_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/game_dat_roundtrip.rs deleted file mode 100644 index 4f3474f..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/game_dat_roundtrip.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Game.dat support: byte-exact round-trip, Title extraction, and translated-Title inject. -//! Uses the corpus `Data/BasicData/Game.dat`. Skips gracefully if it is absent. - -use wolf_decompiler::{extract_game_dat, inject_game_dat, InjectOptions}; -use wolf_formats::game_dat::GameDat; - -fn corpus_game_dat() -> Option> { - let path = - std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/BasicData/Game.dat"); - std::fs::read(&path).ok() -} - -#[test] -fn read_write_is_byte_exact() { - let Some(bytes) = corpus_game_dat() else { - eprintln!("no corpus Game.dat, skipping"); - return; - }; - let gd = GameDat::read(&bytes).expect("parse Game.dat"); - assert!( - gd.write() == bytes, - "Game.dat round-trip must be byte-exact for an unmodified file" - ); -} - -#[test] -fn extract_yields_nonempty_title() { - let Some(bytes) = corpus_game_dat() else { - return; - }; - let gd = GameDat::read(&bytes).unwrap(); - let json = extract_game_dat(&gd); - let root: serde_json::Value = serde_json::from_str(&json).unwrap(); - let title = root["lines"] - .as_array() - .unwrap() - .iter() - .find(|l| l["key"] == "Title") - .expect("a Title line"); - assert!( - !title["source"].as_str().unwrap_or("").is_empty(), - "extracted Title must be non-empty" - ); -} - -#[test] -fn inject_translated_title_persists_and_reparses() { - let Some(bytes) = corpus_game_dat() else { - return; - }; - let gd = GameDat::read(&bytes).unwrap(); - let title_src = { - let json = extract_game_dat(&gd); - let root: serde_json::Value = serde_json::from_str(&json).unwrap(); - root["lines"] - .as_array() - .unwrap() - .iter() - .find(|l| l["key"] == "Title") - .unwrap()["source"] - .as_str() - .unwrap() - .to_string() - }; - - // No-op inject (text == source) must leave the file byte-identical. - let edit_noop = format!( - r#"{{ "lines": [ {{ "key": "Title", "source": {0}, "text": {0} }} ] }}"#, - serde_json::Value::String(title_src.clone()) - ); - let mut gd_noop = GameDat::read(&bytes).unwrap(); - let st = inject_game_dat(&edit_noop, &mut gd_noop, &InjectOptions::default()).expect("inject"); - assert_eq!(st.applied, 0); - assert_eq!(st.drifted, 0); - assert!( - gd_noop.write() == bytes, - "no-op Game.dat inject must be byte-identical" - ); - - // Translate the Title and confirm it persists, the file re-parses, and other fields are intact. - let edit = format!( - r#"{{ "lines": [ {{ "key": "Title", "source": {}, "text": "WolfDawn Title" }} ] }}"#, - serde_json::Value::String(title_src.clone()) - ); - let mut gd2 = GameDat::read(&bytes).unwrap(); - let st = inject_game_dat(&edit, &mut gd2, &InjectOptions::default()).expect("inject"); - assert_eq!(st.applied, 1, "the Title should translate"); - - let out = gd2.write(); - let gd3 = GameDat::read(&out).expect("injected Game.dat must re-parse"); - let json3 = extract_game_dat(&gd3); - let root3: serde_json::Value = serde_json::from_str(&json3).unwrap(); - let new_title = root3["lines"] - .as_array() - .unwrap() - .iter() - .find(|l| l["key"] == "Title") - .unwrap()["source"] - .as_str() - .unwrap(); - assert_eq!(new_title, "WolfDawn Title", "Title must read the new text"); - - // Other non-title fields are untouched. The decrypt key, font, and the housekeeping - // word-data carry over verbatim. - assert_eq!(gd3.decrypt_key, gd.decrypt_key, "decrypt key intact"); - assert_eq!(gd3.font, gd.font, "font intact"); - assert_eq!( - gd3.unknown_word_data, gd.unknown_word_data, - "word data intact" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/gamedat_full.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/gamedat_full.rs deleted file mode 100644 index 5839279..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/gamedat_full.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! Full Game.dat field editing (`dump_game_dat` / `apply_game_dat`): byte-exact no-op round-trip, -//! editing a scalar field, editing the Title (length change), and the `None`-optional omission -//! invariant. Uses an unpacked original Game.dat if present (the `WOLFDAWN_TEST_DATA` fixture first, -//! the corpus `Data/BasicData/Game.dat` as a fallback). Skips gracefully if neither is available. - -use wolf_decompiler::{apply_game_dat, dump_game_dat}; -use wolf_formats::game_dat::GameDat; - -mod common; -use common::test_data; - -/// Locate an unpacked original Game.dat: the fixtures copy first, then the workspace corpus. -fn fixture() -> Option> { - let candidates = [ - test_data("chamber/Data/BasicData/Game.dat"), - Some( - std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../../Data/BasicData/Game.dat"), - ), - ]; - candidates - .into_iter() - .flatten() - .find_map(|p| std::fs::read(&p).ok()) -} - -#[test] -fn dump_apply_write_is_byte_exact_noop() { - let Some(bytes) = fixture() else { - eprintln!("no Game.dat fixture, skipping"); - return; - }; - let mut gd = GameDat::read(&bytes).expect("parse Game.dat"); - let json = dump_game_dat(&gd); - // The dump is valid JSON. - let _: serde_json::Value = serde_json::from_str(&json).expect("dump is valid JSON"); - // Applying the dump back onto the file changes nothing... - let changed = apply_game_dat(&mut gd, &json).expect("apply dump"); - assert_eq!(changed, 0, "round-tripping the dump must change no fields"); - // ...and the write is byte-exact. - assert!( - gd.write() == bytes, - "dump -> apply -> write must reproduce the file byte-exact" - ); -} - -#[test] -fn edit_font_round_trips_and_leaves_other_fields_intact() { - let Some(bytes) = fixture() else { - return; - }; - let original = GameDat::read(&bytes).expect("parse base"); - - // Build an edit JSON from the dump, swapping only the Font. - let dump = dump_game_dat(&original); - let mut root: serde_json::Value = serde_json::from_str(&dump).unwrap(); - root["Font"] = serde_json::Value::String("TestFont".to_string()); - let edit = serde_json::to_string(&root).unwrap(); - - let mut gd = GameDat::read(&bytes).unwrap(); - let changed = apply_game_dat(&mut gd, &edit).expect("apply edit"); - assert_eq!(changed, 1, "only the Font should change"); - - let out = gd.write(); - let reread = GameDat::read(&out).expect("edited Game.dat must re-parse"); - - // Font reads back as the new value. - let reread_json: serde_json::Value = - serde_json::from_str(&dump_game_dat(&reread)).unwrap(); - assert_eq!(reread_json["Font"], "TestFont", "Font must read the new text"); - - // Every other field is byte-identical to the original struct. - assert_eq!(reread.title, original.title, "title intact"); - assert_eq!(reread.title_plus, original.title_plus, "title_plus intact"); - assert_eq!(reread.sub_fonts, original.sub_fonts, "sub_fonts intact"); - assert_eq!( - reread.default_pc_graphic, original.default_pc_graphic, - "default_pc_graphic intact" - ); - assert_eq!(reread.road_img, original.road_img, "road_img intact"); - assert_eq!(reread.gauge_img, original.gauge_img, "gauge_img intact"); - assert_eq!(reread.start_up_msg, original.start_up_msg, "start_up_msg intact"); - assert_eq!(reread.title_msg, original.title_msg, "title_msg intact"); - assert_eq!( - reread.unknown_string14, original.unknown_string14, - "unknown_string14 intact" - ); - assert_eq!(reread.decrypt_key, original.decrypt_key, "decrypt key intact"); - assert_eq!(reread.magic_string, original.magic_string, "magic string intact"); - assert_eq!( - reread.unknown_word_data, original.unknown_word_data, - "word data intact" - ); -} - -#[test] -fn edit_title_length_change_round_trips() { - let Some(bytes) = fixture() else { - return; - }; - let original = GameDat::read(&bytes).unwrap(); - let old_title = { - let d: serde_json::Value = serde_json::from_str(&dump_game_dat(&original)).unwrap(); - d["Title"].as_str().unwrap().to_string() - }; - - // A new title of a clearly different length, to exercise the housekeeping-offset shift. - let new_title = format!("{old_title} -- WolfDawn Full Edit Demo"); - assert_ne!(new_title.len(), old_title.len(), "fixture sanity: length differs"); - - let mut root: serde_json::Value = serde_json::from_str(&dump_game_dat(&original)).unwrap(); - root["Title"] = serde_json::Value::String(new_title.clone()); - let edit = serde_json::to_string(&root).unwrap(); - - let mut gd = GameDat::read(&bytes).unwrap(); - let changed = apply_game_dat(&mut gd, &edit).expect("apply title edit"); - assert_eq!(changed, 1, "only the Title should change"); - - let out = gd.write(); - // The file must re-parse cleanly despite the length change (offsets shifted correctly). - let reread = GameDat::read(&out).expect("re-parse after length-changing Title edit"); - let reread_title = { - let d: serde_json::Value = serde_json::from_str(&dump_game_dat(&reread)).unwrap(); - d["Title"].as_str().unwrap().to_string() - }; - assert_eq!(reread_title, new_title, "Title must read the new (longer) text"); - // Non-title fields still intact. - assert_eq!(reread.font, original.font, "font intact after title resize"); - assert_eq!( - reread.default_pc_graphic, original.default_pc_graphic, - "graphic intact after title resize" - ); -} - -#[test] -fn dump_omits_none_optionals_and_includes_present() { - let Some(bytes) = fixture() else { - return; - }; - let gd = GameDat::read(&bytes).unwrap(); - let json = dump_game_dat(&gd); - let root: serde_json::Value = serde_json::from_str(&json).unwrap(); - let obj = root.as_object().unwrap(); - - // Fixed scalars, the array, and the encoding marker are always present. - assert!(obj.contains_key("encoding"), "encoding marker present"); - assert!(obj.contains_key("Title"), "Title always present"); - assert!(obj.contains_key("Font"), "Font always present"); - assert!(obj.contains_key("DefaultPcGraphic"), "graphic always present"); - assert!(root["SubFonts"].is_array(), "SubFonts is an array"); - assert_eq!( - root["SubFonts"].as_array().unwrap().len(), - gd.sub_fonts.len(), - "SubFonts array length matches the struct" - ); - - // Each optional key appears in the JSON iff the struct cell is Some. - let checks: [(&str, bool); 6] = [ - ("TitlePlus", gd.title_plus.is_some()), - ("RoadImg", gd.road_img.is_some()), - ("GaugeImg", gd.gauge_img.is_some()), - ("StartUpMsg", gd.start_up_msg.is_some()), - ("TitleMsg", gd.title_msg.is_some()), - ("UnknownString14", gd.unknown_string14.is_some()), - ]; - for (key, present) in checks { - assert_eq!( - obj.contains_key(key), - present, - "`{key}` key presence must match the optional cell" - ); - } - - // The read-only encoding marker must match the file. - assert_eq!( - root["encoding"].as_str().unwrap(), - if gd.utf8 { "utf8" } else { "shiftjis" } - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/gamepro_decompile.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/gamepro_decompile.rs deleted file mode 100644 index 93c0547..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/gamepro_decompile.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Probe that reports and never asserts. Runs the decompiler and recompiler against the Wolf-Pro -//! game's 1300 v3.5 common events and maps. Reports (1) byte-exact recompile coverage, (2) any -//! command ids missing from the spec, and (3) how readable the output is (pretty vs @raw -//! fallback), driven by the per-command v3.5 trailing blob. -//! cargo test -p wolf-decompiler --test gamepro_decompile -- --nocapture - -use std::collections::BTreeMap; -use std::path::PathBuf; - -use wolf_decompiler::{compile_commands, decompile_commands, spec}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn pro() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("GamePro_Data") -} - -#[test] -fn recompile_pro_common_events() { - let p = pro().join("BasicData").join("CommonEvent.dat"); - let Ok(bytes) = std::fs::read(&p) else { - eprintln!("missing {}", p.display()); - return; - }; - let ce = CommonEventsFile::read(&bytes).expect("parse Pro CommonEvent.dat"); - eprintln!( - "Pro CommonEvent.dat: v35={} events={}", - ce.v35, - ce.events.len() - ); - - let mut ok = 0usize; - let mut fail = 0usize; - let mut unknown_cids: BTreeMap = BTreeMap::new(); - let mut blob_sizes: BTreeMap = BTreeMap::new(); - let mut pretty_lines = 0usize; - let mut raw_lines = 0usize; - let mut failures: Vec<(u32, String)> = Vec::new(); - - for ev in &ce.events { - for c in &ev.commands { - if spec::command(c.cid).is_none() { - *unknown_cids.entry(c.cid).or_default() += 1; - } - let bl = c.v35_blob.as_ref().map(|b| b.len()).unwrap_or(usize::MAX); - if bl != usize::MAX { - *blob_sizes.entry(bl).or_default() += 1; - } - } - - let text = decompile_commands(&ev.commands); - for line in text.lines() { - let t = line.trim_start(); - if t.is_empty() { - continue; - } - if t.starts_with("@raw ") { - raw_lines += 1; - } else { - pretty_lines += 1; - } - } - - match compile_commands(&text) { - Ok(back) if back == ev.commands => ok += 1, - Ok(back) => { - fail += 1; - let idx = back.iter().zip(&ev.commands).position(|(a, b)| a != b); - let detail = match idx { - Some(i) => format!( - "cmd#{i} cid{}: orig {:?} vs back {:?}", - ev.commands[i].cid, ev.commands[i], back[i] - ), - None => format!("length {} vs {}", back.len(), ev.commands.len()), - }; - failures.push((ev.int_id, detail)); - } - Err(e) => { - fail += 1; - failures.push((ev.int_id, format!("compile error: {e}"))); - } - } - } - - eprintln!( - "recompile byte-exact: {ok}/{} (fail {fail})", - ce.events.len() - ); - eprintln!("readability: pretty lines={pretty_lines} @raw lines={raw_lines}"); - eprintln!("v35 blob size distribution (size: count): {blob_sizes:?}"); - if unknown_cids.is_empty() { - eprintln!("unknown command ids: NONE - every cid is in the spec"); - } else { - eprintln!("unknown command ids (cid: occurrences): {unknown_cids:?}"); - } - eprintln!("--- all {} failures ---", failures.len()); - for (id, detail) in &failures { - eprintln!(" event {id}: {detail}"); - } - - // Dump a couple of examples of each unknown command id, with arg shape, to identify them. - for &cid in &[242u32, 252, 260] { - let mut shown = 0; - for ev in &ce.events { - for c in &ev.commands { - if c.cid == cid && shown < 3 { - eprintln!( - " ex cid{cid}: ints={:?} strs={:?}", - c.int_args, - c.str_args - .iter() - .map(|s| String::from_utf8_lossy(s.as_bytes()) - .trim_end_matches('\0') - .to_string()) - .collect::>() - ); - shown += 1; - } - } - } - } - - assert_eq!( - ok, - ce.events.len(), - "all Pro common events must recompile byte-exact" - ); -} - -#[test] -fn scan_pro_map_commands() { - let dir = pro().join("MapData"); - let Ok(entries) = std::fs::read_dir(&dir) else { - return; - }; - let mut unknown: BTreeMap = BTreeMap::new(); - let mut cmds = 0usize; - let mut maps = 0usize; - for e in entries { - let path = e.unwrap().path(); - if path.extension().and_then(|s| s.to_str()) != Some("mps") { - continue; - } - let bytes = std::fs::read(&path).unwrap(); - let Ok(m) = Map::read(&bytes) else { continue }; - maps += 1; - for ev in &m.events { - for pg in &ev.pages { - for c in &pg.commands { - cmds += 1; - if spec::command(c.cid).is_none() { - *unknown.entry(c.cid).or_default() += 1; - } - } - } - } - } - eprintln!("Pro maps scanned: {maps}, commands: {cmds}"); - if unknown.is_empty() { - eprintln!("unknown map command ids: NONE"); - } else { - eprintln!("unknown map command ids: {unknown:?}"); - } -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/merge_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/merge_roundtrip.rs deleted file mode 100644 index e3f3623..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/merge_roundtrip.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Incremental translation-merge (translation memory) round-trip tests. No corpus is needed. -//! Every fixture is a tiny hand-built JSON, exercising the generic `source`/`text` walker across -//! the different document shapes (names, scenes, db-like). -//! -//! Gates: -//! (a) build_memory keeps only real translations (drops `text == source`) and reports a source -//! that diverges across two files as a conflict, keeping the first value. -//! (b) apply_memory carries a memory hit into an untranslated leaf, leaves a no-match leaf -//! untranslated (still_new), and never overwrites a leaf the new file already translated -//! (kept_existing). -//! (c) Structure preservation: an empty memory leaves every `text` unchanged and re-parses to -//! the same logical content, and non-`source`/`text` fields are preserved exactly. - -use std::collections::HashMap; - -use serde_json::Value; -use wolf_decompiler::{apply_memory, build_memory, dropped_sources, MergeStats}; - -/// Collect `source -> text` from a parsed value, generically (any object with both string fields). -fn pairs(v: &Value) -> HashMap { - fn walk(v: &Value, out: &mut HashMap) { - match v { - Value::Object(m) => { - if let (Some(Value::String(s)), Some(Value::String(t))) = - (m.get("source"), m.get("text")) - { - out.insert(s.clone(), t.clone()); - } - for c in m.values() { - walk(c, out); - } - } - Value::Array(a) => { - for c in a { - walk(c, out); - } - } - _ => {} - } - } - let mut out = HashMap::new(); - walk(v, &mut out); - out -} - -// --------------------------------------------------------------------------- -// (a) build_memory -// --------------------------------------------------------------------------- - -#[test] -fn build_memory_excludes_untranslated() { - // {names:[{source:"あ",text:"A"},{source:"い",text:"い"}]} yields {"あ":"A"} only. - let names = r#"{"kind":"names","names":[ - {"source":"あ","text":"A","occurrences":2,"note":"Monster"}, - {"source":"い","text":"い","occurrences":1,"note":"Item"} - ]}"#; - let (mem, conflicts) = build_memory(&[names]); - assert_eq!(mem.len(), 1); - assert_eq!(mem.get("あ").map(String::as_str), Some("A")); - assert!(!mem.contains_key("い"), "untranslated entry excluded"); - assert!(conflicts.is_empty()); -} - -#[test] -fn build_memory_divergent_source_is_conflict_first_kept() { - // Same source, two distinct translations across two files is a conflict. The first is kept. - let f1 = r#"{"scenes":[{"lines":[{"source":"あ","text":"A1"}]}]}"#; - let f2 = r#"{"scenes":[{"lines":[{"source":"あ","text":"A2"}]}]}"#; - let (mem, conflicts) = build_memory(&[f1, f2]); - assert_eq!(mem.get("あ").map(String::as_str), Some("A1"), "first input wins"); - assert_eq!(conflicts.len(), 1); - let (src, variants) = &conflicts[0]; - assert_eq!(src, "あ"); - assert_eq!(variants.first().map(String::as_str), Some("A1"), "kept value leads"); - assert!(variants.iter().any(|t| t == "A2")); - assert_eq!(variants.len(), 2); -} - -// --------------------------------------------------------------------------- -// (b) apply_memory -// --------------------------------------------------------------------------- - -#[test] -fn apply_memory_carries_and_keeps() { - // New scenes: あ matches memory (carried), う has no entry (still_new), and a pre-translated - // entry (text != source) is kept untouched (kept_existing). - let new = r#"{"scenes":[{"lines":[ - {"cmd":0,"str":0,"speaker":"NPC","source":"あ","text":"あ"}, - {"cmd":1,"str":0,"speaker":"NPC","source":"う","text":"う"}, - {"cmd":2,"str":0,"speaker":"NPC","source":"え","text":"AlreadyDone"} - ]}]}"#; - let mut mem = HashMap::new(); - mem.insert("あ".to_string(), "A".to_string()); - // A memory entry for え exists too, but the new file already translated it, so it must not override. - mem.insert("え".to_string(), "ShouldNotWin".to_string()); - - let (out, stats) = apply_memory(new, &mem).unwrap(); - assert_eq!( - stats, - MergeStats { - carried: 1, - still_new: 1, - kept_existing: 1 - } - ); - let v: Value = serde_json::from_str(&out).unwrap(); - let p = pairs(&v); - assert_eq!(p.get("あ").map(String::as_str), Some("A"), "carried"); - assert_eq!(p.get("う").map(String::as_str), Some("う"), "still untranslated"); - assert_eq!( - p.get("え").map(String::as_str), - Some("AlreadyDone"), - "existing translation kept, not overwritten by memory" - ); -} - -// --------------------------------------------------------------------------- -// (c) structure preservation -// --------------------------------------------------------------------------- - -#[test] -fn apply_empty_memory_is_structural_noop() { - // Applying an empty memory leaves every `text` unchanged and preserves all sibling fields - // (speaker, cmd, row, and so on). The re-serialized JSON parses to the same logical content. - let new = r#"{"kind":"map","scenes":[{"event":4,"lines":[ - {"cmd":3,"str":1,"row":2,"speaker":"勇者","speaker_src":"勇者","source":"あ","text":"あ"}, - {"cmd":4,"str":0,"row":5,"speaker":"魔王","speaker_src":"魔王","source":"い","text":"い"} - ]}]}"#; - let before: Value = serde_json::from_str(new).unwrap(); - let (out, stats) = apply_memory(new, &HashMap::new()).unwrap(); - assert_eq!(stats.carried, 0); - assert_eq!(stats.kept_existing, 0); - assert_eq!(stats.still_new, 2); - let after: Value = serde_json::from_str(&out).unwrap(); - assert_eq!(before, after, "empty-memory apply must be a logical no-op"); -} - -#[test] -fn non_source_text_fields_survive_a_carry() { - // When a carry happens, only `text` changes. Every other field on the leaf is preserved. - let new = r#"{"groups":[{"type":0,"lines":[ - {"row":7,"field":2,"rowName":"Slime","fieldName":"Name","source":"あ","text":"あ"} - ]}]}"#; - let mut mem = HashMap::new(); - mem.insert("あ".to_string(), "A".to_string()); - let (out, _) = apply_memory(new, &mem).unwrap(); - let v: Value = serde_json::from_str(&out).unwrap(); - let leaf = &v["groups"][0]["lines"][0]; - assert_eq!(leaf["text"], Value::String("A".into()), "text carried"); - assert_eq!(leaf["row"], Value::from(7)); - assert_eq!(leaf["field"], Value::from(2)); - assert_eq!(leaf["rowName"], Value::String("Slime".into())); - assert_eq!(leaf["fieldName"], Value::String("Name".into())); - assert_eq!(leaf["source"], Value::String("あ".into()), "source unchanged"); -} - -// --------------------------------------------------------------------------- -// dropped reporting -// --------------------------------------------------------------------------- - -#[test] -fn dropped_sources_lists_removed_in_update() { - let old = r#"{"lines":[{"source":"あ","text":"A"},{"source":"い","text":"I"}]}"#; - let (mem, _) = build_memory(&[old]); - // The new extraction dropped い (removed in the update). - let new = r#"{"lines":[{"source":"あ","text":"あ"}]}"#; - let dropped = dropped_sources(&mem, &[new]); - assert_eq!(dropped, vec!["い".to_string()]); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/names_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/names_roundtrip.rs deleted file mode 100644 index 1a782db..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/names_roundtrip.rs +++ /dev/null @@ -1,407 +0,0 @@ -//! Project-level name glossary round-trip, against the real corpus at `/Data` -//! (resolved via `CARGO_MANIFEST_DIR/../../../Data`). Skips gracefully when the corpus is absent. -//! -//! Gates: -//! (a) Extract then no-op inject (every `text == source`) leaves every DB pair, CommonEvent, -//! and map byte-identical. -//! (b) Translating one name and injecting rewrites it in every place it appears: its stored -//! `DbData.name`, every name/display cell that held it, and every command by-name reference. -//! All other strings and every other file's bytes stay untouched, and every file still -//! re-parses. -//! (c) Extraction is deterministic (byte-identical across runs). - -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; - -use serde_json::Value; -use wolf_decompiler::text::decode_wstr; -use wolf_decompiler::{ - extract_db_name_cells, extract_db_strings, extract_names, inject_names, InjectOptions, -}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -/// The BasicData dir of the corpus, or `None` when the corpus is not present. -fn corpus_basic() -> Option { - let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/BasicData"); - base.join("DataBase.project").exists().then_some(base) -} - -fn map_data_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/MapData") -} - -/// A loaded database with the original on-disk bytes kept for byte-identity checks. -struct LoadedDb { - proj: Vec, - dat: Vec, - db: Database, - stem: &'static str, -} - -fn load_dbs(basic: &Path) -> Vec { - let mut out = Vec::new(); - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - let proj = basic.join(format!("{stem}.project")); - let dat = basic.join(format!("{stem}.dat")); - if let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) { - if let Ok(db) = Database::read(&p, &d) { - out.push(LoadedDb { - proj: p, - dat: d, - db, - stem, - }); - } - } - } - out -} - -fn load_ce(basic: &Path) -> Option<(Vec, CommonEventsFile)> { - let b = std::fs::read(basic.join("CommonEvent.dat")).ok()?; - let ce = CommonEventsFile::read(&b).ok()?; - Some((b, ce)) -} - -fn load_maps() -> Vec<(Vec, Map)> { - let mut out = Vec::new(); - let Ok(rd) = std::fs::read_dir(map_data_dir()) else { - return out; - }; - let mut paths: Vec = rd - .flatten() - .map(|e| e.path()) - .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("mps")) - .collect(); - paths.sort(); - for p in paths { - if let Ok(b) = std::fs::read(&p) { - if let Ok(m) = Map::read(&b) { - out.push((b, m)); - } - } - } - out -} - -fn glossary() -> HashMap { - wolf_decompiler::symbols::load_embedded_engine_glossary() -} - -/// Run extract over the loaded corpus. -fn extract(dbs: &[LoadedDb], ce: Option<&CommonEventsFile>, maps: &[(Vec, Map)]) -> String { - let db_vec: Vec = dbs.iter().map(|l| l.db.clone()).collect(); - let ce_refs: Vec<&CommonEventsFile> = ce.into_iter().collect(); - let map_vec: Vec = maps.iter().map(|(_, m)| m.clone()).collect(); - extract_names(&db_vec, &ce_refs, &map_vec, &glossary()) -} - -#[test] -fn extract_then_noop_inject_is_byte_identical() { - let Some(basic) = corpus_basic() else { - eprintln!("no corpus, skipping"); - return; - }; - let dbs = load_dbs(&basic); - let ce = load_ce(&basic); - let maps = load_maps(); - assert!(!dbs.is_empty(), "corpus DBs must load"); - - // Extract is the identity glossary (every text == source), so injecting it must be a no-op. - let names_json = extract(&dbs, ce.as_ref().map(|(_, c)| c), &maps); - - let mut db_vec: Vec = dbs.iter().map(|l| l.db.clone()).collect(); - let mut ce_clone = ce.as_ref().map(|(_, c)| c.clone()); - let mut ce_refs: Vec<&mut CommonEventsFile> = ce_clone.as_mut().into_iter().collect(); - let mut map_vec: Vec = maps.iter().map(|(_, m)| m.clone()).collect(); - - let st = inject_names( - &names_json, - &mut db_vec, - &mut ce_refs, - &mut map_vec, - &InjectOptions::default(), - &glossary(), - ) - .expect("inject"); - assert_eq!( - st.applied, 0, - "a no-op (identity) glossary must apply zero changes" - ); - - // Every file must re-serialize byte-identical to its original on-disk bytes. - for (l, db) in dbs.iter().zip(&db_vec) { - let (p, d) = db.write(); - assert!( - p == l.proj && d == l.dat, - "{}: no-op inject changed the DB bytes", - l.stem - ); - } - if let (Some((b, _)), Some(c)) = (&ce, &ce_clone) { - assert!( - &c.write() == b, - "no-op inject changed the CommonEvent bytes" - ); - } - for ((b, _), m) in maps.iter().zip(&map_vec) { - assert!(&m.write() == b, "no-op inject changed a map's bytes"); - } -} - -/// Count how many times `source` appears as: a stored row name, a string cell value (any field), -/// and a by-name command reference (cid250/252 str2, cid251 str1, cid112 any str). -struct Occ { - row_names: usize, - cell_values: usize, - refs: usize, -} - -fn count_occ( - source: &str, - dbs: &[Database], - ce: Option<&CommonEventsFile>, - maps: &[Map], -) -> Occ { - let mut o = Occ { - row_names: 0, - cell_values: 0, - refs: 0, - }; - for db in dbs { - let utf8 = db.utf8; - for t in &db.types { - for row in &t.data { - if decode_wstr(&row.name, utf8) == source { - o.row_names += 1; - } - for w in &row.string_values { - if decode_wstr(w, utf8) == source { - o.cell_values += 1; - } - } - } - } - } - let count_refs = |cmds: &[wolf_formats::command::RawCommand], utf8: bool, o: &mut Occ| { - for cmd in cmds { - let idx: Vec = match cmd.cid { - 250 | 252 => vec![2], - 251 => vec![1], - 112 => (0..cmd.str_args.len()).collect(), - _ => vec![], - }; - for si in idx { - if let Some(w) = cmd.str_args.get(si) { - if decode_wstr(w, utf8) == source { - o.refs += 1; - } - } - } - } - }; - if let Some(ce) = ce { - for ev in &ce.events { - count_refs(&ev.commands, ce.utf8, &mut o); - } - } - for m in maps { - for ev in &m.events { - for pg in &ev.pages { - count_refs(&pg.commands, m.utf8, &mut o); - } - } - } - o -} - -#[test] -fn translate_one_name_updates_all_mirrors_consistently() { - let Some(basic) = corpus_basic() else { - eprintln!("no corpus, skipping"); - return; - }; - let dbs = load_dbs(&basic); - let ce = load_ce(&basic); - let maps = load_maps(); - - let names_json = extract(&dbs, ce.as_ref().map(|(_, c)| c), &maps); - let root: Value = serde_json::from_str(&names_json).unwrap(); - let names = root["names"].as_array().unwrap(); - - let db_snap: Vec = dbs.iter().map(|l| l.db.clone()).collect(); - let ce_snap = ce.as_ref().map(|(_, c)| c.clone()); - let map_snap: Vec = maps.iter().map(|(_, m)| m.clone()).collect(); - - // Pick a target name that lives in both a stored row name and a by-name command reference, - // the cross-mirror case the glossary exists to keep consistent. In the sample corpus these - // are the CDB variable rows looked up by `cid250` data-name. - let target = names - .iter() - .map(|e| e["source"].as_str().unwrap().to_string()) - .find(|s| { - let o = count_occ(s, &db_snap, ce_snap.as_ref(), &map_snap); - o.row_names >= 1 && o.refs >= 1 - }) - .expect("a name occurring as both a row name and a by-name reference"); - let before = count_occ(&target, &db_snap, ce_snap.as_ref(), &map_snap); - - // A translation representable in both UTF-8 and Shift-JIS (ASCII) so the encoding guard never - // skips it, and not already present in the corpus. - let translated = "WOLFDAWN_NAME_X"; - assert!( - count_occ(translated, &db_snap, ce_snap.as_ref(), &map_snap).row_names - + count_occ(translated, &db_snap, ce_snap.as_ref(), &map_snap).cell_values - == 0, - "sentinel must not pre-exist" - ); - - // Build a one-entry glossary translating only the target. - let one = serde_json::json!({ - "kind": "names", - "names": [ { "source": target, "text": translated, "occurrences": 0, "note": "" } ] - }) - .to_string(); - - // Inject onto fresh clones. - let mut db_vec: Vec = dbs.iter().map(|l| l.db.clone()).collect(); - let mut ce_clone = ce.as_ref().map(|(_, c)| c.clone()); - let mut ce_refs: Vec<&mut CommonEventsFile> = ce_clone.as_mut().into_iter().collect(); - let mut map_vec: Vec = maps.iter().map(|(_, m)| m.clone()).collect(); - let st = inject_names( - &one, - &mut db_vec, - &mut ce_refs, - &mut map_vec, - &InjectOptions::default(), - &glossary(), - ) - .expect("inject"); - - // Every mirror that held the source now holds the translation. The source is gone. - let after_src = count_occ(&target, &db_vec, ce_clone.as_ref(), &map_vec); - let after_dst = count_occ(translated, &db_vec, ce_clone.as_ref(), &map_vec); - - assert_eq!( - after_src.row_names, 0, - "the source row name must be rewritten" - ); - assert_eq!( - after_src.refs, 0, - "every by-name reference to the source must be rewritten" - ); - assert_eq!( - after_src.cell_values, 0, - "every cell holding the source must be rewritten" - ); - assert_eq!( - after_dst.row_names, before.row_names, - "row-name mirror count preserved under translation" - ); - assert_eq!( - after_dst.refs, before.refs, - "by-name reference count preserved under translation" - ); - assert_eq!( - after_dst.cell_values, before.cell_values, - "cell-value mirror count preserved under translation" - ); - // The applied count is at least the number of mirrors that changed (DB cells, row names, and - // refs). The reference mirror is the headline cross-file guarantee. - assert!(before.refs >= 1 && before.row_names >= 1); - assert!( - st.applied >= before.row_names + before.refs, - "applied ({}) must cover every rewritten mirror (row names {} + refs {})", - st.applied, - before.row_names, - before.refs - ); - - // Every file still re-parses (never ship a structurally-corrupt file). - for db in &db_vec { - let (p, d) = db.write(); - Database::read(&p, &d).expect("injected DB re-parses"); - } - if let Some(c) = &ce_clone { - CommonEventsFile::read(&c.write()).expect("injected CommonEvent re-parses"); - } - for m in &map_vec { - Map::read(&m.write()).expect("injected map re-parses"); - } - - // Untouched files: a file with no occurrence of the target must be byte-identical to the base. - for (l, db) in dbs.iter().zip(&db_vec) { - let touched = count_occ(&target, std::slice::from_ref(&l.db), None, &[]).row_names - + count_occ(&target, std::slice::from_ref(&l.db), None, &[]).cell_values - > 0; - if !touched { - let (p, d) = db.write(); - assert!( - p == l.proj && d == l.dat, - "{}: a DB with no target occurrence must be byte-identical", - l.stem - ); - } - } -} - -#[test] -fn extraction_is_deterministic() { - let Some(basic) = corpus_basic() else { - eprintln!("no corpus, skipping"); - return; - }; - let dbs = load_dbs(&basic); - let ce = load_ce(&basic); - let maps = load_maps(); - - let a = extract(&dbs, ce.as_ref().map(|(_, c)| c), &maps); - let b = extract(&dbs, ce.as_ref().map(|(_, c)| c), &maps); - assert_eq!(a, b, "extraction must be byte-identical across runs"); - assert!( - a.contains("\"kind\": \"names\""), - "names glossary has the expected shape" - ); - assert!(a.matches("\"source\"").count() > 0, "corpus yields names"); -} - -/// The disjoint-ownership invariant: every DB string cell is extracted by at most one path, -/// either db-strings (per-file content) or the glossary (per-cell name). On the real corpus, no -/// `(type, row, field)` locator may appear in both `extract_db_strings` and the glossary's -/// `extract_db_name_cells`. -#[test] -fn db_string_cells_have_disjoint_ownership() { - let Some(basic) = corpus_basic() else { - eprintln!("no corpus, skipping"); - return; - }; - let dbs = load_dbs(&basic); - assert!(!dbs.is_empty(), "corpus DBs must load"); - let g = glossary(); - let mut checked_cells = 0usize; - for l in &dbs { - // db-strings (content) cell locators. - let content: HashSet<(usize, usize, usize)> = extract_db_strings(&l.db, &g) - .iter() - .flat_map(|grp| grp.lines.iter().map(|ln| (ln.type_id, ln.row, ln.field))) - .collect(); - // glossary (name) cell locators. - let names: HashSet<(usize, usize, usize)> = - extract_db_name_cells(&l.db, &g).into_iter().collect(); - checked_cells += content.len() + names.len(); - let overlap: Vec<_> = content.intersection(&names).collect(); - assert!( - overlap.is_empty(), - "{}: {} DB cell(s) extracted by BOTH db-strings and the glossary: {:?}", - l.stem, - overlap.len(), - overlap.into_iter().take(8).collect::>() - ); - } - assert!( - checked_cells > 0, - "corpus must yield some DB cells to make the disjointness check meaningful" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/no_generic_argn.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/no_generic_argn.rs deleted file mode 100644 index e47c1ad..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/no_generic_argn.rs +++ /dev/null @@ -1,56 +0,0 @@ -use wolf_decompiler::{decompile_common_events, decompile_map, SymbolTable}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; -#[test] -fn no_generic_argn() { - let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); - let mut stack = vec![root]; - let mut hits = 0; - let mut files = 0; - let re = |t: &str| -> usize { - t.match_indices("arg") - .filter(|(i, _)| { - let b = t.as_bytes(); - let j = i + 3; - j < b.len() - && b[j].is_ascii_digit() - && t[..*i] - .chars() - .last() - .map_or(true, |c| !c.is_alphanumeric()) - && t[*i..] - .split_once('=') - .map_or(false, |(k, _)| k[3..].chars().all(|c| c.is_ascii_digit())) - }) - .count() - }; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else if p.file_name().and_then(|s| s.to_str()) == Some("CommonEvent.dat") { - if let Ok(ce) = CommonEventsFile::read(&std::fs::read(&p).unwrap_or_default()) { - files += 1; - let t = decompile_common_events(&ce, &SymbolTable::new()); - let h = re(&t); - if h > 0 { - hits += h; - eprintln!("{} argN in {:?}", h, p.file_name().unwrap()); - } - } - } else if p.extension().and_then(|s| s.to_str()) == Some("mps") { - if let Ok(m) = Map::read(&std::fs::read(&p).unwrap_or_default()) { - files += 1; - let t = decompile_map(&m, &SymbolTable::new()); - hits += re(&t); - } - } - } - } - eprintln!("scanned {files} files, total generic argN occurrences: {hits}"); - assert_eq!(hits, 0, "generic argN still present"); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/raw_census.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/raw_census.rs deleted file mode 100644 index 6356115..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/raw_census.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Census of which commands still fall back to `@raw` (the lossy-pretty set to drive to zero), -//! across both games' common events and maps. -//! cargo test -p wolf-decompiler --test raw_census -- --nocapture - -use std::collections::BTreeMap; -use std::path::PathBuf; - -use wolf_decompiler::{decompile_commands, spec}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") -} - -fn count(cmds: &[wolf_formats::command::RawCommand], hist: &mut BTreeMap) { - let text = decompile_commands(cmds); - // Re-walk: a line is @raw for the command at that position. Simplest: re-render each - // command alone is wrong (loses block ctx), so scan the produced lines and attribute - // each @raw line to its cid (the cid is the first token after `@raw `). - for line in text.lines() { - let t = line.trim_start(); - if let Some(rest) = t.strip_prefix("@raw ") { - if let Some(cid) = rest - .split_whitespace() - .next() - .and_then(|s| s.parse::().ok()) - { - *hist.entry(cid).or_default() += 1; - } - } - } -} - -#[test] -fn census() { - let mut hist: BTreeMap = BTreeMap::new(); - let mut total_cmds = 0usize; - - for game in ["Data", "GamePro_Data"] { - let ce_path = root().join(game).join("BasicData").join("CommonEvent.dat"); - if let Ok(b) = std::fs::read(&ce_path) { - let ce = CommonEventsFile::read(&b).unwrap(); - for ev in &ce.events { - total_cmds += ev.commands.len(); - count(&ev.commands, &mut hist); - } - } - let map_dir = root().join(game).join("MapData"); - if let Ok(entries) = std::fs::read_dir(&map_dir) { - for e in entries { - let p = e.unwrap().path(); - if p.extension().and_then(|s| s.to_str()) != Some("mps") { - continue; - } - let Ok(m) = Map::read(&std::fs::read(&p).unwrap()) else { - continue; - }; - for ev in &m.events { - for pg in &ev.pages { - total_cmds += pg.commands.len(); - count(&pg.commands, &mut hist); - } - } - } - } - } - - let total_raw: usize = hist.values().sum(); - eprintln!("total commands scanned: {total_cmds}"); - eprintln!("total @raw lines: {total_raw}"); - eprintln!("--- @raw by cid (cid name: count) ---"); - let mut by_count: Vec<_> = hist.into_iter().collect(); - by_count.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); - for (cid, n) in by_count { - eprintln!(" {cid:5} {:24} {n}", spec::command_name(cid)); - } - - // Every command must have a lossless pretty form. No `@raw` fallbacks in the corpus. - assert_eq!( - total_raw, 0, - "every command must render without an @raw fallback" - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/recompile.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/recompile.rs deleted file mode 100644 index 4782625..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/recompile.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Recompiler core gate: `compile(decompile(cmds)) == cmds`. -//! -//! The synthetic test proves the structural reconstruction (marker commands, suppressed `Blank` -//! slots, indent bytes, condition and operator decoding) round-trips exactly. The corpus test -//! measures how many real common events fully round-trip. - -use wolf_decompiler::{compile_commands, decompile_commands}; -use wolf_formats::command::RawCommand; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::WStr; - -fn leaf(cid: u32, ints: Vec, indent: u8, strs: &[&str]) -> RawCommand { - RawCommand { - cid, - int_args: ints, - indent, - str_args: strs - .iter() - .map(|s| WStr(format!("{s}\0").into_bytes())) - .collect(), - term: 0, - move_route: None, - v35_blob: None, - } -} - -fn bare(cid: u32, ints: Vec, indent: u8) -> RawCommand { - leaf(cid, ints, indent, &[]) -} - -#[test] -fn structural_round_trip_exact() { - // Mirrors Wolf's on-disk shape: an if (VariableCondition) with one branch plus else, - // each block body ending in a trailing Blank, plus a nested loop. - let cmds = vec![ - leaf(101, vec![], 0, &["hello"]), // Message - bare(111, vec![0x01, 1_600_000, 1, 2], 0), // if CSelf[0] == 1 (1 group, OR) - bare(401, vec![1], 0), // ChoiceCase (then) - bare(121, vec![1_100_000, 1, 0, 0], 1), // SetVariable Self[0] = 1 - bare(170, vec![], 1), // loop { - leaf(101, vec![], 2, &["in loop"]), // Message - bare(0, vec![], 2), // Blank (loop body trailing) - bare(498, vec![], 1), // LoopEnd - bare(0, vec![], 1), // Blank (then-body trailing) - bare(420, vec![0], 0), // ElseCase (arg0 = 0) - leaf(106, vec![], 1, &["dbg"]), // DebugMessage - bare(0, vec![], 1), // Blank (else-body trailing) - bare(499, vec![], 0), // BranchEnd - bare(0, vec![], 0), // Blank (top-level trailing) - ]; - - let text = decompile_commands(&cmds); - let back = compile_commands(&text).expect("compile failed"); - - if back != cmds { - eprintln!("--- decompiled ---\n{text}"); - for (i, (a, b)) in cmds.iter().zip(back.iter()).enumerate() { - if a != b { - eprintln!("differ at {i}:\n orig: {a:?}\n back: {b:?}"); - } - } - assert_eq!(back.len(), cmds.len(), "length differs"); - panic!("structural round-trip mismatch"); - } -} - -#[test] -fn editing_produces_edited_bytes() { - // Author a small program, decompile it, edit the readable text the way a modder would, then - // recompile. The round-trip gate only proves identity. This proves the parser builds the byte - // stream from the text, so changed values and inserted commands take effect, not from any - // memory of the original. - let cmds = vec![ - leaf(101, vec![], 0, &["start"]), // Message "start" - bare(121, vec![1_100_000, 1, 0, 0], 0), // SetVariable Self[0] = 1 - bare(170, vec![], 0), // loop { - leaf(101, vec![], 1, &["inside"]), // Message "inside" - bare(0, vec![], 1), // (trailing Blank) - bare(498, vec![], 0), // } LoopEnd - bare(0, vec![], 0), // (top-level trailing Blank) - ]; - - let text = decompile_commands(&cmds); - - // A modder edits the readable text: change a value, add a top-level command, and add a - // command inside a control-flow block. Source indentation is irrelevant since the parser - // reconstructs indent from nesting, so the inserted lines need no exact spacing. - let edited = text - .replace("SetVariable Self[0] = 1", "SetVariable Self[0] = 42") - .replace("loop {", "Message \"added\"\nloop {") - .replace( - "Message \"inside\"", - "Message \"inside\"\nDebugMessage \"dbg\"", - ); - - let back = compile_commands(&edited).expect("edited script compiles"); - - // 1) changed value landed - assert!( - back.iter() - .any(|c| c.cid == 121 && c.int_args == vec![1_100_000, 42, 0, 0]), - "value edit not reflected" - ); - // 2) brand-new top-level command exists at indent 0 - assert!( - back.iter().any(|c| c.cid == 101 - && c.indent == 0 - && c.str_args - .first() - .map_or(false, |s| s.as_bytes() == b"added\0")), - "inserted top-level command missing" - ); - // 3) new command inserted inside the loop body got indent 1 (reconstructed from nesting) - assert!( - back.iter().any(|c| c.cid == 106 && c.indent == 1), - "inserted in-block command missing or mis-indented" - ); - - // And the edited program is itself a stable, fully round-tripping file. Decompiling and - // recompiling it is a no-op. If editing produced something un-recompilable, this fails. - let again = compile_commands(&decompile_commands(&back)).expect("re-compile edited program"); - assert_eq!(again, back, "edited program is not a round-trip fixpoint"); -} - -#[test] -fn command_added_at_block_end_takes_the_blank_slot() { - // A "Blank" is the single trailing empty command Wolf keeps at the end of every block body, - // hidden in the readable view. The Blank's spot is the end of a block. Adding a command there - // places it before the auto-regenerated trailing Blank. The modder never sees or manages the - // Blank. The compiler always emits exactly one per block. - let src = "loop {\n Message \"a\"\n Message \"b\"\n}\n"; - let back = compile_commands(src).expect("compile"); - - let shape: Vec<(u32, u8)> = back.iter().map(|c| (c.cid, c.indent)).collect(); - assert_eq!( - shape, - vec![ - (170, 0), // loop { - (101, 1), // Message "a" - (101, 1), // Message "b" <- added at the end of the block (the Blank's spot) - (0, 1), // Blank <- exactly one, regenerated AFTER the new command - (498, 0), // } LoopEnd - (0, 0), // top-level trailing Blank - ], - "command added at block end should sit before a single regenerated trailing Blank" - ); -} - -#[test] -fn corpus_coverage_report() { - let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../../Data/BasicData/CommonEvent.dat"); - let Ok(bytes) = std::fs::read(&path) else { - eprintln!("corpus not found, skipping"); - return; - }; - let ce = CommonEventsFile::read(&bytes).unwrap(); - - let mut ok = 0usize; - let total = ce.events.len(); - let mut fail_reason: std::collections::HashMap = Default::default(); - - for ev in &ce.events { - let text = decompile_commands(&ev.commands); - match compile_commands(&text) { - Ok(back) if back == ev.commands => ok += 1, - Ok(back) => { - let key = back - .iter() - .zip(&ev.commands) - .position(|(a, b)| a != b) - .map(|i| format!("mismatch@cid{}", ev.commands[i].cid)) - .unwrap_or_else(|| format!("len {}!={}", back.len(), ev.commands.len())); - *fail_reason.entry(key).or_default() += 1; - } - Err(e) => { - let key = format!( - "err: {}", - e.to_string().chars().take(38).collect::() - ); - *fail_reason.entry(key).or_default() += 1; - } - } - } - - eprintln!("common-event command round-trip: {ok}/{total} byte-exact"); - if ok != total { - let mut reasons: Vec<_> = fail_reason.into_iter().collect(); - reasons.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); - for (k, n) in reasons.iter().take(12) { - eprintln!(" {n:5} {k}"); - } - } - assert_eq!(ok, total, "not all common events recompile byte-exact"); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/save_pro_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/save_pro_roundtrip.rs deleted file mode 100644 index 3733e1a..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/save_pro_roundtrip.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! GamePro Pro (marker-3) save codec tests. The fixture-backed cases gate on the real GamePro-Pro -//! save and the ground-truth decrypted inner. They skip gracefully when those files are absent. The -//! LZ4 and pure-synthetic round-trips always run. - -use std::collections::HashMap; - -use wolf_core::codec::lz4; -use wolf_decompiler::save::{read_title_at, update_save}; -use wolf_decompiler::save_pro::{self, decrypt_pro, encrypt_pro, is_pro_save, key16}; - -mod common; -use common::test_data; - -const PACHIMON_SAVE: &str = "pachimon/SaveData01.sav"; -const GROUND_TRUTH: &str = "pachimon/decrypted.bin"; -const CHAMBER_SAVE: &str = "chamber/SaveData01.sav"; - -fn load(rel: &str) -> Option> { - let p = test_data(rel)?; - Some(std::fs::read(&p).expect("read fixture")) -} - -/// Read the 16-bit key the encoder stamps into a produced save: `header[7] | header[4]<<8`. -fn stored_key16(save: &[u8]) -> u16 { - save[7] as u16 | ((save[4] as u16) << 8) -} - -/// (1) The hard gate: `decrypt_pro(real save)` is byte-exact with the ground-truth `decrypted.bin`. -#[test] -fn decrypt_matches_ground_truth() { - let (Some(raw), Some(truth)) = (load(PACHIMON_SAVE), load(GROUND_TRUTH)) else { - eprintln!("skip: pachimon save and/or ground-truth fixture not present"); - return; - }; - let inner = decrypt_pro(&raw).expect("decrypt the real Pro save"); - assert_eq!(inner.len(), truth.len(), "inner length matches ground truth"); - assert_eq!(inner, truth, "decrypt_pro(real save) == decrypted.bin (byte-exact)"); - assert_eq!(inner[0], 0x19, "inner starts with the 0x19 title marker"); -} - -/// (2) Detection: the real Pro save is detected as Pro, and a standard (chamber) save is not. -#[test] -fn detection_pro_vs_standard() { - if let Some(raw) = load(PACHIMON_SAVE) { - assert!(is_pro_save(&raw), "real Pro save must be detected as Pro"); - } else { - eprintln!("skip: pachimon save fixture not present"); - } - if let Some(chamber) = load(CHAMBER_SAVE) { - assert!(!is_pro_save(&chamber), "standard chamber save must not be Pro"); - } else { - eprintln!("skip: chamber save fixture not present"); - } -} - -/// (3) Round-trip on the real inner: `decrypt_pro(encrypt_pro(inner, header)) == inner`. -/// (4) key16 of the produced save validates against `header[7] | header[4]<<8`. -#[test] -fn roundtrip_real_inner_and_key16() { - let Some(raw) = load(PACHIMON_SAVE) else { - eprintln!("skip: pachimon save fixture not present"); - return; - }; - let inner = decrypt_pro(&raw).expect("decrypt"); - let enc = encrypt_pro(&inner, &raw[..20]).expect("encrypt"); - let back = decrypt_pro(&enc).expect("re-decrypt"); - assert_eq!(back, inner, "encrypt->decrypt is identity on the real inner"); - - // key16 the encoder stored must equal the fold of the inner with a4 = header[9]. - assert_eq!(stored_key16(&enc), key16(&inner, raw[9]), "produced key16 validates"); -} - -/// (5) Modified-inner round-trip: flip a byte in the inner, encrypt, then decrypt, which equals -/// the modified inner, and the stored key16 re-validates for the modified payload. -#[test] -fn modified_inner_roundtrip() { - let Some(raw) = load(PACHIMON_SAVE) else { - eprintln!("skip: pachimon save fixture not present"); - return; - }; - let mut inner = decrypt_pro(&raw).expect("decrypt"); - // Flip a byte well past the header so the title marker stays intact. - let i = inner.len() / 2; - inner[i] ^= 0xFF; - - let enc = encrypt_pro(&inner, &raw[..20]).expect("encrypt modified"); - let back = decrypt_pro(&enc).expect("re-decrypt modified"); - assert_eq!(back, inner, "modified inner round-trips"); - assert_eq!(stored_key16(&enc), key16(&inner, raw[9]), "key16 valid for modified inner"); -} - -/// (6) LZ4: compress then decompress is identity on several buffers, including the ~1MB real inner. -#[test] -fn lz4_compress_decompress_identity() { - let mut cases: Vec> = vec![ - Vec::new(), - b"a".to_vec(), - b"hello world hello world".to_vec(), - vec![0u8; 5000], - (0..4096u32).map(|i| (i % 251) as u8).collect(), - ]; - if let Some(truth) = load(GROUND_TRUTH) { - cases.push(truth); // the ~1MB real inner - } else { - eprintln!("note: ground-truth inner absent; testing smaller buffers only"); - } - for data in &cases { - let block = lz4::compress_block(data); - let back = lz4::decompress_block(&block, data.len()).expect("lz4 decompress"); - assert_eq!(&back, data, "lz4 round-trip failed for {}-byte buffer", data.len()); - } -} - -/// (7) End-to-end via `update_save` on the real Pro save: setting `--title "X"` updates it rather -/// than skipping, and re-decrypting yields an inner whose title reads "X" with the 0x19 marker -/// intact. -#[test] -fn update_save_pro_end_to_end() { - let Some(raw) = load(PACHIMON_SAVE) else { - eprintln!("skip: pachimon save fixture not present"); - return; - }; - let (out, stats) = update_save(&raw, Some("X"), &HashMap::new()) - .expect("Pro save is now supported by update_save"); - assert!(stats.title_changed, "title was changed"); - assert_eq!(stats.encoding, "utf8"); - - let inner = decrypt_pro(&out).expect("re-decrypt the updated Pro save"); - assert_eq!(inner[0], save_pro::INNER_MARKER, "0x19 marker intact in the updated inner"); - assert_eq!(read_title_at(&inner, 0, true).as_deref(), Some("X"), "title reads back as X"); - // The stored key16 still validates after the edit. - assert_eq!(stored_key16(&out), key16(&inner, raw[9])); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/save_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/save_roundtrip.rs deleted file mode 100644 index 3109b9d..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/save_roundtrip.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! `.sav` auto-update tests. These exercise the real outer codec and the `update_save` pipeline -//! against actual game saves where present (skipping gracefully when the fixtures are absent), plus -//! a synthetic codec round-trip that always runs. - -use std::collections::HashMap; - -use wolf_core::codec::wolfsave; -use wolf_decompiler::save::{read_title, update_save}; - -mod common; -use common::test_data; - -const CHAMBER_SAVE: &str = "chamber/SaveData01.sav"; -const PACHIMON_SAVE: &str = "pachimon/SaveData01.sav"; - -const START_OFFSET: usize = 0x14; -const VALID_MARKER: u8 = 0x19; - -fn load(rel: &str) -> Option> { - let p = test_data(rel)?; - Some(std::fs::read(&p).expect("read save fixture")) -} - -/// (a) The codec round-trips a real save byte-for-byte. If the stored checksum already matches the -/// body (the normal case), `encrypt(decrypt(raw)) == raw` exactly. If it does not, we still require -/// the structural invariant (`[0x14] == 0x19`) and that a second decrypt of the re-encrypted bytes -/// reproduces the same plaintext (the checksum byte is the only possible difference). -#[test] -fn codec_roundtrips_real_chamber_save() { - let Some(raw) = load(CHAMBER_SAVE) else { - eprintln!("skip: chamber save fixture not present"); - return; - }; - let plain = wolfsave::decrypt(&raw); - assert!(plain.len() > START_OFFSET); - assert_eq!(plain[START_OFFSET], VALID_MARKER, "chamber save must be a valid 0x19 save"); - - let calc = plain[START_OFFSET..] - .iter() - .fold(0u8, |a, &b| a.wrapping_add(b)); - let reenc = wolfsave::encrypt(&plain); - if calc == plain[2] { - assert_eq!(reenc, raw, "stored checksum matches body -> encrypt(decrypt(raw)) is byte-exact"); - } else { - // Checksum byte was stale on disk. Re-encrypt normalises it. Plaintext must still match. - assert_eq!(wolfsave::decrypt(&reenc), plain, "re-decrypt reproduces the same plaintext"); - } -} - -/// (b) `update_save` with an empty map and no new title re-encrypts to a buffer that decrypts back -/// to the original plaintext. -#[test] -fn update_save_noop_preserves_plaintext() { - let Some(raw) = load(CHAMBER_SAVE) else { - eprintln!("skip: chamber save fixture not present"); - return; - }; - let plain = wolfsave::decrypt(&raw); - let (out, stats) = update_save(&raw, None, &HashMap::new()).expect("supported save"); - assert!(!stats.title_changed); - assert_eq!(stats.strings_replaced, 0); - assert_eq!(wolfsave::decrypt(&out), plain, "no-op update preserves the plaintext"); -} - -/// (c) Setting a new title, then re-decrypting, reads the new value back at 0x15, and the 0x14 -/// marker is still intact. -#[test] -fn update_save_sets_new_title() { - let Some(raw) = load(CHAMBER_SAVE) else { - eprintln!("skip: chamber save fixture not present"); - return; - }; - let new = "Translated Title (Test)"; - let (out, stats) = update_save(&raw, Some(new), &HashMap::new()).expect("supported save"); - assert!(stats.title_changed); - let plain = wolfsave::decrypt(&out); - assert_eq!(plain[START_OFFSET], VALID_MARKER, "marker survives the title length shift"); - assert_eq!(read_title(&plain).as_deref(), Some(new)); -} - -/// (d) Synthetic codec unit test (always runs, no fixtures). Build a buffer with a correct -/// checksum, encrypt then decrypt, and confirm identity plus that the body actually changed. -#[test] -fn codec_roundtrips_synthetic() { - let mut plain: Vec = (0u8..=255).cycle().take(START_OFFSET + 301).collect(); - plain[0] = 0x11; - plain[3] = 0x22; - plain[9] = 0x33; - wolfsave::fix_checksum(&mut plain); - let enc = wolfsave::encrypt(&plain); - assert_eq!(wolfsave::decrypt(&enc), plain); - assert_ne!(&enc[START_OFFSET..], &plain[START_OFFSET..], "body is actually enciphered"); - assert_eq!(&enc[..START_OFFSET], &plain[..START_OFFSET], "head carried verbatim"); -} - -/// The GamePro Pro (marker-3) save does not present as a standard save after the outer decrypt -/// (`[0x14] != 0x19`), but is handled by the Pro path. `update_save` accepts it and a re-decrypt -/// of the result reads the new title back. The Pro codec itself is exercised in -/// `save_pro_roundtrip.rs`. -#[test] -fn pachimon_save_handled_by_pro_path() { - let Some(raw) = load(PACHIMON_SAVE) else { - eprintln!("skip: pachimon save fixture not present"); - return; - }; - // It is not a standard save: the standard decrypt does not yield the 0x19 marker at 0x14. - let plain = wolfsave::decrypt(&raw); - assert_ne!(plain[START_OFFSET], VALID_MARKER, "Pro save is not a standard 0x19 save"); - - // But update_save now succeeds via the Pro path and writes the requested title. - let (out, stats) = update_save(&raw, Some("X"), &HashMap::new()) - .expect("pro save must now be supported"); - assert!(stats.title_changed); - assert_eq!(stats.encoding, "utf8"); - let inner = wolf_decompiler::save_pro::decrypt_pro(&out).expect("re-decrypt produced inner"); - assert_eq!(inner[0], VALID_MARKER, "inner 0x19 marker intact"); - assert_eq!( - wolf_decompiler::save::read_title_at(&inner, 0, true).as_deref(), - Some("X") - ); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/strings_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/strings_roundtrip.rs deleted file mode 100644 index 276122e..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/strings_roundtrip.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! Player-text extract/inject round-trip. A no-op inject is byte-identical to the base, and -//! editing one line's translation changes only that string while others stay byte-exact. Covers -//! both the command/dialogue stream (maps) and database content (item, skill, term text). -use wolf_decompiler::{ - db_strings_to_json, extract_db_strings, extract_map, inject_db_strings, inject_map, - scenes_to_json, InjectOptions, -}; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -#[test] -fn db_strings_noop_byte_identical_then_translate() { - let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../Data/BasicData"); - let (Ok(p), Ok(d)) = ( - std::fs::read(base.join("DataBase.project")), - std::fs::read(base.join("DataBase.dat")), - ) else { - eprintln!("no corpus DataBase, skipping"); - return; - }; - let db = Database::read(&p, &d).expect("parse db"); - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let groups = extract_db_strings(&db, &glossary); - assert!(!groups.is_empty(), "expected some player-facing DB strings"); - let json = db_strings_to_json("DataBase.project", &groups); - - // No-op inject must be byte-identical. - let mut db2 = Database::read(&p, &d).unwrap(); - let st = inject_db_strings(&json, &mut db2, &InjectOptions::default()).expect("inject"); - assert_eq!(st.applied, 0); - assert_eq!(st.drifted, 0); - assert!( - db2.write() == (p.clone(), d.clone()), - "no-op DB-strings inject must be byte-identical" - ); - - // Translate one cell and confirm it persists. The sentinel reads as real text (a space, no - // bare-identifier shape) so the re-extraction's player-text filter keeps it. - let g = &groups[0]; - let ln = &g.lines[0]; - let edit = format!( - r#"{{ "groups": [ {{ "type": {}, "lines": [ {{ "row": {}, "field": {}, "source": {}, "text": "WOLFDAWN DB TR" }} ] }} ] }}"#, - g.type_id, - ln.row, - ln.field, - serde_json::Value::String(ln.source.clone()), - ); - let mut db3 = Database::read(&p, &d).unwrap(); - let st = inject_db_strings( - &edit, - &mut db3, - &InjectOptions { - allow_code_drift: true, - ..Default::default() - }, - ) - .expect("inject"); - assert_eq!(st.applied, 1, "one DB cell should translate"); - let (p3, d3) = db3.write(); - let db4 = Database::read(&p3, &d3).unwrap(); - let groups4 = extract_db_strings(&db4, &glossary); - assert!( - groups4 - .iter() - .any(|g| g.lines.iter().any(|l| l.source == "WOLFDAWN DB TR")), - "translated DB cell must read the new text" - ); -} - -/// Find a corpus map that actually has extractable player text. -fn find_map_with_text() -> Option<(std::path::PathBuf, Vec, Map)> { - let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); - let mut stack = vec![root]; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else if p.extension().and_then(|s| s.to_str()) == Some("mps") { - let bytes = std::fs::read(&p).unwrap_or_default(); - if let Ok(m) = Map::read(&bytes) { - if extract_map(&m).iter().any(|s| !s.lines.is_empty()) { - return Some((p, bytes, m)); - } - } - } - } - } - None -} - -#[test] -fn noop_inject_is_byte_identical() { - let Some((_p, bytes, m)) = find_map_with_text() else { - eprintln!("no corpus map with player text, skipping"); - return; - }; - let json = scenes_to_json("m.mps", "map", &extract_map(&m)); - let mut m2 = Map::read(&bytes).unwrap(); - let st = inject_map(&json, &mut m2, &InjectOptions::default()).expect("inject"); - assert_eq!(st.applied, 0, "no-op inject must apply nothing"); - assert_eq!(st.drifted, 0, "no-op inject must not drift"); - assert!(m2.write() == bytes, "no-op inject must be byte-identical"); -} - -#[test] -fn translate_one_line_changes_only_that_string() { - let Some((_p, bytes, m)) = find_map_with_text() else { - return; - }; - let scenes = extract_map(&m); - // Locate the first scene with a line and record its locator and original. - let sc = scenes.iter().find(|s| !s.lines.is_empty()).unwrap(); - let ln = &sc.lines[0]; - let (event, page, cmd, str_idx, source) = ( - sc.event, - sc.page.unwrap(), - ln.cmd, - ln.str_idx, - ln.source.clone(), - ); - - // Build a minimal inject doc translating just that line. The sentinel reads as real text (a - // space, no bare-identifier shape) so the re-extraction's player-text filter keeps it. - let edit = format!( - r#"{{ "scenes": [ {{ "event": {event}, "page": {page}, "lines": [ {{ "cmd": {cmd}, "str": {str_idx}, "source": {src}, "text": "WOLFDAWN TR" }} ] }} ] }}"#, - src = serde_json::Value::String(source.clone()), - ); - - let mut m2 = Map::read(&bytes).unwrap(); - let st = inject_map( - &edit, - &mut m2, - &InjectOptions { - allow_code_drift: true, - ..Default::default() - }, - ) - .expect("inject"); - assert_eq!(st.applied, 1, "exactly one line should apply"); - - // Re-extract and confirm the translated line now reads the new text and others are intact. - let scenes2 = extract_map(&m2); - let sc2 = scenes2 - .iter() - .find(|s| s.event == event && s.page == Some(page)) - .unwrap(); - let ln2 = sc2 - .lines - .iter() - .find(|l| l.cmd == cmd && l.str_idx == str_idx) - .unwrap(); - assert_eq!( - ln2.source, "WOLFDAWN TR", - "translated line must read the new text" - ); -} - -/// Safety guard: a translation that drops an inline control code present in the source is -/// rejected. It is counted in `code_mismatch` and the line is left untouched, so a translator -/// cannot silently break a `\cself`/`\s`/`\f` reference. `--allow-code-drift` overrides. This is -/// the core "never break the game" guarantee. -#[test] -fn control_code_drop_is_blocked_by_default() { - let Some((_p, bytes, m)) = find_map_with_text() else { - return; - }; - // Find a line whose source carries an inline `\code` (so dropping it is detectable). - let scenes = extract_map(&m); - let Some((sc, ln)) = scenes - .iter() - .flat_map(|s| s.lines.iter().map(move |l| (s, l))) - .find(|(_, l)| { - l.source.contains("\\cself[") || l.source.contains("\\s[") || l.source.contains("\\c[") - }) - else { - eprintln!("no corpus line with an inline code, skipping"); - return; - }; - let edit = format!( - r#"{{ "scenes": [ {{ "event": {}, "page": {}, "lines": [ {{ "cmd": {}, "str": {}, "source": {}, "text": "code dropped entirely" }} ] }} ] }}"#, - sc.event, - sc.page.unwrap(), - ln.cmd, - ln.str_idx, - serde_json::Value::String(ln.source.clone()), - ); - - // Default: blocked, counted, base untouched (byte-identical). - let mut m2 = Map::read(&bytes).unwrap(); - let st = inject_map(&edit, &mut m2, &InjectOptions::default()).expect("inject"); - assert_eq!(st.applied, 0, "code-dropping translation must not apply"); - assert_eq!(st.code_mismatch, 1, "the dropped code must be counted"); - assert!( - m2.write() == bytes, - "blocked line must leave the file byte-identical" - ); - - // With --allow-code-drift: applied. - let mut m3 = Map::read(&bytes).unwrap(); - let st = inject_map( - &edit, - &mut m3, - &InjectOptions { - allow_code_drift: true, - ..Default::default() - }, - ) - .expect("inject"); - assert_eq!(st.applied, 1, "allow_code_drift must apply the edit"); - assert_eq!(st.code_mismatch, 0); -} diff --git a/util/wolfdawn-master/crates/wolf-decompiler/tests/txt_events_roundtrip.rs b/util/wolfdawn-master/crates/wolf-decompiler/tests/txt_events_roundtrip.rs deleted file mode 100644 index 468e333..0000000 --- a/util/wolfdawn-master/crates/wolf-decompiler/tests/txt_events_roundtrip.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! External event-text `.txt` round-trip. A no-op inject reproduces the base byte-for-byte, an -//! edit changes only its line while the surrounding `/` command lines stay intact, and extraction -//! drops every command/comment/blank line while keeping dialogue and speaker lines. -//! -//! The real-data cases use the `WOLFDAWN_TEST_DATA` fixture (`g210/Evtext/hev_a000h.txt`) and skip -//! gracefully when it is absent. The synthetic cases always run. - -use wolf_decompiler::{extract_txt_events, inject_txt_events}; - -mod common; -use common::test_data; - -/// Parse the `source` strings out of an extracted document, in order. -fn sources(json: &str) -> Vec { - let v: serde_json::Value = serde_json::from_str(json).unwrap(); - v["lines"] - .as_array() - .unwrap() - .iter() - .map(|l| l["source"].as_str().unwrap().to_owned()) - .collect() -} - -/// A synthetic Shift-JIS event file with CRLF endings, a `//` comment, a `/` command, a speaker -/// line, dialogue, blank-line spacing, and no trailing newline. Mirrors the real format. -fn synthetic_sjis() -> Vec { - let text = "//おねだり_コメント\r\n\ - /evcg,a0,b0,c0\r\n\ - /胸+1\r\n\ - アイリス:\r\n\ - こんにちは、世界\r\n\ - \r\n\ - /b\r\n\ - 最後の行に改行なし"; - encoding_rs::SHIFT_JIS.encode(text).0.into_owned() -} - -#[test] -fn synthetic_noop_is_byte_exact() { - let bytes = synthetic_sjis(); - let json = extract_txt_events(&bytes); - let out = inject_txt_events(&json, &bytes).expect("inject"); - assert_eq!(out, bytes, "no-op inject must reproduce the base byte-for-byte"); -} - -#[test] -fn synthetic_extraction_excludes_commands_and_blanks() { - let bytes = synthetic_sjis(); - let json = extract_txt_events(&bytes); - let got = sources(&json); - assert_eq!( - got, - vec![ - "アイリス:".to_string(), - "こんにちは、世界".to_string(), - "最後の行に改行なし".to_string(), - ], - "only the non-command, non-blank lines are extracted" - ); - // Confirm no command/comment line leaked into the document. - for forbidden in ["/evcg", "/胸+1", "/b", "//おねだり"] { - assert!( - !json.contains(forbidden), - "command/comment {forbidden} must not be extracted" - ); - } -} - -#[test] -fn synthetic_edit_round_trip_keeps_command_intact() { - let bytes = synthetic_sjis(); - let json = extract_txt_events(&bytes); - // Edit the dialogue line, leaving its neighbour command `/evcg` and `/b` untouched. - let edited = json.replacen( - r#""source": "こんにちは、世界", "text": "こんにちは、世界""#, - r#""source": "こんにちは、世界", "text": "やあ、世界""#, - 1, - ); - assert_ne!(edited, json, "edit must have matched a line"); - let out = inject_txt_events(&edited, &bytes).expect("inject"); - // Re-extract and confirm the new text landed. - let re = extract_txt_events(&out); - assert!( - sources(&re).contains(&"やあ、世界".to_string()), - "edited text must be present after re-extraction" - ); - // The command lines next to it must be byte-intact. - let (decoded, _, _) = encoding_rs::SHIFT_JIS.decode(&out); - assert!(decoded.contains("/evcg,a0,b0,c0"), "the /evcg command must survive"); - assert!(decoded.contains("/b"), "the /b page break must survive"); - assert!(!decoded.contains("こんにちは、世界"), "old text should be gone"); -} - -#[test] -fn synthetic_unrepresentable_and_newline_are_skipped() { - // A translation with a char SJIS cannot encode, and one with an embedded newline, are both - // skipped, leaving the file byte-exact. - let text = "せりふいち\r\nせりふに\r\n"; - let bytes = encoding_rs::SHIFT_JIS.encode(text).0.into_owned(); - let json = extract_txt_events(&bytes); - // Line 0 -> an emoji (not SJIS), line 1 -> a two-line value. - let edited = json - .replacen( - r#""source": "せりふいち", "text": "せりふいち""#, - r#""source": "せりふいち", "text": "🎮""#, - 1, - ) - .replacen( - r#""source": "せりふに", "text": "せりふに""#, - r#""source": "せりふに", "text": "a\nb""#, - 1, - ); - let out = inject_txt_events(&edited, &bytes).expect("inject"); - assert_eq!(out, bytes, "both guarded edits are skipped, base preserved"); -} - -#[test] -fn real_file_noop_is_byte_exact_and_keeps_dialogue() { - let Some(path) = test_data("g210/Evtext/hev_a000h.txt") else { - eprintln!("no g210/Evtext fixture, skipping real-file txt test"); - return; - }; - let bytes = std::fs::read(&path).expect("read fixture"); - - // No-op inject is byte-for-byte identical. - let json = extract_txt_events(&bytes); - let out = inject_txt_events(&json, &bytes).expect("inject"); - assert_eq!(out, bytes, "real-file no-op inject must be byte-exact"); - - // Extraction excludes every `/`-prefixed and blank line, and keeps the speaker line. - let srcs = sources(&json); - assert!(!srcs.is_empty(), "the real file has dialogue to extract"); - assert!( - srcs.iter().all(|s| !s.is_empty() && !s.starts_with('/')), - "no command/comment/blank line should be extracted" - ); - assert!( - srcs.iter().any(|s| s.starts_with("アイリス")), - "the アイリス: speaker line should be extracted" - ); - - // Edit one dialogue line and confirm it lands while a command line stays intact. - let target = srcs.iter().find(|s| !s.starts_with("アイリス")).unwrap().clone(); - let needle = format!("\"source\": {0}, \"text\": {0}", serde_json::Value::String(target.clone())); - let replacement = format!( - "\"source\": {}, \"text\": {}", - serde_json::Value::String(target.clone()), - serde_json::Value::String("WOLFDAWN_TXT_TR".to_string()) - ); - let edited = json.replacen(&needle, &replacement, 1); - assert_ne!(edited, json, "edit must have matched a line"); - let out2 = inject_txt_events(&edited, &bytes).expect("inject edit"); - let re = extract_txt_events(&out2); - assert!( - sources(&re).contains(&"WOLFDAWN_TXT_TR".to_string()), - "edited text must be present after re-extraction" - ); - let (decoded, _, _) = encoding_rs::SHIFT_JIS.decode(&out2); - assert!(decoded.contains("/evcg"), "an /evcg command line must remain intact"); -} diff --git a/util/wolfdawn-master/crates/wolf-formats/Cargo.toml b/util/wolfdawn-master/crates/wolf-formats/Cargo.toml deleted file mode 100644 index 4ddf116..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "wolf-formats" -edition.workspace = true -version.workspace = true -license.workspace = true -description = "Byte-faithful typed read/write of Wolf RPG data files (maps, databases, common events). Round-trip contract lives here." - -[dependencies] -wolf-core = { path = "../wolf-core" } - -[dev-dependencies] -# For the end-to-end gate: unpack Data.wolf and parse/round-trip its inner files. -wolf-archive = { path = "../wolf-archive" } diff --git a/util/wolfdawn-master/crates/wolf-formats/src/command.rs b/util/wolfdawn-master/crates/wolf-formats/src/command.rs deleted file mode 100644 index 0375eb8..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/src/command.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Event command and move-route encoding, the core unit a decompiler operates on. -//! -//! Read order: `argCount = byte-1`, `cid` u32, `argCount` ints, `indent` byte, -//! `strCount` byte, `strCount` strings, `terminator` byte. Terminator `0x01` (or -//! cid==Move) is followed by a move-route block. Wolf Pro v3.5 appends a per-command -//! length-prefixed blob. - -use crate::Ctx; -use wolf_core::{Error, Reader, Result, WStr, Writer}; - -/// On-disk command id for `Move` (201). The only cid whose `0x00`-terminated form -/// still carries a route block. -pub const CID_MOVE: u32 = 201; - -/// A single event command, mirroring the on-disk bytes exactly. This is the round-trip -/// authority that the higher decompiler IR is derived from and lowered back to. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RawCommand { - pub cid: u32, - pub int_args: Vec, - pub indent: u8, - pub str_args: Vec, - /// Terminator byte as read (`0x00` normal, `0x01` move). Stored for faithfulness. - pub term: u8, - pub move_route: Option, - /// Wolf Pro v3.5 trailing blob (length-prefixed by a single byte), if present. - pub v35_blob: Option>, -} - -impl RawCommand { - pub fn read(r: &mut Reader, ctx: &Ctx) -> Result { - let arg_count_byte = r.read_u8()?; - let n = arg_count_byte - .checked_sub(1) - .ok_or_else(|| Error::invalid("command int-arg count byte was 0 (expected >= 1)"))? - as usize; - - let cid = r.read_u32()?; - let mut int_args = Vec::with_capacity(n); - for _ in 0..n { - int_args.push(r.read_u32()?); - } - - let indent = r.read_u8()?; - let str_count = r.read_u8()? as usize; - let mut str_args = Vec::with_capacity(str_count); - for _ in 0..str_count { - str_args.push(r.read_string()?); - } - - let term = r.read_u8()?; - let move_route = match term { - 0x01 => Some(MoveRoute::read(r)?), - 0x00 => { - if cid == CID_MOVE { - Some(MoveRoute::read(r)?) - } else { - None - } - } - other => { - return Err(Error::invalid(format!( - "unexpected command terminator 0x{other:02x} (cid {cid})" - ))) - } - }; - - let v35_blob = if ctx.v35 { - let size = r.read_u8()? as usize; - Some(r.read_bytes(size)?) - } else { - None - }; - - Ok(RawCommand { - cid, - int_args, - indent, - str_args, - term, - move_route, - v35_blob, - }) - } - - pub fn write(&self, w: &mut Writer, ctx: &Ctx) { - w.write_u8((self.int_args.len() + 1) as u8); - w.write_u32(self.cid); - for &a in &self.int_args { - w.write_u32(a); - } - w.write_u8(self.indent); - w.write_u8(self.str_args.len() as u8); - for s in &self.str_args { - w.write_string(s); - } - w.write_u8(self.term); - if let Some(mr) = &self.move_route { - mr.write(w); - } - if ctx.v35 { - let blob = self.v35_blob.as_deref().unwrap_or(&[]); - w.write_u8(blob.len() as u8); - w.write_bytes(blob); - } - } -} - -/// The move-route block attached to a `Move` command: 5 opaque bytes, a flags byte, -/// and a length-prefixed list of [`RouteCommand`]s. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MoveRoute { - pub unknown: [u8; 5], - pub flags: u8, - pub commands: Vec, -} - -impl MoveRoute { - pub fn read(r: &mut Reader) -> Result { - let mut unknown = [0u8; 5]; - for b in &mut unknown { - *b = r.read_u8()?; - } - let flags = r.read_u8()?; - let count = r.read_u32()? as usize; - let mut commands = Vec::with_capacity(count); - for _ in 0..count { - commands.push(RouteCommand::read(r)?); - } - Ok(MoveRoute { - unknown, - flags, - commands, - }) - } - - pub fn write(&self, w: &mut Writer) { - w.write_bytes(&self.unknown); - w.write_u8(self.flags); - w.write_u32(self.commands.len() as u32); - for c in &self.commands { - c.write(w); - } - } -} - -/// A single move-route sub-command: `id` byte, `argCount` byte, ints, then a fixed -/// `{01,00}` terminator. Used both inside [`MoveRoute`] and directly in a page's route list. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RouteCommand { - pub id: u8, - pub args: Vec, - pub terminator: [u8; 2], -} - -impl RouteCommand { - pub fn read(r: &mut Reader) -> Result { - let id = r.read_u8()?; - let argc = r.read_u8()? as usize; - let mut args = Vec::with_capacity(argc); - for _ in 0..argc { - args.push(r.read_u32()?); - } - let terminator = [r.read_u8()?, r.read_u8()?]; - if terminator != [0x01, 0x00] { - return Err(Error::invalid(format!( - "route command terminator was {:02x?}, expected [01, 00]", - terminator - ))); - } - Ok(RouteCommand { - id, - args, - terminator, - }) - } - - pub fn write(&self, w: &mut Writer) { - w.write_u8(self.id); - w.write_u8(self.args.len() as u8); - for &a in &self.args { - w.write_u32(a); - } - w.write_bytes(&self.terminator); - } -} - -/// Read a length-prefixed (`u32` count) list of [`RouteCommand`]s. This is the form used -/// directly in a map page's route field (no 5-byte/flags header). -pub fn read_route_list(r: &mut Reader) -> Result> { - let count = r.read_u32()? as usize; - let mut out = Vec::with_capacity(count); - for _ in 0..count { - out.push(RouteCommand::read(r)?); - } - Ok(out) -} - -pub fn write_route_list(w: &mut Writer, list: &[RouteCommand]) { - w.write_u32(list.len() as u32); - for c in list { - c.write(w); - } -} diff --git a/util/wolfdawn-master/crates/wolf-formats/src/common_event.rs b/util/wolfdawn-master/crates/wolf-formats/src/common_event.rs deleted file mode 100644 index 59bffb6..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/src/common_event.rs +++ /dev/null @@ -1,308 +0,0 @@ -//! CommonEvent file (`CommonEvent.dat`): a flat list of shared event programs. -//! Each common event holds a command list (same encoding as map pages) plus a large -//! tail of editor metadata kept opaque for now. -//! -//! Layout on disk: -//! `[crypt-indicator u8=0][9-byte magic][version u8][eventCount u32][events][terminator u8]`. -//! The leading indicator and the v3.5 LZ4 body are crypto-layer concerns. This reader -//! handles the plaintext case (indicator 0, version != 0x93/0xCC) and errors otherwise. - -use crate::command::RawCommand; -use crate::Ctx; -use wolf_core::codec::lz4; -use wolf_core::{Error, Reader, Result, WStr, Writer}; - -/// `{0x57,00,00,0x4F,0x4C,(00|'U'),0x46,0x43,00}`: "WOLFC" with a UTF-8 toggle at idx 5. -const MAGIC_LEN: usize = 9; -const UTF8_MARKER_INDEX: usize = 5; -const EVENT_INDICATOR: u8 = 0x8E; -const DATA_INDICATOR_8F: u8 = 0x8F; -const DATA_INDICATOR_91: u8 = 0x91; -const DATA_INDICATOR_92: u8 = 0x92; -const UNKNOWN8_COUNT: usize = 100; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CommonEventsFile { - /// Leading crypt-indicator byte (0 = plaintext). - pub indicator: u8, - pub magic: [u8; MAGIC_LEN], - pub version: u8, - pub events: Vec, - pub terminator: u8, - pub utf8: bool, - /// True when version is `0x93`/`0xCC` (Wolf Pro v3.5). The body after the version byte - /// is LZ4-compressed and every command carries a trailing blob. - pub v35: bool, - /// For v3.5 files: the original framed LZ4 body `[decSize][encSize][block]`, kept so an - /// unmodified file re-emits byte-exact (LZ4 recompression isn't byte-stable). - pub packed: Option>, -} - -impl CommonEventsFile { - pub fn read(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - - let indicator = r.read_u8()?; - if indicator != 0 { - return Err(Error::invalid( - "CommonEvent.dat is encrypted (indicator != 0); decrypt with wolf-archive first", - )); - } - - let magic_vec = r.read_bytes(MAGIC_LEN)?; - if magic_vec[0] != 0x57 || magic_vec[6] != 0x46 || magic_vec[7] != 0x43 { - return Err(Error::invalid("not a CommonEvent.dat (magic mismatch)")); - } - let mut magic = [0u8; MAGIC_LEN]; - magic.copy_from_slice(&magic_vec); - let utf8 = magic[UTF8_MARKER_INDEX] == b'U'; - - let version = r.read_u8()?; - let v35 = version == 0x93 || version == 0xCC; - let ctx = Ctx { utf8, v35 }; - - // For v3.5 the body after the version byte is an LZ4 block. Decompress it and parse - // the events from the decompressed bytes. Plain files just continue inline. - let rem = r.remaining(); - let (packed, body): (Option>, Vec) = if v35 { - let framed = r.read_bytes(rem)?; - let body = lz4::unpack(&framed)?; - (Some(framed), body) - } else { - (None, r.read_bytes(rem)?) - }; - let mut r = Reader::new(&body); - - let event_count = r.read_u32()? as usize; - let mut events = Vec::with_capacity(event_count); - for _ in 0..event_count { - events.push(CommonEvent::read(&mut r, &ctx)?); - } - - let terminator = r.read_u8()?; - if terminator < 0x89 { - return Err(Error::invalid(format!( - "CommonEvent terminator 0x{terminator:02x} < 0x89" - ))); - } - if !r.is_eof() { - return Err(Error::invalid("CommonEvent.dat has trailing data")); - } - - Ok(CommonEventsFile { - indicator, - magic, - version, - events, - terminator, - utf8, - v35, - packed, - }) - } - - pub fn write(&self) -> Vec { - let ctx = Ctx { - utf8: self.utf8, - v35: self.v35, - }; - let mut w = Writer::new(); - w.write_u8(self.indicator); - w.write_bytes(&self.magic); - w.write_u8(self.version); - - // The compressible body: event count + events + terminator. - let mut body = Writer::new(); - body.write_u32(self.events.len() as u32); - for ev in &self.events { - ev.write(&mut body, &ctx); - } - body.write_u8(self.terminator); - let body = body.into_bytes(); - - if self.v35 { - // Re-emit the original packed bytes verbatim when the body is unchanged - // (byte-exact). Otherwise compress fresh (valid LZ4, content-exact). - match &self.packed { - Some(orig) if lz4::unpack(orig).map(|b| b == body).unwrap_or(false) => { - w.write_bytes(orig); - } - _ => w.write_bytes(&lz4::pack(&body)), - } - } else { - w.write_bytes(&body); - } - w.into_bytes() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CommonEvent { - pub int_id: u32, - pub unknown1: u32, - pub unknown2: [u8; 7], - pub name: WStr, - pub commands: Vec, - pub unknown11: WStr, - pub description: WStr, - pub unknown3: Vec, - pub unknown4: Vec, - pub unknown5: Vec>, - pub unknown6: Vec>, - pub unknown7: [u8; 0x1D], - /// Exactly 100 strings (slot/argument names), no count prefix on disk. - pub unknown8: Vec, - pub unknown9: WStr, - /// Optional `0x92` tail: `(unknown10, unknown12)`. - pub unknown10: Option<(WStr, u32)>, -} - -impl CommonEvent { - fn read(r: &mut Reader, ctx: &Ctx) -> Result { - let ind = r.read_u8()?; - if ind != EVENT_INDICATOR { - return Err(Error::invalid(format!( - "CommonEvent header indicator 0x{ind:02x} != 0x8E" - ))); - } - - let int_id = r.read_u32()?; - let unknown1 = r.read_u32()?; - let unknown2 = read_fixed::<7>(r)?; - let name = r.read_string()?; - - let command_count = r.read_u32()? as usize; - let mut commands = Vec::with_capacity(command_count); - for _ in 0..command_count { - commands.push(RawCommand::read(r, ctx)?); - } - - let unknown11 = r.read_string()?; - let description = r.read_string()?; - - expect(r, DATA_INDICATOR_8F, "0x8F")?; - - let unknown3 = r.read_string_array()?; - - let unknown4 = r.read_byte_array()?; - - let n5 = r.read_u32()? as usize; - let mut unknown5 = Vec::with_capacity(n5); - for _ in 0..n5 { - unknown5.push(r.read_string_array()?); - } - - let n6 = r.read_u32()? as usize; - let mut unknown6 = Vec::with_capacity(n6); - for _ in 0..n6 { - unknown6.push(r.read_int_array()?); - } - - let unknown7 = read_fixed::<0x1D>(r)?; - - let mut unknown8 = Vec::with_capacity(UNKNOWN8_COUNT); - for _ in 0..UNKNOWN8_COUNT { - unknown8.push(r.read_string()?); - } - - expect(r, DATA_INDICATOR_91, "0x91")?; - let unknown9 = r.read_string()?; - - let next = r.read_u8()?; - let unknown10 = match next { - DATA_INDICATOR_91 => None, - DATA_INDICATOR_92 => { - let s = r.read_string()?; - let n = r.read_u32()?; - expect(r, DATA_INDICATOR_92, "0x92")?; - Some((s, n)) - } - other => { - return Err(Error::invalid(format!( - "CommonEvent tail indicator 0x{other:02x} not 0x91/0x92" - ))) - } - }; - - Ok(CommonEvent { - int_id, - unknown1, - unknown2, - name, - commands, - unknown11, - description, - unknown3, - unknown4, - unknown5, - unknown6, - unknown7, - unknown8, - unknown9, - unknown10, - }) - } - - fn write(&self, w: &mut Writer, ctx: &Ctx) { - w.write_u8(EVENT_INDICATOR); - w.write_u32(self.int_id); - w.write_u32(self.unknown1); - w.write_bytes(&self.unknown2); - w.write_string(&self.name); - w.write_u32(self.commands.len() as u32); - for c in &self.commands { - c.write(w, ctx); - } - w.write_string(&self.unknown11); - w.write_string(&self.description); - w.write_u8(DATA_INDICATOR_8F); - - w.write_string_array(&self.unknown3); - w.write_byte_array(&self.unknown4); - - w.write_u32(self.unknown5.len() as u32); - for strs in &self.unknown5 { - w.write_string_array(strs); - } - - w.write_u32(self.unknown6.len() as u32); - for ints in &self.unknown6 { - w.write_int_array(ints); - } - - w.write_bytes(&self.unknown7); - for s in &self.unknown8 { - w.write_string(s); - } - - w.write_u8(DATA_INDICATOR_91); - w.write_string(&self.unknown9); - - match &self.unknown10 { - Some((s, n)) => { - w.write_u8(DATA_INDICATOR_92); - w.write_string(s); - w.write_u32(*n); - w.write_u8(DATA_INDICATOR_92); - } - None => w.write_u8(DATA_INDICATOR_91), - } - } -} - -fn read_fixed(r: &mut Reader) -> Result<[u8; N]> { - let v = r.read_bytes(N)?; - let mut out = [0u8; N]; - out.copy_from_slice(&v); - Ok(out) -} - -fn expect(r: &mut Reader, byte: u8, label: &str) -> Result<()> { - let got = r.read_u8()?; - if got != byte { - return Err(Error::invalid(format!( - "CommonEvent expected indicator {label}, got 0x{got:02x}" - ))); - } - Ok(()) -} diff --git a/util/wolfdawn-master/crates/wolf-formats/src/database.rs b/util/wolfdawn-master/crates/wolf-formats/src/database.rs deleted file mode 100644 index a3e889f..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/src/database.rs +++ /dev/null @@ -1,447 +0,0 @@ -//! Database: the paired `.project` (schema: type/field/data names + field UI types) -//! and `.dat` (field index-info + actual int/string cell values) format. Used for the -//! User Database, System Database (`SysDatabase`) and Changeable Database (`CDatabase`). -//! -//! Handles the plaintext case (dat indicator 0, version != 0xC4, unencrypted project). -//! Pro/LZ4 variants error pending wolf-archive. - -use crate::Ctx; -use wolf_core::codec::lz4; -use wolf_core::{Error, Reader, Result, WStr, Writer}; - -const MAGIC_LEN: usize = 9; -const UTF8_MARKER_INDEX: usize = 5; -const TYPE_SEPARATOR: [u8; 4] = [0xFE, 0xFF, 0xFF, 0xFF]; -const STRING_INDICATOR: u32 = 0x0001_D4C0; -const STRING_START: u32 = 0x07D0; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Database { - pub dat_indicator: u8, - pub dat_magic: [u8; MAGIC_LEN], - pub dat_version: u8, - pub utf8: bool, - pub types: Vec, - /// True when `dat_version == 0xC4` (Wolf Pro v3.5). The dat type/data section is - /// LZ4-compressed. The `.project` schema is never compressed. - pub v35: bool, - /// For v3.5: the original framed LZ4 dat body `[decSize][encSize][block]`, kept so an - /// unmodified database re-emits byte-exact. - pub dat_packed: Option>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DbType { - // Project side. - pub name: WStr, - pub fields: Vec, - pub data: Vec, - pub description: WStr, - pub field_type_list_size: u32, - /// Padding bytes after the per-field type bytes (`field_type_list_size - fields.len()`). - pub field_type_pad: Vec, - // Dat side. - pub dat_unknown1: u32, - pub dat_fields_size: u32, - /// Present only when `dat_unknown1 == STRING_INDICATOR`. - pub dat_unknown2: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DbField { - // project side - pub name: WStr, - pub type_byte: u8, - pub unknown1: WStr, - pub string_args: Vec, - pub args: Vec, - pub default_value: u32, - // dat side - pub index_info: u32, -} - -impl DbField { - /// True if this field stores a string value (its `index_info` is in the string range). - /// Determines whether a data row reads from `string_values` or `int_values`. - pub fn is_string(&self) -> bool { - self.index_info >= STRING_START - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DbData { - pub name: WStr, - pub int_values: Vec, - pub string_values: Vec, -} - -impl Database { - pub fn read(project_bytes: &[u8], dat_bytes: &[u8]) -> Result { - // --- dat header --- - let mut rd = Reader::new(dat_bytes); - let dat_indicator = rd.read_u8()?; - if dat_indicator != 0 { - return Err(Error::invalid( - "database .dat is encrypted (indicator != 0); decrypt with wolf-archive first", - )); - } - let magic_vec = rd.read_bytes(MAGIC_LEN)?; - if magic_vec[0] != 0x57 || magic_vec[6] != 0x46 || magic_vec[7] != 0x4D { - return Err(Error::invalid("not a database .dat (magic mismatch)")); - } - let mut dat_magic = [0u8; MAGIC_LEN]; - dat_magic.copy_from_slice(&magic_vec); - let utf8 = dat_magic[UTF8_MARKER_INDEX] == b'U'; - let dat_version = rd.read_u8()?; - let v35 = dat_version == 0xC4; - let _ = Ctx { utf8, v35 }; - - // For v3.5 the dat type/data section after the version byte is an LZ4 block. - // Decompress it and read the values from the result. The `.project` is plain. - let rem = rd.remaining(); - let (dat_packed, dat_body): (Option>, Vec) = if v35 { - let framed = rd.read_bytes(rem)?; - let body = lz4::unpack(&framed)?; - (Some(framed), body) - } else { - (None, rd.read_bytes(rem)?) - }; - let mut rd = Reader::new(&dat_body); - - // --- project (schema) --- - let mut rp = Reader::new(project_bytes); - let type_cnt = rp.read_u32()? as usize; - let mut types = Vec::with_capacity(type_cnt); - for _ in 0..type_cnt { - types.push(DbType::read_project(&mut rp)?); - } - if !rp.is_eof() { - return Err(Error::invalid("database .project has trailing data")); - } - - // --- dat body (values) --- - let dat_type_cnt = rd.read_u32()? as usize; - if dat_type_cnt != types.len() { - return Err(Error::invalid(format!( - "database type count mismatch: project {} vs dat {}", - types.len(), - dat_type_cnt - ))); - } - for t in &mut types { - t.read_dat(&mut rd)?; - } - let terminator = rd.read_u8()?; - if terminator != dat_version { - return Err(Error::invalid(format!( - "database .dat terminator 0x{terminator:02x} != version 0x{dat_version:02x}" - ))); - } - if !rd.is_eof() { - return Err(Error::invalid("database .dat has trailing data")); - } - - Ok(Database { - dat_indicator, - dat_magic, - dat_version, - utf8, - types, - v35, - dat_packed, - }) - } - - /// Returns `(project_bytes, dat_bytes)`. - pub fn write(&self) -> (Vec, Vec) { - let mut wp = Writer::new(); - wp.write_u32(self.types.len() as u32); - for t in &self.types { - t.write_project(&mut wp); - } - - let mut wd = Writer::new(); - wd.write_u8(self.dat_indicator); - wd.write_bytes(&self.dat_magic); - wd.write_u8(self.dat_version); - - // The compressible dat body: type count + per-type values + terminator. - let mut body = Writer::new(); - body.write_u32(self.types.len() as u32); - for t in &self.types { - t.write_dat(&mut body); - } - body.write_u8(self.dat_version); - let body = body.into_bytes(); - - if self.v35 { - // Re-emit original packed bytes verbatim when unchanged (byte-exact). Else - // compress fresh (valid LZ4, content-exact). - match &self.dat_packed { - Some(orig) if lz4::unpack(orig).map(|b| b == body).unwrap_or(false) => { - wd.write_bytes(orig); - } - _ => wd.write_bytes(&lz4::pack(&body)), - } - } else { - wd.write_bytes(&body); - } - - (wp.into_bytes(), wd.into_bytes()) - } -} - -/// True if a database `.dat` carries the `W..OL.FM` container magic (`indicator 0` + magic). -/// The legacy `SysDataBaseBasic.dat` (Wolf 2.x) does not. See [`BasicDatabase`]. -pub fn dat_has_db_magic(dat_bytes: &[u8]) -> bool { - dat_bytes.len() >= 9 - && dat_bytes[0] == 0 - && dat_bytes[1] == 0x57 - && dat_bytes[7] == 0x46 - && dat_bytes[8] == 0x4D -} - -/// The legacy "basic system database" (`SysDataBaseBasic`, Wolf 2.x). Both halves predate the -/// modern serialization. The `.dat` has no `W..OL.FM` container magic, and the `.project` uses -/// an older field-metadata layout. It is an editor template, never referenced by command -/// operands (the real `SysDatabase` is the system DB), so reversing its older schema buys no -/// modding value. Both halves are carried verbatim. That keeps the pair byte-exact through a -/// repack and never errors, consistent with the toolchain's "unknown bytes preserved" contract. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BasicDatabase { - pub project_raw: Vec, - pub dat_raw: Vec, -} - -impl BasicDatabase { - pub fn read(project_bytes: &[u8], dat_bytes: &[u8]) -> Result { - // A light sanity check so this isn't an accept-anything passthrough. The `.dat` must - // lack the container magic (else it is a normal `Database`), and the `.project` must - // start with a plausible type count. - if dat_has_db_magic(dat_bytes) { - return Err(Error::invalid( - "SysDataBaseBasic .dat has DB magic; read it as a normal Database", - )); - } - let type_cnt = Reader::new(project_bytes).read_u32()?; - if type_cnt > 10_000 { - return Err(Error::invalid( - "basic database .project type count implausible", - )); - } - Ok(BasicDatabase { - project_raw: project_bytes.to_vec(), - dat_raw: dat_bytes.to_vec(), - }) - } - - /// Returns `(project_bytes, dat_bytes)`, both carried verbatim. - pub fn write(&self) -> (Vec, Vec) { - (self.project_raw.clone(), self.dat_raw.clone()) - } -} - -impl DbType { - fn read_project(r: &mut Reader) -> Result { - let name = r.read_string()?; - - let field_cnt = r.read_u32()? as usize; - let mut fields = Vec::with_capacity(field_cnt); - for _ in 0..field_cnt { - fields.push(DbField { - name: r.read_string()?, - type_byte: 0, - unknown1: WStr::default(), - string_args: Vec::new(), - args: Vec::new(), - default_value: 0, - index_info: 0, - }); - } - - let data_cnt = r.read_u32()? as usize; - let mut data = Vec::with_capacity(data_cnt); - for _ in 0..data_cnt { - data.push(DbData { - name: r.read_string()?, - int_values: Vec::new(), - string_values: Vec::new(), - }); - } - - let description = r.read_string()?; - - let field_type_list_size = r.read_u32()?; - for f in &mut fields { - f.type_byte = r.read_u8()?; - } - let pad_len = (field_type_list_size as usize) - .checked_sub(fields.len()) - .ok_or_else(|| Error::invalid("field_type_list_size smaller than field count"))?; - let field_type_pad = r.read_bytes(pad_len)?; - - // Per-field metadata blocks. Each is preceded by a count that (for well-formed - // databases) equals the field count. - read_indexed(r, fields.len(), "unknown1", |r, i| { - fields[i].unknown1 = r.read_string()?; - Ok(()) - })?; - read_indexed(r, fields.len(), "stringArgs", |r, i| { - fields[i].string_args = r.read_string_array()?; - Ok(()) - })?; - read_indexed(r, fields.len(), "args", |r, i| { - fields[i].args = r.read_int_array()?; - Ok(()) - })?; - read_indexed(r, fields.len(), "defaultValue", |r, i| { - fields[i].default_value = r.read_u32()?; - Ok(()) - })?; - - Ok(DbType { - name, - fields, - data, - description, - field_type_list_size, - field_type_pad, - dat_unknown1: 0, - dat_fields_size: 0, - dat_unknown2: None, - }) - } - - fn write_project(&self, w: &mut Writer) { - w.write_string(&self.name); - w.write_u32(self.fields.len() as u32); - for f in &self.fields { - w.write_string(&f.name); - } - w.write_u32(self.data.len() as u32); - for d in &self.data { - w.write_string(&d.name); - } - w.write_string(&self.description); - - w.write_u32(self.field_type_list_size); - for f in &self.fields { - w.write_u8(f.type_byte); - } - w.write_bytes(&self.field_type_pad); - - w.write_u32(self.fields.len() as u32); - for f in &self.fields { - w.write_string(&f.unknown1); - } - w.write_u32(self.fields.len() as u32); - for f in &self.fields { - w.write_string_array(&f.string_args); - } - w.write_u32(self.fields.len() as u32); - for f in &self.fields { - w.write_int_array(&f.args); - } - w.write_u32(self.fields.len() as u32); - for f in &self.fields { - w.write_u32(f.default_value); - } - } - - fn read_dat(&mut self, r: &mut Reader) -> Result<()> { - r.verify(&TYPE_SEPARATOR, "database type separator")?; - self.dat_unknown1 = r.read_u32()?; - self.dat_fields_size = r.read_u32()?; - if self.dat_unknown1 == STRING_INDICATOR { - self.dat_unknown2 = Some(r.read_string()?); - } - - let fields_size = self.dat_fields_size as usize; - if fields_size > self.fields.len() { - return Err(Error::invalid(format!( - "dat fields_size {} exceeds project field count {}", - fields_size, - self.fields.len() - ))); - } - for f in self.fields.iter_mut().take(fields_size) { - f.index_info = r.read_u32()?; - } - - let data_size = r.read_u32()? as usize; - if self.data.len() > data_size { - self.data.truncate(data_size); - } - if data_size > self.data.len() { - return Err(Error::invalid(format!( - "dat data_size {} exceeds project data count {}", - data_size, - self.data.len() - ))); - } - - let int_cnt = self - .fields - .iter() - .take(fields_size) - .filter(|f| !f.is_string()) - .count(); - let str_cnt = fields_size - int_cnt; - - for datum in &mut self.data { - datum.int_values = Vec::with_capacity(int_cnt); - for _ in 0..int_cnt { - datum.int_values.push(r.read_u32()?); - } - datum.string_values = Vec::with_capacity(str_cnt); - for _ in 0..str_cnt { - datum.string_values.push(r.read_string()?); - } - } - Ok(()) - } - - fn write_dat(&self, w: &mut Writer) { - w.write_bytes(&TYPE_SEPARATOR); - w.write_u32(self.dat_unknown1); - w.write_u32(self.dat_fields_size); - if self.dat_unknown1 == STRING_INDICATOR { - // Always present in this branch. Default to an empty string if somehow missing. - let s = self.dat_unknown2.clone().unwrap_or_default(); - w.write_string(&s); - } - let fields_size = self.dat_fields_size as usize; - for f in self.fields.iter().take(fields_size) { - w.write_u32(f.index_info); - } - w.write_u32(self.data.len() as u32); - for datum in &self.data { - for &v in &datum.int_values { - w.write_u32(v); - } - for s in &datum.string_values { - w.write_string(s); - } - } - } -} - -/// Read a `count`-prefixed metadata block. Reads the stored count and applies it to -/// `fields[0..count]`. Guards against the count exceeding the field vector. -fn read_indexed( - r: &mut Reader, - field_count: usize, - label: &str, - mut apply: impl FnMut(&mut Reader, usize) -> Result<()>, -) -> Result<()> { - let n = r.read_u32()? as usize; - if n > field_count { - return Err(Error::invalid(format!( - "database project '{label}' count {n} exceeds field count {field_count}" - ))); - } - for i in 0..n { - apply(r, i)?; - } - Ok(()) -} diff --git a/util/wolfdawn-master/crates/wolf-formats/src/game_dat.rs b/util/wolfdawn-master/crates/wolf-formats/src/game_dat.rs deleted file mode 100644 index 01ffad3..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/src/game_dat.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Game.dat: the per-game settings/title record. Carries the window title, the -//! splash/title-screen messages, the editor font list and a handful of opaque size/offset -//! housekeeping fields the engine uses internally. -//! -//! Layout on disk (plaintext case): -//! `[crypt-indicator u8=0][9-byte magic][unknown1 byteArray][stringCount u32][strings...] -//! [fileSize u32][unknownSize u32][unknownWordSize u32][wordData][intOffset1 u32] -//! [intOffset2 u32][unknown2 tail]`. -//! -//! The leading indicator and any encrypted body are crypto-layer concerns. This reader handles -//! the plaintext case (indicator 0) and errors otherwise. -//! -//! Write quirk: the engine recomputes `fileSize` and shifts `intOffset1`/`intOffset2` by the -//! size delta on dump. To keep an unmodified file byte-exact, anchor the delta on the body size -//! as read. An unchanged file yields `sizeDiff == 0`, so the three fields re-emit verbatim. An -//! edited title shifts them by the exact byte delta. - -use crate::Ctx; -use wolf_core::{Error, Reader, Result, WStr, Writer}; - -/// `{0x57,00,00,0x4F,0x4C,00,0x46,0x4D,(00|'U')}`: "WOLFM" with a UTF-8 toggle at idx 8. -const MAGIC_LEN: usize = 9; -const UTF8_MARKER_INDEX: usize = 8; -/// The fixed `magicString` (string index 1) every Game.dat carries. -const MAGIC_STRING: &[u8] = b"0000-0000"; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct GameDat { - /// Leading crypt-indicator byte (0 = plaintext). - pub indicator: u8, - pub magic: [u8; MAGIC_LEN], - pub utf8: bool, - - /// Opaque header byte-array preceding the string table. - pub unknown1: Vec, - /// The on-disk string count. Gates which optional strings are present (see [`GameDat::read`]). - pub string_count: u32, - - // The string table. Player-facing strings are kept as byte-preserving `WStr`. - /// String 0: the window/game title. - pub title: WStr, - /// String 1: the fixed `0000-0000` marker, preserved verbatim. - pub magic_string: WStr, - /// String 2: the decrypt key, stored as a raw byte-array (not a length-prefixed string). - pub decrypt_key: Vec, - /// String 3: the main editor font. - pub font: WStr, - /// Strings 4 to 6: the three fallback sub-fonts. - pub sub_fonts: Vec, - /// String 7: the default protagonist graphic. - pub default_pc_graphic: WStr, - /// String 8 (present when `string_count >= 9`): the title-suffix or version string. - pub title_plus: Option, - /// Strings 9 to 12 (present when `string_count > 9`): road image, gauge image, the start-up - /// message and the title message. - pub road_img: Option, - pub gauge_img: Option, - pub start_up_msg: Option, - pub title_msg: Option, - /// String 14 (present when `string_count > 13`): an opaque trailing string. - pub unknown_string14: Option, - - // Size/offset housekeeping (recomputed on write, see module docs). - pub file_size: u32, - pub unknown_size: u32, - pub unknown_word_size: u32, - /// `unknown_word_size * 2` opaque bytes. - pub unknown_word_data: Vec, - pub int_offset1: u32, - pub int_offset2: u32, - /// Everything after `int_offset2` until EOF (opaque editor tail). - pub unknown2: Vec, - - /// The body size (everything after the indicator byte) as computed from the data at read - /// time. The write-time `sizeDiff` is `calc_body_size() - body_size_at_read`, so an - /// unmodified file re-serializes byte-exact (delta 0) while an edit shifts the offsets by the - /// exact byte delta. - body_size_at_read: u32, -} - -impl GameDat { - pub fn read(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - - let indicator = r.read_u8()?; - if indicator != 0 { - return Err(Error::invalid( - "Game.dat is encrypted (indicator != 0); decrypt with wolf-archive first", - )); - } - - let magic_vec = r.read_bytes(MAGIC_LEN)?; - if magic_vec[0] != 0x57 || magic_vec[6] != 0x46 || magic_vec[7] != 0x4D { - return Err(Error::invalid("not a Game.dat (magic mismatch)")); - } - let mut magic = [0u8; MAGIC_LEN]; - magic.copy_from_slice(&magic_vec); - let utf8 = magic[UTF8_MARKER_INDEX] == b'U'; - let _ = Ctx { utf8, v35: false }; - - let unknown1 = r.read_byte_array()?; - let string_count = r.read_u32()?; - - // String 0 - let title = r.read_string()?; - // String 1 (the fixed marker) - let magic_string = r.read_string()?; - if magic_string.as_bytes().strip_suffix(&[0u8]).unwrap_or(magic_string.as_bytes()) - != MAGIC_STRING - { - return Err(Error::invalid(format!( - "Game.dat invalid magic string: {:?}", - magic_string.to_lossy() - ))); - } - // String 2 (a byte-array, not a string) - let decrypt_key = r.read_byte_array()?; - // String 3 - let font = r.read_string()?; - // Strings 4-6 - let mut sub_fonts = Vec::with_capacity(3); - for _ in 0..3 { - sub_fonts.push(r.read_string()?); - } - // String 7 - let default_pc_graphic = r.read_string()?; - - // String 8 - let title_plus = if string_count >= 9 { - Some(r.read_string()?) - } else { - None - }; - - // Strings 9-12 - let (road_img, gauge_img, start_up_msg, title_msg) = if string_count > 9 { - ( - Some(r.read_string()?), - Some(r.read_string()?), - Some(r.read_string()?), - Some(r.read_string()?), - ) - } else { - (None, None, None, None) - }; - - // String 14 - let unknown_string14 = if string_count > 13 { - Some(r.read_string()?) - } else { - None - }; - - let file_size = r.read_u32()?; - let unknown_size = r.read_u32()?; - let unknown_word_size = r.read_u32()?; - let unknown_word_data = r.read_bytes(unknown_word_size as usize * 2)?; - let int_offset1 = r.read_u32()?; - let int_offset2 = r.read_u32()?; - let unknown2 = r.read_bytes(r.remaining())?; - - if !r.is_eof() { - return Err(Error::invalid("Game.dat has trailing data")); - } - - let mut gd = GameDat { - indicator, - magic, - utf8, - unknown1, - string_count, - title, - magic_string, - decrypt_key, - font, - sub_fonts, - default_pc_graphic, - title_plus, - road_img, - gauge_img, - start_up_msg, - title_msg, - unknown_string14, - file_size, - unknown_size, - unknown_word_size, - unknown_word_data, - int_offset1, - int_offset2, - unknown2, - body_size_at_read: 0, - }; - gd.body_size_at_read = gd.calc_body_size(); - Ok(gd) - } - - pub fn write(&self) -> Vec { - // The size delta vs. the data as read. It is 0 for an unmodified file (byte-exact), else - // the exact byte difference, applied to the three housekeeping fields. - let new_size = self.calc_body_size(); - let size_diff = new_size as i64 - self.body_size_at_read as i64; - let shift = |v: u32| -> u32 { (v as i64 + size_diff) as u32 }; - - let mut w = Writer::new(); - w.write_u8(self.indicator); - w.write_bytes(&self.magic); - - w.write_byte_array(&self.unknown1); - w.write_u32(self.string_count); - - w.write_string(&self.title); - w.write_string(&self.magic_string); - w.write_byte_array(&self.decrypt_key); - w.write_string(&self.font); - for f in &self.sub_fonts { - w.write_string(f); - } - w.write_string(&self.default_pc_graphic); - if let Some(s) = &self.title_plus { - w.write_string(s); - } - if let (Some(r), Some(g), Some(s), Some(t)) = - (&self.road_img, &self.gauge_img, &self.start_up_msg, &self.title_msg) - { - w.write_string(r); - w.write_string(g); - w.write_string(s); - w.write_string(t); - } - if let Some(s) = &self.unknown_string14 { - w.write_string(s); - } - - w.write_u32(shift(self.file_size)); - w.write_u32(self.unknown_size); - w.write_u32(self.unknown_word_size); - w.write_bytes(&self.unknown_word_data); - w.write_u32(shift(self.int_offset1)); - w.write_u32(shift(self.int_offset2)); - w.write_bytes(&self.unknown2); - - w.into_bytes() - } - - /// The body size everything after the indicator byte serializes to (magic + string table + - /// housekeeping fields + tail). Excludes the leading crypt indicator. Used to anchor the - /// write-time `sizeDiff`. - fn calc_body_size(&self) -> u32 { - let str_size = |s: &WStr| s.len() + 4; // u32 length prefix + bytes - let mut size = MAGIC_LEN; - size += self.unknown1.len() + 4; - size += 4; // string_count - size += str_size(&self.title); - size += str_size(&self.magic_string); - size += self.decrypt_key.len() + 4; - size += str_size(&self.font); - for f in &self.sub_fonts { - size += str_size(f); - } - size += str_size(&self.default_pc_graphic); - if let Some(s) = &self.title_plus { - size += str_size(s); - } - for s in [&self.road_img, &self.gauge_img, &self.start_up_msg, &self.title_msg] - .into_iter() - .flatten() - { - size += str_size(s); - } - if let Some(s) = &self.unknown_string14 { - size += str_size(s); - } - size += 4; // file_size - size += 4; // unknown_size - size += 4; // unknown_word_size - size += self.unknown_word_data.len(); - size += 4; // int_offset1 - size += 4; // int_offset2 - size += self.unknown2.len(); - size as u32 - } -} diff --git a/util/wolfdawn-master/crates/wolf-formats/src/lib.rs b/util/wolfdawn-master/crates/wolf-formats/src/lib.rs deleted file mode 100644 index 1742d73..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! `wolf-formats`: byte-faithful, typed (de)serialization of Wolf RPG data files. -//! -//! Each format exposes `read(&[u8]) -> Result` and `T::write(&self) -> Vec`. -//! The contract is round-trip fidelity. For an unmodified plaintext file, -//! `write(read(bytes)) == bytes`. Anything not yet understood is preserved verbatim -//! as opaque bytes rather than dropped, so recompiled files always remain loadable. - -#![forbid(unsafe_code)] - -pub mod command; -pub mod common_event; -pub mod database; -pub mod game_dat; -pub mod map; - -pub use wolf_core::{Error, Reader, Result, WStr, Writer}; - -/// Context threaded through a load/dump so per-command behaviour matches the owning -/// file. Encoding and v3.5 mode are passed explicitly rather than via globals. -#[derive(Debug, Clone, Copy, Default)] -pub struct Ctx { - /// File strings are UTF-8 (magic toggle byte == `'U'`), else Shift-JIS. - pub utf8: bool, - /// Wolf Pro v3.5 file: every command carries a trailing length-prefixed blob. - pub v35: bool, -} diff --git a/util/wolfdawn-master/crates/wolf-formats/src/map.rs b/util/wolfdawn-master/crates/wolf-formats/src/map.rs deleted file mode 100644 index ff6a32e..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/src/map.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! Map file (`.mps`): `WOLFM` magic, tileset/size header, opaque tile grid, then a -//! list of events, each with pages that hold the event command program. -//! -//! The LZ4-compressed body of v3.5 maps (version >= 0x65) is not yet handled. The local -//! corpus is version 0x64 (plain). - -use crate::command::{read_route_list, write_route_list, RawCommand, RouteCommand}; -use crate::Ctx; -use wolf_core::codec::lz4; -use wolf_core::{Error, Reader, Result, WStr, Writer}; - -const MAGIC_LEN: usize = 20; -const WOLFM: &[u8] = b"WOLFM"; -const UTF8_MARKER_INDEX: usize = 16; - -const EVENT_INDICATOR: u8 = 0x6F; -const MAP_TERMINATOR: u8 = 0x66; -const PAGE_INDICATOR: u8 = 0x79; -const EVENT_TERMINATOR: u8 = 0x70; -const PAGE_TERMINATOR: u8 = 0x7A; - -const EVENT_MAGIC1: [u8; 4] = [0x39, 0x30, 0x00, 0x00]; -const EVENT_MAGIC2: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; - -const LZ4_VERSION_GATE: u32 = 0x65; -const V35_VERSION_GATE: u32 = 0x67; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Map { - /// The full 20-byte magic, kept verbatim (the byte at index 16 is `'U'` for UTF-8). - pub magic: [u8; MAGIC_LEN], - pub version: u32, - pub unknown2: u8, - pub unknown3: WStr, - pub tileset_id: u32, - pub width: u32, - pub height: u32, - /// Present only for version >= 0x67. - pub unknown4: Option, - /// Layer count (3 by default, explicit since v3.5). Used to size the tile grid. - pub layer_count: u32, - /// Raw tile grid bytes (`width * height * layer_count * 4`), opaque for now. - pub tiles: Vec, - /// UTF-8 maps may store a `0xFFFFFFFF` marker meaning "no tile grid". - pub no_tiles_marker: bool, - pub events: Vec, - pub utf8: bool, - /// For v3.5 maps (version >= 0x65): the original framed LZ4 body - /// `[decSize][encSize][block]`, kept so an unmodified map re-emits byte-exact. - pub packed: Option>, -} - -impl Map { - pub fn read(bytes: &[u8]) -> Result { - let mut r = Reader::new(bytes); - - let magic_vec = r.read_bytes(MAGIC_LEN)?; - if &magic_vec[10..15] != WOLFM { - return Err(Error::invalid("not a WOLFM map (magic mismatch)")); - } - let mut magic = [0u8; MAGIC_LEN]; - magic.copy_from_slice(&magic_vec); - let utf8 = magic[UTF8_MARKER_INDEX] == b'U'; - - let version = r.read_u32()?; - let unknown2 = r.read_u8()?; - - let ctx = Ctx { - utf8, - v35: version >= V35_VERSION_GATE, - }; - - // v3.5 maps (version >= 0x65) LZ4-compress everything after [magic][version][unknown2]. - let rem = r.remaining(); - let (packed, body): (Option>, Vec) = if version >= LZ4_VERSION_GATE { - let framed = r.read_bytes(rem)?; - let decompressed = lz4::unpack(&framed)?; - (Some(framed), decompressed) - } else { - (None, r.read_bytes(rem)?) - }; - let mut r = Reader::new(&body); - - let unknown3 = r.read_string()?; - let tileset_id = r.read_u32()?; - let width = r.read_u32()?; - let height = r.read_u32()?; - let event_count = r.read_u32()? as usize; - - let mut unknown4 = None; - let mut layer_count = 3u32; - if version >= V35_VERSION_GATE { - unknown4 = Some(r.read_u32()?); - layer_count = r.read_u32()?; - } - - // UTF-8 maps may signal an absent tile grid with a 0xFFFFFFFF sentinel. - let mut no_tiles_marker = false; - let mut tiles = Vec::new(); - if utf8 && r.peek_u32()? == 0xFFFF_FFFF { - r.read_u32()?; - no_tiles_marker = true; - } - if !no_tiles_marker { - let tile_bytes = (width as usize) - .saturating_mul(height as usize) - .saturating_mul(layer_count as usize) - .saturating_mul(4); - tiles = r.read_bytes(tile_bytes)?; - } - - let mut events = Vec::with_capacity(event_count); - loop { - let indicator = r.read_u8()?; - if indicator == MAP_TERMINATOR { - break; - } - if indicator != EVENT_INDICATOR { - return Err(Error::invalid(format!( - "unexpected map event indicator 0x{indicator:02x} (expected 0x6F or 0x66)" - ))); - } - events.push(Event::read(&mut r, &ctx)?); - } - - if events.len() != event_count { - return Err(Error::invalid(format!( - "map declared {event_count} events but read {}", - events.len() - ))); - } - if !r.is_eof() { - return Err(Error::invalid("map has trailing data after terminator")); - } - - Ok(Map { - magic, - version, - unknown2, - unknown3, - tileset_id, - width, - height, - unknown4, - layer_count, - tiles, - no_tiles_marker, - events, - utf8, - packed, - }) - } - - pub fn write(&self) -> Vec { - let ctx = Ctx { - utf8: self.utf8, - v35: self.version >= V35_VERSION_GATE, - }; - let mut w = Writer::with_capacity(self.tiles.len() + 4096); - - // The fixed, never-compressed header. - w.write_bytes(&self.magic); - w.write_u32(self.version); - w.write_u8(self.unknown2); - - // The compressible body: everything from unknown3 to the map terminator. - let mut body = Writer::with_capacity(self.tiles.len() + 4096); - body.write_string(&self.unknown3); - body.write_u32(self.tileset_id); - body.write_u32(self.width); - body.write_u32(self.height); - body.write_u32(self.events.len() as u32); - - if self.version >= V35_VERSION_GATE { - body.write_u32(self.unknown4.unwrap_or(0)); - body.write_u32(self.layer_count); - } - - if self.no_tiles_marker { - body.write_u32(0xFFFF_FFFF); - } else { - body.write_bytes(&self.tiles); - } - - for event in &self.events { - body.write_u8(EVENT_INDICATOR); - event.write(&mut body, &ctx); - } - body.write_u8(MAP_TERMINATOR); - let body = body.into_bytes(); - - if self.version >= LZ4_VERSION_GATE { - // Re-emit original packed bytes verbatim when unchanged (byte-exact). Else - // compress fresh (valid LZ4, content-exact). - match &self.packed { - Some(orig) if lz4::unpack(orig).map(|b| b == body).unwrap_or(false) => { - w.write_bytes(orig); - } - _ => w.write_bytes(&lz4::pack(&body)), - } - } else { - w.write_bytes(&body); - } - - w.into_bytes() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Event { - pub id: u32, - pub name: WStr, - pub x: u32, - pub y: u32, - pub pages: Vec, -} - -impl Event { - fn read(r: &mut Reader, ctx: &Ctx) -> Result { - r.verify(&EVENT_MAGIC1, "event magic1")?; - let id = r.read_u32()?; - let name = r.read_string()?; - let x = r.read_u32()?; - let y = r.read_u32()?; - let page_count = r.read_u32()? as usize; - r.verify(&EVENT_MAGIC2, "event magic2")?; - - let mut pages = Vec::with_capacity(page_count); - loop { - let indicator = r.read_u8()?; - if indicator == EVENT_TERMINATOR { - break; - } - if indicator != PAGE_INDICATOR { - return Err(Error::invalid(format!( - "unexpected event page indicator 0x{indicator:02x} (expected 0x79 or 0x70)" - ))); - } - pages.push(Page::read(r, ctx)?); - } - - if pages.len() != page_count { - return Err(Error::invalid(format!( - "event {id} declared {page_count} pages but read {}", - pages.len() - ))); - } - - Ok(Event { - id, - name, - x, - y, - pages, - }) - } - - fn write(&self, w: &mut Writer, ctx: &Ctx) { - w.write_bytes(&EVENT_MAGIC1); - w.write_u32(self.id); - w.write_string(&self.name); - w.write_u32(self.x); - w.write_u32(self.y); - w.write_u32(self.pages.len() as u32); - w.write_bytes(&EVENT_MAGIC2); - for page in &self.pages { - w.write_u8(PAGE_INDICATOR); - page.write(w, ctx); - } - w.write_u8(EVENT_TERMINATOR); - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Page { - pub unknown1: u32, - pub graphic_name: WStr, - pub graphic_direction: u8, - pub graphic_frame: u8, - pub graphic_opacity: u8, - pub graphic_render_mode: u8, - /// 37 opaque condition bytes (1 + 4 + 16 + 16). - pub conditions: Vec, - /// 4 opaque movement bytes. - pub movement: Vec, - pub flags: u8, - pub route_flags: u8, - pub route: Vec, - pub commands: Vec, - pub features: u32, - pub shadow_graphic_num: u8, - pub collision_width: u8, - pub collision_height: u8, - /// Present only when `features > 3`. - pub page_transfer: Option, -} - -impl Page { - fn read(r: &mut Reader, ctx: &Ctx) -> Result { - let unknown1 = r.read_u32()?; - let graphic_name = r.read_string()?; - let graphic_direction = r.read_u8()?; - let graphic_frame = r.read_u8()?; - let graphic_opacity = r.read_u8()?; - let graphic_render_mode = r.read_u8()?; - let conditions = r.read_bytes(37)?; - let movement = r.read_bytes(4)?; - let flags = r.read_u8()?; - let route_flags = r.read_u8()?; - let route = read_route_list(r)?; - - let command_count = r.read_u32()? as usize; - let mut commands = Vec::with_capacity(command_count); - for _ in 0..command_count { - commands.push(RawCommand::read(r, ctx)?); - } - - let features = r.read_u32()?; - let shadow_graphic_num = r.read_u8()?; - let collision_width = r.read_u8()?; - let collision_height = r.read_u8()?; - let page_transfer = if features > 3 { - Some(r.read_u8()?) - } else { - None - }; - - let terminator = r.read_u8()?; - if terminator != PAGE_TERMINATOR { - return Err(Error::invalid(format!( - "page terminator was 0x{terminator:02x}, expected 0x7A" - ))); - } - - Ok(Page { - unknown1, - graphic_name, - graphic_direction, - graphic_frame, - graphic_opacity, - graphic_render_mode, - conditions, - movement, - flags, - route_flags, - route, - commands, - features, - shadow_graphic_num, - collision_width, - collision_height, - page_transfer, - }) - } - - fn write(&self, w: &mut Writer, ctx: &Ctx) { - w.write_u32(self.unknown1); - w.write_string(&self.graphic_name); - w.write_u8(self.graphic_direction); - w.write_u8(self.graphic_frame); - w.write_u8(self.graphic_opacity); - w.write_u8(self.graphic_render_mode); - w.write_bytes(&self.conditions); - w.write_bytes(&self.movement); - w.write_u8(self.flags); - w.write_u8(self.route_flags); - write_route_list(w, &self.route); - - w.write_u32(self.commands.len() as u32); - for c in &self.commands { - c.write(w, ctx); - } - - w.write_u32(self.features); - w.write_u8(self.shadow_graphic_num); - w.write_u8(self.collision_width); - w.write_u8(self.collision_height); - if let Some(pt) = self.page_transfer { - w.write_u8(pt); - } - w.write_u8(PAGE_TERMINATOR); - } -} diff --git a/util/wolfdawn-master/crates/wolf-formats/tests/all_versions.rs b/util/wolfdawn-master/crates/wolf-formats/tests/all_versions.rs deleted file mode 100644 index 9334396..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/tests/all_versions.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Cross-version format coverage: round-trip every Wolf editor version's loose Data/ files -//! (CommonEvent, maps, databases) byte-exact. Reveals decoder version gaps. -//! cargo test -p wolf-formats --test all_versions -- --nocapture -use std::path::{Path, PathBuf}; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -fn versions_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("ALLWOLFVERSIONS") -} - -fn find(dir: &Path, name: &str) -> Option { - let mut stack = vec![dir.to_path_buf()]; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else if p.file_name().and_then(|s| s.to_str()) == Some(name) { - return Some(p); - } - } - } - None -} - -fn find_all_mps(dir: &Path) -> Vec { - let mut out = Vec::new(); - let mut stack = vec![dir.to_path_buf()]; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else if p.extension().and_then(|s| s.to_str()) == Some("mps") { - out.push(p); - } - } - } - out -} - -#[test] -fn all_wolf_versions_round_trip() { - let root = versions_root(); - let Ok(dirs) = std::fs::read_dir(&root) else { - eprintln!("no ALLWOLFVERSIONS"); - return; - }; - let mut editors: Vec = dirs - .flatten() - .map(|e| e.path()) - .filter(|p| p.is_dir()) - .collect(); - editors.sort(); - - let mut total_ok = 0; - let mut total_fail = 0; - for ed in &editors { - let name = ed.file_name().unwrap().to_string_lossy().into_owned(); - let mut ok = 0; - let mut fails: Vec = Vec::new(); - - // CommonEvent.dat - if let Some(p) = find(ed, "CommonEvent.dat") { - match std::fs::read(&p) - .map(|b| (CommonEventsFile::read(&b).map(|ce| ce.write() == b), b)) - { - Ok((Ok(true), _)) => ok += 1, - Ok((Ok(false), _)) => fails.push("CommonEvent:diff".into()), - Ok((Err(e), b)) => fails.push(format!( - "CommonEvent:ERR(v{:#x}):{}", - b.get(10).copied().unwrap_or(0), - short(&e.to_string()) - )), - _ => {} - } - } - // Maps - let mut map_ok = 0; - let mut map_fail = 0; - for mp in find_all_mps(ed) { - let Ok(b) = std::fs::read(&mp) else { continue }; - match Map::read(&b) { - Ok(m) if m.write() == b => map_ok += 1, - Ok(_) => map_fail += 1, - Err(_) => map_fail += 1, - } - } - if map_fail == 0 && map_ok > 0 { - ok += 1; - } else if map_fail > 0 { - fails.push(format!("maps:{map_ok}ok/{map_fail}fail")); - } - // Databases (name varies: SysDatabase vs SysDataBaseBasic) - for (proj, dat) in db_candidates(ed) { - let (Ok(pb), Ok(db)) = (std::fs::read(&proj), std::fs::read(&dat)) else { - continue; - }; - match Database::read(&pb, &db) { - Ok(d) => { - let (po, dd) = d.write(); - if po == pb && dd == db { - ok += 1; - } else { - fails.push(format!("{}:diff", stem(&dat))); - } - } - Err(e) => fails.push(format!("{}:ERR:{}", stem(&dat), short(&e.to_string()))), - } - } - - if fails.is_empty() { - total_ok += 1; - eprintln!(" OK {name} ({ok} file-groups)"); - } else { - total_fail += 1; - eprintln!(" FAIL {name} ok={ok} -> {}", fails.join(", ")); - } - } - let total = total_ok + total_fail; - eprintln!( - "\nversions fully round-tripping (excl. legacy v2 SysDataBaseBasic): {total_ok}/{total}" - ); - if total > 0 { - assert_eq!( - total_fail, 0, - "some Wolf version's core data did not round-trip byte-exact" - ); - } -} - -/// The standard databases. The legacy v2 `SysDataBaseBasic` is a different pre-v3 format -/// (no `OLUFM` magic) and is intentionally excluded. This is a documented known gap. -fn db_candidates(ed: &Path) -> Vec<(PathBuf, PathBuf)> { - let mut out = Vec::new(); - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - if let Some(proj) = find(ed, &format!("{stem}.project")) { - let dat = proj.with_extension("dat"); - if dat.exists() { - out.push((proj, dat)); - } - } - } - out -} - -fn stem(p: &Path) -> String { - p.file_stem().unwrap().to_string_lossy().into_owned() -} -fn short(s: &str) -> String { - s.chars().take(40).collect() -} diff --git a/util/wolfdawn-master/crates/wolf-formats/tests/archive_end_to_end.rs b/util/wolfdawn-master/crates/wolf-formats/tests/archive_end_to_end.rs deleted file mode 100644 index 2fc3eac..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/tests/archive_end_to_end.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! End-to-end gate: unpack `Data.wolf` (the real DXArchive/Wolf v3.14 container) and prove -//! the extracted inner files are *complete, valid* Wolf files by parsing them and -//! round-tripping them byte-exact through `wolf-formats`. This proves the archive decode -//! (key + Huffman + LZSS) is byte-perfect even where the loose `Data/` is a different build. -//! cargo test -p wolf-formats --test archive_end_to_end -- --nocapture - -use std::path::PathBuf; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -fn root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") -} - -#[test] -fn data_wolf_inner_files_parse_and_roundtrip() { - let Ok(bytes) = std::fs::read(root().join("Data.wolf")) else { - eprintln!("Data.wolf not present, skipping"); - return; - }; - let files = wolf_archive::extract_archive(&bytes, wolf_archive::DEFAULT_WOLF_PRO_KEY) - .expect("decode Data.wolf"); - let get = |name: &str| -> Vec { - files - .iter() - .find(|(p, _)| p.replace('\\', "/") == name) - .unwrap_or_else(|| panic!("{name} not found in archive")) - .1 - .clone() - }; - - // CommonEvent.dat: parse + byte-exact round-trip. - let ce_bytes = get("BasicData/CommonEvent.dat"); - let ce = CommonEventsFile::read(&ce_bytes).expect("parse extracted CommonEvent.dat"); - assert_eq!( - ce.write(), - ce_bytes, - "extracted CommonEvent.dat must round-trip byte-exact" - ); - eprintln!( - "CommonEvent.dat: {} events, {} bytes, round-trip OK", - ce.events.len(), - ce_bytes.len() - ); - - // Every map: parse + byte-exact round-trip. - let mut maps = 0; - for (path, data) in &files { - if !path.to_ascii_lowercase().ends_with(".mps") { - continue; - } - let m = Map::read(data).unwrap_or_else(|e| panic!("parse {path}: {e}")); - assert_eq!( - &m.write(), - data, - "extracted {path} must round-trip byte-exact" - ); - maps += 1; - } - eprintln!("maps: {maps} parsed + round-tripped byte-exact"); - assert!(maps > 0, "expected maps in the archive"); - - // The three databases: parse + byte-exact round-trip. - for (proj, dat) in [ - ("BasicData/DataBase.project", "BasicData/DataBase.dat"), - ("BasicData/CDataBase.project", "BasicData/CDataBase.dat"), - ("BasicData/SysDatabase.project", "BasicData/SysDatabase.dat"), - ] { - let (pb, db_bytes) = (get(proj), get(dat)); - let db = Database::read(&pb, &db_bytes).unwrap_or_else(|e| panic!("parse {dat}: {e}")); - let (po, dout) = db.write(); - assert_eq!(po, pb, "{proj} round-trip"); - assert_eq!(dout, db_bytes, "{dat} round-trip"); - } - eprintln!("3 databases parsed + round-tripped byte-exact"); -} diff --git a/util/wolfdawn-master/crates/wolf-formats/tests/basic_database.rs b/util/wolfdawn-master/crates/wolf-formats/tests/basic_database.rs deleted file mode 100644 index a5089fc..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/tests/basic_database.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Legacy `SysDataBaseBasic` (Wolf 2.x) round-trip gate: the `.project` schema is parsed and -//! re-emitted byte-exact, the magic-less `.dat` is carried opaque, so the pair reproduces -//! byte-for-byte. This file is preserved losslessly rather than dropped. -//! cargo test -p wolf-formats --test basic_database -- --nocapture - -use std::path::{Path, PathBuf}; - -use wolf_formats::database::{dat_has_db_magic, BasicDatabase}; - -fn root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("ALLWOLFVERSIONS") -} - -/// Collect every `SysDataBaseBasic.project` under `dir`. -fn collect(dir: &Path, out: &mut Vec) { - let Ok(rd) = std::fs::read_dir(dir) else { - return; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - collect(&p, out); - } else if p.file_name().and_then(|s| s.to_str()) == Some("SysDataBaseBasic.project") { - out.push(p); - } - } -} - -#[test] -fn basic_database_round_trips_byte_exact() { - let mut projects = Vec::new(); - collect(&root(), &mut projects); - projects.sort(); - projects.dedup(); - - if projects.is_empty() { - eprintln!("no SysDataBaseBasic found, skipping"); - return; - } - - let mut ok = 0usize; - for proj in &projects { - let dat = proj.with_extension("dat"); - let (Ok(pb), Ok(db_)) = (std::fs::read(proj), std::fs::read(&dat)) else { - continue; - }; - // The legacy .dat must indeed lack the container magic (else it is a normal DB). - assert!( - !dat_has_db_magic(&db_), - "{} unexpectedly has DB magic", - dat.display() - ); - - let parsed = BasicDatabase::read(&pb, &db_).expect("basic database parses"); - let (proj_out, dat_out) = parsed.write(); - assert_eq!(proj_out, pb, "{}: .project not byte-exact", proj.display()); - assert_eq!(dat_out, db_, "{}: .dat not byte-exact", dat.display()); - ok += 1; - } - eprintln!( - "SysDataBaseBasic byte-exact round-trips: {ok}/{}", - projects.len() - ); - assert!(ok > 0, "no basic databases were exercised"); -} diff --git a/util/wolfdawn-master/crates/wolf-formats/tests/gamepro_probe.rs b/util/wolfdawn-master/crates/wolf-formats/tests/gamepro_probe.rs deleted file mode 100644 index 1aa485d..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/tests/gamepro_probe.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! Diagnostic probe (reports, never asserts): point our readers at a second, Wolf-Pro -//! game's files to discover format/version/encryption gaps. Run with: -//! cargo test -p wolf-formats --test gamepro_probe -- --nocapture - -use std::path::PathBuf; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -fn pro_data() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("GamePro_Data") -} - -fn entropy(b: &[u8]) -> f64 { - if b.is_empty() { - return 0.0; - } - let mut counts = [0usize; 256]; - for &x in b { - counts[x as usize] += 1; - } - let n = b.len() as f64; - -counts - .iter() - .filter(|&&c| c > 0) - .map(|&c| { - let p = c as f64 / n; - p * p.log2() - }) - .sum::() -} - -fn report_head(label: &str, bytes: &[u8]) { - let n = bytes.len().min(16); - let hex: String = bytes[..n].iter().map(|b| format!("{b:02x} ")).collect(); - let ent = entropy(&bytes[..bytes.len().min(4096)]); - eprintln!( - " {label:24} {} bytes head: {hex} entropy(4k)={ent:.2}", - bytes.len() - ); -} - -#[test] -fn probe_common_event() { - let p = pro_data().join("BasicData").join("CommonEvent.dat"); - let Ok(bytes) = std::fs::read(&p) else { - eprintln!("missing {}", p.display()); - return; - }; - report_head("CommonEvent.dat", &bytes); - let ce = CommonEventsFile::read(&bytes).expect("parse Pro CommonEvent.dat"); - let rt = ce.write(); - eprintln!( - " PARSED: {} events; round-trip {}", - ce.events.len(), - if rt == bytes { "BYTE-EXACT" } else { "DIFF" } - ); - assert_eq!(rt, bytes, "Pro CommonEvent.dat must round-trip byte-exact"); -} - -#[test] -fn probe_databases() { - let dir = pro_data().join("BasicData"); - for (proj, dat) in [ - ("DataBase.project", "DataBase.dat"), - ("CDataBase.project", "CDataBase.dat"), - ("SysDatabase.project", "SysDatabase.dat"), - ] { - let (pp, dp) = (dir.join(proj), dir.join(dat)); - let (Ok(pb), Ok(db_bytes)) = (std::fs::read(&pp), std::fs::read(&dp)) else { - continue; - }; - report_head(dat, &db_bytes); - let db = - Database::read(&pb, &db_bytes).unwrap_or_else(|e| panic!("parse {proj}/{dat}: {e}")); - let (po, dout) = db.write(); - eprintln!( - " {proj}/{dat} PARSED: {} types; proj {} dat {}", - db.types.len(), - if po == pb { "ok" } else { "DIFF" }, - if dout == db_bytes { - "BYTE-EXACT" - } else { - "DIFF" - } - ); - assert_eq!(po, pb, "{proj} must round-trip byte-exact"); - assert_eq!(dout, db_bytes, "{dat} must round-trip byte-exact"); - } -} - -#[test] -fn probe_maps() { - let dir = pro_data().join("MapData"); - let Ok(entries) = std::fs::read_dir(&dir) else { - eprintln!("missing {}", dir.display()); - return; - }; - let mut ok = 0; - let mut fail = 0; - let mut diff = 0; - let mut total = 0; - let mut first_err = String::new(); - let mut versions: std::collections::BTreeMap = Default::default(); - for e in entries { - let path = e.unwrap().path(); - if path.extension().and_then(|s| s.to_str()) != Some("mps") { - continue; - } - let bytes = std::fs::read(&path).unwrap(); - total += 1; - // map version lives at offset 20 (after 10-byte 0x00 pad + WOLFM magic + 'U'/byte). - if bytes.len() > 23 { - let v = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]); - *versions.entry(v).or_default() += 1; - } - match Map::read(&bytes) { - Ok(m) => { - let rt = m.write(); - if rt == bytes { - ok += 1; - } else { - diff += 1; - } - } - Err(e) => { - fail += 1; - if first_err.is_empty() { - first_err = format!("{}: {e}", path.file_name().unwrap().to_string_lossy()); - } - } - } - } - eprintln!(" maps: total={total} byte-exact={ok} diff={diff} parse-fail={fail}"); - eprintln!(" map versions seen: {versions:?}"); - if !first_err.is_empty() { - eprintln!(" first parse error: {first_err}"); - } - assert_eq!(fail, 0, "all Pro maps must parse"); - assert_eq!(ok, total, "all Pro maps must round-trip byte-exact"); -} diff --git a/util/wolfdawn-master/crates/wolf-formats/tests/roundtrip.rs b/util/wolfdawn-master/crates/wolf-formats/tests/roundtrip.rs deleted file mode 100644 index 88bfda5..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/tests/roundtrip.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Byte-exact round-trip gate over the local plaintext corpus. -//! For every sample file: `write(read(bytes)) == bytes`. - -use std::path::PathBuf; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -/// `Wolf/Data`, three levels up from this crate (`Wolf/wolf-dawn/crates/wolf-formats`). -fn data_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("Data") -} - -fn assert_byte_exact(name: &str, original: &[u8], rewritten: &[u8]) { - if original == rewritten { - return; - } - let first_diff = original - .iter() - .zip(rewritten.iter()) - .position(|(a, b)| a != b); - panic!( - "round-trip mismatch for {name}: original {} bytes, rewritten {} bytes, first differing offset = {:?}", - original.len(), - rewritten.len(), - first_diff - ); -} - -#[test] -fn maps_round_trip_byte_exact() { - let dir = data_dir().join("MapData"); - let mut checked = 0; - - let entries = - std::fs::read_dir(&dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display())); - - for entry in entries { - let path = entry.unwrap().path(); - if path.extension().and_then(|s| s.to_str()) != Some("mps") { - continue; - } - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - let original = std::fs::read(&path).unwrap(); - - let map = Map::read(&original).unwrap_or_else(|e| panic!("failed to parse {name}: {e}")); - let rewritten = map.write(); - - assert_byte_exact(&name, &original, &rewritten); - checked += 1; - } - - assert!(checked > 0, "no .mps files found in {}", dir.display()); - eprintln!("round-tripped {checked} map(s) byte-exact"); -} - -#[test] -fn common_events_round_trip_byte_exact() { - let path = data_dir().join("BasicData").join("CommonEvent.dat"); - let original = - std::fs::read(&path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); - - let ce = CommonEventsFile::read(&original) - .unwrap_or_else(|e| panic!("failed to parse CommonEvent.dat: {e}")); - let rewritten = ce.write(); - - assert_byte_exact("CommonEvent.dat", &original, &rewritten); - eprintln!( - "round-tripped CommonEvent.dat ({} events) byte-exact", - ce.events.len() - ); -} - -#[test] -fn databases_round_trip_byte_exact() { - let dir = data_dir().join("BasicData"); - // (project, dat) pairs present in the sample game. - let pairs = [ - ("DataBase.project", "DataBase.dat"), - ("CDataBase.project", "CDataBase.dat"), - ("SysDatabase.project", "SysDatabase.dat"), - ]; - - let mut checked = 0; - for (proj, dat) in pairs { - let proj_path = dir.join(proj); - let dat_path = dir.join(dat); - if !proj_path.exists() || !dat_path.exists() { - continue; - } - let proj_bytes = std::fs::read(&proj_path).unwrap(); - let dat_bytes = std::fs::read(&dat_path).unwrap(); - - let db = Database::read(&proj_bytes, &dat_bytes) - .unwrap_or_else(|e| panic!("failed to parse {proj}/{dat}: {e}")); - let (proj_out, dat_out) = db.write(); - - assert_byte_exact(proj, &proj_bytes, &proj_out); - assert_byte_exact(dat, &dat_bytes, &dat_out); - eprintln!( - "round-tripped {proj} + {dat} ({} types) byte-exact", - db.types.len() - ); - checked += 1; - } - assert!(checked > 0, "no database pairs found in {}", dir.display()); -} diff --git a/util/wolfdawn-master/crates/wolf-formats/tests/ver5_archive.rs b/util/wolfdawn-master/crates/wolf-formats/tests/ver5_archive.rs deleted file mode 100644 index 5ccf0e8..0000000 --- a/util/wolfdawn-master/crates/wolf-formats/tests/ver5_archive.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Verify the DXArchive VER5 decoder against the real Wolf 2.01 sample-game Data.wolf: -//! extracted inner files must parse + round-trip byte-exact through wolf-formats. -//! cargo test -p wolf-formats --test ver5_archive -- --nocapture -use std::path::{Path, PathBuf}; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::map::Map; - -fn find_wolf(dir: &Path) -> Option { - let mut stack = vec![dir.to_path_buf()]; - while let Some(d) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&d) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - stack.push(p); - } else if p.extension().and_then(|s| s.to_str()) == Some("wolf") { - return Some(p); - } - } - } - None -} - -#[test] -fn ver5_ver6_archives_parse() { - // VER5 = Wolf 2.01, VER6 = Wolf 2.20. - for editor in ["WolfRPGEditor_201", "WolfRPGEditor_220"] { - let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("..") - .join("ALLWOLFVERSIONS") - .join(editor); - let Some(arc) = find_wolf(&base) else { - eprintln!("{editor}: no Data.wolf, skipping"); - continue; - }; - let data = std::fs::read(&arc).expect("read Data.wolf"); - let files = wolf_archive::extract_archive(&data, wolf_archive::DEFAULT_WOLF_PRO_KEY) - .unwrap_or_else(|e| panic!("{editor}: archive decode failed: {e}")); - eprintln!("{editor}: extracted {} files", files.len()); - assert!(files.len() > 100, "{editor}: expected a populated archive"); - - // CommonEvent.dat was LZ-compressed: parse + round-trip proves keycode-LZSS + key decode. - let ce = files - .iter() - .find(|(p, _)| p.replace('\\', "/").ends_with("BasicData/CommonEvent.dat")) - .unwrap_or_else(|| panic!("{editor}: CommonEvent.dat not in archive")); - let parsed = CommonEventsFile::read(&ce.1) - .unwrap_or_else(|e| panic!("{editor}: parse CommonEvent: {e}")); - assert_eq!(parsed.write(), ce.1, "{editor}: CommonEvent.dat round-trip"); - eprintln!( - " CommonEvent.dat: {} events, {} bytes, round-trip OK", - parsed.events.len(), - ce.1.len() - ); - - let mut maps = 0; - for (p, b) in &files { - if !p.to_ascii_lowercase().ends_with(".mps") { - continue; - } - let m = Map::read(b).unwrap_or_else(|e| panic!("{editor}: parse {p}: {e}")); - assert_eq!(&m.write(), b, "{editor}: map {p} round-trip"); - maps += 1; - } - eprintln!(" maps round-tripped byte-exact: {maps}"); - assert!(maps > 0, "{editor}: expected maps"); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/Cargo.toml b/util/wolfdawn-master/crates/wolf-gui/Cargo.toml deleted file mode 100644 index a0df4c7..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[package] -name = "wolf-gui" -edition.workspace = true -version.workspace = true -license.workspace = true -description = "WolfDawn Studio - the desktop GUI front-end for the WolfDawn toolchain (egui/eframe)." - -[[bin]] -name = "wolf-gui" -path = "src/main.rs" - -[dependencies] -# Native, immediate-mode GUI. eframe re-exports the matching egui; rfd gives native -# OS file dialogs. All three are pure-safe Rust (the `#![forbid(unsafe_code)]` lint at -# the crate root is satisfied - the unsafe lives inside the dependencies, not here). -# `persistence` enables eframe's on-disk key/value storage (RON under the OS config dir) plus the -# `eframe::get_value`/`set_value` helpers + the `App::save` hook, which the Settings section uses to -# persist preferences across runs. It pulls in serde-backed storage; still pure-safe Rust. -eframe = { version = "0.29", default-features = false, features = ["default_fonts", "glow", "persistence"] } -egui = "0.29" -# `TableBuilder` (the translation grid) lives in the base crate; no image/svg/http loaders needed. -egui_extras = "0.29" -rfd = "0.15" - -# Global allocator. The default Windows allocator holds freed pages, so RAM stayed high after a big -# extract / decompile / DB load even once the data was dropped. mimalloc returns freed memory to the -# OS promptly (and allocates faster), which keeps the long-running GUI light. Its unsafe lives inside -# the dependency, so the crate root's `#![forbid(unsafe_code)]` still holds. -mimalloc = "0.1" - -# JSON is the lingua franca of the translation pipeline: the library emits the per-file extract -# shapes as JSON, and the GUI parses them into its editable row model (and back) with serde_json. -serde_json = "1" -# The persisted `Settings` payload is a plain serde-derive struct (stored via eframe storage). -serde = { version = "1", features = ["derive"] } - -# The WolfDawn libraries the GUI drives directly (no shelling out to the CLI). -wolf-core = { path = "../wolf-core" } -wolf-formats = { path = "../wolf-formats" } -wolf-decompiler = { path = "../wolf-decompiler" } -wolf-archive = { path = "../wolf-archive" } - -[dev-dependencies] -# The Saves section's round-trip test re-encodes baked strings to skip ones a Shift-JIS save can't -# represent, matching the library's own encoding guard. -encoding_rs = "0.8" diff --git a/util/wolfdawn-master/crates/wolf-gui/src/app.rs b/util/wolfdawn-master/crates/wolf-gui/src/app.rs deleted file mode 100644 index b4988fb..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/app.rs +++ /dev/null @@ -1,4533 +0,0 @@ -//! `WolfDawnApp`, the application shell: state, the top/side/bottom/central panel layout, and -//! the per-section dispatch. Phase 0 ships the Project section (which reads real `Game.dat`) and -//! stubs every later section so the full navigation shape is visible from day one. - -use std::path::{Path, PathBuf}; -use std::sync::mpsc::{Receiver, Sender}; -use std::time::{Duration, Instant}; - -use serde::{Deserialize, Serialize}; - -use crate::archive::{self, CryptSource, PackFormat, PackOptions}; -use crate::database::{self, DbModel}; -use crate::decompile; -use crate::gamedat::{self, Group, LoadedGameDat}; -use crate::log::Log; -use crate::project::{self, NoteLevel, Project}; -use crate::saves::{self, LoadedSave}; -use crate::task::JobManager; -use crate::translation::{ - self, Conflict, FileKind, InjectTarget, TranslationModel, -}; -use crate::verify::{self, CorpusOutcome, Verdict}; -use crate::widgets; -use wolf_decompiler::{InjectOptions, SaveFormat}; - -/// The top-level navigation sections. Each later section plugs its real UI in where the stub is. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Section { - Project, - Archive, - Decompile, - Database, - GameDat, - Translation, - Saves, - Verify, - Settings, -} - -impl Section { - /// Every section, in sidebar order. Tests iterate this to exercise the dispatch exhaustively. - pub const ALL: [Section; 9] = [ - Section::Project, - Section::Archive, - Section::Decompile, - Section::Database, - Section::GameDat, - Section::Translation, - Section::Saves, - Section::Verify, - Section::Settings, - ]; - - /// The text label. - pub fn name(self) -> &'static str { - match self { - Section::Project => "Project", - Section::Archive => "Archive", - Section::Decompile => "Decompile", - Section::Database => "Database", - Section::GameDat => "Game.dat", - Section::Translation => "Translation", - Section::Saves => "Saves", - Section::Verify => "Verify", - Section::Settings => "Settings", - } - } -} - -/// Per-section UI state. Phase 0 only needs the Project section's. The rest are stubbed so the -/// fields exist for later phases to fill in (each section owns its inputs/options here). -#[derive(Default)] -struct SectionState { - archive: ArchiveState, - database: DatabaseState, - decompile: DecompileState, - gamedat: GameDatState, - translation: TranslationState, - saves: SavesState, - verify: VerifyState, - // Settings live on the app (`WolfDawnApp::settings`), not here, since they persist across runs - // and other sections read their defaults. The rest of `SectionState` is per-session scratch. -} - -/// Persisted user preferences (the Settings section). Serde-derived so the whole struct round-trips -/// through eframe's on-disk storage (see [`WolfDawnApp::new`] / [`eframe::App::save`]). Defaults -/// match the sections' historical defaults so a fresh install behaves exactly as before. -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[serde(default)] -pub struct Settings { - /// Dark vs light theme (applied via `ctx.set_visuals` on startup + when toggled). - pub dark_mode: bool, - /// Default for the Translation section's `--en-punct` flag. - pub default_en_punct: bool, - /// Default for the Translation section's `--allow-code-drift` flag. - pub default_allow_code_drift: bool, - /// Default for the "back up originals" toggles (Saves / Database / Game.dat). - pub default_backup: bool, - /// Gate for the periodic auto-detect rescan while a project is open. - pub auto_detect: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - // egui's own default visuals are dark, so default to dark to match the unchanged look. - dark_mode: true, - default_en_punct: false, - default_allow_code_drift: false, - // Every section historically defaulted its backup toggle ON. Preserve that. - default_backup: true, - // Live auto-detect on by default. The Settings toggle gates it. - auto_detect: true, - } - } -} - -/// Which Archive sub-panel is active. -#[derive(Clone, Copy, PartialEq, Eq)] -enum ArchiveMode { - Unpack, - UnpackAll, - Repack, -} - -/// Archive section scratch state: the Unpack inputs (archive + output folder) and the Repack inputs -/// (folder + output `.wolf` + the encrypt/version/like/format options that mirror the CLI `pack` -/// flags). -struct ArchiveState { - /// Which sub-panel is shown (Unpack / Repack). - mode: ArchiveMode, - - // --- Unpack --- - /// The `.wolf` archive to extract. - unpack_archive: Option, - /// Where to extract it (defaults to `/Data`). - unpack_out: Option, - - // --- Unpack all (a per-category game's whole set of .wolf files) --- - /// The folder holding the `.wolf` archives (a game's `Data` folder). - unpack_all_dir: Option, - /// Where to write the per-archive folders (defaults to the archive folder itself, in place). - unpack_all_out: Option, - - // --- Repack --- - /// The folder to pack. - repack_in: Option, - /// The output `.wolf`. - repack_out: Option, - /// Encrypt the output (`--encrypt`). - repack_encrypt: bool, - /// Where the crypt params come from when encrypting (default / typed version / like an archive). - repack_crypt: RepackCryptMode, - /// The typed cryptVersion text (parsed to a `u16`, accepts `0x14b` or decimal). - repack_version_text: String, - /// The existing `.wolf` to inherit crypto from (for the "like existing" mode). - repack_like: Option, - /// The container format (`auto` / `ver5` / `ver6`). - repack_format: PackFormat, -} - -/// Where the encrypt-repack crypto parameters come from (the small radio in the Repack panel). -#[derive(Clone, Copy, PartialEq, Eq)] -enum RepackCryptMode { - /// The CLI default cryptVersion. - Default, - /// A manually-typed cryptVersion. - Version, - /// Inherit cryptVersion + password from an existing `.wolf` (`--like`). - Like, -} - -impl Default for ArchiveState { - fn default() -> Self { - Self { - mode: ArchiveMode::Unpack, - unpack_archive: None, - unpack_out: None, - unpack_all_dir: None, - unpack_all_out: None, - repack_in: None, - repack_out: None, - repack_encrypt: false, - repack_crypt: RepackCryptMode::Default, - repack_version_text: format!("{:#x}", archive::DEFAULT_CRYPT_VERSION), - repack_like: None, - repack_format: PackFormat::Auto, - } - } -} - -/// The heavy result a background Database job hands back to the UI thread (mirrors [`GameDatResult`]): -/// the freshly-loaded (or reloaded-after-save) grid model, over the per-section side-channel. -enum DbResult { - /// A freshly loaded model replaces the current one. - Loaded(Box), -} - -/// The Database section's scratch state: the `.project` path, the loaded editable grid model, the -/// selected type + a row-name filter, the in-place-vs-output choice + a backup toggle, and the -/// side-channel a background Load/Save job hands the model back on. -struct DatabaseState { - /// The `.project` to load (its sibling `.dat` is paired by extension). - path: Option, - /// The loaded, editable grid model (None until Load runs / after Reload-clears). - loaded: Option, - /// Which type (table) is shown on the right. - selected_type: usize, - /// The row-name filter (matches the row name, case-insensitive). - filter: String, - /// Output target: in-place (`None`) or a chosen output `.project`. - out_project: Option, - /// Back up the originals before an in-place overwrite (default ON). - backup: bool, - /// Receiver for a running Load/Save job's heavy result (the loaded/reloaded model). - result_rx: Option>, -} - -impl Default for DatabaseState { - fn default() -> Self { - Self { - path: None, - loaded: None, - selected_type: 0, - filter: String::new(), - out_project: None, - backup: true, - result_rx: None, - } - } -} - -/// The heavy result a background Decompile job hands back to the UI thread (mirrors [`VerifyResult`] -/// / [`SaveResult`]): the freshly-decompiled WolfScript text for the chosen input, carried over the -/// per-section side-channel since the shared [`JobManager`] only carries progress/log/summary. -enum DecompileResult { - /// Decompiled WolfScript text for `input` (the path it came from, kept as the compile `--base`). - Text(PathBuf, String), -} - -/// The Decompile section's scratch state: the input `.mps`/`CommonEvent.dat`, the editable WolfScript -/// buffer + the base it was decompiled from (needed as the compile `--base` so unknown bytes are -/// preserved), the chosen compile output, and the side-channel a background job hands its text back on. -struct DecompileState { - /// The binary event file to decompile (a `.mps` map or `CommonEvent.dat`). - input: Option, - /// The editable WolfScript document (empty until Decompile / Open .wscript runs). - script: String, - /// The original file the current script was decompiled from, the compile `--base`. Set on a - /// successful Decompile. Required before Compile (Open .wscript leaves it as the picked input). - base: Option, - /// Where Compile writes the recompiled file (defaults to `.out`). - out: Option, - /// Receiver for a running Decompile job's heavy result (the WolfScript text). - result_rx: Option>, - /// The find-in-editor (Ctrl+F) bar's state. - find: FindState, -} - -impl Default for DecompileState { - fn default() -> Self { - Self { - input: None, - script: String::new(), - base: None, - out: None, - result_rx: None, - find: FindState::default(), - } - } -} - -/// The Decompile editor's find-in-text (Ctrl+F) state: whether the bar is open, the query text, the -/// case toggle, and the index of the currently-selected match (so Next/Prev step through). The match -/// list itself is recomputed each frame from the script + query (`find_matches` is linear and cheap), -/// so it never goes stale against an edited script. -#[derive(Default)] -struct FindState { - /// Whether the find bar is shown (toggled by Ctrl+F / the Find button / the close button). - open: bool, - /// The search query. - query: String, - /// Case-sensitive matching (off = case-insensitive, the default). - case_sensitive: bool, - /// Which match is "current" (0-based) for the counter + scroll-to. Clamped to the match count. - current: usize, - /// Set when the bar was just opened, so the query field grabs focus for one frame. - focus_query: bool, - /// A pending match (char range) to select + scroll the editor to on this frame's `TextEdit`. - pending_select: Option<(usize, usize)>, -} - -/// The heavy result a background Game.dat job hands back to the UI thread: the freshly-loaded (or -/// reloaded-after-save) editable model, over the per-section side-channel. -enum GameDatResult { - /// A freshly loaded model replaces the current one. - Loaded(Box), -} - -/// The Game.dat section's scratch state: the `Game.dat` path, the loaded editable form model, the -/// in-place backup toggle, and the side-channel a background Load/Save job hands the model back on. -struct GameDatState { - /// The `Game.dat` to load (defaults to the open project's `game_dat`). - path: Option, - /// The loaded, editable form model (None until Load runs / after Reload-clears). - loaded: Option, - /// Back up the original before an in-place overwrite (default ON). - backup: bool, - /// Receiver for a running Load/Save job's heavy result (the loaded/reloaded model). - result_rx: Option>, -} - -impl Default for GameDatState { - fn default() -> Self { - Self { - path: None, - loaded: None, - backup: true, - result_rx: None, - } - } -} - -/// The Verify section's scratch state: the single-file pick + its last verdict, and the corpus -/// folder + its last per-file results. A background job hands results back via `result_rx`. -struct VerifyState { - /// The single data file to round-trip. - file: Option, - /// The last single-file verdict (shown in the result panel until re-run/cleared). - file_verdict: Option<(PathBuf, Verdict)>, - /// The folder to verify as a corpus. - corpus_dir: Option, - /// The last corpus run's per-file rows + summary. - corpus: Option, - /// Receiver for a running job's heavy result (a single verdict or a whole corpus outcome). - result_rx: Option>, -} - -impl Default for VerifyState { - fn default() -> Self { - Self { - file: None, - file_verdict: None, - corpus_dir: None, - corpus: None, - result_rx: None, - } - } -} - -/// The heavy result a background Verify job hands back to the UI thread (mirrors [`TrResult`] / -/// [`SaveResult`]): a single file's verdict, or a whole corpus outcome. -enum VerifyResult { - /// A single file's round-trip verdict, with the file it was run on. - Single(PathBuf, Box), - /// A whole corpus run's outcome. - Corpus(Box), -} - -/// A heavy result a background translation job hands back to the UI thread. The shared -/// [`JobManager`] only carries progress/log/summary. This side-channel carries the actual model the -/// job built so the UI can swap it in once the job lands. -enum TrResult { - /// A freshly extracted (or merged-into) model replaces the current one. - Model(Box), - /// The name-conflict check's findings, to render in the conflicts panel. - Conflicts(Vec), -} - -/// The Translation section's scratch state: the source folder, the loaded editable model, the grid -/// controls (selection / filter / counters), the inject options + output choice, and the -/// side-channel a background job uses to hand a rebuilt model back to the UI. -struct TranslationState { - /// The data dir to extract from (defaults to the open project's `data_dir`). - source_dir: Option, - /// The loaded editable model (None until Extract / Load runs). - model: Option, - /// Which left-list entry is selected: `None` = the names glossary, `Some(i)` = files\[i\]. - selected: SelectedEntry, - /// The grid's text filter (matches source or translation, case-insensitive). - filter: String, - /// Show only rows that are still untranslated. - untranslated_only: bool, - /// `--en-punct`: normalize Japanese punctuation to ASCII on inject. - en_punct: bool, - /// `--allow-code-drift`: relax the inline-code preservation guard on inject. - allow_code_drift: bool, - /// Output target: in-place when `None`, else a chosen output dir. - out_dir: Option, - /// The last name-conflict check's findings (shown in a small panel until re-run/cleared). - conflicts: Vec, - /// Receiver for a running job's heavy result (the rebuilt model / conflicts). - result_rx: Option>, -} - -/// Which left-list entry the grid is showing. -#[derive(Clone, Copy, PartialEq, Eq)] -enum SelectedEntry { - Names, - File(usize), -} - -impl Default for TranslationState { - fn default() -> Self { - Self { - source_dir: None, - model: None, - selected: SelectedEntry::Names, - filter: String::new(), - untranslated_only: false, - en_punct: false, - allow_code_drift: false, - out_dir: None, - conflicts: Vec::new(), - result_rx: None, - } - } -} - -/// The heavy result a background Saves job hands back to the UI thread, mirroring [`TrResult`]: the -/// shared [`JobManager`] only carries progress/log/summary, so the freshly-inspected save (or the -/// re-inspect after a write) comes back over this per-section side-channel. -enum SaveResult { - /// A freshly inspected (or re-inspected) save replaces the loaded one. - Loaded(Box), -} - -/// Which source the batch title comes from. -#[derive(Clone, Copy, PartialEq, Eq)] -enum BatchTitleMode { - /// No title change in the batch (only refresh strings). - None, - /// A literally-typed title. - Typed, - /// The title read from a chosen (translated) `Game.dat`. - FromGameDat, -} - -/// The Saves section's scratch state: the single-file open inputs + the loaded/editable save, and -/// the batch-update inputs. A background job hands a (re-)inspected save back via `result_rx`. -struct SavesState { - /// The `.sav` the user picked to inspect (single-file flow). - sav_path: Option, - /// The loaded, editable save (None until Inspect runs / after a clear). - loaded: Option, - /// The string-table filter (matches original or edited, case-insensitive). - filter: String, - /// Back up the original before writing (default ON), for both single + batch flows. - backup: bool, - /// Receiver for a running single-file job's heavy result (the inspected save). - result_rx: Option>, - - // --- batch flow --- - /// The save folder to batch-update. - batch_dir: Option, - /// Where the batch title comes from. - batch_title_mode: BatchTitleMode, - /// The typed batch title (used when `batch_title_mode == Typed`). - batch_title: String, - /// The translated `Game.dat` to read a title from (used when `batch_title_mode == FromGameDat`). - batch_game_dat: Option, - /// Optional translation file/dir(s) to refresh baked strings from in the batch. - batch_translations: Option, -} - -impl Default for SavesState { - fn default() -> Self { - Self { - sav_path: None, - loaded: None, - filter: String::new(), - backup: true, - result_rx: None, - batch_dir: None, - batch_title_mode: BatchTitleMode::None, - batch_title: String::new(), - batch_game_dat: None, - batch_translations: None, - } - } -} - -/// The application. `eframe` calls [`eframe::App::update`] each frame. -pub struct WolfDawnApp { - /// The active navigation section. - section: Section, - /// The open game, if any. - project: Option, - /// The scrolling activity log (bottom panel). - log: Log, - /// Background-job manager (progress bar + worker threads). - jobs: JobManager, - /// Per-section UI state. - state: SectionState, - /// Persisted user preferences (theme + section defaults + auto-detect gate). - settings: Settings, - /// When the open project's file index was last re-walked, for throttling the live auto-detect - /// in [`render_frame`](Self::render_frame). `None` until the first tick (or while no project). - last_scan: Option, - /// The output dir of an in-flight Unpack job, if any. Set when an Unpack starts. Consumed when - /// it finishes to immediately rescan the index (and adopt the dir as `data_dir` when it sits - /// under the project root) so the freshly-extracted files show in the dropdowns at once. - pending_unpack_out: Option, - /// Whether a job was running on the previous frame, so the per-frame tick can detect the - /// running→idle transition (job-just-finished) without a completion callback in `JobManager`. - job_was_running: bool, -} - -impl Default for WolfDawnApp { - fn default() -> Self { - let mut log = Log::new(); - log.info("WolfDawn Studio ready. Open a game from the Project section to begin."); - Self { - section: Section::Project, - project: None, - log, - jobs: JobManager::new(), - state: SectionState::default(), - settings: Settings::default(), - last_scan: None, - pending_unpack_out: None, - job_was_running: false, - } - } -} - -impl WolfDawnApp { - /// The eframe storage key the persisted [`Settings`] live under. - const SETTINGS_KEY: &'static str = "settings"; - - /// Construct the app for an eframe creation context. Installs a Japanese fallback font so CJK - /// text renders instead of tofu, loads the persisted [`Settings`] - /// (theme + defaults) from eframe storage, applies the theme, and seeds the sections' option - /// flags from the saved defaults so a returning user gets their preferences on startup. - pub fn new(cc: &eframe::CreationContext<'_>) -> Self { - install_cjk_font(&cc.egui_ctx); - let mut app = Self::default(); - // Restore the saved settings (falls back to defaults the first run / if storage is empty). - if let Some(storage) = cc.storage { - if let Some(saved) = eframe::get_value::(storage, Self::SETTINGS_KEY) { - app.settings = saved; - } - } - app.apply_theme(&cc.egui_ctx); - app.seed_section_defaults(); - app - } - - /// Apply the current theme preference to the egui context (called on startup + on every toggle). - fn apply_theme(&self, ctx: &egui::Context) { - ctx.set_visuals(if self.settings.dark_mode { - egui::Visuals::dark() - } else { - egui::Visuals::light() - }); - } - - /// Seed the sections' option flags from the persisted [`Settings`] defaults. Called once on - /// startup (and after a "reset" could be added later); sections then own those flags for the - /// session, so toggling a section checkbox is never clobbered by the settings default. - fn seed_section_defaults(&mut self) { - self.state.translation.en_punct = self.settings.default_en_punct; - self.state.translation.allow_code_drift = self.settings.default_allow_code_drift; - self.state.saves.backup = self.settings.default_backup; - self.state.database.backup = self.settings.default_backup; - self.state.gamedat.backup = self.settings.default_backup; - } - - // --- the section dispatch ------------------------------------------------------------- - - /// Render the body of the currently-selected section into `ui`. Public so the headless smoke - /// test can drive each section without a window. - pub fn render_current_section(&mut self, ui: &mut egui::Ui) { - match self.section { - Section::Project => self.ui_project(ui), - Section::Archive => self.ui_archive(ui), - Section::Decompile => self.ui_decompile(ui), - Section::Database => self.ui_database(ui), - Section::GameDat => self.ui_gamedat(ui), - Section::Translation => self.ui_translation(ui), - Section::Saves => self.ui_saves(ui), - Section::Verify => self.ui_verify(ui), - Section::Settings => self.ui_settings(ui), - } - } - - /// Switch the active section. The sidebar sets it inline. This is the programmatic entry the - /// smoke tests use to walk every section. - #[allow(dead_code)] - pub fn set_section(&mut self, section: Section) { - self.section = section; - } - - /// Install a translation model into the Translation section (test-only entry so the headless - /// render smoke test can exercise the grid with a loaded model, not just the empty state). - #[cfg(test)] - pub fn set_translation_model(&mut self, model: TranslationModel) { - self.state.translation.selected = if model.files.is_empty() { - SelectedEntry::Names - } else { - SelectedEntry::File(0) - }; - self.state.translation.model = Some(model); - } - - /// Install a loaded save into the Saves section (test-only entry so the headless render smoke - /// test can exercise the save editor + string table with a real loaded save). - #[cfg(test)] - pub fn set_loaded_save(&mut self, loaded: LoadedSave) { - self.state.saves.loaded = Some(loaded); - } - - /// Install WolfScript + its base into the Decompile section (test-only entry so the headless - /// render smoke test can exercise the code editor + Compile controls with real content). - #[cfg(test)] - pub fn set_decompile_script(&mut self, input: PathBuf, script: String) { - self.state.decompile.base = Some(input.clone()); - self.state.decompile.input = Some(input); - self.state.decompile.script = script; - } - - /// Open the Decompile find bar with `query` (test-only entry so the headless render smoke test - /// can exercise the find bar + a query on a loaded script without panicking). - #[cfg(test)] - pub fn open_decompile_find(&mut self, query: &str) { - self.state.decompile.find.open = true; - self.state.decompile.find.query = query.to_string(); - // Trigger an initial jump-to-first-match, the way a fresh query would. - let matches = decompile::find_matches( - &self.state.decompile.script, - query, - self.state.decompile.find.case_sensitive, - ); - if let Some(m) = matches.first() { - self.state.decompile.find.pending_select = Some((m.start, m.end)); - } - } - - /// Install a loaded Game.dat model into the Game.dat section (test-only entry so the headless - /// render smoke test can exercise the field form with a real loaded file). - #[cfg(test)] - pub fn set_gamedat_loaded(&mut self, loaded: LoadedGameDat) { - self.state.gamedat.loaded = Some(loaded); - } - - /// Install a loaded database model into the Database section (test-only entry so the headless - /// render smoke test can exercise the grid with a real loaded DB). - #[cfg(test)] - pub fn set_database_loaded(&mut self, loaded: DbModel) { - self.state.database.loaded = Some(loaded); - } - - // --- Unload / clear (free a section's loaded data so the RAM returns to the OS) ------------- - // Dropping the model frees its memory; with the mimalloc allocator that memory is handed back to - // the OS, so unloading a file you are done with actually lowers the process footprint. Paths are - // kept so the file can be reloaded with one click. - - /// Drop the loaded database grid (its rows/cells). Discards unsaved edits. - fn unload_database(&mut self) { - self.state.database.loaded = None; - self.state.database.selected_type = 0; - self.state.database.filter.clear(); - } - - /// Drop the decompiled script (and its editor layout). Discards unsaved edits. - fn clear_decompile(&mut self) { - self.state.decompile.script = String::new(); - self.state.decompile.find = FindState::default(); - } - - /// Drop the loaded translation model (every file's rows + docs + the glossary). Discards unsaved - /// edits. The source folder is kept so Extract can rebuild it. - fn unload_translation(&mut self) { - self.state.translation.model = None; - self.state.translation.conflicts.clear(); - self.state.translation.selected = SelectedEntry::Names; - self.state.translation.filter.clear(); - } - - /// Drop the loaded Game.dat model. Discards unsaved edits. - fn unload_gamedat(&mut self) { - self.state.gamedat.loaded = None; - } - - /// Drop the loaded save. Discards unsaved edits. - fn unload_save(&mut self) { - self.state.saves.loaded = None; - } - - /// Install an open project (test-only entry so the headless render smoke test can exercise the - /// per-section dropdowns + the Settings section with a project loaded). - #[cfg(test)] - pub fn set_project(&mut self, project: Project) { - self.project = Some(project); - } - - // --- Project section ------------------------------------------------------------------ - - fn ui_project(&mut self, ui: &mut egui::Ui) { - ui.heading("Project"); - widgets::caption( - ui, - "Open a Wolf RPG game to read its Game.dat and locate Data.wolf / the unpacked Data folder.", - ); - ui.add_space(8.0); - - ui.horizontal(|ui| { - if ui - .button("Open Game…") - .on_hover_text("Pick the game folder, or a Data.wolf / Game.dat file.") - .clicked() - { - if let Some(picked) = widgets::pick_folder() { - self.open_from(picked); - } - } - if ui - .button("Open File…") - .on_hover_text("Pick a Data.wolf archive or a Game.dat directly.") - .clicked() - { - if let Some(picked) = widgets::pick_file(&[ - ("Wolf archive / Game data", &["wolf", "dat"]), - ("All files", &["*"]), - ]) { - self.open_from(picked); - } - } - if self.project.is_some() { - if ui - .button("Refresh") - .on_hover_text("Re-scan the game's folders now for newly-added maps / databases / saves.") - .clicked() - { - self.refresh_project_index(); - } - if ui.button("Close").on_hover_text("Close the open game.").clicked() { - self.project = None; - self.last_scan = None; - self.log.info("closed project"); - } - } - }); - - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); - - match &self.project { - None => { - widgets::caption(ui, "No project open."); - } - Some(p) => project_details(ui, p), - } - } - - /// Open a project from a picked path (synchronous, Game.dat parsing is fast). Routes the - /// outcome notes into the log. - fn open_from(&mut self, picked: PathBuf) { - self.log.info(format!("opening {}…", picked.display())); - let outcome = project::open_project(&picked); - for (level, msg) in outcome.notes { - match level { - NoteLevel::Info => self.log.info(msg), - NoteLevel::Warn => self.log.warn(msg), - } - } - self.project = Some(outcome.project); - // A fresh open resets the auto-detect clock so the next tick rescans promptly. - self.last_scan = None; - } - - /// Manually re-scan the open project's index now (the Project section's Refresh button), - /// logging whether anything changed. - fn refresh_project_index(&mut self) { - let Some(project) = self.project.as_mut() else { - return; - }; - if project.rescan() { - self.log.info(format!( - "refreshed: {} code file(s), {} database(s), {} save(s)", - project.code_files.len(), - project.databases.len(), - project.saves.len(), - )); - } else { - self.log.info("refreshed: no changes"); - } - self.last_scan = Some(Instant::now()); - } - - // --- Archive section: unpack a .wolf / repack a folder --------------------------------- - - fn ui_archive(&mut self, ui: &mut egui::Ui) { - ui.heading("Archive"); - widgets::caption( - ui, - "Unpack Data.wolf to an editable folder, or repack a folder back into a Data.wolf.", - ); - widgets::caption( - ui, - "Most workflows run on the loose, unpacked Data folder; encryption/version only matter \ - if you are shipping an encrypted archive.", - ); - ui.add_space(6.0); - - // Seed the unpack archive + the repack folder from the open project the first time we show. - self.archive_default_paths(); - - // Sub-panel selector (Unpack / Unpack all / Repack). - ui.horizontal(|ui| { - ui.selectable_value(&mut self.state.archive.mode, ArchiveMode::Unpack, "Unpack") - .on_hover_text("Extract one Data.wolf to an editable folder."); - ui.selectable_value(&mut self.state.archive.mode, ArchiveMode::UnpackAll, "Unpack all") - .on_hover_text( - "Some games split their data across many .wolf files (BasicData.wolf, \ - MapData.wolf, Evtext.wolf, ...). This unpacks every archive in a folder at once.", - ); - ui.selectable_value(&mut self.state.archive.mode, ArchiveMode::Repack, "Repack") - .on_hover_text("Rebuild Data.wolf from a folder."); - }); - ui.add_space(6.0); - ui.separator(); - ui.add_space(6.0); - - match self.state.archive.mode { - ArchiveMode::Unpack => self.archive_unpack_pane(ui), - ArchiveMode::UnpackAll => self.archive_unpack_all_pane(ui), - ArchiveMode::Repack => self.archive_repack_pane(ui), - } - } - - /// Seed the unpack archive + the repack input folder from the open project (its `data_wolf` / - /// `root`), once, so a project user has sensible defaults. - fn archive_default_paths(&mut self) { - if self.state.archive.unpack_archive.is_none() { - if let Some(p) = self.project.as_ref().and_then(|p| p.data_wolf.clone()) { - self.state.archive.unpack_archive = Some(p); - } - } - if self.state.archive.repack_in.is_none() { - if let Some(d) = self.project.as_ref().and_then(|p| p.data_dir.clone()) { - self.state.archive.repack_in = Some(d); - } - } - // Unpack-all defaults to the folder holding the archives: the project data dir, else the - // folder the single Data.wolf sits in. - if self.state.archive.unpack_all_dir.is_none() { - let guess = self - .project - .as_ref() - .and_then(|p| p.data_dir.clone()) - .or_else(|| { - self.project - .as_ref() - .and_then(|p| p.data_wolf.as_ref()) - .and_then(|w| w.parent().map(std::path::Path::to_path_buf)) - }); - if let Some(d) = guess { - self.state.archive.unpack_all_dir = Some(d); - } - } - } - - /// The Unpack pane: pick a `.wolf` + an output folder (default `/Data`), then Unpack. - fn archive_unpack_pane(&mut self, ui: &mut egui::Ui) { - let mut archive = self.state.archive.unpack_archive.clone(); - let changed = widgets::path_field( - ui, - "Archive", - &mut archive, - "Unpack = extract Data.wolf to an editable folder. Pick the .wolf to extract.", - || widgets::pick_file(&[("Wolf archive", &["wolf"]), ("All files", &["*"])]), - ); - self.state.archive.unpack_archive = archive; - // Default the output folder to /Data when a fresh archive is picked (or first show). - if (changed || self.state.archive.unpack_out.is_none()) - && self.state.archive.unpack_archive.is_some() - { - if let Some(arc) = &self.state.archive.unpack_archive { - let default_out = arc.parent().map(|d| d.join("Data")); - if self.state.archive.unpack_out.is_none() || changed { - self.state.archive.unpack_out = default_out; - } - } - } - - let start = self.state.archive.unpack_out.clone(); - let mut out = self.state.archive.unpack_out.clone(); - widgets::path_field( - ui, - "Output folder", - &mut out, - "Where to extract the archive's files (its inner folder tree is preserved).", - move || widgets::pick_folder_in(start.as_deref()), - ); - self.state.archive.unpack_out = out; - - ui.add_space(8.0); - let running = self.jobs.is_running(); - let ready = self.state.archive.unpack_archive.is_some() - && self.state.archive.unpack_out.is_some(); - ui.add_enabled_ui(ready && !running, |ui| { - if ui - .button("Unpack ▶") - .on_hover_text("Extract every file from the archive into the output folder on a background thread.") - .clicked() - { - self.start_unpack_job(); - } - }); - if !ready { - widgets::caption(ui, "Pick a .wolf archive and an output folder to enable Unpack."); - } - } - - /// The Unpack-all pane: pick the folder of `.wolf` archives and an output root, then unpack - /// every archive at once. Each `Name.wolf` becomes a `Name/` folder under the output, the loose - /// per-category tree the game loads and the Translation section can read. - fn archive_unpack_all_pane(&mut self, ui: &mut egui::Ui) { - let start_dir = self.state.archive.unpack_all_dir.clone(); - let mut dir = self.state.archive.unpack_all_dir.clone(); - let changed = widgets::path_field( - ui, - "Archives folder", - &mut dir, - "The folder holding the game's .wolf archives (its Data folder). Every .wolf in it is unpacked.", - move || widgets::pick_folder_in(start_dir.as_deref()), - ); - self.state.archive.unpack_all_dir = dir; - // Default the output root to the archives folder itself (unpack in place, the layout the - // game loads), refreshed when a new folder is picked. - if (changed || self.state.archive.unpack_all_out.is_none()) - && self.state.archive.unpack_all_dir.is_some() - { - self.state.archive.unpack_all_out = self.state.archive.unpack_all_dir.clone(); - } - - let start_out = self.state.archive.unpack_all_out.clone(); - let mut out = self.state.archive.unpack_all_out.clone(); - widgets::path_field( - ui, - "Output root", - &mut out, - "Where the per-archive folders are written. Defaults to the archives folder, so the \ - loose files sit beside the .wolf and the game loads them.", - move || widgets::pick_folder_in(start_out.as_deref()), - ); - self.state.archive.unpack_all_out = out; - - ui.add_space(8.0); - let running = self.jobs.is_running(); - let ready = self.state.archive.unpack_all_dir.is_some() - && self.state.archive.unpack_all_out.is_some(); - ui.add_enabled_ui(ready && !running, |ui| { - if ui - .button("Unpack all ▶") - .on_hover_text("Unpack every .wolf in the folder, each into its own subfolder, on a background thread.") - .clicked() - { - self.start_unpack_all_job(); - } - }); - if !ready { - widgets::caption(ui, "Pick the folder of .wolf archives and an output root to enable Unpack all."); - } - } - - /// The Repack pane: pick a folder + output `.wolf`, set the encrypt/version/like/format options - /// (mirroring the CLI `pack` flags), then Repack. - fn archive_repack_pane(&mut self, ui: &mut egui::Ui) { - let start_in = self.state.archive.repack_in.clone(); - let mut input = self.state.archive.repack_in.clone(); - widgets::path_field( - ui, - "Input folder", - &mut input, - "Repack = rebuild Data.wolf from a folder. Pick the folder of loose game files to pack.", - move || widgets::pick_folder_in(start_in.as_deref()), - ); - self.state.archive.repack_in = input; - - let mut out = self.state.archive.repack_out.clone(); - widgets::path_field( - ui, - "Output .wolf", - &mut out, - "Where to write the rebuilt archive.", - || widgets::save_file("Data.wolf", &[("Wolf archive", &["wolf"])]), - ); - self.state.archive.repack_out = out; - - ui.add_space(8.0); - // Format dropdown (auto / ver5 / ver6) in a shared form row so its label lines up with - // the path rows above. - widgets::form_row( - ui, - "Format", - "auto = the modern Data.wolf container. ver5/ver6 are legacy DXArchive containers for \ - old Wolf 2.0x games - leave on auto unless you specifically need them.", - |ui| { - egui::ComboBox::from_id_salt("repack_format") - .selected_text(self.state.archive.repack_format.label()) - .show_ui(ui, |ui| { - for f in [PackFormat::Auto, PackFormat::Ver5, PackFormat::Ver6] { - ui.selectable_value(&mut self.state.archive.repack_format, f, f.label()); - } - }); - }, - ); - - // Encryption is only meaningful for the modern (auto) container. - let auto = self.state.archive.repack_format == PackFormat::Auto; - ui.add_enabled_ui(auto, |ui| { - widgets::labeled_checkbox( - ui, - &mut self.state.archive.repack_encrypt, - "Encrypt", - "Encrypt the output archive. Usually only needed when shipping an encrypted \ - Data.wolf; most workflows leave this off and run the game on a loose Data folder.", - ); - }); - - // The crypt-source radio + its inputs, shown only when encrypting an auto archive. - if auto && self.state.archive.repack_encrypt { - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.radio_value( - &mut self.state.archive.repack_crypt, - RepackCryptMode::Default, - "default version", - ) - .on_hover_text("Use the standard default cryptVersion."); - ui.radio_value( - &mut self.state.archive.repack_crypt, - RepackCryptMode::Version, - "match version", - ) - .on_hover_text("Type a specific cryptVersion (e.g. 0x14b)."); - ui.radio_value( - &mut self.state.archive.repack_crypt, - RepackCryptMode::Like, - "like existing .wolf", - ) - .on_hover_text("Inherit the cryptVersion + embedded password from an original archive (turnkey repack)."); - }); - match self.state.archive.repack_crypt { - RepackCryptMode::Default => {} - RepackCryptMode::Version => { - widgets::form_row(ui, "Version", "", |ui| { - ui.add( - egui::TextEdit::singleline(&mut self.state.archive.repack_version_text) - .hint_text("0x14b") - .desired_width(120.0), - ) - .on_hover_text("A cryptVersion as hex (0x14b) or decimal."); - }); - if parse_crypt_version(&self.state.archive.repack_version_text).is_none() { - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - "Warning: not a valid version number", - ); - } - } - RepackCryptMode::Like => { - let mut like = self.state.archive.repack_like.clone(); - widgets::path_field( - ui, - "Original .wolf", - &mut like, - "An existing encrypted archive to copy the crypto from.", - || widgets::pick_file(&[("Wolf archive", &["wolf"]), ("All files", &["*"])]), - ); - self.state.archive.repack_like = like; - } - } - } - - ui.add_space(8.0); - let running = self.jobs.is_running(); - let ready = self.repack_ready(); - ui.add_enabled_ui(ready && !running, |ui| { - if ui - .button("Repack ▶") - .on_hover_text("Rebuild the archive from the folder on a background thread.") - .clicked() - { - self.start_repack_job(); - } - }); - if !ready { - widgets::caption( - ui, - "Pick an input folder and an output .wolf (and, for \"like\"/\"match\", a valid \ - source/version) to enable Repack.", - ); - } - } - - /// True when the Repack inputs are complete enough to run (folder + output, and a resolvable - /// crypt source when encrypting). - fn repack_ready(&self) -> bool { - let a = &self.state.archive; - if a.repack_in.is_none() || a.repack_out.is_none() { - return false; - } - if a.repack_format == PackFormat::Auto && a.repack_encrypt { - return match a.repack_crypt { - RepackCryptMode::Default => true, - RepackCryptMode::Version => parse_crypt_version(&a.repack_version_text).is_some(), - RepackCryptMode::Like => a.repack_like.is_some(), - }; - } - true - } - - /// Spawn the Unpack job: extract the archive into the output folder on a worker thread, reusing - /// the library's `extract_archive` via [`archive::unpack`]. Bad/odd filenames are sanitised and - /// reported, never written outside the output folder. - fn start_unpack_job(&mut self) { - let Some(arc) = self.state.archive.unpack_archive.clone() else { - return; - }; - let Some(out) = self.state.archive.unpack_out.clone() else { - return; - }; - // Remember the output dir so the running→idle transition can rescan it (and adopt it as the - // project's data_dir when it sits under the project root) the moment the job finishes. - self.pending_unpack_out = Some(out.clone()); - self.jobs.run(format!("Unpack {}", arc.display()), &mut self.log, move |rep| { - let outcome = archive::unpack(&arc, &out, |frac, msg| rep.progress(frac, msg))?; - for (orig, safe) in &outcome.sanitised { - if safe.is_empty() { - rep.warn(format!("skipped odd archive entry {orig:?} (no safe path)")); - } else { - rep.warn(format!("sanitised archive path {orig:?} → {safe:?}")); - } - } - rep.info(format!("{} file(s) → {}", outcome.written, outcome.out_dir.display())); - Ok(format!("{} file(s) → {}", outcome.written, outcome.out_dir.display())) - }); - } - - /// Spawn the Unpack-all job: unpack every `.wolf` in the chosen folder into per-archive - /// subfolders on a worker thread, reusing [`archive::unpack_all`]. Each archive's result is - /// logged, and the output root is rescanned when the job finishes (so new files show up). - fn start_unpack_all_job(&mut self) { - let Some(dir) = self.state.archive.unpack_all_dir.clone() else { - return; - }; - let Some(out) = self.state.archive.unpack_all_out.clone() else { - return; - }; - self.pending_unpack_out = Some(out.clone()); - self.jobs.run(format!("Unpack all in {}", dir.display()), &mut self.log, move |rep| { - let outcome = archive::unpack_all(&dir, &out, |frac, msg| rep.progress(frac, msg))?; - for (name, res) in &outcome.details { - match res { - Ok(n) => rep.info(format!("{name} → {n} file(s)")), - Err(e) => rep.warn(format!("{name} failed: {e}")), - } - } - let summary = if outcome.failed > 0 { - format!( - "{} archive(s), {} file(s) → {} ({} failed)", - outcome.ok, - outcome.files, - outcome.out_root.display(), - outcome.failed - ) - } else { - format!( - "{} archive(s), {} file(s) → {}", - outcome.ok, - outcome.files, - outcome.out_root.display() - ) - }; - Ok(summary) - }); - } - - /// Spawn the Repack job: gather the folder + pack it with the chosen options on a worker thread, - /// reusing the library `pack_*` paths via [`archive::repack`]. - fn start_repack_job(&mut self) { - let a = &self.state.archive; - let Some(input) = a.repack_in.clone() else { - return; - }; - let Some(out) = a.repack_out.clone() else { - return; - }; - // Assemble the PackOptions from the UI (resolve the crypt source). - let crypt = match a.repack_crypt { - RepackCryptMode::Default => CryptSource::Default, - RepackCryptMode::Version => match parse_crypt_version(&a.repack_version_text) { - Some(v) => CryptSource::Version(v), - None => { - self.log.error("repack: invalid cryptVersion"); - return; - } - }, - RepackCryptMode::Like => match a.repack_like.clone() { - Some(p) => CryptSource::Like(p), - None => { - self.log.error("repack: pick an original .wolf for \"like existing\""); - return; - } - }, - }; - let opts = PackOptions { - encrypt: a.repack_encrypt, - crypt, - format: a.repack_format, - }; - self.jobs.run(format!("Repack → {}", out.display()), &mut self.log, move |rep| { - let outcome = archive::repack(&input, &out, &opts, |frac, msg| rep.progress(frac, msg))?; - Ok(format!( - "{} file(s) → {} ({} bytes, {})", - outcome.files, - outcome.out.display(), - outcome.bytes, - outcome.mode - )) - }); - } - - // --- stubbed sections ----------------------------------------------------------------- - - fn ui_decompile(&mut self, ui: &mut egui::Ui) { - // Pump any background result (the freshly-decompiled WolfScript) into section state first, so - // the rest of this frame renders what the just-finished job produced. - self.poll_decompile_result(); - - ui.heading("Decompile"); - widgets::caption(ui, "Render maps and common events to readable, editable WolfScript - and recompile byte-exact."); - ui.add_space(6.0); - - // Ctrl+F toggles the find bar (only meaningful once a script is loaded). Consuming the - // shortcut here keeps it working from anywhere in the section. - let have_script = !self.state.decompile.script.is_empty(); - if have_script - && ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::F)) - { - let f = &mut self.state.decompile.find; - f.open = true; - f.focus_query = true; // grab the query field next frame so the user can type at once. - } - - // Default the input to the open project's data dir (no file pre-selected) the first time. - self.decompile_input_row(ui); - ui.add_space(6.0); - self.decompile_action_row(ui); - ui.add_space(6.0); - ui.separator(); - ui.add_space(6.0); - self.decompile_editor(ui); - } - - /// The input field + the Decompile button. - fn decompile_input_row(&mut self, ui: &mut egui::Ui) { - // A project dropdown of the scanned code files (MapData/*.mps + CommonEvent.dat), above the - // Browse field for files outside the project. - if let Some(code) = self.project.as_ref().map(|p| p.code_files.clone()) { - let mut input = self.state.decompile.input.clone(); - if project_combo( - ui, - "decompile_combo", - "Project file", - &code, - &mut input, - "Pick a map / CommonEvent from the open game (or use Browse below for a file outside it).", - ) { - self.state.decompile.input = input; - } - } - - // Seed the picker's start dir from the open project's data dir. Don't auto-pick a file, - // since there are many candidate .mps maps and the user chooses. - let start = self - .project - .as_ref() - .and_then(|p| p.data_dir.clone()); - let mut input = self.state.decompile.input.clone(); - widgets::path_field( - ui, - "Input", - &mut input, - "Decompile = turn a map/common-event into readable WolfScript you can edit. Pick a .mps \ - map or CommonEvent.dat.", - move || { - widgets::pick_file_in( - start.as_deref(), - &[("Wolf event file", &["mps", "dat"]), ("All files", &["*"])], - ) - }, - ); - self.state.decompile.input = input; - - // Warn early if the pick isn't a code-bearing file. Decompile still runs and gives a clear error. - if let Some(f) = &self.state.decompile.input { - if !decompile::is_supported(f) { - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - "Warning: not a decompilable file (decompile supports .mps maps and CommonEvent.dat).", - ); - } - } - } - - /// The action row: Decompile, plus Open/Save .wscript and the Compile controls. - fn decompile_action_row(&mut self, ui: &mut egui::Ui) { - let running = self.jobs.is_running(); - let have_input = self.state.decompile.input.is_some(); - ui.horizontal(|ui| { - ui.add_enabled_ui(have_input && !running, |ui| { - if ui - .button("Decompile ▶") - .on_hover_text("Decompile the input to editable WolfScript on a background thread.") - .clicked() - { - self.start_decompile_job(); - } - }); - ui.add_enabled_ui(!running, |ui| { - if ui - .button("Open .wscript…") - .on_hover_text("Load an existing WolfScript document to compile (its base is the Input above).") - .clicked() - { - self.decompile_open_script(); - } - if !self.state.decompile.script.is_empty() - && ui - .button("Save .wscript…") - .on_hover_text("Export the current WolfScript text to a .wscript file.") - .clicked() - { - self.decompile_save_script(); - } - if !self.state.decompile.script.is_empty() - && ui - .button("Clear") - .on_hover_text("Close the script and free its memory (a large file's editor layout is the heaviest thing the GUI holds). Discards unsaved edits.") - .clicked() - { - self.clear_decompile(); - } - }); - }); - - // The Compile row: output target + the Compile button (needs a script + a base). - let mut out = self.state.decompile.out.clone(); - let start = out.clone().or_else(|| self.state.decompile.input.clone()); - widgets::path_field( - ui, - "Compile to", - &mut out, - "Where Compile writes the recompiled file (defaults to .out).", - move || widgets::save_file( - &start - .as_ref() - .and_then(|p| p.file_name().and_then(|s| s.to_str())) - .map(|n| format!("{n}.out")) - .unwrap_or_else(|| "compiled.out".to_string()), - &[("Wolf event file", &["mps", "dat", "out"]), ("All files", &["*"])], - ), - ); - self.state.decompile.out = out; - - let have_script = !self.state.decompile.script.is_empty(); - let have_base = self.state.decompile.base.is_some() || self.state.decompile.input.is_some(); - ui.add_enabled_ui(have_script && have_base && !running, |ui| { - if ui - .button("Compile ▶") - .on_hover_text("Compile needs the original file as a base so unknown bytes are preserved. Recompiles the edited WolfScript onto the base.") - .clicked() - { - self.start_compile_job(); - } - }); - if !have_script { - widgets::caption(ui, "Decompile a file (or Open a .wscript) to enable Compile."); - } else if !have_base { - widgets::caption(ui, "Pick the original Input file (the compile base) to enable Compile."); - } - } - - /// The WolfScript editor: a large monospace code editor inside a scroll area, with a line count. - /// - /// Perf: egui re-lays-out the whole `TextEdit` buffer every frame, and word-wrapping is the - /// dominant cost. Wrapping forces per-glyph width measurement of every line against the viewport - /// width on each layout pass. Giving the editor an infinite desired width inside a **both-axis** - /// `ScrollArea` disables wrapping, so each line lays out at its natural width with no wrap-point - /// search (horizontal scrolling instead). For code this is also the correct behaviour (lines - /// shouldn't reflow). On a big CommonEvent (thousands of lines) this is much cheaper than a - /// vertical-only scroll that clamps the editor to the viewport width and wraps every line each - /// frame. No re-decompile or buffer re-allocation happens per frame. Only the TextEdit's own - /// galley layout runs. - fn decompile_editor(&mut self, ui: &mut egui::Ui) { - /// Scripts at/above this size get a "large file" notice (in-app edits still work). - const LARGE_BYTES: usize = 200 * 1024; - /// …or at/above this many lines. - const LARGE_LINES: usize = 5000; - - if self.state.decompile.script.is_empty() { - widgets::caption( - ui, - "No WolfScript loaded. Pick a .mps map or CommonEvent.dat and press Decompile, or \ - Open an existing .wscript.", - ); - return; - } - let lines = self.state.decompile.script.lines().count(); - let chars = self.state.decompile.script.len(); - ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("{lines} line(s) · {chars} bytes")).strong()); - if let Some(base) = &self.state.decompile.base { - ui.separator(); - ui.label( - egui::RichText::new(format!("base: {}", base.display())).weak(), - ); - } - }); - - // For very large scripts, surface the "edit externally" escape hatch. In-app edits still - // work. Save .wscript, edit in your editor, then Open .wscript to bring it back. - if chars >= LARGE_BYTES || lines >= LARGE_LINES { - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - "Warning: large script — editing stays responsive (lines don't wrap; scroll horizontally). \ - For heavy edits you can Save .wscript, edit it externally, then Open .wscript.", - ); - } - // The find-in-editor (Ctrl+F) bar sits just above the editor. It also returns the pending - // match (a char range) to select + scroll to in this frame's TextEdit. - self.decompile_find_bar(ui); - - ui.add_space(4.0); - // A generous, responsive code editor for potentially large CommonEvents. Both-axis scroll - // plus an infinite desired width disable word-wrap (the big per-frame layout cost). See the - // method doc. The CJK fallback is installed on the Monospace family (see `install_cjk_font`), - // so `.code_editor()` (which uses Monospace) renders Japanese here instead of tofu. - let pending = self.state.decompile.find.pending_select.take(); - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - let mut output = egui::TextEdit::multiline(&mut self.state.decompile.script) - .code_editor() - .desired_rows(28) - // Infinite width means no wrap-point search. Lines lay out at natural width and - // the horizontal ScrollArea handles overflow. - .desired_width(f32::INFINITY) - .show(ui); - - // If the find bar asked to jump to a match, set the editor's selection to that char - // range and scroll it into view. We set the selection on the stored state (so egui - // shows it next frame) AND scroll directly via the galley this frame (so the jump is - // immediate and reliable regardless of focus timing). egui's own cursor-scroll only - // fires when the editor is focused and the selection changed. - if let Some((start, end)) = pending { - use egui::text::{CCursor, CCursorRange}; - let range = CCursorRange::two(CCursor::new(start), CCursor::new(end)); - output.state.cursor.set_char_range(Some(range)); - output.state.clone().store(ui.ctx(), output.response.id); - // Scroll the inner ScrollArea to the match's rect (galley-local to screen coords). - let cursor_rect = output - .galley - .pos_from_ccursor(CCursor::new(start)) - .translate(output.galley_pos.to_vec2()); - ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center)); - // Focus the editor so the selection highlight is visible. - output.response.request_focus(); - } - }); - } - - /// The find-in-editor (Ctrl+F) bar: a query field, Prev/Next, a match counter, an optional - /// case toggle, and a close (×). Recomputes the match list each frame from the script + query, and - /// (on a navigation) stashes the current match's char range in `find.pending_select` for the - /// editor to select + scroll to. Renders nothing when the bar is closed. - fn decompile_find_bar(&mut self, ui: &mut egui::Ui) { - if !self.state.decompile.find.open { - return; - } - // Compute matches up front (immutable borrow of the script), then mutate find state. - let matches = decompile::find_matches( - &self.state.decompile.script, - &self.state.decompile.find.query, - self.state.decompile.find.case_sensitive, - ); - let count = matches.len(); - - let f = &mut self.state.decompile.find; - // Keep `current` in range as matches change. - if count == 0 { - f.current = 0; - } else if f.current >= count { - f.current = count - 1; - } - - // `go` is set when the user navigates this frame: +1 next, -1 prev, 0 = jump to current - // (e.g. just after typing). We resolve it to a concrete match index at the end. - let mut nav: Option = None; - - ui.horizontal(|ui| { - ui.label("Find"); - // Enter = next, Shift+Enter = prev, handled via the response below. - let resp = ui.add( - egui::TextEdit::singleline(&mut f.query) - .hint_text("find in script") - .desired_width(220.0), - ); - if f.focus_query { - resp.request_focus(); - f.focus_query = false; - } - // On a query change, jump to the first match from the top. - if resp.changed() { - f.current = 0; - nav = Some(0); - } - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - let shift = ui.input(|i| i.modifiers.shift); - nav = Some(if shift { -1 } else { 1 }); - // Keep typing/searching: re-focus the query field after an Enter. - f.focus_query = true; - } - - ui.add_enabled_ui(count > 0, |ui| { - if ui.button("◀ Prev").on_hover_text("Previous match (Shift+Enter).").clicked() { - nav = Some(-1); - } - if ui.button("Next ▶").on_hover_text("Next match (Enter).").clicked() { - nav = Some(1); - } - }); - - // The match counter ("3 / 17", or "0 / 0" / "no matches"). - let counter = if f.query.is_empty() { - String::new() - } else if count == 0 { - "no matches".to_string() - } else { - format!("{} / {}", f.current + 1, count) - }; - if !counter.is_empty() { - ui.label(egui::RichText::new(counter).weak()); - } - - ui.checkbox(&mut f.case_sensitive, "Aa") - .on_hover_text("Case-sensitive matching (off = case-insensitive)."); - - if ui.button("×").on_hover_text("Close find (Esc).").clicked() { - f.open = false; - } - }); - - // Esc closes the bar. - if ui.input(|i| i.key_pressed(egui::Key::Escape)) { - f.open = false; - } - - // Resolve a navigation request to a concrete match and stash its range for the editor. - if let Some(delta) = nav { - if count > 0 { - let cur = f.current as isize; - let next = (cur + delta).rem_euclid(count as isize) as usize; - f.current = next; - let m = matches[next]; - f.pending_select = Some((m.start, m.end)); - } - } - } - - // --- Decompile background jobs + sync helpers ----------------------------------------- - - /// Drain the heavy-result side-channel: a finished Decompile job hands its WolfScript here. - fn poll_decompile_result(&mut self) { - let Some(rx) = &self.state.decompile.result_rx else { - return; - }; - let mut disconnected = false; - loop { - match rx.try_recv() { - Ok(DecompileResult::Text(input, text)) => { - self.state.decompile.script = text; - // The decompiled input is the compile base (so unknown bytes are preserved). - self.state.decompile.base = Some(input.clone()); - // Default the compile output to .out the first time (or when unset). - if self.state.decompile.out.is_none() { - let mut name = input.file_name().map(|s| s.to_os_string()).unwrap_or_default(); - name.push(".out"); - self.state.decompile.out = Some(input.with_file_name(name)); - } - } - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - disconnected = true; - break; - } - } - } - if disconnected && !self.jobs.is_running() { - self.state.decompile.result_rx = None; - } - } - - /// Spawn the Decompile job: render the input to editable WolfScript off the worker thread, hand - /// the text back via the side-channel (mirrors the CLI's `decompile --mode edit`). - fn start_decompile_job(&mut self) { - let Some(input) = self.state.decompile.input.clone() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.decompile.result_rx = Some(rx); - self.jobs.run(format!("Decompile {}", input.display()), &mut self.log, move |rep| { - rep.progress(0.2, "decompiling…"); - let text = decompile::decompile_edit(&input)?; - let lines = text.lines().count(); - let _ = tx.send(DecompileResult::Text(input.clone(), text)); - rep.progress(1.0, "done"); - Ok(format!("decompiled {} ({lines} line(s) of WolfScript)", input.display())) - }); - } - - /// Spawn the Compile job: recompile the edited WolfScript onto the stored base (the original - /// input, so unknown bytes are preserved) and write to the chosen output (mirrors the CLI's - /// `compile`). The compiled bytes are re-parsed before the write is reported. - fn start_compile_job(&mut self) { - let text = self.state.decompile.script.clone(); - let Some(base) = self - .state - .decompile - .base - .clone() - .or_else(|| self.state.decompile.input.clone()) - else { - self.log.error("compile: no base file (Decompile a file or pick the Input first)"); - return; - }; - // Default the output to .out when none was chosen. - let out = self.state.decompile.out.clone().unwrap_or_else(|| { - let mut name = base.file_name().map(|s| s.to_os_string()).unwrap_or_default(); - name.push(".out"); - base.with_file_name(name) - }); - self.jobs.run(format!("Compile → {}", out.display()), &mut self.log, move |rep| { - rep.progress(0.3, "compiling…"); - let outcome = decompile::compile_to(&text, &base, &out)?; - rep.progress(1.0, "done"); - Ok(format!( - "wrote {} ({} bytes{})", - outcome.out.display(), - outcome.bytes, - if outcome.byte_identical { ", byte-identical to base" } else { "" } - )) - }); - } - - /// Save the current WolfScript buffer to a chosen `.wscript` file (synchronous text write). - fn decompile_save_script(&mut self) { - let default = self - .state - .decompile - .base - .as_ref() - .or(self.state.decompile.input.as_ref()) - .and_then(|p| p.file_stem().and_then(|s| s.to_str())) - .map(|s| format!("{s}.wscript")) - .unwrap_or_else(|| "script.wscript".to_string()); - let Some(path) = widgets::save_file(&default, &[("WolfScript", &["wscript", "txt"])]) else { - return; - }; - match std::fs::write(&path, &self.state.decompile.script) { - Ok(()) => self.log.info(format!("saved WolfScript → {}", path.display())), - Err(e) => self.log.error(format!("save .wscript failed: {e}")), - } - } - - /// Load an existing `.wscript` into the editor (synchronous). The compile base stays the Input - /// file the user picked above (compile needs the matching original). - fn decompile_open_script(&mut self) { - let Some(path) = widgets::pick_file(&[("WolfScript", &["wscript", "txt"]), ("All files", &["*"])]) - else { - return; - }; - match std::fs::read_to_string(&path) { - Ok(text) => { - let lines = text.lines().count(); - self.state.decompile.script = text; - self.log.info(format!( - "loaded WolfScript ({lines} line(s)) from {} — set the Input above to its original file before Compile", - path.display() - )); - } - Err(e) => self.log.error(format!("open .wscript failed: {e}")), - } - } - - fn ui_database(&mut self, ui: &mut egui::Ui) { - // Pump any background result (a freshly loaded / reloaded grid model) into section state first. - self.poll_database_result(); - - // Default the path to the open project's main DataBase.project the first time we're shown. - if self.state.database.path.is_none() { - if let Some(p) = self.database_default_project() { - self.state.database.path = Some(p); - } - } - - // Layout: the heading + path row sit in a fixed top region, the Save controls in a fixed - // bottom region, and the (independently scrolling) grid fills the space between them. Using - // nested panels keeps each region in its own rectangle so the grid never draws over the - // controls (the old single-`ui` flow let the grid's inner CentralPanel claim all remaining - // height and paint on top of the Save row). - egui::TopBottomPanel::top("db_top") - .resizable(false) - .show_inside(ui, |ui| { - ui.heading("Database"); - widgets::caption( - ui, - "Database = items, skills, enemies, etc. — edit values in the grid (no JSON needed).", - ); - ui.add_space(6.0); - self.database_path_row(ui); - ui.add_space(4.0); - }); - - if self.state.database.loaded.is_none() { - egui::CentralPanel::default().show_inside(ui, |ui| { - widgets::caption( - ui, - "No database loaded. Pick a .project (a game has several: DataBase / CDataBase / \ - SysDatabase) and press Load.", - ); - }); - return; - } - - // Save controls pinned to the bottom (drawn before the central grid so the grid gets the - // remaining height and the two regions never overlap). - egui::TopBottomPanel::bottom("db_save") - .resizable(false) - .show_inside(ui, |ui| { - ui.add_space(4.0); - self.database_save_row(ui); - }); - - // The grid fills the remaining space and scrolls independently below the controls. - egui::CentralPanel::default().show_inside(ui, |ui| { - self.database_grid(ui); - }); - } - - /// The open project's `DataBase.project`, if its data dir holds one (the sensible default). - fn database_default_project(&self) -> Option { - let data_dir = self.project.as_ref().and_then(|p| p.data_dir.clone())?; - // The DB pairs live under the data dir or its BasicData/ subfolder. - for cand in [data_dir.join("DataBase.project"), data_dir.join("BasicData").join("DataBase.project")] { - if cand.exists() { - return Some(cand); - } - } - None - } - - /// The `.project` path field + Load/Reload buttons + the kind/encoding badges. - fn database_path_row(&mut self, ui: &mut egui::Ui) { - let mut path_changed = false; - // A project dropdown of the scanned databases (BasicData/*.project), above the Browse field. - if let Some(dbs) = self.project.as_ref().map(|p| p.databases.clone()) { - let mut path = self.state.database.path.clone(); - if project_combo( - ui, - "database_combo", - "Project DB", - &dbs, - &mut path, - "Pick a database (.project) from the open game: DataBase / CDataBase / SysDatabase.", - ) { - self.state.database.path = path; - self.state.database.out_project = None; - path_changed = true; - } - } - - let start = self.state.database.path.clone(); - let mut path = self.state.database.path.clone(); - let changed = widgets::path_field( - ui, - "Database", - &mut path, - "A database .project (its sibling .dat is loaded automatically). A game has several: \ - DataBase (items/skills/enemies), CDataBase (system config), SysDatabase.", - move || { - let start_dir = start.as_ref().and_then(|p| p.parent()); - widgets::pick_file_in(start_dir, &[("Database project", &["project"]), ("All files", &["*"])]) - }, - ); - self.state.database.path = path; - // Picking a fresh .project clears the previous output choice (it pointed at the old DB). - if changed { - self.state.database.out_project = None; - path_changed = true; - } - - // Auto-load the moment the selection or path changes, so switching databases does not need a - // manual Load click. Discards unsaved edits to the previous DB (it was a different table). - if path_changed && self.state.database.path.is_some() && !self.jobs.is_running() { - self.start_database_load_job(); - } - - ui.add_space(4.0); - let running = self.jobs.is_running(); - let have = self.state.database.path.is_some(); - ui.horizontal(|ui| { - ui.add_enabled_ui(have && !running, |ui| { - if ui - .button("Load") - .on_hover_text("Read the .project + .dat pair and fill the grid with its current values.") - .clicked() - { - self.start_database_load_job(); - } - }); - if self.state.database.loaded.is_some() - && ui - .button("Reload") - .on_hover_text("Discard edits and reload the values from disk.") - .clicked() - { - self.start_database_load_job(); - } - if self.state.database.loaded.is_some() - && ui - .button("Unload") - .on_hover_text("Close the database and free its memory (discards unsaved edits).") - .clicked() - { - self.unload_database(); - } - // Export the whole database to a JSON file (full structure, all rows/fields/numbers) so it - // can be edited outside the GUI, and import an edited JSON back. Exports the ON-DISK state, - // so Save first if you want grid edits included. - ui.add_enabled_ui(have && !running, |ui| { - if ui - .button("Export JSON…") - .on_hover_text("Write the full database to a .json file you can edit in any editor (Save first to include grid edits).") - .clicked() - { - if let Some(out) = widgets::save_file("database.json", &[("JSON", &["json"])]) { - self.start_database_export_job(out); - } - } - if ui - .button("Import JSON…") - .on_hover_text("Apply an edited database .json back onto the .project + .dat (the originals are backed up first), then reload.") - .clicked() - { - if let Some(json) = widgets::pick_file(&[("JSON", &["json"]), ("All files", &["*"])]) { - self.start_database_import_job(json); - } - } - }); - // The kind + encoding badges (read-only), shown once a model is loaded. - if let Some(m) = &self.state.database.loaded { - ui.separator(); - ui.label( - egui::RichText::new(format!(" {} ", m.kind)) - .strong() - .background_color(egui::Color32::from_rgb(0x40, 0x80, 0xE0)) - .color(egui::Color32::BLACK), - ) - .on_hover_text("UDB = the main user database (items/skills/enemies); CDB = system config; SDB = engine system."); - ui.separator(); - ui.label(egui::RichText::new(format!(" {} ", m.encoding)).strong()) - .on_hover_text("How the strings are stored. A Shift-JIS database can only hold characters representable in Shift-JIS."); - } - }); - } - - /// The two-pane grid: the type (table) selector on the left, the selected type's rows as an - /// editable table on the right. - fn database_grid(&mut self, ui: &mut egui::Ui) { - // Left: a selectable list of types with their row counts. - egui::SidePanel::left("db_types") - .resizable(true) - .default_width(240.0) - .show_inside(ui, |ui| { - ui.label(egui::RichText::new("Types (tables)").strong()) - .on_hover_text("Each type is a table: Skill, Item, Enemy, … Pick one to edit its rows."); - ui.add_space(2.0); - egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { - let db = &mut self.state.database; - let Some(model) = &db.loaded else { return }; - if db.selected_type >= model.types.len() { - db.selected_type = 0; - } - for (i, t) in model.types.iter().enumerate() { - let edited = t.changed_count(); - let mut label = format!("{} ({} rows)", t.name, t.rows.len()); - if edited > 0 { - // The edit badge stays on the row (it wraps with the rest of the text). - label.push_str(&format!(" ● {edited}")); - } - // A custom full-width row that allocates at the wrapped text height and paints - // a rounded, padded highlight, so a two-line type name is fully contained. - if selectable_row(ui, db.selected_type == i, &label).clicked() { - db.selected_type = i; - db.filter.clear(); - } - } - }); - }); - - // Right: the selected type's filter/counter bar + the editable rows table. - egui::CentralPanel::default().show_inside(ui, |ui| { - self.database_rows_pane(ui); - }); - } - - /// The right pane: a filter box over row names, a row counter, and the editable rows table - /// (first column = the row name, then one column per data field). - fn database_rows_pane(&mut self, ui: &mut egui::Ui) { - use egui_extras::{Column, TableBuilder}; - - let db = &mut self.state.database; - let Some(model) = &mut db.loaded else { return }; - let Some(t) = model.types.get_mut(db.selected_type) else { - widgets::caption(ui, "Select a type on the left."); - return; - }; - - // Filter/counter bar. - let total = t.rows.len(); - ui.horizontal(|ui| { - ui.label(egui::RichText::new(&t.name).strong()); - ui.separator(); - ui.label("Filter:"); - ui.add( - egui::TextEdit::singleline(&mut db.filter) - .hint_text("search row names") - .desired_width(200.0), - ); - ui.separator(); - ui.label(egui::RichText::new(format!("{total} row(s)")).strong()); - }); - ui.add_space(4.0); - - if t.fields.is_empty() { - widgets::caption(ui, "This type has no editable data fields."); - return; - } - - // Which row indices pass the (row-name) filter. When no filter is active we skip building an - // index Vec entirely and address rows directly, so the common case has no per-frame O(rows) - // allocation or scan on top of the now-virtualized body. - let needle = db.filter.to_lowercase(); - let filtered: Option> = if needle.is_empty() { - None - } else { - Some( - t.rows - .iter() - .enumerate() - .filter(|(_, r)| r.name.to_lowercase().contains(&needle)) - .map(|(i, _)| i) - .collect(), - ) - }; - let count = filtered.as_ref().map_or(t.rows.len(), Vec::len); - if count == 0 { - widgets::caption(ui, "No rows match the current filter."); - return; - } - - let text_h = egui::TextStyle::Body.resolve(ui.style()).size; - let field_labels = t.fields.clone(); - // The egui_extras Table only scrolls vertically. A type with many fields is wider than the - // pane, so wrap the whole table in a horizontal ScrollArea: the outer area pans sideways - // while the Table's own vertical scroll keeps the rows virtualized. Bound the table height to - // the space left so the inner vertical scroll has a viewport to virtualize against. - let avail_h = ui.available_height(); - egui::ScrollArea::horizontal() - .auto_shrink([false, false]) - .show(ui, |ui| { - let mut builder = TableBuilder::new(ui) - .striped(true) - .resizable(true) - .max_scroll_height(avail_h) - .cell_layout(egui::Layout::left_to_right(egui::Align::TOP)) - .column(Column::auto().at_least(40.0)) // id - .column(Column::initial(160.0).at_least(90.0)); // name - for _ in &field_labels { - builder = builder.column(Column::initial(130.0).at_least(70.0)); - } - builder - .header(text_h + 6.0, |mut header| { - header.col(|ui| { - ui.strong("id"); - }); - header.col(|ui| { - ui.strong("name").on_hover_text( - "The row's display name. Tip: translating names is best done in the \ - Translation section's glossary so by-name lookups stay consistent.", - ); - }); - for label in &field_labels { - header.col(|ui| { - ui.strong(label.as_str()).on_hover_text(label.as_str()); - }); - } - }) - // Virtualized body: egui_extras only builds widgets for the rows actually on - // screen, so a type with thousands of rows no longer lays out thousands of - // TextEdits every frame. `r.index()` indexes into the filtered set. - .body(|body| { - body.rows(text_h * 2.0, count, |mut r| { - let ri = filtered.as_ref().map_or(r.index(), |v| v[r.index()]); - let row = &mut t.rows[ri]; - r.col(|ui| { - ui.add( - egui::Label::new( - egui::RichText::new(row.id.to_string()).weak().small(), - ) - .wrap(), - ); - }); - r.col(|ui| { - ui.add( - egui::TextEdit::singleline(&mut row.name) - .desired_width(f32::INFINITY), - ); - }); - for cell in row.cells.iter_mut() { - r.col(|ui| { - let hint = if cell.is_string { "" } else { "(number)" }; - ui.add( - egui::TextEdit::singleline(&mut cell.value) - .hint_text(hint) - .desired_width(f32::INFINITY), - ); - }); - } - }); - }); - }); - } - - /// The Options + Save row: output choice (in place vs an output `.project`), backup toggle, - /// change counter, and Save. - fn database_save_row(&mut self, ui: &mut egui::Ui) { - let running = self.jobs.is_running(); - - // Output choice: in place (default) vs a Browse'd output .project. - let mut out = self.state.database.out_project.clone(); - let mut in_place = out.is_none(); - ui.horizontal(|ui| { - if ui - .radio(in_place, "Save in place") - .on_hover_text("Overwrite the loaded .project + .dat (backing up the originals first if the box is checked).") - .clicked() - { - in_place = true; - out = None; - } - let pick = ui - .radio(!in_place, "Save to a new .project") - .on_hover_text("Write the result to a chosen .project (its sibling .dat is written next to it); the originals are left untouched.") - .clicked(); - if pick { - in_place = false; - if out.is_none() { - out = widgets::save_file("DataBase.project", &[("Database project", &["project"])]); - } - } - }); - if !in_place { - let mut o = out.clone(); - widgets::path_field( - ui, - "Output .project", - &mut o, - "Where to write the edited database (the sibling .dat is written alongside it).", - || widgets::save_file("DataBase.project", &[("Database project", &["project"])]), - ); - out = o; - } - self.state.database.out_project = out; - - ui.add_space(4.0); - widgets::labeled_checkbox( - ui, - &mut self.state.database.backup, - "Back up originals", - "Copy the .project + .dat to *.bak before overwriting them in place. Leave ON unless you \ - have your own backups (ignored when saving to a new file).", - ); - - let model = self.state.database.loaded.as_ref().expect("loaded checked by caller"); - let changed = model.changed_count(); - ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("{changed} cell(s)/name(s) edited")).strong()); - }); - ui.add_space(4.0); - - let dirty = model.dirty(); - let in_place_now = self.state.database.out_project.is_none(); - ui.add_enabled_ui(dirty && !running, |ui| { - if ui - .button("Save ▶") - .on_hover_text(if in_place_now { - "Apply the edits and write the .project + .dat back in place, byte-exact for untouched data." - } else { - "Apply the edits and write the .project + .dat to the chosen output, byte-exact for untouched data." - }) - .clicked() - { - self.start_database_save_job(); - } - }); - if !dirty { - widgets::caption(ui, "Edit a cell or row name to enable Save."); - } - } - - // --- Database background jobs + sync helpers ------------------------------------------- - - /// Drain the heavy-result side-channel: a finished Load/Save job hands its (re)loaded model here. - fn poll_database_result(&mut self) { - let Some(rx) = &self.state.database.result_rx else { - return; - }; - let mut disconnected = false; - loop { - match rx.try_recv() { - Ok(DbResult::Loaded(m)) => { - let ntypes = m.types.len(); - self.state.database.loaded = Some(*m); - if self.state.database.selected_type >= ntypes { - self.state.database.selected_type = 0; - } - } - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - disconnected = true; - break; - } - } - } - if disconnected && !self.jobs.is_running() { - self.state.database.result_rx = None; - } - } - - /// Spawn the Load job: read + render the `.project`+`.dat` pair into the grid model off the worker - /// thread, hand it back via the side-channel, and report type/row counts (mirrors `db-json`). - fn start_database_load_job(&mut self) { - let Some(path) = self.state.database.path.clone() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.database.result_rx = Some(rx); - self.jobs.run(format!("Load {}", path.display()), &mut self.log, move |rep| { - rep.progress(0.3, "reading database…"); - let model = database::load(&path)?; - let summary = format!( - "{} type(s), {} data row(s), {} encoding", - model.types.len(), - model.total_rows(), - model.encoding - ); - let _ = tx.send(DbResult::Loaded(Box::new(model))); - rep.progress(1.0, "done"); - Ok(summary) - }); - } - - /// Spawn the Save job: apply the grid edits onto a freshly-read base, re-parse the written pair to - /// verify it round-trips, and write both halves (in place when no output is chosen), then reload - /// from the written `.project` so the grid shows the persisted state. Mirrors the CLI's `db-apply`. - fn start_database_save_job(&mut self) { - // Move the loaded model onto the worker. It comes back (reloaded) via the side-channel. - let Some(model) = self.state.database.loaded.take() else { - return; - }; - let backup = self.state.database.backup; - let output = self.state.database.out_project.clone().unwrap_or_else(|| model.project.clone()); - let in_place = output == model.project; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.database.result_rx = Some(rx); - self.jobs.run(format!("Save database → {}", output.display()), &mut self.log, move |rep| { - rep.progress(0.3, "applying edits…"); - match database::save(&model, &output, backup && in_place) { - Ok(outcome) => { - for bak in &outcome.backups { - rep.info(format!("backed up original → {}", bak.display())); - } - // Reload from the written .project so the grid reflects the persisted state. - match database::load(&outcome.out_project) { - Ok(fresh) => { - let _ = tx.send(DbResult::Loaded(Box::new(fresh))); - } - Err(e) => { - rep.warn(format!("reload after save failed: {e}")); - let _ = tx.send(DbResult::Loaded(Box::new(model))); - } - } - rep.progress(1.0, "done"); - Ok(format!( - "{} cell(s)/name(s) changed → {} + {}{}", - outcome.changed, - outcome.out_project.display(), - outcome.out_dat.display(), - if outcome.byte_identical { " (byte-identical to base)" } else { "" } - )) - } - Err(e) => { - // Put the model back unchanged so the user's edits aren't lost. - let _ = tx.send(DbResult::Loaded(Box::new(model))); - rep.progress(1.0, "done"); - Err(e) - } - } - }); - } - - /// Spawn the DB export job: write the on-disk database to a `.json` file on a worker thread - /// (reads only the base path, so the live grid is untouched). - fn start_database_export_job(&mut self, out: PathBuf) { - let Some(base) = self.state.database.path.clone() else { - return; - }; - self.jobs.run(format!("Export DB → {}", out.display()), &mut self.log, move |rep| { - rep.progress(0.4, "reading database…"); - database::export_json(&base, &out)?; - rep.progress(1.0, "done"); - Ok(format!("exported → {}", out.display())) - }); - } - - /// Spawn the DB import job: apply an edited `.json` onto the loaded database in place (backing up - /// the originals first), then reload the grid from the written result. - fn start_database_import_job(&mut self, json: PathBuf) { - let Some(base) = self.state.database.path.clone() else { - return; - }; - let backup = self.state.database.backup; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.database.result_rx = Some(rx); - self.jobs.run(format!("Import DB ← {}", json.display()), &mut self.log, move |rep| { - rep.progress(0.3, "applying JSON…"); - let outcome = database::import_json(&base, &json, &base, backup)?; - for bak in &outcome.backups { - rep.info(format!("backed up original → {}", bak.display())); - } - // Reload the grid from the written .project so it reflects the imported state. - match database::load(&outcome.out_project) { - Ok(fresh) => { - let _ = tx.send(DbResult::Loaded(Box::new(fresh))); - } - Err(e) => rep.warn(format!("reload after import failed: {e}")), - } - rep.progress(1.0, "done"); - Ok(format!( - "{} cell(s)/name(s) changed{}", - outcome.changed, - if outcome.byte_identical { " (no change)" } else { "" } - )) - }); - } - - fn ui_gamedat(&mut self, ui: &mut egui::Ui) { - // Pump any background result (a freshly loaded / reloaded model) into section state first. - self.poll_gamedat_result(); - - ui.heading("Game.dat"); - widgets::caption(ui, "Edit every Game.dat field: title, fonts, splash/title messages, image paths."); - ui.add_space(6.0); - - // Default the path to the open project's Game.dat the first time we're shown. - if self.state.gamedat.path.is_none() { - if let Some(p) = self.project.as_ref().and_then(|p| p.game_dat.clone()) { - self.state.gamedat.path = Some(p); - } - } - - self.gamedat_path_row(ui); - ui.add_space(8.0); - ui.separator(); - ui.add_space(6.0); - - if self.state.gamedat.loaded.is_none() { - widgets::caption(ui, "No Game.dat loaded. Pick a Game.dat and press Load."); - return; - } - self.gamedat_form(ui); - } - - /// The Game.dat path field + Load button + the encoding badge. - fn gamedat_path_row(&mut self, ui: &mut egui::Ui) { - let start = self.state.gamedat.path.clone(); - let mut path = self.state.gamedat.path.clone(); - widgets::path_field( - ui, - "Game.dat", - &mut path, - "The game's Game.dat (holds the window title, fonts, splash/title messages and image paths).", - move || { - let start_dir = start.as_ref().and_then(|p| p.parent()); - widgets::pick_file_in(start_dir, &[("Game data", &["dat"]), ("All files", &["*"])]) - }, - ); - self.state.gamedat.path = path; - - ui.add_space(4.0); - let running = self.jobs.is_running(); - let have = self.state.gamedat.path.is_some(); - ui.horizontal(|ui| { - ui.add_enabled_ui(have && !running, |ui| { - if ui - .button("Load") - .on_hover_text("Read the Game.dat and fill the form with its current field values.") - .clicked() - { - self.start_gamedat_load_job(); - } - }); - if self.state.gamedat.loaded.is_some() - && ui - .button("Reload") - .on_hover_text("Discard edits and reload the field values from disk.") - .clicked() - { - self.start_gamedat_load_job(); - } - if self.state.gamedat.loaded.is_some() - && ui - .button("Unload") - .on_hover_text("Close Game.dat and free its memory (discards unsaved edits).") - .clicked() - { - self.unload_gamedat(); - } - // The encoding badge (read-only), shown once a model is loaded. - if let Some(g) = &self.state.gamedat.loaded { - ui.separator(); - ui.label( - egui::RichText::new(format!(" {} ", g.encoding)) - .strong() - .background_color(egui::Color32::from_rgb(0x40, 0x80, 0xE0)) - .color(egui::Color32::BLACK), - ) - .on_hover_text("How the strings are stored (utf8 or shiftjis). A Shift-JIS file can only hold characters representable in Shift-JIS."); - } - }); - } - - /// The grouped field form + the Save controls. - fn gamedat_form(&mut self, ui: &mut egui::Ui) { - let running = self.jobs.is_running(); - // Render the grouped fields inside a scroll area (a file can carry many). - egui::ScrollArea::vertical() - .auto_shrink([false, false]) - .max_height(360.0) - .show(ui, |ui| { - let g = self.state.gamedat.loaded.as_mut().expect("loaded checked by caller"); - Self::gamedat_group(ui, "Titles & messages", g, Group::Titles, false); - Self::gamedat_group(ui, "Fonts", g, Group::Fonts, true); - Self::gamedat_group(ui, "Graphics", g, Group::Graphics, false); - Self::gamedat_group(ui, "Other", g, Group::Other, false); - }); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(4.0); - - // Backup toggle + change counter + Save. - widgets::labeled_checkbox( - ui, - &mut self.state.gamedat.backup, - "Back up original", - "Copy the file to a *.bak before overwriting it in place. Leave ON unless you have your \ - own backups (ignored when saving to a new file).", - ); - let g = self.state.gamedat.loaded.as_ref().expect("loaded"); - let changed = g.changed_count(); - ui.horizontal(|ui| { - ui.label(egui::RichText::new(format!("{changed} field(s) edited")).strong()); - }); - ui.add_space(4.0); - let dirty = g.dirty(); - ui.horizontal(|ui| { - ui.add_enabled_ui(dirty && !running, |ui| { - if ui - .button("Save (in place) ▶") - .on_hover_text("Apply the edits and write the Game.dat back in place (backing up the original first if the box is checked).") - .clicked() - { - self.start_gamedat_save_job(None); - } - }); - ui.add_enabled_ui(dirty && !running, |ui| { - if ui - .button("Save as…") - .on_hover_text("Apply the edits and write the result to a chosen file (the original is left untouched).") - .clicked() - { - if let Some(out) = - widgets::save_file("Game.dat", &[("Game data", &["dat"]), ("All files", &["*"])]) - { - self.start_gamedat_save_job(Some(out)); - } - } - }); - }); - if !dirty { - widgets::caption(ui, "Edit a field to enable Save."); - } - } - - /// Render the fields of one group as a labelled form section, plus (when `with_subfonts`) the - /// SubFonts array under the same header. Skips the section entirely when it has no fields. - /// - /// Every row goes through the shared [`widgets::text_row`], so every input fills the row to the - /// same width and starts at the same x (the fixed [`widgets::LABEL_COL_W`] left column). The - /// inputs line up on both edges across all groups, and the whole Game.dat form matches the path - /// rows of the other sections. - fn gamedat_group( - ui: &mut egui::Ui, - header: &str, - g: &mut LoadedGameDat, - group: Group, - with_subfonts: bool, - ) { - let any_fields = g.fields.iter().any(|f| f.group == group); - let any_subfonts = with_subfonts && !g.sub_fonts.is_empty(); - if !any_fields && !any_subfonts { - return; - } - ui.add_space(4.0); - ui.label(egui::RichText::new(header).strong()); - ui.add_space(2.0); - for f in g.fields.iter_mut().filter(|f| f.group == group) { - // The Font field's label gets a tofu-busting tooltip (the non-coder rescue). - let label_tip = if f.key == "Font" { - "the game's font; if text shows as boxes/tofu on your system, try another installed \ - Japanese font here" - } else { - "" - }; - widgets::text_row(ui, f.label, &mut f.value, label_tip, "(empty)"); - } - if any_subfonts { - for (i, s) in g.sub_fonts.iter_mut().enumerate() { - widgets::text_row( - ui, - &format!("Sub-font {}", i + 1), - &mut s.value, - "A fallback font the engine uses when the main font lacks a glyph.", - "(empty)", - ); - } - } - } - - // --- Game.dat background jobs + sync helpers ------------------------------------------- - - /// Drain the heavy-result side-channel: a finished Load/Save job hands its (re)loaded model here. - fn poll_gamedat_result(&mut self) { - let Some(rx) = &self.state.gamedat.result_rx else { - return; - }; - let mut disconnected = false; - loop { - match rx.try_recv() { - Ok(GameDatResult::Loaded(g)) => self.state.gamedat.loaded = Some(*g), - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - disconnected = true; - break; - } - } - } - if disconnected && !self.jobs.is_running() { - self.state.gamedat.result_rx = None; - } - } - - /// Spawn the Load job: read + parse the Game.dat into the form model off the worker thread, hand - /// it back via the side-channel, and report the field/encoding counts (mirrors `gamedat-json`). - fn start_gamedat_load_job(&mut self) { - let Some(path) = self.state.gamedat.path.clone() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.gamedat.result_rx = Some(rx); - self.jobs.run(format!("Load {}", path.display()), &mut self.log, move |rep| { - rep.progress(0.3, "reading Game.dat…"); - let g = gamedat::load(&path)?; - let summary = format!( - "{} field(s) + {} sub-font(s), {} encoding", - g.fields.len(), - g.sub_fonts.len(), - g.encoding - ); - let _ = tx.send(GameDatResult::Loaded(Box::new(g))); - rep.progress(1.0, "done"); - Ok(summary) - }); - } - - /// Spawn the Save job: apply the form edits onto a fresh base, re-parse to verify the round-trip, - /// and write to `out` (in place when `None`), then reload from the written bytes so the form shows - /// the persisted state. Mirrors the CLI's `gamedat-apply` guard. - fn start_gamedat_save_job(&mut self, out: Option) { - // Move the loaded model onto the worker. It comes back (reloaded) via the side-channel. - let Some(loaded) = self.state.gamedat.loaded.take() else { - return; - }; - let backup = self.state.gamedat.backup; - let output = out.unwrap_or_else(|| loaded.path.clone()); - let in_place = output == loaded.path; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.gamedat.result_rx = Some(rx); - self.jobs.run(format!("Save Game.dat → {}", output.display()), &mut self.log, move |rep| { - rep.progress(0.3, "applying edits…"); - let result = gamedat::save(&loaded, &output, backup && in_place); - match result { - Ok(outcome) => { - if let Some(bak) = &outcome.backup { - rep.info(format!("backed up original → {}", bak.display())); - } - // Reload from the written file so the form reflects the persisted state. - match gamedat::load(&outcome.out) { - Ok(fresh) => { - let _ = tx.send(GameDatResult::Loaded(Box::new(fresh))); - } - Err(e) => { - rep.warn(format!("reload after save failed: {e}")); - let _ = tx.send(GameDatResult::Loaded(Box::new(loaded))); - } - } - rep.progress(1.0, "done"); - Ok(format!( - "{} field(s) changed → {}{}", - outcome.changed, - outcome.out.display(), - if outcome.byte_identical { " (byte-identical to base)" } else { "" } - )) - } - Err(e) => { - // Put the model back unchanged so the user's edits aren't lost. - let _ = tx.send(GameDatResult::Loaded(Box::new(loaded))); - rep.progress(1.0, "done"); - Err(e) - } - } - }); - } - - fn ui_translation(&mut self, ui: &mut egui::Ui) { - // Pump any background result (a rebuilt model / conflict list) into the section state first, - // so the rest of this frame renders what the just-finished job produced. - self.poll_translation_result(); - - ui.heading("Translation"); - widgets::caption(ui, "Extract player-facing text, edit translations, inject them back, and carry them across game updates."); - ui.add_space(6.0); - - // Default the source folder to the open project's data dir the first time we're shown. - if self.state.translation.source_dir.is_none() { - if let Some(d) = self.project.as_ref().and_then(|p| p.data_dir.clone()) { - self.state.translation.source_dir = Some(d); - } - } - - self.translation_source_row(ui); - ui.add_space(6.0); - self.translation_action_row(ui); - ui.add_space(8.0); - ui.separator(); - - if self.state.translation.model.is_none() { - ui.add_space(8.0); - widgets::caption( - ui, - "No translation loaded. Pick a game's unpacked Data folder and press Extract, \ - or Load a saved translation project.", - ); - return; - } - - if let Some(model) = &self.state.translation.model { - widgets::caption( - ui, - &format!( - "{} file(s) · {} / {} lines translated overall", - model.files.len(), - model.translated_rows(), - model.total_rows(), - ), - ); - } - ui.add_space(4.0); - self.translation_options_row(ui); - ui.add_space(6.0); - ui.separator(); - ui.add_space(6.0); - self.translation_grid(ui); - self.translation_conflicts_panel(ui); - } - - /// The source-folder field + Extract button. - fn translation_source_row(&mut self, ui: &mut egui::Ui) { - let mut dir = self.state.translation.source_dir.clone(); - let start = dir.clone(); - widgets::path_field( - ui, - "Source folder", - &mut dir, - "The game's unpacked Data folder (holds BasicData/ and MapData/). Extract reads every \ - text file under it.", - move || widgets::pick_folder_in(start.as_deref()), - ); - self.state.translation.source_dir = dir; - - ui.add_space(6.0); - let running = self.jobs.is_running(); - let have_dir = self.state.translation.source_dir.is_some(); - ui.horizontal(|ui| { - ui.add_enabled_ui(have_dir && !running, |ui| { - if ui - .button("Extract") - .on_hover_text("Read every map, CommonEvent, database and Game.dat under the folder into an editable grid.") - .clicked() - { - self.start_extract_job(); - } - }); - ui.add_enabled_ui(!running, |ui| { - if ui - .button("Load project…") - .on_hover_text("Restore a translation you saved earlier (a .wdtr.json file).") - .clicked() - { - self.translation_load_project(); - } - if self.state.translation.model.is_some() - && ui - .button("Save project…") - .on_hover_text("Save the whole translation (all edits) to a single JSON so it persists across sessions.") - .clicked() - { - self.translation_save_project(); - } - }); - }); - } - - /// The Merge / Check-names action row (only useful once a model is loaded). - fn translation_action_row(&mut self, ui: &mut egui::Ui) { - let running = self.jobs.is_running(); - let have_model = self.state.translation.model.is_some(); - ui.horizontal(|ui| { - ui.add_enabled_ui(have_model && !running, |ui| { - if ui - .button("Merge older translation…") - .on_hover_text("Carry translations from a previous version's exported JSON(s) into this model, matched by source text.") - .clicked() - { - self.start_merge_job(); - } - if ui - .button("Check names") - .on_hover_text("Check that each glossary name is translated to ONE value everywhere (inconsistent names break by-name lookups).") - .clicked() - { - self.start_check_job(); - } - }); - if have_model - && ui - .button("Unload") - .on_hover_text("Close the translation and free its memory (discards unsaved edits; Save or Inject first to keep them).") - .clicked() - { - self.unload_translation(); - } - }); - } - - /// The inject options (en-punct / allow-code-drift), the output choice, and the Inject button. - fn translation_options_row(&mut self, ui: &mut egui::Ui) { - let tr = &mut self.state.translation; - ui.horizontal(|ui| { - widgets::labeled_checkbox( - ui, - &mut tr.en_punct, - "--en-punct", - "Convert Japanese punctuation 「」~。 to ASCII for English (applied to your translations on inject).", - ); - widgets::labeled_checkbox( - ui, - &mut tr.allow_code_drift, - "--allow-code-drift", - "Allow dropping/altering \\codes — risky. Off keeps the strict control-code guard that protects byte-exactness.", - ); - }); - - // Output choice: in place (default, with a note) vs a Browse'd output dir. - let mut out = tr.out_dir.clone(); - let mut in_place = out.is_none(); - ui.horizontal(|ui| { - if ui - .radio(in_place, "Write in place") - .on_hover_text("Overwrite the files in the source folder (make a backup of your game first).") - .clicked() - { - in_place = true; - out = None; - } - let pick_out = ui - .radio(!in_place, "Write to output folder") - .on_hover_text("Write the translated files into a separate folder, mirroring the source layout.") - .clicked(); - if pick_out { - in_place = false; - if out.is_none() { - out = widgets::pick_folder(); - } - } - }); - if !in_place { - let start = out.clone(); - widgets::path_field( - ui, - "Output folder", - &mut out, - "Where to write the translated files (mirrors the source folder's layout).", - move || widgets::pick_folder_in(start.as_deref()), - ); - } - self.state.translation.out_dir = out; - - ui.add_space(6.0); - let running = self.jobs.is_running(); - let in_place_now = self.state.translation.out_dir.is_none(); - ui.add_enabled_ui(!running, |ui| { - let btn = ui.button("Inject ▶").on_hover_text(if in_place_now { - "Apply your translations back into the game files IN PLACE (overwrites the source folder)." - } else { - "Apply your translations into the chosen output folder, byte-exact." - }); - if btn.clicked() { - self.start_inject_job(); - } - }); - } - - /// The two-pane grid: the file/names list on the left, the selected entry's rows on the right. - fn translation_grid(&mut self, ui: &mut egui::Ui) { - // Left: a selectable list of files + the names glossary. - egui::SidePanel::left("tr_files") - .resizable(true) - .default_width(220.0) - .show_inside(ui, |ui| { - ui.label(egui::RichText::new("Files").strong()); - ui.add_space(2.0); - egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { - let tr = &mut self.state.translation; - let Some(model) = &tr.model else { return }; - // The names glossary entry first. - let names_label = format!( - "Names ({}/{})", - model.names.iter().filter(|r| r.is_translated()).count(), - model.names.len() - ); - if ui - .selectable_label(tr.selected == SelectedEntry::Names, names_label) - .clicked() - { - tr.selected = SelectedEntry::Names; - } - ui.separator(); - for (i, f) in model.files.iter().enumerate() { - let label = format!( - "{} {} ({}/{})", - kind_icon(f.kind), - f.display_name(), - f.translated_count(), - f.rows.len() - ); - if ui - .selectable_label(tr.selected == SelectedEntry::File(i), label) - .clicked() - { - tr.selected = SelectedEntry::File(i); - } - } - }); - }); - - // Right: the filter/toggle/counter bar + the rows table for the selected entry. - egui::CentralPanel::default().show_inside(ui, |ui| { - self.translation_rows_pane(ui); - }); - } - - /// The right pane: filter box, untranslated toggle, live counter, and the editable rows table. - fn translation_rows_pane(&mut self, ui: &mut egui::Ui) { - use egui_extras::{Column, TableBuilder}; - - let tr = &mut self.state.translation; - let Some(model) = &mut tr.model else { return }; - - // Resolve the selected entry's rows. - let rows = match tr.selected { - SelectedEntry::Names => &mut model.names, - SelectedEntry::File(i) => match model.files.get_mut(i) { - Some(f) => &mut f.rows, - None => { - tr.selected = SelectedEntry::Names; - &mut model.names - } - }, - }; - - // Filter/toggle/counter bar. - let (translated, total) = (rows.iter().filter(|r| r.is_translated()).count(), rows.len()); - ui.horizontal(|ui| { - ui.label("Filter:"); - ui.add( - egui::TextEdit::singleline(&mut tr.filter) - .hint_text("search source or translation") - .desired_width(220.0), - ); - ui.checkbox(&mut tr.untranslated_only, "untranslated only") - .on_hover_text("Show only rows you have not translated yet."); - ui.separator(); - ui.label( - egui::RichText::new(format!("{translated} / {total} translated")).strong(), - ); - }); - ui.add_space(4.0); - - // Which row indices pass the filter/toggle. With no text filter and the toggle off we address - // rows directly (no per-frame index Vec), so an unfiltered file of thousands of lines costs - // nothing beyond the now-virtualized body. - let needle = tr.filter.to_lowercase(); - let active = !needle.is_empty() || tr.untranslated_only; - let filtered: Option> = if !active { - None - } else { - Some( - rows.iter() - .enumerate() - .filter(|(_, r)| { - if tr.untranslated_only && r.is_translated() { - return false; - } - needle.is_empty() - || r.source.to_lowercase().contains(&needle) - || r.translation.to_lowercase().contains(&needle) - }) - .map(|(i, _)| i) - .collect(), - ) - }; - let count = filtered.as_ref().map_or(rows.len(), Vec::len); - if count == 0 { - widgets::caption(ui, "No rows match the current filter."); - return; - } - - let text_h = egui::TextStyle::Body.resolve(ui.style()).size; - TableBuilder::new(ui) - .striped(true) - .resizable(true) - .cell_layout(egui::Layout::left_to_right(egui::Align::TOP)) - .column(Column::remainder().at_least(180.0)) // Source - .column(Column::remainder().at_least(180.0)) // Translation - .column(Column::auto().at_least(90.0)) // Note/Loc - .header(text_h + 6.0, |mut header| { - header.col(|ui| { - ui.strong("Source"); - }); - header.col(|ui| { - ui.strong("Translation"); - }); - header.col(|ui| { - ui.strong("Note / Loc"); - }); - }) - // Virtualized: only on-screen rows are built, so a file with thousands of lines (the - // unpacked .txt scenes) does not lay out every TextEdit every frame. - .body(|body| { - body.rows(text_h * 3.2, count, |mut r| { - let idx = filtered.as_ref().map_or(r.index(), |v| v[r.index()]); - let row = &mut rows[idx]; - r.col(|ui| { - ui.add( - egui::Label::new(egui::RichText::new(&row.source).monospace()) - .wrap(), - ); - }); - r.col(|ui| { - ui.add( - egui::TextEdit::multiline(&mut row.translation) - .desired_rows(2) - .desired_width(f32::INFINITY), - ); - }); - r.col(|ui| { - ui.vertical(|ui| { - ui.add( - egui::Label::new( - egui::RichText::new(&row.location).weak().small(), - ) - .wrap(), - ); - if !row.note.is_empty() { - ui.add( - egui::Label::new( - egui::RichText::new(&row.note).weak().small(), - ) - .wrap(), - ); - } - }); - }); - }); - }); - } - - /// The small name-conflict panel, shown below the grid when the last check found any. - fn translation_conflicts_panel(&mut self, ui: &mut egui::Ui) { - let tr = &mut self.state.translation; - if tr.conflicts.is_empty() { - return; - } - ui.add_space(6.0); - ui.separator(); - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - format!("Warning: {} name(s) translated inconsistently:", tr.conflicts.len()), - ); - egui::ScrollArea::vertical() - .max_height(120.0) - .auto_shrink([false, true]) - .show(ui, |ui| { - for c in &tr.conflicts { - ui.label(format!( - "{:?} → {}", - c.source, - c.variants - .iter() - .map(|v| format!("{:?}", v.text)) - .collect::>() - .join(" · ") - )); - } - }); - if ui.button("Clear conflicts").clicked() { - tr.conflicts.clear(); - } - } - - // --- Translation background jobs + sync helpers --------------------------------------- - - /// Drain the heavy-result side-channel: a finished job hands its rebuilt model / conflicts here. - fn poll_translation_result(&mut self) { - let Some(rx) = &self.state.translation.result_rx else { - return; - }; - // Take whatever is available. The channel closes when the worker drops its sender. - let mut disconnected = false; - loop { - match rx.try_recv() { - Ok(TrResult::Model(m)) => { - let nfiles = m.files.len(); - self.state.translation.model = Some(*m); - // Keep the user's current selection across inject/merge/check (the model comes - // back the same shape). Only fix it up if it now points past the end (a fresh - // extract with fewer files, or no files at all). - let stale = match self.state.translation.selected { - SelectedEntry::File(i) => i >= nfiles, - SelectedEntry::Names => false, - }; - if stale { - self.state.translation.selected = - if nfiles > 0 { SelectedEntry::File(0) } else { SelectedEntry::Names }; - } - } - Ok(TrResult::Conflicts(c)) => self.state.translation.conflicts = c, - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - disconnected = true; - break; - } - } - } - if disconnected && !self.jobs.is_running() { - self.state.translation.result_rx = None; - } - } - - /// Spawn the Extract job: build the editable model off the worker thread, hand it back via the - /// side-channel, and report counts through the job log/progress (the scaffold's pattern). - fn start_extract_job(&mut self) { - let Some(dir) = self.state.translation.source_dir.clone() else { - return; - }; - // A fresh extract invalidates any prior conflict findings. - self.state.translation.conflicts.clear(); - let (tx, rx) = std::sync::mpsc::channel(); - self.state.translation.result_rx = Some(rx); - self.jobs.run(format!("Extract {}", dir.display()), &mut self.log, move |rep| { - rep.progress(0.0, "scanning folder…"); - let outcome = translation::extract_model(&dir, |frac, msg| rep.progress(frac, msg))?; - for w in &outcome.warnings { - rep.warn(w.clone()); - } - let strings: usize = outcome.model.files.iter().map(|f| f.rows.len()).sum(); - let names = outcome.model.names.len(); - let nfiles = outcome.model.files.len(); - send_model(&tx, outcome.model); - rep.progress(1.0, "done"); - Ok(format!("{strings} strings across {nfiles} file(s), {names} names")) - }); - } - - /// Spawn the Inject job: serialize the edited model and apply it via the library inject paths. - fn start_inject_job(&mut self) { - // The model lives in section state and carries the edits, so re-extracting would drop them. - // Take the model out, run inject, and put it back via the side-channel so the (unchanged) - // model returns to the UI. - let Some(model) = self.state.translation.model.take() else { - return; - }; - let opts = InjectOptions { - allow_code_drift: self.state.translation.allow_code_drift, - normalize_punct: self.state.translation.en_punct, - }; - let target = match self.state.translation.out_dir.clone() { - Some(od) => InjectTarget::OutDir(od), - None => InjectTarget::InPlace, - }; - let where_label = match &target { - InjectTarget::InPlace => "in place".to_string(), - InjectTarget::OutDir(p) => format!("→ {}", p.display()), - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.translation.result_rx = Some(rx); - self.jobs.run(format!("Inject ({where_label})"), &mut self.log, move |rep| { - let mut model = model; - rep.progress(0.0, "injecting…"); - let result = translation::inject_model( - &mut model, - &opts, - &target, - |frac, msg| rep.progress(frac, msg), - ); - // Whatever happens, hand the model back so the UI keeps the user's edits. - let report = match result { - Ok(reports) => { - let (mut a, mut s, mut d) = (0, 0, 0); - for r in &reports { - rep.info(format!( - " {} — {} applied, {} skipped, {} drifted", - r.file, r.applied, r.skipped, r.drifted - )); - a += r.applied; - s += r.skipped; - d += r.drifted; - } - Ok(format!("{a} applied, {s} skipped, {d} drifted across {} file(s)", reports.len())) - } - Err(e) => Err(e), - }; - send_model(&tx, model); - rep.progress(1.0, "done"); - report - }); - } - - /// Spawn the Merge job: pick old translation file(s)/dir, carry their translations into the model. - fn start_merge_job(&mut self) { - // Pick the old translations on the UI thread (file dialogs must not run on a worker). - let mut old: Vec = Vec::new(); - if let Some(dir) = widgets::pick_folder() { - old.push(dir); - } else if let Some(files) = - rfd::FileDialog::new().add_filter("Translation JSON", &["json"]).pick_files() - { - old.extend(files); - } - if old.is_empty() { - return; - } - let Some(model) = self.state.translation.model.take() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.translation.result_rx = Some(rx); - self.jobs.run("Merge older translation".to_string(), &mut self.log, move |rep| { - let mut model = model; - rep.progress(0.1, "building translation memory…"); - let result = translation::merge_into_model(&mut model, &old); - let summary = match &result { - Ok(m) => Ok(format!("carried {} translation(s); {} still new", m.carried, m.still_new)), - Err(e) => Err(e.clone()), - }; - send_model(&tx, model); - rep.progress(1.0, "done"); - summary - }); - } - - /// Spawn the Check-names job: run the conflict check over the model, surface findings in the panel. - fn start_check_job(&mut self) { - let Some(model) = self.state.translation.model.take() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.translation.result_rx = Some(rx); - self.jobs.run("Check names".to_string(), &mut self.log, move |rep| { - let mut model = model; - rep.progress(0.2, "scanning for conflicts…"); - let conflicts = translation::check_conflicts(&mut model); - let n = conflicts.len(); - // Hand both the (unchanged) model and the conflict list back. - let _ = tx.send(TrResult::Model(Box::new(model))); - let _ = tx.send(TrResult::Conflicts(conflicts)); - rep.progress(1.0, "done"); - if n == 0 { - Ok("no name-translation conflicts".to_string()) - } else { - Ok(format!("{n} name(s) translated inconsistently — see the panel")) - } - }); - } - - /// Save the whole model to a single project JSON (synchronous, serialization is fast). - fn translation_save_project(&mut self) { - let Some(model) = self.state.translation.model.as_mut() else { - return; - }; - let Some(path) = widgets::save_file( - "translation.wdtr.json", - &[("WolfDawn translation", &["json"])], - ) else { - return; - }; - match translation::save_to_path(model, &path) { - Ok(()) => self.log.info(format!("saved translation project → {}", path.display())), - Err(e) => self.log.error(format!("save failed: {e}")), - } - } - - /// Load a model from a project JSON (synchronous). - fn translation_load_project(&mut self) { - let Some(path) = widgets::pick_file(&[("WolfDawn translation", &["json"])]) else { - return; - }; - match translation::load_from_path(&path) { - Ok(model) => { - let total = model.total_rows(); - self.state.translation.selected = - if model.files.is_empty() { SelectedEntry::Names } else { SelectedEntry::File(0) }; - if self.state.translation.source_dir.is_none() { - self.state.translation.source_dir = Some(model.data_dir.clone()); - } - self.state.translation.model = Some(model); - self.state.translation.conflicts.clear(); - self.log.info(format!("loaded translation project ({total} rows) from {}", path.display())); - } - Err(e) => self.log.error(format!("load failed: {e}")), - } - } - - fn ui_saves(&mut self, ui: &mut egui::Ui) { - // Pump any background result (a freshly inspected / re-inspected save) into the section state - // first, so the rest of this frame renders what the just-finished job produced. - self.poll_saves_result(); - - ui.heading("Saves"); - widgets::caption(ui, "Refresh a baked save's title and player-facing strings to match the translated build."); - widgets::caption( - ui, - "A save bakes in the game's title; a translated build rejects a save whose title doesn't \ - match its own Game.dat (\"trying to load from another game\"). Rewriting the title is \ - exactly what lets an old / Japanese save load in the translated build.", - ); - ui.add_space(6.0); - - // Default the open path / batch dir to the project's Save dir the first time we're shown. - self.saves_default_paths(); - - // A shared backup toggle (used by both the single-file and batch writes). - widgets::labeled_checkbox( - ui, - &mut self.state.saves.backup, - "Back up original", - "Copy each save to a *.bak (single file) or a save_backup_/ folder (batch) \ - before overwriting it. Leave this ON unless you have your own backups.", - ); - ui.add_space(6.0); - - egui::CollapsingHeader::new("Single save") - .default_open(true) - .show(ui, |ui| self.saves_single_pane(ui)); - - ui.add_space(8.0); - - egui::CollapsingHeader::new("Batch update a save folder") - .default_open(false) - .show(ui, |ui| self.saves_batch_pane(ui)); - } - - /// Seed the batch folder from the open project's scanned `Save/` dir, once (only when empty, so a - /// folder the user picked is never clobbered). - fn saves_default_paths(&mut self) { - if self.state.saves.batch_dir.is_none() { - if let Some(dir) = self.project.as_ref().and_then(|p| p.save_dir.clone()) { - self.state.saves.batch_dir = Some(dir); - } - } - } - - /// The single-save pane: open a `.sav`, Inspect it, edit the title + baked strings, and save. - fn saves_single_pane(&mut self, ui: &mut egui::Ui) { - // A project dropdown of the scanned saves (Save/*.sav), above the Browse field. - if let Some(saves) = self.project.as_ref().map(|p| p.saves.clone()) { - let mut path = self.state.saves.sav_path.clone(); - if project_combo( - ui, - "saves_combo", - "Project save", - &saves, - &mut path, - "Pick a .sav from the open game's Save/ folder (or Browse below for one elsewhere).", - ) { - self.state.saves.sav_path = path; - } - } - - let start = self.state.saves.batch_dir.clone(); - let mut path = self.state.saves.sav_path.clone(); - widgets::path_field( - ui, - "Save file", - &mut path, - "A single .sav to inspect and edit.", - move || { - widgets::pick_file_in(start.as_deref(), &[("Wolf save", &["sav"]), ("All files", &["*"])]) - }, - ); - self.state.saves.sav_path = path; - - ui.add_space(4.0); - let running = self.jobs.is_running(); - let have = self.state.saves.sav_path.is_some(); - ui.horizontal(|ui| { - ui.add_enabled_ui(have && !running, |ui| { - if ui - .button("Inspect") - .on_hover_text("Read the save on a background thread: detect its format, decode the title, and list its baked strings.") - .clicked() - { - self.start_inspect_job(); - } - }); - if self.state.saves.loaded.is_some() - && ui - .button("Close save") - .on_hover_text("Clear the loaded save and free its memory (discards unsaved edits).") - .clicked() - { - self.unload_save(); - } - }); - if !have { - widgets::caption(ui, "Pick a .sav (or open a game with a Save/ folder) and press Inspect."); - } - - ui.add_space(6.0); - if self.state.saves.loaded.is_some() { - ui.separator(); - ui.add_space(4.0); - self.saves_editor(ui); - } - } - - /// The loaded-save editor: a format/encoding badge, the editable title, the baked-string table, - /// and the Save-changes button. For an unsupported save, editing is disabled with a clear note. - fn saves_editor(&mut self, ui: &mut egui::Ui) { - let running = self.jobs.is_running(); - let loaded = self.state.saves.loaded.as_mut().expect("loaded checked by caller"); - - // Format badge + encoding. - ui.horizontal(|ui| { - let (label, color) = match loaded.format { - SaveFormat::Standard => ("Standard", egui::Color32::from_rgb(0x40, 0xA0, 0x60)), - SaveFormat::GameProPro => ("GamePro Pro", egui::Color32::from_rgb(0x40, 0x80, 0xE0)), - SaveFormat::Unsupported => ("Unsupported", egui::Color32::from_rgb(0xE0, 0x60, 0x60)), - }; - ui.label(egui::RichText::new(format!(" {label} ")).strong().background_color(color).color(egui::Color32::BLACK)) - .on_hover_text("The detected save codec. Standard and GamePro Pro can be edited; an unsupported save is read-only."); - ui.separator(); - ui.label(format!("Encoding: {}", loaded.encoding)) - .on_hover_text("How the baked strings are stored (utf8 or sjis). A Shift-JIS save can only hold characters representable in Shift-JIS."); - ui.separator(); - ui.label(egui::RichText::new(loaded.path.display().to_string()).weak()); - }); - ui.add_space(6.0); - - if loaded.format == SaveFormat::Unsupported { - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - "Warning: This save's format isn't supported yet, so it can't be edited. Only standard and \ - GamePro Pro saves can be rewritten.", - ); - return; - } - - // Editable title (in a shared form row so it aligns with the section's path rows). - widgets::text_row( - ui, - "Title", - &mut loaded.title, - "The baked game identity. Set this to the translated build's Game.dat title so the \ - save loads there. This is the field that fixes \"trying to load from another game\".", - "", - ); - if loaded.title_changed() { - ui.label( - egui::RichText::new(format!("was: {}", loaded.original_title)).weak().small(), - ); - } - ui.add_space(6.0); - - // Baked-string filter + counter. - let changed = loaded.changed_count(); - let total = loaded.strings.len(); - ui.horizontal(|ui| { - ui.label("Filter:"); - ui.add( - egui::TextEdit::singleline(&mut self.state.saves.filter) - .hint_text("search baked strings") - .desired_width(220.0), - ); - ui.separator(); - ui.label(egui::RichText::new(format!("{changed} edited · {total} strings")).strong()); - }); - ui.add_space(4.0); - - self.saves_string_table(ui); - - ui.add_space(8.0); - let loaded = self.state.saves.loaded.as_ref().expect("loaded"); - let dirty = loaded.title_changed() || loaded.changed_count() > 0; - ui.add_enabled_ui(dirty && !running, |ui| { - if ui - .button("Save changes ▶") - .on_hover_text("Write the edited title + changed baked strings back into the save (backing up the original first if the box is checked).") - .clicked() - { - self.start_save_changes_job(); - } - }); - if !dirty { - widgets::caption(ui, "Edit the title or a baked string to enable Save changes."); - } - } - - /// The baked-string table: a `TableBuilder` with one editable `TextEdit` per string and the - /// original shown dim alongside. Honours the filter box. - fn saves_string_table(&mut self, ui: &mut egui::Ui) { - use egui_extras::{Column, TableBuilder}; - - let loaded = self.state.saves.loaded.as_mut().expect("loaded"); - let needle = self.state.saves.filter.to_lowercase(); - let visible: Vec = loaded - .strings - .iter() - .enumerate() - .filter(|(_, r)| { - needle.is_empty() - || r.original.to_lowercase().contains(&needle) - || r.edited.to_lowercase().contains(&needle) - }) - .map(|(i, _)| i) - .collect(); - - if loaded.strings.is_empty() { - widgets::caption(ui, "This save has no baked player-facing strings to edit."); - return; - } - if visible.is_empty() { - widgets::caption(ui, "No baked strings match the current filter."); - return; - } - - let text_h = egui::TextStyle::Body.resolve(ui.style()).size; - TableBuilder::new(ui) - .striped(true) - .resizable(true) - .cell_layout(egui::Layout::left_to_right(egui::Align::TOP)) - .column(Column::remainder().at_least(180.0)) // Original (dim) - .column(Column::remainder().at_least(180.0)) // Edited - .header(text_h + 6.0, |mut header| { - header.col(|ui| { - ui.strong("Original"); - }); - header.col(|ui| { - ui.strong("Edited"); - }); - }) - .body(|mut body| { - for &i in &visible { - let row = &mut loaded.strings[i]; - body.row(text_h * 2.0, |mut r| { - r.col(|ui| { - ui.add( - egui::Label::new( - egui::RichText::new(&row.original).monospace().weak(), - ) - .wrap(), - ); - }); - r.col(|ui| { - ui.add( - egui::TextEdit::singleline(&mut row.edited) - .desired_width(f32::INFINITY), - ); - }); - }); - } - }); - } - - /// The batch pane: pick a save folder, choose a new title (typed or from a translated Game.dat) - /// and/or a translations file/dir, then run update_save over every *.sav (mirrors the CLI). - fn saves_batch_pane(&mut self, ui: &mut egui::Ui) { - widgets::caption( - ui, - "Apply a new title and/or refreshed baked strings to every .sav in a folder at once - \ - the same as the command-line save-update.", - ); - ui.add_space(4.0); - - let mut dir = self.state.saves.batch_dir.clone(); - let start = dir.clone(); - widgets::path_field( - ui, - "Save folder", - &mut dir, - "The folder holding the .sav files (e.g. the game's Save/ folder).", - move || widgets::pick_folder_in(start.as_deref()), - ); - self.state.saves.batch_dir = dir; - ui.add_space(6.0); - - // Title source: None / Typed / from Game.dat. - let sv = &mut self.state.saves; - ui.horizontal(|ui| { - ui.label("New title:").on_hover_text( - "What to set every save's baked title to. Pick \"From Game.dat\" to copy the \ - translated build's title, so the saves load there.", - ); - ui.radio_value(&mut sv.batch_title_mode, BatchTitleMode::None, "leave as-is") - .on_hover_text("Don't change the title; only refresh baked strings (needs a translations file below)."); - ui.radio_value(&mut sv.batch_title_mode, BatchTitleMode::Typed, "type one"); - ui.radio_value(&mut sv.batch_title_mode, BatchTitleMode::FromGameDat, "from Game.dat") - .on_hover_text("Read the title from a translated Game.dat (the same value the game checks)."); - }); - - match self.state.saves.batch_title_mode { - BatchTitleMode::Typed => { - widgets::text_row( - ui, - "Title text", - &mut self.state.saves.batch_title, - "", - "the translated game title", - ); - } - BatchTitleMode::FromGameDat => { - let mut gd = self.state.saves.batch_game_dat.clone(); - widgets::path_field( - ui, - "Game.dat", - &mut gd, - "A translated Game.dat; its Title is read and applied to every save.", - || widgets::pick_file(&[("Game data", &["dat"]), ("All files", &["*"])]), - ); - self.state.saves.batch_game_dat = gd; - } - BatchTitleMode::None => {} - } - ui.add_space(6.0); - - // Optional translations source for the baked-string refresh. - let mut tr = self.state.saves.batch_translations.clone(); - widgets::path_field( - ui, - "Translations", - &mut tr, - "Optional: a translation .json file or a folder of them. Their source→target text is \ - used to refresh the saves' baked strings (the same map the game itself uses).", - || { - // Prefer a folder, fall back to a single JSON file. - widgets::pick_folder().or_else(|| widgets::pick_file(&[("Translation JSON", &["json"])])) - }, - ); - self.state.saves.batch_translations = tr; - - ui.add_space(8.0); - let running = self.jobs.is_running(); - let have_dir = self.state.saves.batch_dir.is_some(); - // Something to do? a title source that yields a title, or a translations path. - let has_title = match self.state.saves.batch_title_mode { - BatchTitleMode::None => false, - BatchTitleMode::Typed => !self.state.saves.batch_title.trim().is_empty(), - BatchTitleMode::FromGameDat => self.state.saves.batch_game_dat.is_some(), - }; - let has_tr = self.state.saves.batch_translations.is_some(); - let ready = have_dir && (has_title || has_tr); - ui.add_enabled_ui(ready && !running, |ui| { - if ui - .button("Batch update ▶") - .on_hover_text("Update every .sav in the folder. Unsupported saves are skipped, not mangled.") - .clicked() - { - self.start_batch_job(); - } - }); - if !ready { - widgets::caption( - ui, - "Pick a save folder and choose a new title and/or a translations file to enable Batch update.", - ); - } - } - - // --- Saves background jobs + sync helpers --------------------------------------------- - - /// Drain the heavy-result side-channel: a finished single-file job hands its (re-)inspected save - /// here (mirrors `poll_translation_result`). - fn poll_saves_result(&mut self) { - let Some(rx) = &self.state.saves.result_rx else { - return; - }; - let mut disconnected = false; - loop { - match rx.try_recv() { - Ok(SaveResult::Loaded(s)) => { - self.state.saves.loaded = Some(*s); - self.state.saves.filter.clear(); - } - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - disconnected = true; - break; - } - } - } - if disconnected && !self.jobs.is_running() { - self.state.saves.result_rx = None; - } - } - - /// Spawn the Inspect job: read + inspect the save off the worker thread, hand the editable model - /// back via the side-channel, and report the format/encoding/counts through the job log. - fn start_inspect_job(&mut self) { - let Some(path) = self.state.saves.sav_path.clone() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.saves.result_rx = Some(rx); - self.jobs.run(format!("Inspect {}", path.display()), &mut self.log, move |rep| { - rep.progress(0.2, "reading save…"); - let loaded = saves::inspect(&path)?; - let summary = format!( - "{} save, {} encoding, title {:?}, {} baked string(s)", - loaded.format.label(), - loaded.encoding, - loaded.original_title, - loaded.strings.len() - ); - if !loaded.editable() { - rep.warn("this save's format isn't supported; it opened read-only"); - } - let _ = tx.send(SaveResult::Loaded(Box::new(loaded))); - rep.progress(1.0, "done"); - Ok(summary) - }); - } - - /// Spawn the Save-changes job: write the edited save (backing up first), then re-inspect the - /// written bytes and hand the fresh model back so the UI shows the persisted state. - fn start_save_changes_job(&mut self) { - // Move the loaded save onto the worker. It comes back (re-inspected) via the side-channel. - let Some(loaded) = self.state.saves.loaded.take() else { - return; - }; - let backup = self.state.saves.backup; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.saves.result_rx = Some(rx); - self.jobs.run("Save changes".to_string(), &mut self.log, move |rep| { - rep.progress(0.2, "writing save…"); - let path = loaded.path.clone(); - let result = saves::save_changes(&loaded, backup); - // Whatever happens, hand a model back so the editor isn't left empty: the re-inspected - // file on success, or the (unchanged) in-memory model on failure. - match result { - Ok(stats) => { - rep.info(format!( - "title changed: {} · strings replaced: {} · encoding: {}", - stats.title_changed, stats.strings_replaced, stats.encoding - )); - if backup { - rep.info(format!("backed up original → {}.bak", path.display())); - } - match saves::inspect(&path) { - Ok(fresh) => { - let _ = tx.send(SaveResult::Loaded(Box::new(fresh))); - } - Err(e) => { - rep.warn(format!("re-inspect after write failed: {e}")); - let _ = tx.send(SaveResult::Loaded(Box::new(loaded))); - } - } - rep.progress(1.0, "done"); - Ok(format!( - "saved {} (title {}, {} string(s) replaced)", - path.display(), - if stats.title_changed { "updated" } else { "unchanged" }, - stats.strings_replaced - )) - } - Err(e) => { - // Put the model back unchanged so the user's edits aren't lost. - let _ = tx.send(SaveResult::Loaded(Box::new(loaded))); - rep.progress(1.0, "done"); - Err(e) - } - } - }); - } - - /// Spawn the Batch job: resolve the title (typed or from Game.dat) + the string map (from the - /// translations path) on the UI thread, then run update_save over every *.sav on the worker. - fn start_batch_job(&mut self) { - let sv = &self.state.saves; - let Some(dir) = sv.batch_dir.clone() else { - return; - }; - // Resolve the new title up front (Game.dat read is cheap and surfaces errors immediately). - let new_title: Option = match sv.batch_title_mode { - BatchTitleMode::None => None, - BatchTitleMode::Typed => { - let t = sv.batch_title.trim(); - (!t.is_empty()).then(|| t.to_string()) - } - BatchTitleMode::FromGameDat => match &sv.batch_game_dat { - Some(gd) => match saves::title_from_game_dat(gd) { - Ok(t) => { - self.log.info(format!("batch: title from {} = {t:?}", gd.display())); - Some(t) - } - Err(e) => { - self.log.error(format!("batch: {e}")); - return; - } - }, - None => None, - }, - }; - // Build the baked-string map from the translations path, if given. - let map = match &sv.batch_translations { - Some(p) => match saves::build_string_map(std::slice::from_ref(p)) { - Ok((m, conflicts)) => { - if conflicts > 0 { - self.log.warn(format!( - "batch: {conflicts} source(s) had divergent translations across files; kept the first" - )); - } - m - } - Err(e) => { - self.log.error(format!("batch: {e}")); - return; - } - }, - None => std::collections::HashMap::new(), - }; - let backup = sv.backup; - - self.jobs.run(format!("Batch update {}", dir.display()), &mut self.log, move |rep| { - rep.progress(0.0, "scanning saves…"); - let outcome = saves::batch_update( - &dir, - new_title.as_deref(), - &map, - backup, - |frac, msg| rep.progress(frac, msg), - )?; - if let Some(bdir) = &outcome.backup_dir { - rep.info(format!("backed up originals → {}", bdir.display())); - } - for entry in &outcome.entries { - match entry { - saves::BatchEntry::Updated(name, stats) => rep.info(format!( - " {name}: title={} strings={} enc={}", - u8::from(stats.title_changed), - stats.strings_replaced, - stats.encoding - )), - saves::BatchEntry::Skipped(name) => { - rep.warn(format!(" {name}: SKIPPED (unsupported save format)")) - } - saves::BatchEntry::Failed(name, msg) => { - rep.error(format!(" {name}: {msg}")) - } - } - } - rep.progress(1.0, "done"); - Ok(format!( - "{} updated, {} skipped, {} failed", - outcome.updated(), - outcome.skipped(), - outcome.failed() - )) - }); - } - - fn ui_verify(&mut self, ui: &mut egui::Ui) { - // Pump any background result (a single verdict / a corpus outcome) into section state first. - self.poll_verify_result(); - - ui.heading("Verify"); - widgets::caption( - ui, - "Verify confirms a file decodes and re-encodes without loss (byte-exact where possible) \ - — your safety net before shipping.", - ); - ui.add_space(6.0); - - // Default the corpus folder to the open project's data dir the first time we're shown. - if self.state.verify.corpus_dir.is_none() { - if let Some(d) = self.project.as_ref().and_then(|p| p.data_dir.clone()) { - self.state.verify.corpus_dir = Some(d); - } - } - - egui::CollapsingHeader::new("Single file") - .default_open(true) - .show(ui, |ui| self.verify_single_pane(ui)); - - ui.add_space(8.0); - - egui::CollapsingHeader::new("Corpus (a whole folder)") - .default_open(true) - .show(ui, |ui| self.verify_corpus_pane(ui)); - } - - /// The single-file pane: pick a data file, Verify it, show a PASS/FAIL/ERR result panel. - fn verify_single_pane(&mut self, ui: &mut egui::Ui) { - let mut file = self.state.verify.file.clone(); - widgets::path_field( - ui, - "Data file", - &mut file, - "Any single data file (.mps map, CommonEvent.dat, Game.dat, or a database .project).", - || { - widgets::pick_file(&[ - ("Wolf data", &["mps", "dat", "project"]), - ("All files", &["*"]), - ]) - }, - ); - self.state.verify.file = file; - - // Warn early if the pick isn't a verifiable file type. Verify still runs and gives an ERR verdict. - if let Some(f) = &self.state.verify.file { - if !verify::is_supported(f) { - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - "Warning: this is not a verifiable file type (.mps, CommonEvent.dat, Game.dat, or a database .project).", - ); - } - } - - ui.add_space(4.0); - let running = self.jobs.is_running(); - let have = self.state.verify.file.is_some(); - ui.add_enabled_ui(have && !running, |ui| { - if ui - .button("Verify ▶") - .on_hover_text("Round-trip this file on a background thread and report PASS / FAIL.") - .clicked() - { - self.start_verify_file_job(); - } - }); - if !have { - widgets::caption(ui, "Pick a data file to verify."); - } - - // The result panel for the last single-file verdict. - if let Some((path, verdict)) = &self.state.verify.file_verdict { - ui.add_space(6.0); - let (color, tag) = verdict_color(verdict); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!(" {tag} ")) - .strong() - .background_color(color) - .color(egui::Color32::BLACK), - ); - ui.label(egui::RichText::new(path.display().to_string()).weak()); - }); - ui.label(verdict.detail()); - } - } - - /// The corpus pane: pick a folder, Verify all, show a per-file table + the byte-exact summary. - fn verify_corpus_pane(&mut self, ui: &mut egui::Ui) { - let start = self.state.verify.corpus_dir.clone(); - let mut dir = self.state.verify.corpus_dir.clone(); - widgets::path_field( - ui, - "Folder", - &mut dir, - "A folder of game data; every supported file under it is round-tripped.", - move || widgets::pick_folder_in(start.as_deref()), - ); - self.state.verify.corpus_dir = dir; - - ui.add_space(4.0); - let running = self.jobs.is_running(); - let have = self.state.verify.corpus_dir.is_some(); - ui.add_enabled_ui(have && !running, |ui| { - if ui - .button("Verify all ▶") - .on_hover_text("Walk the folder and round-trip every supported file on a background thread.") - .clicked() - { - self.start_verify_corpus_job(); - } - }); - if !have { - widgets::caption(ui, "Pick a folder to verify its files."); - } - - if let Some(outcome) = &self.state.verify.corpus { - ui.add_space(6.0); - let (pass, fail, err, total) = - (outcome.passed(), outcome.failed(), outcome.errored(), outcome.total()); - let summary = egui::RichText::new(format!("{pass}/{total} byte-exact")) - .strong() - .color(if fail == 0 && err == 0 { - egui::Color32::from_rgb(0x40, 0xA0, 0x60) - } else { - egui::Color32::from_rgb(0xE0, 0xB0, 0x40) - }); - ui.horizontal(|ui| { - ui.label(summary); - if fail > 0 || err > 0 { - ui.label( - egui::RichText::new(format!("· {fail} differ · {err} error")).weak(), - ); - } - }); - ui.add_space(4.0); - self.verify_corpus_table(ui); - } - } - - /// The per-file corpus result table: file | result. - fn verify_corpus_table(&mut self, ui: &mut egui::Ui) { - use egui_extras::{Column, TableBuilder}; - - let Some(outcome) = &self.state.verify.corpus else { - return; - }; - let text_h = egui::TextStyle::Body.resolve(ui.style()).size; - TableBuilder::new(ui) - .striped(true) - .resizable(true) - .cell_layout(egui::Layout::left_to_right(egui::Align::TOP)) - .column(Column::remainder().at_least(200.0)) // File - .column(Column::auto().at_least(220.0)) // Result - .header(text_h + 6.0, |mut header| { - header.col(|ui| { - ui.strong("File"); - }); - header.col(|ui| { - ui.strong("Result"); - }); - }) - .body(|mut body| { - for row in &outcome.rows { - body.row(text_h * 1.8, |mut r| { - r.col(|ui| { - ui.add( - egui::Label::new(egui::RichText::new(&row.rel).monospace()).wrap(), - ); - }); - r.col(|ui| { - let (color, tag) = verdict_color(&row.verdict); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!(" {tag} ")) - .strong() - .background_color(color) - .color(egui::Color32::BLACK), - ); - if !row.verdict.is_pass() { - ui.add( - egui::Label::new( - egui::RichText::new(row.verdict.detail()).weak().small(), - ) - .wrap(), - ); - } - }); - }); - }); - } - }); - } - - // --- Verify background jobs + sync helpers --------------------------------------------- - - /// Drain the heavy-result side-channel: a finished verify job hands its verdict / corpus here - /// (mirrors `poll_translation_result` / `poll_saves_result`). - fn poll_verify_result(&mut self) { - let Some(rx) = &self.state.verify.result_rx else { - return; - }; - let mut disconnected = false; - loop { - match rx.try_recv() { - Ok(VerifyResult::Single(path, v)) => { - self.state.verify.file_verdict = Some((path, *v)); - } - Ok(VerifyResult::Corpus(c)) => self.state.verify.corpus = Some(*c), - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - disconnected = true; - break; - } - } - } - if disconnected && !self.jobs.is_running() { - self.state.verify.result_rx = None; - } - } - - /// Spawn the single-file Verify job: round-trip the file on a worker thread, hand the verdict - /// back via the side-channel, and log a PASS/FAIL/ERR line. - fn start_verify_file_job(&mut self) { - let Some(path) = self.state.verify.file.clone() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.verify.result_rx = Some(rx); - self.jobs.run(format!("Verify {}", path.display()), &mut self.log, move |rep| { - rep.progress(0.2, "round-tripping…"); - let verdict = verify::verify_file(&path); - let summary = format!("{} — {}", verdict.tag(), verdict.detail()); - match &verdict { - Verdict::Pass => rep.info(summary.clone()), - Verdict::Fail { .. } => rep.warn(summary.clone()), - Verdict::Error(_) => rep.error(summary.clone()), - } - let _ = tx.send(VerifyResult::Single(path.clone(), Box::new(verdict))); - rep.progress(1.0, "done"); - Ok(summary) - }); - } - - /// Spawn the Corpus Verify job: walk + round-trip every supported file on a worker thread, hand - /// the outcome back via the side-channel, and log the summary. - fn start_verify_corpus_job(&mut self) { - let Some(dir) = self.state.verify.corpus_dir.clone() else { - return; - }; - let (tx, rx) = std::sync::mpsc::channel(); - self.state.verify.result_rx = Some(rx); - self.jobs.run(format!("Verify corpus {}", dir.display()), &mut self.log, move |rep| { - let outcome = verify::verify_corpus(&dir, |frac, msg| rep.progress(frac, msg))?; - let (pass, fail, err, total) = - (outcome.passed(), outcome.failed(), outcome.errored(), outcome.total()); - // Surface every non-pass file in the log too. - for row in &outcome.rows { - if !row.verdict.is_pass() { - rep.warn(format!(" {} — {}: {}", row.verdict.tag(), row.rel, row.verdict.detail())); - } - } - let _ = tx.send(VerifyResult::Corpus(Box::new(outcome))); - Ok(format!("{pass}/{total} byte-exact ({fail} differ, {err} error)")) - }); - } - - fn ui_settings(&mut self, ui: &mut egui::Ui) { - ui.heading("Settings"); - widgets::caption(ui, "Theme and tool preferences. These persist across runs."); - ui.add_space(8.0); - - // --- Theme --- - ui.label(egui::RichText::new("Theme").strong()); - ui.add_space(2.0); - ui.horizontal(|ui| { - // A Dark/Light selector. Applying the visuals immediately on a change keeps it live. - let mut dark = self.settings.dark_mode; - let mut changed = ui - .selectable_value(&mut dark, true, "Dark") - .on_hover_text("Dark theme (the default).") - .changed(); - changed |= ui - .selectable_value(&mut dark, false, "Light") - .on_hover_text("Light theme.") - .changed(); - if changed { - self.settings.dark_mode = dark; - self.apply_theme(ui.ctx()); - } - }); - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - // --- Translation defaults --- - ui.label(egui::RichText::new("Translation defaults").strong()); - ui.add_space(2.0); - widgets::caption( - ui, - "Used to initialise the Translation section's inject options; changing one here doesn't \ - override what you've already set this session.", - ); - widgets::labeled_checkbox( - ui, - &mut self.settings.default_en_punct, - "Default --en-punct on", - "Start the Translation section with \"convert Japanese punctuation to ASCII\" enabled.", - ); - widgets::labeled_checkbox( - ui, - &mut self.settings.default_allow_code_drift, - "Default --allow-code-drift on", - "Start the Translation section with the strict control-code guard relaxed (risky - leave \ - OFF unless you know you need it).", - ); - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - // --- Saves / Database defaults --- - ui.label(egui::RichText::new("Saves & Database defaults").strong()); - ui.add_space(2.0); - widgets::labeled_checkbox( - ui, - &mut self.settings.default_backup, - "Back up originals by default", - "Start the Saves / Database / Game.dat sections with \"back up originals before \ - overwriting\" enabled. Leave ON unless you keep your own backups.", - ); - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - // --- Auto-detect --- - ui.label(egui::RichText::new("Files").strong()); - ui.add_space(2.0); - widgets::labeled_checkbox( - ui, - &mut self.settings.auto_detect, - "Auto-detect new files", - "While a game is open, periodically re-scan its folders so newly-added maps / databases \ - / saves (e.g. right after an Unpack) appear in the per-section dropdowns automatically.", - ); - if !self.settings.auto_detect { - widgets::caption( - ui, - "Auto-detect is off — use the Refresh button on the Project section to re-scan.", - ); - } - - ui.add_space(10.0); - widgets::caption(ui, "Settings are saved automatically when you close the app."); - } - - // --- chrome --------------------------------------------------------------------------- - - /// Top panel: app title + a one-line summary of the open project. - fn top_bar(&mut self, ctx: &egui::Context) { - egui::TopBottomPanel::top("top_bar").show(ctx, |ui| { - ui.horizontal(|ui| { - ui.heading("WolfDawn Studio"); - ui.separator(); - match &self.project { - None => { - ui.label(egui::RichText::new("No project").weak()); - } - Some(p) => { - let title = if p.title.is_empty() { "(untitled)" } else { &p.title }; - ui.label(egui::RichText::new(title).strong()); - ui.separator(); - ui.label(project::encoding_label(p.utf8)); - if !p.version.is_empty() { - ui.separator(); - ui.label(&p.version); - } - if !p.font.is_empty() { - ui.separator(); - ui.label(format!("font: {}", p.font)); - } - } - } - }); - }); - } - - /// Left panel: one selectable button per section. - fn side_bar(&mut self, ctx: &egui::Context) { - egui::SidePanel::left("nav") - .resizable(false) - .exact_width(150.0) - .show(ctx, |ui| { - ui.add_space(6.0); - ui.spacing_mut().item_spacing.y = 2.0; - for section in Section::ALL { - let selected = self.section == section; - if nav_button(ui, selected, section.name()).clicked() { - self.section = section; - } - } - }); - } - - /// Bottom panel: the log, plus the progress bar while a job runs. - fn bottom_bar(&mut self, ctx: &egui::Context) { - egui::TopBottomPanel::bottom("status") - .resizable(true) - .default_height(150.0) - .show(ctx, |ui| { - if self.jobs.is_running() { - ui.horizontal(|ui| { - ui.label(egui::RichText::new(self.jobs.label()).strong()); - if !self.jobs.status().is_empty() { - ui.label(egui::RichText::new(self.jobs.status()).weak()); - } - }); - ui.add( - egui::ProgressBar::new(self.jobs.progress()) - .show_percentage() - .animate(true), - ); - ui.separator(); - } - ui.label(egui::RichText::new("Log").weak()); - self.log.show(ui); - }); - } -} - -impl WolfDawnApp { - /// Lay out and render one frame's worth of UI into `ctx`: pump the job channel, draw the four - /// panels, and request a repaint while a job runs. This is the whole per-frame body, factored - /// out of [`eframe::App::update`] so the headless tests can drive a full frame without an - /// `eframe::Frame` (which has no public constructor). - pub fn render_frame(&mut self, ctx: &egui::Context) { - // Pump the background job channel first so progress/log are current for this frame. - let running = self.jobs.poll(&mut self.log); - - // A job just finished this frame (running→idle): if it was an Unpack, immediately rescan the - // project index so the extracted files appear in the dropdowns now, not on the next throttle. - if self.job_was_running && !running { - self.on_job_finished(); - } - self.job_was_running = running; - - // Live auto-detect: while a project is open (and auto-detect is enabled), periodically - // re-walk its small index dirs so files added externally (or by a just-finished Unpack) - // surface in the per-section dropdowns without a manual refresh. - self.tick_auto_detect(); - - self.top_bar(ctx); - self.side_bar(ctx); - self.bottom_bar(ctx); - egui::CentralPanel::default().show(ctx, |ui| { - self.render_current_section(ui); - }); - - // While a job runs, keep animating even without user input so the bar moves and messages - // arrive promptly (otherwise egui only repaints on interaction). - if running { - ctx.request_repaint(); - } else if self.project.is_some() && self.settings.auto_detect { - // Keep the auto-detect tick running without user input while a project is open: ask for - // a repaint ~1s out so the throttled rescan keeps firing even on an idle window. - ctx.request_repaint_after(Duration::from_secs(1)); - } - } - - /// The throttled auto-detect tick: when a project is open and auto-detect is on, re-walk its - /// index at most every ~1.5s. Log a one-line summary only when the file set actually changed. - fn tick_auto_detect(&mut self) { - /// How long between background rescans of the open project's index dirs. - const RESCAN_EVERY: Duration = Duration::from_millis(1500); - - if self.project.is_none() || !self.settings.auto_detect { - // Reset the clock so re-enabling / re-opening rescans promptly rather than waiting. - self.last_scan = None; - return; - } - let due = self.last_scan.map_or(true, |t| t.elapsed() >= RESCAN_EVERY); - if !due { - return; - } - self.last_scan = Some(Instant::now()); - if let Some(project) = self.project.as_mut() { - if project.rescan() { - self.log.info(format!( - "detected file changes ({} code file(s), {} database(s), {} save(s))", - project.code_files.len(), - project.databases.len(), - project.saves.len(), - )); - } - } - } - - /// Handle a job finishing (the running→idle transition). When the finished job was an Unpack and - /// a project is open: adopt the output dir as `data_dir` if it sits under the project root (so - /// Translation/Verify default to the just-extracted folder), then rescan the index immediately - /// so the freshly-extracted code/databases/saves populate the dropdowns at once. - fn on_job_finished(&mut self) { - let Some(out) = self.pending_unpack_out.take() else { - return; - }; - let Some(project) = self.project.as_mut() else { - return; - }; - // Adopt the unpack output as the data dir when it lives under (or is) the project root. - if out.starts_with(&project.root) && project.data_dir.as_deref() != Some(out.as_path()) { - project.data_dir = Some(out.clone()); - self.log.info(format!("data dir set to unpack output: {}", out.display())); - } - if project.rescan() { - self.log.info(format!( - "detected file changes ({} code file(s), {} database(s), {} save(s)) after unpack", - project.code_files.len(), - project.databases.len(), - project.saves.len(), - )); - } - // Force the next throttled tick to run promptly too (the index just moved). - self.last_scan = Some(Instant::now()); - } -} - -impl eframe::App for WolfDawnApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - self.render_frame(ctx); - } - - /// Persist the user [`Settings`] to eframe's on-disk storage. eframe calls this periodically and - /// on exit (the `persistence` feature provides the storage backend under the OS config dir). - fn save(&mut self, storage: &mut dyn eframe::Storage) { - eframe::set_value(storage, Self::SETTINGS_KEY, &self.settings); - } -} - -/// Render the open project's details into the Project body. -fn project_details(ui: &mut egui::Ui, p: &Project) { - egui::Grid::new("project_details") - .num_columns(2) - .spacing([16.0, 6.0]) - .striped(true) - .show(ui, |ui| { - row(ui, "Title", if p.title.is_empty() { "—".into() } else { p.title.clone() }); - row(ui, "Version", if p.version.is_empty() { "—".into() } else { p.version.clone() }); - row(ui, "Encoding", project::encoding_label(p.utf8).to_string()); - row(ui, "Font", if p.font.is_empty() { "—".into() } else { p.font.clone() }); - row(ui, "Game root", p.root.display().to_string()); - row(ui, "Data.wolf", opt_path(&p.data_wolf)); - row(ui, "Data dir", opt_path(&p.data_dir)); - row(ui, "Game.dat", opt_path(&p.game_dat)); - row( - ui, - "Indexed", - format!( - "{} code file(s) · {} database(s) · {} save(s)", - p.code_files.len(), - p.databases.len(), - p.saves.len(), - ), - ); - }); - - if !p.game_dat_ok { - ui.add_space(8.0); - ui.colored_label( - egui::Color32::from_rgb(0xE0, 0xB0, 0x40), - "Warning: Game.dat was not found or could not be read - the project is open with what was located. See the log below.", - ); - } -} - -fn row(ui: &mut egui::Ui, key: &str, value: String) { - ui.label(egui::RichText::new(key).strong()); - ui.label(value); - ui.end_row(); -} - -fn opt_path(p: &Option) -> String { - p.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "—".into()) -} - -/// Hand a (possibly large) model back to the UI thread over the translation side-channel. Boxed so -/// the `TrResult` enum stays small. A send failure (UI hung up / app closing) is a no-op. -fn send_model(tx: &Sender, model: TranslationModel) { - let _ = tx.send(TrResult::Model(Box::new(model))); -} - -/// A project-file dropdown: a `ComboBox` over `options` (the scanned index for this section). The -/// shown text is each path's basename (or its tail relative to a common ancestor when basenames -/// collide, so two `Map001.mps` from different folders stay distinguishable). The *full* path is -/// stored into `target` on selection. Returns `true` when the selection changed this frame. -/// -/// Paths are stored as `Option` everywhere in the sections (matching `widgets::path_field`), -/// so the dropdown reuses that type rather than a raw `String`. Renders nothing (and returns false) -/// when there are no options, so a section with an empty index falls back to its Browse button. -fn project_combo( - ui: &mut egui::Ui, - id: &str, - label: &str, - options: &[PathBuf], - target: &mut Option, - tooltip: &str, -) -> bool { - if options.is_empty() { - return false; - } - let mut changed = false; - // Use the shared form row so the combo's label sits in the same fixed left column as the path - // rows, and fill the width up to the same right edge those rows' inputs reach (leaving the - // BTN_COL reserve), so the dropdown lines up with the Browse fields under it. - widgets::form_row(ui, label, tooltip, |ui| { - // The selected text: the current target's display name, or a prompt when none is chosen. - let selected_text = target - .as_ref() - .map(|p| combo_label(p, options)) - .unwrap_or_else(|| "— pick from project —".to_string()); - let combo_w = (ui.available_width() - widgets::BTN_COL_W).max(120.0); - egui::ComboBox::from_id_salt(id) - .selected_text(selected_text) - .width(combo_w) - .show_ui(ui, |ui| { - for opt in options { - let is_sel = target.as_deref() == Some(opt.as_path()); - if ui - .selectable_label(is_sel, combo_label(opt, options)) - .on_hover_text(opt.display().to_string()) - .clicked() - && !is_sel - { - *target = Some(opt.clone()); - changed = true; - } - } - }) - .response - .on_hover_text(tooltip); - }); - changed -} - -/// The label shown for one project-combo entry: the file's basename, disambiguated with its parent -/// dir's name when another option shares the same basename (e.g. `MapData/Map001.mps`). -fn combo_label(path: &Path, options: &[PathBuf]) -> String { - let base = path - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| path.display().to_string()); - let collides = options - .iter() - .filter(|o| o.as_path() != path) - .any(|o| o.file_name() == path.file_name()); - if collides { - if let Some(parent) = path.parent().and_then(|p| p.file_name()) { - return format!("{}/{}", parent.to_string_lossy(), base); - } - } - base -} - -/// A full-width sidebar nav row. Text-only (no icon column): emoji pictographs render as tofu on -/// systems lacking an emoji font, so the sidebar shows just the section name, left-aligned at a -/// fixed x so every row's text starts at the same column. Mimics `SelectableLabel` visuals. -fn nav_button(ui: &mut egui::Ui, selected: bool, name: &str) -> egui::Response { - let height = 26.0; - let (rect, resp) = - ui.allocate_exact_size(egui::vec2(ui.available_width(), height), egui::Sense::click()); - let visuals = ui.style().interact_selectable(&resp, selected); - if selected { - ui.painter() - .rect_filled(rect, 4.0, ui.visuals().selection.bg_fill); - } else if resp.hovered() { - ui.painter().rect_filled(rect, 4.0, visuals.weak_bg_fill); - } - let color = visuals.text_color(); - let font = egui::FontId::proportional(15.0); - let mid_y = rect.center().y; - ui.painter().text( - egui::pos2(rect.left() + 10.0, mid_y), - egui::Align2::LEFT_CENTER, - name, - font, - color, - ); - resp -} - -/// A full-width, multi-line-aware selectable list row (used by the Database type list). Unlike -/// `ui.selectable_label`, this allocates the row at the *wrapped* text height and paints its own -/// rounded, padded highlight, so a long type name that wraps to two lines (e.g. -/// `アニメーション / Animation (34 rows)`) is fully contained by the highlight instead of being -/// clipped by a harsh single-line fill. Mirrors `nav_button`'s painting approach: selected uses -/// `selection.bg_fill`, hovered uses a subtle `weak_bg_fill`, with readable selected-text colour. -/// Returns the row's `Response` (so the caller handles `.clicked()`). -fn selectable_row(ui: &mut egui::Ui, selected: bool, text: &str) -> egui::Response { - /// Inner padding around the label inside the highlight. - const PAD_X: f32 = 8.0; - const PAD_Y: f32 = 4.0; - /// Rounded-corner radius of the highlight (matches `nav_button`). - const ROUNDING: f32 = 4.0; - - let full_w = ui.available_width(); - // Lay out the label wrapped to the row width (minus the horizontal padding) so we know how tall - // the row must be to contain every wrapped line. - let wrap_w = (full_w - 2.0 * PAD_X).max(1.0); - let galley = ui.painter().layout( - text.to_owned(), - egui::TextStyle::Body.resolve(ui.style()), - // PLACEHOLDER so the glyph colour is taken from the `fallback_color` passed to - // `painter.galley` below, the interaction visuals' (selected-aware) text colour. - egui::Color32::PLACEHOLDER, - wrap_w, - ); - let row_h = galley.size().y + 2.0 * PAD_Y; - - let (rect, resp) = - ui.allocate_exact_size(egui::vec2(full_w, row_h), egui::Sense::click()); - let visuals = ui.style().interact_selectable(&resp, selected); - if selected { - ui.painter() - .rect_filled(rect, ROUNDING, ui.visuals().selection.bg_fill); - } else if resp.hovered() { - ui.painter().rect_filled(rect, ROUNDING, visuals.weak_bg_fill); - } - // Draw the (already-wrapped) label with padding inside the highlight, using the interaction - // visuals' text colour so the selected row stays readable. - ui.painter().galley( - egui::pos2(rect.left() + PAD_X, rect.top() + PAD_Y), - galley, - visuals.text_color(), - ); - resp -} - -/// Parse a cryptVersion from the Repack "match version" field: accepts `0x14b` (hex) or a decimal -/// number, mirroring `cmd_pack`'s `--version` parsing. `None` for anything that doesn't parse. -fn parse_crypt_version(text: &str) -> Option { - let t = text.trim(); - if t.is_empty() { - return None; - } - let stripped = t.trim_start_matches("0x").trim_start_matches("0X"); - u16::from_str_radix(stripped, 16).ok().or_else(|| t.parse().ok()) -} - -/// The badge colour + tag word for a verify [`Verdict`] (green PASS / amber FAIL / red ERR). -fn verdict_color(verdict: &Verdict) -> (egui::Color32, &'static str) { - match verdict { - Verdict::Pass => (egui::Color32::from_rgb(0x40, 0xA0, 0x60), "PASS"), - Verdict::Fail { .. } => (egui::Color32::from_rgb(0xE0, 0xB0, 0x40), "FAIL"), - Verdict::Error(_) => (egui::Color32::from_rgb(0xE0, 0x60, 0x60), "ERR"), - } -} - -/// Append a Japanese system font as a fallback so CJK glyphs render. egui ships Latin-only fonts, so -/// without this every Japanese string in the tool shows as tofu (□). The JP font goes to the END of -/// both families, so Latin keeps the crisp default and only glyphs the default lacks (kana/kanji) -/// fall back to it. Tries a few common Windows fonts. If none is found it's a no-op (Latin only). -fn install_cjk_font(ctx: &egui::Context) { - const CANDIDATES: &[&str] = &[ - r"C:\Windows\Fonts\YuGothR.ttc", - r"C:\Windows\Fonts\meiryo.ttc", - r"C:\Windows\Fonts\msgothic.ttc", - r"C:\Windows\Fonts\YuGothM.ttc", - r"C:\Windows\Fonts\msmincho.ttc", - ]; - let Some(bytes) = CANDIDATES.iter().find_map(|p| std::fs::read(p).ok()) else { - return; - }; - let mut fonts = egui::FontDefinitions::default(); - fonts - .font_data - .insert("jp".to_owned(), egui::FontData::from_owned(bytes)); - for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] { - fonts.families.entry(family).or_default().push("jp".to_owned()); - } - ctx.set_fonts(fonts); -} - -/// A short, always-renderable text tag for each translatable file kind in the left list (shown in -/// brackets before the file name). Replaces the old emoji icons so the list never tofus. -fn kind_icon(kind: FileKind) -> &'static str { - match kind { - FileKind::Map => "[Map]", - FileKind::CommonEvent => "[Common]", - FileKind::Database => "[DB]", - FileKind::GameDat => "[Game]", - FileKind::TxtEvent => "[Text]", - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/archive.rs b/util/wolfdawn-master/crates/wolf-gui/src/archive.rs deleted file mode 100644 index 92dabaf..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/archive.rs +++ /dev/null @@ -1,463 +0,0 @@ -//! The Archive section's headless logic: unpack a `.wolf` to a folder and repack a folder back -//! into a `.wolf`, calling the **same** `wolf_archive` library functions the CLI's `cmd_unpack` / -//! `cmd_pack` use (no shelling out). The egui rendering lives in `app.rs`. This module owns the -//! file-level work so the whole flow can be exercised headlessly in tests. -//! -//! * [`unpack`] mirrors `cmd_unpack`: read the archive, call [`wolf_archive::extract_archive`] -//! (which handles any supported crypt regime), then write each file into the -//! output dir under a *sanitised* relative path (the same `..`/`.`-stripping the CLI does, so a -//! hostile or malformed archive path can't escape the output folder). -//! * [`repack`] mirrors `cmd_pack`: gather `(relative-path, bytes)` for every file under the input -//! folder, then call the matching `wolf_archive::pack_*` path for the chosen [`PackOptions`] -//! (plaintext / encrypted / new-crypt / `--like` an existing archive / the legacy ver5|ver6 -//! containers). -//! -//! Repack fidelity: an unpack/repack/unpack round-trips the *file set and contents* byte-for-byte, -//! but a fresh `.wolf` is not guaranteed byte-identical to an editor-produced original (see the -//! `wolf_archive::pack` notes, the header/compression differs slightly). The library's own -//! `extract_archive` reads our output back losslessly, which is what the verify/round-trip tests -//! assert. - -use std::path::{Path, PathBuf}; - -/// The CLI's default cryptVersion when `--encrypt` is given without an explicit `--version` -/// (Wolf v3.00). Kept in sync with `cmd_pack`'s `version` default. -pub const DEFAULT_CRYPT_VERSION: u16 = 0x12C; - -/// The default new-crypt password used when encrypting a new-crypt version (`0x14B`/`0x15E`) -/// without a `--like` source or an explicit password. Mirrors `cmd_pack`'s `default_pwd`. The -/// archive embeds whatever password it was built with, so it stays self-describing. -const DEFAULT_NEWCRYPT_PWD: [u8; 15] = *b"WolfDawnRepack!"; - -/// The container format the user picked, mirroring the CLI `pack --format` flag. `Auto` is the -/// modern VER8 container (plaintext, or encrypted per [`PackOptions::encrypt`]); `Ver5`/`Ver6` are -/// the legacy DXArchive containers for Wolf 2.0x. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub enum PackFormat { - #[default] - Auto, - Ver5, - Ver6, -} - -impl PackFormat { - /// The dropdown label. - pub fn label(self) -> &'static str { - match self { - PackFormat::Auto => "auto", - PackFormat::Ver5 => "ver5", - PackFormat::Ver6 => "ver6", - } - } -} - -/// Where the encryption parameters for an encrypted repack come from. Mutually exclusive (a small -/// radio in the UI): the CLI default cryptVersion, a manually-typed version, or inherited from an -/// existing `.wolf` (`--like`). Only consulted when [`PackOptions::encrypt`] is set and the format -/// is [`PackFormat::Auto`]. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum CryptSource { - /// The CLI default cryptVersion ([`DEFAULT_CRYPT_VERSION`]). - Default, - /// A manually-entered cryptVersion (e.g. `0x14b`). - Version(u16), - /// Inherit cryptVersion + embedded password from an existing archive (turnkey repack). - Like(PathBuf), -} - -impl Default for CryptSource { - fn default() -> Self { - CryptSource::Default - } -} - -/// The repack options, assembled from the UI controls. The GUI equivalent of `cmd_pack`'s flags. -#[derive(Clone, Debug)] -pub struct PackOptions { - /// Encrypt the output (the `--encrypt` flag). Ignored for the legacy ver5/ver6 formats, which - /// have their own (un-keyed) container. - pub encrypt: bool, - /// Where the crypt params come from when `encrypt` is set on an `Auto`-format archive. - pub crypt: CryptSource, - /// The container format. - pub format: PackFormat, -} - -impl Default for PackOptions { - fn default() -> Self { - Self { - encrypt: false, - crypt: CryptSource::Default, - format: PackFormat::Auto, - } - } -} - -/// The outcome of an [`unpack`]: how many files were written, and the (already-logged) names of any -/// files whose archive path had to be sanitised (rare, a malformed/hostile path). -pub struct UnpackOutcome { - /// Number of files written to the output dir. - pub written: usize, - /// `(original archive path, sanitised path)` for files whose name needed `..`/`.` stripped. - pub sanitised: Vec<(String, String)>, - /// The output directory files were written under. - pub out_dir: PathBuf, -} - -/// Sanitise an archive's inner path into a safe relative path: drop empty/`.`/`..` segments and the -/// drive/leading-separator, exactly like `cmd_unpack`, so a file can never escape the output dir. -fn safe_relative(path: &str) -> PathBuf { - path.split(['\\', '/']) - .filter(|s| !s.is_empty() && *s != "." && *s != "..") - .collect() -} - -/// Extract every file from a `.wolf` archive into `out_dir`, preserving the inner directory tree. -/// Calls the library's [`wolf_archive::extract_archive`] (the same path `cmd_unpack` uses), which -/// handles every crypt regime and is resilient to odd archives. `report` drives a progress bar. Pass -/// a no-op in tests. Returns the file count + any sanitised-path warnings. -/// -/// The empty key string (`b""`) lets the library pick the right default WolfPro key, matching the -/// CLI. Files are written under a sanitised relative path so a malformed archive entry cannot write -/// outside `out_dir`. -pub fn unpack( - archive: &Path, - out_dir: &Path, - mut report: impl FnMut(f32, String), -) -> Result { - report(0.05, "reading archive…".to_string()); - let data = std::fs::read(archive) - .map_err(|e| format!("cannot read {}: {e}", archive.display()))?; - - report(0.3, "decoding archive…".to_string()); - let files = wolf_archive::extract_archive(&data, b"") - .map_err(|e| format!("not a readable Wolf archive ({e})"))?; - let total = files.len(); - if total == 0 { - return Err("the archive holds no files".to_string()); - } - - std::fs::create_dir_all(out_dir) - .map_err(|e| format!("cannot create {}: {e}", out_dir.display()))?; - - let mut sanitised = Vec::new(); - let mut written = 0usize; - for (i, (path, bytes)) in files.iter().enumerate() { - // Progress over the back 0.3..1.0 of the bar (the table decode took the first 0.3). - if total > 0 { - let frac = 0.3 + 0.7 * (i as f32) / (total as f32); - report(frac, format!("writing {path}")); - } - let safe = safe_relative(path); - if safe.as_os_str().is_empty() { - // A path that sanitised away to nothing (all `.`/`..`); skip it rather than write to the - // dir root. Record it so the caller can warn. - sanitised.push((path.clone(), String::new())); - continue; - } - // If sanitising changed the path, note it (so the UI can warn about the odd entry). - let safe_str = safe.to_string_lossy().replace('\\', "/"); - let orig_norm = path.replace('\\', "/"); - if safe_str != orig_norm { - sanitised.push((path.clone(), safe_str.clone())); - } - let target = out_dir.join(&safe); - if let Some(parent) = target.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("cannot create {}: {e}", parent.display()))?; - } - std::fs::write(&target, bytes) - .map_err(|e| format!("cannot write {}: {e}", target.display()))?; - written += 1; - } - report(1.0, "done".to_string()); - Ok(UnpackOutcome { - written, - sanitised, - out_dir: out_dir.to_path_buf(), - }) -} - -/// The outcome of an [`unpack_all`]: how many archives unpacked, how many failed, the total file -/// count, the output root, and a per-archive `(name, written-or-error)` list for the log. -pub struct UnpackAllOutcome { - pub ok: usize, - pub failed: usize, - pub files: usize, - pub out_root: PathBuf, - pub details: Vec<(String, Result)>, -} - -/// Unpack every `.wolf` archive directly inside `dir` into `//`. A per-category -/// game splits its data across many archives (BasicData.wolf, MapData.wolf, Evtext.wolf, ...); -/// this takes the whole set apart in one pass. Each `Name.wolf` lands in `/Name/`, which -/// mirrors how the engine maps a category archive to a `Name/` folder, so the result is a loose -/// data tree the game can load and the per-file extractors can read. One bad archive is recorded -/// and skipped rather than aborting the run. Mirrors the CLI `unpack-all`. -pub fn unpack_all( - dir: &Path, - out_root: &Path, - mut report: impl FnMut(f32, String), -) -> Result { - let mut archives: Vec = std::fs::read_dir(dir) - .map_err(|e| format!("cannot read {}: {e}", dir.display()))? - .flatten() - .map(|e| e.path()) - .filter(|p| { - p.is_file() - && p.extension() - .and_then(|x| x.to_str()) - .map(|x| x.eq_ignore_ascii_case("wolf")) - .unwrap_or(false) - }) - .collect(); - archives.sort(); - if archives.is_empty() { - return Err(format!("no .wolf archives found in {}", dir.display())); - } - - let total = archives.len(); - let mut out = UnpackAllOutcome { - ok: 0, - failed: 0, - files: 0, - out_root: out_root.to_path_buf(), - details: Vec::with_capacity(total), - }; - for (i, arc) in archives.iter().enumerate() { - let stem = arc - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("archive") - .to_string(); - let name = arc - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(&stem) - .to_string(); - report(i as f32 / total as f32, format!("unpacking {name}…")); - let dest = out_root.join(&stem); - // Swallow the inner per-file progress; the outer bar steps once per archive. - match unpack(arc, &dest, |_f, _m| {}) { - Ok(o) => { - out.ok += 1; - out.files += o.written; - out.details.push((name, Ok(o.written))); - } - Err(e) => { - out.failed += 1; - out.details.push((name, Err(e))); - } - } - } - report(1.0, "done".to_string()); - Ok(out) -} - -/// The outcome of a [`repack`]: the output path, the file count packed, and the output size. -pub struct RepackOutcome { - pub out: PathBuf, - pub files: usize, - pub bytes: usize, - /// A short human description of the crypto mode used (for the log). - pub mode: String, -} - -/// Recursively gather `(relative-path, bytes)` for every file under `dir`, the same as `cmd_pack`'s -/// `gather_files` (slashes normalised so the archive's inner paths are platform-independent). -fn gather_files(base: &Path, dir: &Path, out: &mut Vec<(String, Vec)>) -> Result<(), String> { - let rd = std::fs::read_dir(dir).map_err(|e| format!("cannot read {}: {e}", dir.display()))?; - for entry in rd { - let p = entry.map_err(|e| e.to_string())?.path(); - if p.is_dir() { - gather_files(base, &p, out)?; - } else { - let rel = p - .strip_prefix(base) - .unwrap_or(&p) - .to_string_lossy() - .replace('\\', "/"); - let bytes = std::fs::read(&p).map_err(|e| format!("cannot read {}: {e}", p.display()))?; - out.push((rel, bytes)); - } - } - Ok(()) -} - -/// Pack every file under `input_dir` into a `.wolf` at `out`, using the crypto/format the -/// [`PackOptions`] describe. Dispatches to the same `wolf_archive::pack_*` functions `cmd_pack` does. -/// `report` drives a progress bar. Pass a no-op in tests. -/// -/// For an encrypted `Auto` archive the cryptVersion (and, for `--like`, the embedded password) is -/// resolved up front. New-crypt versions (`0x14B`/`0x15E`) use the embedded default password when no -/// `--like` source is given. The output is written to `out` and its size reported. -pub fn repack( - input_dir: &Path, - out: &Path, - opts: &PackOptions, - mut report: impl FnMut(f32, String), -) -> Result { - report(0.05, "gathering files…".to_string()); - let mut files: Vec<(String, Vec)> = Vec::new(); - gather_files(input_dir, input_dir, &mut files)?; - if files.is_empty() { - return Err(format!("no files found under {}", input_dir.display())); - } - files.sort(); - report(0.4, format!("packing {} file(s)…", files.len())); - - // Resolve the crypto mode, mirroring cmd_pack's dispatch. - let (bytes, mode) = match opts.format { - PackFormat::Ver5 => ( - wolf_archive::pack_ver5(&files)?, - "ver5 (Wolf 2.0x)".to_string(), - ), - PackFormat::Ver6 => ( - wolf_archive::pack_ver6(&files)?, - "ver6 (Wolf 2.0x)".to_string(), - ), - PackFormat::Auto if !opts.encrypt => { - (wolf_archive::pack_plaintext(&files)?, "unencrypted".to_string()) - } - PackFormat::Auto => { - // Resolve cryptVersion (+ embedded password for --like). - let (version, pwd, src_note) = match &opts.crypt { - CryptSource::Default => (DEFAULT_CRYPT_VERSION, None, ""), - CryptSource::Version(v) => (*v, None, ""), - CryptSource::Like(orig) => { - let params = std::fs::read(orig) - .ok() - .and_then(|d| wolf_archive::archive_crypt_params(&d)); - match params { - Some((cv, p)) => (cv, Some(p), " (--like)"), - None => { - return Err(format!( - "could not read crypt params from {}", - orig.display() - )) - } - } - } - }; - let packed = if matches!(version, 0x14B | 0x15E) { - wolf_archive::pack_newcrypt(&files, version, &pwd.unwrap_or(DEFAULT_NEWCRYPT_PWD))? - } else { - wolf_archive::pack_encrypted(&files, version)? - }; - (packed, format!("encrypted cryptVersion={version:#x}{src_note}")) - } - }; - - if let Some(parent) = out.parent() { - if !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("cannot create {}: {e}", parent.display()))?; - } - } - std::fs::write(out, &bytes).map_err(|e| format!("cannot write {}: {e}", out.display()))?; - report(1.0, "done".to_string()); - Ok(RepackOutcome { - out: out.to_path_buf(), - files: files.len(), - bytes: bytes.len(), - mode, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = Path::new(&base).join(rel); - p.exists().then_some(p) - } - - fn tmp_dir(tag: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!( - "wolfdawn_archive_{tag}_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::create_dir_all(&p); - p - } - - /// Unpack the Chamber fixture's Data.wolf (if present) and confirm the data files appear. Writes - /// only to a temp dir, never touches the game folder. Skips gracefully when the archive is absent. - #[test] - fn unpack_chamber_archive() { - let Some(archive) = test_data("chamber/Data.wolf") else { - eprintln!("skip unpack_chamber_archive: archive fixture not present"); - return; - }; - let out = tmp_dir("unpack"); - let outcome = unpack(&archive, &out, |_, _| {}).expect("unpack"); - assert!(outcome.written > 0, "some files should have been written"); - assert!( - out.join("BasicData/CommonEvent.dat").exists(), - "the unpacked tree should contain BasicData/CommonEvent.dat" - ); - let _ = std::fs::remove_dir_all(&out); - } - - /// Pack a small temp folder to a `.wolf`, unpack it again, and confirm the files round-trip with - /// identical content. Uses copies of a couple of fixture data files (never modifies the fixtures root). - #[test] - fn repack_round_trip() { - // Source a couple of small, real data files from the fixtures root (read-only). - let present: Vec = [ - "chamber/Data/BasicData/Game.dat", - "chamber/Data/BasicData/CommonEvent.dat", - "chamber/Data/MapData/TitleMap.mps", - ] - .iter() - .filter_map(|r| test_data(r)) - .collect(); - if present.is_empty() { - eprintln!("skip repack_round_trip: no data fixtures present"); - return; - } - - // Build a small input folder of COPIES (with a nested subdir to exercise the tree). - let src = tmp_dir("repack_src"); - let _ = std::fs::create_dir_all(src.join("BasicData")); - let mut expect: Vec<(String, Vec)> = Vec::new(); - for (i, p) in present.iter().enumerate() { - let name = p.file_name().unwrap().to_string_lossy().to_string(); - let rel = format!("BasicData/{name}"); - let dst = src.join("BasicData").join(&name); - std::fs::copy(p, &dst).expect("copy"); - expect.push((rel, std::fs::read(p).expect("read source"))); - // Only take up to two files to keep the archive small. - if i >= 1 { - break; - } - } - - let out_wolf = tmp_dir("repack_out").join("Data.wolf"); - let outcome = repack(&src, &out_wolf, &PackOptions::default(), |_, _| {}).expect("repack"); - assert!(outcome.files >= 1, "at least one file should be packed"); - assert!(outcome.bytes > 0, "the archive should be non-empty"); - - // Unpack the freshly-built archive and confirm every input file comes back with the same bytes. - let back = tmp_dir("repack_back"); - let un = unpack(&out_wolf, &back, |_, _| {}).expect("unpack repacked"); - assert!(un.written >= 1, "files should extract from the repacked archive"); - for (rel, bytes) in &expect { - let got = back.join(rel); - assert!(got.exists(), "{rel} should round-trip through repack"); - assert_eq!(&std::fs::read(&got).expect("read back"), bytes, "{rel} content must match"); - } - - let _ = std::fs::remove_dir_all(&src); - let _ = std::fs::remove_dir_all(out_wolf.parent().unwrap()); - let _ = std::fs::remove_dir_all(&back); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/database.rs b/util/wolfdawn-master/crates/wolf-gui/src/database.rs deleted file mode 100644 index c8eb2e8..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/database.rs +++ /dev/null @@ -1,678 +0,0 @@ -//! The Database section's data model and the headless logic behind it. -//! -//! A spreadsheet-like editor over a Wolf database (`*.project` + sibling `*.dat`) so a non-coder can -//! change values like item prices, skill power, or enemy HP without touching JSON. This module owns -//! everything *except* the egui rendering (which lives in `app.rs`), so the whole load/edit/save -//! flow can be exercised headlessly in tests. -//! -//! It mirrors the CLI's `db-json` (`database_to_json`) + `db-apply` (`apply_database_edit`) pair, -//! calling the **same** `wolf_decompiler` library functions directly (no shelling out), so the GUI -//! and CLI agree on the JSON shape and the byte-exact apply: -//! -//! * [`load`] reads the `.project`+`.dat` pair with [`Database::read`], renders it to JSON with -//! [`database_to_json`] (exactly what `cmd_db_json` does, via the same per-stem `kind` label), -//! parses it into a [`DbModel`] of [`DbType`]s -> [`DbRow`]s -> [`Cell`]s, and **keeps the parsed -//! `doc`** so a save can rewrite just the edited leaves. Each editable cell carries a JSON -//! Pointer (RFC 6901) to its leaf in `doc`, exactly like `translation.rs`, so an edit touches -//! one leaf and every other byte stays identical. -//! * [`save`] writes each row's edited value into the leaf its pointer addresses, serializes the -//! patched `doc`, and applies it with [`apply_database_edit`] onto a freshly-read base (the -//! `db-apply` path), re-parses the [`Database::write`] output to verify it round-trips, and -//! writes the `.project`+`.dat` pair together byte-exact, optionally backing up the originals -//! first. Untouched data re-serializes verbatim, so a no-change save reproduces both halves -//! byte-for-byte. -//! -//! The DB `.project` is the canonical handle. The sibling `.dat` is paired by extension, mirroring -//! the CLI's `cmd_db_apply` (`base.with_extension("dat")`). - -use std::path::{Path, PathBuf}; - -use serde_json::Value; -use wolf_decompiler::{apply_database_edit, database_to_json}; -use wolf_formats::database::Database; - -/// One editable cell of a data row: which field column it belongs to, the editable value (as text - -/// ints are edited as their decimal string and validated on apply), whether it is a string or int -/// field (so the grid can hint and the apply path can re-type it), and the JSON Pointer to this -/// cell's leaf inside the owning [`DbModel::doc`]. The pointer is what makes a save lossless: an edit -/// rewrites exactly that one leaf, leaving every other byte of the document untouched. -#[derive(Clone)] -pub struct Cell { - /// The owning field's display key (the same key the JSON `values` object uses). - pub field: String, - /// The editable value, as text (string cells verbatim, int cells as their decimal form). - pub value: String, - /// The value as loaded (read-only, for change detection). - pub original: String, - /// `true` for a string field, `false` for an int field. - pub is_string: bool, - /// RFC 6901 JSON Pointer to this cell's value leaf inside [`DbModel::doc`]. - ptr: String, -} - -impl Cell { - /// True when the user changed this cell from its loaded value. - pub fn is_changed(&self) -> bool { - self.value != self.original - } -} - -/// One data row of a type: its editable display name (+ pointer) plus one [`Cell`] per data field. -#[derive(Clone)] -pub struct DbRow { - /// The row's stable id (its index in the type's data), shown for orientation. - pub id: usize, - /// The editable row name (the first grid column). - pub name: String, - /// The name as loaded (read-only, for change detection). - pub name_original: String, - /// JSON Pointer to this row's `name` leaf inside [`DbModel::doc`]. - name_ptr: String, - /// One cell per data field, in column order. - pub cells: Vec, -} - -impl DbRow { - /// How many of this row's editable leaves changed (name counts as one). - pub fn changed_count(&self) -> usize { - let n = usize::from(self.name != self.name_original); - n + self.cells.iter().filter(|c| c.is_changed()).count() - } -} - -/// One database type (タイプ), a table like "Skill", "Item", "Enemy": its display name, the data -/// field column names (in order), and its data rows. -pub struct DbType { - /// The type's display name (e.g. `技能 / Skill`). - pub name: String, - /// The data field column names, in order (the grid header, after the row-name column). - pub fields: Vec, - /// The data rows. - pub rows: Vec, -} - -impl DbType { - /// How many editable leaves the user changed across this type. - pub fn changed_count(&self) -> usize { - self.rows.iter().map(DbRow::changed_count).sum() - } -} - -/// The loaded, editable view of a database: where it came from (the `.project`, with the `.dat` -/// paired by extension), the read-only kind/encoding badges, and the types. -/// -/// The full parsed JSON document is NOT kept resident. It is the heaviest part of a large database -/// (every value and a repeated key per row, plus map overhead), so save/export rebuild it on demand -/// from the base file and apply the edits then. The editable grid keeps only what a user can change -/// (cell values + row names) plus each leaf's JSON pointer. -pub struct DbModel { - /// The on-disk `.project` this was loaded from (the [`save`] in-place target, `.dat` is sibling). - pub project: PathBuf, - /// The DB kind label (`UDB` / `CDB` / `SDB`), read-only. - pub kind: String, - /// The text encoding badge (`UTF-8` / `Shift-JIS`), read-only. - pub encoding: String, - /// The types (tables), in order. - pub types: Vec, -} - -impl DbModel { - /// Total data rows across every type. - pub fn total_rows(&self) -> usize { - self.types.iter().map(|t| t.rows.len()).sum() - } - - /// How many editable leaves the user changed across the whole model. - pub fn changed_count(&self) -> usize { - self.types.iter().map(DbType::changed_count).sum() - } - - /// True when anything was edited (so the Save button can gate on it). - pub fn dirty(&self) -> bool { - self.changed_count() > 0 - } -} - -/// The per-stem `kind` label, matching the CLI's `db_json_one` exactly. -fn kind_for(proj: &Path) -> &'static str { - match proj.file_stem().and_then(|s| s.to_str()) { - Some("DataBase") => "UDB", - Some("CDataBase") => "CDB", - Some("SysDatabase") => "SDB", - _ => "DB", - } -} - -/// Escape one path token for an RFC 6901 JSON Pointer (`~` -> `~0`, `/` -> `~1`). Field keys can -/// contain `/` (e.g. `発動回数/武器の影響`) so this must run on every dynamic segment. -fn esc(token: &str) -> String { - token.replace('~', "~0").replace('/', "~1") -} - -/// Read and parse the `.project`+`.dat` pair into the editable grid model, reusing [`Database::read`] -/// + [`database_to_json`] (the CLI's `db-json` path) with the same per-stem `kind` label. The -/// returned model keeps the parsed JSON so a later [`save`] rewrites only the edited leaves. -pub fn load(project: &Path) -> Result { - let dat = project.with_extension("dat"); - let pb = std::fs::read(project) - .map_err(|e| format!("cannot read {}: {e}", project.display()))?; - let db_ = std::fs::read(&dat).map_err(|e| format!("cannot read {}: {e}", dat.display()))?; - let db = Database::read(&pb, &db_).map_err(|e| format!("database parse failed: {e}"))?; - let kind = kind_for(project); - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = database_to_json(&db, kind, &glossary); - let doc: Value = serde_json::from_str(&json).map_err(|e| format!("invalid DB JSON: {e}"))?; - - let kind_s = doc - .get("kind") - .and_then(Value::as_str) - .unwrap_or(kind) - .to_string(); - let encoding = doc - .get("encoding") - .and_then(Value::as_str) - .unwrap_or("UTF-8") - .to_string(); - - let types = types_from_doc(&doc); - // `doc` is dropped here: the grid carries everything editable, and save/export rebuild the doc - // from the base file when needed, so it is not held for the session. - Ok(DbModel { - project: project.to_path_buf(), - kind: kind_s, - encoding, - types, - }) -} - -/// Build the editable types/rows/cells (each carrying its JSON pointer) from the parsed document. -fn types_from_doc(doc: &Value) -> Vec { - let mut out = Vec::new(); - let Some(types) = doc.get("types").and_then(Value::as_array) else { - return out; - }; - for (idx, t) in types.iter().enumerate() { - let ti = t.get("id").and_then(Value::as_u64).map(|n| n as usize).unwrap_or(idx); - let type_name = display_name(t); - - // The data field columns are the keys of the FIRST row's `values` object, in order (only the - // `fields_size` data fields appear there, schema-only fields are absent, matching the export). - // We derive the column order + each cell's string/int kind from the `fields` schema, keyed by - // the same `values` key (unique field name, else `#`), so a missing first row still works. - let columns = value_columns(t); - let field_labels: Vec = columns.iter().map(|c| c.label.clone()).collect(); - - let mut rows = Vec::new(); - if let Some(rs) = t.get("rows").and_then(Value::as_array) { - for r in rs { - let ri = r.get("id").and_then(Value::as_u64).unwrap_or(0) as usize; - let name = r.get("name").and_then(Value::as_str).unwrap_or("").to_string(); - let mut cells = Vec::with_capacity(columns.len()); - for col in &columns { - let v = r.get("values").and_then(|vv| vv.get(&col.key)); - let value = match v { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(other) => other.to_string(), - None => String::new(), - }; - cells.push(Cell { - field: col.label.clone(), - original: value.clone(), - value, - is_string: col.is_string, - ptr: format!("/types/{ti}/rows/{ri}/values/{}", esc(&col.key)), - }); - } - rows.push(DbRow { - id: ri, - name_original: name.clone(), - name, - name_ptr: format!("/types/{ti}/rows/{ri}/name"), - cells, - }); - } - } - - out.push(DbType { - name: type_name, - fields: field_labels, - rows, - }); - } - out -} - -/// A data column derived from a type's schema: the `values` key, a friendly label, and the kind. -struct Column { - /// The key as it appears in each row's `values` object. - key: String, - /// A friendly column label (`name_en` when present, else the JP `name`). - label: String, - /// `true` for a string field. - is_string: bool, -} - -/// Derive the ordered data columns from a type's `fields` schema. A field carries row data unless it -/// is tagged `schemaOnly: true`. Its `values` key is its unique field name, else `#`, the same -/// rule `value_slots`/`database_to_json` use, so the keys always match the row objects. -fn value_columns(t: &Value) -> Vec { - let Some(fields) = t.get("fields").and_then(Value::as_array) else { - return Vec::new(); - }; - // Count name occurrences among data-bearing fields so duplicates fall back to `#`. - let data_fields: Vec<&Value> = fields - .iter() - .filter(|f| !f.get("schemaOnly").and_then(Value::as_bool).unwrap_or(false)) - .collect(); - let mut name_counts: std::collections::HashMap<&str, u32> = std::collections::HashMap::new(); - for f in &data_fields { - let n = f.get("name").and_then(Value::as_str).unwrap_or(""); - if !n.is_empty() { - *name_counts.entry(n).or_default() += 1; - } - } - let mut out = Vec::with_capacity(data_fields.len()); - for f in &data_fields { - let fi = f.get("id").and_then(Value::as_u64).unwrap_or(0) as usize; - let jp = f.get("name").and_then(Value::as_str).unwrap_or(""); - let en = f.get("name_en").and_then(Value::as_str); - let key = if jp.is_empty() || name_counts.get(jp).copied().unwrap_or(0) > 1 { - format!("#{fi}") - } else { - jp.to_string() - }; - let label = match en { - Some(e) if !e.is_empty() && e != jp => format!("{jp} / {e}"), - _ if jp.is_empty() => format!("#{fi}"), - _ => jp.to_string(), - }; - let is_string = f.get("kind").and_then(Value::as_str) == Some("string"); - out.push(Column { key, label, is_string }); - } - out -} - -/// A friendly display name for a type/row: `name_en` appended when present (`JP / EN`), else JP. -fn display_name(v: &Value) -> String { - let jp = v.get("name").and_then(Value::as_str).unwrap_or(""); - match v.get("name_en").and_then(Value::as_str) { - Some(en) if !en.is_empty() && en != jp => format!("{jp} / {en}"), - _ => jp.to_string(), - } -} - -// ---------------------------------------------------------------------------- -// Save / export: rebuild the doc from the base, patch in the edits, apply + write -// ---------------------------------------------------------------------------- - -/// Push each row's edited name + cell values into `doc` (a freshly rebuilt `database_to_json` -/// document), via each leaf's JSON pointer, so the document is current before it is fed to -/// `apply_database_edit`. Int cells write a JSON number (so the apply path's `as_i64` accepts them). -/// String cells write a JSON string. An int cell that does not parse is a hard error here with a -/// clear field reference. -fn apply_edits_to_doc(model: &DbModel, doc: &mut Value) -> Result<(), String> { - for t in &model.types { - for r in &t.rows { - if r.name != r.name_original { - if let Some(slot) = doc.pointer_mut(&r.name_ptr) { - *slot = Value::String(r.name.clone()); - } - } - for c in &r.cells { - if !c.is_changed() { - continue; - } - let Some(slot) = doc.pointer_mut(&c.ptr) else { - continue; - }; - if c.is_string { - *slot = Value::String(c.value.clone()); - } else { - let n: i64 = c.value.trim().parse().map_err(|_| { - format!( - "field {:?} in type {:?} row {} ({:?}): {:?} is not a whole number", - c.field, t.name, r.id, r.name, c.value - ) - })?; - *slot = Value::Number(n.into()); - } - } - } - } - Ok(()) -} - -/// Rebuild the full `database_to_json` document from the base `.project`+`.dat` and patch in the -/// grid's edits. The doc is not kept resident, so this re-reads the base (deterministic, same -/// structure as load) and applies the edits via their pointers. Returns the edited doc plus the base -/// bytes, so [`save`] can apply onto the same bytes without a second read. -fn edited_doc(model: &DbModel) -> Result<(Value, Vec, Vec), String> { - let base_proj = &model.project; - let base_dat = base_proj.with_extension("dat"); - let pb = std::fs::read(base_proj) - .map_err(|e| format!("cannot read base {}: {e}", base_proj.display()))?; - let db_ = std::fs::read(&base_dat) - .map_err(|e| format!("cannot read base {}: {e}", base_dat.display()))?; - let db = Database::read(&pb, &db_).map_err(|e| format!("base parse failed: {e}"))?; - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = database_to_json(&db, kind_for(base_proj), &glossary); - let mut doc: Value = serde_json::from_str(&json).map_err(|e| format!("invalid DB JSON: {e}"))?; - apply_edits_to_doc(model, &mut doc)?; - Ok((doc, pb, db_)) -} - -/// Apply an edits JSON onto a fresh base, verify the result re-parses, then write the `.project`+ -/// `.dat` pair (backing up existing halves first when asked). The shared tail of [`save`] and -/// [`import_json`]. `base_pb`/`base_dat_bytes`, when supplied, are the already-read base bytes (save -/// reuses them); pass `None` to read the base here (import). -fn apply_and_write( - base_project: &Path, - edited_json: &str, - out_project: &Path, - backup: bool, - base_bytes: Option<(Vec, Vec)>, -) -> Result { - let base_dat = base_project.with_extension("dat"); - let out_dat = out_project.with_extension("dat"); - let (pb, db_) = match base_bytes { - Some(b) => b, - None => ( - std::fs::read(base_project) - .map_err(|e| format!("cannot read base {}: {e}", base_project.display()))?, - std::fs::read(&base_dat) - .map_err(|e| format!("cannot read base {}: {e}", base_dat.display()))?, - ), - }; - let mut db = Database::read(&pb, &db_).map_err(|e| format!("base parse failed: {e}"))?; - let changed = - apply_database_edit(edited_json, &mut db).map_err(|e| format!("apply failed: {e}"))?; - - let (out_pb, out_db) = db.write(); - // Re-parse the output to verify it still round-trips before committing it. Never ship a - // structurally-corrupt pair. - Database::read(&out_pb, &out_db) - .map_err(|e| format!("internal error: edited database no longer parses ({e})"))?; - let byte_identical = out_pb == pb && out_db == db_; - - let mut backups = Vec::new(); - if backup { - for half in [out_project, out_dat.as_path()] { - if half.exists() { - let bak = backup_path(half); - std::fs::copy(half, &bak) - .map_err(|e| format!("backup failed ({}): {e}", bak.display()))?; - backups.push(bak); - } - } - } - if let Some(parent) = out_project.parent() { - if !parent.as_os_str().is_empty() { - let _ = std::fs::create_dir_all(parent); - } - } - if let (Err(e), _) | (_, Err(e)) = ( - std::fs::write(out_project, &out_pb), - std::fs::write(&out_dat, &out_db), - ) { - return Err(format!("write failed: {e}")); - } - Ok(SaveOutcome { - changed, - byte_identical, - out_project: out_project.to_path_buf(), - out_dat, - backups, - }) -} - -/// Export the on-disk database as a pretty-printed `database_to_json` document for editing outside -/// the GUI (re-import with [`import_json`]). Reads only the base path, so it runs on a worker without -/// borrowing the live grid. Grid edits are NOT included here, so Save first to bake them in. -pub fn export_json(base_project: &Path, out: &Path) -> Result<(), String> { - let base_dat = base_project.with_extension("dat"); - let pb = std::fs::read(base_project) - .map_err(|e| format!("cannot read {}: {e}", base_project.display()))?; - let db_ = std::fs::read(&base_dat) - .map_err(|e| format!("cannot read {}: {e}", base_dat.display()))?; - let db = Database::read(&pb, &db_).map_err(|e| format!("database parse failed: {e}"))?; - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = database_to_json(&db, kind_for(base_project), &glossary); - let doc: Value = serde_json::from_str(&json).map_err(|e| format!("invalid DB JSON: {e}"))?; - let pretty = serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())?; - std::fs::write(out, pretty.as_bytes()) - .map_err(|e| format!("cannot write {}: {e}", out.display()))?; - Ok(()) -} - -/// Import an edited `database_to_json` document (full or patch-style), apply it onto `base_project`'s -/// pair, and write to `out_project`. The same verify-then-write guard as [`save`]. -pub fn import_json( - base_project: &Path, - json_path: &Path, - out_project: &Path, - backup: bool, -) -> Result { - let json = std::fs::read_to_string(json_path) - .map_err(|e| format!("cannot read {}: {e}", json_path.display()))?; - apply_and_write(base_project, &json, out_project, backup, None) -} - -/// Where a backup goes for an in-place save of one half. -fn backup_path(file: &Path) -> PathBuf { - let mut name = file.file_name().map(|s| s.to_os_string()).unwrap_or_default(); - name.push(".bak"); - file.with_file_name(name) -} - -/// The outcome of a [`save`] run, for the log. -pub struct SaveOutcome { - /// How many cells/names `apply_database_edit` actually changed. - pub changed: usize, - /// True when both output halves are byte-identical to the base (a no-op save, or edits that net - /// to the original values). - pub byte_identical: bool, - /// Where the `.project` was written. - pub out_project: PathBuf, - /// Where the `.dat` was written. - pub out_dat: PathBuf, - /// The backup paths made (`.project.bak`, `.dat.bak`), if any. - pub backups: Vec, -} - -/// Apply the grid's edits onto the base database and write the `.project`+`.dat` pair to -/// `out_project` (+ its sibling `.dat`), mirroring the CLI's `db-apply`: patch the parsed doc with -/// the edited leaves, [`apply_database_edit`] it onto a freshly-read base, **re-parse the -/// [`Database::write`] output to verify it round-trips** before writing, and report the change count -/// + byte-identity. When `backup` is set and an output half already exists, the original is copied to -/// `.bak` first. -/// -/// Byte-exactness: `apply_database_edit` only rewrites cells whose value changed (it skips unchanged -/// strings to dodge the empty-string / trailing-NUL re-encode ambiguity), so untouched data -/// re-serializes verbatim and a no-change save reproduces both halves byte-for-byte. -pub fn save(model: &DbModel, out_project: &Path, backup: bool) -> Result { - let (doc, pb, db_) = edited_doc(model)?; - let edited_json = serde_json::to_string(&doc).map_err(|e| e.to_string())?; - apply_and_write(&model.project, &edited_json, out_project, backup, Some((pb, db_))) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A unique temp dir for scratch output (never a game folder or the fixtures root). - fn tmp_dir(tag: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!( - "wolfdawn_db_{tag}_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::create_dir_all(&p); - p - } - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// The shared DB fixture (`.project` plus its `.dat` sibling); tests skip when it is absent. - fn fixture() -> Option { - test_data("chamber/Data/BasicData/DataBase.project") - .filter(|p| p.with_extension("dat").exists()) - } - - /// Copy the `.project`+`.dat` pair into `dir`, returning the copied `.project` path. - fn copy_pair(src_proj: &Path, dir: &Path) -> PathBuf { - let stem = src_proj.file_name().unwrap(); - let dst_proj = dir.join(stem); - let dst_dat = dst_proj.with_extension("dat"); - std::fs::copy(src_proj, &dst_proj).expect("copy .project"); - std::fs::copy(src_proj.with_extension("dat"), &dst_dat).expect("copy .dat"); - dst_proj - } - - /// Load a real DB, edit one cell's value, save to a temp output, reload from the output: the - /// edited cell shows the new value and an UNEDITED type/row is unchanged. Never touches the - /// fixtures root (reads the fixture, copies + writes only into a temp dir). - #[test] - fn load_edit_cell_save_reload_round_trip() { - let Some(src) = fixture() else { - eprintln!("skip load_edit_cell_save_reload_round_trip: no DB fixture present"); - return; - }; - let work = tmp_dir("rt"); - let base = copy_pair(&src, &work); - - let mut m = load(&base).expect("load"); - assert!(!m.types.is_empty(), "DB should have at least one type"); - assert!(m.total_rows() >= 1, "DB should have at least one data row"); - - // Find a type with at least one row that has at least one cell, and edit the first cell. - let (ti, ri, ci, new_value, is_string) = { - let mut found = None; - 'outer: for (ti, t) in m.types.iter().enumerate() { - for (ri, r) in t.rows.iter().enumerate() { - if !r.cells.is_empty() { - found = Some((ti, ri)); - break 'outer; - } - } - } - let (ti, ri) = found.expect("a type/row with at least one cell"); - let cell = &m.types[ti].rows[ri].cells[0]; - // For a string cell, append a marker. For an int cell, bump it by 1 (stays a valid int). - let new = if cell.is_string { - format!("{}_EDIT", cell.value) - } else { - let cur: i64 = cell.value.trim().parse().unwrap_or(0); - (cur + 1).to_string() - }; - (ti, ri, 0usize, new, cell.is_string) - }; - // Record an UNEDITED reference cell elsewhere (a different row, or a different type). - let reference = { - let mut chosen = None; - 'r: for (oti, t) in m.types.iter().enumerate() { - for (ori, r) in t.rows.iter().enumerate() { - if (oti, ori) != (ti, ri) && !r.cells.is_empty() { - chosen = Some((oti, ori, r.cells[0].value.clone())); - break 'r; - } - } - } - chosen - }; - - m.types[ti].rows[ri].cells[ci].value = new_value.clone(); - assert_eq!(m.changed_count(), 1, "exactly one cell changed"); - - let out = work.join("edited.project"); - let outcome = save(&mut m, &out, false).expect("save"); - assert_eq!(outcome.changed, 1, "apply should report one cell changed"); - assert!(!outcome.byte_identical, "an edited save differs from base"); - assert!(out.with_extension("dat").exists(), "the .dat half was written"); - - // Reload from the OUTPUT pair: the edited cell shows the new value. - let re = load(&out).expect("reload"); - let got = &re.types[ti].rows[ri].cells[ci]; - assert_eq!(got.value, new_value, "edited cell shows the new value"); - assert_eq!(got.is_string, is_string, "cell kind preserved"); - - // The unedited reference cell is unchanged. - if let Some((oti, ori, orig)) = reference { - assert_eq!( - re.types[oti].rows[ori].cells[0].value, orig, - "an unedited cell must be unchanged after the round-trip" - ); - } - - let _ = std::fs::remove_dir_all(&work); - } - - /// A no-change save is byte-exact: load -> save with no edits -> both output halves are - /// byte-identical to the inputs (lossless-pointer + db-apply's byte-exact pairing). - #[test] - fn no_change_save_is_byte_exact() { - let Some(src) = fixture() else { - eprintln!("skip no_change_save_is_byte_exact: no DB fixture present"); - return; - }; - let work = tmp_dir("noop"); - let base = copy_pair(&src, &work); - let in_proj = std::fs::read(&base).unwrap(); - let in_dat = std::fs::read(base.with_extension("dat")).unwrap(); - - let mut m = load(&base).expect("load"); - assert_eq!(m.changed_count(), 0, "nothing edited yet"); - - let out = work.join("out.project"); - let outcome = save(&mut m, &out, false).expect("save"); - assert_eq!(outcome.changed, 0, "no cells changed"); - assert!(outcome.byte_identical, "a no-change save must be byte-identical to base"); - assert_eq!(std::fs::read(&out).unwrap(), in_proj, ".project is byte-identical"); - assert_eq!( - std::fs::read(out.with_extension("dat")).unwrap(), - in_dat, - ".dat is byte-identical" - ); - - let _ = std::fs::remove_dir_all(&work); - } - - /// In-place save with backups: both `.project.bak` and `.dat.bak` hold the original bytes. - #[test] - fn in_place_save_backs_up_both_halves() { - let Some(src) = fixture() else { - eprintln!("skip in_place_save_backs_up_both_halves: no DB fixture present"); - return; - }; - let work = tmp_dir("bak"); - let base = copy_pair(&src, &work); - let orig_proj = std::fs::read(&base).unwrap(); - let orig_dat = std::fs::read(base.with_extension("dat")).unwrap(); - - let mut m = load(&base).expect("load"); - // Edit a row name so a save actually rewrites bytes. - let row = m - .types - .iter_mut() - .find(|t| !t.rows.is_empty()) - .map(|t| &mut t.rows[0]) - .expect("a row"); - row.name = format!("{}_X", row.name); - - let outcome = save(&mut m, &base, true).expect("save in place"); - assert_eq!(outcome.backups.len(), 2, "both halves backed up"); - assert_eq!(std::fs::read(&outcome.backups[0]).unwrap(), orig_proj); - assert_eq!(std::fs::read(&outcome.backups[1]).unwrap(), orig_dat); - - let _ = std::fs::remove_dir_all(&work); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/decompile.rs b/util/wolfdawn-master/crates/wolf-gui/src/decompile.rs deleted file mode 100644 index 9927b19..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/decompile.rs +++ /dev/null @@ -1,379 +0,0 @@ -//! The Decompile section's headless logic: turn a binary event file (a `.mps` map or -//! `CommonEvent.dat`) into readable, editable WolfScript, then recompile the edited text back into a -//! runnable file using the original as a *base* so every opaque byte is preserved. -//! -//! This mirrors the CLI's `decompile --mode edit` (`decompile_edit_path`) and `compile` -//! (`cmd_compile`) exactly, calling the same `wolf_decompiler` library functions directly (no -//! shelling out), so the GUI and CLI produce identical output: -//! -//! * [`decompile_edit`] reads the input, parses it with the matching `wolf_formats` reader, builds -//! a symbol-aware table (CommonEvent + the three databases under the game's BasicData), and -//! renders the **edit** document (`decompile_*_edit_annotated`), the recompilable, bilingual -//! form `compile` consumes. -//! * [`compile_to`] re-reads the *base* file (the original input), applies the edited WolfScript -//! onto it (`compile_*_edit`), and writes the result. The base supplies all opaque metadata. -//! Only the command bodies are swapped in, so untouched code re-serializes byte-identical. -//! -//! Only code-bearing files (`.mps`, `CommonEvent.dat`) are supported. Anything else is rejected with -//! a clear message, matching the CLI. - -use std::path::{Path, PathBuf}; - -use wolf_decompiler::{ - compile_common_events_edit, compile_map_edit, decompile_common_events_edit_annotated, - decompile_map_edit_annotated, SymbolTable, -}; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::map::Map; - -/// Which kind of code-bearing file we're working with. Decides the reader/decompiler/compiler -/// pair, mirroring the CLI's `classify` (only the two code-bearing variants are relevant here). -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum CodeKind { - Map, - CommonEvent, -} - -/// Classify a path as a map / common-event, or `None` for anything decompile does not understand -/// (a database, Game.dat, an unrelated file). Mirrors the relevant arms of the CLI's `classify`. -fn classify(path: &Path) -> Option { - let ext = path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - let name = path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - match ext.as_str() { - "mps" => Some(CodeKind::Map), - "dat" if name == "commonevent.dat" => Some(CodeKind::CommonEvent), - _ => None, - } -} - -/// True when `path` is a file decompile understands (a `.mps` map or `CommonEvent.dat`), so the UI -/// can warn early on an unsupported pick. The action still runs and gives a clear error. -pub fn is_supported(path: &Path) -> bool { - classify(path).is_some() -} - -/// Load the three standard databases under `basic` into `symbols`, exactly like the CLI's -/// `add_databases`. Skips any that are missing or fail to parse (symbols are best-effort labels). -fn add_databases(symbols: &mut SymbolTable, basic: &Path) { - for (kind, stem) in [("UDB", "DataBase"), ("CDB", "CDataBase"), ("SDB", "SysDatabase")] { - let proj = basic.join(format!("{stem}.project")); - let dat = basic.join(format!("{stem}.dat")); - if let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) { - if let Ok(db) = Database::read(&p, &d) { - symbols.add_database(kind, &db); - } - } - } -} - -/// Build the symbol table for a map, locating the game's BasicData folder the same way the CLI's -/// `load_symbols` does (`.../Data/MapData/X.mps` → `.../Data/BasicData`) and loading CommonEvent + -/// the databases + the embedded glossaries. Best-effort: a missing BasicData just yields fewer labels. -fn map_symbols(map_path: &Path) -> SymbolTable { - let mut symbols = SymbolTable::new(); - let basic = map_path.parent().and_then(|p| p.parent()).map(|data| data.join("BasicData")); - let Some(basic) = basic else { - return symbols; - }; - if let Ok(bytes) = std::fs::read(basic.join("CommonEvent.dat")) { - if let Ok(ce) = CommonEventsFile::read(&bytes) { - symbols.add_common_events(&ce); - } - } - add_databases(&mut symbols, &basic); - symbols.set_glossary(wolf_decompiler::symbols::load_embedded_glossary()); - symbols.set_engine_text(wolf_decompiler::symbols::load_embedded_engine_glossary()); - symbols -} - -/// Build the symbol table for a CommonEvent.dat: its own events, plus the sibling databases in the -/// same folder + the embedded glossaries (mirrors the CLI's `decompile_edit_path` CommonEvent arm). -fn common_event_symbols(ce: &CommonEventsFile, ce_dir: Option<&Path>) -> SymbolTable { - let mut symbols = SymbolTable::new(); - symbols.add_common_events(ce); - if let Some(dir) = ce_dir { - add_databases(&mut symbols, dir); - } - symbols.set_glossary(wolf_decompiler::symbols::load_embedded_glossary()); - symbols.set_engine_text(wolf_decompiler::symbols::load_embedded_engine_glossary()); - symbols -} - -/// Decompile `input` to the **edit** WolfScript document (the recompilable, identity-delimited form -/// `compile` consumes), exactly like the CLI's `decompile --mode edit`. Symbol-aware: each operand -/// carries its index and a bilingual label, so the one file is both readable and recompilable. -/// Returns the WolfScript text. Errors for an unsupported file or a parse failure. -pub fn decompile_edit(input: &Path) -> Result { - let bytes = std::fs::read(input).map_err(|e| format!("cannot read {}: {e}", input.display()))?; - match classify(input) { - Some(CodeKind::Map) => { - let map = Map::read(&bytes).map_err(|e| e.to_string())?; - Ok(decompile_map_edit_annotated(&map, &map_symbols(input))) - } - Some(CodeKind::CommonEvent) => { - let ce = CommonEventsFile::read(&bytes).map_err(|e| e.to_string())?; - let symbols = common_event_symbols(&ce, input.parent()); - Ok(decompile_common_events_edit_annotated(&ce, &symbols)) - } - None => Err(format!( - "{} is not a code-bearing file (decompile supports .mps maps and CommonEvent.dat)", - input.display() - )), - } -} - -/// The outcome of a [`compile_to`] run, for the log: the bytes written, whether the result is -/// byte-identical to the base (i.e. the code was untouched), and the output path. -pub struct CompileOutcome { - pub out: PathBuf, - pub bytes: usize, - pub byte_identical: bool, -} - -/// Compile the edited WolfScript `text` back into a runnable file, using `base` (the original input -/// the text was decompiled from) as the base, and write the result to `output`. Mirrors the CLI's -/// `cmd_compile`: the base supplies all opaque metadata, only the command bodies are swapped in, so -/// with the code untouched the output is byte-identical to the base. -/// -/// As a safety net (and what the GUI's round-trip test asserts), the freshly compiled bytes are -/// re-parsed with the matching reader before the write is reported, so a structurally-corrupt result -/// never lands on disk silently. -pub fn compile_to(text: &str, base: &Path, output: &Path) -> Result { - let base_bytes = - std::fs::read(base).map_err(|e| format!("cannot read base {}: {e}", base.display()))?; - let out_bytes = match classify(base) { - Some(CodeKind::CommonEvent) => { - let mut ce = CommonEventsFile::read(&base_bytes) - .map_err(|e| format!("base parse failed: {e}"))?; - compile_common_events_edit(text, &mut ce).map_err(|e| format!("compile failed: {e}"))?; - let out = ce.write(); - // Re-parse before reporting: never ship a structurally-corrupt file. - CommonEventsFile::read(&out) - .map_err(|e| format!("internal error: compiled CommonEvent no longer parses ({e})"))?; - out - } - Some(CodeKind::Map) => { - let mut map = Map::read(&base_bytes).map_err(|e| format!("base parse failed: {e}"))?; - compile_map_edit(text, &mut map).map_err(|e| format!("compile failed: {e}"))?; - let out = map.write(); - Map::read(&out) - .map_err(|e| format!("internal error: compiled map no longer parses ({e})"))?; - out - } - None => { - return Err(format!( - "base {} must be a .mps map or CommonEvent.dat", - base.display() - )) - } - }; - - let byte_identical = out_bytes == base_bytes; - std::fs::write(output, &out_bytes) - .map_err(|e| format!("cannot write {}: {e}", output.display()))?; - Ok(CompileOutcome { - out: output.to_path_buf(), - bytes: out_bytes.len(), - byte_identical, - }) -} - -/// One match of a find query in the editor text, as a half-open range of *character* indices -/// `[start, end)` (not byte offsets). Character indices are what egui's `CCursor` wants, so the find -/// bar can set the editor selection directly from these. Kept here (not in `app.rs`) so the -/// match-finding logic is unit-testable without a UI. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct FindMatch { - /// Inclusive start, as a character index into the script. - pub start: usize, - /// Exclusive end, as a character index into the script. - pub end: usize, -} - -/// Find every (non-overlapping) occurrence of `query` in `haystack`, returning each as a character -/// range [`FindMatch`]. Empty when `query` is empty or has no match. When `case_sensitive` is false -/// the comparison is ASCII-case-insensitive (lowercasing both sides). CJK text is unaffected by -/// case folding, so the default insensitive mode is safe for the Japanese content this tool edits. -/// -/// Matching is done on chars (not bytes), and the returned ranges are char indices, so they map -/// straight onto egui's `CCursor`/selection model. Overlapping matches are skipped (search resumes -/// after each hit), matching a typical "find next" stepping behaviour. -pub fn find_matches(haystack: &str, query: &str, case_sensitive: bool) -> Vec { - if query.is_empty() { - return Vec::new(); - } - // Work on char vectors so the reported indices are char positions (what egui's CCursor uses), - // not byte offsets. Fold per char (1:1) so case-insensitive matching never shifts those indices. - let hay: Vec = haystack.chars().map(|c| fold_char(c, case_sensitive)).collect(); - let need: Vec = query.chars().map(|c| fold_char(c, case_sensitive)).collect(); - - let mut out = Vec::new(); - if need.len() > hay.len() { - return out; - } - let mut i = 0usize; - while i + need.len() <= hay.len() { - if hay[i..i + need.len()] == need[..] { - out.push(FindMatch { start: i, end: i + need.len() }); - i += need.len(); // non-overlapping: resume after the match. - } else { - i += 1; - } - } - out -} - -/// Case-fold one char without changing the char count (so character indices stay aligned with the -/// original string). For Latin + CJK content this is exactly ASCII lowercasing. CJK is returned -/// unchanged. Using a 1:1 fold keeps the [`find_matches`] ranges valid as `CCursor` char indices. -fn fold_char(c: char, case_sensitive: bool) -> char { - if case_sensitive { - c - } else { - c.to_ascii_lowercase() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// `find_matches` returns the correct char ranges for ASCII, is case-insensitive by default, - /// honours a case-sensitive flag, handles non-overlapping repeats, and is char- (not byte-) - /// indexed across multi-byte (CJK) text. - #[test] - fn find_matches_ranges_are_correct() { - // Basic ASCII, case-insensitive (default). - let hay = "abc ABC abc"; - let m = find_matches(hay, "abc", false); - assert_eq!(m.len(), 3, "three case-insensitive hits"); - assert_eq!(m[0], FindMatch { start: 0, end: 3 }); - assert_eq!(m[1], FindMatch { start: 4, end: 7 }); - assert_eq!(m[2], FindMatch { start: 8, end: 11 }); - - // Case-sensitive: only the two lowercase runs. - let m = find_matches(hay, "abc", true); - assert_eq!(m.len(), 2, "two case-sensitive hits"); - assert_eq!(m[0], FindMatch { start: 0, end: 3 }); - assert_eq!(m[1], FindMatch { start: 8, end: 11 }); - - // Empty query → no matches. - assert!(find_matches(hay, "", false).is_empty()); - // No match. - assert!(find_matches(hay, "zzz", false).is_empty()); - - // Non-overlapping: "aa" in "aaaa" → 2 matches (0..2, 2..4), not 3. - let m = find_matches("aaaa", "aa", false); - assert_eq!(m, vec![FindMatch { start: 0, end: 2 }, FindMatch { start: 2, end: 4 }]); - - // Char-indexed across CJK: prefix is 2 multi-byte chars, so the match starts at char 2. - let hay = "あい行 X 行"; - let m = find_matches(hay, "行", false); - assert_eq!(m.len(), 2, "two occurrences of 行"); - assert_eq!(m[0], FindMatch { start: 2, end: 3 }, "char index, not byte offset"); - // The second 行 is at char index 6 ("あ い 行 ␠ X ␠ 行"). - assert_eq!(m[1], FindMatch { start: 6, end: 7 }); - } - - - /// A unique temp dir for a test's scratch output (never under a game folder or the fixtures root). - fn tmp_dir(tag: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!( - "wolfdawn_decompile_{tag}_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::create_dir_all(&p); - p - } - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// The first present code-bearing fixture from the fixtures root (read-only). A map is - /// preferred (it exercises the BasicData symbol lookup), then CommonEvent.dat. - fn fixture() -> Option { - [ - "chamber/Data/MapData/TitleMap.mps", - "chamber/Data/BasicData/CommonEvent.dat", - ] - .into_iter() - .find_map(test_data) - } - - /// Decompile a real file to WolfScript (non-empty), compile it back onto the original as base to - /// a temp output, and assert the result re-decompiles to the same text (semantic round-trip). For - /// an untouched document compile is byte-exact, so we also assert byte-identity to the base. Never - /// modifies the fixtures root. Reads the fixture, writes only into a temp dir. - #[test] - fn decompile_compile_round_trip() { - let Some(input) = fixture() else { - eprintln!("skip decompile_compile_round_trip: no code fixture present"); - return; - }; - assert!(is_supported(&input), "{input:?} should be supported"); - - let text = decompile_edit(&input).expect("decompile"); - assert!(!text.is_empty(), "decompiled WolfScript should be non-empty"); - - let work = tmp_dir("rt"); - // Keep the base's file name so the compiled output re-classifies (and re-decompiles) the - // same way (a `.mps` stays a map, `CommonEvent.dat` stays a common-event). - let out = work.join(input.file_name().expect("file name")); - let outcome = compile_to(&text, &input, &out).expect("compile"); - assert!(out.exists(), "compiled output should be written"); - assert!(outcome.bytes > 0, "output should be non-empty"); - - // Untouched code is byte-identical to base (the strong guarantee compile gives). - let base_bytes = std::fs::read(&input).expect("read base"); - let out_bytes = std::fs::read(&out).expect("read out"); - assert_eq!(out_bytes, base_bytes, "untouched compile must be byte-identical to base"); - assert!(outcome.byte_identical, "outcome should report byte-identity"); - - // Semantic check: re-decompile the compiled output and compare to a decompile of the *base - // placed in the same temp dir*. Both share the same (BasicData-less) symbol context, so the - // bilingual operand labels match. Comparing against the original `text` (decompiled at the - // real location, where the databases resolve those labels) would differ only in those labels, - // and the compiler strips labels, so the bytes already proved equal above. - let base_here = work.join("base").join(input.file_name().unwrap()); - std::fs::create_dir_all(base_here.parent().unwrap()).unwrap(); - std::fs::copy(&input, &base_here).expect("copy base"); - let baseline = decompile_edit(&base_here).expect("decompile base copy"); - let re = decompile_edit(&out).expect("re-decompile"); - assert_eq!(re, baseline, "re-decompiled WolfScript should match the base's own decompile"); - - let _ = std::fs::remove_dir_all(&work); - } - - /// An unsupported file (e.g. a plain .txt) is rejected by both entry points, not panicked on. - #[test] - fn unsupported_file_is_rejected() { - let work = tmp_dir("unsup"); - let f = work.join("notes.txt"); - std::fs::write(&f, b"hello").unwrap(); - assert!(!is_supported(&f)); - assert!(decompile_edit(&f).is_err(), "txt should not decompile"); - assert!( - compile_to("", &f, &work.join("out")).is_err(), - "txt base should not compile" - ); - let _ = std::fs::remove_dir_all(&work); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/gamedat.rs b/util/wolfdawn-master/crates/wolf-gui/src/gamedat.rs deleted file mode 100644 index 0239f90..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/gamedat.rs +++ /dev/null @@ -1,376 +0,0 @@ -//! The Game.dat section's data model and headless logic. -//! -//! A friendly form over *every* editable `Game.dat` string field, so a non-coder can change the -//! window title or the editor font without touching JSON. This mirrors the CLI's `gamedat-json` -//! (`dump_game_dat`) + `gamedat-apply` (`apply_game_dat`) pair, calling the same `wolf_decompiler` -//! library functions directly (no shelling out), so the GUI and CLI agree on the field set and the -//! byte-exact apply. -//! -//! The module owns everything except the egui rendering (which lives in `app.rs`), so the whole -//! workflow can be exercised headlessly: -//! -//! * [`load`] reads a `Game.dat` with [`GameDat::read`], dumps every editable field to JSON with -//! [`dump_game_dat`], and parses it into typed [`Field`]s. **Only the fields actually present** -//! for this file's `string_count` (no phantom keys), plus the read-only `encoding` badge. -//! * [`save`] builds a JSON object of *only the changed fields*, applies it with [`apply_game_dat`] -//! onto a freshly-read base, re-parses the [`GameDat::write`] output to verify it round-trips -//! (exactly as `cmd_gamedat_apply` does), and writes it, optionally backing up the original -//! first. Untouched fields re-serialize byte-exact (guaranteed by `apply_game_dat` + the -//! `GameDat::write` housekeeping anchor), so a no-change save reproduces the file byte-for-byte. - -use std::path::{Path, PathBuf}; - -use serde_json::{Map, Value}; -use wolf_decompiler::{apply_game_dat, dump_game_dat}; -use wolf_formats::game_dat::GameDat; - -/// Which group a field belongs to, so the form can render sensible section headers. -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Group { - /// Window title + splash/title-screen messages. - Titles, - /// The editor font + fallback sub-fonts. - Fonts, - /// Image / graphic path fields. - Graphics, - /// The opaque trailing string. - Other, -} - -/// One editable scalar field of a loaded Game.dat: its JSON key, an editable value (seeded from the -/// original), the read-only original (for "changed?" detection), and which form group it belongs to. -pub struct Field { - /// The JSON key (`"Title"`, `"Font"`, …), the same key `apply_game_dat` recognizes. - pub key: &'static str, - /// A friendly label for the form. - pub label: &'static str, - /// The editable value (starts equal to `original`). - pub value: String, - /// The value as loaded (read-only reference, for change detection). - pub original: String, - /// Which form section this field is shown under. - pub group: Group, -} - -impl Field { - /// True when the user changed this field's value. - pub fn is_changed(&self) -> bool { - self.value != self.original - } -} - -/// The loaded, editable view of a Game.dat: where it came from, the read-only encoding badge, the -/// present scalar fields (in form order), and the present `SubFonts` (0..=3, as the file carries). -pub struct LoadedGameDat { - /// The on-disk `Game.dat` this was loaded from (the [`save`] in-place target). - pub path: PathBuf, - /// The encoding badge (`"utf8"` / `"shiftjis"`), read-only. - pub encoding: String, - /// The present scalar fields, in display order. - pub fields: Vec, - /// The present sub-font values (editable), in order. Empty when the file carries none. - pub sub_fonts: Vec, -} - -/// One editable sub-font slot (a `SubFonts` array entry). -pub struct SubFont { - pub value: String, - pub original: String, -} - -impl SubFont { - pub fn is_changed(&self) -> bool { - self.value != self.original - } -} - -/// The form-order + group + label for each scalar key. Only keys actually present in the dumped JSON -/// become [`Field`]s, so an absent optional never shows (and is never materialized on save). -const SCALAR_FORM: &[(&str, &str, Group)] = &[ - ("Title", "Title", Group::Titles), - ("TitlePlus", "Title plus", Group::Titles), - ("StartUpMsg", "Start-up message", Group::Titles), - ("TitleMsg", "Title message", Group::Titles), - ("Font", "Font", Group::Fonts), - ("DefaultPcGraphic", "Default PC graphic", Group::Graphics), - ("RoadImg", "Road image", Group::Graphics), - ("GaugeImg", "Gauge image", Group::Graphics), - ("UnknownString14", "Unknown string 14", Group::Other), -]; - -impl LoadedGameDat { - /// How many fields (scalars + sub-fonts) the user has changed. - pub fn changed_count(&self) -> usize { - self.fields.iter().filter(|f| f.is_changed()).count() - + self.sub_fonts.iter().filter(|s| s.is_changed()).count() - } - - /// True when anything was edited (so the Save button can gate on it). - pub fn dirty(&self) -> bool { - self.changed_count() > 0 - } - - /// Build the minimal `apply_game_dat` JSON: an object holding only the *changed* fields (each - /// already proven present at load time, so `apply_game_dat` only edits existing cells). Including - /// just the changes keeps unchanged cells untouched, which is what makes a no-change save - /// byte-exact. `SubFonts` must be passed whole (the array is replaced atomically) whenever any - /// entry changed, so the unchanged entries carry their current value through. - fn edits_json(&self) -> String { - let mut obj = Map::new(); - for f in &self.fields { - if f.is_changed() { - obj.insert(f.key.to_string(), Value::String(f.value.clone())); - } - } - if self.sub_fonts.iter().any(SubFont::is_changed) { - let arr: Vec = - self.sub_fonts.iter().map(|s| Value::String(s.value.clone())).collect(); - obj.insert("SubFonts".to_string(), Value::Array(arr)); - } - Value::Object(obj).to_string() - } -} - -/// Read and parse a `Game.dat` into the editable form model, reusing [`GameDat::read`] + -/// [`dump_game_dat`] (the CLI's `gamedat-json` path). Only the fields the file actually carries -/// appear (the dump omits absent optionals). The `encoding` badge is read-only. -pub fn load(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?; - let gd = GameDat::read(&bytes).map_err(|e| format!("Game.dat parse failed: {e}"))?; - let json = dump_game_dat(&gd); - let root: Value = serde_json::from_str(&json).map_err(|e| format!("invalid dump JSON: {e}"))?; - let obj = root.as_object().ok_or("Game.dat dump must be a JSON object")?; - - let encoding = obj - .get("encoding") - .and_then(Value::as_str) - .unwrap_or("utf8") - .to_string(); - - // Present scalars, in form order (skip any the dump omitted for this file's string_count). - let mut fields = Vec::new(); - for (key, label, group) in SCALAR_FORM { - if let Some(v) = obj.get(*key).and_then(Value::as_str) { - fields.push(Field { - key, - label, - value: v.to_string(), - original: v.to_string(), - group: *group, - }); - } - } - - // SubFonts: a present array (0..=3 entries). Each becomes an editable slot. - let sub_fonts = obj - .get("SubFonts") - .and_then(Value::as_array) - .map(|arr| { - arr.iter() - .filter_map(Value::as_str) - .map(|s| SubFont { value: s.to_string(), original: s.to_string() }) - .collect() - }) - .unwrap_or_default(); - - Ok(LoadedGameDat { - path: path.to_path_buf(), - encoding, - fields, - sub_fonts, - }) -} - -/// Where a backup goes for an in-place save. -fn backup_path(file: &Path) -> PathBuf { - let mut name = file.file_name().map(|s| s.to_os_string()).unwrap_or_default(); - name.push(".bak"); - file.with_file_name(name) -} - -/// The outcome of a [`save`] run, for the log. -pub struct SaveOutcome { - /// How many fields `apply_game_dat` actually changed. - pub changed: usize, - /// True when the output is byte-identical to the base (an unchanged save, or edits that net to - /// the original values). - pub byte_identical: bool, - /// Where the result was written. - pub out: PathBuf, - /// The backup path, if one was made. - pub backup: Option, -} - -/// Apply the form's edits onto the base `Game.dat` and write the result to `output`, mirroring the -/// CLI's `gamedat-apply`: build a minimal JSON of the changed fields, [`apply_game_dat`] it onto a -/// freshly-read base, **re-parse the [`GameDat::write`] output to verify it round-trips** before -/// writing, and report the change count + byte-identity. When `backup` is set and the output already -/// exists, the original is copied to `.bak` first. -/// -/// Byte-exactness: `apply_game_dat` only rewrites cells whose value changed and `GameDat::write` -/// anchors its size/offset housekeeping on the read-time body size, so untouched fields re-serialize -/// verbatim and a no-change save reproduces the base byte-for-byte. -pub fn save(loaded: &LoadedGameDat, output: &Path, backup: bool) -> Result { - // Always read the base fresh from disk (the load-time bytes), so the apply is onto a pristine - // base and the byte-identity check is honest. - let base_bytes = std::fs::read(&loaded.path) - .map_err(|e| format!("cannot read base {}: {e}", loaded.path.display()))?; - let mut gd = GameDat::read(&base_bytes).map_err(|e| format!("base parse failed: {e}"))?; - - let edits = loaded.edits_json(); - let changed = apply_game_dat(&mut gd, &edits).map_err(|e| format!("apply failed: {e}"))?; - - let out_bytes = gd.write(); - // Re-parse the output to verify it still round-trips before committing it to disk (the guard - // `cmd_gamedat_apply` runs). - GameDat::read(&out_bytes) - .map_err(|e| format!("internal error: edited Game.dat no longer parses ({e})"))?; - let byte_identical = out_bytes == base_bytes; - - // Back up the original (only when overwriting an existing target). - let backup_made = if backup && output.exists() { - let bak = backup_path(output); - std::fs::copy(output, &bak) - .map_err(|e| format!("backup failed ({}): {e}", bak.display()))?; - Some(bak) - } else { - None - }; - - if let Some(parent) = output.parent() { - if !parent.as_os_str().is_empty() { - let _ = std::fs::create_dir_all(parent); - } - } - std::fs::write(output, &out_bytes) - .map_err(|e| format!("write failed ({}): {e}", output.display()))?; - - Ok(SaveOutcome { - changed, - byte_identical, - out: output.to_path_buf(), - backup: backup_made, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A unique temp dir for scratch output (never a game folder or the fixtures root). - fn tmp_dir(tag: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!( - "wolfdawn_gamedat_{tag}_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::create_dir_all(&p); - p - } - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// The first present Game.dat fixture from the fixtures root (read-only). - fn fixture() -> Option { - ["chamber/Data/BasicData/Game.dat"] - .into_iter() - .find_map(test_data) - } - - /// Helper: find a field by key in a loaded model. - fn field<'a>(g: &'a LoadedGameDat, key: &str) -> Option<&'a Field> { - g.fields.iter().find(|f| f.key == key) - } - - /// Load a real Game.dat, change the Font field through this module's code, save to a temp output, - /// then reload. Font is the new value and Title etc. unchanged. Then a no-change save reproduces - /// the file byte-for-byte. Never touches the fixtures root (reads the fixture, writes only temp). - #[test] - fn load_edit_font_save_reload_round_trip() { - let Some(input) = fixture() else { - eprintln!("skip load_edit_font_save_reload_round_trip: no Game.dat fixture present"); - return; - }; - // Work on a COPY so the base path is in a temp dir (we never write into the fixtures root). - let work = tmp_dir("rt"); - let copy = work.join("Game.dat"); - std::fs::copy(&input, ©).expect("copy fixture"); - - let mut g = load(©).expect("load"); - assert!(field(&g, "Title").is_some(), "Title should always be present"); - assert!(field(&g, "Font").is_some(), "Font should always be present"); - let original_title = field(&g, "Title").unwrap().value.clone(); - let original_font = field(&g, "Font").unwrap().value.clone(); - - // Change the Font field. - let new_font = format!("{original_font}_X"); - g.fields.iter_mut().find(|f| f.key == "Font").unwrap().value = new_font.clone(); - assert_eq!(g.changed_count(), 1, "exactly one field changed"); - - let out = work.join("edited.dat"); - let outcome = save(&g, &out, false).expect("save"); - assert_eq!(outcome.changed, 1, "apply should report one field changed"); - assert!(!outcome.byte_identical, "an edited save differs from base"); - - // Reload the edited file: Font is the new value, Title unchanged. - let re = load(&out).expect("reload"); - assert_eq!(field(&re, "Font").unwrap().value, new_font, "Font should be the new value"); - assert_eq!( - field(&re, "Title").unwrap().value, - original_title, - "Title must be unchanged" - ); - assert_eq!(re.encoding, g.encoding, "encoding preserved"); - - // A no-change save reproduces the (original) file byte-for-byte. - let pristine = load(©).expect("reload pristine"); - let noop_out = work.join("noop.dat"); - let noop = save(&pristine, &noop_out, false).expect("no-op save"); - assert_eq!(noop.changed, 0, "no fields changed"); - assert!(noop.byte_identical, "a no-change save must be byte-identical to base"); - assert_eq!( - std::fs::read(&noop_out).unwrap(), - std::fs::read(©).unwrap(), - "no-op output equals the original bytes" - ); - - let _ = std::fs::remove_dir_all(&work); - } - - /// In-place save with a backup writes a `.bak` holding the original bytes, then overwrites. - #[test] - fn in_place_save_backs_up_original() { - let Some(input) = fixture() else { - eprintln!("skip in_place_save_backs_up_original: no Game.dat fixture present"); - return; - }; - let work = tmp_dir("bak"); - let copy = work.join("Game.dat"); - std::fs::copy(&input, ©).expect("copy fixture"); - let original_bytes = std::fs::read(©).unwrap(); - - let mut g = load(©).expect("load"); - let title = g.fields.iter_mut().find(|f| f.key == "Title").unwrap(); - title.value = format!("{} [GUI]", title.value); - - // Save in place (output == loaded.path) with backup ON. - let outcome = save(&g, ©, true).expect("save in place"); - assert!(outcome.backup.is_some(), "a backup should have been made"); - let bak = outcome.backup.unwrap(); - assert_eq!(std::fs::read(&bak).unwrap(), original_bytes, "backup holds the original bytes"); - // The in-place file is now the edited one (reloads to the new title). - let re = load(©).expect("reload"); - assert!(field(&re, "Title").unwrap().value.ends_with("[GUI]"), "title was rewritten"); - - let _ = std::fs::remove_dir_all(&work); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/log.rs b/util/wolfdawn-master/crates/wolf-gui/src/log.rs deleted file mode 100644 index 0b96b00..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/log.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! A bounded, colour-coded activity log shown in the bottom panel. -//! -//! It is a simple ring buffer of `(level, message)`: helpers `info`/`warn`/`error` append a line, -//! the oldest lines are dropped past a cap so a long session never grows unbounded, and -//! [`Log::show`] renders it bottom-anchored with auto-scroll. Background jobs feed it through the -//! [`Reporter`](crate::task::Reporter). The UI thread feeds it directly. - -// `len`/`is_empty` are part of the log's public surface (and exercised by tests) but not called by -// the Phase 0 bin itself. Allow them rather than gating on `cfg(test)`. -#![allow(dead_code)] - -use egui::Color32; - -/// Severity of a log line, controlling its colour. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Level { - Info, - Warn, - Error, -} - -impl Level { - fn color(self) -> Color32 { - match self { - Level::Info => Color32::from_rgb(0xC8, 0xC8, 0xC8), - Level::Warn => Color32::from_rgb(0xE0, 0xB0, 0x40), - Level::Error => Color32::from_rgb(0xE0, 0x60, 0x60), - } - } -} - -/// A scrolling activity log with a fixed line cap. -pub struct Log { - lines: Vec<(Level, String)>, - cap: usize, -} - -impl Default for Log { - fn default() -> Self { - Self { - lines: Vec::new(), - cap: 500, - } - } -} - -impl Log { - pub fn new() -> Self { - Self::default() - } - - /// Append a line at `level`, dropping the oldest if the cap is exceeded. - pub fn push(&mut self, level: Level, msg: impl Into) { - self.lines.push((level, msg.into())); - if self.lines.len() > self.cap { - let overflow = self.lines.len() - self.cap; - self.lines.drain(0..overflow); - } - } - - pub fn info(&mut self, msg: impl Into) { - self.push(Level::Info, msg); - } - - pub fn warn(&mut self, msg: impl Into) { - self.push(Level::Warn, msg); - } - - pub fn error(&mut self, msg: impl Into) { - self.push(Level::Error, msg); - } - - /// Number of buffered lines (used by tests). - pub fn len(&self) -> usize { - self.lines.len() - } - - pub fn is_empty(&self) -> bool { - self.lines.is_empty() - } - - /// Render the log into the current panel, scrolled to the newest line. - pub fn show(&self, ui: &mut egui::Ui) { - egui::ScrollArea::vertical() - .auto_shrink([false, false]) - .stick_to_bottom(true) - .show(ui, |ui| { - ui.spacing_mut().item_spacing.y = 1.0; - for (level, msg) in &self.lines { - ui.colored_label(level.color(), egui::RichText::new(msg).monospace()); - } - }); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/main.rs b/util/wolfdawn-master/crates/wolf-gui/src/main.rs deleted file mode 100644 index b4d01bf..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/main.rs +++ /dev/null @@ -1,759 +0,0 @@ -//! WolfDawn Studio. The desktop GUI front-end for the WolfDawn toolchain. -//! -//! Phase 0 ships the app shell (top/side/bottom/central panel layout), the Project section (which -//! reads a real `Game.dat`), and the shared infrastructure every later section plugs into: -//! background jobs with a progress bar, a colour-coded activity log, native file pickers, and the -//! reusable labelled controls. The CLI is unchanged. This is purely additive. -//! -//! The workspace forbids `unsafe`. eframe/egui/rfd carry their own `unsafe` internally. None is -//! written here. - -#![forbid(unsafe_code)] -// On Windows, suppress the console window for a release GUI build. Debug builds keep the console -// so `eprintln!`/panics are visible while developing. -#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] - -// Use mimalloc so freed memory (after a large extract / decompile / DB load) is returned to the OS -// instead of being held by the default allocator. No unsafe here: the attribute on a static is safe, -// the GlobalAlloc impl lives in the dependency. -#[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; - -mod app; -mod archive; -mod database; -mod decompile; -mod gamedat; -mod log; -mod project; -mod saves; -mod task; -mod translation; -mod verify; -mod widgets; - -use app::WolfDawnApp; - -fn main() -> eframe::Result<()> { - let native_options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_title("WolfDawn Studio") - .with_inner_size([1000.0, 680.0]) - .with_min_inner_size([720.0, 480.0]), - ..Default::default() - }; - eframe::run_native( - "WolfDawn Studio", - native_options, - Box::new(|cc| Ok(Box::new(WolfDawnApp::new(cc)))), - ) -} - -#[cfg(test)] -mod tests { - use super::app::{Section, WolfDawnApp}; - use super::project; - use std::path::{Path, PathBuf}; - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file/dir is missing, so the data-dependent gates skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// The first present fixture among `rels`, resolved under `WOLFDAWN_TEST_DATA`. - fn first_present(rels: &[&str]) -> Option { - rels.iter().find_map(|r| test_data(r)) - } - - /// Drive `f` under a fresh headless egui context (no window/GPU), the way the test gates - /// exercise the UI. `ctx.run` calls the closure to build a frame. `FnMut` since egui may call - /// it more than once. - fn run_headless(f: impl FnMut(&egui::Context)) { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), f); - } - - /// Every `Section` must render without panicking under a headless context. - #[test] - fn every_section_renders() { - for section in Section::ALL { - let mut app = WolfDawnApp::default(); - app.set_section(section); - run_headless(|ctx| { - egui::CentralPanel::default().show(ctx, |ui| { - app.render_current_section(ui); - }); - }); - } - } - - /// A full frame (all four panels, the same body `eframe::App::update` runs) must render for - /// every section without panicking. - #[test] - fn full_frame_runs_for_every_section() { - for section in Section::ALL { - let mut app = WolfDawnApp::default(); - app.set_section(section); - // Two frames: the second exercises any state seeded on the first (e.g. the archive - // section defaulting its selection from the open project). - for _ in 0..2 { - run_headless(|ctx| app.render_frame(ctx)); - } - } - } - - /// The Translation section must render with a *loaded* model too (the grid, not just the empty - /// state). Builds the model through the section's own extract code, installs it, and drives a - /// full headless frame. Skips gracefully when the data fixture is absent. - #[test] - fn translation_section_renders_with_model() { - use super::translation; - let Some(dir) = test_data("chamber/Data") else { - eprintln!("skip translation_section_renders_with_model: fixture not present"); - return; - }; - if !dir.join("BasicData").join("CommonEvent.dat").exists() { - eprintln!("skip translation_section_renders_with_model: fixture not present"); - return; - } - let outcome = translation::extract_model(&dir, |_, _| {}).expect("extract"); - let mut app = WolfDawnApp::default(); - app.set_section(Section::Translation); - app.set_translation_model(outcome.model); - // Two frames: the first lays out the grid, the second re-renders after any state settles. - for _ in 0..2 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - } - - /// The Saves section must render with a *loaded* save too (the editor + baked-string table, not - /// just the empty state). Inspects a COPY of a real save through the section's own code path, - /// installs it, and drives a full headless frame. Skips gracefully when no fixture is present. - #[test] - fn saves_section_renders_with_loaded_save() { - use super::saves; - let Some(fixture) = - first_present(&["chamber/SaveData01.sav", "pachimon/SaveData01.sav"]) - else { - eprintln!("skip saves_section_renders_with_loaded_save: no save fixture present"); - return; - }; - // Inspect a COPY so the game folder is never touched. - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_saves_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let copy = tmp.join("SaveData01.sav"); - std::fs::copy(fixture, ©).expect("copy fixture"); - let loaded = saves::inspect(©).expect("inspect"); - - let mut app = WolfDawnApp::default(); - app.set_section(Section::Saves); - app.set_loaded_save(loaded); - for _ in 0..2 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - let _ = std::fs::remove_dir_all(&tmp); - } - - /// The Saves section's headless code path: inspect a real save (on a COPY), change the title + - /// one baked string, save it through the section logic, re-inspect from the written bytes, and - /// assert the title + string changed and the format is preserved. Never touches game folders. - #[test] - fn saves_inspect_edit_save_reinspect() { - use super::saves; - let Some(fixture) = - first_present(&["chamber/SaveData01.sav", "pachimon/SaveData01.sav"]) - else { - eprintln!("skip saves_inspect_edit_save_reinspect: no save fixture present"); - return; - }; - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_rt_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let copy = tmp.join("SaveData01.sav"); - std::fs::copy(fixture, ©).expect("copy fixture"); - - let mut loaded = saves::inspect(©).expect("inspect"); - assert!(loaded.editable(), "real save should be editable"); - let fmt = loaded.format; - let new_title = format!("{} [UI]", loaded.original_title); - loaded.title = new_title.clone(); - // Edit the first string that re-encodes in this save's encoding. - let utf8 = loaded.encoding == "utf8"; - let mut marker = None; - for r in loaded.strings.iter_mut() { - if r.original.is_empty() { - continue; - } - let cand = format!("{} X", r.original); - let ok = utf8 || !encoding_rs::SHIFT_JIS.encode(&cand).2; - if ok { - r.edited = cand.clone(); - marker = Some(cand); - break; - } - } - - let stats = saves::save_changes(&loaded, true).expect("save"); - assert!(stats.title_changed); - let re = saves::inspect(©).expect("re-inspect"); - assert_eq!(re.format, fmt, "format preserved"); - assert_eq!(re.title, new_title, "title changed"); - if let Some(m) = marker { - assert!(re.strings.iter().any(|r| r.original == m), "string changed"); - } - let _ = std::fs::remove_dir_all(&tmp); - } - - /// The Decompile section must render with a *loaded* WolfScript document too (the code editor + - /// Compile controls, not just the empty state). Decompiles a real file through the section's own - /// code path, installs it, and drives a full headless frame. Skips when no code fixture. - #[test] - fn decompile_section_renders_with_script() { - use super::decompile; - let Some(input) = first_present(&[ - "chamber/Data/MapData/TitleMap.mps", - "chamber/Data/BasicData/CommonEvent.dat", - ]) else { - eprintln!("skip decompile_section_renders_with_script: no code fixture present"); - return; - }; - let script = decompile::decompile_edit(&input).expect("decompile"); - let mut app = WolfDawnApp::default(); - app.set_section(Section::Decompile); - app.set_decompile_script(input.clone(), script); - for _ in 0..2 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - } - - /// The Decompile find bar (Ctrl+F) must render + run a query on a loaded script without - /// panicking: open the bar with a query that occurs in the decompiled WolfScript and drive a few - /// full headless frames (the find bar sets a selection + scrolls the editor). Skips when no - /// code fixture is present. This guards the find-bar UI path (selection store + scroll-to). - #[test] - fn decompile_find_bar_renders_with_query() { - use super::decompile; - let Some(input) = first_present(&[ - "chamber/Data/MapData/TitleMap.mps", - "chamber/Data/BasicData/CommonEvent.dat", - ]) else { - eprintln!("skip decompile_find_bar_renders_with_query: no code fixture present"); - return; - }; - let script = decompile::decompile_edit(&input).expect("decompile"); - // Pick a query that is guaranteed to occur (the first word of the script, else a common - // token). An empty match list is fine too. The bar must still render without panicking. - let query = script - .split_whitespace() - .next() - .map(|s| s.to_string()) - .unwrap_or_else(|| "Event".to_string()); - - let mut app = WolfDawnApp::default(); - app.set_section(Section::Decompile); - app.set_decompile_script(input.to_path_buf(), script); - app.open_decompile_find(&query); - // Several frames: open + jump-to-match, then re-render after the selection/scroll settles. - for _ in 0..3 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - } - - /// The Decompile section's headless code path: decompile a real map/common-event to WolfScript, - /// assert non-empty, compile it back onto the original base to a temp output, and assert the - /// re-decompiled text matches (and the untouched compile is byte-identical to the base). Never - /// modifies the fixtures root (reads the fixture, writes only into a temp dir). - #[test] - fn decompile_compile_round_trip_via_section() { - use super::decompile; - let Some(input) = first_present(&[ - "chamber/Data/MapData/TitleMap.mps", - "chamber/Data/BasicData/CommonEvent.dat", - ]) else { - eprintln!("skip decompile_compile_round_trip_via_section: no code fixture present"); - return; - }; - let input = input.as_path(); - let text = decompile::decompile_edit(input).expect("decompile"); - assert!(!text.is_empty(), "WolfScript should be non-empty"); - - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_dec_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - // Keep the base's file name so the compiled output re-classifies the same way. - let out = tmp.join(input.file_name().expect("file name")); - let outcome = decompile::compile_to(&text, input, &out).expect("compile"); - assert!(out.exists() && outcome.bytes > 0, "compiled output should be written"); - assert!(outcome.byte_identical, "untouched compile is byte-identical to base"); - // Compare the re-decompile against a decompile of the base placed in the same temp dir, so - // both share the same (BasicData-less) symbol context (the operand labels match). The - // byte-identity above is the strong guarantee, the labels are stripped by the compiler. - let base_here = tmp.join("base").join(input.file_name().unwrap()); - std::fs::create_dir_all(base_here.parent().unwrap()).unwrap(); - std::fs::copy(input, &base_here).expect("copy base"); - let baseline = decompile::decompile_edit(&base_here).expect("decompile base copy"); - let re = decompile::decompile_edit(&out).expect("re-decompile"); - assert_eq!(re, baseline, "re-decompiled WolfScript should match the base's own decompile"); - let _ = std::fs::remove_dir_all(&tmp); - } - - /// The Game.dat section must render with a *loaded* model too (the field form, not just the empty - /// state). Loads a real Game.dat (on a COPY) through the section's own code path, installs it, and - /// drives a full headless frame. Skips gracefully when the Game.dat fixture is absent. - #[test] - fn gamedat_section_renders_with_loaded() { - use super::gamedat; - let Some(fixture) = first_present(&["chamber/Data/BasicData/Game.dat"]) else { - eprintln!("skip gamedat_section_renders_with_loaded: no Game.dat fixture present"); - return; - }; - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_gd_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let copy = tmp.join("Game.dat"); - std::fs::copy(fixture, ©).expect("copy fixture"); - let loaded = gamedat::load(©).expect("load"); - - let mut app = WolfDawnApp::default(); - app.set_section(Section::GameDat); - app.set_gamedat_loaded(loaded); - for _ in 0..2 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - let _ = std::fs::remove_dir_all(&tmp); - } - - /// The Game.dat section's headless code path: load a real Game.dat (on a COPY), change Font, save - /// to a temp output, then reload. Font is new and Title unchanged. A no-change save is byte-exact. - #[test] - fn gamedat_edit_font_round_trip_via_section() { - use super::gamedat; - let Some(fixture) = first_present(&["chamber/Data/BasicData/Game.dat"]) else { - eprintln!("skip gamedat_edit_font_round_trip_via_section: no Game.dat fixture present"); - return; - }; - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_gdrt_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let copy = tmp.join("Game.dat"); - std::fs::copy(fixture, ©).expect("copy fixture"); - - let mut g = gamedat::load(©).expect("load"); - let title = g.fields.iter().find(|f| f.key == "Title").map(|f| f.value.clone()); - let font = g.fields.iter().find(|f| f.key == "Font").expect("Font present").value.clone(); - let new_font = format!("{font}_X"); - g.fields.iter_mut().find(|f| f.key == "Font").unwrap().value = new_font.clone(); - - let out = tmp.join("edited.dat"); - let outcome = gamedat::save(&g, &out, false).expect("save"); - assert_eq!(outcome.changed, 1, "one field changed"); - let re = gamedat::load(&out).expect("reload"); - assert_eq!( - re.fields.iter().find(|f| f.key == "Font").unwrap().value, - new_font, - "Font is the new value" - ); - assert_eq!( - re.fields.iter().find(|f| f.key == "Title").map(|f| f.value.clone()), - title, - "Title unchanged" - ); - - // No-change save reproduces the original byte-for-byte. - let pristine = gamedat::load(©).expect("reload pristine"); - let noop = tmp.join("noop.dat"); - let n = gamedat::save(&pristine, &noop, false).expect("no-op save"); - assert!(n.byte_identical, "no-change save is byte-identical"); - assert_eq!(std::fs::read(&noop).unwrap(), std::fs::read(©).unwrap()); - let _ = std::fs::remove_dir_all(&tmp); - } - - /// The Database section must render with a *loaded* model too (the type selector + editable grid, - /// not just the empty state). Loads a real DB (on a COPY of the .project+.dat pair) through the - /// section's own code path, installs it, and drives a full headless frame. Skips when absent. - #[test] - fn database_section_renders_with_loaded() { - use super::database; - let Some(src) = test_data("chamber/Data/BasicData/DataBase.project") - .filter(|p| p.with_extension("dat").exists()) - else { - eprintln!("skip database_section_renders_with_loaded: no database fixture present"); - return; - }; - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_db_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let proj = tmp.join("DataBase.project"); - std::fs::copy(&src, &proj).expect("copy .project"); - std::fs::copy(src.with_extension("dat"), proj.with_extension("dat")).expect("copy .dat"); - let loaded = database::load(&proj).expect("load"); - - let mut app = WolfDawnApp::default(); - app.set_section(Section::Database); - app.set_database_loaded(loaded); - for _ in 0..2 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - let _ = std::fs::remove_dir_all(&tmp); - } - - /// Unpack the Chamber fixture's Data.wolf (if present) through the Archive section's own code path - /// into a temp dir, and assert the data files appear. Never touches the game folder. Skips when - /// the archive is absent. - #[test] - fn archive_unpack_data_wolf() { - use super::archive; - let Some(arc) = test_data("chamber/Data.wolf") else { - eprintln!("skip archive_unpack_data_wolf: archive fixture not present"); - return; - }; - let out = std::env::temp_dir().join(format!("wolfdawn_gui_unpack_{}", std::process::id())); - let _ = std::fs::create_dir_all(&out); - let outcome = archive::unpack(&arc, &out, |_, _| {}).expect("unpack"); - assert!(outcome.written > 0, "files should be written"); - assert!( - out.join("BasicData/CommonEvent.dat").exists(), - "BasicData/CommonEvent.dat should be unpacked" - ); - let _ = std::fs::remove_dir_all(&out); - } - - /// Repack round-trip through the Archive section: pack a small temp folder (copies of a couple - /// fixture data files) to a temp .wolf, unpack it, and confirm the files + content come back. - /// Never modifies the fixtures root (read-only copies + temp dirs). - #[test] - fn archive_repack_round_trip() { - use super::archive; - let present: Vec = [ - "chamber/Data/BasicData/Game.dat", - "chamber/Data/BasicData/CommonEvent.dat", - ] - .iter() - .filter_map(|r| test_data(r)) - .collect(); - if present.is_empty() { - eprintln!("skip archive_repack_round_trip: no data fixtures present"); - return; - } - let base = std::env::temp_dir().join(format!("wolfdawn_gui_repack_{}", std::process::id())); - let src = base.join("src"); - let _ = std::fs::create_dir_all(src.join("BasicData")); - let mut expect: Vec<(String, Vec)> = Vec::new(); - for p in &present { - let name = p.file_name().unwrap().to_string_lossy().to_string(); - std::fs::copy(p, src.join("BasicData").join(&name)).expect("copy"); - expect.push((format!("BasicData/{name}"), std::fs::read(p).expect("read"))); - } - - let out_wolf = base.join("Data.wolf"); - let outcome = - archive::repack(&src, &out_wolf, &archive::PackOptions::default(), |_, _| {}).expect("repack"); - assert!(outcome.files >= 1 && outcome.bytes > 0, "should pack a non-empty archive"); - - let back = base.join("back"); - archive::unpack(&out_wolf, &back, |_, _| {}).expect("unpack repacked"); - for (rel, bytes) in &expect { - let got = back.join(rel); - assert!(got.exists(), "{rel} should round-trip"); - assert_eq!(&std::fs::read(&got).expect("read back"), bytes, "{rel} content matches"); - } - let _ = std::fs::remove_dir_all(&base); - } - - /// The Verify section's single-file path: a known-good plaintext data file from the fixtures must - /// verify PASS. Read-only. Skips when absent. - #[test] - fn verify_single_known_good() { - use super::verify; - let Some(path) = first_present(&[ - "chamber/Data/MapData/TitleMap.mps", - "chamber/Data/BasicData/Game.dat", - "chamber/Data/BasicData/CommonEvent.dat", - ]) else { - eprintln!("skip verify_single_known_good: no data fixture present"); - return; - }; - let verdict = verify::verify_file(&path); - assert!( - verdict.is_pass(), - "{} should PASS, got {}: {}", - path.display(), - verdict.tag(), - verdict.detail() - ); - } - - /// Opening a known game dir populates the project's Game.dat facts. Skips gracefully when the - /// fixture is absent (CI without the data corpus). - #[test] - fn opens_known_game_dir() { - let Some(dir) = test_data("chamber/Data") else { - eprintln!("skip opens_known_game_dir: fixture not present"); - return; - }; - if !dir.join("BasicData").join("Game.dat").exists() { - eprintln!("skip opens_known_game_dir: fixture {dir:?} not present"); - return; - } - let outcome = project::open_project(&dir); - let p = outcome.project; - assert!(p.game_dat_ok, "Game.dat should have parsed for the fixture"); - assert!(p.game_dat.is_some(), "Game.dat path should be recorded"); - assert!(!p.title.is_empty(), "a title should have been decoded"); - // The fixture's BasicData is the data dir. It must have been located. - assert!(p.data_dir.is_some(), "the data dir should have been located"); - } - - /// Opening a path with no Game.dat is non-fatal: the project still opens, flagged not-ok. - #[test] - fn open_missing_game_dat_is_non_fatal() { - let tmp = std::env::temp_dir().join(format!("wolfdawn_gui_test_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let outcome = project::open_project(&tmp); - assert!(!outcome.project.game_dat_ok); - assert!(outcome.notes.iter().any(|(_, m)| m.contains("Game.dat"))); - let _ = std::fs::remove_dir_all(&tmp); - } - - /// A unique temp game dir for the index tests, with the minimal layout the scan looks at: - /// `BasicData/CommonEvent.dat`, `BasicData/DataBase.project`, `MapData/X.mps`, and a - /// `Save/SaveData01.sav`. The files are fabricated (rescan only *lists* by name/extension, it - /// never parses), so this never touches real game folders or the fixtures root. - fn make_temp_game(tag: &str) -> std::path::PathBuf { - let root = std::env::temp_dir() - .join(format!("wolfdawn_gui_idx_{tag}_{}", std::process::id())); - let _ = std::fs::remove_dir_all(&root); - let data = root.join("Data"); - std::fs::create_dir_all(data.join("BasicData")).expect("mk BasicData"); - std::fs::create_dir_all(data.join("MapData")).expect("mk MapData"); - std::fs::create_dir_all(root.join("Save")).expect("mk Save"); - std::fs::write(data.join("BasicData").join("CommonEvent.dat"), b"x").expect("CommonEvent"); - std::fs::write(data.join("BasicData").join("DataBase.project"), b"x").expect("project"); - std::fs::write(data.join("BasicData").join("DataBase.dat"), b"x").expect("project .dat"); - std::fs::write(data.join("MapData").join("MapA.mps"), b"x").expect("MapA"); - std::fs::write(root.join("Save").join("SaveData01.sav"), b"x").expect("save"); - root - } - - /// Opening a fabricated game dir populates the scanned index (code files / databases / saves), - /// and a later `rescan()` after a NEW .mps is added returns `true` and lists the new file. - #[test] - fn project_index_and_rescan_detects_new_file() { - let root = make_temp_game("rescan"); - - let mut p = project::open_project(&root).project; - // CommonEvent.dat + MapA.mps = 2 code files, one .project, one save. - assert!( - p.code_files.iter().any(|f| f.ends_with("MapData/MapA.mps")), - "MapA.mps should be indexed; got {:?}", - p.code_files - ); - assert!( - p.code_files.iter().any(|f| f.ends_with("BasicData/CommonEvent.dat")), - "CommonEvent.dat should be indexed" - ); - assert_eq!(p.databases.len(), 1, "one database .project indexed"); - assert_eq!(p.saves.len(), 1, "one save indexed"); - assert!(p.save_dir.is_some(), "the Save/ dir should be recorded"); - - // A no-op rescan reports no change. - assert!(!p.rescan(), "rescan with no fs change returns false"); - - // Add a second map, then rescan returns true and the new map appears. - let new_map = root.join("Data").join("MapData").join("MapB.mps"); - std::fs::write(&new_map, b"x").expect("write MapB"); - assert!(p.rescan(), "rescan after adding a .mps returns true"); - assert!( - p.code_files.iter().any(|f| f.ends_with("MapData/MapB.mps")), - "the newly-added MapB.mps should now be indexed; got {:?}", - p.code_files - ); - - let _ = std::fs::remove_dir_all(&root); - } - - /// The persisted `Settings` payload round-trips through serde unchanged (covers the eframe - /// storage value), for both a non-default and the default configuration. - #[test] - fn settings_serde_round_trip() { - use super::app::Settings; - let custom = Settings { - dark_mode: false, - default_en_punct: true, - default_allow_code_drift: true, - default_backup: false, - auto_detect: false, - }; - let json = serde_json::to_string(&custom).expect("serialize"); - let back: Settings = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(custom, back, "custom settings round-trip equal"); - - let def = Settings::default(); - let json = serde_json::to_string(&def).expect("serialize default"); - let back: Settings = serde_json::from_str(&json).expect("deserialize default"); - assert_eq!(def, back, "default settings round-trip equal"); - } - - /// The Settings section must render in a full headless frame with a project loaded (so the - /// per-section dropdowns also render), without panicking. Uses a fabricated temp game dir. - #[test] - fn settings_section_renders_with_project() { - let root = make_temp_game("render"); - let project = project::open_project(&root).project; - - let mut app = WolfDawnApp::default(); - app.set_project(project); - // Render the Settings section, then walk a couple sections that show dropdowns, each over - // two frames so any first-frame state settles. - for section in [Section::Settings, Section::Decompile, Section::Database, Section::Saves] { - app.set_section(section); - for _ in 0..2 { - let ctx = egui::Context::default(); - let _ = ctx.run(Default::default(), |ctx| app.render_frame(ctx)); - } - } - let _ = std::fs::remove_dir_all(&root); - } - - // ------------------------------------------------------------------------------------------ - // No-emoji guard: the GUI must render its glyphs on ANY system, including ones without an OS - // emoji font. egui's bundled fonts (and the optional CJK fallback) cover Latin, the Geometric - // Shapes / Arrows blocks, and CJK, but NOT the Unicode emoji / pictograph blocks, which show - // as tofu (□) without an emoji font. These two tests keep the UI free of those pictographs. - // ------------------------------------------------------------------------------------------ - - /// The safe non-ASCII glyphs the UI uses in string/char literals: ASCII/Latin-1 - /// punctuation, Geometric Shapes (U+25A0..U+25FF), and Arrows (U+2190..U+21FF). Anything else - /// non-ASCII in a literal must be CJK-range (Japanese sample/help text). A stray emoji is caught. - const SAFE_GLYPHS: &[char] = &[ - '×', // U+00D7 multiplication sign, the clear/close button - '·', // U+00B7 middle dot, separators - '—', // U+2014 em dash, captions/log separators - '…', // U+2026 horizontal ellipsis, "Browse…", "Open…" - '→', // U+2192 rightwards arrow, "src → dst" log lines - '←', // U+2190 leftwards arrow, "Import DB ← file" log lines - '▶', // U+25B6 black right-pointing triangle, action buttons / log start marker - '◀', // U+25C0 black left-pointing triangle, find "Prev" - '●', // U+25CF black circle, the Database "edited" marker - ]; - - /// Every `.rs` file under `src/`, as (path, contents). - fn gui_source_files() -> Vec<(std::path::PathBuf, String)> { - let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); - let mut out = Vec::new(); - for entry in std::fs::read_dir(&src).expect("read src dir") { - let path = entry.expect("dir entry").path(); - if path.extension().and_then(|e| e.to_str()) == Some("rs") { - let text = std::fs::read_to_string(&path).expect("read source"); - out.push((path, text)); - } - } - assert!(!out.is_empty(), "found no .rs files under {}", src.display()); - out - } - - /// True for a codepoint in the Unicode emoji / pictograph blocks we must never ship: everything - /// at/above U+1F000 (Mahjong/Domino/Playing-cards through Symbols & Pictographs Extended-A). - fn is_emoji_pictograph(c: char) -> bool { - c as u32 >= 0x1F000 - } - - /// A codepoint that the always-loaded CJK fallback font covers (so it renders, not tofu): the - /// main CJK/kana blocks plus CJK Symbols & Punctuation and the Fullwidth/Halfwidth Forms used in - /// Japanese help text (e.g. 「」、。~). Conservative but ample for the tool's sample strings. - fn is_cjk(c: char) -> bool { - let u = c as u32; - (0x3000..=0x30FF).contains(&u) // CJK Symbols/Punctuation + Hiragana + Katakana - || (0x3400..=0x4DBF).contains(&u) // CJK Ext A - || (0x4E00..=0x9FFF).contains(&u) // CJK Unified Ideographs - || (0xFF00..=0xFFEF).contains(&u) // Halfwidth/Fullwidth Forms - } - - /// HARD GATE: no emoji-pictograph codepoint (>= U+1F000) may appear ANYWHERE in the GUI source. - /// Not in strings, not even in comments (there should be none). This makes it impossible to ship - /// a pictograph that could tofu on a user's machine. - #[test] - fn no_emoji_pictographs_in_source() { - let mut offenders: Vec = Vec::new(); - for (path, text) in gui_source_files() { - for (lineno, line) in text.lines().enumerate() { - for c in line.chars() { - if is_emoji_pictograph(c) { - offenders.push(format!( - "{}:{}: U+{:04X} {:?}", - path.file_name().unwrap().to_string_lossy(), - lineno + 1, - c as u32, - c - )); - } - } - } - } - assert!( - offenders.is_empty(), - "found emoji pictograph(s) (>=U+1F000) in GUI source — these tofu on systems without an \ - emoji font; replace with text or a Geometric/Arrow glyph:\n{}", - offenders.join("\n") - ); - } - - /// Strip Rust line comments (`//` and doc `///`) from `src`, returning code + string/char - /// literals only. The GUI source uses line comments exclusively (no `/* */` blocks), and no - /// string literal contains a `//` sequence, so truncating each line at its first `//` is exact - /// here. That is more robust than a hand lexer (no lifetime / raw-string / char-literal edge - /// cases). This keeps benign non-ASCII that lives ONLY in comments (e.g. the tofu glyph □, the - /// ␠ space marker, a ▼ doc example) from being mistaken for a UI glyph. - fn strip_comments(src: &str) -> String { - let mut out = String::with_capacity(src.len()); - for line in src.lines() { - let code = match line.find("//") { - Some(idx) => &line[..idx], - None => line, - }; - out.push_str(code); - out.push('\n'); - } - out - } - - /// ALLOW-LIST GATE: every non-ASCII glyph that survives comment-stripping (i.e. lives in code or - /// in a string/char literal) must be either CJK-range (Japanese sample/help text the CJK font - /// covers) or on the [`SAFE_GLYPHS`] list (Latin-1 punctuation + Geometric/Arrow glyphs the - /// bundled fonts cover). This catches a future stray dingbat (e.g. ✔ ✖ ⚠ ★) even though it sits - /// below U+1F000 and so escapes the hard pictograph gate above. - #[test] - fn non_ascii_ui_glyphs_are_safe_or_cjk() { - let mut offenders: Vec = Vec::new(); - for (path, text) in gui_source_files() { - let code = strip_comments(&text); - for c in code.chars() { - if (c as u32) <= 0x7F || c.is_whitespace() { - continue; - } - if SAFE_GLYPHS.contains(&c) || is_cjk(c) { - continue; - } - offenders.push(format!( - "{}: U+{:04X} {:?}", - path.file_name().unwrap().to_string_lossy(), - c as u32, - c - )); - } - } - offenders.sort(); - offenders.dedup(); - assert!( - offenders.is_empty(), - "found non-ASCII glyph(s) in GUI code/literals that are neither CJK nor on the safe \ - allow-list — they may tofu on systems lacking the right font; use text or a \ - Geometric/Arrow glyph (and extend SAFE_GLYPHS if you added an intentional one):\n{}", - offenders.join("\n") - ); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/project.rs b/util/wolfdawn-master/crates/wolf-gui/src/project.rs deleted file mode 100644 index 4bb530c..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/project.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! The open game **project**: locating the game's files on disk and reading `Game.dat`. -//! -//! A project is opened from either a folder or a `Data.wolf` / `Game.dat` file. From whatever the -//! user picks we resolve the *game root* (the dir holding `Data.wolf` and/or an unpacked `Data/`), -//! then locate the three things every later section needs: the `Data.wolf` archive, the unpacked -//! `Data/` directory, and `Game.dat`. `Game.dat` is parsed with [`GameDat::read`] to surface the -//! title / encoding / font / version. A missing or unparseable `Game.dat` is non-fatal. The -//! project still opens with whatever was found, and the caller logs a warning. - -use std::path::{Path, PathBuf}; - -use wolf_decompiler::extract_game_dat; -use wolf_decompiler::text::decode_wstr; -use wolf_formats::game_dat::GameDat; - -/// An opened game, with the on-disk locations later sections operate on plus the human-readable -/// facts read out of `Game.dat`. -#[derive(Default, Clone)] -pub struct Project { - /// The game root (the directory that holds `Data.wolf` and/or `Data/`). - pub root: PathBuf, - /// The packed `Data.wolf` archive, if present. - pub data_wolf: Option, - /// The unpacked `Data/` directory, if present. - pub data_dir: Option, - /// The `Game.dat` we parsed (root, `Data/`, or `Data/BasicData/`), if found. - pub game_dat: Option, - - // --- a cheap scanned index of the small relevant dirs (kept fresh by `rescan`) ------------- - /// Decompilable code files: `MapData/*.mps` + `BasicData/CommonEvent.dat` (sorted). - pub code_files: Vec, - /// Editable databases: `BasicData/*.project` (sorted). - pub databases: Vec, - /// `.sav` files found under the game's `Save/` dir (sorted). - pub saves: Vec, - /// The `Save/` dir the saves were found under (the batch-update default), if any. - pub save_dir: Option, - - // --- facts read from Game.dat (empty / defaults when it was absent or unparseable) --- - /// Window/game title (decoded for display). - pub title: String, - /// A short version/encoding descriptor (e.g. `UTF-8` vs `Shift-JIS`, plus title-suffix). - pub version: String, - /// True when `Game.dat` declared UTF-8 (vs Shift-JIS). - pub utf8: bool, - /// The editor's main font (decoded for display). - pub font: String, - /// True if `Game.dat` was found and parsed. - pub game_dat_ok: bool, -} - -impl Project { - /// Re-walk the small relevant subdirs (`MapData/`, `BasicData/`, and `Save/`) and refresh the - /// scanned file index ([`code_files`](Self::code_files) / [`databases`](Self::databases) / - /// [`saves`](Self::saves) + [`save_dir`](Self::save_dir)). Returns `true` if any of those sets - /// changed versus the previous scan, so the caller can log / repaint only on a real change. - /// - /// Cheap: it only `read_dir`s those specific folders (never the huge asset trees - /// like Picture/ or Sound/), and only stats by extension/name. The lists are sorted so the - /// dropdowns built from them have a stable order. - pub fn rescan(&mut self) -> bool { - let prev_code = self.code_files.clone(); - let prev_db = self.databases.clone(); - let prev_saves = self.saves.clone(); - let prev_save_dir = self.save_dir.clone(); - - // The data dir is where MapData/ and BasicData/ live (it may be the root itself when the - // project was opened on a folder that *is* a Data dir). Fall back to the root. - let data_base = self.data_dir.clone().unwrap_or_else(|| self.root.clone()); - - // Code: MapData/*.mps + BasicData/CommonEvent.dat. - let mut code_files = list_with_ext(&data_base.join("MapData"), "mps"); - let common = data_base.join("BasicData").join("CommonEvent.dat"); - if common.is_file() { - code_files.push(common); - } - code_files.sort(); - self.code_files = code_files; - - // Databases: BasicData/*.project (paired .dat is loaded by the section). - let mut databases = list_with_ext(&data_base.join("BasicData"), "project"); - databases.sort(); - self.databases = databases; - - // Saves: *.sav under a Save/ dir at the game root (and/or next to Game.exe == the root). - let (saves, save_dir) = scan_saves(&self.root); - self.saves = saves; - self.save_dir = save_dir; - - self.code_files != prev_code - || self.databases != prev_db - || self.saves != prev_saves - || self.save_dir != prev_save_dir - } -} - -/// List the files directly in `dir` whose extension matches `ext` (case-insensitive). Empty when -/// `dir` is missing or unreadable. The scan is best-effort and never fails a project open. -fn list_with_ext(dir: &Path, ext: &str) -> Vec { - let mut out = Vec::new(); - let Ok(entries) = std::fs::read_dir(dir) else { - return out; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path - .extension() - .and_then(|s| s.to_str()) - .is_some_and(|e| e.eq_ignore_ascii_case(ext)) - { - out.push(path); - } - } - out -} - -/// Locate the game's `Save/` dir (at the root) and list the `*.sav` files in it. Returns the sorted -/// save list plus the dir they came from (the batch-update default). Empty / `None` when there is no -/// `Save/` dir yet. -fn scan_saves(root: &Path) -> (Vec, Option) { - let save_dir = root.join("Save"); - if !save_dir.is_dir() { - return (Vec::new(), None); - } - let mut saves = list_with_ext(&save_dir, "sav"); - saves.sort(); - (saves, Some(save_dir)) -} - -/// The result of opening a project: the project itself plus any human-readable notes to log -/// (warnings for things that were missing or failed to parse, info for what was found). -pub struct OpenOutcome { - pub project: Project, - pub notes: Vec<(NoteLevel, String)>, -} - -/// Severity for an [`OpenOutcome`] note, mapped onto the log by the caller. -#[derive(Clone, Copy)] -pub enum NoteLevel { - Info, - Warn, -} - -/// Resolve the game root from whatever the user picked (a folder, a `Data.wolf`, or a `Game.dat`), -/// then locate `Data.wolf` / `Data/` / `Game.dat`, parse `Game.dat`, and build the [`Project`]. -/// -/// Never errors: a missing/garbled `Game.dat` produces a project with `game_dat_ok == false` and a -/// warning note. (The only "failure" worth refusing is a path that does not exist at all.) -pub fn open_project(picked: &Path) -> OpenOutcome { - let mut notes: Vec<(NoteLevel, String)> = Vec::new(); - - // Resolve the game root. If the user picked a file, the root is (heuristically) its parent, or - // its grandparent when the file sits inside a `Data/` (or `Data/BasicData/`) subtree. - let root = resolve_root(picked); - - // Locate Data.wolf (root or Data/) and the unpacked Data/ dir. - let data_wolf = ["Data.wolf"] - .iter() - .map(|n| root.join(n)) - .find(|p| p.is_file()); - let data_dir = { - let d = root.join("Data"); - if d.is_dir() { - Some(d) - } else { - // A folder that *is* a Data dir (holds BasicData) counts too. - root.join("BasicData").is_dir().then(|| root.clone()) - } - }; - - // Locate Game.dat. Try, in order: an explicitly-picked Game.dat, then the common spots. - let game_dat = locate_game_dat(picked, &root, data_dir.as_deref()); - - let mut project = Project { - root: root.clone(), - data_wolf: data_wolf.clone(), - data_dir: data_dir.clone(), - game_dat: game_dat.clone(), - ..Default::default() - }; - // Build the initial scanned index (code files / databases / saves) from the resolved dirs. - project.rescan(); - - // Log what we found, so the user sees the layout we resolved. - notes.push((NoteLevel::Info, format!("game root: {}", root.display()))); - match &data_wolf { - Some(p) => notes.push((NoteLevel::Info, format!("found Data.wolf: {}", p.display()))), - None => notes.push((NoteLevel::Info, "no Data.wolf (will work from unpacked Data/)".into())), - } - match &data_dir { - Some(p) => notes.push((NoteLevel::Info, format!("found unpacked data dir: {}", p.display()))), - None => notes.push((NoteLevel::Info, "no unpacked Data/ dir yet (unpack Data.wolf to create one)".into())), - } - // Surface the scanned index counts so the user sees what the per-section dropdowns will offer. - notes.push(( - NoteLevel::Info, - format!( - "indexed {} code file(s), {} database(s), {} save(s)", - project.code_files.len(), - project.databases.len(), - project.saves.len(), - ), - )); - - // Parse Game.dat if we found one. Failure is non-fatal. - match &game_dat { - Some(path) => match read_game_facts(path) { - Ok(facts) => { - project.title = facts.title; - project.version = facts.version; - project.utf8 = facts.utf8; - project.font = facts.font; - project.game_dat_ok = true; - notes.push(( - NoteLevel::Info, - format!("parsed Game.dat: title={:?}, encoding={}", project.title, encoding_label(project.utf8)), - )); - } - Err(e) => { - notes.push(( - NoteLevel::Warn, - format!("Game.dat found but could not be read ({e}); opening project without it"), - )); - } - }, - None => notes.push(( - NoteLevel::Warn, - "no Game.dat found (looked in the root, Data/, and Data/BasicData/)".into(), - )), - } - - OpenOutcome { project, notes } -} - -/// Human label for the encoding flag. -pub fn encoding_label(utf8: bool) -> &'static str { - if utf8 { - "UTF-8" - } else { - "Shift-JIS" - } -} - -/// Resolve the game root from a picked path. -fn resolve_root(picked: &Path) -> PathBuf { - if picked.is_dir() { - // If they picked a `Data` dir, the game root is its parent. Otherwise it's the dir itself. - if picked.file_name().and_then(|s| s.to_str()) == Some("Data") { - if let Some(parent) = picked.parent() { - return parent.to_path_buf(); - } - } - // If they picked a BasicData dir, climb to the game root (BasicData -> Data -> root). - if picked.file_name().and_then(|s| s.to_str()) == Some("BasicData") { - if let Some(root) = picked.parent().and_then(|p| p.parent()) { - return root.to_path_buf(); - } - } - return picked.to_path_buf(); - } - // A file: walk up out of a known `Data`/`BasicData` subtree to the game root. - let parent = picked.parent().unwrap_or(Path::new(".")); - let parent_name = parent.file_name().and_then(|s| s.to_str()); - match parent_name { - Some("BasicData") => parent - .parent() - .and_then(|p| p.parent()) - .unwrap_or(parent) - .to_path_buf(), - Some("Data") => parent.parent().unwrap_or(parent).to_path_buf(), - _ => parent.to_path_buf(), - } -} - -/// Locate `Game.dat`, trying (in order): an explicitly-picked `Game.dat`, the game root, -/// `Data/`, and `Data/BasicData/`. -fn locate_game_dat(picked: &Path, root: &Path, data_dir: Option<&Path>) -> Option { - if picked.is_file() && picked.file_name().and_then(|s| s.to_str()) == Some("Game.dat") { - return Some(picked.to_path_buf()); - } - let mut candidates = vec![ - root.join("Game.dat"), - root.join("Data").join("Game.dat"), - root.join("Data").join("BasicData").join("Game.dat"), - ]; - if let Some(d) = data_dir { - candidates.push(d.join("Game.dat")); - candidates.push(d.join("BasicData").join("Game.dat")); - } - candidates.into_iter().find(|p| p.is_file()) -} - -/// The subset of `Game.dat` facts we display. -struct GameFacts { - title: String, - version: String, - utf8: bool, - font: String, -} - -/// Read and decode the display facts from a `Game.dat` file. -fn read_game_facts(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - let gd = GameDat::read(&bytes).map_err(|e| e.to_string())?; - let utf8 = gd.utf8; - let title = decode_wstr(&gd.title, utf8); - let font = decode_wstr(&gd.font, utf8); - - // "version" is a soft concept in Game.dat: surface the encoding plus the title-suffix string - // (string 8, e.g. a "Ver1.02" the author bakes in), which is the closest user-visible version. - let suffix = gd - .title_plus - .as_ref() - .map(|w| decode_wstr(w, utf8)) - .filter(|s| !s.trim().is_empty()); - let version = match suffix { - Some(s) => format!("{} · {}", encoding_label(utf8), s.trim()), - None => encoding_label(utf8).to_string(), - }; - - // Sanity-cross-check against extract_game_dat: it always emits Title first, decoded in-encoding. - // If our direct decode somehow disagrees (it shouldn't), prefer the extractor's value so the UI - // matches the translation tooling exactly. Cheap and keeps the two paths from ever drifting. - let title = title_from_extract(&gd).unwrap_or(title); - - Ok(GameFacts { - title, - version, - utf8, - font, - }) -} - -/// Pull the Title `source` out of `extract_game_dat`'s JSON (the same value the translation -/// pipeline sees), so the GUI's title display can never disagree with the tooling. -fn title_from_extract(gd: &GameDat) -> Option { - let json = extract_game_dat(gd); - // Fixed machine shape: `{"key": "Title", "source": "...", "text": "..."}`. Scan for it without - // pulling in a JSON dep (mirrors wolf-cli's own minimal scan in save-update). - let marker = json.find("\"key\": \"Title\"")?; - let after = &json[marker..]; - let src_at = after.find("\"source\":")?; - let after = &after[src_at + "\"source\":".len()..]; - let open = after.find('"')? + 1; - let rest = &after[open..]; - let mut out = String::new(); - let mut chars = rest.chars(); - while let Some(c) = chars.next() { - match c { - '"' => return Some(out), - '\\' => match chars.next()? { - 'n' => out.push('\n'), - 'r' => out.push('\r'), - 't' => out.push('\t'), - '"' => out.push('"'), - '\\' => out.push('\\'), - '/' => out.push('/'), - 'u' => { - let hex: String = chars.by_ref().take(4).collect(); - let code = u32::from_str_radix(&hex, 16).ok()?; - out.push(char::from_u32(code)?); - } - other => out.push(other), - }, - _ => out.push(c), - } - } - None -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/saves.rs b/util/wolfdawn-master/crates/wolf-gui/src/saves.rs deleted file mode 100644 index 4df6faa..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/saves.rs +++ /dev/null @@ -1,501 +0,0 @@ -//! The Saves section's data model and the headless logic behind it. -//! -//! A `.sav` bakes in the game *title* and player-facing *strings* (item / skill / mode names shown -//! on the save-load screen). After a fan translation those go stale: the translated build rejects a -//! save whose baked title does not match its own `Game.dat` title ("trying to load from another -//! game"), and the baked strings still read in the original language. This section refreshes both, -//! exactly as the CLI's `save-update` does, over either a single `.sav` or a whole save folder. -//! -//! This module owns everything *except* the egui rendering (which lives in `app.rs`), so the whole -//! workflow can be exercised headlessly in tests: -//! -//! * [`inspect`] reads a `.sav` and calls the library's [`inspect_save`] to surface the format, -//! encoding, baked title, and the editable baked-string list, the same records `update_save` -//! can rewrite. -//! * [`save_changes`] turns the user's edits (a possibly-changed title + the rows the user -//! actually changed) into a `new_title` + `source->target` map, optionally backs up the -//! original, then calls the library's [`update_save`] and writes the result. -//! * [`batch_update`] mirrors the CLI's `cmd_save_update`: a new title (typed or read from a -//! translated `Game.dat`) and/or a translation file/dir (turned into a `source->target` map via -//! [`build_memory`], like the CLI), applied with `update_save` over every `*.sav` in a folder. -//! -//! Every codec is handled by `wolf_decompiler` (standard and GamePro Pro). An unsupported buffer is -//! reported and skipped, never written. - -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -use wolf_decompiler::{ - build_memory, extract_game_dat, inspect_save, update_save, SaveFormat, SaveInfo, - SaveUpdateStats, -}; -use wolf_formats::game_dat::GameDat; - -/// One editable baked string: the read-only `original` (used as the `update_save` map key) and the -/// editable `edited` value (starts equal to `original`). -#[derive(Clone)] -pub struct StringRow { - pub original: String, - pub edited: String, -} - -impl StringRow { - /// True when the user actually changed this string (so only real edits go into the map). - pub fn is_changed(&self) -> bool { - self.edited != self.original - } -} - -/// The loaded-and-editable view of a single inspected `.sav`: where it came from, its detected -/// format/encoding, the editable title (seeded from the baked title), and the editable string rows. -pub struct LoadedSave { - /// The on-disk `.sav` this was inspected from (the [`save_changes`] write target). - pub path: PathBuf, - pub format: SaveFormat, - pub encoding: &'static str, - /// The baked title as read (read-only reference, for "changed?" detection and the dim original). - pub original_title: String, - /// The editable title field (starts equal to `original_title`). - pub title: String, - /// The editable baked strings (deduplicated, in first-seen order). - pub strings: Vec, -} - -impl LoadedSave { - /// True when the save's format is one we can edit/write. - pub fn editable(&self) -> bool { - self.format != SaveFormat::Unsupported - } - - /// True when the title field differs from the baked title. - pub fn title_changed(&self) -> bool { - self.title != self.original_title - } - - /// How many string rows the user has changed. - pub fn changed_count(&self) -> usize { - self.strings.iter().filter(|r| r.is_changed()).count() - } - - /// Build the `new_title` + `source->target` map that [`save_changes`] hands to `update_save`, - /// from only what the user actually changed. - fn build_edits(&self) -> (Option, HashMap) { - let new_title = self.title_changed().then(|| self.title.clone()); - let mut map = HashMap::new(); - for r in &self.strings { - if r.is_changed() && !r.original.is_empty() { - // Last write wins if the same original appears twice with different edits. The rows - // are deduplicated on load so that should not happen in practice. - map.insert(r.original.clone(), r.edited.clone()); - } - } - (new_title, map) - } -} - -/// Read and inspect a `.sav`, building the editable [`LoadedSave`]. The baked-string list is -/// deduplicated (keeping first-seen order) so the same item/skill name shows once. Editing it -/// rewrites *every* occurrence on save (that is how `update_save`'s map works). An `Unsupported` -/// save still loads (so the UI can show the not-supported note), just with editing disabled. -pub fn inspect(path: &Path) -> Result { - let raw = std::fs::read(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?; - let info: SaveInfo = inspect_save(&raw)?; - - // Deduplicate the strings, preserving first-seen order, so each unique baked string is one row. - let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new(); - let mut strings = Vec::new(); - for s in &info.strings { - if seen.insert(s.as_str()) { - strings.push(StringRow { - original: s.clone(), - edited: s.clone(), - }); - } - } - - Ok(LoadedSave { - path: path.to_path_buf(), - format: info.format, - encoding: info.encoding, - original_title: info.title.clone(), - title: info.title, - strings, - }) -} - -/// Where a backup goes for a single-file save change. -fn backup_path(save: &Path) -> PathBuf { - let mut name = save.file_name().map(|s| s.to_os_string()).unwrap_or_default(); - name.push(".bak"); - save.with_file_name(name) -} - -/// Apply the user's edits (title + changed strings) to the loaded save and write it back, optionally -/// backing up the original to `*.bak` first. Returns the [`SaveUpdateStats`] from `update_save`. -/// Refuses to write an unsupported save. Mirrors the single-file path of the CLI's `cmd_save_update`. -pub fn save_changes(loaded: &LoadedSave, backup: bool) -> Result { - if !loaded.editable() { - return Err("this save's format is not supported; nothing was written".to_string()); - } - let (new_title, map) = loaded.build_edits(); - if new_title.is_none() && map.is_empty() { - return Err("no changes to save (edit the title or a baked string first)".to_string()); - } - - let raw = std::fs::read(&loaded.path) - .map_err(|e| format!("cannot read {}: {e}", loaded.path.display()))?; - let (out, stats) = update_save(&raw, new_title.as_deref(), &map)?; - - if backup { - let bak = backup_path(&loaded.path); - std::fs::copy(&loaded.path, &bak) - .map_err(|e| format!("backup failed ({}): {e}", bak.display()))?; - } - std::fs::write(&loaded.path, &out) - .map_err(|e| format!("write failed ({}): {e}", loaded.path.display()))?; - Ok(stats) -} - -/// Read the baked title out of a (translated) `Game.dat`, the same way the CLI's `--game` does: -/// parse it and pull the Title `source` from `extract_game_dat`'s fixed JSON shape. -pub fn title_from_game_dat(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?; - let gd = GameDat::read(&bytes).map_err(|e| format!("Game.dat parse failed: {e}"))?; - let json = extract_game_dat(&gd); - let value: serde_json::Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - value - .get("lines") - .and_then(serde_json::Value::as_array) - .into_iter() - .flatten() - .find(|l| l.get("key").and_then(serde_json::Value::as_str) == Some("Title")) - .and_then(|l| l.get("source").and_then(serde_json::Value::as_str)) - .map(str::to_owned) - .ok_or_else(|| format!("{} has no Title entry", path.display())) -} - -/// Collect every `*.json` under each path (recursively for dirs, the file itself if it is `.json`). -fn collect_json_files(paths: &[PathBuf]) -> Vec { - fn walk(path: &Path, out: &mut Vec) { - if path.is_dir() { - if let Ok(rd) = std::fs::read_dir(path) { - for e in rd.flatten() { - walk(&e.path(), out); - } - } - } else if path.extension().and_then(|s| s.to_str()) == Some("json") { - out.push(path.to_path_buf()); - } - } - let mut out = Vec::new(); - for p in paths { - walk(p, &mut out); - } - out.sort(); - out.dedup(); - out -} - -/// Build the `source->target` baked-string map from translation JSON file(s)/dir(s), reusing the -/// library's [`build_memory`] exactly as the CLI's `build_save_string_map` does. Returns the map and -/// the count of sources that had divergent translations across files (the first value is kept). -pub fn build_string_map(paths: &[PathBuf]) -> Result<(HashMap, usize), String> { - let files = collect_json_files(paths); - if files.is_empty() { - return Err("no *.json found under the chosen translation path(s)".to_string()); - } - let mut texts: Vec = Vec::with_capacity(files.len()); - for p in &files { - texts.push(std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display()))?); - } - let refs: Vec<&str> = texts.iter().map(String::as_str).collect(); - let (memory, conflicts) = build_memory(&refs); - Ok((memory, conflicts.len())) -} - -/// One save's outcome in a [`batch_update`] run, for the log. -pub enum BatchEntry { - /// Updated: `(file name, stats)`. - Updated(String, SaveUpdateStats), - /// Skipped because its format is unsupported: `file name`. - Skipped(String), - /// An I/O error on this file: `(file name, message)`. - Failed(String, String), -} - -/// The summary of a whole batch run. -pub struct BatchOutcome { - pub entries: Vec, - pub backup_dir: Option, -} - -impl BatchOutcome { - pub fn updated(&self) -> usize { - self.entries.iter().filter(|e| matches!(e, BatchEntry::Updated(..))).count() - } - pub fn skipped(&self) -> usize { - self.entries.iter().filter(|e| matches!(e, BatchEntry::Skipped(..))).count() - } - pub fn failed(&self) -> usize { - self.entries.iter().filter(|e| matches!(e, BatchEntry::Failed(..))).count() - } -} - -/// List every `*.sav` directly under `dir`, sorted (mirrors the CLI's discovery). -fn list_saves(dir: &Path) -> Vec { - let mut v: Vec = std::fs::read_dir(dir) - .into_iter() - .flatten() - .flatten() - .map(|e| e.path()) - .filter(|p| p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("sav")) - .collect(); - v.sort(); - v -} - -/// A short timestamp (`YYYYmmdd_HHMMSS`) for a backup dir name, like the CLI's `backup_stamp`. -pub fn backup_stamp() -> String { - let secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - let days = secs / 86_400; - let tod = secs % 86_400; - let (hh, mm, ss) = (tod / 3600, (tod % 3600) / 60, tod % 60); - // Civil-from-days (Howard Hinnant's algorithm). - let z = days as i64 + 719_468; - let era = if z >= 0 { z } else { z - 146_096 } / 146_097; - let doe = z - era * 146_097; - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let y = yoe + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; - let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let year = if m <= 2 { y + 1 } else { y }; - format!("{year:04}{m:02}{d:02}_{hh:02}{mm:02}{ss:02}") -} - -/// Apply a `new_title` and/or a `source->target` string `map` over every `*.sav` in `dir`, in place, -/// mirroring the CLI's `cmd_save_update`. When `backup` is set, the originals are copied into a -/// `save_backup_/` under `dir` first. Each file's outcome (updated / skipped-unsupported / -/// failed) is recorded for the log. `report` drives a progress bar. Pass a no-op in tests. -/// -/// Errors only when there is nothing to do (no title and an empty map) or no `*.sav` is found. -pub fn batch_update( - dir: &Path, - new_title: Option<&str>, - map: &HashMap, - backup: bool, - mut report: impl FnMut(f32, String), -) -> Result { - if new_title.is_none() && map.is_empty() { - return Err("nothing to do (give a new title and/or a translations file)".to_string()); - } - let saves = list_saves(dir); - if saves.is_empty() { - return Err(format!("no *.sav files found under {}", dir.display())); - } - - // Back up the originals into a stamped dir before overwriting (mirrors the CLI). - let backup_dir = if backup { - let root = dir.join(format!("save_backup_{}", backup_stamp())); - std::fs::create_dir_all(&root) - .map_err(|e| format!("cannot create backup dir {}: {e}", root.display()))?; - for src in &saves { - let name = src.file_name().unwrap_or_default(); - std::fs::copy(src, root.join(name)) - .map_err(|e| format!("backup failed for {}: {e}", src.display()))?; - } - Some(root) - } else { - None - }; - - let total = saves.len(); - let mut entries = Vec::with_capacity(total); - for (i, src) in saves.iter().enumerate() { - let name = src - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("?") - .to_string(); - report((i as f32) / (total as f32), format!("update {name}")); - let raw = match std::fs::read(src) { - Ok(b) => b, - Err(e) => { - entries.push(BatchEntry::Failed(name, format!("read failed: {e}"))); - continue; - } - }; - match update_save(&raw, new_title, map) { - Ok((out, stats)) => match std::fs::write(src, &out) { - Ok(()) => entries.push(BatchEntry::Updated(name, stats)), - Err(e) => entries.push(BatchEntry::Failed(name, format!("write failed: {e}"))), - }, - // Err means the buffer is neither a standard nor a detectable Pro save: skip, never write. - Err(_) => entries.push(BatchEntry::Skipped(name)), - } - } - report(1.0, "done".to_string()); - Ok(BatchOutcome { entries, backup_dir }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// The two real save fixtures. Tests skip gracefully when neither is present. - fn fixtures() -> Vec { - ["chamber/SaveData01.sav", "pachimon/SaveData01.sav"] - .into_iter() - .filter_map(test_data) - .collect() - } - - fn tmp_dir(tag: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!( - "wolfdawn_saves_{tag}_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::create_dir_all(&p); - p - } - - /// Inspect a real save (on a COPY), change the title + one baked string through this module's - /// own code path, write it, re-inspect from the written bytes, and assert the title + string - /// changed and the format is preserved. The game folders are never touched. - #[test] - fn inspect_edit_save_reinspect_round_trip() { - let fixtures = fixtures(); - if fixtures.is_empty() { - eprintln!("skip inspect_edit_save_reinspect_round_trip: no save fixture present"); - return; - } - for fixture in fixtures { - let work = tmp_dir("rt"); - let copy = work.join("SaveData01.sav"); - std::fs::copy(&fixture, ©).expect("copy fixture"); - - let mut loaded = inspect(©).expect("inspect"); - assert!(loaded.editable(), "{fixture:?}: real save should be editable"); - assert!(!loaded.original_title.is_empty(), "title should be non-empty"); - let original_format = loaded.format; - - // Change the title. - let new_title = format!("{} [GUI]", loaded.original_title); - loaded.title = new_title.clone(); - - // Change the first baked string that can re-encode in this save's encoding. - let utf8 = loaded.encoding == "utf8"; - let mut edited_marker: Option = None; - for r in &mut loaded.strings { - if r.original.is_empty() { - continue; - } - let candidate = format!("{} X", r.original); - // Shift-JIS saves: only edit a string whose new value is representable. - let representable = utf8 || { - let (_, _, had_err) = encoding_rs::SHIFT_JIS.encode(&candidate); - !had_err - }; - if representable { - r.edited = candidate.clone(); - edited_marker = Some(candidate); - break; - } - } - - let stats = save_changes(&loaded, true).expect("save changes"); - assert!(stats.title_changed); - assert_eq!(stats.encoding, loaded.encoding); - // The backup must exist and still hold the original bytes. - let bak = copy.with_file_name("SaveData01.sav.bak"); - assert!(bak.exists(), "backup should have been written"); - assert_eq!( - std::fs::read(&bak).unwrap(), - std::fs::read(&fixture).unwrap(), - "backup should equal the original fixture" - ); - - // Re-inspect from the written file: title + string changed, format preserved. - let re = inspect(©).expect("re-inspect"); - assert_eq!(re.format, original_format, "format must be preserved"); - assert_eq!(re.title, new_title, "new title should re-inspect"); - if let Some(marker) = edited_marker { - assert!( - re.strings.iter().any(|r| r.original == marker), - "the edited baked string should re-inspect" - ); - } - - let _ = std::fs::remove_dir_all(&work); - } - } - - /// Batch update over a folder of save copies: applies a new title and reports per-file outcomes, - /// with the originals backed up to a stamped dir first. - #[test] - fn batch_update_folder_titles() { - let fixtures = fixtures(); - if fixtures.is_empty() { - eprintln!("skip batch_update_folder_titles: no save fixture present"); - return; - } - let fixture = &fixtures[0]; - let work = tmp_dir("batch"); - // Two copies so the batch has more than one file to walk. - for n in ["SaveData01.sav", "SaveData02.sav"] { - std::fs::copy(fixture, work.join(n)).expect("copy"); - } - - let new_title = "Batched Title"; - let outcome = - batch_update(&work, Some(new_title), &HashMap::new(), true, |_, _| {}).expect("batch"); - assert!(outcome.updated() >= 1, "at least one save should update"); - assert_eq!(outcome.failed(), 0, "no I/O failures expected"); - assert!(outcome.backup_dir.is_some(), "a backup dir should have been made"); - let backup_dir = outcome.backup_dir.unwrap(); - assert!(backup_dir.join("SaveData01.sav").exists(), "original backed up"); - - // Every updated file re-inspects to the new title. - for n in ["SaveData01.sav", "SaveData02.sav"] { - let re = inspect(&work.join(n)).expect("re-inspect"); - if re.editable() { - assert_eq!(re.title, new_title); - } - } - - let _ = std::fs::remove_dir_all(&work); - } - - /// `save_changes` refuses a no-op (nothing edited) and never writes. - #[test] - fn save_changes_rejects_no_op() { - let fixtures = fixtures(); - if fixtures.is_empty() { - eprintln!("skip save_changes_rejects_no_op: no save fixture present"); - return; - } - let work = tmp_dir("noop"); - let copy = work.join("SaveData01.sav"); - std::fs::copy(&fixtures[0], ©).expect("copy"); - let loaded = inspect(©).expect("inspect"); - let err = save_changes(&loaded, true).unwrap_err(); - assert!(err.contains("no changes"), "got: {err}"); - let _ = std::fs::remove_dir_all(&work); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/task.rs b/util/wolfdawn-master/crates/wolf-gui/src/task.rs deleted file mode 100644 index f35532e..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/task.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Background jobs. The shared pattern every long-running section uses. -//! -//! Long operations (unpack, extract, inject, save-update...) must never run on the UI thread -//! or the window freezes. The pattern here: -//! -//! 1. A section calls [`JobManager::run`] with a label and a closure that does the work. The -//! closure receives a [`Reporter`] it pushes progress + log messages through. -//! 2. The closure runs on a fresh `std::thread`. Each [`Reporter::progress`] / `Reporter::log` -//! call sends a [`JobMsg`] down an `mpsc` channel. The final `Result` is sent as -//! [`JobMsg::Done`]. -//! 3. Every frame, [`JobManager::poll`] drains the channel: progress updates the bar, log -//! messages append to the [`Log`], and `Done` clears the active job. While a job is live the -//! app keeps calling `ctx.request_repaint()` so the UI animates even with no user input. -//! -//! Sections copy this shape verbatim: build the inputs on the UI thread (cheap), then hand the -//! heavy work to `run(...)` so the result and progress flow back through the channel. - -// `Reporter::warn`/`error` round out the reporting API for the heavier jobs that later sections -// add. The Phase 0 demo job only needs `info`/`progress`, so allow the unused-yet variants here. -#![allow(dead_code)] - -use std::sync::mpsc::{Receiver, Sender}; - -use crate::log::{Level, Log}; - -/// A message from a running job back to the UI thread. -pub enum JobMsg { - /// Fractional progress in `0.0..=1.0` plus a short status line. - Progress(f32, String), - /// A line to append to the log at the given level. - Log(Level, String), - /// The job finished. `Ok` carries a human summary. `Err` carries the failure message. - Done(Result), -} - -/// Handed to a job closure so it can report progress and log lines back to the UI. Cloneable so a -/// job can pass it into sub-steps. Sends are best-effort: if the UI side has hung up (app closing) -/// the calls become no-ops rather than panicking. -#[derive(Clone)] -pub struct Reporter { - tx: Sender, -} - -impl Reporter { - /// Report fractional progress (`0.0..=1.0`) and a status line shown next to the bar. - pub fn progress(&self, fraction: f32, status: impl Into) { - let _ = self.tx.send(JobMsg::Progress(fraction.clamp(0.0, 1.0), status.into())); - } - - /// Append an info line to the log. - pub fn info(&self, msg: impl Into) { - let _ = self.tx.send(JobMsg::Log(Level::Info, msg.into())); - } - - /// Append a warning line to the log. - pub fn warn(&self, msg: impl Into) { - let _ = self.tx.send(JobMsg::Log(Level::Warn, msg.into())); - } - - /// Append an error line to the log. - pub fn error(&self, msg: impl Into) { - let _ = self.tx.send(JobMsg::Log(Level::Error, msg.into())); - } -} - -/// The single in-flight background job (if any) plus its latest progress, owned by the app. -/// -/// At most one job runs at a time. The section UIs disable their action buttons via -/// [`JobManager::is_running`] while one is live, so the worker never contends with itself. -#[derive(Default)] -pub struct JobManager { - /// Receiver for the active job's messages. `None` when idle. - rx: Option>, - /// Label of the active job (shown by the progress bar). - label: String, - /// Latest fractional progress in `0.0..=1.0`. - progress: f32, - /// Latest status line from the job. - status: String, -} - -impl JobManager { - pub fn new() -> Self { - Self::default() - } - - /// True while a background job is in flight. - pub fn is_running(&self) -> bool { - self.rx.is_some() - } - - pub fn label(&self) -> &str { - &self.label - } - - pub fn progress(&self) -> f32 { - self.progress - } - - pub fn status(&self) -> &str { - &self.status - } - - /// Spawn `work` on a worker thread, routing its progress/log/result back to this manager. - /// - /// `work` returns `Ok(summary)` on success or `Err(message)` on failure. Both are surfaced in - /// the log when the job completes. If a job is already running the call is ignored (the caller - /// should gate its button on [`is_running`](Self::is_running)). - pub fn run(&mut self, label: impl Into, log: &mut Log, work: F) - where - F: FnOnce(&Reporter) -> Result + Send + 'static, - { - if self.is_running() { - log.warn("a job is already running; please wait for it to finish"); - return; - } - let label = label.into(); - let (tx, rx) = std::sync::mpsc::channel(); - self.rx = Some(rx); - self.label = label.clone(); - self.progress = 0.0; - self.status = "starting…".to_string(); - log.info(format!("▶ {label}")); - - // The worker owns its Reporter (and thus the Sender). When the thread ends the Sender drops - // and the channel closes, which is how `poll` detects completion even if `Done` is missed. - std::thread::spawn(move || { - let reporter = Reporter { tx }; - let result = work(&reporter); - let _ = reporter.tx.send(JobMsg::Done(result)); - }); - } - - /// Drain all pending messages from the active job into the log + progress fields. Returns - /// `true` while a job is still running (the caller then requests a repaint so the bar animates). - pub fn poll(&mut self, log: &mut Log) -> bool { - let Some(rx) = &self.rx else { - return false; - }; - // Drain everything available this frame so the bar/log stay current under a chatty job. - let mut finished = false; - loop { - match rx.try_recv() { - Ok(JobMsg::Progress(frac, status)) => { - self.progress = frac; - self.status = status; - } - Ok(JobMsg::Log(level, msg)) => log.push(level, msg), - Ok(JobMsg::Done(result)) => { - match result { - Ok(summary) => log.info(format!("done — {} — {summary}", self.label)), - Err(err) => log.error(format!("failed — {} — {err}", self.label)), - } - finished = true; - } - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - // Worker thread ended without an explicit Done (panic / early drop). Treat the - // channel close as completion so the UI never gets stuck "running" forever. - finished = true; - break; - } - } - } - if finished { - self.rx = None; - self.progress = 0.0; - self.status.clear(); - self.label.clear(); - return false; - } - true - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/translation.rs b/util/wolfdawn-master/crates/wolf-gui/src/translation.rs deleted file mode 100644 index 56d3626..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/translation.rs +++ /dev/null @@ -1,1248 +0,0 @@ -//! The Translation section's data model and the headless logic behind it. -//! -//! A translator opens a game's unpacked `Data/`, gets an editable source<->translation grid, edits -//! the translations, then injects them back into byte-exact game files. This module owns everything -//! *except* the egui rendering (which lives in `app.rs`), so the whole workflow can be exercised in -//! tests without a window: -//! -//! * [`extract_model`] walks a data dir, calls the **same** `wolf_decompiler` extract functions the -//! CLI uses, and parses the per-file JSON they emit into a [`TranslationModel`] of editable -//! [`Row`]s. Each file keeps its *original parsed JSON* (`doc`) so injection is byte-for-byte -//! lossless: a row edit only rewrites a single `text` leaf inside that `Value`, never rebuilds -//! the shape (exactly how the library's own merge does it). -//! * [`inject_model`] serializes each file's (edited) JSON back and calls the matching -//! `inject_*` / `inject_names` library path with [`InjectOptions`] from the UI checkboxes. -//! * [`merge_into_model`] / [`check_conflicts`] / save+load round out the workflow. -//! -//! The DB `.project` is the canonical handle for a database. Injection writes the `.project`+`.dat` -//! pair together, mirroring the CLI's `strings_inject_db` / `names-inject`. - -use std::path::{Path, PathBuf}; - -use serde_json::Value; -use wolf_decompiler::{ - apply_memory, build_memory, check_name_conflicts, db_strings_to_json, extract_common_events, - extract_db_strings, extract_game_dat, extract_map, extract_names, extract_txt_events, - inject_common_events, inject_db_strings, inject_game_dat, inject_map, inject_names, - inject_txt_events, scenes_to_json, InjectOptions, -}; - -// Re-export the library's conflict type so the section UI can name it without reaching past this -// module (it owns the translation surface). -pub use wolf_decompiler::Conflict; -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::Database; -use wolf_formats::game_dat::GameDat; -use wolf_formats::map::Map; - -/// What kind of file a [`FileEntry`] came from. Decides which extract/inject library path applies -/// and (for the DB) that the `.project`+`.dat` pair must be written together. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum FileKind { - /// A `.mps` map (`scenes` JSON). - Map, - /// `CommonEvent.dat` (`scenes` JSON). - CommonEvent, - /// A database `.project` (+ sibling `.dat`); `db` JSON. - Database, - /// `Game.dat` (`gamedat` JSON). - GameDat, - /// An external event-text `.txt` scene file (`txt` JSON). Some GamePro games keep their - /// dialogue in Shift-JIS `.txt` files (the unpacked `Evtext*.wolf`), one scene per file. - TxtEvent, -} - -impl FileKind { - /// A short label for the file list. - pub fn tag(self) -> &'static str { - match self { - FileKind::Map => "map", - FileKind::CommonEvent => "common", - FileKind::Database => "db", - FileKind::GameDat => "gamedat", - FileKind::TxtEvent => "txt", - } - } -} - -/// One editable translation row: the read-only `source`, the editable `translation`, plus a -/// human-readable `location`/`note` and the JSON-pointer (`ptr`) of this row's `text` leaf inside -/// its owning [`FileEntry::doc`]. The pointer is what makes injection lossless: an edit rewrites -/// exactly that one leaf, leaving every other byte of the document untouched. -#[derive(Clone)] -pub struct Row { - pub source: String, - pub translation: String, - /// Where this string lives (e.g. `ev42/p0 · ▼Speaker`, `Type 3 row 7 · Name`). - pub location: String, - /// A small dim note (occurrence count for names, field hint for DB, etc.). - pub note: String, - /// JSON Pointer (RFC 6901) to the `text` value inside the owning document. - ptr: String, -} - -impl Row { - /// True when the translator has actually changed the text from the source. - pub fn is_translated(&self) -> bool { - self.translation != self.source && !self.translation.is_empty() - } -} - -/// One translatable file's rows plus the parsed JSON document they index into. `path` is the -/// on-disk file the inject writes back to (the `.project` for a database). -pub struct FileEntry { - pub path: PathBuf, - pub kind: FileKind, - pub rows: Vec, - /// The original extract JSON, parsed. Rows mutate `text` leaves here. Inject serializes it back. - doc: Value, -} - -impl FileEntry { - /// A short display name for the file list (the file name, or a DB-friendly stem). - pub fn display_name(&self) -> String { - self.path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("?") - .to_string() - } - - /// Translated-row count, for the per-file and overall counters. - pub fn translated_count(&self) -> usize { - self.rows.iter().filter(|r| r.is_translated()).count() - } -} - -/// The whole editable project: every translatable file plus the project-level name glossary. -#[derive(Default)] -pub struct TranslationModel { - /// The data dir this was extracted from (so inject/merge default sensibly). - pub data_dir: PathBuf, - pub files: Vec, - /// The glossary rows (the "Names" entry), kept beside its own parsed `names.json` document. - pub names: Vec, - names_doc: Value, -} - -impl TranslationModel { - /// Total source lines across files + names. - pub fn total_rows(&self) -> usize { - self.files.iter().map(|f| f.rows.len()).sum::() + self.names.len() - } - - /// Total translated lines across files + names. - pub fn translated_rows(&self) -> usize { - self.files.iter().map(FileEntry::translated_count).sum::() - + self.names.iter().filter(|r| r.is_translated()).count() - } - - /// Push the editable `translation` values back into the parsed documents, so the documents are - /// current before serialization (inject/save). Called by [`inject_model`] and `save_to_path`. - fn sync_docs(&mut self) { - for f in &mut self.files { - apply_rows_to_doc(&mut f.doc, &f.rows); - } - apply_rows_to_doc(&mut self.names_doc, &self.names); - } -} - -/// Write each row's `translation` into the `text` leaf its pointer addresses. An untranslated row -/// (empty `translation`) writes its `source` back, so the doc's text equals the source (the inject -/// then treats it as a no-op) and clearing a translation correctly resets the cell to its source. -fn apply_rows_to_doc(doc: &mut Value, rows: &[Row]) { - for r in rows { - if let Some(slot) = doc.pointer_mut(&r.ptr) { - let text = if r.is_translated() { &r.translation } else { &r.source }; - *slot = Value::String(text.clone()); - } - } -} - -// ---------------------------------------------------------------------------- -// Extraction: walk the data dir, call the library extract fns, parse to rows -// ---------------------------------------------------------------------------- - -/// Resolve which of `dir` / `dir/BasicData` holds `CommonEvent.dat` + the DB pairs (mirrors the -/// CLI's `basic_data_dir`). -fn basic_data_dir(dir: &Path) -> PathBuf { - if dir.join("CommonEvent.dat").exists() || dir.join("DataBase.project").exists() { - dir.to_path_buf() - } else if dir.join("BasicData").join("CommonEvent.dat").exists() - || dir.join("BasicData").join("DataBase.project").exists() - { - dir.join("BasicData") - } else { - dir.to_path_buf() - } -} - -/// Every translatable file under a data dir, in a stable order: maps, then `CommonEvent.dat`, the -/// three DB pairs, and `Game.dat` (mirrors how the CLI's `discover_data_dir` + `cmd_*` locate them). -fn discover_files(dir: &Path) -> (Vec, Vec<(FileKind, PathBuf)>) { - let basic = basic_data_dir(dir); - - // Maps live in MapData (or directly under dir for a flattened layout). - let mut maps: Vec = Vec::new(); - for sub in [dir.to_path_buf(), dir.join("MapData")] { - let Ok(rd) = std::fs::read_dir(&sub) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.extension().and_then(|s| s.to_str()) == Some("mps") { - maps.push(p); - } - } - } - maps.sort(); - maps.dedup(); - - let mut others: Vec<(FileKind, PathBuf)> = Vec::new(); - let ce = basic.join("CommonEvent.dat"); - if ce.exists() { - others.push((FileKind::CommonEvent, ce)); - } - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - let proj = basic.join(format!("{stem}.project")); - if proj.exists() && proj.with_extension("dat").exists() { - others.push((FileKind::Database, proj)); - } - } - let game_dat = basic.join("Game.dat"); - if game_dat.exists() { - others.push((FileKind::GameDat, game_dat)); - } - (maps, others) -} - -/// Every external event-text `.txt` file anywhere under `dir`, sorted by path. These are the -/// unpacked `Evtext*.wolf` scene files. The walk is bounded by a depth limit so a deep tree cannot -/// run away, and it only collects `.txt` (the sole `.txt` use in a Wolf game), so it skips the -/// image/audio archives cheaply. -fn discover_txt_files(dir: &Path) -> Vec { - fn walk(dir: &Path, depth: usize, out: &mut Vec) { - if depth > 8 { - return; - } - let Ok(rd) = std::fs::read_dir(dir) else { - return; - }; - for e in rd.flatten() { - let p = e.path(); - if p.is_dir() { - walk(&p, depth + 1, out); - } else if p.extension().and_then(|s| s.to_str()).map(|x| x.eq_ignore_ascii_case("txt")) == Some(true) { - out.push(p); - } - } - } - let mut out = Vec::new(); - walk(dir, 0, &mut out); - out.sort(); - out -} - -/// Extract the whole data dir into a [`TranslationModel`]. `report` receives progress callbacks -/// (`fraction`, `message`) so a background job can drive the progress bar. Pass a no-op closure in -/// tests. Errors only on a truly empty dir. An unreadable single file is logged-and-skipped via the -/// returned `warnings` list. -pub struct ExtractOutcome { - pub model: TranslationModel, - pub warnings: Vec, -} - -pub fn extract_model( - dir: &Path, - mut report: impl FnMut(f32, String), -) -> Result { - let (maps, others) = discover_files(dir); - // External event-text .txt files (unpacked Evtext*). Found anywhere under the dir, so a game - // that keeps its dialogue in .txt (some GamePro titles) gets that text too, not just the - // command stream. Most games have none, so this adds nothing for them. - let txts = discover_txt_files(dir); - let total = maps.len() + others.len() + txts.len() + 1; // +1 for the names glossary pass - if total <= 1 { - return Err(format!( - "no translatable files found under {} (looked for *.mps, CommonEvent.dat, the DB pairs, Game.dat, event .txt)", - dir.display() - )); - } - let mut done = 0usize; - let mut warnings = Vec::new(); - let mut files: Vec = Vec::new(); - - let step = |report: &mut dyn FnMut(f32, String), done: &mut usize, msg: String| { - *done += 1; - report(*done as f32 / total as f32, msg); - }; - - // Maps. - for p in &maps { - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("?").to_string(); - step(&mut report, &mut done, format!("map {name}")); - match extract_map_file(p) { - Ok(entry) => files.push(entry), - Err(e) => warnings.push(format!("skip {name}: {e}")), - } - } - // CommonEvent / DB / Game.dat. - for (kind, p) in &others { - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("?").to_string(); - step(&mut report, &mut done, format!("{} {name}", kind.tag())); - let r = match kind { - FileKind::CommonEvent => extract_common_file(p), - FileKind::Database => extract_db_file(p), - FileKind::GameDat => extract_gamedat_file(p), - FileKind::Map => unreachable!("maps handled above"), - FileKind::TxtEvent => unreachable!("txt handled in its own loop"), - }; - match r { - Ok(entry) => files.push(entry), - Err(e) => warnings.push(format!("skip {name}: {e}")), - } - } - // External event-text .txt scene files, one entry each (so the per-file grid stays responsive - // even when a game ships thousands of them). - for p in &txts { - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("?").to_string(); - step(&mut report, &mut done, format!("txt {name}")); - match extract_txt_file(p) { - Ok(entry) => files.push(entry), - Err(e) => warnings.push(format!("skip {name}: {e}")), - } - } - - // Names glossary (whole-dir). - step(&mut report, &mut done, "names glossary".to_string()); - let (names, names_doc) = match extract_names_doc(dir) { - Ok(v) => v, - Err(e) => { - warnings.push(format!("names glossary: {e}")); - (Vec::new(), Value::Null) - } - }; - - Ok(ExtractOutcome { - model: TranslationModel { - data_dir: dir.to_path_buf(), - files, - names, - names_doc, - }, - warnings, - }) -} - -fn extract_map_file(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - let map = Map::read(&bytes).map_err(|e| e.to_string())?; - let fname = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let json = scenes_to_json(fname, "map", &extract_map(&map)); - let doc: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - let rows = rows_from_scenes(&doc); - Ok(FileEntry { path: path.to_path_buf(), kind: FileKind::Map, rows, doc }) -} - -fn extract_common_file(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - let ce = CommonEventsFile::read(&bytes).map_err(|e| e.to_string())?; - let fname = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let json = scenes_to_json(fname, "common", &extract_common_events(&ce)); - let doc: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - let rows = rows_from_scenes(&doc); - Ok(FileEntry { path: path.to_path_buf(), kind: FileKind::CommonEvent, rows, doc }) -} - -fn extract_db_file(proj: &Path) -> Result { - let dat = proj.with_extension("dat"); - let (p, d) = ( - std::fs::read(proj).map_err(|e| e.to_string())?, - std::fs::read(&dat).map_err(|e| e.to_string())?, - ); - let db = Database::read(&p, &d).map_err(|e| e.to_string())?; - let fname = proj.file_name().and_then(|s| s.to_str()).unwrap_or(""); - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = db_strings_to_json(fname, &extract_db_strings(&db, &glossary)); - let doc: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - let rows = rows_from_db(&doc); - Ok(FileEntry { path: proj.to_path_buf(), kind: FileKind::Database, rows, doc }) -} - -fn extract_gamedat_file(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - let gd = GameDat::read(&bytes).map_err(|e| e.to_string())?; - let json = extract_game_dat(&gd); - let doc: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - let rows = rows_from_gamedat(&doc); - Ok(FileEntry { path: path.to_path_buf(), kind: FileKind::GameDat, rows, doc }) -} - -fn extract_txt_file(path: &Path) -> Result { - let bytes = std::fs::read(path).map_err(|e| e.to_string())?; - let json = extract_txt_events(&bytes); - let doc: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - let rows = rows_from_txt(&doc); - Ok(FileEntry { path: path.to_path_buf(), kind: FileKind::TxtEvent, rows, doc }) -} - -/// Extract the project-level name glossary, returning its rows + parsed `names.json`. -fn extract_names_doc(dir: &Path) -> Result<(Vec, Value), String> { - let basic = basic_data_dir(dir); - let mut dbs = Vec::new(); - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - let proj = basic.join(format!("{stem}.project")); - let dat = basic.join(format!("{stem}.dat")); - if let (Ok(p), Ok(d)) = (std::fs::read(&proj), std::fs::read(&dat)) { - if let Ok(db) = Database::read(&p, &d) { - dbs.push(db); - } - } - } - if dbs.is_empty() { - return Err("no databases found for the name glossary".into()); - } - let ce = std::fs::read(basic.join("CommonEvent.dat")) - .ok() - .and_then(|b| CommonEventsFile::read(&b).ok()); - let mut maps = Vec::new(); - for sub in [dir.to_path_buf(), dir.join("MapData")] { - let Ok(rd) = std::fs::read_dir(&sub) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.extension().and_then(|s| s.to_str()) == Some("mps") { - if let Ok(b) = std::fs::read(&p) { - if let Ok(m) = Map::read(&b) { - maps.push(m); - } - } - } - } - } - let ce_refs: Vec<&CommonEventsFile> = ce.iter().collect(); - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let json = extract_names(&dbs, &ce_refs, &maps, &glossary); - let doc: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - let rows = rows_from_names(&doc); - Ok((rows, doc)) -} - -// --- JSON document -> editable rows (each row carries the pointer to its `text`) -------------- - -fn rows_from_scenes(doc: &Value) -> Vec { - let mut rows = Vec::new(); - let Some(scenes) = doc.get("scenes").and_then(Value::as_array) else { - return rows; - }; - for (si, sc) in scenes.iter().enumerate() { - let event = sc.get("event").and_then(Value::as_i64).unwrap_or(0); - let page = sc.get("page").and_then(Value::as_i64); - let name = sc.get("name").and_then(Value::as_str).unwrap_or(""); - let loc_prefix = match page { - Some(p) => format!("ev{event}/p{p}"), - None => format!("ev{event}"), - }; - let Some(lines) = sc.get("lines").and_then(Value::as_array) else { - continue; - }; - for (li, ln) in lines.iter().enumerate() { - let source = ln.get("source").and_then(Value::as_str).unwrap_or("").to_string(); - let t = ln.get("text").and_then(Value::as_str).unwrap_or(&source).to_string(); - // Untranslated rows hold an empty translation (no copy of the source), which both saves - // memory on big files and gives translators a clean target box. is_translated/apply treat - // empty as "same as source". - let text = if t == source { String::new() } else { t }; - let speaker = ln.get("speaker").and_then(Value::as_str).unwrap_or(""); - let mut note = String::new(); - if !speaker.is_empty() { - note = speaker.to_string(); - } else if !name.is_empty() { - note = name.to_string(); - } - rows.push(Row { - source, - translation: text, - location: loc_prefix.clone(), - note, - ptr: format!("/scenes/{si}/lines/{li}/text"), - }); - } - } - rows -} - -fn rows_from_db(doc: &Value) -> Vec { - let mut rows = Vec::new(); - let Some(groups) = doc.get("groups").and_then(Value::as_array) else { - return rows; - }; - for (gi, g) in groups.iter().enumerate() { - let type_name = g.get("typeName").and_then(Value::as_str).unwrap_or(""); - let Some(lines) = g.get("lines").and_then(Value::as_array) else { - continue; - }; - for (li, ln) in lines.iter().enumerate() { - let source = ln.get("source").and_then(Value::as_str).unwrap_or("").to_string(); - let t = ln.get("text").and_then(Value::as_str).unwrap_or(&source).to_string(); - // Untranslated rows hold an empty translation (no copy of the source), which both saves - // memory on big files and gives translators a clean target box. is_translated/apply treat - // empty as "same as source". - let text = if t == source { String::new() } else { t }; - let row_name = ln.get("rowName").and_then(Value::as_str).unwrap_or(""); - let field = ln.get("fieldName").and_then(Value::as_str).unwrap_or(""); - rows.push(Row { - source, - translation: text, - location: format!("{type_name} · {row_name}"), - note: field.to_string(), - ptr: format!("/groups/{gi}/lines/{li}/text"), - }); - } - } - rows -} - -fn rows_from_gamedat(doc: &Value) -> Vec { - let mut rows = Vec::new(); - let Some(lines) = doc.get("lines").and_then(Value::as_array) else { - return rows; - }; - for (li, ln) in lines.iter().enumerate() { - let source = ln.get("source").and_then(Value::as_str).unwrap_or("").to_string(); - let t = ln.get("text").and_then(Value::as_str).unwrap_or(&source).to_string(); - let text = if t == source { String::new() } else { t }; - let key = ln.get("key").and_then(Value::as_str).unwrap_or(""); - rows.push(Row { - source, - translation: text, - location: key.to_string(), - note: String::new(), - ptr: format!("/lines/{li}/text"), - }); - } - rows -} - -/// Rows for an event-text `.txt` document (`{ lines: [ {i, source, text} ] }`). Each translatable -/// line becomes a row pointing at its `text` leaf. The `i` (the physical line index in the file) -/// is shown as the location so a translator can find it in the original. -fn rows_from_txt(doc: &Value) -> Vec { - let mut rows = Vec::new(); - let Some(lines) = doc.get("lines").and_then(Value::as_array) else { - return rows; - }; - for (li, ln) in lines.iter().enumerate() { - let source = ln.get("source").and_then(Value::as_str).unwrap_or("").to_string(); - let t = ln.get("text").and_then(Value::as_str).unwrap_or(&source).to_string(); - let text = if t == source { String::new() } else { t }; - let i = ln.get("i").and_then(Value::as_i64).unwrap_or(li as i64); - rows.push(Row { - source, - translation: text, - location: format!("line {i}"), - note: String::new(), - ptr: format!("/lines/{li}/text"), - }); - } - rows -} - -fn rows_from_names(doc: &Value) -> Vec { - let mut rows = Vec::new(); - let Some(names) = doc.get("names").and_then(Value::as_array) else { - return rows; - }; - for (i, e) in names.iter().enumerate() { - let source = e.get("source").and_then(Value::as_str).unwrap_or("").to_string(); - let t = e.get("text").and_then(Value::as_str).unwrap_or(&source).to_string(); - let text = if t == source { String::new() } else { t }; - let occ = e.get("occurrences").and_then(Value::as_i64).unwrap_or(0); - let glossary_note = e.get("note").and_then(Value::as_str).unwrap_or(""); - let note = if glossary_note.is_empty() { - format!("×{occ}") - } else { - format!("×{occ} · {glossary_note}") - }; - rows.push(Row { - source, - translation: text, - location: "name".to_string(), - note, - ptr: format!("/names/{i}/text"), - }); - } - rows -} - -// ---------------------------------------------------------------------------- -// Injection: serialize the edited docs back, call the library inject paths -// ---------------------------------------------------------------------------- - -/// Where an inject writes. `InPlace` overwrites the source files. `OutDir` mirrors the data dir's -/// relative layout under a chosen output directory (like the CLI's `-o`). -#[derive(Clone)] -pub enum InjectTarget { - InPlace, - OutDir(PathBuf), -} - -/// Per-file inject result line for the log. -pub struct InjectReport { - pub file: String, - pub applied: usize, - pub skipped: usize, - pub drifted: usize, -} - -/// Inject the whole (edited) model back into byte-exact game files. Calls the same `inject_*` / -/// `inject_names` library paths the CLI uses, with `opts` from the UI checkboxes. `report` drives -/// progress. The returned per-file [`InjectReport`]s are logged by the caller. -pub fn inject_model( - model: &mut TranslationModel, - opts: &InjectOptions, - target: &InjectTarget, - mut report: impl FnMut(f32, String), -) -> Result, String> { - model.sync_docs(); - let total = model.files.len() + 1; // +1 names - let mut done = 0usize; - let mut out = Vec::new(); - - let out_path = |orig: &Path| -> PathBuf { - match target { - InjectTarget::InPlace => orig.to_path_buf(), - InjectTarget::OutDir(od) => od.join(rel(&model.data_dir, orig)), - } - }; - - for f in &model.files { - done += 1; - let name = f.display_name(); - report(done as f32 / total as f32, format!("inject {name}")); - let json = serde_json::to_string(&f.doc).map_err(|e| e.to_string())?; - let dest = out_path(&f.path); - let rep = inject_one_file(f.kind, &f.path, &dest, &json, opts) - .map_err(|e| format!("{name}: {e}"))?; - out.push(rep); - } - - // Names glossary: applied consistently across the WHOLE dir (DBs + CE + maps), like names-inject. - done += 1; - report(done as f32 / total as f32, "inject names".to_string()); - let names_json = serde_json::to_string(&model.names_doc).map_err(|e| e.to_string())?; - if let Some(rep) = inject_names_all(&model.data_dir, &names_json, opts, target)? { - out.push(rep); - } - - Ok(out) -} - -/// Apply one file's translation JSON to its base, writing byte-exact output. Databases write the -/// `.project`+`.dat` pair (mirrors the CLI's `strings_inject_db`). Every written file is re-parsed -/// to never ship a structurally-corrupt file. -fn inject_one_file( - kind: FileKind, - base: &Path, - dest: &Path, - json: &str, - opts: &InjectOptions, -) -> Result { - let name = base.file_name().and_then(|s| s.to_str()).unwrap_or("?").to_string(); - if let Some(parent) = dest.parent() { - let _ = std::fs::create_dir_all(parent); - } - // External event-text .txt: `inject_txt_events` rebuilds the file byte-exact from the base, so - // there is no structured stats struct. Count the intended edits (lines whose text differs from - // source) for the report, then return early. - if kind == FileKind::TxtEvent { - let base_bytes = std::fs::read(base).map_err(|e| e.to_string())?; - let out = inject_txt_events(json, &base_bytes)?; - std::fs::write(dest, &out).map_err(|e| e.to_string())?; - let applied = serde_json::from_str::(json) - .ok() - .and_then(|v| { - v.get("lines").and_then(|l| l.as_array()).map(|ls| { - ls.iter() - .filter(|l| { - l.get("text").and_then(Value::as_str) - != l.get("source").and_then(Value::as_str) - }) - .count() - }) - }) - .unwrap_or(0); - return Ok(InjectReport { file: name, applied, skipped: 0, drifted: 0 }); - } - let stats = match kind { - FileKind::CommonEvent => { - let base_bytes = std::fs::read(base).map_err(|e| e.to_string())?; - let mut ce = CommonEventsFile::read(&base_bytes).map_err(|e| e.to_string())?; - let st = inject_common_events(json, &mut ce, opts)?; - let out = ce.write(); - CommonEventsFile::read(&out).map_err(|e| format!("re-parse failed: {e}"))?; - std::fs::write(dest, &out).map_err(|e| e.to_string())?; - st - } - FileKind::Map => { - let base_bytes = std::fs::read(base).map_err(|e| e.to_string())?; - let mut m = Map::read(&base_bytes).map_err(|e| e.to_string())?; - let st = inject_map(json, &mut m, opts)?; - let out = m.write(); - Map::read(&out).map_err(|e| format!("re-parse failed: {e}"))?; - std::fs::write(dest, &out).map_err(|e| e.to_string())?; - st - } - FileKind::GameDat => { - let base_bytes = std::fs::read(base).map_err(|e| e.to_string())?; - let mut gd = GameDat::read(&base_bytes).map_err(|e| e.to_string())?; - let st = inject_game_dat(json, &mut gd, opts)?; - let out = gd.write(); - GameDat::read(&out).map_err(|e| format!("re-parse failed: {e}"))?; - std::fs::write(dest, &out).map_err(|e| e.to_string())?; - st - } - FileKind::Database => { - // The `.project`+`.dat` pair, written together byte-exact. - let proj = base.to_path_buf(); - let base_dat = proj.with_extension("dat"); - let out_dat = dest.with_extension("dat"); - let (p, d) = ( - std::fs::read(&proj).map_err(|e| e.to_string())?, - std::fs::read(&base_dat).map_err(|e| e.to_string())?, - ); - let mut db = Database::read(&p, &d).map_err(|e| e.to_string())?; - let st = inject_db_strings(json, &mut db, opts)?; - let (op, od) = db.write(); - Database::read(&op, &od).map_err(|e| format!("re-parse failed: {e}"))?; - std::fs::write(dest, &op).map_err(|e| e.to_string())?; - std::fs::write(&out_dat, &od).map_err(|e| e.to_string())?; - st - } - FileKind::TxtEvent => unreachable!("txt handled before the match"), - }; - Ok(InjectReport { - file: name, - applied: stats.applied, - skipped: stats.untranslated + stats.code_mismatch + stats.unrepresentable, - drifted: stats.drifted, - }) -} - -/// Apply the name glossary across the whole data dir (DB pairs + CommonEvent + maps), consistently, -/// exactly as the CLI's `names-inject` does. Returns `None` if the glossary is empty/absent. -fn inject_names_all( - dir: &Path, - names_json: &str, - opts: &InjectOptions, - target: &InjectTarget, -) -> Result, String> { - // Parse everything up front so a partial write can't desync the name mirrors. - let basic = basic_data_dir(dir); - - // The names pass must LAYER on top of the per-file string injects: when writing to an output - // dir, the string pass has already written its edits to `/`. So we read each base file - // from its output destination if that exists (string-inject touched it), else from the original. - // This stops the names pass from clobbering a string-edited file with a fresh original copy. - // In place, the destination IS the original, so it reads the just-string-injected file directly. - let remap = |orig: &Path| -> PathBuf { - match target { - InjectTarget::InPlace => orig.to_path_buf(), - InjectTarget::OutDir(od) => od.join(rel(dir, orig)), - } - }; - let base_of = |orig: &Path| -> PathBuf { - let dest = remap(orig); - if dest.exists() { - dest - } else { - orig.to_path_buf() - } - }; - - let mut db_paths: Vec<(PathBuf, PathBuf)> = Vec::new(); - for stem in ["DataBase", "CDataBase", "SysDatabase"] { - let proj = basic.join(format!("{stem}.project")); - let dat = basic.join(format!("{stem}.dat")); - if proj.exists() && dat.exists() { - db_paths.push((proj, dat)); - } - } - if db_paths.is_empty() { - return Ok(None); - } - let mut db_loaded: Vec<(PathBuf, PathBuf, Database)> = Vec::new(); - for (proj, dat) in &db_paths { - let (p, d) = ( - std::fs::read(base_of(proj)).map_err(|e| e.to_string())?, - std::fs::read(base_of(dat)).map_err(|e| e.to_string())?, - ); - let db = Database::read(&p, &d).map_err(|e| e.to_string())?; - db_loaded.push((proj.clone(), dat.clone(), db)); - } - let ce_path = basic.join("CommonEvent.dat"); - let mut ce_loaded: Option<(PathBuf, CommonEventsFile)> = None; - if ce_path.exists() { - let b = std::fs::read(base_of(&ce_path)).map_err(|e| e.to_string())?; - let ce = CommonEventsFile::read(&b).map_err(|e| e.to_string())?; - ce_loaded = Some((ce_path.clone(), ce)); - } - let mut map_paths: Vec = Vec::new(); - for sub in [dir.to_path_buf(), dir.join("MapData")] { - let Ok(rd) = std::fs::read_dir(&sub) else { - continue; - }; - for e in rd.flatten() { - let p = e.path(); - if p.extension().and_then(|s| s.to_str()) == Some("mps") { - map_paths.push(p); - } - } - } - map_paths.sort(); - map_paths.dedup(); - let mut map_loaded: Vec<(PathBuf, Map)> = Vec::new(); - for p in &map_paths { - let b = std::fs::read(base_of(p)).map_err(|e| e.to_string())?; - let m = Map::read(&b).map_err(|e| e.to_string())?; - map_loaded.push((p.clone(), m)); - } - - let glossary = wolf_decompiler::symbols::load_embedded_engine_glossary(); - let mut dbs: Vec = db_loaded.iter().map(|(_, _, db)| db.clone()).collect(); - let mut ce_vec: Vec<&mut CommonEventsFile> = - ce_loaded.as_mut().map(|(_, c)| c).into_iter().collect(); - let mut maps: Vec = map_loaded.iter().map(|(_, m)| m.clone()).collect(); - let stats = inject_names(names_json, &mut dbs, &mut ce_vec, &mut maps, opts, &glossary)?; - // Move the mutated copies back. - for ((_, _, db), new) in db_loaded.iter_mut().zip(dbs) { - *db = new; - } - for ((_, m), new) in map_loaded.iter_mut().zip(maps) { - *m = new; - } - - let ensure_parent = |p: &Path| { - if let Some(parent) = p.parent() { - let _ = std::fs::create_dir_all(parent); - } - }; - - // Write everything (re-parsing each) only after the whole apply succeeded. - for (proj, dat, db) in &db_loaded { - let (op, od) = db.write(); - Database::read(&op, &od).map_err(|e| format!("names: DB re-parse failed: {e}"))?; - let (out_proj, out_dat) = (remap(proj), remap(dat)); - ensure_parent(&out_proj); - std::fs::write(&out_proj, &op).map_err(|e| e.to_string())?; - std::fs::write(&out_dat, &od).map_err(|e| e.to_string())?; - } - if let Some((p, c)) = &ce_loaded { - let out = c.write(); - CommonEventsFile::read(&out).map_err(|e| format!("names: CE re-parse failed: {e}"))?; - let dest = remap(p); - ensure_parent(&dest); - std::fs::write(&dest, &out).map_err(|e| e.to_string())?; - } - for (p, m) in &map_loaded { - let out = m.write(); - Map::read(&out).map_err(|e| format!("names: map re-parse failed: {e}"))?; - let dest = remap(p); - ensure_parent(&dest); - std::fs::write(&dest, &out).map_err(|e| e.to_string())?; - } - - Ok(Some(InjectReport { - file: "Names (glossary)".to_string(), - applied: stats.applied, - skipped: stats.untranslated + stats.code_mismatch + stats.unrepresentable, - drifted: stats.drifted, - })) -} - -// ---------------------------------------------------------------------------- -// Merge (translation memory), name-conflict check, save/load -// ---------------------------------------------------------------------------- - -/// Outcome of a merge: how many rows got a translation carried in, and how many are still untouched. -pub struct MergeOutcome { - pub carried: usize, - pub still_new: usize, -} - -/// Carry existing translations from old translation JSON file(s)/dir into the current model, matched -/// by exact source string. Reuses the library's `build_memory` + `apply_memory`: a memory is built -/// from every old `*.json`, then applied to each file's parsed document. The carried `text` values -/// are read back into the rows. Untouched (already-translated) rows are kept. -pub fn merge_into_model(model: &mut TranslationModel, old_paths: &[PathBuf]) -> Result { - // Build one global memory from every old JSON (files + every *.json under old dirs). - let mut old_files: Vec = Vec::new(); - for p in old_paths { - collect_json_files(p, &mut old_files); - } - old_files.sort(); - old_files.dedup(); - if old_files.is_empty() { - return Err("no *.json found under the chosen old translation path(s)".into()); - } - let mut texts: Vec = Vec::with_capacity(old_files.len()); - for p in &old_files { - texts.push(std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display()))?); - } - let refs: Vec<&str> = texts.iter().map(String::as_str).collect(); - let (memory, _conflicts) = build_memory(&refs); - - // Sync current edits into the docs first so we don't clobber existing in-session translations. - model.sync_docs(); - - let mut carried = 0usize; - let mut still_new = 0usize; - for f in &mut model.files { - let json = serde_json::to_string(&f.doc).map_err(|e| e.to_string())?; - let (merged, stats) = apply_memory(&json, &memory)?; - f.doc = serde_json::from_str(&merged).map_err(|e| e.to_string())?; - read_docs_into_rows(&f.doc, &mut f.rows); - carried += stats.carried; - still_new += stats.still_new; - } - if !model.names_doc.is_null() { - let json = serde_json::to_string(&model.names_doc).map_err(|e| e.to_string())?; - let (merged, stats) = apply_memory(&json, &memory)?; - model.names_doc = serde_json::from_str(&merged).map_err(|e| e.to_string())?; - read_docs_into_rows(&model.names_doc, &mut model.names); - carried += stats.carried; - still_new += stats.still_new; - } - Ok(MergeOutcome { carried, still_new }) -} - -/// Read the `text` leaf each row points at back into the row's `translation` (the inverse of -/// [`apply_rows_to_doc`]). Used after a merge rewrote the documents. -fn read_docs_into_rows(doc: &Value, rows: &mut [Row]) { - for r in rows { - if let Some(Value::String(s)) = doc.pointer(&r.ptr) { - // Keep the empty-when-untranslated invariant so the memory + UX stay consistent. - r.translation = if *s == r.source { String::new() } else { s.clone() }; - } - } -} - -/// Run the name-conflict check over the current model's name glossary + every file's current JSON -/// (so a name translated inconsistently between the glossary and a DB/scene is caught). Returns the -/// conflicts the library found. -pub fn check_conflicts(model: &mut TranslationModel) -> Vec { - model.sync_docs(); - let mut docs: Vec<(String, String)> = Vec::new(); - docs.push(("Names".to_string(), model.names_doc.to_string())); - for f in &model.files { - docs.push((f.display_name(), f.doc.to_string())); - } - let refs: Vec<(String, &str)> = docs.iter().map(|(n, j)| (n.clone(), j.as_str())).collect(); - check_name_conflicts(&refs) -} - -/// Serialize the whole model to a single self-describing JSON for the project file. Stores each -/// file's path/kind + its (synced) document, plus the names document, enough to fully restore. -pub fn save_to_path(model: &mut TranslationModel, path: &Path) -> Result<(), String> { - model.sync_docs(); - let files: Vec = model - .files - .iter() - .map(|f| { - serde_json::json!({ - "path": f.path.to_string_lossy(), - "kind": f.kind.tag(), - "doc": f.doc, - }) - }) - .collect(); - let root = serde_json::json!({ - "wolfdawn_translation": 1, - "data_dir": model.data_dir.to_string_lossy(), - "files": files, - "names": model.names_doc, - }); - let text = serde_json::to_string_pretty(&root).map_err(|e| e.to_string())?; - std::fs::write(path, text).map_err(|e| e.to_string()) -} - -/// Restore a model previously written by [`save_to_path`]. -pub fn load_from_path(path: &Path) -> Result { - let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?; - let root: Value = serde_json::from_str(&text).map_err(|e| e.to_string())?; - if root.get("wolfdawn_translation").is_none() { - return Err("not a WolfDawn translation project file".into()); - } - let data_dir = PathBuf::from(root.get("data_dir").and_then(Value::as_str).unwrap_or("")); - let mut files = Vec::new(); - if let Some(arr) = root.get("files").and_then(Value::as_array) { - for f in arr { - let path = PathBuf::from(f.get("path").and_then(Value::as_str).unwrap_or("")); - let kind = match f.get("kind").and_then(Value::as_str).unwrap_or("") { - "map" => FileKind::Map, - "common" => FileKind::CommonEvent, - "db" => FileKind::Database, - "gamedat" => FileKind::GameDat, - "txt" => FileKind::TxtEvent, - other => return Err(format!("unknown file kind {other:?} in project file")), - }; - let doc = f.get("doc").cloned().unwrap_or(Value::Null); - let rows = match kind { - FileKind::Map | FileKind::CommonEvent => rows_from_scenes(&doc), - FileKind::Database => rows_from_db(&doc), - FileKind::GameDat => rows_from_gamedat(&doc), - FileKind::TxtEvent => rows_from_txt(&doc), - }; - files.push(FileEntry { path, kind, rows, doc }); - } - } - let names_doc = root.get("names").cloned().unwrap_or(Value::Null); - let names = rows_from_names(&names_doc); - Ok(TranslationModel { data_dir, files, names, names_doc }) -} - -// --- small helpers (mirror the CLI's) --------------------------------------------------------- - -/// Relative path of `path` under `base`, as a `PathBuf` for re-joining under an output dir. -fn rel(base: &Path, path: &Path) -> PathBuf { - path.strip_prefix(base).unwrap_or(path).to_path_buf() -} - -/// Collect every `*.json` under `path` (recursively for a dir, the file itself if it is a `.json`). -fn collect_json_files(path: &Path, out: &mut Vec) { - if path.is_dir() { - let Ok(rd) = std::fs::read_dir(path) else { - return; - }; - for e in rd.flatten() { - collect_json_files(&e.path(), out); - } - } else if path.extension().and_then(|s| s.to_str()) == Some("json") { - out.push(path.to_path_buf()); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file/dir is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = std::path::Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// The shared Data-dir fixture. Tests skip gracefully when it is absent (CI without the corpus). - fn fixture_dir() -> Option { - test_data("chamber/Data") - .filter(|d| d.join("BasicData").join("CommonEvent.dat").exists()) - } - - fn tmp_out(tag: &str) -> PathBuf { - let p = std::env::temp_dir().join(format!( - "wolfdawn_tr_{tag}_{}_{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::create_dir_all(&p); - p - } - - /// A self-contained data dir with one event-text `.txt` (no external fixture needed) round-trips - /// through the model: extract picks it up as a TxtEvent entry with the dialogue (not the `/` - /// command lines), and an edit injects back into the file byte-aware while the commands survive. - #[test] - fn txt_event_round_trip_through_model() { - let dir = tmp_out("txt"); - let evt = dir.join("Evtext"); - std::fs::create_dir_all(&evt).unwrap(); - let txt_path = evt.join("scene.txt"); - // UTF-8 so the test needs no encoding crate; the extractor detects UTF-8 vs Shift-JIS. - let original = "/evcg,a0\r\nアイリス:\r\n元のセリフ\r\n/b\r\n"; - std::fs::write(&txt_path, original.as_bytes()).unwrap(); - - let mut outcome = extract_model(&dir, |_, _| {}).expect("extract"); - let entry = outcome - .model - .files - .iter_mut() - .find(|f| f.kind == FileKind::TxtEvent) - .expect("a TxtEvent entry from the .txt"); - assert!( - entry.rows.iter().any(|r| r.source == "元のセリフ"), - "the dialogue line should be extracted" - ); - assert!( - entry.rows.iter().all(|r| !r.source.starts_with('/')), - "no /command or //comment line should be extracted" - ); - - // Edit the dialogue line, inject in place. - let row = entry.rows.iter_mut().find(|r| r.source == "元のセリフ").unwrap(); - row.translation = "新しいセリフ".to_string(); - let reports = inject_model( - &mut outcome.model, - &InjectOptions::default(), - &InjectTarget::InPlace, - |_, _| {}, - ) - .expect("inject"); - assert!(reports.iter().any(|r| r.applied >= 1), "the txt edit should apply"); - - // The file now holds the new text, the old text is gone, and the commands are intact. - let after = std::fs::read_to_string(&txt_path).unwrap(); - assert!(after.contains("新しいセリフ"), "edited text present"); - assert!(!after.contains("元のセリフ"), "old text gone"); - assert!(after.contains("/evcg,a0") && after.contains("/b"), "command lines intact"); - let _ = std::fs::remove_dir_all(&dir); - } - - /// Extract -> edit one row -> inject to a temp out dir -> re-extract -> the edit is present. - /// Proves extract->edit->inject works through the GUI's own code paths. - #[test] - fn round_trip_extract_edit_inject() { - let Some(dir) = fixture_dir() else { - eprintln!("skip round_trip_extract_edit_inject: fixture not present"); - return; - }; - let mut outcome = extract_model(&dir, |_, _| {}).expect("extract"); - let model = &mut outcome.model; - assert!(!model.files.is_empty(), "should have extracted at least one file"); - assert!(model.total_rows() > 0, "model should have non-empty rows"); - - // Find the first file with at least one row and edit its first row. - let (fi, edited_source, edited_text) = { - let f = model - .files - .iter_mut() - .find(|f| !f.rows.is_empty()) - .expect("a file with rows"); - let row = &mut f.rows[0]; - let src = row.source.clone(); - let new = format!("{src} [EDITED-BY-TEST]"); - row.translation = new.clone(); - (f.path.clone(), src, new) - }; - - let out = tmp_out("rt"); - let reports = inject_model( - model, - &InjectOptions::default(), - &InjectTarget::OutDir(out.clone()), - |_, _| {}, - ) - .expect("inject"); - assert!(reports.iter().any(|r| r.applied >= 1), "at least one translation applied"); - - // Re-extract from the output dir and confirm the edit survived the byte-exact round-trip. - let re = extract_model(&out, |_, _| {}).expect("re-extract"); - let same_file = re - .model - .files - .iter() - .find(|f| f.path.file_name() == fi.file_name()) - .expect("the edited file re-extracted"); - let found = same_file - .rows - .iter() - .any(|r| r.source == edited_text || r.translation == edited_text); - assert!(found, "the injected edit {edited_text:?} should re-extract (source was {edited_source:?})"); - - let _ = std::fs::remove_dir_all(&out); - } - - /// en-punct: a translation containing 「」, injected with normalize_punct=true, re-extracts as - /// ASCII quotes. - #[test] - fn en_punct_normalizes_brackets() { - let Some(dir) = fixture_dir() else { - eprintln!("skip en_punct_normalizes_brackets: fixture not present"); - return; - }; - let mut outcome = extract_model(&dir, |_, _| {}).expect("extract"); - let model = &mut outcome.model; - - // Pick a CLEAN source row (no inline `\code`, `@window` prefix, or `/` tag, and no - // newlines) so replacing it wholesale with a bracketed translation can never trip the - // control-code guard, and the assertion is unambiguous. Set it to a fixed `「Hello」`. - let marker = "「Hello」"; - let edited_loc = { - let mut chosen: Option<(usize, usize)> = None; - 'outer: for (fi, f) in model.files.iter().enumerate() { - for (ri, r) in f.rows.iter().enumerate() { - if !r.source.contains(['\\', '@', '<', '>', '\n']) && !r.source.is_empty() { - chosen = Some((fi, ri)); - break 'outer; - } - } - } - let (fi, ri) = chosen.expect("a clean source row"); - model.files[fi].rows[ri].translation = marker.to_string(); - (fi, ri) - }; - - let out = tmp_out("punct"); - let opts = InjectOptions { - normalize_punct: true, - ..Default::default() - }; - let reports = - inject_model(model, &opts, &InjectTarget::OutDir(out.clone()), |_, _| {}).expect("inject"); - assert!(reports.iter().any(|r| r.applied >= 1), "the en-punct edit should apply"); - - // Re-extract from the output dir: the brackets must have become ASCII double quotes. - let re = extract_model(&out, |_, _| {}).expect("re-extract"); - let edited_file = &model.files[edited_loc.0]; - let same = re - .model - .files - .iter() - .find(|f| f.path.file_name() == edited_file.path.file_name()) - .expect("edited file re-extracted"); - let found = same.rows.iter().any(|r| r.source == "\"Hello\""); - assert!( - found, - "en-punct should have produced ASCII quotes (\"Hello\"); re-extracted sources did not contain it" - ); - - let _ = std::fs::remove_dir_all(&out); - } - - /// Save then load restores the model's rows and edits. - #[test] - fn save_load_round_trip() { - let Some(dir) = fixture_dir() else { - eprintln!("skip save_load_round_trip: fixture not present"); - return; - }; - let mut outcome = extract_model(&dir, |_, _| {}).expect("extract"); - let model = &mut outcome.model; - let total = model.total_rows(); - // Edit a row so we can assert the translation survives save/load. - let edited = { - let f = model.files.iter_mut().find(|f| !f.rows.is_empty()).expect("rows"); - f.rows[0].translation = format!("{} [SAVED]", f.rows[0].source); - f.rows[0].translation.clone() - }; - - let out = tmp_out("save"); - let proj_file = out.join("project.wdtr.json"); - save_to_path(model, &proj_file).expect("save"); - let restored = load_from_path(&proj_file).expect("load"); - assert_eq!(restored.total_rows(), total, "row count should survive save/load"); - let found = restored - .files - .iter() - .flat_map(|f| f.rows.iter()) - .any(|r| r.translation == edited); - assert!(found, "the edited translation should survive save/load"); - - let _ = std::fs::remove_dir_all(&out); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/verify.rs b/util/wolfdawn-master/crates/wolf-gui/src/verify.rs deleted file mode 100644 index 5fe02da..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/verify.rs +++ /dev/null @@ -1,376 +0,0 @@ -//! The Verify section's headless logic: round-trip a data file (or a whole corpus) to prove the -//! library reads and re-writes it without loss. The same check the CLI's `verify-roundtrip` runs. -//! -//! A round-trip is `write(read(bytes)) == bytes`: parse the file with the matching `wolf_formats` -//! reader, re-serialise it, and compare against the original bytes. A match is PASS (byte-exact). A -//! mismatch is FAIL with the first-differing offset for diagnosis. An unreadable file is an error. -//! This module mirrors `cmd.rs`'s `classify` / `roundtrip_path` / `verify_corpus` exactly, so the -//! GUI and CLI agree on what "verified" means. - -use std::path::{Path, PathBuf}; - -use wolf_formats::common_event::CommonEventsFile; -use wolf_formats::database::{BasicDatabase, Database}; -use wolf_formats::game_dat::GameDat; -use wolf_formats::map::Map; - -/// Which kind of file we're verifying. Decides the reader/writer pair, mirroring the CLI's -/// `FileKind` + `classify`. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum FileKind { - Map, - CommonEvent, - GameDat, - /// A database, represented by its `.project` (the `.dat` sibling is paired in). - Database, - /// Legacy `SysDatabaseBasic` (Wolf 2.x): schema-only `.project` + opaque `.dat`. - BasicDatabase, - Unsupported, -} - -/// Classify a path by extension/name, exactly like the CLI's `classify`. -fn classify(path: &Path) -> FileKind { - let ext = path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - let name = path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - match ext.as_str() { - "mps" => FileKind::Map, - "project" if name == "sysdatabasebasic.project" => FileKind::BasicDatabase, - "dat" if name == "sysdatabasebasic.dat" => FileKind::BasicDatabase, - "project" => FileKind::Database, - "dat" if name == "commonevent.dat" => FileKind::CommonEvent, - "dat" if name == "game.dat" => FileKind::GameDat, - // A .dat with a sibling .project is the value half of a database. - "dat" if path.with_extension("project").exists() => FileKind::Database, - _ => FileKind::Unsupported, - } -} - -/// True when the file is one verify understands (so the corpus walk knows what to include and the -/// single-file flow can warn early on an unsupported pick). -pub fn is_supported(path: &Path) -> bool { - !matches!(classify(path), FileKind::Unsupported) -} - -/// The verdict of one file's round-trip. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum Verdict { - /// `write(read(bytes)) == bytes`, byte-exact. - Pass, - /// Parsed and re-serialised, but the output differs. Carries the first-differing byte offset - /// and the two lengths, for a clear diagnostic. - Fail { - /// Index of the first differing byte (or the shorter length when one is a prefix of the other). - offset: usize, - original_len: usize, - rewritten_len: usize, - }, - /// The file could not be read/parsed (unreadable, unsupported, or a parse error). Carries the - /// message. - Error(String), -} - -impl Verdict { - /// A short status word for the UI/log (`PASS` / `FAIL` / `ERR`). - pub fn tag(&self) -> &'static str { - match self { - Verdict::Pass => "PASS", - Verdict::Fail { .. } => "FAIL", - Verdict::Error(_) => "ERR", - } - } - - /// A one-line human description of the verdict (the diagnostic). - pub fn detail(&self) -> String { - match self { - Verdict::Pass => "byte-exact round-trip".to_string(), - Verdict::Fail { - offset, - original_len, - rewritten_len, - } => format!( - "differs at byte {offset} (original {original_len} bytes, rewritten {rewritten_len} bytes)" - ), - Verdict::Error(e) => e.clone(), - } - } - - pub fn is_pass(&self) -> bool { - matches!(self, Verdict::Pass) - } -} - -/// The index of the first byte that differs between `a` and `b` (or the shorter length when one is -/// a strict prefix of the other). Used to build the [`Verdict::Fail`] diagnostic. -fn first_diff(a: &[u8], b: &[u8]) -> usize { - a.iter().zip(b.iter()).position(|(x, y)| x != y).unwrap_or_else(|| a.len().min(b.len())) -} - -/// Compare an original buffer to a re-serialised one, producing [`Verdict::Pass`] or a -/// [`Verdict::Fail`] with the first-differing offset. -fn compare(original: &[u8], rewritten: Vec) -> Verdict { - if rewritten == original { - Verdict::Pass - } else { - Verdict::Fail { - offset: first_diff(original, &rewritten), - original_len: original.len(), - rewritten_len: rewritten.len(), - } - } -} - -/// Round-trip a single file, returning the [`Verdict`]. Reads the file, parses it with the matching -/// `wolf_formats` reader, re-serialises, and compares. Same logic as the CLI's `roundtrip_path`, -/// but returning a structured verdict (with the diff offset) instead of a bare bool. -/// -/// For a database the `.project` is the canonical handle and its sibling `.dat` is paired in. Both -/// halves must re-serialise byte-exact to PASS. -pub fn verify_file(path: &Path) -> Verdict { - let bytes = match std::fs::read(path) { - Ok(b) => b, - Err(e) => return Verdict::Error(format!("cannot read {}: {e}", path.display())), - }; - match classify(path) { - FileKind::Map => match Map::read(&bytes) { - Ok(m) => compare(&bytes, m.write()), - Err(e) => Verdict::Error(e.to_string()), - }, - FileKind::CommonEvent => match CommonEventsFile::read(&bytes) { - Ok(ce) => compare(&bytes, ce.write()), - Err(e) => Verdict::Error(e.to_string()), - }, - FileKind::GameDat => match GameDat::read(&bytes) { - Ok(gd) => compare(&bytes, gd.write()), - Err(e) => Verdict::Error(e.to_string()), - }, - FileKind::Database => { - // `path` may be the .project or its .dat. Verify the pair via the .project. - let proj_path = path.with_extension("project"); - let dat_path = path.with_extension("dat"); - let (proj_bytes, dat_bytes) = - match (std::fs::read(&proj_path), std::fs::read(&dat_path)) { - (Ok(p), Ok(d)) => (p, d), - _ => { - return Verdict::Error(format!( - "cannot read DB pair {} + {}", - proj_path.display(), - dat_path.display() - )) - } - }; - match Database::read(&proj_bytes, &dat_bytes) { - Ok(db) => { - let (proj_out, dat_out) = db.write(); - // Report the .project diff first, then the .dat. - let proj_v = compare(&proj_bytes, proj_out); - if !proj_v.is_pass() { - return proj_v; - } - compare(&dat_bytes, dat_out) - } - Err(e) => Verdict::Error(e.to_string()), - } - } - FileKind::BasicDatabase => { - let proj_path = path.with_extension("project"); - let dat_path = path.with_extension("dat"); - let (proj_bytes, dat_bytes) = - match (std::fs::read(&proj_path), std::fs::read(&dat_path)) { - (Ok(p), Ok(d)) => (p, d), - _ => { - return Verdict::Error(format!( - "cannot read DB pair {} + {}", - proj_path.display(), - dat_path.display() - )) - } - }; - match BasicDatabase::read(&proj_bytes, &dat_bytes) { - Ok(db) => { - let (proj_out, dat_out) = db.write(); - let proj_v = compare(&proj_bytes, proj_out); - if !proj_v.is_pass() { - return proj_v; - } - compare(&dat_bytes, dat_out) - } - Err(e) => Verdict::Error(e.to_string()), - } - } - FileKind::Unsupported => { - Verdict::Error("not a verifiable file type (.mps, CommonEvent.dat, Game.dat, .project)".to_string()) - } - } -} - -/// One file's result in a corpus run: its path relative to the corpus root + its verdict. -pub struct CorpusRow { - pub rel: String, - pub verdict: Verdict, -} - -/// A whole corpus verify run: the per-file rows + the pass/total summary. -pub struct CorpusOutcome { - pub rows: Vec, -} - -impl CorpusOutcome { - /// How many rows passed (byte-exact). - pub fn passed(&self) -> usize { - self.rows.iter().filter(|r| r.verdict.is_pass()).count() - } - /// How many rows failed (parsed but differed). - pub fn failed(&self) -> usize { - self.rows.iter().filter(|r| matches!(r.verdict, Verdict::Fail { .. })).count() - } - /// How many rows errored (could not parse). - pub fn errored(&self) -> usize { - self.rows.iter().filter(|r| matches!(r.verdict, Verdict::Error(_))).count() - } - /// Total rows checked. - pub fn total(&self) -> usize { - self.rows.len() - } -} - -/// Recursively collect every file under `dir` (depth-first), like the CLI's `collect`. -fn collect(dir: &Path, out: &mut Vec) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let p = entry.path(); - if p.is_dir() { - collect(&p, out); - } else { - out.push(p); - } - } -} - -/// Path of `path` relative to `base` (for the per-file table), like the CLI's `rel`. -fn rel(base: &Path, path: &Path) -> String { - path.strip_prefix(base).unwrap_or(path).display().to_string() -} - -/// Walk every supported file under `dir`, round-trip each, and return the per-file rows + summary. -/// The `verify-roundtrip --corpus` path. The lone `.dat` half of a database is skipped (it is -/// verified through its `.project`, matching the CLI). `report` drives a progress bar. Pass a no-op -/// in tests. -pub fn verify_corpus(dir: &Path, mut report: impl FnMut(f32, String)) -> Result { - if !dir.is_dir() { - return Err(format!("{} is not a folder", dir.display())); - } - report(0.0, "scanning folder…".to_string()); - let mut files: Vec = Vec::new(); - collect(dir, &mut files); - files.sort(); - - // Keep only the files verify understands. Drop the lone .dat half of a DB pair (verified via the - // .project) so each pair is checked once. - let to_check: Vec = files - .into_iter() - .filter(|p| match classify(p) { - FileKind::Unsupported => false, - FileKind::Database | FileKind::BasicDatabase - if p.extension().and_then(|s| s.to_str()) == Some("dat") => - { - false - } - _ => true, - }) - .collect(); - - if to_check.is_empty() { - return Err(format!("no verifiable files found under {}", dir.display())); - } - - let total = to_check.len(); - let mut rows = Vec::with_capacity(total); - for (i, path) in to_check.iter().enumerate() { - let r = rel(dir, path); - report((i as f32) / (total as f32), format!("verify {r}")); - let verdict = verify_file(path); - rows.push(CorpusRow { rel: r, verdict }); - } - report(1.0, "done".to_string()); - Ok(CorpusOutcome { rows }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Resolve a fixture by its clean relative path under `WOLFDAWN_TEST_DATA`. Returns `None` when - /// the var is unset or the file/dir is missing, so the data-dependent tests skip gracefully. - fn test_data(rel: &str) -> Option { - let base = std::env::var_os("WOLFDAWN_TEST_DATA")?; - let p = Path::new(&base).join(rel); - p.exists().then_some(p) - } - - /// A known-good plaintext data file from the fixtures root should verify PASS through the - /// section's own code path. Read-only. Skips when absent. - #[test] - fn verify_single_known_good_passes() { - let Some(path) = [ - "chamber/Data/MapData/TitleMap.mps", - "chamber/Data/BasicData/Game.dat", - "chamber/Data/BasicData/CommonEvent.dat", - "chamber/Data/BasicData/DataBase.project", - ] - .iter() - .find_map(|r| test_data(r)) else { - eprintln!("skip verify_single_known_good_passes: no data fixture present"); - return; - }; - let verdict = verify_file(&path); - assert!( - verdict.is_pass(), - "{} should round-trip byte-exact, got {:?}: {}", - path.display(), - verdict.tag(), - verdict.detail() - ); - } - - /// A corpus walk over the fixtures Data dir produces rows and a sensible summary. Assert it - /// runs and at least one file passes. Do not require zero failures (the corpus may include - /// edge files), matching the CLI's tolerant report. - #[test] - fn verify_corpus_runs() { - let Some(dir) = test_data("chamber/Data").filter(|p| p.is_dir()) else { - eprintln!("skip verify_corpus_runs: data fixture not present"); - return; - }; - let outcome = verify_corpus(&dir, |_, _| {}).expect("corpus verify"); - assert!(outcome.total() > 0, "corpus should have verifiable files"); - assert!(outcome.passed() > 0, "at least one file should pass"); - assert_eq!( - outcome.passed() + outcome.failed() + outcome.errored(), - outcome.total(), - "every row has exactly one verdict" - ); - } - - /// An unsupported file type reports an error verdict, not a panic. - #[test] - fn unsupported_file_errors() { - let tmp = std::env::temp_dir().join(format!("wolfdawn_verify_unsup_{}", std::process::id())); - let _ = std::fs::create_dir_all(&tmp); - let f = tmp.join("notes.txt"); - std::fs::write(&f, b"hello").unwrap(); - let v = verify_file(&f); - assert!(matches!(v, Verdict::Error(_)), "txt should be an error verdict"); - assert!(!is_supported(&f)); - let _ = std::fs::remove_dir_all(&tmp); - } -} diff --git a/util/wolfdawn-master/crates/wolf-gui/src/widgets.rs b/util/wolfdawn-master/crates/wolf-gui/src/widgets.rs deleted file mode 100644 index b83a2d2..0000000 --- a/util/wolfdawn-master/crates/wolf-gui/src/widgets.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Reusable UI pieces: native file dialogs (thin `rfd` wrappers) and the small labelled -//! controls the sections lean on heavily (a checkbox-with-tooltip and a path-field-plus-Browse). -//! -//! An option flag like `--en-punct` becomes one `labeled_checkbox(...)` call, and a path argument -//! becomes one `path_field(...)` call, with the tooltip and Browse button handled here. Every -//! non-obvious control gets a plain-language tooltip. - -// Shared widget/dialog toolkit. Some helpers are not wired into a section yet, so allow dead code -// here rather than annotating each one. -#![allow(dead_code)] - -use std::path::PathBuf; - -// ---------------------------------------------------------------------------- -// File dialogs (rfd) -// ---------------------------------------------------------------------------- - -/// Pick an existing folder. `None` if the user cancels. -pub fn pick_folder() -> Option { - rfd::FileDialog::new().pick_folder() -} - -/// Pick an existing folder, starting the dialog at `start` when it exists. -pub fn pick_folder_in(start: Option<&std::path::Path>) -> Option { - let mut dlg = rfd::FileDialog::new(); - if let Some(dir) = start.filter(|p| p.is_dir()) { - dlg = dlg.set_directory(dir); - } - dlg.pick_folder() -} - -/// Pick an existing file. `filters` is a list of `(label, &[extensions])`, e.g. -/// `&[("Wolf archive", &["wolf"]), ("Game data", &["dat"])]`. `None` if cancelled. -pub fn pick_file(filters: &[(&str, &[&str])]) -> Option { - let mut dlg = rfd::FileDialog::new(); - for (label, exts) in filters { - dlg = dlg.add_filter(*label, exts); - } - dlg.pick_file() -} - -/// Pick an existing file, starting the dialog in `start` when it is a directory. `filters` is a list -/// of `(label, &[extensions])`. `None` if cancelled. -pub fn pick_file_in(start: Option<&std::path::Path>, filters: &[(&str, &[&str])]) -> Option { - let mut dlg = rfd::FileDialog::new(); - if let Some(dir) = start.filter(|p| p.is_dir()) { - dlg = dlg.set_directory(dir); - } - for (label, exts) in filters { - dlg = dlg.add_filter(*label, exts); - } - dlg.pick_file() -} - -/// Choose a destination path for a save, pre-filled with `default_name`. `None` if cancelled. -pub fn save_file(default_name: &str, filters: &[(&str, &[&str])]) -> Option { - let mut dlg = rfd::FileDialog::new().set_file_name(default_name); - for (label, exts) in filters { - dlg = dlg.add_filter(*label, exts); - } - dlg.save_file() -} - -// ---------------------------------------------------------------------------- -// Reusable controls -// ---------------------------------------------------------------------------- - -/// A checkbox with a label and a hover tooltip. Returns `true` if the value changed this frame. -/// Sections use this for optional flags (`--en-punct`, `--allow-code-drift`, encrypt toggles…). -pub fn labeled_checkbox(ui: &mut egui::Ui, value: &mut bool, label: &str, tooltip: &str) -> bool { - ui.checkbox(value, label).on_hover_text(tooltip).changed() -} - -// ---------------------------------------------------------------------------- -// Aligned form rows (shared layout for every section) -// ---------------------------------------------------------------------------- -// -// These helpers give one consistent layout: a fixed-width left LABEL column, an input that FILLS -// the remaining width (so every input in a section shares the same left AND right edge), and a -// fixed right-hand BUTTON area for Browse/× so those align too. The input fills available width -// rather than taking a fixed pixel width, which keeps rows uniform and responsive to window size. - -/// The fixed width (px) of a form row's left label column. Wide enough for the longest labels used -/// across the sections ("Start-up message", "Default PC graphic", "Output folder", …) so every -/// input starts at the same x regardless of its label. -pub const LABEL_COL_W: f32 = 128.0; - -/// The reserved width (px) of a path row's right-hand button area. Room for "Browse… ×" so the -/// Browse buttons (and the optional clear ×) line up in a fixed column across a section. -pub const BTN_COL_W: f32 = 128.0; - -/// Emit `label` into a fixed-width [`LABEL_COL_W`] left cell (left-aligned, vertically centred to -/// match the adjacent control), then run `content` for the rest of the row inside one -/// `ui.horizontal`. The label's `Response` carries `tooltip`. Returns whatever `content` returns. -/// -/// The other form helpers (and the sections' bespoke rows) build on this, so every labelled row -/// across the app shares the same left column and baseline. -pub fn form_row( - ui: &mut egui::Ui, - label: &str, - tooltip: &str, - content: impl FnOnce(&mut egui::Ui) -> R, -) -> R { - ui.horizontal(|ui| { - // Reserve an EXACT fixed-width cell for the label. `allocate_ui_with_layout` advances the - // cursor by the label's width, not the desired width, so short labels leave each row's input - // starting at a different x. `allocate_exact_size` reserves the full column unconditionally, - // so every input begins at a constant x and is uniform width. The label is painted (clipped - // to the cell) like `nav_button`. - let h = ui.spacing().interact_size.y; - let (rect, resp) = ui.allocate_exact_size(egui::vec2(LABEL_COL_W, h), egui::Sense::hover()); - let font = egui::TextStyle::Body.resolve(ui.style()); - let color = ui.visuals().widgets.noninteractive.fg_stroke.color; - ui.painter().with_clip_rect(rect).text( - egui::pos2(rect.left(), rect.center().y), - egui::Align2::LEFT_CENTER, - label, - font, - color, - ); - if !tooltip.is_empty() { - resp.on_hover_text(tooltip); - } - content(ui) - }) - .inner -} - -/// A labelled path row: a read-only [`TextEdit`] that FILLS the row up to a common right edge -/// (`available_width - BTN_COL_W`), followed by a "Browse…" button (and, when `clearable`, a ×) in -/// the reserved right-hand area. Because the input fills to a fixed offset from the right, every -/// path row in a section is identical width and every Browse lines up. When the button is pressed -/// `browse` produces a new path. A returned `Some` replaces `value`. Returns `true` when `value` -/// changed this frame. Shows a placeholder when empty. -pub fn path_row( - ui: &mut egui::Ui, - label: &str, - value: &mut Option, - tooltip: &str, - clearable: bool, - browse: impl FnOnce() -> Option, -) -> bool { - let mut changed = false; - form_row(ui, label, tooltip, |ui| { - // The input fills everything except the reserved right-hand button area, so it ends at a - // common x and the Browse button(s) line up across the section. - let input_w = (ui.available_width() - BTN_COL_W).max(80.0); - // Editable: the text is derived from `value` each frame and written straight back when the - // user types, so a path can be typed directly (for example naming an output folder - // "extracted") without the Browse dialog. Browse still fills it for those who prefer it. - let mut text = value - .as_ref() - .map(|p| p.display().to_string()) - .unwrap_or_default(); - let resp = ui.add( - egui::TextEdit::singleline(&mut text) - .hint_text("type a path or use Browse…") - .desired_width(input_w), - ); - if resp.changed() { - let t = text.trim(); - *value = if t.is_empty() { None } else { Some(PathBuf::from(t)) }; - changed = true; - } - if ui.button("Browse…").on_hover_text(tooltip).clicked() { - if let Some(picked) = browse() { - *value = Some(picked); - changed = true; - } - } - if clearable && value.is_some() && ui.button("×").on_hover_text("Clear").clicked() { - *value = None; - changed = true; - } - }); - changed -} - -/// A labelled text-input row: a `&mut String` editor that FILLS the whole remaining row width (no -/// trailing button), so every text row in a section is uniform full width. `hint` is the -/// placeholder shown when empty. Returns the input's `Response` so callers can attach a tooltip or -/// inspect `.changed()`. -pub fn text_row( - ui: &mut egui::Ui, - label: &str, - value: &mut String, - label_tooltip: &str, - hint: &str, -) -> egui::Response { - form_row(ui, label, label_tooltip, |ui| { - // `desired_width` (not `add_sized`) reliably makes the TextEdit fill the column. `add_sized` - // lets TextEdit's intrinsic content sizing win, which left filled rows narrower than empty ones. - let avail = ui.available_width().max(80.0); - ui.add(egui::TextEdit::singleline(value).hint_text(hint).desired_width(avail)) - }) -} - -/// A read-only path field followed by a "Browse…" button. Thin wrapper over [`path_row`] (clearable) -/// that sections call as their entry point. The layout is the shared aligned one. -pub fn path_field( - ui: &mut egui::Ui, - label: &str, - value: &mut Option, - tooltip: &str, - browse: impl FnOnce() -> Option, -) -> bool { - path_row(ui, label, value, tooltip, true, browse) -} - -/// A small, dimmed caption line, used for the "what this section will do" blurbs. -pub fn caption(ui: &mut egui::Ui, text: &str) { - ui.label(egui::RichText::new(text).weak()); -} - -/// The standard "coming soon" body for a not-yet-built section: a heading, a one-line description -/// of what the section will do, and the phase it lands in. Keeps the full nav shape visible now. -pub fn coming_soon(ui: &mut egui::Ui, phase: u8, description: &str) { - ui.add_space(8.0); - ui.heading("Coming soon"); - ui.add_space(4.0); - caption(ui, description); - ui.add_space(8.0); - ui.label( - egui::RichText::new(format!("Planned for Phase {phase}.")) - .italics() - .weak(), - ); -} diff --git a/util/wolfdawn/__init__.py b/util/wolfdawn/__init__.py index 4f0dfe2..a65d396 100644 --- a/util/wolfdawn/__init__.py +++ b/util/wolfdawn/__init__.py @@ -1,13 +1,15 @@ """WolfDawn CLI bootstrap and thin subprocess wrappers. -WolfDawn is the vendored Rust toolchain under ``util/wolfdawn-master`` that -unpacks, extracts, injects, and repacks WOLF RPG Editor game data. It ships as -source, so the ``wolf`` binary is built on demand with ``cargo build --release`` -the first time it is needed (mirrors the on-demand tool bootstrap in -``util/ace``). Everything the DazedMTLTool Wolf workflow needs goes through the -helpers here so command syntax and exit-code handling live in one place. +WolfDawn is the Rust toolchain (https://gitgud.io/zero64801/wolfdawn) that +unpacks, extracts, injects, and repacks WOLF RPG Editor game data. DazedMTLTool +ships prebuilt ``wolf`` binaries offline under ``util/wolfdawn/bin//`` +so end users never need a Rust toolchain. When no offline binary is bundled for +the running platform, the tool pulls a prebuilt one from the WolfDawn release +page and caches it into that same folder. Everything the Wolf workflow needs +goes through the helpers here so command syntax and exit-code handling live in +one place. -Exit codes emitted by ``wolf`` (see crates/wolf-cli/src/main.rs): +Exit codes emitted by ``wolf`` (see WolfDawn crates/wolf-cli/src/main.rs): 0 success 2 round-trip mismatch / inject guard / name-consistency failure 4 read / parse / write / crypto failure @@ -16,9 +18,14 @@ Exit codes emitted by ``wolf`` (see crates/wolf-cli/src/main.rs): from __future__ import annotations -import shutil +import io +import json +import re import subprocess import sys +import urllib.error +import urllib.request +import zipfile from dataclasses import dataclass from pathlib import Path from typing import Iterable, Optional, Sequence, Union @@ -26,9 +33,10 @@ from typing import Iterable, Optional, Sequence, Union __all__ = [ "WolfDawnError", "WolfResult", - "wolfdawn_source_dir", "wolf_binary_path", + "bundled_binary_path", "ensure_wolf_binary", + "download_wolf_binary", "unpack_all", "strings_extract", "names_extract", @@ -39,20 +47,35 @@ __all__ = [ "names_check", ] -# ``util/wolfdawn`` -> repo root is two parents up; the vendored source sits at -# ``util/wolfdawn-master``. -_PACKAGE_DIR = Path(__file__).resolve().parent -_UTIL_DIR = _PACKAGE_DIR.parent -_SOURCE_DIR = _UTIL_DIR / "wolfdawn-master" # Committed, prebuilt binaries live here (per-platform) so end users don't need a -# Rust toolchain. A build is only attempted when no bundled binary is present. +# Rust toolchain. When one is missing we download it from the WolfDawn release. +_PACKAGE_DIR = Path(__file__).resolve().parent _BUNDLED_DIR = _PACKAGE_DIR / "bin" +# WolfDawn upstream project on gitgud.io (a GitLab instance). The numeric project +# id is used for the uploads URL because the human-readable project path sits +# behind a Cloudflare challenge, while ``/-/project//uploads/...`` does not. +_GITGUD_HOST = "https://gitgud.io" +_WOLFDAWN_PROJECT_ID = 48753 +_RELEASES_API = f"{_GITGUD_HOST}/api/v4/projects/{_WOLFDAWN_PROJECT_ID}/releases" +# Substring that identifies each platform's prebuilt zip in a release. Only the +# assets the upstream maintainer publishes can be pulled; unpublished platforms +# fall back to the committed offline binary. +_RELEASE_ASSET_MATCH = { + "windows": "win", + "linux": "linux", + "macos": "mac", +} +_DOWNLOAD_UA = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "DazedMTLTool WolfDawn-fetch" +) + PathLike = Union[str, Path] class WolfDawnError(RuntimeError): - """Raised when the WolfDawn binary can't be built/located or a command fails hard.""" + """Raised when the WolfDawn binary can't be located/downloaded or a command fails hard.""" @dataclass @@ -77,11 +100,6 @@ class WolfResult: return self -def wolfdawn_source_dir() -> Path: - """Absolute path to the vendored WolfDawn Rust workspace.""" - return _SOURCE_DIR - - def _exe_name() -> str: return "wolf.exe" if sys.platform.startswith("win") else "wolf" @@ -94,101 +112,120 @@ def _platform_dir() -> str: return "linux" -def bundled_binary_path() -> Path: - """Committed prebuilt binary path for the current platform (may not exist).""" - return _BUNDLED_DIR / _platform_dir() / _exe_name() - - -def _built_binary_path() -> Path: - """Path of a locally cargo-built binary under the source tree's target/.""" - return _SOURCE_DIR / "target" / "release" / _exe_name() +def bundled_binary_path(platform: Optional[str] = None) -> Path: + """Committed/cached prebuilt binary path for a platform (may not exist).""" + platform = platform or _platform_dir() + exe = "wolf.exe" if platform == "windows" else "wolf" + return _BUNDLED_DIR / platform / exe def wolf_binary_path() -> Path: - """Preferred ``wolf`` binary path: bundled if present, else the built path.""" - bundled = bundled_binary_path() - if bundled.is_file(): - return bundled - return _built_binary_path() + """Preferred ``wolf`` binary path for the current platform (may not exist).""" + return bundled_binary_path() -def _persist_binary(src: Path, log_fn=None) -> Path: - """Copy a freshly built binary into the committed per-platform bundle dir.""" - dest = bundled_binary_path() +def _find_release_upload_url(platform: str) -> Optional[str]: + """Look up the newest release and return the download URL of ``platform``'s zip. + + WolfDawn attaches prebuilt zips to a release as GitLab *uploads* referenced + from the release description markdown, e.g. + ``[WolfDawn-win64.zip](/uploads//WolfDawn-win64.zip)``. Returns the + numeric-project uploads URL (which bypasses the Cloudflare challenge) or + ``None`` when the platform isn't published. + """ + match = _RELEASE_ASSET_MATCH.get(platform) + if not match: + return None + req = urllib.request.Request(_RELEASES_API, headers={"User-Agent": _DOWNLOAD_UA}) + with urllib.request.urlopen(req, timeout=30) as resp: + releases = json.loads(resp.read().decode("utf-8")) + link_re = re.compile(r"\[([^\]]+)\]\((/uploads/[0-9a-f]+/[^)]+)\)") + for release in releases: + description = release.get("description") or "" + for filename, upload_path in link_re.findall(description): + name = filename.lower() + if match in name and name.endswith(".zip"): + return f"{_GITGUD_HOST}/-/project/{_WOLFDAWN_PROJECT_ID}{upload_path}" + return None + + +def _extract_wolf_from_zip(zip_bytes: bytes, dest: Path, log_fn=None) -> Path: + """Extract the ``wolf``/``wolf.exe`` member from a release zip into ``dest``.""" + want = dest.name.lower() + with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: + member = None + for info in zf.infolist(): + base = info.filename.rsplit("/", 1)[-1].lower() + if base == want: + member = info + break + if member is None: + names = ", ".join(i.filename for i in zf.infolist()) + raise WolfDawnError( + f"WolfDawn release zip did not contain '{dest.name}' (found: {names})." + ) + payload = zf.read(member) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(payload) try: - dest.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(src, dest) dest.chmod(0o755) - _log(f"Saved WolfDawn binary to {dest}", log_fn) - return dest - except Exception as exc: # pragma: no cover - best effort persistence - _log(f"Warning: could not persist WolfDawn binary to {dest}: {exc}", log_fn) - return src + except OSError: # pragma: no cover - non-POSIX filesystems + pass + _log(f"Saved WolfDawn binary to {dest}", log_fn) + return dest + + +def download_wolf_binary(platform: Optional[str] = None, log_fn=print) -> Path: + """Pull the prebuilt ``wolf`` binary from the WolfDawn release and cache it. + + Downloads the platform's release zip, extracts the ``wolf`` executable into + ``util/wolfdawn/bin//``, and returns its path. Raises + :class:`WolfDawnError` when the platform has no published binary or the + download fails. + """ + platform = platform or _platform_dir() + dest = bundled_binary_path(platform) + _log(f"Looking up WolfDawn '{platform}' binary on {_GITGUD_HOST} ...", log_fn) + try: + url = _find_release_upload_url(platform) + except (urllib.error.URLError, TimeoutError, ValueError) as exc: + raise WolfDawnError( + f"Could not reach the WolfDawn release page: {exc}. " + f"Place a prebuilt binary at {dest} to work fully offline." + ) from exc + if not url: + raise WolfDawnError( + f"No prebuilt WolfDawn binary is published for '{platform}'. " + f"Place one at {dest}, or build it from source " + "(https://gitgud.io/zero64801/wolfdawn)." + ) + _log(f"Downloading WolfDawn binary from {url} ...", log_fn) + try: + req = urllib.request.Request(url, headers={"User-Agent": _DOWNLOAD_UA}) + with urllib.request.urlopen(req, timeout=120) as resp: + zip_bytes = resp.read() + except (urllib.error.URLError, TimeoutError) as exc: + raise WolfDawnError(f"Failed to download WolfDawn binary: {exc}") from exc + return _extract_wolf_from_zip(zip_bytes, dest, log_fn) def ensure_wolf_binary(force: bool = False, log_fn=print) -> Path: - """Return the ``wolf`` binary path, building it with cargo only if needed. + """Return the ``wolf`` binary path, downloading it only if needed. Resolution order: - 1. Committed, prebuilt binary under ``util/wolfdawn/bin//`` — used - directly so end users never need a Rust toolchain. - 2. A locally cargo-built binary under the source ``target/release``. - 3. Build from source with ``cargo build --release`` and persist the result - into the committed bundle dir for reuse. + 1. Committed/cached prebuilt binary under ``util/wolfdawn/bin//`` + — used directly so the tool works fully offline. + 2. Otherwise (or when ``force`` is set), pull the prebuilt binary from the + WolfDawn release page and cache it into that same folder. - Raises :class:`WolfDawnError` with actionable guidance when the source tree - is missing, ``cargo`` is not on PATH, or the build fails. + Raises :class:`WolfDawnError` with actionable guidance when no offline binary + is present and the download can't be completed. """ if not force: bundled = bundled_binary_path() if bundled.is_file(): return bundled - built = _built_binary_path() - if built.is_file(): - return _persist_binary(built, log_fn) - - if not _SOURCE_DIR.is_dir(): - raise WolfDawnError( - f"WolfDawn source not found at {_SOURCE_DIR}. The vendored " - "'wolfdawn-master' tree is required to build the 'wolf' CLI." - ) - - cargo = shutil.which("cargo") - if not cargo: - raise WolfDawnError( - "WolfDawn needs to be compiled but 'cargo' (the Rust toolchain) was " - "not found on PATH. Install Rust from https://rustup.rs/ and try " - "again, or place a prebuilt binary at " - f"{bundled_binary_path()}." - ) - - _log(f"Building WolfDawn 'wolf' binary (cargo build --release) in {_SOURCE_DIR} ...", log_fn) - try: - proc = subprocess.run( - [cargo, "build", "--release", "--bin", "wolf"], - cwd=str(_SOURCE_DIR), - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - except Exception as exc: # pragma: no cover - subprocess spawn failure - raise WolfDawnError(f"Failed to run cargo build for WolfDawn: {exc}") from exc - - if proc.returncode != 0: - tail = (proc.stderr or proc.stdout or "").strip().splitlines()[-20:] - raise WolfDawnError( - "cargo build --release failed for WolfDawn:\n" + "\n".join(tail) - ) - - built = _built_binary_path() - if not built.is_file(): - raise WolfDawnError( - f"cargo build reported success but {built} is missing. " - "Check the WolfDawn workspace layout." - ) - _log("WolfDawn 'wolf' binary ready", log_fn) - return _persist_binary(built, log_fn) + return download_wolf_binary(log_fn=log_fn) def _log(msg: str, log_fn) -> None: diff --git a/util/wolfdawn/bin/windows/wolf.exe b/util/wolfdawn/bin/windows/wolf.exe new file mode 100644 index 0000000000000000000000000000000000000000..25f470480f3a51de4c1b9682c1ce7571c3679ac4 GIT binary patch literal 2334720 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%0A=9|MStw!x(}xxb7u@{0Jd0V&t-iM9 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�doJ#`Ghx`)B&M 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ξSWry)|)pRUw6XY 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 z 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%0Al1O;z>R7-*Ro}WsE1Yky~h;Y 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:muruWcHHsTCIrLFu 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 literal 0 HcmV?d00001

IU4<7&s>@S$%a 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 z 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