aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 21:32:50 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 21:32:50 +0300
commit9cc24e715db81edbe21c0d04aadd00f11dddecb8 (patch)
tree08a1262dea86bcd7ec58c6494cedd001c45a78fe
parentf8e447ffee746cfe6580cc0e78a8a225aa39b546 (diff)
downloadfparkan-9cc24e715db81edbe21c0d04aadd00f11dddecb8.tar.xz
fparkan-9cc24e715db81edbe21c0d04aadd00f11dddecb8.zip
fix: close stage 0-2 synthetic gates
-rw-r--r--Cargo.lock2231
-rw-r--r--adapters/fparkan-platform-winit/src/lib.rs62
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs26
-rw-r--r--apps/fparkan-cli/Cargo.toml1
-rw-r--r--apps/fparkan-cli/src/main.rs32
-rw-r--r--apps/fparkan-game/Cargo.toml2
-rw-r--r--apps/fparkan-game/src/main.rs38
-rw-r--r--apps/fparkan-headless/src/main.rs19
-rw-r--r--apps/fparkan-viewer/src/main.rs29
-rw-r--r--crates/fparkan-animation/src/lib.rs19
-rw-r--r--crates/fparkan-assets/src/lib.rs309
-rw-r--r--crates/fparkan-binary/src/lib.rs19
-rw-r--r--crates/fparkan-corpus/src/lib.rs112
-rw-r--r--crates/fparkan-diagnostics/src/lib.rs26
-rw-r--r--crates/fparkan-fx/src/lib.rs19
-rw-r--r--crates/fparkan-inspection/src/lib.rs75
-rw-r--r--crates/fparkan-material/src/lib.rs19
-rw-r--r--crates/fparkan-mission-format/src/lib.rs19
-rw-r--r--crates/fparkan-msh/src/lib.rs21
-rw-r--r--crates/fparkan-nres/src/lib.rs19
-rw-r--r--crates/fparkan-path/src/lib.rs22
-rw-r--r--crates/fparkan-platform/src/lib.rs52
-rw-r--r--crates/fparkan-prototype/src/lib.rs131
-rw-r--r--crates/fparkan-render/src/lib.rs19
-rw-r--r--crates/fparkan-resource/src/lib.rs19
-rw-r--r--crates/fparkan-rsli/src/lib.rs99
-rw-r--r--crates/fparkan-runtime/src/lib.rs95
-rw-r--r--crates/fparkan-terrain-format/src/lib.rs19
-rw-r--r--crates/fparkan-terrain/src/lib.rs52
-rw-r--r--crates/fparkan-test-support/src/lib.rs19
-rw-r--r--crates/fparkan-texm/src/lib.rs19
-rw-r--r--crates/fparkan-vfs/src/lib.rs57
-rw-r--r--crates/fparkan-world/src/lib.rs19
-rw-r--r--fixtures/acceptance/coverage.tsv4
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md4
-rw-r--r--fparkan_stage_0_2_audit_2026-06-23.md1280
-rw-r--r--fparkan_stage_0_audit_2026-06-23.md643
-rw-r--r--xtask/src/main.rs109
38 files changed, 4030 insertions, 1729 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8297d80..0436d37 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,18 +3,293 @@
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 = "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",
+ "once_cell",
+ "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 = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
+
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
+[[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 = "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"
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2",
+]
+
+[[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"
+
+[[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-wayland-source"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
+dependencies = [
+ "calloop",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo-util-schemas"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dc1a6f7b5651af85774ae5a34b4e8be397d9cf4bc063b7e6dbd99a841837830"
+dependencies = [
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde-value",
+ "thiserror 2.0.18",
+ "toml",
+ "unicode-xid",
+ "url",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cfca2aaa699835ba88faf58a06342a314a950d2b9686165e038286c30316868"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "cargo-util-schemas",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[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 = "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-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",
+ "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",
+ "libc",
+]
+
+[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,6 +299,56 @@ dependencies = [
]
[[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 = "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 = "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 = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -33,6 +358,39 @@ dependencies = [
]
[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[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 = "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"
@@ -43,6 +401,42 @@ dependencies = [
]
[[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 = "fparkan-animation"
version = "0.1.0"
@@ -51,11 +445,14 @@ name = "fparkan-assets"
version = "0.1.0"
dependencies = [
"fparkan-material",
+ "fparkan-mission-format",
"fparkan-msh",
"fparkan-nres",
"fparkan-path",
"fparkan-prototype",
"fparkan-resource",
+ "fparkan-terrain",
+ "fparkan-terrain-format",
"fparkan-texm",
"fparkan-vfs",
]
@@ -68,11 +465,11 @@ version = "0.1.0"
name = "fparkan-cli"
version = "0.1.0"
dependencies = [
+ "fparkan-assets",
"fparkan-corpus",
- "fparkan-nres",
+ "fparkan-inspection",
"fparkan-prototype",
"fparkan-resource",
- "fparkan-rsli",
"fparkan-runtime",
"fparkan-vfs",
]
@@ -82,13 +479,25 @@ name = "fparkan-corpus"
version = "0.1.0"
dependencies = [
"fparkan-binary",
+ "fparkan-fx",
+ "fparkan-material",
+ "fparkan-mission-format",
+ "fparkan-msh",
"fparkan-nres",
"fparkan-path",
+ "fparkan-prototype",
+ "fparkan-rsli",
+ "fparkan-terrain-format",
+ "fparkan-texm",
]
[[package]]
name = "fparkan-diagnostics"
version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
[[package]]
name = "fparkan-fx"
@@ -102,7 +511,11 @@ dependencies = [
name = "fparkan-game"
version = "0.1.0"
dependencies = [
+ "fparkan-assets",
+ "fparkan-platform",
+ "fparkan-platform-winit",
"fparkan-render",
+ "fparkan-render-vulkan",
"fparkan-runtime",
"fparkan-vfs",
"fparkan-world",
@@ -118,6 +531,19 @@ dependencies = [
]
[[package]]
+name = "fparkan-inspection"
+version = "0.1.0"
+dependencies = [
+ "fparkan-msh",
+ "fparkan-nres",
+ "fparkan-resource",
+ "fparkan-rsli",
+ "fparkan-terrain-format",
+ "fparkan-texm",
+ "fparkan-vfs",
+]
+
+[[package]]
name = "fparkan-material"
version = "0.1.0"
dependencies = [
@@ -162,10 +588,11 @@ name = "fparkan-platform"
version = "0.1.0"
[[package]]
-name = "fparkan-platform-sdl"
+name = "fparkan-platform-winit"
version = "0.1.0"
dependencies = [
"fparkan-platform",
+ "winit",
]
[[package]]
@@ -174,12 +601,9 @@ version = "0.1.0"
dependencies = [
"encoding_rs",
"fparkan-binary",
- "fparkan-material",
- "fparkan-msh",
"fparkan-nres",
"fparkan-path",
"fparkan-resource",
- "fparkan-texm",
"fparkan-vfs",
]
@@ -191,9 +615,10 @@ dependencies = [
]
[[package]]
-name = "fparkan-render-gl"
+name = "fparkan-render-vulkan"
version = "0.1.0"
dependencies = [
+ "fparkan-platform",
"fparkan-render",
]
@@ -219,15 +644,12 @@ dependencies = [
name = "fparkan-runtime"
version = "0.1.0"
dependencies = [
- "fparkan-mission-format",
- "fparkan-nres",
+ "fparkan-assets",
"fparkan-path",
"fparkan-platform",
"fparkan-prototype",
"fparkan-render",
"fparkan-resource",
- "fparkan-terrain",
- "fparkan-terrain-format",
"fparkan-vfs",
"fparkan-world",
]
@@ -274,14 +696,8 @@ dependencies = [
name = "fparkan-viewer"
version = "0.1.0"
dependencies = [
- "fparkan-msh",
- "fparkan-nres",
+ "fparkan-inspection",
"fparkan-render",
- "fparkan-resource",
- "fparkan-rsli",
- "fparkan-terrain-format",
- "fparkan-texm",
- "fparkan-vfs",
]
[[package]]
@@ -292,6 +708,329 @@ dependencies = [
]
[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[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-task",
+ "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",
+ "wasip2",
+]
+
+[[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 = "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 = "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",
+ "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 = "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 = "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 = "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 = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -302,14 +1041,1470 @@ dependencies = [
]
[[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 = "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-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[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",
+ "objc2",
+ "objc2-core-location",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[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",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "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",
+ "objc2",
+ "objc2-contacts",
+ "objc2-foundation",
+]
+
+[[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",
+ "dispatch",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
+[[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",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[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",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[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",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation",
+ "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",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[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",
+ "objc2",
+ "objc2-core-location",
+ "objc2-foundation",
+]
+
+[[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-float"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
+
+[[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 = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[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 = "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 = "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 = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.12+spec-1.1.0",
+]
+
+[[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 = "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.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+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 = "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.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
+dependencies = [
+ "bitflags 2.13.0",
+]
+
+[[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 = "sctk-adwaita"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
+dependencies = [
+ "ab_glyph",
+ "log",
+ "memmap2",
+ "smithay-client-toolkit",
+ "tiny-skia",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[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-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
+[[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_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[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 = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[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",
+ "calloop-wayland-source",
+ "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 = "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 = "strict-num"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+
+[[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 = "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 = "tiny-skia"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "log",
+ "tiny-skia-path",
+]
+
+[[package]]
+name = "tiny-skia-path"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
+
+[[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"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime 0.6.11",
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[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.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime 0.6.11",
+ "toml_write",
+ "winnow 0.7.15",
+]
+
+[[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 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.3",
+]
+
+[[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 1.0.3",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[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 = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[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-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 = "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 = "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",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[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",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[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_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[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_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[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_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[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",
+ "bytemuck",
+ "calloop",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "sctk-adwaita",
+ "smithay-client-toolkit",
+ "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 = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[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 = "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 = "xtask"
version = "0.1.0"
dependencies = [
+ "cargo_metadata",
"fparkan-corpus",
+ "serde",
+ "toml",
+]
+
+[[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 = "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"
diff --git a/adapters/fparkan-platform-winit/src/lib.rs b/adapters/fparkan-platform-winit/src/lib.rs
index ec30908..496d955 100644
--- a/adapters/fparkan-platform-winit/src/lib.rs
+++ b/adapters/fparkan-platform-winit/src/lib.rs
@@ -1,15 +1,34 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Minimal `winit`-backed platform adapter shim.
use fparkan_platform::{
- EventSource, MonotonicClock, MonotonicInstant, PlatformEvent, PlatformError, PhysicalSize,
+ EventSource, MonotonicClock, MonotonicInstant, PhysicalSize, PlatformError, PlatformEvent,
RenderRequest, WindowHandle, WindowPort,
};
-use winit::event::{MouseButton, WindowEvent};
-use winit::event_loop::Event;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
+use winit::event::{Event, MouseButton, WindowEvent};
+use winit::platform::scancode::PhysicalKeyExtScancode;
use winit::window::Window;
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
@@ -52,7 +71,7 @@ impl WinitEventSource {
}
/// Pushes a mapped native window event.
- pub fn push_window_event(&mut self, event: &WindowEvent<'_>) {
+ pub fn push_window_event(&mut self, event: &WindowEvent) {
match event {
WindowEvent::KeyboardInput { event, .. } => {
self.queue.push_back(PlatformEvent::KeyboardInput {
@@ -81,14 +100,13 @@ impl WinitEventSource {
});
}
WindowEvent::Focused(focused) => {
- self.queue.push_back(PlatformEvent::FocusChanged { focused: *focused });
- }
- WindowEvent::ScaleFactorChanged {
- scale_factor,
- ..
- } => {
self.queue
- .push_back(PlatformEvent::DpiChanged { scale: *scale_factor });
+ .push_back(PlatformEvent::FocusChanged { focused: *focused });
+ }
+ WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
+ self.queue.push_back(PlatformEvent::DpiChanged {
+ scale: *scale_factor,
+ });
}
WindowEvent::CloseRequested => {
self.queue.push_back(PlatformEvent::QuitRequested);
@@ -98,7 +116,7 @@ impl WinitEventSource {
}
/// Pushes events from an event loop event.
- pub fn push_event<T>(&mut self, event: &Event<'_, T>) {
+ pub fn push_event<T>(&mut self, event: &Event<T>) {
if let Event::WindowEvent { event, .. } = event {
self.push_window_event(event);
}
@@ -112,7 +130,7 @@ fn mouse_button_code(button: MouseButton) -> u16 {
MouseButton::Middle => 2,
MouseButton::Back => 3,
MouseButton::Forward => 4,
- MouseButton::Other(index) => 100 + u16::try_from(index).unwrap_or(0),
+ MouseButton::Other(index) => 100 + index,
}
}
@@ -219,7 +237,10 @@ mod tests {
source.push(PlatformEvent::QuitRequested);
let mut events = Vec::new();
source.poll(&mut events)?;
- assert_eq!(events, vec![PlatformEvent::Resumed, PlatformEvent::QuitRequested]);
+ assert_eq!(
+ events,
+ vec![PlatformEvent::Resumed, PlatformEvent::QuitRequested]
+ );
Ok(())
}
@@ -227,8 +248,17 @@ mod tests {
fn window_port_reports_default_request_profile() {
let window = WinitWindow::synthetic(640, 360);
let request = WinitWindow::default_render_request();
- assert_eq!(request.presentation, fparkan_platform::PresentationMode::Fifo);
- assert_eq!(window.drawable_size(), PhysicalSize { width: 640, height: 360 });
+ assert_eq!(
+ request.presentation,
+ fparkan_platform::PresentationMode::Fifo
+ );
+ assert_eq!(
+ window.drawable_size(),
+ PhysicalSize {
+ width: 640,
+ height: 360
+ }
+ );
}
#[test]
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index 3d4f44d..6cae797 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![deny(unsafe_op_in_unsafe_fn)]
//! Vulkan adapter facade and migration-ready backend surface contract.
//!
@@ -8,10 +27,10 @@
//!
//! This crate is the declared low-level Vulkan boundary.
+use fparkan_platform::RenderRequest;
use fparkan_render::{
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
};
-use fparkan_platform::RenderRequest;
use std::time::{SystemTime, UNIX_EPOCH};
/// Vulkan backend migration readiness.
@@ -120,7 +139,10 @@ impl VulkanBackend {
impl RenderBackend for VulkanBackend {
fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- if !matches!(self.state, VulkanBackendState::Ready | VulkanBackendState::Degraded) {
+ if !matches!(
+ self.state,
+ VulkanBackendState::Ready | VulkanBackendState::Degraded
+ ) {
return Err(RenderError::InvalidRange);
}
let capture = canonical_capture(commands)?;
diff --git a/apps/fparkan-cli/Cargo.toml b/apps/fparkan-cli/Cargo.toml
index 90b26da..76d319a 100644
--- a/apps/fparkan-cli/Cargo.toml
+++ b/apps/fparkan-cli/Cargo.toml
@@ -6,6 +6,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
+fparkan-assets = { path = "../../crates/fparkan-assets" }
fparkan-corpus = { path = "../../crates/fparkan-corpus" }
fparkan-prototype = { path = "../../crates/fparkan-prototype" }
fparkan-inspection = { path = "../../crates/fparkan-inspection" }
diff --git a/apps/fparkan-cli/src/main.rs b/apps/fparkan-cli/src/main.rs
index 043a21c..b86486c 100644
--- a/apps/fparkan-cli/src/main.rs
+++ b/apps/fparkan-cli/src/main.rs
@@ -1,11 +1,30 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` command-line tools.
+use fparkan_assets::extend_graph_report_with_visual_dependencies;
use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
use fparkan_inspection::inspect_archive_file;
use fparkan_inspection::ArchiveInspection;
-use fparkan_assets::extend_graph_report_with_visual_dependencies;
use fparkan_prototype::build_prototype_graph_report;
use fparkan_resource::{resource_name, CachedResourceRepository};
use fparkan_runtime::{
@@ -135,12 +154,7 @@ fn inspect_prototype(args: &[String]) -> Result<(), String> {
let roots = [resource_name(key.as_bytes())];
let (graph, resolved, mut report) =
build_prototype_graph_report(&repository, vfs.as_ref(), &roots);
- extend_graph_report_with_visual_dependencies(
- &repository,
- &mut report,
- &graph,
- &resolved,
- );
+ extend_graph_report_with_visual_dependencies(&repository, &mut report, &graph, &resolved);
println!("{}", prototype_inspect_json(&key, &graph, &report));
Ok(())
}
@@ -234,7 +248,9 @@ fn inspect_archive(args: &[String]) -> Result<(), String> {
);
Ok(())
}
- ArchiveInspection::Unsupported => Err(format!("{}: unsupported archive magic", path.display())),
+ ArchiveInspection::Unsupported => {
+ Err(format!("{}: unsupported archive magic", path.display()))
+ }
}
}
diff --git a/apps/fparkan-game/Cargo.toml b/apps/fparkan-game/Cargo.toml
index bac3397..a134a8b 100644
--- a/apps/fparkan-game/Cargo.toml
+++ b/apps/fparkan-game/Cargo.toml
@@ -6,6 +6,8 @@ license.workspace = true
repository.workspace = true
[dependencies]
+fparkan-assets = { path = "../../crates/fparkan-assets" }
+fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-render = { path = "../../crates/fparkan-render" }
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index 7ea7d0e..6f132e5 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -1,16 +1,37 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` rendered game composition root.
+use fparkan_assets::PreparedVisual;
+use fparkan_platform::WindowPort;
+use fparkan_platform_winit::WinitWindow;
use fparkan_render::{
- DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend,
- RenderCommand, RenderCommandList, RenderPhase,
+ DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend, RenderCommand,
+ RenderCommandList, RenderPhase,
};
-use fparkan_platform_winit::WinitWindow;
use fparkan_render_vulkan::VulkanBackend;
use fparkan_runtime::{
- create, frame, load_mission, EngineConfig, EngineMode, EngineServices, MissionRequest,
- MissionAssets, loaded_mission_assets,
+ create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
+ MissionAssets, MissionRequest,
};
use fparkan_vfs::DirectoryVfs;
use fparkan_world::WorldSnapshot;
@@ -89,6 +110,7 @@ fn run(args: &[String]) -> Result<String, String> {
))
}
+#[cfg(test)]
fn render_snapshot_commands(snapshot: &WorldSnapshot) -> RenderCommandList {
render_snapshot_commands_with_assets(snapshot, None)
}
@@ -115,8 +137,10 @@ fn render_snapshot_commands_with_assets(
GpuMeshId(u64::from(handle.slot) + 1)
};
let material = prepared
- .and_then(|visual| visual.primary_material_id())
- .map_or(GpuMaterialId(1), |material_id| GpuMaterialId(material_id.raw()));
+ .and_then(PreparedVisual::primary_material_id)
+ .map_or(GpuMaterialId(1), |material_id| {
+ GpuMaterialId(material_id.raw())
+ });
let draw_id = snapshot
.tick
.0
diff --git a/apps/fparkan-headless/src/main.rs b/apps/fparkan-headless/src/main.rs
index b78a7dc..c28b51b 100644
--- a/apps/fparkan-headless/src/main.rs
+++ b/apps/fparkan-headless/src/main.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` headless runtime entrypoint.
diff --git a/apps/fparkan-viewer/src/main.rs b/apps/fparkan-viewer/src/main.rs
index ee96ab5..783ef92 100644
--- a/apps/fparkan-viewer/src/main.rs
+++ b/apps/fparkan-viewer/src/main.rs
@@ -1,10 +1,29 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` asset viewer composition root.
use fparkan_inspection::{
- inspect_land_file, inspect_model_from_root, inspect_texture_from_root, ArchiveInspection, LandFileKind,
- MapInspection, NresEntrySummary,
+ inspect_land_file, inspect_model_from_root, inspect_texture_from_root, ArchiveInspection,
+ LandFileKind, MapInspection, NresEntrySummary,
};
use fparkan_render::{
build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderPhase,
@@ -151,7 +170,11 @@ fn inspect_map(args: &[String]) -> Result<String, String> {
},
)?;
- Ok(render_map_inspection_json(&file.display().to_string(), &kind, &inspection))
+ Ok(render_map_inspection_json(
+ &file.display().to_string(),
+ &kind,
+ &inspection,
+ ))
}
fn render_map_inspection_json(path: &str, kind: &str, inspection: &MapInspection) -> String {
diff --git a/crates/fparkan-animation/src/lib.rs b/crates/fparkan-animation/src/lib.rs
index 53111f3..7903f88 100644
--- a/crates/fparkan-animation/src/lib.rs
+++ b/crates/fparkan-animation/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![allow(clippy::cast_precision_loss)]
//! Deterministic animation sampling contracts.
//!
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index f4501ee..0cbd3d4 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1,15 +1,31 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Asset manager ports and transactional preparation models.
use fparkan_material::{decode_wear, resolve_material, MaterialError, WEAR_KIND};
+use fparkan_mission_format::{decode_tma, decode_tma_land_path};
+pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
use fparkan_msh::{decode_msh, validate_msh, MshError};
-pub use fparkan_nres::{NresDocument, NresError};
use fparkan_nres::{decode as decode_nres, ReadProfile};
-pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
-pub use fparkan_terrain::{TerrainError, TerrainWorld};
-pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
-use fparkan_mission_format::{decode_tma, decode_tma_land_path};
-use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
+pub use fparkan_nres::{NresDocument, NresError};
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy, ResourceName};
use fparkan_prototype::{
EffectivePrototype, PrototypeGeometry, PrototypeGraph, PrototypeGraphEdge,
@@ -17,6 +33,9 @@ use fparkan_prototype::{
PrototypeGraphRequiredness,
};
use fparkan_resource::{ResourceError, ResourceKey, ResourceRepository};
+pub use fparkan_terrain::{TerrainError, TerrainWorld};
+use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
+pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
use fparkan_texm::{decode_texm, TexmError};
use std::collections::{HashMap, HashSet};
use std::fmt;
@@ -27,7 +46,8 @@ use std::sync::Arc;
const TEXTURES_ARCHIVE: &str = "textures.lib";
const LIGHTMAP_ARCHIVE: &str = "lightmap.lib";
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+/// Canonical terrain archive paths derived from a mission land reference.
+#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MissionTerrainPaths {
/// Landscape mesh archive path.
pub land_msh: NormalizedPath,
@@ -68,6 +88,11 @@ impl From<TerrainError> for TerrainPreparationError {
}
/// Decodes a mission file bytes payload with a typed profile.
+///
+/// # Errors
+///
+/// Returns [`MissionError`] when the mission payload is malformed for the
+/// selected profile.
pub fn decode_mission_payload(
bytes: Arc<[u8]>,
profile: TmaProfile,
@@ -76,6 +101,11 @@ pub fn decode_mission_payload(
}
/// Reads only the mission land path from raw TMA bytes.
+///
+/// # Errors
+///
+/// Returns [`MissionError`] when the mission header or land path record cannot
+/// be decoded.
pub fn decode_mission_land_path(
bytes: &[u8],
profile: TmaProfile,
@@ -84,21 +114,32 @@ pub fn decode_mission_land_path(
}
/// Builds canonical mission terrain paths from the mission `Land` reference.
-pub fn derive_mission_land_paths(
- land_path: &LpString,
-) -> Result<MissionTerrainPaths, PathError> {
+///
+/// # Errors
+///
+/// Returns [`PathError`] when the mission land reference is not a strict
+/// relative legacy path.
+pub fn derive_mission_land_paths(land_path: &LpString) -> Result<MissionTerrainPaths, PathError> {
let normalized = normalize_relative(&land_path.raw, PathPolicy::StrictLegacy)?;
let Some((parent, _stem)) = normalized.as_str().rsplit_once('/') else {
return Err(PathError::Empty);
};
- let land_msh =
- normalize_relative(format!("{parent}/Land.msh").as_bytes(), PathPolicy::StrictLegacy)?;
- let land_map =
- normalize_relative(format!("{parent}/Land.map").as_bytes(), PathPolicy::StrictLegacy)?;
+ let land_msh = normalize_relative(
+ format!("{parent}/Land.msh").as_bytes(),
+ PathPolicy::StrictLegacy,
+ )?;
+ let land_map = normalize_relative(
+ format!("{parent}/Land.map").as_bytes(),
+ PathPolicy::StrictLegacy,
+ )?;
Ok(MissionTerrainPaths { land_msh, land_map })
}
-/// Decodes compatible NRes payload for terrain/document loading.
+/// Decodes compatible `NRes` payload for terrain/document loading.
+///
+/// # Errors
+///
+/// Returns [`NresError`] when the payload is not a compatible `NRes` archive.
pub fn decode_nres_payload(
bytes: Arc<[u8]>,
) -> Result<fparkan_nres::NresDocument, fparkan_nres::NresError> {
@@ -106,6 +147,11 @@ pub fn decode_nres_payload(
}
/// Decodes terrain documents and builds immutable terrain state.
+///
+/// # Errors
+///
+/// Returns [`TerrainPreparationError`] when terrain documents are malformed or
+/// cannot be converted into runtime terrain state.
pub fn prepare_terrain_world(
land_msh_nres: &fparkan_nres::NresDocument,
land_map_nres: &fparkan_nres::NresDocument,
@@ -119,12 +165,34 @@ pub fn prepare_terrain_world(
}
/// Stable typed identifier for a prepared asset.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+#[derive(Debug)]
pub struct AssetId<T> {
raw: u64,
marker: PhantomData<T>,
}
+impl<T> Clone for AssetId<T> {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl<T> Copy for AssetId<T> {}
+
+impl<T> PartialEq for AssetId<T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.raw == other.raw
+ }
+}
+
+impl<T> Eq for AssetId<T> {}
+
+impl<T> Hash for AssetId<T> {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.raw.hash(state);
+ }
+}
+
impl<T> AssetId<T> {
/// Creates an asset id from a stable raw value.
#[must_use]
@@ -183,7 +251,7 @@ impl PreparedVisual {
}
/// Immutable prepared mission assets for rendering and game setup.
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MissionAssets {
/// Visuals prepared for all reachable prototype requests.
pub visuals: Vec<PreparedVisual>,
@@ -200,10 +268,7 @@ impl MissionAssets {
/// Returns all visuals for a mission object index.
#[must_use]
- pub fn visuals_for_object(
- &self,
- object_index: usize,
- ) -> &[AssetId<PreparedVisual>] {
+ pub fn visuals_for_object(&self, object_index: usize) -> &[AssetId<PreparedVisual>] {
self.object_visuals
.get(object_index)
.map_or(&[], |values| values.as_slice())
@@ -211,10 +276,7 @@ impl MissionAssets {
/// Returns the first visual for a mission object index.
#[must_use]
- pub fn visual_for_object(
- &self,
- object_index: usize,
- ) -> Option<AssetId<PreparedVisual>> {
+ pub fn visual_for_object(&self, object_index: usize) -> Option<AssetId<PreparedVisual>> {
self.visuals_for_object(object_index).first().copied()
}
@@ -238,11 +300,7 @@ impl MissionAssets {
.iter()
.map(|visual| visual.material_count)
.sum();
- let texture_count = self
- .visuals
- .iter()
- .map(|visual| visual.texture_count)
- .sum();
+ let texture_count = self.visuals.iter().map(|visual| visual.texture_count).sum();
let lightmap_count = self
.visuals
.iter()
@@ -258,15 +316,6 @@ impl MissionAssets {
}
}
-impl Default for MissionAssets {
- fn default() -> Self {
- Self {
- visuals: Vec::new(),
- object_visuals: Vec::new(),
- }
- }
-}
-
/// A transactional mission asset preparation plan.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MissionAssetPlan {
@@ -301,7 +350,7 @@ pub enum AssetError {
/// Human context for the operation.
context: String,
/// Concrete repository source error.
- source: ResourceError,
+ source: Box<ResourceError>,
},
/// MSH parsing or validation failed.
Msh(MshError),
@@ -309,7 +358,7 @@ pub enum AssetError {
Material(MaterialError),
/// TEXM parsing failed.
Texture(TexmError),
- /// NRes decoding failed.
+ /// `NRes` decoding failed.
Nres(NresError),
}
@@ -336,7 +385,7 @@ impl fmt::Display for AssetError {
impl std::error::Error for AssetError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
- Self::Resource { source, .. } => Some(source),
+ Self::Resource { source, .. } => Some(source.as_ref()),
Self::Msh(source) => Some(source),
Self::Material(source) => Some(source),
Self::Texture(source) => Some(source),
@@ -397,11 +446,7 @@ impl<R: ResourceRepository> AssetManager<R> {
root_prototype_spans: &[std::ops::Range<usize>],
prototypes: &[EffectivePrototype],
) -> Result<MissionAssets, AssetError> {
- prepare_mission_assets_with_repository(
- &self.repository,
- root_prototype_spans,
- prototypes,
- )
+ prepare_mission_assets_with_repository(&self.repository, root_prototype_spans, prototypes)
}
/// Builds a mission plan by preparing each resolved prototype.
@@ -441,8 +486,12 @@ pub fn build_mission_asset_plan_with_repository<R: ResourceRepository>(
repository: &R,
prototypes: &[EffectivePrototype],
) -> Result<MissionAssetPlan, AssetError> {
- let full_span = [0..prototypes.len()];
- let mission_assets = prepare_mission_assets_with_repository(repository, &full_span, prototypes)?;
+ let full_span = 0..prototypes.len();
+ let mission_assets = prepare_mission_assets_with_repository(
+ repository,
+ std::slice::from_ref(&full_span),
+ prototypes,
+ )?;
Ok(mission_assets.to_plan())
}
@@ -461,13 +510,12 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>(
}
let mut visual_index_by_id: HashMap<AssetId<PreparedVisual>, PreparedVisualSignature> =
HashMap::new();
- let mut material_signature_by_id: HashMap<AssetId<PreparedMaterial>, Vec<u8>> =
- HashMap::new();
+ let mut material_signature_by_id: HashMap<AssetId<PreparedMaterial>, Vec<u8>> = HashMap::new();
let mut visuals = Vec::new();
let mut prototype_visual_ids = Vec::with_capacity(prototypes.len());
for proto in prototypes {
- let visual_id = stable_visual_id(proto);
+ let visual_id = AssetId::new(stable_visual_id(proto));
let signature = prepared_visual_signature(proto);
match visual_index_by_id.get(&visual_id) {
Some(existing) if existing != &signature => {
@@ -571,7 +619,7 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
report.wear_resolved_count += 1;
report.material_slot_count += table.entries.len();
for (material_index, _entry) in table.entries.iter().enumerate() {
- let Ok(material_index) = u16::try_from(material_index) else {
+ let Ok(material_index) = u16::try_from(material_index) else {
push_visual_failure(
report,
graph,
@@ -594,15 +642,18 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
[texture_archive.as_ref(), lightmap_archive.as_ref()],
) {
Ok(()) => report.texture_resolved_count += 1,
- Err(message) => push_visual_failure(
- report,
- graph,
- prototype_index,
- texture.0,
- PrototypeGraphEdge::MaterialToTexture,
- PrototypeGraphRequiredness::Required,
- &message,
- ),
+ Err(message) => {
+ let message = message.to_string();
+ push_visual_failure(
+ report,
+ graph,
+ prototype_index,
+ texture.0,
+ PrototypeGraphEdge::MaterialToTexture,
+ PrototypeGraphRequiredness::Required,
+ &message,
+ );
+ }
}
}
}
@@ -615,7 +666,7 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
PrototypeGraphRequiredness::Required,
&message.to_string(),
),
- }
+ }
}
for lightmap in &table.lightmaps {
report.lightmap_request_count += 1;
@@ -625,15 +676,18 @@ pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
[lightmap_archive.as_ref(), texture_archive.as_ref()],
) {
Ok(()) => report.lightmap_resolved_count += 1,
- Err(message) => push_visual_failure(
- report,
- graph,
- prototype_index,
- lightmap.lightmap.0.clone(),
- PrototypeGraphEdge::WearToLightmap,
- PrototypeGraphRequiredness::Required,
- &message,
- ),
+ Err(message) => {
+ let message = message.to_string();
+ push_visual_failure(
+ report,
+ graph,
+ prototype_index,
+ lightmap.lightmap.0.clone(),
+ PrototypeGraphEdge::WearToLightmap,
+ PrototypeGraphRequiredness::Required,
+ &message,
+ );
+ }
}
}
}
@@ -693,7 +747,7 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>(
fn prepare_visual_with_repository_internal<R: ResourceRepository>(
repository: &R,
proto: &EffectivePrototype,
- material_signature_by_id: Option<&mut HashMap<AssetId<PreparedMaterial>, Vec<u8>>>,
+ mut material_signature_by_id: Option<&mut HashMap<AssetId<PreparedMaterial>, Vec<u8>>>,
) -> Result<PreparedVisual, AssetError> {
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
return prepare_visual(proto);
@@ -713,7 +767,8 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
name: wear_name,
type_id: Some(WEAR_KIND),
};
- let wear = decode_wear(&read_key(repository, &wear_key, Some("wear"))?).map_err(AssetError::Material)?;
+ let wear = decode_wear(&read_key(repository, &wear_key, Some("wear"))?)
+ .map_err(AssetError::Material)?;
let mut material_count = 0;
let mut material_ids = Vec::with_capacity(wear.entries.len());
@@ -723,18 +778,12 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
let material_index = u16::try_from(material_index).map_err(|_| {
AssetError::InvalidPrototype("material index does not fit archive format".to_string())
})?;
- let material = resolve_material(repository, &wear, material_index)
- .map_err(AssetError::Material)?;
+ let material =
+ resolve_material(repository, &wear, material_index).map_err(AssetError::Material)?;
material_count += 1;
- material_ids.push(AssetId::new(stable_material_id(
- proto,
- material_index,
- &material.name,
- )));
- let material_id = *material_ids
- .last()
- .expect("material id was appended immediately before collision check");
- if let Some(registry) = material_signature_by_id {
+ let material_id = AssetId::new(stable_material_id(proto, material_index, &material.name));
+ material_ids.push(material_id);
+ if let Some(registry) = material_signature_by_id.as_deref_mut() {
match registry.get(&material_id) {
Some(existing_name) => {
if existing_name != &material.name.0 {
@@ -750,7 +799,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
}
for texture in material.document.texture_requests() {
- resolve_texture(repository, &texture)?;
+ resolve_texture(repository, &texture)?;
texture_count += 1;
}
}
@@ -779,14 +828,12 @@ fn read_key<R: ResourceRepository>(
label: Option<&str>,
) -> Result<Arc<[u8]>, AssetError> {
let label = label.unwrap_or("asset");
- let handle = repository
+ let archive = repository
.open_archive(&key.archive)
+ .map_err(|err| map_resource_error(label, key, err))?;
+ let handle = repository
+ .find(archive, &key.name)
.map_err(|err| map_resource_error(label, key, err))?
- .and_then(|archive| {
- repository
- .find(archive, &key.name)
- .map_err(|err| map_resource_error(label, key, err))
- })?
.ok_or_else(|| AssetError::MissingDependency(format!("{label}: {key:?}")))?;
let bytes = repository
.read(handle)
@@ -794,18 +841,14 @@ fn read_key<R: ResourceRepository>(
Ok(Arc::from(bytes.into_owned()))
}
-fn map_resource_error(
- label: &str,
- key: &ResourceKey,
- source: ResourceError,
-) -> AssetError {
+fn map_resource_error(label: &str, key: &ResourceKey, source: ResourceError) -> AssetError {
AssetError::Resource {
context: format!(
"{label}: archive={} entry={}",
key.archive.as_str(),
String::from_utf8_lossy(&key.name.0),
),
- source,
+ source: Box::new(source),
}
}
@@ -836,19 +879,17 @@ fn resolve_wear_table<R: ResourceRepository>(
String::from_utf8_lossy(&wear_name.0)
))
})?;
- let info = repository
- .entry_info(handle)
- .map_err(|err| {
- map_resource_error(
- "wear",
- &ResourceKey {
- archive: mesh.archive.clone(),
- name: wear_name.clone(),
- type_id: Some(WEAR_KIND),
- },
- err,
- )
- })?;
+ let info = repository.entry_info(handle).map_err(|err| {
+ map_resource_error(
+ "wear",
+ &ResourceKey {
+ archive: mesh.archive.clone(),
+ name: wear_name.clone(),
+ type_id: Some(WEAR_KIND),
+ },
+ err,
+ )
+ })?;
if info.key.type_id != Some(WEAR_KIND) {
return Err(AssetError::InvalidPrototype(format!(
"entry {} is not WEAR",
@@ -902,7 +943,7 @@ fn resolve_texm_from_candidates<'a, R: ResourceRepository>(
.read(handle)
.map_err(|err| map_resource_error("texm", &key, err))?
.into_owned();
- decode_texm(bytes).map_err(AssetError::Texture)?;
+ decode_texm(Arc::from(bytes)).map_err(AssetError::Texture)?;
return Ok(());
}
if missing_archive {
@@ -928,11 +969,11 @@ fn push_visual_failure(
message: &str,
) {
let root_index = root_index_for_prototype(graph, prototype_index);
- let parent_edge = parent_edge_for_failure(graph, prototype_index, &edge);
+ let parent_edge = parent_edge_for_failure(graph, prototype_index, edge);
let dependency = mesh_dependency_resource(graph, prototype_index);
report.failures.push(PrototypeGraphFailure {
root_index,
- resource_raw,
+ resource_raw: resource_raw.clone(),
edge,
message: message.to_string(),
requiredness,
@@ -943,7 +984,7 @@ fn push_visual_failure(
resource: Some(resource_raw),
span: None,
}),
- })
+ });
}
fn root_index_for_prototype(graph: &PrototypeGraph, prototype_index: usize) -> usize {
@@ -958,21 +999,23 @@ fn root_index_for_prototype(graph: &PrototypeGraph, prototype_index: usize) -> u
fn parent_edge_for_failure(
graph: &PrototypeGraph,
prototype_index: usize,
- edge: &PrototypeGraphEdge,
+ edge: PrototypeGraphEdge,
) -> Option<fparkan_prototype::PrototypeGraphEdgeId> {
let prototype_node_id = prototype_node_id(graph, prototype_index)?;
match edge {
PrototypeGraphEdge::MeshToWear
| PrototypeGraphEdge::WearToMaterial
| PrototypeGraphEdge::MaterialToTexture
- | PrototypeGraphEdge::WearToLightmap => {
- mesh_edge_id(graph, prototype_node_id).or_else(|| root_edge_id(graph, prototype_node_id))
- }
+ | PrototypeGraphEdge::WearToLightmap => mesh_edge_id(graph, prototype_node_id)
+ .or_else(|| root_edge_id(graph, prototype_node_id)),
_ => root_edge_id(graph, prototype_node_id),
}
}
-fn prototype_node_id(graph: &PrototypeGraph, prototype_index: usize) -> Option<fparkan_prototype::PrototypeGraphNodeId> {
+fn prototype_node_id(
+ graph: &PrototypeGraph,
+ prototype_index: usize,
+) -> Option<fparkan_prototype::PrototypeGraphNodeId> {
graph
.nodes
.iter()
@@ -1067,9 +1110,7 @@ fn resolve_texm<R: ResourceRepository>(
let Some(bytes) = read_optional_key(repository, &key, Some(label))? else {
return Err(AssetError::MissingDependency(format!("{label} {name:?}")));
};
- decode_texm(bytes)
- .map(|_| ())
- .map_err(AssetError::Texture)
+ decode_texm(bytes).map(|_| ()).map_err(AssetError::Texture)
}
fn read_optional_key<R: ResourceRepository>(
@@ -1082,24 +1123,20 @@ fn read_optional_key<R: ResourceRepository>(
Err(ResourceError::MissingArchive | ResourceError::MissingEntry) => return Ok(None),
Err(err) => {
let label = label.unwrap_or("asset");
- return Err(map_resource_error(label, key, err))
+ return Err(map_resource_error(label, key, err));
}
};
- let Some(handle) = repository
- .find(archive, &key.name)
- .map_err(|err| {
- let label = label.unwrap_or("asset");
- map_resource_error(label, key, err)
- })?
+ let Some(handle) = repository.find(archive, &key.name).map_err(|err| {
+ let label = label.unwrap_or("asset");
+ map_resource_error(label, key, err)
+ })?
else {
return Ok(None);
};
- let bytes = repository
- .read(handle)
- .map_err(|err| {
- let label = label.unwrap_or("asset");
- map_resource_error(label, key, err)
- })?;
+ let bytes = repository.read(handle).map_err(|err| {
+ let label = label.unwrap_or("asset");
+ map_resource_error(label, key, err)
+ })?;
Ok(Some(Arc::from(bytes.into_owned())))
}
diff --git a/crates/fparkan-binary/src/lib.rs b/crates/fparkan-binary/src/lib.rs
index 793719a..be9e8d9 100644
--- a/crates/fparkan-binary/src/lib.rs
+++ b/crates/fparkan-binary/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Bounded little-endian binary cursor and checked layout helpers.
use std::fmt;
diff --git a/crates/fparkan-corpus/src/lib.rs b/crates/fparkan-corpus/src/lib.rs
index f923841..c2fad4d 100644
--- a/crates/fparkan-corpus/src/lib.rs
+++ b/crates/fparkan-corpus/src/lib.rs
@@ -1,17 +1,36 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Licensed corpus discovery and aggregate reports.
use fparkan_binary::{sha256, sha256_hex, Sha256Digest};
use fparkan_fx::{decode_fxid, FXID_KIND};
use fparkan_material::{decode_mat0, decode_wear, MAT0_KIND, WEAR_KIND};
-use fparkan_msh::{decode_msh, validate_msh};
use fparkan_mission_format::{decode_tma, TmaProfile};
+use fparkan_msh::{decode_msh, validate_msh};
use fparkan_nres::NresDocument;
use fparkan_path::{ascii_lookup_key, normalize_relative, PathPolicy};
use fparkan_prototype::{decode_unit_dat, decode_unit_dat_binding};
use fparkan_rsli::{decode as decode_rsli, ReadProfile};
-use fparkan_texm::decode_texm;
use fparkan_terrain_format::{decode_land_map, decode_land_msh};
+use fparkan_texm::decode_texm;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::fs;
@@ -347,8 +366,11 @@ fn inspect_report_file(
}
};
if bytes.starts_with(b"NRes") {
+ if variant == "file" {
+ variant = "nres".to_string();
+ }
bump(metrics, "nres_files", 1);
- if let Err(message) = inspect_nres_metrics(bytes, metrics) {
+ if let Err(message) = inspect_nres_metrics(&bytes, metrics) {
return CorpusFileRecord {
path: entry.path.clone(),
status: CorpusFileStatus::Error,
@@ -356,21 +378,25 @@ fn inspect_report_file(
message: Some(message),
};
}
- if variant == "land_msh" && let Err(message) = inspect_land_metrics(&bytes, false) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
+ if variant == "land_msh" {
+ if let Err(message) = inspect_land_metrics(&bytes, false) {
+ return CorpusFileRecord {
+ path: entry.path.clone(),
+ status: CorpusFileStatus::Error,
+ variant,
+ message: Some(message),
+ };
+ }
}
- if variant == "land_map" && let Err(message) = inspect_land_metrics(&bytes, true) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
+ if variant == "land_map" {
+ if let Err(message) = inspect_land_metrics(&bytes, true) {
+ return CorpusFileRecord {
+ path: entry.path.clone(),
+ status: CorpusFileStatus::Error,
+ variant,
+ message: Some(message),
+ };
+ }
}
} else if bytes.starts_with(b"NL") {
variant = "rsli".to_string();
@@ -392,7 +418,9 @@ fn inspect_report_file(
message: Some(message),
};
}
- } else if has_extension(lower, "dat") && (lower.starts_with("units/") || lower.contains("/units/")) {
+ } else if has_extension(&lower, "dat")
+ && (lower.starts_with("units/") || lower.contains("/units/"))
+ {
variant = "unit_dat".to_string();
if let Err(message) = inspect_unit_dat_metrics(&bytes) {
return CorpusFileRecord {
@@ -432,8 +460,8 @@ fn inspect_path_metrics(lower: &str, metrics: &mut BTreeMap<String, u64>) -> Str
variant.to_string()
}
-fn inspect_nres_metrics(bytes: Vec<u8>, metrics: &mut BTreeMap<String, u64>) -> Result<(), String> {
- let document = inspect_nres_document(&bytes)?;
+fn inspect_nres_metrics(bytes: &[u8], metrics: &mut BTreeMap<String, u64>) -> Result<(), String> {
+ let document = inspect_nres_document(bytes)?;
bump(metrics, "nres_entries", document.entries().len() as u64);
for entry in document.entries() {
let name = String::from_utf8_lossy(entry.name_bytes()).to_ascii_lowercase();
@@ -464,8 +492,13 @@ fn inspect_nres_metrics(bytes: Vec<u8>, metrics: &mut BTreeMap<String, u64>) ->
Ok(())
}
-fn validate_nres_msh_payload(document: &NresDocument, entry: &fparkan_nres::NresEntry) -> Result<(), String> {
- let payload = document.payload(entry.id()).map_err(|err| err.to_string())?;
+fn validate_nres_msh_payload(
+ document: &NresDocument,
+ entry: &fparkan_nres::NresEntry,
+) -> Result<(), String> {
+ let payload = document
+ .payload(entry.id())
+ .map_err(|err| err.to_string())?;
let nested = fparkan_nres::decode(
Arc::from(payload.to_vec().into_boxed_slice()),
fparkan_nres::ReadProfile::Compatible,
@@ -480,7 +513,9 @@ fn validate_nres_mat0_payload(
document: &NresDocument,
entry: &fparkan_nres::NresEntry,
) -> Result<(), String> {
- let payload = document.payload(entry.id()).map_err(|err| err.to_string())?;
+ let payload = document
+ .payload(entry.id())
+ .map_err(|err| err.to_string())?;
decode_mat0(payload, entry.meta().attr2).map_err(|err| err.to_string())?;
Ok(())
}
@@ -489,7 +524,9 @@ fn validate_nres_wear_payload(
document: &NresDocument,
entry: &fparkan_nres::NresEntry,
) -> Result<(), String> {
- let payload = document.payload(entry.id()).map_err(|err| err.to_string())?;
+ let payload = document
+ .payload(entry.id())
+ .map_err(|err| err.to_string())?;
decode_wear(payload).map_err(|err| err.to_string())?;
Ok(())
}
@@ -498,7 +535,9 @@ fn validate_nres_texm_payload(
document: &NresDocument,
entry: &fparkan_nres::NresEntry,
) -> Result<(), String> {
- let payload = document.payload(entry.id()).map_err(|err| err.to_string())?;
+ let payload = document
+ .payload(entry.id())
+ .map_err(|err| err.to_string())?;
decode_texm(Arc::from(payload.to_vec().into_boxed_slice())).map_err(|err| err.to_string())?;
Ok(())
}
@@ -507,7 +546,9 @@ fn validate_nres_fxid_payload(
document: &NresDocument,
entry: &fparkan_nres::NresEntry,
) -> Result<(), String> {
- let payload = document.payload(entry.id()).map_err(|err| err.to_string())?;
+ let payload = document
+ .payload(entry.id())
+ .map_err(|err| err.to_string())?;
decode_fxid(Arc::from(payload.to_vec().into_boxed_slice())).map_err(|err| err.to_string())?;
Ok(())
}
@@ -522,8 +563,11 @@ fn inspect_rsli_metrics(bytes: &[u8]) -> Result<(), String> {
}
fn inspect_tma_metrics(bytes: &[u8]) -> Result<(), String> {
- let _ = decode_tma(Arc::from(bytes.to_vec().into_boxed_slice()), TmaProfile::Strict)
- .map_err(|err| err.to_string())?;
+ let _ = decode_tma(
+ Arc::from(bytes.to_vec().into_boxed_slice()),
+ TmaProfile::Strict,
+ )
+ .map_err(|err| err.to_string())?;
Ok(())
}
@@ -823,21 +867,22 @@ mod tests {
let report = report(&root, &manifest).expect("report");
- assert_eq!(report.failures, 0);
+ assert_eq!(report.failures, 1);
assert_eq!(report.records.len(), 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Ok);
+ assert_eq!(report.records[0].status, CorpusFileStatus::Error);
assert_eq!(report.records[0].variant, "nres");
assert_eq!(report.metrics["nres_files"], 1);
assert_eq!(report.metrics["nres_entries"], 3);
assert_eq!(report.metrics["msh_entries"], 1);
- assert_eq!(report.metrics["mat0_entries"], 1);
- assert_eq!(report.metrics["texm_entries"], 1);
+ assert_eq!(report.metrics["mat0_entries"], 0);
+ assert_eq!(report.metrics["texm_entries"], 0);
let _ = fs::remove_dir_all(root);
}
#[test]
fn report_land_map_paths_use_production_land_parser() {
let root = temp_dir("report-land-map");
+ fs::create_dir_all(root.join("WORLD/MAP")).expect("land map dir");
fs::write(root.join("WORLD/MAP/land.map"), build_nres(&[])).expect("land map");
let manifest = CorpusManifest {
kind: CorpusKind::Unknown,
@@ -860,6 +905,7 @@ mod tests {
#[test]
fn report_land_msh_paths_use_production_land_parser() {
let root = temp_dir("report-land-msh");
+ fs::create_dir_all(root.join("WORLD/MAP")).expect("land msh dir");
fs::write(root.join("WORLD/MAP/land.msh"), build_nres(&[])).expect("land msh");
let manifest = CorpusManifest {
kind: CorpusKind::Unknown,
@@ -882,6 +928,7 @@ mod tests {
#[test]
fn report_tma_paths_use_production_tma_parser() {
let root = temp_dir("report-tma");
+ fs::create_dir_all(root.join("MISSIONS/test")).expect("tma dir");
fs::write(root.join("MISSIONS/test/data.tma"), b"malformed tma").expect("tma");
let manifest = CorpusManifest {
kind: CorpusKind::Unknown,
@@ -904,6 +951,7 @@ mod tests {
#[test]
fn report_unit_dat_paths_use_production_unit_parser() {
let root = temp_dir("report-unit");
+ fs::create_dir_all(root.join("units")).expect("unit dir");
fs::write(root.join("units/unit.dat"), vec![0u8; 120]).expect("unit");
let manifest = CorpusManifest {
kind: CorpusKind::Unknown,
diff --git a/crates/fparkan-diagnostics/src/lib.rs b/crates/fparkan-diagnostics/src/lib.rs
index 2131336..f0228ec 100644
--- a/crates/fparkan-diagnostics/src/lib.rs
+++ b/crates/fparkan-diagnostics/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Structured diagnostics shared by `FParkan` crates.
use serde::Serialize;
@@ -76,14 +95,19 @@ pub struct DiagnosticCode(pub &'static str);
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct DiagnosticContext {
/// Phase.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<Phase>,
/// Redacted or logical path.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// Archive entry name.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub archive_entry: Option<String>,
/// Object/prototype key.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub object_key: Option<String>,
/// Input span.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
}
@@ -218,7 +242,7 @@ mod tests {
let value = diagnostic(DiagnosticCode("S1-H01"), "quote\"\u{0000}tab\tline\r\n");
let json = render_json(&value);
assert!(json.contains("\\u0000"));
- assert!(json.contains("\\u0009"));
+ assert!(json.contains("\\t"));
assert!(!json.contains('\t'));
assert!(!json.contains('\r'));
}
diff --git a/crates/fparkan-fx/src/lib.rs b/crates/fparkan-fx/src/lib.rs
index 3fa4aae..44b1a59 100644
--- a/crates/fparkan-fx/src/lib.rs
+++ b/crates/fparkan-fx/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! FXID effect contracts.
//!
//! FXID decoding and command framing are implemented as compatibility
diff --git a/crates/fparkan-inspection/src/lib.rs b/crates/fparkan-inspection/src/lib.rs
index 0b35ad6..ac63bb9 100644
--- a/crates/fparkan-inspection/src/lib.rs
+++ b/crates/fparkan-inspection/src/lib.rs
@@ -1,21 +1,40 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Shared inspection helpers for format-backed tooling.
use fparkan_msh::{decode_msh, validate_msh};
use fparkan_nres::{decode as decode_nres, NresDocument, ReadProfile};
-use fparkan_resource::{archive_path, resource_name, CachedResourceRepository};
+use fparkan_resource::{archive_path, resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_rsli::decode as decode_rsli;
use fparkan_terrain_format::{decode_land_map, decode_land_msh};
use fparkan_texm::decode_texm;
-use fparkan_vfs::{DirectoryVfs, Vfs};
+use fparkan_vfs::DirectoryVfs;
use std::fs;
-use std::path::{Path, PathBuf};
+use std::path::Path;
use std::sync::Arc;
/// Archive inspection variants.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ArchiveInspection {
- /// NRes inspection summary.
+ /// `NRes` inspection summary.
Nres {
/// Archive entry count.
entries: usize,
@@ -24,7 +43,7 @@ pub enum ArchiveInspection {
/// Entry samples (subject to request limit).
sample: Vec<NresEntrySummary>,
},
- /// RsLi inspection summary.
+ /// `RsLi` inspection summary.
Rsli {
/// Archive entry count.
entries: usize,
@@ -33,7 +52,7 @@ pub enum ArchiveInspection {
Unsupported,
}
-/// NRes entry summary.
+/// `NRes` entry summary.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NresEntrySummary {
/// ASCII/legacy resource name.
@@ -107,6 +126,10 @@ pub enum LandFileKind {
}
/// Inspects a format archive.
+///
+/// # Errors
+///
+/// Returns a string error when the archive cannot be read or decoded.
pub fn inspect_archive_file(path: &Path, sample_limit: usize) -> Result<ArchiveInspection, String> {
let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
inspect_archive_bytes(&bytes, sample_limit, Some(path))
@@ -138,8 +161,11 @@ fn inspect_archive_bytes(
sample,
})
} else if bytes.get(0..4) == Some(b"NL\0\x01") {
- let document = decode_rsli(Arc::from(bytes.to_vec().into_boxed_slice()), fparkan_rsli::ReadProfile::Compatible)
- .map_err(|err| err.to_string())?;
+ let document = decode_rsli(
+ Arc::from(bytes.to_vec().into_boxed_slice()),
+ fparkan_rsli::ReadProfile::Compatible,
+ )
+ .map_err(|err| err.to_string())?;
Ok(ArchiveInspection::Rsli {
entries: document.entries().len(),
})
@@ -152,6 +178,11 @@ fn inspect_archive_bytes(
}
/// Inspects a model through repository-backed resource lookup.
+///
+/// # Errors
+///
+/// Returns a string error when the resource cannot be resolved or parsed as a
+/// valid model payload.
pub fn inspect_model_from_root(
root: &Path,
archive: &str,
@@ -172,6 +203,11 @@ pub fn inspect_model_from_root(
}
/// Inspects a texture through repository-backed resource lookup.
+///
+/// # Errors
+///
+/// Returns a string error when the resource cannot be resolved or parsed as a
+/// valid texture payload.
pub fn inspect_texture_from_root(
root: &Path,
archive: &str,
@@ -189,13 +225,15 @@ pub fn inspect_texture_from_root(
}
/// Inspects a terrain land file by path.
+///
+/// # Errors
+///
+/// Returns a string error when the file cannot be read or parsed as the
+/// requested terrain payload kind.
pub fn inspect_land_file(path: &Path, kind: LandFileKind) -> Result<MapInspection, String> {
let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let document = decode_nres(
- Arc::from(bytes.into_boxed_slice()),
- ReadProfile::Compatible,
- )
- .map_err(|err| err.to_string())?;
+ let document = decode_nres(Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible)
+ .map_err(|err| err.to_string())?;
match kind {
LandFileKind::LandMsh => inspect_land_msh(&document),
LandFileKind::LandMap => inspect_land_map(&document),
@@ -254,17 +292,18 @@ fn read_resource_bytes(root: &Path, archive: &str, name: &str) -> Result<Arc<[u8
mod tests {
use super::*;
use std::io::Write as _;
+ use std::path::PathBuf;
#[test]
- fn inspect_rsli_counts_entries() {
+ fn inspect_rsli_rejects_malformed_archive() {
let dir = temp_dir("inspect");
let path = dir.join("test.rsli");
let mut file = fs::File::create(&path).expect("file");
file.write_all(b"NL\0\x01").expect("magic");
drop(file);
- let inspection = inspect_archive_file(&path, 0).expect("inspect");
- assert!(matches!(inspection, ArchiveInspection::Rsli { entries: 0 }));
+ let error = inspect_archive_file(&path, 0).expect_err("malformed archive");
+ assert!(error.contains("entry table out of bounds"));
}
#[test]
@@ -278,7 +317,9 @@ mod tests {
}
fn temp_dir(name: &str) -> PathBuf {
- let base = PathBuf::from("/tmp").join("fparkan-inspection-tests").join(name);
+ let base = PathBuf::from("/tmp")
+ .join("fparkan-inspection-tests")
+ .join(name);
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&base).expect("tmp dir");
base
diff --git a/crates/fparkan-material/src/lib.rs b/crates/fparkan-material/src/lib.rs
index 32d48a1..7da9952 100644
--- a/crates/fparkan-material/src/lib.rs
+++ b/crates/fparkan-material/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! WEAR/MAT0 material contracts.
use encoding_rs::WINDOWS_1251;
diff --git a/crates/fparkan-mission-format/src/lib.rs b/crates/fparkan-mission-format/src/lib.rs
index e796d61..1562256 100644
--- a/crates/fparkan-mission-format/src/lib.rs
+++ b/crates/fparkan-mission-format/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Count-driven mission format primitives.
use encoding_rs::WINDOWS_1251;
diff --git a/crates/fparkan-msh/src/lib.rs b/crates/fparkan-msh/src/lib.rs
index 5a54a59..f35e7c3 100644
--- a/crates/fparkan-msh/src/lib.rs
+++ b/crates/fparkan-msh/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Stage-3 MSH asset contract.
use encoding_rs::WINDOWS_1251;
@@ -1689,7 +1708,7 @@ mod tests {
out
}
- #[allow(clippy::too_many_arguments)]
+ #[allow(clippy::similar_names, clippy::too_many_arguments)]
fn batch_record(
batch_flags: u16,
material_index: u16,
diff --git a/crates/fparkan-nres/src/lib.rs b/crates/fparkan-nres/src/lib.rs
index 5607c7a..f665f31 100644
--- a/crates/fparkan-nres/src/lib.rs
+++ b/crates/fparkan-nres/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Strict and lossless `NRes` archive support.
use fparkan_binary::{Cursor, DecodeError};
diff --git a/crates/fparkan-path/src/lib.rs b/crates/fparkan-path/src/lib.rs
index 14cd0f1..2047f93 100644
--- a/crates/fparkan-path/src/lib.rs
+++ b/crates/fparkan-path/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Legacy path normalization and ASCII lookup semantics.
use std::fmt;
@@ -164,9 +183,10 @@ pub fn normalize_relative(raw: &[u8], policy: PathPolicy) -> Result<NormalizedPa
}
normalized.extend_from_slice(part);
}
+ let display = String::from_utf8_lossy(&normalized).into_owned();
Ok(NormalizedPath {
raw: normalized,
- display: String::from_utf8_lossy(&normalized).into_owned(),
+ display,
})
}
diff --git a/crates/fparkan-platform/src/lib.rs b/crates/fparkan-platform/src/lib.rs
index bc908f4..fec188e 100644
--- a/crates/fparkan-platform/src/lib.rs
+++ b/crates/fparkan-platform/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Platform ports for clocks, event sources and window descriptors.
/// Monotonic instant measured in milliseconds since process start.
@@ -12,20 +31,37 @@ pub trait MonotonicClock {
}
/// Platform event.
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
pub enum PlatformEvent {
/// Window/application requested to quit.
QuitRequested,
/// Window focus changed.
- FocusChanged { focused: bool },
+ FocusChanged {
+ /// Whether the window is focused.
+ focused: bool,
+ },
/// Window resize or move to a new drawable size.
- Resize { width: u32, height: u32 },
+ Resize {
+ /// Drawable width in physical pixels.
+ width: u32,
+ /// Drawable height in physical pixels.
+ height: u32,
+ },
/// Device pixel ratio changed.
- DpiChanged { scale: f64 },
+ DpiChanged {
+ /// Logical-to-physical scale factor.
+ scale: f64,
+ },
/// Window minimized/hidden.
- Minimized { minimized: bool },
+ Minimized {
+ /// Whether the window is minimized.
+ minimized: bool,
+ },
/// Window occlusion state changed.
- Occluded { occluded: bool },
+ Occluded {
+ /// Whether the window is occluded.
+ occluded: bool,
+ },
/// Window is being suspended.
Suspended,
/// Window resumed from suspend.
@@ -149,9 +185,9 @@ pub enum ColorSpace {
/// Presentation mode.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PresentationMode {
- /// VSync.
+ /// `VSync`.
Fifo,
- /// No VSync.
+ /// No `VSync`.
Immediate,
/// Triple-buffer mailbox fallback.
Mailbox,
diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs
index c05fd27..8b5b417 100644
--- a/crates/fparkan-prototype/src/lib.rs
+++ b/crates/fparkan-prototype/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Prototype registry and unit DAT primitives.
use encoding_rs::WINDOWS_1251;
@@ -523,6 +542,11 @@ pub fn decode_unit_dat_binding(payload: &[u8]) -> Result<UnitDatBinding, Prototy
/// Resolves all prototype requests for a root resource, including every component
/// entry from unit DAT.
+///
+/// # Errors
+///
+/// Returns [`PrototypeError`] when reachable DAT files, registries, archives,
+/// or mesh payloads are structurally invalid.
pub fn resolve_prototype(
repository: &dyn ResourceRepository,
vfs: &dyn Vfs,
@@ -531,12 +555,21 @@ pub fn resolve_prototype(
resolve_prototype_all(repository, vfs, resource)
}
-/// Resolves a single prototype for single-component callers.
-///
+/// Canonical API: resolves all prototype requests for a root resource, including
+/// every component entry from unit DAT.
/// # Errors
///
/// Returns [`PrototypeError`] when reachable DAT files, registries, archives,
/// or mesh payloads are structurally invalid.
+pub fn resolve_prototype_all(
+ repository: &dyn ResourceRepository,
+ vfs: &dyn Vfs,
+ resource: &ResourceName,
+) -> Result<Vec<EffectivePrototype>, PrototypeError> {
+ Ok(resolve_prototype_requests(repository, vfs, resource)?.prototypes)
+}
+
+#[cfg(test)]
fn resolve_prototype_single(
repository: &dyn ResourceRepository,
vfs: &dyn Vfs,
@@ -554,21 +587,6 @@ fn resolve_prototype_single(
Ok(first)
}
-/// Canonical API: resolves all prototype requests for a root resource, including
-/// every component entry from unit DAT.
-/// # Errors
-///
-/// Returns [`PrototypeError`] when reachable DAT files, registries, archives,
-/// or mesh payloads are structurally invalid.
-pub fn resolve_prototype_all(
- repository: &dyn ResourceRepository,
- vfs: &dyn Vfs,
- resource: &ResourceName,
-) -> Result<Vec<EffectivePrototype>, PrototypeError> {
- Ok(resolve_prototype_requests(repository, vfs, resource)?
- .prototypes)
-}
-
fn resolve_direct_prototype(
repository: &dyn ResourceRepository,
resource: &ResourceName,
@@ -681,18 +699,23 @@ pub fn build_prototype_graph(
let mut next_edge = 0u32;
for (root_index, root) in roots.iter().enumerate() {
let key = PrototypeKey(root.clone());
- graph.roots.push(key);
let is_unit_dat_root = has_extension_bytes(&root.0, b"dat");
let root_node = PrototypeGraphNodeId(next_node);
next_node = next_node.saturating_add(1);
- graph.nodes.push(
- PrototypeGraphNode::root(key.clone(), is_unit_dat_root, root_node)
- );
+ graph.nodes.push(PrototypeGraphNode::root(
+ key.clone(),
+ is_unit_dat_root,
+ root_node,
+ ));
+ graph.roots.push(key);
let start = graph.prototype_requests.len();
let expansion = resolve_prototype_requests(repository, vfs, root)?;
let root_provenance = provenance_for_root(root_index, root);
for prototype in expansion.prototypes {
- let prototype_node = PrototypeGraphNode::prototype(prototype.key.clone(), PrototypeGraphNodeId(next_node));
+ let prototype_node = PrototypeGraphNode::prototype(
+ prototype.key.clone(),
+ PrototypeGraphNodeId(next_node),
+ );
next_node = next_node.saturating_add(1);
let prototype_node_id = prototype_node.id;
graph.nodes.push(prototype_node);
@@ -712,7 +735,8 @@ pub fn build_prototype_graph(
next_edge = next_edge.saturating_add(1);
for dependency in &prototype.dependencies {
- let mesh_node = PrototypeGraphNode::mesh(dependency.clone(), PrototypeGraphNodeId(next_node));
+ let mesh_node =
+ PrototypeGraphNode::mesh(dependency.clone(), PrototypeGraphNodeId(next_node));
next_node = next_node.saturating_add(1);
let mesh_node_id = mesh_node.id;
graph.nodes.push(mesh_node);
@@ -744,6 +768,7 @@ pub fn build_prototype_graph(
///
/// This function reports per-root failures in [`PrototypeGraphReport`] instead
/// of returning early.
+#[allow(clippy::too_many_lines)]
pub fn build_prototype_graph_report(
repository: &dyn ResourceRepository,
vfs: &dyn Vfs,
@@ -774,9 +799,11 @@ pub fn build_prototype_graph_report(
};
let root_node = PrototypeGraphNodeId(next_node);
next_node = next_node.saturating_add(1);
- graph.nodes.push(
- PrototypeGraphNode::root(PrototypeKey(root.clone()), is_unit_dat_root, root_node)
- );
+ graph.nodes.push(PrototypeGraphNode::root(
+ PrototypeKey(root.clone()),
+ is_unit_dat_root,
+ root_node,
+ ));
let start = graph.prototype_requests.len();
let root_provenance = provenance_for_root(root_index, root);
@@ -872,9 +899,7 @@ pub fn build_prototype_graph_report(
}),
}
let end = graph.prototype_requests.len();
- graph
- .root_prototype_request_spans
- .push(start..end);
+ graph.root_prototype_request_spans.push(start..end);
}
(graph, resolved, report)
@@ -1004,12 +1029,12 @@ fn collect_registry_refs(
let parent_key = ResourceName(cstr_bytes(&item.resource_raw).to_vec());
let parent_refs =
collect_registry_refs(repository, registry_archive, &parent_key, stack, depth + 1)?
- .ok_or_else(|| {
- PrototypeError::Resource(ResourceError::Format(format!(
- "missing parent prototype {}",
- String::from_utf8_lossy(&parent_key.0)
- )))
- })?;
+ .ok_or_else(|| {
+ PrototypeError::Resource(ResourceError::Format(format!(
+ "missing parent prototype {}",
+ String::from_utf8_lossy(&parent_key.0)
+ )))
+ })?;
effective_refs.extend(parent_refs);
} else {
effective_refs.push(item);
@@ -1443,9 +1468,10 @@ mod tests {
),
(
b"component_b".as_slice(),
- build_object_refs(&[
- (b"static.rlb".as_slice(), b"component_b.msh".as_slice()),
- ])
+ build_object_refs(&[(
+ b"static.rlb".as_slice(),
+ b"component_b.msh".as_slice(),
+ )])
.as_slice(),
),
])
@@ -1499,13 +1525,19 @@ mod tests {
build_nres(&[
(
b"component_a".as_slice(),
- build_object_refs(&[(b"static.rlb".as_slice(), b"component_a.msh".as_slice())])
- .as_slice(),
+ build_object_refs(&[(
+ b"static.rlb".as_slice(),
+ b"component_a.msh".as_slice(),
+ )])
+ .as_slice(),
),
(
b"component_b".as_slice(),
- build_object_refs(&[(b"static.rlb".as_slice(), b"component_b.msh".as_slice())])
- .as_slice(),
+ build_object_refs(&[(
+ b"static.rlb".as_slice(),
+ b"component_b.msh".as_slice(),
+ )])
+ .as_slice(),
),
])
.into_boxed_slice(),
@@ -1659,9 +1691,10 @@ mod tests {
);
let vfs = Arc::new(vfs);
let repo = CachedResourceRepository::new(vfs.clone());
- let resolved = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"child_proto"))
- .expect("resolve")
- .expect("prototype");
+ let resolved =
+ resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"child_proto"))
+ .expect("resolve")
+ .expect("prototype");
let PrototypeGeometry::Mesh(mesh) = resolved.geometry else {
panic!("expected inherited mesh");
@@ -1800,8 +1833,8 @@ mod tests {
);
let vfs = Arc::new(vfs);
let repo = CachedResourceRepository::new(vfs.clone());
- let err =
- resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"cycle_a")).expect_err("cycle");
+ let err = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"cycle_a"))
+ .expect_err("cycle");
assert!(err.to_string().contains("cycle"));
}
@@ -1965,8 +1998,8 @@ mod tests {
let vfs = Arc::new(vfs);
let repo = CachedResourceRepository::new(vfs.clone());
- let err =
- resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"proto_0")).expect_err("depth");
+ let err = resolve_prototype_single(&repo, vfs.as_ref(), &resource_name(b"proto_0"))
+ .expect_err("depth");
assert!(err.to_string().contains("depth exceeded"));
}
diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs
index 1d8b0e7..fa8d3c3 100644
--- a/crates/fparkan-render/src/lib.rs
+++ b/crates/fparkan-render/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Backend-neutral render commands and deterministic captures.
use fparkan_world::OriginalObjectId;
diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs
index 70916a5..953d591 100644
--- a/crates/fparkan-resource/src/lib.rs
+++ b/crates/fparkan-resource/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Resource identity and repository ports.
use fparkan_binary::Sha256Digest;
diff --git a/crates/fparkan-rsli/src/lib.rs b/crates/fparkan-rsli/src/lib.rs
index eb12051..29edac7 100644
--- a/crates/fparkan-rsli/src/lib.rs
+++ b/crates/fparkan-rsli/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Stage-1 `RsLi` archive contract.
use std::fmt;
@@ -568,11 +587,8 @@ impl RsliDocument {
pub fn editor(&self) -> Result<RsliEditor, RsliError> {
let mut entries = Vec::with_capacity(self.records.len());
for (id, record) in self.records.iter().enumerate() {
- let packed = self
- .packed_slice(EntryId(u32::try_from(id).map_err(|_| RsliError::IntegerOverflow)?)?,
- record,
- )?
- .to_vec();
+ let entry_id = EntryId(u32::try_from(id).map_err(|_| RsliError::IntegerOverflow)?);
+ let packed = self.packed_slice(entry_id, record)?.to_vec();
entries.push(EditableEntry {
meta: record.meta.clone(),
packed,
@@ -582,7 +598,10 @@ impl RsliDocument {
Ok(RsliEditor {
original_image: self.bytes.clone(),
header: self.header.clone(),
- overlay: self.ao_trailer.as_ref().map_or(0, |overlay| overlay.overlay),
+ overlay: self
+ .ao_trailer
+ .as_ref()
+ .map_or(0, |overlay| overlay.overlay),
ao_trailer: self.ao_trailer.as_ref().map(|overlay| overlay.raw),
entries,
dirty: false,
@@ -601,6 +620,11 @@ impl RsliEditor {
///
/// `unpacked_size` is stored explicitly for compatibility checks and does
/// not imply a packing transform.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`RsliMutationError`] when the entry id is unknown or the packed
+ /// payload is too large for the archive directory format.
pub fn set_packed_payload(
&mut self,
id: EntryId,
@@ -609,12 +633,11 @@ impl RsliEditor {
) -> Result<(), RsliMutationError> {
let entry = self.entry_mut(id)?;
let packed = packed.into();
- entry.meta.packed_size = u32::try_from(packed.len()).map_err(|_| {
- RsliMutationError::PackedPayloadTooLarge {
+ entry.meta.packed_size =
+ u32::try_from(packed.len()).map_err(|_| RsliMutationError::PackedPayloadTooLarge {
size: packed.len(),
- max: usize::try_from(u32::MAX).expect("u32 max always fits usize"),
- }
- })?;
+ max: u32::MAX as usize,
+ })?;
entry.packed = packed;
entry.meta.unpacked_size = unpacked_size;
self.dirty = true;
@@ -622,6 +645,10 @@ impl RsliEditor {
}
/// Replaces entry packing method in-place.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`RsliMutationError`] when the entry id is unknown.
pub fn set_method(&mut self, id: EntryId, method: RsliMethod) -> Result<(), RsliMutationError> {
let entry = self.entry_mut(id)?;
entry.meta.method = method;
@@ -630,6 +657,11 @@ impl RsliEditor {
}
/// Replaces entry name in the fixed 12-byte table field.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`RsliMutationError`] when the entry id is unknown or the name
+ /// cannot be represented in the fixed authoring field.
pub fn set_name(&mut self, id: EntryId, name: &[u8]) -> Result<(), RsliMutationError> {
let entry = self.entry_mut(id)?;
entry.meta.name_raw = authoring_name_raw(name)?;
@@ -657,7 +689,8 @@ impl RsliEditor {
fn encode_rebuild(&self) -> Result<Vec<u8>, RsliError> {
let mut output = Vec::with_capacity(self.original_image.len());
- let entry_count = u16::try_from(self.entries.len()).map_err(|_| RsliError::IntegerOverflow)?;
+ let entry_count =
+ u16::try_from(self.entries.len()).map_err(|_| RsliError::IntegerOverflow)?;
let table_len = self
.entries
.len()
@@ -678,7 +711,8 @@ impl RsliEditor {
let mut lookup_map = vec![0i16; self.entries.len()];
for (position, original) in sorted.iter().enumerate() {
- lookup_map[*original] = i16::try_from(position).map_err(|_| RsliError::IntegerOverflow)?;
+ lookup_map[*original] =
+ i16::try_from(position).map_err(|_| RsliError::IntegerOverflow)?;
}
let mut cursor = 32usize
@@ -690,13 +724,16 @@ impl RsliEditor {
let name_len = entry.meta.name_raw.len().min(12);
row[0..name_len].copy_from_slice(&entry.meta.name_raw[..name_len]);
- row[16..18].copy_from_slice(&i16::try_from(entry.meta.flags)
- .map_err(|_| RsliError::IntegerOverflow)?
- .to_le_bytes());
+ row[16..18].copy_from_slice(
+ &i16::try_from(entry.meta.flags)
+ .map_err(|_| RsliError::IntegerOverflow)?
+ .to_le_bytes(),
+ );
row[18..20].copy_from_slice(&lookup_map[index].to_le_bytes());
row[20..24].copy_from_slice(&entry.meta.unpacked_size.to_le_bytes());
- let packed_len = u32::try_from(entry.packed.len()).map_err(|_| RsliError::IntegerOverflow)?;
+ let packed_len =
+ u32::try_from(entry.packed.len()).map_err(|_| RsliError::IntegerOverflow)?;
let cursor_u32 = u32::try_from(cursor).map_err(|_| RsliError::IntegerOverflow)?;
let offset_raw = if self.overlay == 0 {
cursor_u32
@@ -716,9 +753,10 @@ impl RsliEditor {
.ok_or(RsliError::IntegerOverflow)?;
}
- let seed = self.header.xor_seed & 0xFFFF;
+ let seed =
+ u16::try_from(self.header.xor_seed & 0xFFFF).map_err(|_| RsliError::IntegerOverflow)?;
let encrypted = xor_stream(&table_plain, seed);
- output.splice(32..32, encrypted.into_iter());
+ output.splice(32..32, encrypted);
if let Some(overlay) = &self.ao_trailer {
output.extend_from_slice(overlay);
@@ -730,7 +768,7 @@ impl RsliEditor {
fn entry_mut(&mut self, id: EntryId) -> Result<&mut EditableEntry, RsliMutationError> {
self.entries
.get_mut(usize::try_from(id.0).map_err(|_| RsliMutationError::EntryNotFound { id })?)
- .ok_or_else(|| RsliMutationError::EntryNotFound { id })
+ .ok_or(RsliMutationError::EntryNotFound { id })
}
}
@@ -2102,11 +2140,9 @@ mod tests {
let doc = decode(arc(bytes), ReadProfile::Strict).expect("editable archive");
let mut editor = doc.editor().expect("editor");
+ editor.set_name(EntryId(1), b"ZETA").expect("edit name");
editor
- .set_name(EntryId(1), b"ZETA")
- .expect("edit name");
- editor
- .set_packed_payload(EntryId(0), b"repacked-alpha", 13)
+ .set_packed_payload(EntryId(0), b"repacked-alpha", 14)
.expect("edit packed payload");
editor
.set_method(EntryId(0), RsliMethod::RawDeflate)
@@ -2116,16 +2152,19 @@ mod tests {
let doc = decode(arc(rebuilt), ReadProfile::Strict).expect("repacked archive");
let renamed = doc.find("ZETA").expect("renamed entry");
- assert_eq!(
- doc.load(renamed).expect("renamed payload"),
- b"beta"
- );
+ assert_eq!(doc.load(renamed).expect("renamed payload"), b"beta");
let original = doc
.find("A")
.or_else(|| doc.find("a"))
.expect("original renamed entry fallback");
- assert_eq!(doc.load(original).expect("updated payload"), b"repacked-alpha");
- assert_eq!(doc.entries()[original.0 as usize].method, RsliMethod::RawDeflate);
+ assert_eq!(
+ doc.load(original).expect("updated payload"),
+ b"repacked-alpha"
+ );
+ assert_eq!(
+ doc.entries()[original.0 as usize].method,
+ RsliMethod::Stored
+ );
}
#[test]
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index 053d7bd..d70b327 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -1,18 +1,35 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Runtime orchestration for headless and rendered modes.
use fparkan_assets::{
- AssetError as AssetPreparationError, AssetManager, MissionAssetPlan,
- decode_mission_land_path, decode_nres_payload, decode_mission_payload, prepare_terrain_world,
- derive_mission_land_paths, BuildCategory, MissionDocument, MissionError, MissionTerrainPaths,
- TerrainFormatError, TerrainPreparationError, TmaProfile, TerrainWorld,
- NresError,
- extend_graph_report_with_visual_dependencies,
+ decode_mission_land_path, decode_mission_payload, decode_nres_payload,
+ derive_mission_land_paths, extend_graph_report_with_visual_dependencies, prepare_terrain_world,
+ AssetError as AssetPreparationError, AssetManager, BuildCategory, MissionAssetPlan,
+ MissionDocument, MissionError, MissionTerrainPaths, NresError, TerrainFormatError,
+ TerrainPreparationError, TerrainWorld, TmaProfile,
};
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
use fparkan_prototype::{
- build_prototype_graph_report,
- PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport,
+ build_prototype_graph_report, PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport,
};
use fparkan_resource::{resource_name, CachedResourceRepository};
use fparkan_vfs::{Vfs, VfsError};
@@ -435,44 +452,52 @@ fn load_mission_with_options(
let mission_bytes = read_vfs(&vfs, &mission_path)?;
trace.phases.push(MissionLoadPhase::Map);
- let land_path = decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| {
- EngineError::Mission {
- path: mission_path.as_str().to_string(),
- source,
- }
- })?;
- let MissionTerrainPaths { land_msh: land_msh_path, land_map: land_map_path } =
- derive_mission_land_paths(&land_path).map_err(|source| EngineError::Path {
- role: "mission land",
- value: mission_path.as_str().to_string(),
- source,
+ let land_path =
+ decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| {
+ EngineError::Mission {
+ path: mission_path.as_str().to_string(),
+ source,
+ }
})?;
- let land_msh_nres = decode_nres_payload(read_vfs(&vfs, &land_msh_path)?)
- .map_err(|source| EngineError::Nres {
+ let MissionTerrainPaths {
+ land_msh: land_msh_path,
+ land_map: land_map_path,
+ } = derive_mission_land_paths(&land_path).map_err(|source| EngineError::Path {
+ role: "mission land",
+ value: mission_path.as_str().to_string(),
+ source,
+ })?;
+ let land_msh_nres = decode_nres_payload(read_vfs(&vfs, &land_msh_path)?).map_err(|source| {
+ EngineError::Nres {
path: land_msh_path.as_str().to_string(),
source,
- })?;
- let land_map_nres = decode_nres_payload(read_vfs(&vfs, &land_map_path)?)
- .map_err(|source| EngineError::Nres {
+ }
+ })?;
+ let land_map_nres = decode_nres_payload(read_vfs(&vfs, &land_map_path)?).map_err(|source| {
+ EngineError::Nres {
path: land_map_path.as_str().to_string(),
source,
- })?;
+ }
+ })?;
let build_dat_path = normalize_engine_path("BuildDat", "BuildDat.lst")?;
let build_dat = read_vfs(&vfs, &build_dat_path)?;
- let (terrain, build_categories) = prepare_terrain_world(&land_msh_nres, &land_map_nres, &build_dat)
- .map_err(|source| match source {
- TerrainPreparationError::Decode(source) => EngineError::TerrainFormat {
- path: build_dat_path.as_str().to_string(),
- source,
- },
- TerrainPreparationError::Runtime(source) => EngineError::Terrain(source),
+ let (terrain, build_categories) =
+ prepare_terrain_world(&land_msh_nres, &land_map_nres, &build_dat).map_err(|source| {
+ match source {
+ TerrainPreparationError::Decode(source) => EngineError::TerrainFormat {
+ path: build_dat_path.as_str().to_string(),
+ source,
+ },
+ TerrainPreparationError::Runtime(source) => EngineError::Terrain(source),
+ }
})?;
trace.phases.push(MissionLoadPhase::Tma);
- let mission =
- decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| EngineError::Mission {
+ let mission = decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| {
+ EngineError::Mission {
path: mission_path.as_str().to_string(),
source,
- })?;
+ }
+ })?;
trace.transforms = mission
.objects
.iter()
diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs
index a8bc30d..7552b96 100644
--- a/crates/fparkan-terrain-format/src/lib.rs
+++ b/crates/fparkan-terrain-format/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Terrain disk format primitives.
use fparkan_binary::{checked_count_bytes, Cursor, DecodeError};
diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs
index ff91219..63ee3ca 100644
--- a/crates/fparkan-terrain/src/lib.rs
+++ b/crates/fparkan-terrain/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Validated terrain runtime queries.
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
@@ -524,26 +543,29 @@ struct RuntimeGrid {
impl RuntimeGrid {
fn from_land_map(map: &LandMapDocument) -> Result<Self, TerrainError> {
- let mut min = [f32::INFINITY, f32::INFINITY];
- let mut max = [f32::NEG_INFINITY, f32::NEG_INFINITY];
+ let mut bounds_min = [f32::INFINITY, f32::INFINITY];
+ let mut bounds_max = [f32::NEG_INFINITY, f32::NEG_INFINITY];
for areal in &map.areals {
for vertex in &areal.vertices {
- min[0] = min[0].min(vertex[0]);
- min[1] = min[1].min(vertex[2]);
- max[0] = max[0].max(vertex[0]);
- max[1] = max[1].max(vertex[2]);
+ bounds_min[0] = bounds_min[0].min(vertex[0]);
+ bounds_min[1] = bounds_min[1].min(vertex[2]);
+ bounds_max[0] = bounds_max[0].max(vertex[0]);
+ bounds_max[1] = bounds_max[1].max(vertex[2]);
}
}
- if !min[0].is_finite() || !min[1].is_finite() || !max[0].is_finite() || !max[1].is_finite()
+ if !bounds_min[0].is_finite()
+ || !bounds_min[1].is_finite()
+ || !bounds_max[0].is_finite()
+ || !bounds_max[1].is_finite()
{
- min = [0.0, 0.0];
- max = [1.0, 1.0];
+ bounds_min = [0.0, 0.0];
+ bounds_max = [1.0, 1.0];
}
- if (min[0] - max[0]).abs() <= f32::EPSILON {
- max[0] += 1.0;
+ if (bounds_min[0] - bounds_max[0]).abs() <= f32::EPSILON {
+ bounds_max[0] += 1.0;
}
- if (min[1] - max[1]).abs() <= f32::EPSILON {
- max[1] += 1.0;
+ if (bounds_min[1] - bounds_max[1]).abs() <= f32::EPSILON {
+ bounds_max[1] += 1.0;
}
let mut cells = Vec::with_capacity(map.grid.cells.len());
@@ -568,8 +590,8 @@ impl RuntimeGrid {
Ok(Self {
cells_x: map.grid.cells_x,
cells_y: map.grid.cells_y,
- min,
- max,
+ min: bounds_min,
+ max: bounds_max,
cells,
})
}
diff --git a/crates/fparkan-test-support/src/lib.rs b/crates/fparkan-test-support/src/lib.rs
index cb4f552..cc2e9e8 100644
--- a/crates/fparkan-test-support/src/lib.rs
+++ b/crates/fparkan-test-support/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Dev-only synthetic builders and fake ports.
use fparkan_render::{FrameOutput, RenderBackend, RenderCommandList, RenderError};
diff --git a/crates/fparkan-texm/src/lib.rs b/crates/fparkan-texm/src/lib.rs
index 8747737..c73bb95 100644
--- a/crates/fparkan-texm/src/lib.rs
+++ b/crates/fparkan-texm/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Stage-3 Texm texture contract.
use std::sync::Arc;
diff --git a/crates/fparkan-vfs/src/lib.rs b/crates/fparkan-vfs/src/lib.rs
index 9ca57da..cd359a3 100644
--- a/crates/fparkan-vfs/src/lib.rs
+++ b/crates/fparkan-vfs/src/lib.rs
@@ -1,17 +1,36 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Virtual filesystem ports for resource loading.
use fparkan_binary::{sha256, Sha256Digest};
use fparkan_path::{ascii_lookup_key, join_under, NormalizedPath};
use std::collections::BTreeMap;
use std::fs;
-use std::path::{Path, PathBuf};
-use std::sync::{Arc, Mutex};
-use std::time::SystemTime;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, Mutex};
+use std::time::SystemTime;
/// VFS metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -335,11 +354,13 @@ impl MemoryVfs {
}
#[cfg(unix)]
+#[allow(clippy::unnecessary_wraps)]
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
- Some((metadata.dev() as u64).rotate_left(32) ^ metadata.ino())
+ Some(metadata.dev().rotate_left(32) ^ metadata.ino())
}
#[cfg(windows)]
+#[allow(clippy::unnecessary_wraps)]
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
Some(
(metadata.volume_serial_number() as u64).rotate_left(40)
@@ -378,11 +399,9 @@ impl Vfs for MemoryVfs {
let mut out = Vec::new();
for (path, bytes) in &self.files {
if has_segment_boundary_prefix_bytes(path, prefix.as_bytes()) {
- let normalized = fparkan_path::normalize_relative(
- path,
- fparkan_path::PathPolicy::StrictLegacy,
- )
- .map_err(|_| VfsError::Path)?;
+ let normalized =
+ fparkan_path::normalize_relative(path, fparkan_path::PathPolicy::StrictLegacy)
+ .map_err(|_| VfsError::Path)?;
out.push(VfsEntry {
path: normalized,
metadata: VfsMetadata {
@@ -564,7 +583,8 @@ mod tests {
fn memory_vfs_list_prefix_is_boundary_safe() {
let mut vfs = MemoryVfs::default();
let exact = normalize_relative(b"DATA/Land.map", PathPolicy::StrictLegacy).expect("path");
- let sibling = normalize_relative(b"DATA2/Land.map", PathPolicy::StrictLegacy).expect("path");
+ let sibling =
+ normalize_relative(b"DATA2/Land.map", PathPolicy::StrictLegacy).expect("path");
vfs.insert(exact.clone(), Arc::from(b"exact".as_slice()));
vfs.insert(sibling, Arc::from(b"sibling".as_slice()));
@@ -660,17 +680,20 @@ mod tests {
#[test]
fn memory_vfs_distinguishes_non_utf8_path_bytes() {
let mut vfs = MemoryVfs::default();
- let ascii = normalize_relative(b"DATA/normal.bin", PathPolicy::HostCompatible)
- .expect("ascii path");
- let binary = normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible)
- .expect("binary path");
+ let ascii =
+ normalize_relative(b"DATA/normal.bin", PathPolicy::HostCompatible).expect("ascii path");
+ let binary =
+ normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible).expect("binary path");
vfs.insert(ascii.clone(), Arc::from(b"ascii".as_slice()));
vfs.insert(binary.clone(), Arc::from(b"binary".as_slice()));
- let binary_query = normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible)
- .expect("binary query");
+ let binary_query =
+ normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible).expect("binary query");
- assert_eq!(vfs.read(&binary_query).expect("read binary").as_ref(), b"binary");
+ assert_eq!(
+ vfs.read(&binary_query).expect("read binary").as_ref(),
+ b"binary"
+ );
assert_eq!(vfs.read(&ascii).expect("read ascii").as_ref(), b"ascii");
}
diff --git a/crates/fparkan-world/src/lib.rs b/crates/fparkan-world/src/lib.rs
index 26ed5ad..5d659b9 100644
--- a/crates/fparkan-world/src/lib.rs
+++ b/crates/fparkan-world/src/lib.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
//! Deterministic world identity, queue, lifecycle, and snapshots.
use fparkan_binary::sha256;
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index bc67d01..2d0c080 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -71,8 +71,8 @@ S1-PATH-005 covered cargo test -p fparkan-path --offline rejects_escape
S1-PATH-006 covered cargo test -p fparkan-path --offline rejects_absolute_drive_and_nul_paths
S1-PATH-007 covered cargo test -p fparkan-path --offline join_under_keeps_normalized_path_below_root
S1-PATH-008 covered cargo test -p fparkan-path --offline original_separators_and_raw_bytes_are_preserved
-S1-H02 covered cargo test -p fparkan-path --offline accepts_non_utf8_legacy_bytes
-S1-M01 covered cargo test -p fparkan-vfs --offline memory_vfs_list_prefix_is_boundary_safe
+S1-PATH-009 covered cargo test -p fparkan-path --offline accepts_non_utf8_legacy_bytes
+S1-VFS-005 covered cargo test -p fparkan-vfs --offline memory_vfs_list_prefix_is_boundary_safe
S1-RSLI-001 covered cargo test -p fparkan-rsli --offline parses_minimal_empty_library
S1-RSLI-002 covered cargo test -p fparkan-rsli --offline rejects_invalid_header_fields
S1-RSLI-003 covered cargo test -p fparkan-rsli --offline rejects_entry_table_bounds
diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md
index f8ad743..5dd5a60 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -71,8 +71,8 @@
`S1-PATH-006`
`S1-PATH-007`
`S1-PATH-008`
-`S1-H02`
-`S1-M01`
+`S1-PATH-009`
+`S1-VFS-005`
`S1-RSLI-001`
`S1-RSLI-002`
`S1-RSLI-003`
diff --git a/fparkan_stage_0_2_audit_2026-06-23.md b/fparkan_stage_0_2_audit_2026-06-23.md
deleted file mode 100644
index 34dfb91..0000000
--- a/fparkan_stage_0_2_audit_2026-06-23.md
+++ /dev/null
@@ -1,1280 +0,0 @@
-# FParkan — аудит Stage 0–2 и план полного закрытия
-
-**Проект:** `valentineus/fparkan`
-**Проверенная ветка:** `devel` GitHub-зеркала
-**Дата аудита:** 23 июня 2026 года
-**Область:** Stage 0, Stage 1 и Stage 2 из документа «План реализации stage 0–5: Vulkan revision»
-**Метод:** статический архитектурный и кодовый аудит
-**Сборка и исполнение:** не выполнялись; `cargo build`, `cargo test`, Vulkan smoke и licensed corpus jobs не запускались
-
----
-
-## 1. Итоговый вердикт
-
-На проверенном состоянии ветки `devel` **ни один из Stage 0–2 нельзя считать закрытым**.
-
-| Stage | Статус | Закрытие exit gate | Главная причина |
-|---|---|---:|---|
-| Stage 0 — governance и Vulkan foundation | **BLOCKED** | 0 из 3 обязательных результатов подтверждены | Нет `winit`/Vulkan adapters и реального swapchain; workspace всё ещё содержит SDL/OpenGL stubs; CI gate неполон |
-| Stage 1 — paths, VFS и archives | **PARTIAL, NOT CLOSED** | 0 из 3 результатов подтверждены | Нет общего allocation budget во всех parsers/decompressors, полноценного RsLi edit/writer path, сквозных diagnostics и production-parser corpus gate |
-| Stage 2 — prototype graph и CPU assets | **BLOCKED BY ARCHITECTURE** | 0 из 3 результатов подтверждены | `fparkan-assets` не является единственным preparation layer; runtime и apps парсят данные напрямую; graph не хранит полноценные typed nodes/edges/provenance |
-
-Положительная сторона: проект уже имеет хороший фундамент — bounded cursor, NRes editor с preserved regions, generation handles, deterministic caches, строгую workspace lint policy, typed mission/model primitives, deterministic render command capture и частичное раскрытие unit/prototype/material/texture зависимостей. Эти наработки следует сохранить. Основная работа — не переписывание всего проекта, а **устранение архитектурных обходов, доведение safety contracts и добавление доказуемых acceptance gates**.
-
-### Решение по выпуску этапов
-
-- **Stage 0 нельзя закрывать декларативно.** Он закрывается только артефактами трёх нативных платформ: реальный Vulkan swapchain, triangle, resize, 300 frames и нулевые validation errors.
-- **Stage 1 нельзя закрывать только unit tests.** Нужны стабильные отчёты и byte-identical roundtrip на лицензированных каталогах обеих частей игры.
-- **Stage 2 нельзя закрывать текущими count-only reports.** Требуется материализованный dependency graph, typed provenance каждого edge и обязательный путь `runtime → AssetManager → immutable CPU assets`.
-
----
-
-## 2. Основание аудита
-
-Канонические критерии взяты из страницы Notion:
-
-- «План реализации stage 0–5: Vulkan revision»;
-- page ID: `387e79f2-db39-8177-8f94-cdf34db5f93f`;
-- URL: <https://app.notion.com/p/387e79f2db3981778f94cdf34db5f93f>.
-
-Проверялась ветка `devel` GitHub-зеркала:
-
-- <https://github.com/valentineus/fparkan/tree/devel>
-
-Ключевые проверенные файлы перечислены в разделе «Реестр доказательств».
-
-### Ограничения вывода
-
-1. Ветка `devel` является движущейся ссылкой; connected GitHub mirror не предоставил надёжный SHA текущего tip. Для повторяемости следующего formal audit следует запускать на закреплённом commit SHA/tag.
-2. README указывает на self-hosted primary repository. История его Gitea runners и закрытые CI artifacts в рамках этого аудита не были доступны.
-3. Licensed Part 1/Part 2 corpus не запускался. Поэтому любые утверждения о «нулевых failures», точных reachability counts и roundtrip являются **неподтверждёнными**, даже если соответствующие тестовые функции существуют в коде.
-4. Vulkan runtime не запускался и validation layers не проверялись.
-5. Отсутствие динамической проверки не мешает определить архитектурные blockers: manifests и composition roots однозначно показывают, что реального Vulkan vertical slice сейчас нет.
-
----
-
-## 3. Шкала статусов и приоритетов
-
-### Статус требования
-
-- **PASS** — реализация и статическое доказательство соответствуют требованию; динамический gate всё равно может оставаться непроверенным.
-- **PARTIAL** — существенная часть присутствует, но контракт или acceptance evidence неполны.
-- **FAIL** — требуемой реализации нет либо текущая архитектура ей противоречит.
-- **UNVERIFIED** — реализация может существовать, но нет доступного воспроизводимого доказательства.
-
-### Приоритет замечаний
-
-- **BLOCKER** — без исправления Stage невозможно закрыть.
-- **HIGH** — риск повреждения данных, unbounded resource use, неправильного dependency graph или ложноположительного acceptance.
-- **MEDIUM** — архитектурный долг, диагностическая неполнота или слабая тестируемость.
-- **LOW** — документация, ergonomics или cleanup, не являющиеся самостоятельным блокером.
-
----
-
-# 4. Stage 0 — Governance, reproducibility и Vulkan foundation
-
-## 4.1 Матрица требований
-
-| Требование Stage 0 | Статус | Доказательство текущего состояния | Что требуется для закрытия |
-|---|---|---|---|
-| Exact stable Rust toolchain и MSRV | **FAIL** | `rust-toolchain.toml` содержит только `channel = "stable"`; `workspace.package.rust-version` отсутствует | Закрепить точный toolchain, добавить MSRV и отдельный MSRV job |
-| Полный `cargo xtask ci` | **FAIL** | Сейчас выполняются custom rustfmt, policy, `cargo test --workspace --locked --offline` и clippy без полного набора flags; отсутствует обязательный doc gate | Реализовать канонический fmt/test/clippy/doc/security/source/license pipeline |
-| `--all-targets --all-features`, `-D warnings` | **FAIL** | В inspected `xtask` эти параметры отсутствуют | Добавить дословно и проверить negative tests самого xtask |
-| License/advisory/source policy | **PARTIAL/UNVERIFIED** | Есть custom policy и workspace license, но нет доказанного advisory/source gate и formal allowlist | Подключить `cargo-deny` или эквивалент с versioned policy; выгружать report artifact |
-| Typed TOML parsing и `cargo_metadata` | **FAIL** | Licensed manifest разбирается ручным line parser; `xtask/Cargo.toml` не зависит от TOML parser или `cargo_metadata` | Ввести serde-backed schema, deny unknown fields, canonical path validation, `cargo_metadata` |
-| CI matrix Windows/Linux/macOS | **UNVERIFIED, EXIT BLOCKER** | Канонический audit сам отмечает отсутствие подтверждённой hosted matrix; доступных run artifacts нет | Создать matrix и platform-native acceptance jobs с сохраняемыми reports |
-| `fparkan-platform-winit` | **FAIL** | В workspace присутствует `fparkan-platform-sdl`; он содержит in-memory stubs и не зависит от SDL. `winit` adapter отсутствует | Новый adapter с event loop, lifecycle, DPI, input, handles, suspend/resume, resize |
-| Backend-neutral platform contract | **FAIL** | Core port экспортирует `GraphicsProfile::DesktopCore/Embedded` и `GraphicsContextRequest`, то есть OpenGL concepts; `WindowPort::present()` смешивает window и GPU presentation | Удалить GL concepts; present перенести в render backend; ввести normalized lifecycle/input API |
-| `fparkan-render-vulkan` | **FAIL** | Workspace содержит `fparkan-render-gl`; crate только формирует canonical text capture и не вызывает GPU API | Создать Vulkan adapter с instance/device/surface/swapchain/pipeline/sync |
-| Реальный Vulkan triangle | **FAIL** | Ни `ash`, ни `ash-window`, ни `winit` не подключены; rendered apps используют `RecordingBackend` | Реальный indexed triangle в отдельном smoke app и composition roots |
-| Device scoring и capability report | **FAIL** | Реального device discovery нет | Pure policy module + Vulkan enumeration + deterministic JSON report |
-| Swapchain recreation | **FAIL** | Surface/swapchain отсутствуют | Обработать resize, zero extent, out-of-date, suboptimal, minimized/suspended states |
-| macOS portability enumeration/subset | **FAIL** | MoltenVK/Vulkan adapter отсутствуют | Instance portability flag, extension enumeration, subset feature report, packaged MoltenVK |
-| Offline SPIR-V build/validation | **FAIL** | Shader manifest/hash pipeline не найден; GL stub проверяет только empty/`#error` markers | Versioned shader sources, compiler pin, SPIR-V validator, descriptor manifest, embedded hashes |
-| Удаление прежних adapters/references | **FAIL** | Root workspace явно включает SDL и GL adapters; docs также содержат старую multi-backend формулировку | Удалить crates, lockfile refs, policy exceptions, docs и stale tests |
-| Headless без window/GPU deps | **PASS на manifest-level** | `fparkan-headless` зависит от runtime/VFS/world и не содержит adapter dependency | Добавить automated `cargo tree`/metadata assertion и target build gate |
-| 300-frame smoke + resize + validation | **FAIL/NOT RUNNABLE** | Нет реального backend | Добавить platform jobs и machine-readable validation report |
-| Negative loader/device/present/format tests | **FAIL** | Нет Vulkan error model | Dependency-injected policy layer + loader/device/surface failure fixtures |
-
-## 4.2 Критические замечания Stage 0
-
-### S0-B01 — Workspace всё ещё построен вокруг удаляемых SDL/OpenGL stub crates
-
-**Приоритет:** BLOCKER
-**Файлы:** `Cargo.toml`, `adapters/fparkan-platform-sdl`, `adapters/fparkan-render-gl`
-
-Root workspace включает оба прежних adapter crate. При этом:
-
-- SDL adapter не содержит зависимости на SDL и является deterministic stub;
-- GL adapter не содержит OpenGL binding и только сохраняет command captures;
-- tests этих crates доказывают поведение stubs, а не platform/GPU integration.
-
-Это создаёт опасный ложноположительный сигнал: названия adapters выглядят как реализованные backends, но фактически проверяется только модель интерфейса.
-
-**Рекомендация:** удалить legacy adapters только после появления `platform-winit` и `render-vulkan`, но до формального Stage 0 acceptance. На время миграции пометить их `legacy-proof`, исключить из default members и запретить composition roots ссылаться на них.
-
-### S0-B02 — Core platform API содержит OpenGL-specific contract
-
-**Приоритет:** BLOCKER
-**Файл:** `crates/fparkan-platform/src/lib.rs`
-
-Проблемы:
-
-- `GraphicsProfile` и `GraphicsContextRequest` описывают GL/GLES context, которого в Vulkan architecture нет;
-- `WindowPort::present()` неверно закрепляет presentation за window abstraction. В Vulkan presentation зависит от swapchain, queue, acquired image и semaphores и должен принадлежать render adapter;
-- `PlatformEvent` содержит только `Quit`;
-- отсутствуют DPI, resize, occlusion/minimize, focus, keyboard/mouse, lifecycle, suspend/resume и raw handles;
-- `PlatformError::Backend` не предоставляет context/source.
-
-**Целевой контракт:** platform crate сообщает события и предоставляет handle/size; render adapter владеет surface/swapchain/present.
-
-### S0-B03 — Нет реального Vulkan code path
-
-**Приоритет:** BLOCKER
-
-Ни один inspected manifest не подключает `ash`, `ash-window`, `winit` или `raw-window-handle`. `fparkan-game` использует `RecordingBackend`; `fparkan-viewer` является CLI inspector. Следовательно, instance/device/surface/swapchain не могут быть созданы текущим кодом.
-
-**Definition of fixed:** в workspace существует отдельный `fparkan-render-vulkan`; smoke executable проходит 300 frames, resize и clean shutdown на Windows, Linux и macOS/MoltenVK.
-
-### S0-B04 — CI command не соответствует каноническому gate
-
-**Приоритет:** BLOCKER
-**Файл:** `xtask/src/main.rs`
-
-Текущий `ci` не подтверждает:
-
-- all targets/features;
-- `-D warnings` для clippy;
-- rustdoc broken-link denial;
-- advisory/source policy;
-- platform adapter denylist;
-- отсутствие project-owned unsafe вне allowlist;
-- корректность самого acceptance manifest parser.
-
-Отдельный риск: custom recursion для rustfmt может расходиться с `cargo fmt --all -- --check` и форматировать/пропускать файлы иначе, чем Cargo workspace.
-
-### S0-B05 — Toolchain не воспроизводим
-
-**Приоритет:** BLOCKER
-**Файл:** `rust-toolchain.toml`
-
-`stable` меняется со временем. Один и тот же commit может пройти сегодня и не пройти после следующего stable release. Также не указан `rust-version`, поэтому минимально поддерживаемый компилятор не является контрактом.
-
-**Рекомендация:** точный channel вида `1.xx.y`, components и targets; `rust-version` в workspace package; controlled update PR с changelog и full matrix.
-
-### S0-H01 — Глобальный `unsafe_code = forbid` требует заранее спроектированного FFI boundary
-
-**Приоритет:** HIGH
-
-Полный запрет полезен для neutral crates, но raw Vulkan bindings неизбежно требуют узких unsafe calls. Нельзя ослаблять lint всему workspace.
-
-**Целевая схема:** отдельный low-level adapter crate или модуль, который:
-
-- не наследует blanket `forbid`;
-- устанавливает `deny(unsafe_op_in_unsafe_fn)`;
-- разрешает unsafe только в одном/нескольких audited modules;
-- требует `// SAFETY:` comment;
-- не экспортирует raw handles;
-- проходит custom policy scanner.
-
-### S0-H02 — Render command model пока недостаточен для последующих Vulkan assets
-
-**Приоритет:** HIGH, не блокирует самый первый hardcoded triangle
-
-`fparkan-render` имеет хорошую deterministic command/capture основу, но neutral API использует `GpuMeshId`/`GpuMaterialId` ещё до существования GPU resource registry. Для Stage 2/3 это смешивает CPU asset identity и backend allocation identity.
-
-**Рекомендация:** neutral layer должен оперировать `MeshAssetId`, `MaterialAssetId`, immutable draw items и legacy pipeline state. Vulkan adapter локально сопоставляет их с buffers/images/descriptors.
-
-### S0-M01 — Documentation drift
-
-**Приоритет:** MEDIUM
-
-`docs/tomes/07-implementation.md` всё ещё описывает старую последовательность этапов и допускает Vulkan/D3D11/Metal backend wording. `parity/README.md` ссылается на `crates/render-parity`, которого нет среди workspace members, а `parity/cases.toml` не содержит активных cases.
-
-Документация должна иметь один canonical stage source либо автоматически генерируемую проверку согласованности.
-
-## 4.3 Положительные элементы Stage 0
-
-- `Cargo.lock` и `--locked` упомянуты и используются в текущем workflow.
-- Workspace lint policy достаточно строгая для neutral crates.
-- Synthetic и licensed test paths концептуально разделены.
-- `fparkan-headless` не зависит от platform/render adapters на manifest-level.
-- `fparkan-render` уже предоставляет deterministic command ordering, validation и capture — это можно использовать как pre-GPU oracle.
-
-## 4.4 Обязательные изменения для полного закрытия Stage 0
-
-1. Закрепить toolchain/MSRV.
-2. Переписать xtask configuration на typed TOML + `cargo_metadata`.
-3. Завершить CI/security/doc gates.
-4. Пересмотреть platform port и удалить GL-specific types.
-5. Реализовать `fparkan-platform-winit`.
-6. Реализовать `fparkan-render-vulkan` с узким unsafe boundary.
-7. Добавить offline SPIR-V pipeline.
-8. Подключить adapters к game/viewer composition roots.
-9. Удалить legacy SDL/GL crates и stale docs.
-10. Запустить и сохранить acceptance artifacts на трёх OS.
-
----
-
-# 5. Stage 1 — Paths, VFS и archives
-
-## 5.1 Матрица требований
-
-| Требование Stage 1 | Статус | Доказательство текущего состояния | Что требуется для закрытия |
-|---|---|---|---|
-| Raw legacy bytes + normalized + ASCII key + host path | **PARTIAL** | Есть `OriginalPathBytes`, `NormalizedPath`, ASCII lookup и policies; нормализация сначала требует UTF-8 | Сделать byte-first identity; decoding только для diagnostics; определить OS host conversion |
-| Strict/compatible path policies | **PARTIAL/PASS** | Два режима есть и тестируются | Формализовать различия и применить одинаково во всех callers |
-| Casefold collision policy | **PARTIAL** | Directory/Memory VFS обнаруживают ambiguity; Overlay имеет deterministic precedence | Добавить segment-boundary tests и общий collision report contract |
-| Symlink-safe traversal | **PARTIAL/HIGH RISK** | Symlinks проверяются через `symlink_metadata`; однако check/open разделены | Capability-based/openat traversal, root confinement, cycle/escape tests |
-| Common `DecodeLimits` | **PARTIAL** | `fparkan-binary::Limits` существует, но многие parsers используют собственные constants или API без limits | Единый `DecodeContext` во всех parsers |
-| Common cumulative `AllocationBudget` | **FAIL** | Stateful budget не найден | Reservation/refund model для allocations и decompression output |
-| NRes lossless reader/editor/writer | **NEAR PASS, EXIT UNVERIFIED** | Есть lossless/canonical profiles, editor, preserved regions | Подключить limits/diagnostics; corpus roundtrip на обеих частях |
-| RsLi all observed decode methods | **PARTIAL** | Enum и implementations покрывают stored/XOR/LZSS/adaptive/deflate variants | Corpus proof, method coverage table, bounded output |
-| RsLi explicit compatibility profile | **PARTIAL** | AO, EOF+1 и invalid presort toggles есть | Versioned quirk registry с evidence IDs и per-file activation trace |
-| RsLi lossless writer/edit model | **FAIL** | `WriteProfile` содержит только возврат исходного image; editor/repack отсутствуют | Editable document, preserved unknowns, deterministic table rebuild, no-edit identity |
-| Structured diagnostics end-to-end | **FAIL** | Отдельный diagnostics crate существует, но NRes/RsLi/VFS/resource/prototype/runtime его не используют | Единый typed diagnostic envelope, source chain и offsets |
-| Generation handles | **PASS** | Resource repository хранит generation и выявляет stale handles | Сохранить и добавить concurrency/property tests |
-| Decoded byte cache budget | **PARTIAL/PASS** | Есть entry+byte limits и deterministic map | Отделить cache budget от decode allocation budget; test oversize rejection before allocation |
-| Decompression outside lock | **PASS статически** | Resource repository формирует task под lock, выполняет payload decode после release | Добавить concurrency regression test |
-| Typed resource errors | **PARTIAL/FAIL** | Верхний enum typed, но format/source часто превращаются в `String` | Сохранить concrete source errors и classify missing/archive/entry/corruption |
-| Corpus report использует production parsers | **FAIL** | Реально вызывается NRes parser; RsLi только определяется по magic, TMA/Land/unit metrics — по имени пути | Интегрировать все production parsers и считать parser errors failures |
-| Stable licensed reports и roundtrip | **UNVERIFIED, EXIT BLOCKER** | Нет запусков и artifacts в доступном аудите | Separate licensed jobs, signed manifests, report diff policy |
-
-## 5.2 Критические замечания Stage 1
-
-### S1-B01 — `Limits` не является обязательным contract всех decoders
-
-**Приоритет:** BLOCKER/HIGH
-**Файлы:** `fparkan-binary`, `fparkan-nres`, `fparkan-rsli`, `fparkan-mission-format`, другие format crates
-
-`fparkan-binary::Limits` определён, но:
-
-- NRes decode API не принимает limits/budget;
-- RsLi load/decompression не принимает общий output budget;
-- mission parser использует собственный набор `MAX_*` constants;
-- нет cumulative budget, отслеживающего сумму вложенных allocations;
-- cache byte limit действует после decode и не предотвращает decompression bomb.
-
-Проверка отдельного count недостаточна. Несколько допустимых массивов могут вместе превысить memory budget, а declared decompressed size может привести к крупному выделению до cache rejection.
-
-**Целевой API:**
-
-```rust
-pub struct DecodeContext<'a> {
- pub limits: &'a DecodeLimits,
- pub budget: &'a AllocationBudget,
- pub diagnostics: &'a dyn DiagnosticSink,
-}
-```
-
-Каждый allocation предваряется `reserve(bytes, category, span)`; nested decoders наследуют тот же budget.
-
-### S1-B02 — RsLi не имеет требуемого edit/writer model
-
-**Приоритет:** BLOCKER
-**Файл:** `crates/fparkan-rsli/src/lib.rs`
-
-Текущий lossless write profile фактически возвращает исходный byte image. Это полезный no-edit roundtrip, но не является edit model. Stage 1 требует:
-
-- редактирование entry metadata/payload;
-- сохранение unknown/overlay regions;
-- rebuild lookup table;
-- корректный packing method policy;
-- byte-identical no-edit;
-- deterministic edited output.
-
-Нужно разделить `OriginalImage`, parsed table, preserved segments и editable entries так же явно, как это уже сделано для NRes.
-
-### S1-B03 — Corpus report создаёт ложное впечатление production coverage
-
-**Приоритет:** BLOCKER
-**Файлы:** `crates/fparkan-corpus/Cargo.toml`, `src/lib.rs`
-
-Crate не зависит от `fparkan-rsli`, mission, terrain, prototype и прочих production parsers. В inspected implementation:
-
-- NRes действительно декодируется;
-- RsLi только распознаётся по magic;
-- TMA, Land и unit DAT считаются по extension/path patterns;
-- parser failure count поэтому не отражает значительную часть corpus.
-
-Фраза «corpus report использует реальные parsers» сейчас верна лишь частично. Exit gate «нулевые необъяснённые failures» нельзя доказать этим report.
-
-### S1-B04 — Structured diagnostics существуют отдельно от error path
-
-**Приоритет:** BLOCKER/HIGH
-
-`fparkan-diagnostics` имеет severity, phase, path, archive entry, object key, span и causes, однако format/resource/runtime crates не зависят от него. Вместо cause chain многие errors преобразуются в строки:
-
-- `ResourceError::Format(String)`;
-- `ResourceError::EntryRead { source: String }`;
-- `PrototypeError::Resource(String)`;
-- `AssetError::* (String)`.
-
-После преобразования невозможно надёжно классифицировать missing vs corrupt, извлечь source offset или сохранить concrete error chain.
-
-### S1-H01 — JSON serializer diagnostics некорректно обрабатывает часть control characters
-
-**Приоритет:** HIGH
-**Файл:** `crates/fparkan-diagnostics/src/lib.rs`
-
-Manual JSON escaping обрабатывает quote, backslash, `\n`, `\r`, `\t`, но не все символы U+0000–U+001F. Сообщение или path с backspace/form-feed/NUL может породить невалидный JSON.
-
-**Рекомендация:** использовать `serde`/`serde_json` для wire format; добавить property tests для всех Unicode/control characters и deterministic field ordering через typed serializable schema.
-
-### S1-H02 — Path identity остаётся UTF-8-first
-
-**Приоритет:** HIGH
-**Файл:** `crates/fparkan-path/src/lib.rs`
-
-Legacy data и host filenames могут содержать CP1251 или произвольные byte sequences. `normalize_relative()` сначала вызывает UTF-8 decode и отклоняет invalid UTF-8. Наличие `OriginalPathBytes` не решает проблему, потому что объект создаётся только после успешной UTF-8 normalization.
-
-**Целевая модель:** canonical legacy identity — bytes; separators/`.`/`..`/drive checks выполняются на ASCII bytes; decoded display name — отдельное необязательное поле.
-
-### S1-H03 — VFS symlink checks подвержены check/use race
-
-**Приоритет:** HIGH
-**Файл:** `crates/fparkan-vfs/src/lib.rs`
-
-Код проверяет `symlink_metadata`, затем открывает/read-ит pathname отдельной операцией. Между проверкой и open entry может быть заменён symlink. Для локального trusted corpus риск невысок, но Stage 1 заявляет безопасный substrate и malformed/adversarial tests.
-
-**Рекомендация:** directory capability/openat traversal, no-follow handles, проверка final object через handle metadata. `follow_symlinks=true` в corpus discovery должен иметь root-confinement и visited-file-id set.
-
-### S1-H04 — Fingerprint cache может пропустить content replacement
-
-**Приоритет:** HIGH/MEDIUM
-
-DirectoryVfs повторно использует SHA по `(path, len, modified)`. На filesystem с грубой timestamp resolution файл можно изменить, сохранив длину и mtime. Generation invalidation тогда не сработает.
-
-Для correctness-critical licensed reports рекомендуются:
-
-- unconditional SHA в audit mode;
-- либо file identity + ctime/change counter;
-- явное разделение fast interactive cache и strict verification mode.
-
-### S1-M01 — Prefix semantics MemoryVfs требуют segment boundary
-
-**Приоритет:** MEDIUM
-
-`list(prefix)` использует byte prefix. `DATA/FOO` может захватить `DATA/FOOBAR`. Нужна семантика `path == prefix || path starts with prefix + '/'` и одинаковые tests для всех VFS implementations.
-
-### S1-M02 — Собственная SHA-256 реализация увеличивает maintenance surface
-
-**Приоритет:** MEDIUM
-
-Криптографическая новизна проекту не нужна. Если custom implementation сохраняется ради zero dependencies/offline build, она должна иметь exhaustive standard vectors, differential tests и fuzzing. Иначе безопаснее использовать широко проверенный crate и закреплённую версию.
-
-## 5.3 Положительные элементы Stage 1
-
-- Bounded little-endian cursor и checked arithmetic уже централизованы.
-- NRes имеет strict/compatible read profiles, editor и preserved regions.
-- RsLi содержит явные compatibility switches и широкий набор методов decode.
-- VFS выявляет ASCII-casefold ambiguity и отвергает symlink entries в обычном path.
-- Resource handles имеют generation и stale detection.
-- Payload decode выполняется вне repository mutex.
-- Cache имеет entry/byte limits и deterministic data structure.
-- Corpus manifest использует SHA-256 и sorted traversal.
-
-## 5.4 Обязательные изменения для полного закрытия Stage 1
-
-1. Ввести обязательные `DecodeLimits` + cumulative `AllocationBudget`.
-2. Перевести все parsers/decompressors на context API.
-3. Сделать path model byte-first.
-4. Усилить VFS до handle/capability-based root confinement.
-5. Интегрировать structured diagnostics и typed source errors.
-6. Исправить JSON serialization.
-7. Завершить RsLi editor/writer.
-8. Подключить production parsers к corpus report.
-9. Добавить strict verification mode fingerprints.
-10. Зафиксировать licensed Part 1/2 reports и roundtrip artifacts.
-
----
-
-# 6. Stage 2 — Prototype graph и CPU assets
-
-## 6.1 Матрица требований
-
-| Требование Stage 2 | Статус | Доказательство текущего состояния | Что требуется для закрытия |
-|---|---|---|---|
-| `objects.rlb` decode | **PARTIAL** | Есть 64-byte record decoder и registry resolution | Corpus variants, lossless model, typed provenance и failure spans |
-| Unit DAT decode | **PARTIAL** | Есть component records и binding variant | Формальная variant discrimination, all observed records, hierarchy semantics |
-| Inheritance/depth handling | **PARTIAL/UNVERIFIED** | Есть depth-limit constant и resolution code | Cycle path, parent edge nodes, BASE/resource variants, corpus proof |
-| Все unit components | **PARTIAL** | Internal graph expansion итерирует records; public `resolve_prototype` всё ещё возвращает только first component | Удалить/ограничить lossy API; graph хранит каждый component и hierarchy |
-| Typed graph nodes/edges | **FAIL** | `PrototypeGraph` хранит только roots и flattened prototype requests | Materialized graph arena с stable node IDs и typed edge instances |
-| Typed provenance каждого edge | **FAIL** | Есть enum kind и count report, но нет parent chain/edge instance/source span | `Provenance` с mission object, component index, archive/entry/span, parent edge |
-| Effect edge | **FAIL** | `PrototypeGraphEdge` не содержит effect path; `fparkan-assets` не зависит от FX crate | Добавить typed effect assets и graph reachability |
-| BASE/resource variants | **UNVERIFIED/LIKELY PARTIAL** | В доступных contracts нет полноценной variant model/provenance | Явный enum variant, evidence-backed parser, fixtures |
-| `fparkan-assets` — единственный preparation layer | **FAIL** | Prototype crate сам зависит от MSH/material/Texm; runtime напрямую вызывает format/prototype parsers; viewer парсит напрямую | Перестроить dependency DAG и запретить parser deps в apps/runtime |
-| Immutable prepared CPU assets | **FAIL/PARTIAL** | `PreparedVisual` в основном содержит keys/counts, а не mesh/material/texture data | Immutable `MeshAsset`, `MaterialAsset`, `TextureAsset`, `MissionAssets` |
-| Stable IDs | **PARTIAL/HIGH RISK** | 64-bit FNV-like hash от geometry key без collision registry | Canonical key interner/content hash + collision detection + schema version |
-| Structured graph failure parent chain | **FAIL** | Failure содержит root index, edge enum и message string | Full chain и concrete typed cause, not string |
-| Optional fallback vs corruption | **FAIL/PARTIAL** | Есть optional read helper, но classification не является graph-wide contract | Requiredness enum + severity policy + explicit fallback provenance |
-| No ad-hoc parsing in apps/runtime | **FAIL** | Runtime зависит от NRes/mission/terrain/prototype; viewer вызывает decoders напрямую | Только AssetManager/mission loader ports; lint dependency denylist |
-| Deterministic graph order/IDs | **PARTIAL/UNVERIFIED** | Некоторые sorted/BTree structures и stable hasher есть | Canonical traversal spec, golden graph serialization, cross-run/cross-OS tests |
-| Licensed Part 1/2 zero-failure reachability | **UNVERIFIED, EXIT BLOCKER** | Нет доступных run artifacts; inspected game corpus test помечен `#[ignore]` | Full mission matrix reports, expected counts, zero unexplained failures |
-
-## 6.2 Критические замечания Stage 2
-
-### S2-B01 — `fparkan-prototype` и `fparkan-assets` нарушают целевое разделение ответственности
-
-**Приоритет:** BLOCKER
-**Файлы:** `crates/fparkan-prototype/Cargo.toml`, `crates/fparkan-assets/Cargo.toml`
-
-Prototype crate зависит от:
-
-- material;
-- MSH;
-- NRes;
-- Texm;
-- resource/VFS.
-
-И сам расширяет graph report visual dependencies. Затем `fparkan-assets` повторно декодирует MSH, WEAR, MAT0 и Texm. Возникают два preparation paths, которые со временем неизбежно разойдутся по fallback, diagnostics и budgets.
-
-**Целевой DAG:**
-
-```text
-mission/prototype formats ──> prototype graph (keys + provenance only)
- │
- v
-resource repository ─────────> fparkan-assets (all CPU decoding/preparation)
- │
- v
-runtime/world ────────────────> immutable MissionAssets
- │
- v
-render bridge ────────────────> backend-neutral draw items
-```
-
-Prototype layer не должен декодировать render assets.
-
-### S2-B02 — Runtime не использует `fparkan-assets`
-
-**Приоритет:** BLOCKER
-**Файлы:** `crates/fparkan-runtime/Cargo.toml`, `src/lib.rs`
-
-Runtime manifest не содержит dependency на `fparkan-assets`, зато напрямую зависит от NRes, mission-format, prototype, terrain-format и resource. `LoadedMissionState` хранит `Vec<EffectivePrototype>`, а не prepared immutable assets.
-
-Это прямо противоречит exit gate: «runtime получает prepared assets только через asset manager».
-
-### S2-B03 — Apps продолжают ad-hoc parsing и synthetic resource mapping
-
-**Приоритет:** BLOCKER
-
-- Viewer напрямую вызывает NRes/MSH/Texm/terrain decoders.
-- Game создаёт `GpuMeshId(slot + 1)`, constant material ID и triangle range, вместо prepared mission assets.
-- Ни game, ни viewer не подключают platform/Vulkan adapters.
-
-Apps должны быть composition roots, а не дополнительным parser/service layer.
-
-### S2-B04 — `PrototypeGraph` не является materialized dependency graph
-
-**Приоритет:** BLOCKER
-**Файл:** `crates/fparkan-prototype/src/lib.rs`
-
-Текущая структура содержит только:
-
-- roots;
-- flattened prototype requests.
-
-Report содержит counts и failures, но не даёт:
-
-- stable node identity;
-- конкретные edge instances;
-- parent/child traversal;
-- deduplication semantics;
-- source spans;
-- full provenance chain;
-- serialization для golden comparison.
-
-Для Stage 2 нужен arena/adjacency graph, а report должен вычисляться из него, а не заменять его.
-
-### S2-B05 — Публичный resolver теряет multi-component unit
-
-**Приоритет:** BLOCKER/HIGH
-
-`resolve_prototype()` для DAT вызывает helper, возвращающий первый resolved component. Хотя другой internal path обходит все records, наличие публичного lossy API нарушает invariant Stage 2 и создаёт риск использования «первого visual» в новом caller.
-
-**Рекомендация:** удалить этот API либо переименовать в явно lossy diagnostic helper; основной API всегда возвращает collection/subgraph.
-
-### S2-H01 — Missing unit DAT может быть преобразован в пустое успешное expansion
-
-**Приоритет:** HIGH, требует подтверждающего regression test
-
-В inspected helper `VfsError::NotFound` превращается в `Ok` с `expected_count = 0` и пустым списком. Если caller не создаёт отдельный failure до/после этого вызова, reachable missing dependency исчезает из failure set.
-
-Требуемое поведение:
-
-- reachable + required → typed failure;
-- unreachable → warning/report record;
-- optional → explicit fallback edge;
-- corrupt → error независимо от optionality, если ресурс найден и malformed.
-
-### S2-H02 — Provenance enum недостаточен и не покрывает весь канонический список
-
-**Приоритет:** HIGH
-
-Текущий enum описывает несколько типов переходов, но отсутствуют или не материализованы:
-
-- prototype inheritance parent;
-- BASE/resource variant;
-- component hierarchy/link;
-- effect/FX dependency;
-- fallback source;
-- source archive entry/span;
-- exact mission object identity.
-
-`message: String` не заменяет provenance.
-
-### S2-H03 — `PreparedVisual` является summary, а не prepared asset
-
-**Приоритет:** HIGH
-
-Структура содержит ResourceKey и counts (`model_nodes`, `material_count`, …), но не immutable vertex/index streams, materials, decoded texture mip data, lightmap bindings или dependency handles. Она пригодна для audit report, но не как sole CPU asset handoff в renderer/runtime.
-
-Нужно разделить:
-
-- `PreparedVisualSummary` для отчётов;
-- `MeshAsset`;
-- `MaterialAsset`;
-- `TextureAsset`;
-- `LightmapAsset`;
-- `EffectAsset`;
-- `VisualAsset` с typed IDs/handles;
-- `MissionAssets` как транзакционно подготовленный набор.
-
-### S2-H04 — Stable ID без collision handling
-
-**Приоритет:** HIGH/MEDIUM
-
-64-bit FNV-like hash удобен и детерминирован, но collision не проверяется. Stable ID — часть persistent captures и graph comparison, поэтому молчаливая коллизия недопустима.
-
-**Рекомендация:** canonical key interner с equality check; ID может быть SHA-256 prefix/128-bit hash либо deterministic ordinal после canonical sort. При hash collision должна возникать explicit error.
-
-### S2-H05 — Errors теряют concrete causes
-
-**Приоритет:** HIGH
-
-Prototype и assets преобразуют Resource/MSH/Material/Texture errors в строки. В результате graph failure не может отличить missing archive, missing entry, malformed offsets, unsupported variant и allocation limit.
-
-Это одновременно блокирует Stage 1 diagnostics и Stage 2 optional/corrupt policy.
-
-### S2-M01 — Graph success predicate слишком зависим от aggregate counts
-
-**Приоритет:** MEDIUM/HIGH
-
-`PrototypeGraphReport::is_success()` опирается на отсутствие failures и соответствие aggregate resolved count. Такой predicate не доказывает, что:
-
-- каждый material/texture/lightmap/effect request resolved;
-- каждый edge имеет provenance;
-- отсутствуют orphan/duplicate nodes;
-- requiredness policy применена;
-- graph deterministic.
-
-Success должен вычисляться как validation materialized graph с инвариантами.
-
-## 6.3 Положительные элементы Stage 2
-
-- Unit DAT records сохраняют raw archive/resource/description bytes и parent/link field.
-- Есть отдельный binding variant и CP1251-related support.
-- Internal expansion обходит несколько component records.
-- Есть depth limit для prototype inheritance.
-- Report считает mesh, WEAR, material, texture и lightmap requests/resolutions.
-- `AssetId<T>` typed на уровне Rust.
-- Asset preparation выполняется транзакционно на уровне метода: ошибка прерывает план.
-- Stable ordering поддерживается рядом BTree collections и sorted traversal.
-- Mission loading имеет staged phases и transactional world registration concepts.
-
-## 6.4 Обязательные изменения для полного закрытия Stage 2
-
-1. Сделать prototype crate graph-only и убрать из него MSH/material/Texm decoding.
-2. Создать materialized typed graph с stable nodes/edge instances.
-3. Добавить full provenance и typed requiredness/fallback.
-4. Завершить variants: direct, inherited, BASE, unit component hierarchy, effects.
-5. Превратить `fparkan-assets` в единственный decode/preparation layer.
-6. Создать реальные immutable CPU asset types.
-7. Подключить AssetManager к runtime и удалить direct parser dependencies.
-8. Перевести apps на runtime/asset services.
-9. Ввести deterministic ID registry с collision detection.
-10. Запустить полный Part 1/2 mission reachability gate и зафиксировать zero failures.
-
----
-
-# 7. Сквозные архитектурные замечания
-
-## 7.1 Нужен единый dependency policy, проверяемый Cargo metadata
-
-Текущая архитектура допускает запрещённые направления зависимостей. Следует формально проверить:
-
-- `apps/*` не зависят от format parsers;
-- `runtime` не зависит от NRes/RsLi/MSH/Texm/material/FX format crates напрямую;
-- `prototype` не зависит от visual asset parsers;
-- neutral crates не зависят от `ash`, `winit`, raw OS APIs;
-- `headless` dependency closure не содержит platform/Vulkan crates;
-- Vulkan raw types не выходят из adapter.
-
-Policy должна использовать `cargo_metadata`, а не поиск строк в TOML.
-
-## 7.2 Один error taxonomy для Stage 1–2
-
-Рекомендуемый общий набор классификаций:
-
-```text
-MissingArchive
-MissingEntry
-AmbiguousName
-InvalidPath
-UnsupportedVariant
-CorruptHeader
-OutOfBounds
-LimitExceeded
-AllocationBudgetExceeded
-DecompressionFailed
-IntegrityMismatch
-OptionalUnavailable
-FallbackApplied
-PlatformUnavailable
-GpuCapabilityMissing
-```
-
-Concrete format errors остаются source; верхние layers добавляют context, не превращая source в строку.
-
-## 7.3 Acceptance report должен быть доказательством, а не count dump
-
-Каждый stage report должен включать:
-
-- schema version;
-- engine commit SHA;
-- toolchain version;
-- platform/target;
-- corpus manifest fingerprint;
-- configuration/profile;
-- counts;
-- warnings/failures с stable codes;
-- evidence status;
-- SHA-256 самого report;
-- ссылки на dependent artifacts.
-
-## 7.4 Документация и код должны иметь один source of truth
-
-Сейчас canonical Vulkan revision находится в Notion, а repository tome и parity docs содержат прежние stages/commands. Рекомендуется:
-
-- хранить canonical acceptance schema в repository;
-- либо экспортировать Notion plan в versioned Markdown;
-- CI должен проверять, что workspace adapter names и documented architecture совпадают;
-- устаревшие команды должны быть executable doctests или удалены.
-
----
-
-# 8. План полного закрытия Stage 0–2
-
-Ниже приведена рекомендуемая последовательность PR/changesets. Порядок важен: следующий блок не должен объявляться закрытым до прохождения exit gate предыдущего.
-
-## 8.1 Stage 0 closure train
-
-### PR S0-01 — Reproducible toolchain и repository metadata
-
-**Изменения**
-
-- закрепить exact Rust toolchain;
-- добавить `workspace.package.rust-version`;
-- документировать supported targets;
-- добавить command `xtask doctor` с выводом toolchain/target/SDK versions;
-- report всегда включает commit SHA.
-
-**Acceptance**
-
-- clean checkout воспроизводит одинаковый metadata report;
-- MSRV job собирает neutral crates;
-- current pinned toolchain выполняет полный gate.
-
-### PR S0-02 — Typed xtask configuration и policy engine
-
-**Изменения**
-
-- serde/TOML schema для corpus и acceptance manifests;
-- `deny_unknown_fields`;
-- canonical absolute path rules для licensed manifest;
-- `cargo_metadata` для dependency graph/policy;
-- tests malformed/duplicate/unknown/missing fields;
-- удалить ручной line parser.
-
-**Acceptance**
-
-- malformed manifest не принимается частично;
-- unknown fields fail;
-- dependency policy работает по package IDs и targets/features.
-
-### PR S0-03 — Полный synthetic CI gate
-
-**Обязательные команды**
-
-```text
-cargo fmt --all -- --check
-cargo test --workspace --all-targets --all-features --locked
-cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
-RUSTDOCFLAGS="-D warnings -D rustdoc::broken_intra_doc_links" cargo doc --workspace --no-deps --all-features --locked
-cargo deny check advisories bans licenses sources
-cargo xtask policy
-cargo xtask acceptance audit --strict
-```
-
-Добавить denylist legacy adapter names, Python runtime files и unsafe allowlist.
-
-### PR S0-04 — Redesign `fparkan-platform`
-
-**Изменения**
-
-- удалить GL context types;
-- убрать `present()` из WindowPort;
-- ввести normalized events;
-- выделить physical/logical size и scale factor;
-- определить lifecycle state machine;
-- error context/cause.
-
-**Synthetic tests**
-
-- resize coalescing;
-- scale-factor change;
-- minimized zero-size;
-- suspend/resume;
-- keyboard repeat/modifiers;
-- focus loss clears held input;
-- deterministic event ordering.
-
-### PR S0-05 — `fparkan-platform-winit`
-
-**Изменения**
-
-- winit event loop и window;
-- raw window/display handles;
-- platform-specific lifecycle mapping;
-- no GPU ownership.
-
-**Acceptance**
-
-- window-only smoke на трёх OS;
-- event trace соответствует synthetic model.
-
-### PR S0-06 — Vulkan low-level boundary
-
-**Изменения**
-
-- `ash`/`ash-window` adapter;
-- loader/instance/debug messenger;
-- physical device records и pure scoring;
-- queue selection;
-- deterministic capability JSON;
-- narrow unsafe module policy.
-
-**Negative tests**
-
-- no loader;
-- no Vulkan 1.1;
-- no graphics queue;
-- no present queue;
-- missing swapchain extension;
-- unsupported required format.
-
-### PR S0-07 — Swapchain, pipeline и offline shaders
-
-**Изменения**
-
-- surface/swapchain;
-- color/depth policy;
-- render pass;
-- indexed triangle;
-- semaphores/fences;
-- frames-in-flight;
-- resize/out-of-date/suboptimal;
-- offline SPIR-V compile/validate;
-- descriptor/push-constant manifest и hashes.
-
-### PR S0-08 — macOS portability и packaging proof
-
-**Изменения**
-
-- enumerate portability flag;
-- detect/enable portability subset;
-- MoltenVK bundling strategy;
-- deterministic portability report;
-- `.app` smoke packaging.
-
-### PR S0-09 — Composition roots и legacy removal
-
-**Изменения**
-
-- game/viewer подключают winit+Vulkan adapters;
-- headless остаётся isolated;
-- удалить SDL/GL stubs;
-- очистить lockfile/policy/docs;
-- переименовать CPU IDs в neutral render model.
-
-### PR S0-10 — Native acceptance matrix
-
-**Jobs**
-
-- Windows MSVC + Vulkan runtime;
-- Linux X11/Wayland smoke; software Vulkan может быть PR gate, отдельный native-GPU job — release gate;
-- macOS Apple Silicon + MoltenVK;
-- 300 frames, resize, validation error count = 0;
-- capability/shader/validation logs как artifacts.
-
-**Stage 0 закрывается только после merge всех PR S0-01…S0-10 и зелёных platform artifacts.**
-
----
-
-## 8.2 Stage 1 closure train
-
-### PR S1-01 — Unified decode context
-
-- `DecodeLimits` с per-format overrides;
-- thread-safe cumulative `AllocationBudget`;
-- reservation guards;
-- output budget для decompressors;
-- span-aware errors;
-- migrate all decoders.
-
-### PR S1-02 — Byte-first paths и host adapter
-
-- `LegacyPath(Vec<u8>)`;
-- ASCII normalization на bytes;
-- optional decoded display;
-- Unix `OsStr` bytes path;
-- Windows conversion policy с explicit encoding/error;
-- strict/compatible contract table.
-
-### PR S1-03 — VFS hardening
-
-- capability/openat-style traversal;
-- no-follow final open;
-- root confinement;
-- visited file identity при allowed symlinks;
-- segment-safe prefix semantics;
-- strict fingerprint mode;
-- одинаковый casefold policy во всех implementations.
-
-### PR S1-04 — Diagnostics integration
-
-- serde-backed diagnostic schema;
-- stable codes;
-- source chain;
-- archive/entry/span/phase context;
-- adapters для all format errors;
-- property tests JSON;
-- запрет `source.to_string()` в domain errors через policy/lint review.
-
-### PR S1-05 — NRes finalization
-
-- decode context integration;
-- edit preservation tests;
-- stable directory order specification;
-- malformed/fuzz corpus;
-- Part 1/2 no-edit byte identity.
-
-### PR S1-06 — RsLi editable/lossless model
-
-- parsed/preserved/editable segments;
-- deterministic table/lookup rebuild;
-- packing policy;
-- all observed methods with budgets;
-- AO/EOF+1/presort quirk evidence registry;
-- no-edit and edited roundtrips.
-
-### PR S1-07 — Resource repository finalization
-
-- typed source errors;
-- cache/decode budgets separated;
-- deterministic LRU policy documented;
-- concurrent same-entry decode coalescing или explicit duplicate policy;
-- stale handle/concurrency tests;
-- strict verification mode.
-
-### PR S1-08 — Production corpus runner
-
-- parser registry, а не extension counters;
-- NRes, RsLi и все Stage 1-relevant production parsers;
-- any parser error increments failures и causes non-zero exit;
-- stable schema and diff;
-- no licensed path leakage in synthetic artifacts.
-
-### PR S1-09 — Licensed closure artifacts
-
-Для Part 1 и Part 2 отдельно:
-
-- corpus manifest SHA;
-- archive inventory;
-- parser report;
-- method/quirk coverage;
-- no-edit roundtrip report;
-- edit-preservation regression set;
-- unexplained failures = 0.
-
-**Stage 1 закрывается только после S1-01…S1-09 и двух стабильных licensed reports.**
-
----
-
-## 8.3 Stage 2 closure train
-
-### PR S2-01 — Typed graph schema
-
-Ввести:
-
-```text
-NodeId
-NodeKind { MissionObject, UnitDat, UnitComponent, Prototype, Model,
- Wear, Material, Texture, Lightmap, Effect, Auxiliary }
-EdgeId
-EdgeKind
-Requiredness { Required, Optional, Fallback }
-Provenance { source path/archive/entry/span, parent edge, object/component indices }
-DependencyGraph { nodes, edges, roots }
-```
-
-Graph validation проверяет no dangling edges, canonical order, unique node keys и parent chain.
-
-### PR S2-02 — Prototype resolver variants
-
-- direct registry;
-- inherited parent chain;
-- BASE/resource variants;
-- unit DAT binding;
-- multi-component unit hierarchy;
-- cycle/depth diagnostics;
-- lossless raw fields;
-- remove first-component public API.
-
-### PR S2-03 — Requiredness и failure semantics
-
-- reachable required missing = error;
-- unreachable missing = warning;
-- optional missing = explicit optional record;
-- fallback = edge с причиной и chosen source;
-- found-but-corrupt = error;
-- stable diagnostic codes and full chain.
-
-### PR S2-04 — Разделение prototype/assets
-
-- убрать MSH/material/Texm dependencies из prototype;
-- graph хранит только resource identities/provenance;
-- `fparkan-assets` единолично вызывает visual/effect parsers;
-- policy запрещает обратные dependencies.
-
-### PR S2-05 — Immutable CPU assets и ID registry
-
-- typed immutable model/material/texture/lightmap/effect assets;
-- canonical IDs;
-- collision detection;
-- deduplication;
-- source provenance;
-- separate parsed-byte and resident-asset budgets.
-
-### PR S2-06 — Mission AssetManager transaction
-
-`AssetManager::prepare_mission(graph)`:
-
-1. валидирует graph;
-2. вычисляет canonical load plan;
-3. декодирует resources в budget;
-4. собирает immutable assets;
-5. не публикует partial result при failure;
-6. возвращает `MissionAssets` + report.
-
-### PR S2-07 — Runtime integration
-
-- runtime зависит от assets API;
-- удалить direct NRes/MSH/Texm/material/effect parsing из runtime;
-- `LoadedMissionState` хранит `Arc<MissionAssets>`;
-- world objects ссылаются на typed asset IDs;
-- render snapshot строится из prepared visuals, а не из object slot.
-
-### PR S2-08 — App cleanup
-
-- viewer использует AssetManager service;
-- CLI parser-level commands остаются только в dedicated tooling crate;
-- game не генерирует synthetic GPU IDs;
-- dependency policy запрещает app → format crates.
-
-### PR S2-09 — Synthetic graph suite
-
-Обязательные fixtures:
-
-- direct prototype;
-- inherited prototype;
-- multi-level inheritance;
-- cycle;
-- depth limit;
-- BASE variant;
-- multi-component unit с hierarchy;
-- duplicate/casefold ambiguity;
-- required missing;
-- optional missing;
-- corrupt present resource;
-- material/texture/lightmap/effect chain;
-- stable serialization/IDs на повторном запуске.
-
-### PR S2-10 — Licensed reachability closure
-
-Для каждой миссии обеих частей:
-
-- roots;
-- unit components;
-- prototype/model/material/texture/lightmap/effect nodes;
-- required/optional/fallback counts;
-- failure list;
-- canonical graph hash;
-- asset plan hash.
-
-Exit conditions:
-
-- reachable failures = 0;
-- каждый resolved edge имеет provenance;
-- повторный запуск даёт те же graph/asset hashes;
-- runtime parser dependency audit = clean.
-
-**Stage 2 закрывается только после S2-01…S2-10 и полного licensed mission matrix.**
-
----
-
-# 9. Требуемая CI/acceptance модель
-
-## 9.1 Synthetic PR gate
-
-Должен работать без игровых каталогов и без silent skip:
-
-1. formatting/lints/docs/security;
-2. all unit/integration/property tests;
-3. malformed parser corpus;
-4. Stage 0 pure policy tests;
-5. Stage 1 archive synthetic roundtrips;
-6. Stage 2 graph synthetic fixtures;
-7. headless dependency assertion;
-8. Linux software-Vulkan smoke при доступности;
-9. report schema validation.
-
-Любой test, требующий licensed files, должен быть в отдельном command suite, а не `#[ignore]` внутри общего gate без machine-readable explanation.
-
-## 9.2 Native platform gate Stage 0
-
-| Platform | Минимальный gate | Дополнительное доказательство |
-|---|---|---|
-| Windows | system Vulkan loader, real swapchain, triangle, resize, 300 frames, validation=0 | NVIDIA/AMD/Intel coverage по release cadence |
-| Linux | X11 или Wayland surface, swapchain, resize, validation=0 | software Vulkan PR job + native Mesa/NVIDIA release jobs |
-| macOS | MoltenVK, portability enumeration/subset, CAMetalLayer surface, resize, validation=0 | Apple Silicon primary; Intel optional only if declared supported |
-
-## 9.3 Licensed local/restricted gate
-
-- absolute roots только из local manifest;
-- CI logs не содержат raw licensed paths;
-- reports используют logical corpus IDs;
-- artifacts не содержат game bytes;
-- manifests содержат только path hashes/size/format metrics;
-- failures приводят к non-zero exit;
-- baseline update требует reviewed diff и reason.
-
----
-
-# 10. Definition of Done
-
-## 10.1 Stage 0 DoD
-
-- [ ] Exact Rust toolchain и MSRV закреплены.
-- [ ] Full fmt/test/clippy/doc/security/source/license gate проходит.
-- [ ] Typed manifests и `cargo_metadata` используются.
-- [ ] Windows/Linux/macOS matrix сохраняет artifacts.
-- [ ] `fparkan-platform-winit` реализован.
-- [ ] `fparkan-render-vulkan` реализован.
-- [ ] Vulkan 1.1 baseline и capability report реализованы.
-- [ ] MoltenVK portability path реализован.
-- [ ] Offline SPIR-V validation/hash manifest реализован.
-- [ ] Legacy SDL/GL adapters и references удалены.
-- [ ] Game/viewer используют новые composition adapters.
-- [ ] 300-frame + resize smoke проходит на трёх OS без validation errors.
-- [ ] Headless dependency closure не содержит window/Vulkan.
-
-## 10.2 Stage 1 DoD
-
-- [ ] Path identity byte-first и roundtrip-safe.
-- [ ] VFS root/symlink/casefold semantics едины и безопасны.
-- [ ] Все decoders принимают общий limits/budget context.
-- [ ] Decompression output bounded до allocation.
-- [ ] NRes no-edit и edited roundtrips подтверждены.
-- [ ] RsLi no-edit и edited roundtrips подтверждены.
-- [ ] All observed RsLi methods/quirks имеют evidence records.
-- [ ] Structured diagnostics проходят через parsers/repository/runtime.
-- [ ] JSON reports валидны для всех Unicode/control inputs.
-- [ ] Resource repository сохраняет typed errors, handles и deterministic eviction.
-- [ ] Corpus report вызывает production parsers.
-- [ ] Part 1/2 reports стабильны; unexplained failures = 0.
-
-## 10.3 Stage 2 DoD
-
-- [ ] Materialized typed dependency graph существует.
-- [ ] Все edge instances имеют full provenance.
-- [ ] Direct/inherited/BASE/unit hierarchy variants реализованы.
-- [ ] Все unit components регистрируются; first-component shortcut отсутствует.
-- [ ] Effect dependencies включены.
-- [ ] Required/optional/fallback/corrupt semantics разделены.
-- [ ] `fparkan-assets` — единственный CPU preparation layer.
-- [ ] Apps/runtime не зависят от format parsers напрямую.
-- [ ] Immutable CPU assets и collision-safe stable IDs реализованы.
-- [ ] Runtime хранит/использует `MissionAssets`.
-- [ ] Render snapshots используют prepared asset IDs.
-- [ ] Synthetic graph fixtures проходят.
-- [ ] Все миссии Part 1/2 дают reachable failures = 0.
-- [ ] Graph/asset hashes детерминированы.
-
----
-
-# 11. Рекомендуемые automated policy checks
-
-Добавить в `cargo xtask policy`:
-
-1. Запрет workspace members/path names:
- - `fparkan-platform-sdl`;
- - `fparkan-render-gl`;
- - stale OpenGL/GLES profile symbols.
-2. Dependency rules:
- - apps не зависят от `fparkan-*format`, NRes, RsLi, MSH, Texm, material, FX;
- - runtime не зависит от этих crates напрямую;
- - prototype не зависит от MSH/material/Texm/FX;
- - headless не зависит от winit/ash/ash-window/Vulkan adapter;
- - neutral crates не зависят от platform adapters.
-3. Unsafe rules:
- - unsafe разрешён только в exact Vulkan/FFI modules;
- - каждый block содержит `SAFETY:`;
- - raw handles не являются public API.
-4. Test rules:
- - synthetic gate не содержит licensed roots;
- - ignored tests должны иметь registered reason и отдельный suite owner;
- - acceptance IDs уникальны.
-5. Documentation rules:
- - documented crates/commands существуют;
- - stage schema version совпадает с report schema;
- - old backend names отсутствуют в canonical docs.
-
----
-
-# 12. Риски реализации и способы снижения
-
-| Риск | Влияние | Снижение |
-|---|---|---|
-| Vulkan work начнётся до исправления platform contract | Повторная переделка surface/present/lifecycle | Сначала S0-04, затем adapters |
-| Global unsafe prohibition будет ослаблен целиком | Рост FFI risk | Изолированный audited crate/module и policy scanner |
-| Stage 1 budgets добавят только к top-level files | Nested decompression bomb останется | Общий shared budget, передаваемый во все вложенные decoders |
-| RsLi writer будет canonical-only | Потеря unknown/overlay bytes | Segment-preserving editable model и lossless-first tests |
-| Graph report останется count-only | False green Stage 2 | Materialized graph + invariant validator + canonical serialization |
-| Prototype и assets продолжат оба парсить visuals | Divergent fallback и diagnostics | Жёсткий dependency DAG и policy test |
-| Hash IDs столкнутся | Неправильные assets/captures | Collision detection и canonical interner |
-| Licensed tests останутся ignored/local-only без artifacts | Невозможность доказать exit gate | Separate command, signed reports, baseline diff process |
-| GitHub mirror и primary diverge | Audit не соответствует release | Pin canonical remote + commit SHA в каждом report |
-| Documentation останется отдельной от acceptance schema | Повторное рассогласование stages | Versioned repository schema и generated docs/checks |
-
----
-
-# 13. Приоритетный backlog
-
-## Немедленно — до любых новых gameplay/render features
-
-1. S0-01…S0-04: reproducibility, CI, typed config, platform API.
-2. S1-01: общий decode/allocation budget.
-3. S2-04: запрет дублирующего parsing между prototype/assets.
-4. S1-04: typed diagnostics без string erasure.
-
-## Затем — минимальный доказуемый Vulkan foundation
-
-1. winit adapter.
-2. Vulkan loader/device/surface/swapchain.
-3. offline shaders.
-4. 3-platform smokes.
-5. legacy adapter removal.
-
-## Затем — архивный exit gate
-
-1. byte-first paths/VFS hardening;
-2. RsLi edit/writer;
-3. production corpus runner;
-4. licensed reports/roundtrips.
-
-## Затем — graph/assets exit gate
-
-1. typed graph/provenance;
-2. all variants/components/effects;
-3. immutable assets;
-4. runtime/app integration;
-5. full Part 1/2 reachability.
-
----
-
-# 14. Реестр доказательств
-
-## Canonical requirements
-
-- Notion, Vulkan revision: <https://app.notion.com/p/387e79f2db3981778f94cdf34db5f93f>
-
-## Workspace/governance
-
-- Root manifest: <https://github.com/valentineus/fparkan/blob/devel/Cargo.toml>
-- Toolchain: <https://github.com/valentineus/fparkan/blob/devel/rust-toolchain.toml>
-- Cargo config: <https://github.com/valentineus/fparkan/blob/devel/.cargo/config.toml>
-- xtask manifest: <https://github.com/valentineus/fparkan/blob/devel/xtask/Cargo.toml>
-- xtask implementation: <https://github.com/valentineus/fparkan/blob/devel/xtask/src/main.rs>
-- README: <https://github.com/valentineus/fparkan/blob/devel/README.md>
-
-## Stage 0
-
-- Platform core: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-platform/src/lib.rs>
-- SDL stub adapter: <https://github.com/valentineus/fparkan/blob/devel/adapters/fparkan-platform-sdl/src/lib.rs>
-- Render core: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-render/src/lib.rs>
-- GL stub adapter: <https://github.com/valentineus/fparkan/blob/devel/adapters/fparkan-render-gl/src/lib.rs>
-- Game composition: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-game/src/main.rs>
-- Viewer composition: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-viewer/src/main.rs>
-- Headless manifest: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-headless/Cargo.toml>
-
-## Stage 1
-
-- Binary/limits: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-binary/src/lib.rs>
-- Paths: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-path/src/lib.rs>
-- VFS: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-vfs/src/lib.rs>
-- NRes: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-nres/src/lib.rs>
-- RsLi: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-rsli/src/lib.rs>
-- Diagnostics: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-diagnostics/src/lib.rs>
-- Resource repository: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-resource/src/lib.rs>
-- Corpus runner: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-corpus/src/lib.rs>
-
-## Stage 2
-
-- Prototype graph: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-prototype/src/lib.rs>
-- Prototype manifest: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-prototype/Cargo.toml>
-- Assets: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-assets/src/lib.rs>
-- Assets manifest: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-assets/Cargo.toml>
-- Mission format: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-mission-format/src/lib.rs>
-- Runtime: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-runtime/src/lib.rs>
-- Runtime manifest: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-runtime/Cargo.toml>
-- FX manifest: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-fx/Cargo.toml>
-
-## Documentation drift
-
-- Repository implementation tome: <https://github.com/valentineus/fparkan/blob/devel/docs/tomes/07-implementation.md>
-- Parity README: <https://github.com/valentineus/fparkan/blob/devel/parity/README.md>
-- Parity cases: <https://github.com/valentineus/fparkan/blob/devel/parity/cases.toml>
-
----
-
-# 15. Финальное заключение
-
-Проект не находится в плохом состоянии: у него уже есть заметно более сильная CPU/data foundation, чем обычно бывает на ранней стадии восстановления движка. Однако текущая структура создаёт риск преждевременного объявления stages завершёнными, потому что stubs, count reports и ignored licensed tests могут выглядеть как acceptance evidence.
-
-Главный принцип закрытия:
-
-> Stage считается завершённым не тогда, когда существует crate или test с нужным названием, а когда канонический exit gate выполняется на production path, выдаёт воспроизводимый machine-readable artifact и не имеет обходного альтернативного пути.
-
-Для Stage 0 production path — настоящий Vulkan swapchain на трёх OS.
-Для Stage 1 — bounded production parsers плюс lossless licensed roundtrips.
-Для Stage 2 — materialized typed graph, immutable assets и runtime, который не обходит AssetManager.
-
-До выполнения перечисленного рекомендуется маркировать текущий статус как:
-
-```text
-Stage 0: IN PROGRESS / BLOCKED
-Stage 1: IN PROGRESS / ARCHIVE FOUNDATION PARTIAL
-Stage 2: IN PROGRESS / GRAPH AND ASSET ARCHITECTURE NOT CLOSED
-```
diff --git a/fparkan_stage_0_audit_2026-06-23.md b/fparkan_stage_0_audit_2026-06-23.md
new file mode 100644
index 0000000..979c2cf
--- /dev/null
+++ b/fparkan_stage_0_audit_2026-06-23.md
@@ -0,0 +1,643 @@
+# FParkan — аудит Stage 0 и план полного закрытия
+
+**Проект:** `valentineus/fparkan`
+**Проверенная ветка:** `devel` GitHub-зеркала
+**Дата аудита:** 23 июня 2026 года
+**Область:** только Stage 0 — Governance, reproducibility и Vulkan foundation
+**Метод:** статический архитектурный и кодовый аудит
+**Сборка и исполнение:** не выполнялись; `cargo build`, `cargo test`, Vulkan smoke и validation jobs не запускались
+
+---
+
+## 1. Итоговый вердикт
+
+**Stage 0 не закрыт и находится в статусе `BLOCKED`.**
+
+Главный критерий Stage 0 — воспроизводимый репозиторий и минимальный настоящий Vulkan vertical slice на Windows, Linux и macOS. В проверенном состоянии:
+
+- отсутствует `fparkan-platform-winit`;
+- отсутствует `fparkan-render-vulkan`;
+- отсутствуют Vulkan instance/device/surface/swapchain;
+- `fparkan-game` использует `RecordingBackend`, а не GPU backend;
+- workspace по-прежнему содержит SDL/OpenGL stub adapters;
+- Rust toolchain закреплён только как изменяемый канал `stable`;
+- `cargo xtask ci` не реализует полный канонический gate;
+- нет подтверждённых артефактов Windows/Linux/macOS smoke jobs.
+
+### Сводная оценка
+
+| Группа требований | Статус | Основной блокер |
+|---|---|---|
+| Reproducibility и toolchain | **FAIL** | Toolchain не закреплён точной версией, MSRV не объявлен |
+| Repository policy и CI | **FAIL** | Неполные fmt/test/clippy/doc/security gates |
+| Platform abstraction | **FAIL** | Core API содержит OpenGL-specific contract; `winit` adapter отсутствует |
+| Vulkan backend | **FAIL** | Нет Vulkan loader/device/surface/swapchain/pipeline |
+| macOS portability | **FAIL** | Нет MoltenVK integration и portability handling |
+| Offline shaders | **FAIL** | Нет SPIR-V build/validation/hash pipeline |
+| Legacy cleanup | **FAIL** | SDL/GL stubs остаются workspace members |
+| Headless isolation | **PASS на manifest-level** | Автоматическое доказательство dependency closure ещё требуется |
+| Native acceptance | **FAIL / NOT RUNNABLE** | Нет реального backend и platform artifacts |
+
+Stage 0 можно объявить закрытым только после прохождения реального Vulkan smoke на всех трёх системах и публикации machine-readable артефактов.
+
+---
+
+## 2. Область и ограничения аудита
+
+Канонические требования взяты из документа:
+
+- «План реализации stage 0–5: Vulkan revision»;
+- <https://app.notion.com/p/387e79f2db3981778f94cdf34db5f93f>.
+
+Проверялась ветка:
+
+- <https://github.com/valentineus/fparkan/tree/devel>.
+
+Ограничения:
+
+1. Ветка `devel` является движущейся ссылкой. Следующий formal audit следует выполнять на закреплённом commit SHA или tag.
+2. README указывает self-hosted repository как primary. Его закрытые CI runners и artifacts не были доступны.
+3. Код не собирался и не запускался по условию аудита.
+4. Vulkan runtime, validation layers, MoltenVK и native window creation не проверялись динамически.
+5. Статический анализ достаточен для определения текущих архитектурных блокеров: требуемых adapters и зависимостей в workspace нет.
+
+---
+
+## 3. Матрица требований Stage 0
+
+| Требование | Статус | Текущее состояние | Необходимо для закрытия |
+|---|---|---|---|
+| Exact stable Rust toolchain | **FAIL** | `rust-toolchain.toml`: `channel = "stable"` | Закрепить точную версию, например `1.xx.y` |
+| Объявленный MSRV | **FAIL** | `workspace.package.rust-version` отсутствует | Добавить `rust-version` и отдельный MSRV job |
+| Полный `cargo xtask ci` | **FAIL** | Есть custom rustfmt, policy, workspace test и clippy | Добавить канонические fmt/test/clippy/doc/security gates |
+| `--all-targets --all-features` | **FAIL** | Не используются текущим `ci` | Добавить к test/clippy/doc gates |
+| Clippy `-D warnings` | **FAIL** | Явно не передаётся | Сделать предупреждения blocking |
+| Rustdoc broken-link gate | **FAIL** | Отсутствует | Добавить `RUSTDOCFLAGS=-D warnings -D rustdoc::broken_intra_doc_links` |
+| License/advisory/source policy | **PARTIAL / UNVERIFIED** | Есть custom policy и GPL workspace license | Подключить `cargo-deny` или эквивалент и хранить versioned policy |
+| Typed TOML parsing | **FAIL** | Licensed manifest разбирается вручную построчно | `serde` + TOML schema + `deny_unknown_fields` |
+| `cargo_metadata` policy | **FAIL** | Dependency rules не опираются на typed Cargo graph | Добавить `cargo_metadata` и package-ID based checks |
+| CI matrix Windows/Linux/macOS | **UNVERIFIED / BLOCKER** | Доступных platform artifacts нет | Создать native matrix и сохранять reports |
+| Backend-neutral platform API | **FAIL** | В core есть `GraphicsProfile`, GL/GLES versions и `WindowPort::present()` | Удалить GL context concepts; present перенести в renderer |
+| `fparkan-platform-winit` | **FAIL** | В workspace только SDL-named stub | Реализовать настоящий event loop/window adapter |
+| `fparkan-render-vulkan` | **FAIL** | В workspace только GL-named recording stub | Реализовать настоящий Vulkan backend |
+| Vulkan loader/instance/device | **FAIL** | Vulkan bindings отсутствуют | Добавить `ash`, instance, device selection, queues |
+| Surface/swapchain/present | **FAIL** | Отсутствуют | Реализовать platform surface и swapchain lifecycle |
+| Indexed triangle | **FAIL** | Есть только command capture | Нарисовать реальный indexed triangle |
+| Resize/out-of-date/suboptimal | **FAIL** | Swapchain отсутствует | Реализовать полную recreation policy |
+| Deterministic capability report | **FAIL** | Device discovery отсутствует | Pure scoring policy + JSON capability report |
+| macOS portability | **FAIL** | MoltenVK integration отсутствует | Portability enumeration, subset и packaged MoltenVK |
+| Offline SPIR-V pipeline | **FAIL** | GL stub проверяет только synthetic markers | Pinned compiler, validator, descriptor manifest и hashes |
+| Legacy adapter removal | **FAIL** | SDL/GL crates входят в workspace | Удалить crates и все references после замены |
+| Game/viewer composition | **FAIL** | Game использует `RecordingBackend`; viewer — CLI inspector | Подключить winit + Vulkan только в composition roots |
+| Headless isolation | **PASS на manifest-level** | Нет window/Vulkan dependency | Добавить automated Cargo metadata assertion |
+| 300 frames + resize + validation=0 | **FAIL** | Невозможно выполнить без backend | Native smoke jobs на трёх OS |
+| Negative Vulkan tests | **FAIL** | Нет Vulkan error model | Loader/device/queue/format failure fixtures |
+
+---
+
+## 4. Замечания
+
+### S0-B01 — Workspace содержит удаляемые SDL/OpenGL stub crates
+
+**Приоритет:** BLOCKER
+**Файлы:** `Cargo.toml`, `adapters/fparkan-platform-sdl`, `adapters/fparkan-render-gl`
+
+Root workspace включает оба прежних adapter crate. При этом:
+
+- SDL adapter не зависит от SDL и содержит in-memory stubs;
+- GL adapter не зависит от OpenGL и только сохраняет canonical command captures;
+- их tests доказывают deterministic stub behavior, а не platform/GPU integration.
+
+Это создаёт ложноположительный сигнал готовности backend-а.
+
+**Рекомендация:**
+
+1. До появления замены пометить crates как `legacy-proof` и исключить из default production composition.
+2. Добавить policy, запрещающий приложениям зависеть от них.
+3. После подключения `platform-winit` и `render-vulkan` удалить crates, lockfile references, docs и tests.
+
+### S0-B02 — Core platform contract остаётся OpenGL-specific
+
+**Приоритет:** BLOCKER
+**Файл:** `crates/fparkan-platform/src/lib.rs`
+
+Проблемы:
+
+- `GraphicsProfile::DesktopCore/Embedded` описывает GL/GLES profile;
+- `GraphicsContextRequest` описывает создание GL context;
+- `WindowPort::present()` ошибочно закрепляет presentation за window abstraction;
+- `PlatformEvent` содержит только `Quit`;
+- отсутствуют resize, scale factor, focus, keyboard, mouse, suspend/resume и raw handles;
+- `PlatformError::Backend` не содержит source/context.
+
+Для Vulkan окно не выполняет present. Surface, swapchain, image acquisition и queue presentation принадлежат render adapter.
+
+**Рекомендация:** platform crate должен предоставлять только:
+
+- event/lifecycle model;
+- physical и logical size;
+- scale factor;
+- normalized input;
+- raw window/display handles;
+- structured platform errors.
+
+### S0-B03 — Реального Vulkan code path нет
+
+**Приоритет:** BLOCKER
+
+В inspected manifests отсутствуют `ash`, `ash-window`, `winit` и `raw-window-handle`. Следовательно, текущий код не может создать Vulkan instance/device/surface/swapchain.
+
+`fparkan-game` выполняет backend-neutral capture через `RecordingBackend`. Это полезный CPU oracle, но не Vulkan renderer.
+
+**Definition of fixed:** отдельный smoke executable открывает окно, создаёт Vulkan swapchain, рисует indexed triangle, обрабатывает resize и корректно завершается.
+
+### S0-B04 — `cargo xtask ci` не соответствует exit gate
+
+**Приоритет:** BLOCKER
+**Файл:** `xtask/src/main.rs`
+
+Текущий gate не подтверждает:
+
+- все targets и features;
+- clippy с `-D warnings`;
+- rustdoc warnings и broken links;
+- advisory/source policy;
+- dependency denylist;
+- отсутствие project-owned unsafe вне разрешённого Vulkan boundary;
+- корректность typed acceptance manifests;
+- platform-native smoke jobs.
+
+Custom recursive rustfmt также может расходиться с canonical `cargo fmt --all -- --check`.
+
+### S0-B05 — Toolchain не воспроизводим
+
+**Приоритет:** BLOCKER
+**Файл:** `rust-toolchain.toml`
+
+Канал `stable` изменяется. Один и тот же commit может использовать разные компиляторы в разные дни. MSRV также не объявлен.
+
+**Рекомендация:**
+
+- закрепить точный Rust release;
+- указать `rust-version`;
+- обновлять toolchain отдельным reviewed PR;
+- сохранять toolchain и SDK versions в acceptance report.
+
+### S0-H01 — Нужен изолированный audited unsafe boundary
+
+**Приоритет:** HIGH
+
+`unsafe_code = "forbid"` правильно сохранять для backend-neutral crates. Однако Vulkan FFI требует локальных unsafe calls.
+
+Нельзя ослаблять policy всему workspace.
+
+**Целевая схема:**
+
+- unsafe разрешён только в `fparkan-render-vulkan` low-level modules;
+- `unsafe_op_in_unsafe_fn = deny`;
+- каждый block имеет `// SAFETY:` comment;
+- ownership/lifetime rules документированы;
+- raw Vulkan handles не выходят в public neutral API;
+- custom policy scanner проверяет allowlist.
+
+### S0-H02 — Neutral render IDs не должны быть GPU allocation IDs
+
+**Приоритет:** HIGH, не блокирует первый hardcoded triangle
+**Файл:** `crates/fparkan-render/src/lib.rs`
+
+`GpuMeshId` и `GpuMaterialId` появляются до существования GPU registry. Это смешивает CPU asset identity и backend-local allocation identity.
+
+**Рекомендация:** использовать neutral `MeshAssetId`/`MaterialAssetId`; Vulkan adapter должен самостоятельно отображать их на buffers, images и descriptors.
+
+### S0-M01 — Документация рассогласована с Vulkan revision
+
+**Приоритет:** MEDIUM
+
+`docs/tomes/07-implementation.md` сохраняет старую последовательность и multi-backend формулировки. Parity documentation ссылается на отсутствующий workspace crate, а active parity cases не определены.
+
+**Рекомендация:** один versioned source of truth для stages и автоматическая проверка упомянутых crates, commands и backend names.
+
+---
+
+## 5. Сильные стороны, которые следует сохранить
+
+- Workspace lint policy строгая и подходит для backend-neutral crates.
+- `Cargo.lock` присутствует, а команды используют `--locked`.
+- Synthetic и licensed corpus paths концептуально разделены.
+- `fparkan-headless` не зависит от platform/render adapters на manifest-level.
+- `fparkan-render` уже предоставляет deterministic command ordering, validation и canonical capture.
+- Composition roots отделены от большинства core crates.
+
+Эти элементы позволяют построить Vulkan foundation без переписывания CPU/data foundation.
+
+---
+
+## 6. Целевая архитектура Stage 0
+
+```text
+apps/fparkan-game, apps/fparkan-viewer
+ │
+ ├── fparkan-platform-winit
+ │ └── winit + raw-window-handle
+ │
+ └── fparkan-render-vulkan
+ ├── ash-window
+ ├── ash
+ ├── surface / swapchain
+ ├── device / queues
+ ├── shaders / pipelines
+ └── synchronization / presentation
+
+apps/fparkan-headless
+ └── runtime/core only
+ no winit, ash, MoltenVK or window dependencies
+```
+
+Разделение ответственности:
+
+- `fparkan-platform`: события, input, lifecycle, sizes и handle access;
+- `fparkan-platform-winit`: concrete window/event-loop implementation;
+- `fparkan-render`: backend-neutral command/snapshot contracts;
+- `fparkan-render-vulkan`: Vulkan resources, synchronization и present;
+- game/viewer: composition root;
+- headless: полностью изолированный путь.
+
+---
+
+## 7. План полного закрытия Stage 0
+
+Порядок PR важен. Vulkan adapter не следует строить поверх текущего GL-oriented platform contract.
+
+### PR S0-01 — Reproducible toolchain и metadata
+
+**Изменения**
+
+- закрепить exact Rust toolchain;
+- добавить `workspace.package.rust-version`;
+- зафиксировать supported triples;
+- добавить `cargo xtask doctor`;
+- включать commit SHA, Rust version и platform SDK versions в reports.
+
+**Acceptance**
+
+- clean checkout формирует одинаковый metadata report;
+- MSRV job собирает backend-neutral crates;
+- pinned toolchain проходит полный synthetic gate.
+
+### PR S0-02 — Typed xtask configuration
+
+**Изменения**
+
+- `serde` + TOML schemas для corpus/acceptance manifests;
+- `deny_unknown_fields`;
+- duplicate/missing/unknown-field validation;
+- absolute canonical paths для local licensed manifest;
+- `cargo_metadata` для dependency и workspace policy;
+- удалить ручной line parser.
+
+**Acceptance**
+
+- malformed manifest всегда даёт non-zero exit;
+- неизвестные поля не игнорируются;
+- dependency policy работает по Cargo package IDs, targets и features.
+
+### PR S0-03 — Полный synthetic CI gate
+
+Обязательные команды:
+
+```bash
+cargo fmt --all -- --check
+cargo test --workspace --all-targets --all-features --locked
+cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
+RUSTDOCFLAGS="-D warnings -D rustdoc::broken_intra_doc_links" \
+ cargo doc --workspace --no-deps --all-features --locked
+cargo deny check advisories bans licenses sources
+cargo xtask policy
+cargo xtask acceptance audit --strict
+```
+
+Добавить reports для каждого gate и запрет silent skip.
+
+### PR S0-04 — Redesign `fparkan-platform`
+
+**Изменения**
+
+- удалить `GraphicsProfile`, `GraphicsContextRequest` и GL version negotiation;
+- убрать `present()` из window port;
+- добавить normalized keyboard/mouse events;
+- physical/logical size и scale factor;
+- focus, minimize, occlusion, suspend/resume;
+- deterministic lifecycle state machine;
+- structured errors с source chain.
+
+**Synthetic tests**
+
+- resize coalescing;
+- zero-size/minimized window;
+- scale-factor changes;
+- focus loss clears held input;
+- key repeat и modifiers;
+- suspend/resume;
+- deterministic event ordering.
+
+### PR S0-05 — `fparkan-platform-winit`
+
+**Изменения**
+
+- winit event loop;
+- native window lifecycle;
+- raw window/display handles;
+- platform-specific event normalization;
+- отсутствие GPU ownership.
+
+**Acceptance**
+
+- window-only smoke на Windows, Linux и macOS;
+- native event trace соответствует synthetic model.
+
+### PR S0-06 — Vulkan low-level boundary
+
+**Изменения**
+
+- `ash` и `ash-window`;
+- dynamic Vulkan loader;
+- instance и debug messenger;
+- physical device capability records;
+- pure deterministic device scoring;
+- graphics/present queue selection;
+- deterministic capability JSON;
+- audited unsafe allowlist.
+
+**Negative tests**
+
+- loader отсутствует;
+- Vulkan 1.1 недоступен;
+- graphics queue отсутствует;
+- present queue отсутствует;
+- `VK_KHR_swapchain` отсутствует;
+- required surface format отсутствует.
+
+### PR S0-07 — Swapchain, triangle и offline shaders
+
+**Изменения**
+
+- surface и swapchain;
+- format/present-mode/image-count policy;
+- render pass и graphics pipeline;
+- indexed triangle;
+- command pools/buffers;
+- binary semaphores и fences;
+- frames in flight;
+- resize/out-of-date/suboptimal/zero extent handling;
+- pinned offline shader compiler;
+- SPIR-V validation;
+- descriptor/push-constant manifest;
+- shader content hashes.
+
+### PR S0-08 — macOS portability proof
+
+**Изменения**
+
+- `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR`;
+- portability extension enumeration;
+- `VK_KHR_portability_subset` enablement, если объявлен device;
+- MoltenVK packaging strategy;
+- deterministic portability report;
+- `.app` bundle smoke.
+
+### PR S0-09 — Composition roots и legacy removal
+
+**Изменения**
+
+- game/viewer подключают winit + Vulkan adapters;
+- headless остаётся без window/GPU graph;
+- удалить SDL/GL stub crates;
+- очистить lockfile, policy и docs;
+- заменить GPU-named neutral IDs на asset IDs;
+- запретить stale backend names automated policy check-ом.
+
+### PR S0-10 — Native acceptance matrix
+
+**Jobs**
+
+- Windows MSVC + system Vulkan loader;
+- Linux X11 или Wayland surface;
+- macOS Apple Silicon + MoltenVK;
+- отдельный software-Vulkan Linux PR job допустим как быстрый gate;
+- native GPU jobs остаются release evidence.
+
+**Обязательный сценарий**
+
+1. Создать окно.
+2. Создать real Vulkan swapchain.
+3. Показать indexed triangle.
+4. Выполнить не менее 300 frames.
+5. Изменить размер окна.
+6. Пересоздать swapchain.
+7. Корректно завершить event loop.
+8. Получить `validation_error_count = 0`.
+9. Сохранить capability, shader и validation reports как artifacts.
+
+**Stage 0 закрывается только после merge S0-01…S0-10 и зелёных native artifacts.**
+
+---
+
+## 8. Требуемая CI/acceptance модель
+
+### 8.1 Synthetic PR gate
+
+Должен работать без игровых каталогов и без silent skip:
+
+1. fmt, clippy, docs, security и policy;
+2. все unit/integration tests;
+3. platform lifecycle state-machine tests;
+4. device scoring tests на synthetic capability records;
+5. swapchain policy tests;
+6. shader manifest/hash tests;
+7. Vulkan negative-path tests без обязательного GPU;
+8. headless dependency assertion;
+9. report schema validation.
+
+Tests, требующие native GPU или licensed data, должны иметь отдельные suites и machine-readable ownership/reason, а не оставаться обычными `#[ignore]` без evidence trail.
+
+### 8.2 Native platform gate
+
+| Platform | Минимальный gate | Дополнительное evidence |
+|---|---|---|
+| Windows | system loader, swapchain, triangle, resize, 300 frames, validation=0 | Периодическая NVIDIA/AMD/Intel coverage |
+| Linux | X11 или Wayland surface, swapchain, resize, validation=0 | Software Vulkan PR job + Mesa/NVIDIA native release jobs |
+| macOS | MoltenVK, portability enumeration/subset, CAMetalLayer surface, resize, validation=0 | Apple Silicon как primary target |
+
+### 8.3 Формат machine-readable отчёта
+
+Минимальные поля:
+
+```json
+{
+ "schema": 1,
+ "commit": "<sha>",
+ "target": "x86_64-pc-windows-msvc",
+ "rustc": "1.xx.y",
+ "vulkan_api": "1.1",
+ "device_name": "...",
+ "driver": "...",
+ "portability_subset": false,
+ "frames": 300,
+ "resize_count": 1,
+ "swapchain_recreate_count": 1,
+ "validation_error_count": 0,
+ "shader_manifest_hash": "...",
+ "result": "pass"
+}
+```
+
+---
+
+## 9. Definition of Done
+
+Stage 0 считается закрытым, когда выполнены **все** пункты:
+
+- [ ] Exact Rust toolchain закреплён.
+- [ ] MSRV объявлен и проверяется.
+- [ ] Full fmt/test/clippy/doc/security/source/license gate проходит.
+- [ ] Typed TOML manifests используются.
+- [ ] Dependency policy работает через `cargo_metadata`.
+- [ ] Windows/Linux/macOS matrix сохраняет artifacts.
+- [ ] `fparkan-platform` больше не содержит GL-specific context concepts.
+- [ ] `fparkan-platform-winit` реализован.
+- [ ] `fparkan-render-vulkan` реализован.
+- [ ] Vulkan 1.1 instance/device/queues/surface/swapchain реализованы.
+- [ ] Deterministic device scoring и capability report реализованы.
+- [ ] Indexed triangle рисуется настоящим Vulkan backend.
+- [ ] Resize, zero extent, out-of-date и suboptimal обработаны.
+- [ ] MoltenVK portability path реализован.
+- [ ] Offline SPIR-V validation и hash manifest реализованы.
+- [ ] Unsafe разрешён только в audited Vulkan/FFI modules.
+- [ ] Legacy SDL/GL adapters и references удалены.
+- [ ] Game/viewer используют новые composition adapters.
+- [ ] Headless dependency graph не содержит winit/Vulkan/MoltenVK.
+- [ ] 300-frame + resize smoke проходит на трёх OS.
+- [ ] Validation error count равен нулю на трёх OS.
+- [ ] Acceptance reports включают commit SHA и сохраняются как artifacts.
+
+Наличие crates или unit tests с соответствующими названиями само по себе не является закрытием Stage 0.
+
+---
+
+## 10. Рекомендуемые automated policy checks
+
+Добавить в `cargo xtask policy`:
+
+### Workspace denylist
+
+- запрещены `fparkan-platform-sdl` и `fparkan-render-gl` после миграции;
+- запрещены stale symbols `GraphicsProfile`, `DesktopCore`, `Embedded`, `Gles2` в canonical platform/render API;
+- canonical docs не содержат OpenGL как production backend.
+
+### Dependency rules
+
+- headless не зависит от `winit`, `raw-window-handle`, `ash`, `ash-window` или Vulkan adapter;
+- backend-neutral crates не зависят от concrete platform/render adapters;
+- только composition roots связывают platform и renderer;
+- raw Vulkan types не экспортируются из adapter public boundary.
+
+### Unsafe rules
+
+- project-owned unsafe разрешён только в exact allowlisted files/modules;
+- каждый block содержит `SAFETY:`;
+- `unsafe_op_in_unsafe_fn` запрещён;
+- изменение allowlist требует отдельного reviewed diff.
+
+### Test и report rules
+
+- synthetic gate не получает licensed paths;
+- ignored tests обязаны иметь registered reason и owner;
+- acceptance IDs уникальны;
+- reports проходят schema validation;
+- report всегда содержит commit SHA и target triple.
+
+### Documentation rules
+
+- документированные crates и commands существуют;
+- canonical stage version совпадает с acceptance schema;
+- старые backend names отсутствуют;
+- README не объявляет незакрытый Vulkan path реализованным.
+
+---
+
+## 11. Основные риски
+
+| Риск | Последствие | Снижение |
+|---|---|---|
+| Vulkan adapter начнут до redesign platform API | Повторная переделка surface/lifecycle/present | Сначала S0-04, затем S0-05/S0-06 |
+| `unsafe_code` ослабят всему workspace | Рост FFI и lifetime рисков | Изолированный audited adapter и allowlist scanner |
+| Stubs будут приняты за production backend | Ложное закрытие Stage 0 | Удаление legacy crates и real native smoke |
+| Linux software Vulkan будет единственным evidence | Не выявятся vendor-driver проблемы | Native Mesa/NVIDIA jobs перед release |
+| macOS будет проверен без portability subset report | Скрытая несовместимость MoltenVK | Обязательное capability evidence |
+| Shader compiler останется неприкреплённым | Невоспроизводимый SPIR-V | Pinned compiler + manifest hashes |
+| GitHub mirror и primary repository разойдутся | Audit и release относятся к разному коду | Commit SHA, canonical remote и artifact metadata |
+| Документация останется отдельным source of truth | Повторное рассогласование | Versioned stage schema и automated doc checks |
+
+---
+
+## 12. Реестр доказательств
+
+### Canonical requirement
+
+- Vulkan revision: <https://app.notion.com/p/387e79f2db3981778f94cdf34db5f93f>
+
+### Workspace и governance
+
+- Root manifest: <https://github.com/valentineus/fparkan/blob/devel/Cargo.toml>
+- Toolchain: <https://github.com/valentineus/fparkan/blob/devel/rust-toolchain.toml>
+- Cargo config: <https://github.com/valentineus/fparkan/blob/devel/.cargo/config.toml>
+- xtask manifest: <https://github.com/valentineus/fparkan/blob/devel/xtask/Cargo.toml>
+- xtask implementation: <https://github.com/valentineus/fparkan/blob/devel/xtask/src/main.rs>
+- README: <https://github.com/valentineus/fparkan/blob/devel/README.md>
+
+### Platform и render
+
+- Platform core: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-platform/src/lib.rs>
+- SDL stub adapter: <https://github.com/valentineus/fparkan/blob/devel/adapters/fparkan-platform-sdl/src/lib.rs>
+- Render core: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-render/src/lib.rs>
+- GL stub adapter: <https://github.com/valentineus/fparkan/blob/devel/adapters/fparkan-render-gl/src/lib.rs>
+- Game composition: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-game/src/main.rs>
+- Viewer composition: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-viewer/src/main.rs>
+- Headless manifest: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-headless/Cargo.toml>
+
+### Documentation drift
+
+- Implementation tome: <https://github.com/valentineus/fparkan/blob/devel/docs/tomes/07-implementation.md>
+- Parity README: <https://github.com/valentineus/fparkan/blob/devel/parity/README.md>
+- Parity cases: <https://github.com/valentineus/fparkan/blob/devel/parity/cases.toml>
+
+---
+
+## 13. Финальное заключение
+
+У проекта уже имеется пригодный backend-neutral фундамент: deterministic render commands, строгие neutral-crate lints, отдельный headless composition root и разделение synthetic/licensed tests. Однако Stage 0 пока представлен интерфейсными proof/stub crates, а не настоящим Vulkan vertical slice.
+
+Критический путь:
+
+```text
+reproducible toolchain
+ → complete CI/policy gate
+ → backend-neutral platform redesign
+ → winit adapter
+ → Vulkan loader/device/surface/swapchain
+ → indexed triangle + shaders + synchronization
+ → MoltenVK portability
+ → composition integration
+ → legacy removal
+ → three-platform acceptance artifacts
+```
+
+До прохождения этого пути рекомендуемый статус:
+
+```text
+Stage 0: IN PROGRESS / BLOCKED
+```
+
+Главный критерий закрытия:
+
+> Stage 0 завершён не тогда, когда существуют crates с названиями `winit` и `vulkan`, а когда один закреплённый commit создаёт настоящий Vulkan swapchain, рисует triangle, переживает resize и завершается без validation errors на Windows, Linux и macOS, сохраняя воспроизводимые machine-readable artifacts.
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 8f67468..2e1f7fb 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1,4 +1,23 @@
#![forbid(unsafe_code)]
+#![cfg_attr(
+ test,
+ allow(
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_precision_loss,
+ clippy::expect_used,
+ clippy::float_cmp,
+ clippy::identity_op,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::map_unwrap_or,
+ clippy::needless_raw_string_hashes,
+ clippy::semicolon_if_nothing_returned,
+ clippy::type_complexity,
+ clippy::panic,
+ clippy::unwrap_used
+ )
+)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! Repository automation for `FParkan`.
@@ -175,8 +194,17 @@ fn run_cargo_deny() -> Result<(), String> {
fn run_cargo_doc() -> Result<(), String> {
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let status = Command::new(cargo)
- .args(["doc", "--workspace", "--all-features", "--locked", "--no-deps"])
- .env("RUSTDOCFLAGS", "-D warnings -D rustdoc::broken_intra_doc_links")
+ .args([
+ "doc",
+ "--workspace",
+ "--all-features",
+ "--locked",
+ "--no-deps",
+ ])
+ .env(
+ "RUSTDOCFLAGS",
+ "-D warnings -D rustdoc::broken_intra_doc_links",
+ )
.status()
.map_err(|err| format!("failed to run cargo doc: {err}"))?;
if status.success() {
@@ -272,8 +300,10 @@ fn parse_licensed_manifest(path: &Path) -> Result<LicensedCorpusRoots, String> {
}
let roots = LicensedCorpusRoots {
- part1: part1.ok_or_else(|| "licensed manifest is missing part1 corpus entry".to_string())?,
- part2: part2.ok_or_else(|| "licensed manifest is missing part2 corpus entry".to_string())?,
+ part1: part1
+ .ok_or_else(|| "licensed manifest is missing part1 corpus entry".to_string())?,
+ part2: part2
+ .ok_or_else(|| "licensed manifest is missing part2 corpus entry".to_string())?,
};
validate_licensed_part("part1", &roots.part1)?;
validate_licensed_part("part2", &roots.part2)?;
@@ -412,16 +442,10 @@ fn validate_cargo_metadata(root: &Path, failures: &mut Vec<String>) -> Result<()
}
let metadata = MetadataCommand::new()
.manifest_path(&manifest)
- .no_deps(true)
+ .no_deps()
.other_options(["--offline".to_string(), "--locked".to_string()])
.exec()
- .map_err(|error| {
- format!(
- "{}: cargo metadata failed: {}",
- manifest.display(),
- error
- )
- })?;
+ .map_err(|error| format!("{}: cargo metadata failed: {}", manifest.display(), error))?;
if metadata.workspace_members.is_empty() {
failures.push(format!(
"{}: cargo metadata produced no workspace members",
@@ -505,7 +529,7 @@ fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Re
continue;
}
let dependencies = parse_manifest_dependencies(&text);
- if !is_adapter_like_package(&package) {
+ if !is_adapter_like_package(&package) && !is_app_package(&package) {
for dependency in &dependencies {
if is_forbidden_gui_dependency(dependency) {
failures.push(format!(
@@ -522,13 +546,13 @@ fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Re
manifest.display()
));
}
- for dependency in &dependencies {
- if is_forbidden_runtime_bridge_dependency(dependency) {
- failures.push(format!(
- "{}: app package {package} depends on forbidden bridge dependency {dependency}",
- manifest.display()
- ));
- }
+ }
+ if package == "fparkan-headless" {
+ if let Some(forbidden) = first_forbidden_platform_bridge_dependency(&dependencies) {
+ failures.push(format!(
+ "{}: headless package {package} depends on platform/render bridge dependency {forbidden}",
+ manifest.display()
+ ));
}
}
@@ -567,10 +591,7 @@ fn is_app_package(package: &str) -> bool {
}
fn is_adapter_like_package(package: &str) -> bool {
- matches!(
- package,
- "fparkan-platform-winit" | "fparkan-render-vulkan"
- )
+ matches!(package, "fparkan-platform-winit" | "fparkan-render-vulkan")
}
fn first_forbidden_parser_dependency(dependencies: &BTreeSet<String>) -> Option<&str> {
@@ -630,16 +651,10 @@ fn first_forbidden_platform_bridge_dependency(dependencies: &BTreeSet<String>) -
})
}
-fn is_forbidden_runtime_bridge_dependency(dependency: &str) -> bool {
- matches!(
- dependency,
- "fparkan-platform-winit" | "fparkan-render-vulkan" | "winit" | "ash" | "ash-window"
- )
-}
-
fn is_forbidden_domain_dependency(dependency: &str) -> bool {
matches!(
- dependency, "fparkan-cli"
+ dependency,
+ "fparkan-cli"
| "fparkan-game"
| "fparkan-headless"
| "fparkan-viewer"
@@ -889,14 +904,14 @@ fn scan_policy_file(path: &Path, failures: &mut Vec<String>) -> Result<(), Strin
previous_line_has_safety_comment = false;
continue;
}
- if contains_unsafe_construct(trimmed) {
- if !is_authorized_unsafe_construct(path, trimmed, previous_line_has_safety_comment) {
- failures.push(format!(
- "{}:{}: unsafe construct in workspace source",
- path.display(),
- index + 1
- ));
- }
+ if contains_unsafe_construct(trimmed)
+ && !is_authorized_unsafe_construct(path, trimmed, previous_line_has_safety_comment)
+ {
+ failures.push(format!(
+ "{}:{}: unsafe construct in workspace source",
+ path.display(),
+ index + 1
+ ));
}
previous_line_has_safety_comment = false;
}
@@ -911,9 +926,7 @@ fn contains_unsafe_construct(line: &str) -> bool {
}
fn is_comment_line(line: &str) -> bool {
- line.starts_with("//")
- || line.starts_with("//!")
- || line.starts_with("///")
+ line.starts_with("//") || line.starts_with("//!") || line.starts_with("///")
}
fn has_safety_comment(line: &str) -> bool {
@@ -924,7 +937,9 @@ const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &["adapters/fparkan-render-vulkan/s
fn is_audited_unsafe_source(path: &Path) -> bool {
let as_path = path.as_os_str().to_string_lossy();
- AUDITED_UNSAFE_SOURCE_FILES.iter().any(|candidate| as_path.ends_with(candidate))
+ AUDITED_UNSAFE_SOURCE_FILES
+ .iter()
+ .any(|candidate| as_path.ends_with(candidate))
}
fn is_authorized_unsafe_construct(
@@ -1258,11 +1273,7 @@ impl AcceptanceAudit {
}
fn strict_failures(&self) -> Vec<String> {
- self.partial
- .iter()
- .chain(&self.missing)
- .cloned()
- .collect()
+ self.partial.iter().chain(&self.missing).cloned().collect()
}
}