diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..cf6d8174 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ + +# Keep bundled package bytes reproducible across checkouts. +sdk/examples/** text eol=lf +sdk/examples/**/*.glb binary +mods/*.zip binary diff --git a/.gitignore b/.gitignore index 8f5208dd..ede2d0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,36 @@ __pycache__/ /Build-SessionMarker.ps1 /SETUP-PROVENANCE.json /LaunchPlants.bat +/sdk/examples/mario-kart/rider.json + +/mods/.cache/ + +# Bundled mods requested for source checkout and distribution. +!/mods/native-trainer.zip +!/mods/mario-kart.zip +!/sdk/examples/mario-kart/kart.glb +!/sdk/examples/mario-kart/rider.json + +/BUILD-MULTIPLAYER-TEST.bat +/Build-FakieFix.ps1 +/Build-FakieTrainer.ps1 +/Build-LuaSDK.ps1 +/Build-ModScrollbar.ps1 +/Build-ModWindows.ps1 +/Build-NativeTrainer.ps1 +/Build-ScrollSettings.ps1 +/Build-Showcase.ps1 +/Build-VehicleSDK.ps1 +/PLAY-FAKIE-FIX.bat +/PLAY-FAKIE-TRAINER.bat +/PLAY-LUA-SDK.bat +/PLAY-MARIO-KART.bat +/PLAY-MOD-SCROLLBAR.bat +/PLAY-MOD-WINDOWS.bat +/PLAY-MULTIPLAYER-10-INSTANCES.bat +/PLAY-MULTIPLAYER-2-INSTANCES.bat +/PLAY-MULTIPLAYER-MENU.bat +/PLAY-MULTIPLAYER-SMOOTH-2-INSTANCES.bat +/PLAY-NATIVE-TRAINER.bat +/PLAY-SCROLL-SETTINGS.bat +/PLAY-SHOWCASE.bat diff --git a/Build-Release.ps1 b/Build-Release.ps1 index ea23d37a..2784498b 100644 --- a/Build-Release.ps1 +++ b/Build-Release.ps1 @@ -33,6 +33,8 @@ try { pdb_sha256 = (Get-FileHash -LiteralPath "$symbols/skate3rust.pdb" -Algorithm SHA256).Hash compiler = (& rustc --version).Trim() } | ConvertTo-Json | Set-Content -LiteralPath "$symbols/build.json" -Encoding UTF8 + New-Item -ItemType Directory -Path "$stage/mods" -Force | Out-Null + Copy-Item -LiteralPath mods/native-trainer.zip,mods/mario-kart.zip,mods/README.md -Destination "$stage/mods" $sourceStage = Join-Path $stage '../setup-source' $toolsRoot = Join-Path $PSScriptRoot 'tools' foreach ($source in Get-ChildItem -LiteralPath $toolsRoot -File -Recurse) { diff --git a/Cargo.lock b/Cargo.lock index aaaeca5a..07dccbec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alsa" version = "0.9.1" @@ -175,6 +181,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -1029,7 +1044,7 @@ dependencies = [ "arrayvec", "bevy_reflect", "derive_more", - "glam", + "glam 0.30.10", "itertools 0.14.0", "libm", "rand", @@ -1201,7 +1216,7 @@ dependencies = [ "downcast-rs 2.0.2", "erased-serde", "foldhash 0.2.0", - "glam", + "glam 0.30.10", "indexmap", "inventory", "petgraph", @@ -1261,7 +1276,7 @@ dependencies = [ "downcast-rs 2.0.2", "encase", "fixedbitset", - "glam", + "glam 0.30.10", "image", "indexmap", "js-sys", @@ -1428,7 +1443,7 @@ dependencies = [ "crossbeam-queue", "derive_more", "futures-lite", - "heapless", + "heapless 0.9.3", "pin-project", ] @@ -1739,6 +1754,16 @@ dependencies = [ "piper", ] +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -2165,6 +2190,17 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -2254,6 +2290,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "encase" version = "0.12.1" @@ -2620,6 +2665,41 @@ dependencies = [ "serde_core", ] +[[package]] +name = "glam" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7" + +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" + +[[package]] +name = "glam" +version = "0.33.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e762b9b188634fc508d4ec54b62ea3d352c43fd431d18d05d46f559f217f4725" +dependencies = [ + "approx", + "libm", +] + +[[package]] +name = "glamx" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9847f980afb0628849bcd6a406cdffd29eb9d53fca83e1646005901a9acd4c5c" +dependencies = [ + "approx", + "glam 0.33.7", + "nalgebra", + "num-traits", + "simba", +] + [[package]] name = "glob" version = "0.3.4" @@ -2799,6 +2879,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", "equivalent", "foldhash 0.2.0", "serde", @@ -2810,6 +2891,19 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] [[package]] name = "heapless" @@ -2835,7 +2929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29a164ceff4500f2a72b1d21beaa8aa8ad83aec2b641844c659b190cb3ea2e0b" dependencies = [ "constgebra", - "glam", + "glam 0.30.10", "tinyvec", ] @@ -3152,6 +3246,25 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "lua-src" +version = "551.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d400ffef0e3d4d29287092bdc5a276d0bf468b2c69709d5bab0d469312d9f947" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.7.3+1ee778a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869665372263eb337b14f480cfb864b89f12eade4eb42cb415f71517b4a67572" +dependencies = [ + "cc", + "which", +] + [[package]] name = "mach2" version = "0.4.3" @@ -3179,6 +3292,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.8.3" @@ -3235,6 +3358,38 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mlua" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96e5d00f19d8c46c71ceaced99593b90c31c57aa1fe2cb3e93a8b1698eedba9" +dependencies = [ + "bstr", + "either", + "erased-serde", + "libc", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash 2.1.3", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b806d7ade031f5d6607eae3e283fb034cb795a76247dd0d1ba753c8c42debccf" +dependencies = [ + "cc", + "cfg-if", + "libc", + "lua-src", + "luajit-src", + "pkg-config", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -3289,6 +3444,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "nalgebra" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc43a60c217b0c6ff46e47f26911015ad8d2e5a8be1af668c67e370d99a4346" +dependencies = [ + "approx", + "glam 0.30.10", + "glam 0.31.1", + "glam 0.32.1", + "glam 0.33.7", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "ndk" version = "0.8.0" @@ -3388,6 +3574,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -3399,6 +3604,26 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3730,6 +3955,15 @@ dependencies = [ "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 = "ordered-float" version = "5.5.0" @@ -3777,6 +4011,34 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "parry3d" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b00bf3ea4e0961a3f44aeb666b7dc3b0e87027b629561bd8283a7f6b2e2ed1" +dependencies = [ + "approx", + "arrayvec", + "bitflags 2.13.1", + "downcast-rs 2.0.2", + "either", + "ena", + "foldhash 0.2.0", + "glamx", + "hashbrown 0.17.1", + "log", + "num-derive", + "num-traits", + "ordered-float 5.5.0", + "rstar", + "simba", + "slab", + "smallvec", + "spade", + "static_assertions", + "thiserror 2.0.20", +] + [[package]] name = "paste" version = "1.0.15" @@ -3940,6 +4202,19 @@ name = "profiling" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] [[package]] name = "pxfm" @@ -4034,12 +4309,40 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" +[[package]] +name = "rapier3d" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b14829af0960a134e2c39605bd6617a5c70e9837ce1b7c3c738efedc6fd8610" +dependencies = [ + "approx", + "arrayvec", + "bitflags 2.13.1", + "glamx", + "log", + "nalgebra", + "num-traits", + "parry3d", + "profiling", + "serde", + "simba", + "static_assertions", + "thiserror 2.0.20", + "wide", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "read-fonts" version = "0.36.0" @@ -4130,6 +4433,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + [[package]] name = "rodio" version = "0.20.1" @@ -4160,6 +4469,17 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rstar" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5912b862fa5ffb462607bfd1e35036c458c537921f508c8235a83d5f3987edfe" +dependencies = [ + "heapless 0.8.0", + "num-traits", + "smallvec", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4222,6 +4542,15 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "safe_arch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4284,6 +4613,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -4338,6 +4677,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "simba" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a7200d82ff1c7b4235efd12f11e2537a402c91cf83a5cb97ce80eef787fde7" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -4385,7 +4736,21 @@ dependencies = [ "serde_json", "skate-core", "skate-data", + "skate-mods", "skate-net", + "skate-vehicles", +] + +[[package]] +name = "skate-mods" +version = "0.1.0" +dependencies = [ + "blake3", + "mlua", + "serde", + "serde_json", + "skate-vehicles", + "zip", ] [[package]] @@ -4406,6 +4771,15 @@ dependencies = [ "steamworks", ] +[[package]] +name = "skate-vehicles" +version = "0.1.0" +dependencies = [ + "rapier3d", + "serde", + "serde_json", +] + [[package]] name = "skrifa" version = "0.39.0" @@ -4481,6 +4855,18 @@ dependencies = [ "serde", ] +[[package]] +name = "spade" +version = "2.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" +dependencies = [ + "hashbrown 0.16.1", + "num-traits", + "robust", + "smallvec", +] + [[package]] name = "spin" version = "0.10.1" @@ -4872,6 +5258,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "typewit" version = "1.15.2" @@ -5281,7 +5673,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "objc", "once_cell", - "ordered-float", + "ordered-float 5.5.0", "parking_lot", "portable-atomic", "portable-atomic-util", @@ -5313,6 +5705,25 @@ dependencies = [ "web-sys", ] +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "wide" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf05ca94c9fba0c51316899caa1d1e74a064fc310a5efa7375e614343d415be" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5964,6 +6375,20 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.7" @@ -5976,6 +6401,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 8bc30e50..3231ce38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/skate-core", "crates/skate-data", "crates/skate-game", "crates/skate-net", "crates/skate-steam-relay"] +members = ["crates/skate-core", "crates/skate-data", "crates/skate-game", "crates/skate-mods", "crates/skate-vehicles", "crates/skate-net", "crates/skate-steam-relay"] resolver = "3" [workspace.package] diff --git a/README.md b/README.md index a5cd2a0d..aaa67fa2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ A Rust and Bevy skating project built from Skate 3 reverse-engineering research. Includes skating, tricks, grinds, offboard movement, difficulty settings and `.skate` map support. Gameplay parity is still a work in progress. +## Mods and agent authoring + +Drop mod ZIPs into top-level `mods/` and use `PLAY-MARIO-KART.bat` for the integrated +mod build in this checkout. **Point your coding agent at [sdk/AGENTS.md](sdk/AGENTS.md)** +to make a mod. See [package structure](docs/mod-packages.md), [Lua API](docs/lua-modding.md), +[vehicle API](docs/vehicle-sdk.md), [multiplayer mod SDK](docs/multiplayer-mods.md) and [Mixamo workflow](docs/mixamo-vehicle-workflow.md). +Editable examples live in `sdk/examples/`; `tools/package_mod.py` validates and packages them. + ## Play Extract the Windows release ZIP and run `skate3rust.exe`. Select your Skate 3 diff --git a/crates/skate-core/src/riding/anti_flip.rs b/crates/skate-core/src/riding/anti_flip.rs index 708124a4..5a422dfc 100644 --- a/crates/skate-core/src/riding/anti_flip.rs +++ b/crates/skate-core/src/riding/anti_flip.rs @@ -1,6 +1,7 @@ //! Complete TU3 Toolkit_AntiFlipTorque 82D94190, distinct from heading/manual. use super::vector::{cross, dot3, normalize}; use crate::{point_graph::PointGraph, trigonometry::acos}; +#[derive(Clone)] pub struct AntiFlipSettings { pub axis_96_response: PointGraph<8>, // +1832/+1864 pub axis_64_response: PointGraph<8>, // +1912/+1944 diff --git a/crates/skate-core/src/riding/ground_force.rs b/crates/skate-core/src/riding/ground_force.rs index ab6ea08c..140dc0ca 100644 --- a/crates/skate-core/src/riding/ground_force.rs +++ b/crates/skate-core/src/riding/ground_force.rs @@ -2,6 +2,7 @@ //! The research index has not recovered its original name. Keep its four //! caller-supplied scalar arguments explicit instead of inventing units. use super::vector::{clamp, dot3, normalize}; +#[derive(Clone)] pub struct GroundForceSettings { pub range_1220: f32, pub speed_scale_1224: f32, diff --git a/crates/skate-core/src/riding/heading.rs b/crates/skate-core/src/riding/heading.rs index 4aceec8d..75d5957e 100644 --- a/crates/skate-core/src/riding/heading.rs +++ b/crates/skate-core/src/riding/heading.rs @@ -2,6 +2,7 @@ //! AntiFlipTorque; the two are distinct. The caller owns board+284 history. use crate::{physics::reciprocal_sqrt::estimate, point_graph::PointGraph, trigonometry::acos}; +#[derive(Clone)] pub struct HeadingSettings { pub manual_wrong_wheel_scalar: f32, // +904 pub manual_damping: f32, // +908 diff --git a/crates/skate-core/src/riding/slide_friction.rs b/crates/skate-core/src/riding/slide_friction.rs index 257df578..efa60fb2 100644 --- a/crates/skate-core/src/riding/slide_friction.rs +++ b/crates/skate-core/src/riding/slide_friction.rs @@ -2,6 +2,7 @@ use super::vector::{clamp, dot3}; use crate::{point_graph::PointGraph, trigonometry::acos}; +#[derive(Clone)] pub struct SlideFrictionSettings { pub angle_response: PointGraph<16>, // +196/+260 pub speed_response: PointGraph<8>, // +324/+356 diff --git a/crates/skate-core/src/riding/speed_model.rs b/crates/skate-core/src/riding/speed_model.rs index c3bbd1c2..998a0aae 100644 --- a/crates/skate-core/src/riding/speed_model.rs +++ b/crates/skate-core/src/riding/speed_model.rs @@ -4,6 +4,7 @@ use super::vector::{clamp, dot3, normalize}; use crate::point_graph::PointGraph; +#[derive(Clone)] pub struct SpeedModelSettings { /// physics_speedmodel layout +0,+8,+16,+20,+24. pub negative_gain: f32, diff --git a/crates/skate-core/src/riding/straighten.rs b/crates/skate-core/src/riding/straighten.rs index 6a4b9a55..2f78539a 100644 --- a/crates/skate-core/src/riding/straighten.rs +++ b/crates/skate-core/src/riding/straighten.rs @@ -1,6 +1,7 @@ //! Complete TU3 Toolkit_CalcStraightenOutTorque 82D92B60. use super::vector::{clamp, cross, dot3}; use crate::point_graph::PointGraph; +#[derive(Clone)] pub struct StraightenSettings { pub time_response: PointGraph<16>, // settings +768/+832 pub opposite_turn_limit: f32, // +896 diff --git a/crates/skate-data/examples/vehicle_bone_names.rs b/crates/skate-data/examples/vehicle_bone_names.rs new file mode 100644 index 00000000..bd9e5da6 --- /dev/null +++ b/crates/skate-data/examples/vehicle_bone_names.rs @@ -0,0 +1,10 @@ +//! Export the exact native order for an authored vehicle animation package. +fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args_os().skip(1).collect(); + if args.len() != 2 { return Err("Usage: vehicle_bone_names OnBoard.abin bone-names.json".into()); } + let bank = skate_data::abin::Bank::load(std::path::Path::new(&args[0]))?; + let hierarchy = bank.hierarchy().ok_or("Bank has no skeleton hierarchy")?; + std::fs::write(&args[1], serde_json::to_vec_pretty(&hierarchy.bone_names)?)?; + println!("Exported {} bone names", hierarchy.bone_names.len()); + Ok(()) +} diff --git a/crates/skate-game/Cargo.toml b/crates/skate-game/Cargo.toml index 612b96f1..9cd5bde7 100644 --- a/crates/skate-game/Cargo.toml +++ b/crates/skate-game/Cargo.toml @@ -29,4 +29,7 @@ skate-data.workspace = true half = "=2.7.1" serde_json.workspace = true serde.workspace = true +skate-mods = { path = "../skate-mods" } + +skate-vehicles = { path = "../skate-vehicles" } skate-net = { path = "../skate-net" } diff --git a/crates/skate-game/src/animation.rs b/crates/skate-game/src/animation.rs index 5c2df903..a103f2ae 100644 --- a/crates/skate-game/src/animation.rs +++ b/crates/skate-game/src/animation.rs @@ -231,3 +231,48 @@ pub(crate) fn native_matrix(matrix: NativeMatrix) -> Mat4 { Vec3::from_array(matrix[3][..3].try_into().unwrap()).extend(1.0), ) } + +/// Vehicle clips use the same native model-space skeleton matrices as the SDK. +pub(crate) fn vehicle_pose(world:&mut World, pose:&[Mat4], steering:Option<(&[Mat4],f32)>) { + let board=world.resource::().animation.evaluator.frames.bone_names.iter().position(|n|n=="SKATEBOARD_ROOT"); + world.resource_scope(|world,animation:Mut| { + let basis=render_basis(); + for binding in &animation.bindings { + let Some(global)=pose.get(binding.bone).map(|m|*m*basis) else {continue;}; + let local=if let Some(parent)=binding.parent_bone {let Some(parent)=pose.get(parent) else {continue;};(*parent*basis).inverse()*global} else {global}; + let mut target=Transform::from_matrix(local); + if let Some((turn,weight))=steering { + if let Some(global)=turn.get(binding.bone).map(|m|*m*basis) { + let local=if let Some(parent)=binding.parent_bone {turn.get(parent).map(|p|(*p*basis).inverse()*global).unwrap_or(global)} else {global}; + target=crate::presentation::blend(target,Transform::from_matrix(local),weight.clamp(0.,1.)); + } + } + if let Some(mut transform)=world.get_mut::(binding.entity) {*transform=target;if Some(binding.bone)==board {transform.scale=Vec3::splat(0.001);}} + } + }); +} + +pub(crate) fn capture_vehicle_visual(world: &mut World) -> Vec<(Entity, Transform)> { + let mut ids:Vec<_>=world.resource::().bindings.iter().map(|b|b.entity).collect(); + ids.extend(world.query_filtered::>().iter(world)); + ids.into_iter().filter_map(|id|world.get::(id).copied().map(|t|(id,t))).collect() +} +pub(crate) fn blend_vehicle_visual(world:&mut World, from:&[(Entity,Transform)], alpha:f32) { + for &(id,previous) in from {if let Some(mut current)=world.get_mut::(id) {*current=crate::presentation::blend(previous,*current,alpha);}} +} + +/// Read the final displayed hierarchy, including vehicle steering and stance blends. +pub(crate) fn network_visual(world: &mut World) -> skate_net::packed::PoseState { + let root=world.query_filtered::<&Transform,With>().iter(world).next().copied().unwrap_or_default(); + let status=world.resource::(); + let mut locals=std::collections::BTreeMap::new(); + for b in &status.bindings {if let Some(t)=world.get::(b.entity) {locals.entry(b.bone).or_insert((b.parent_bone,t.to_matrix()));}} + fn global(i:usize,locals:&std::collections::BTreeMap,Mat4)>,depth:usize)->Mat4 { + if depth>128 {return Mat4::IDENTITY;} + let Some(&(parent,m))=locals.get(&i) else {return Mat4::IDENTITY;}; + parent.map_or(m,|p|global(p,locals,depth+1)*m) + } + let anchors=crate::physics::network::anchors(world.resource::()); + let basis=render_basis().inverse(); + skate_net::packed::PoseState{root:crate::physics::network::pose(root.to_matrix()),bones:anchors.into_iter().map(|i|skate_net::Bone{index:i as u16,pose:crate::physics::network::pose(if locals.contains_key(&i) {global(i,&locals,0)*basis} else {native_matrix(world.resource::().render_pose[i])})}).collect()} +} diff --git a/crates/skate-game/src/animation_pose.rs b/crates/skate-game/src/animation_pose.rs index c1462740..36ffaa38 100644 --- a/crates/skate-game/src/animation_pose.rs +++ b/crates/skate-game/src/animation_pose.rs @@ -17,6 +17,7 @@ mod authored_clips; pub(crate) struct PoseEvaluator { pub frames: AnimationFrames, authored: authored_clips::Replacements, + mod_clips: std::sync::RwLock>, } impl PoseEvaluator { @@ -29,6 +30,7 @@ impl PoseEvaluator { Ok(Self { frames: AnimationFrames::from_banks(banks)?, authored: Default::default(), + mod_clips: Default::default(), }) } @@ -37,9 +39,29 @@ impl PoseEvaluator { Ok(()) } + /// Reuses the existing constrained body-clip parser; never replaces board/trajectory data. + pub(crate) fn install_mod_clips(&self, owner: &str, text: &str) -> Result<(), String> { + let clips = authored_clips::Replacements::parse(text, &self.frames)?; + let mut all = self.mod_clips.write().map_err(|_| "Animation override lock poisoned")?; + for (id, other) in all.iter() { + if id != owner && clips.0.keys().any(|k| other.0.contains_key(k)) { + return Err(format!("Animation slots conflict with mod {id}")); + } + } + all.insert(owner.to_owned(), clips); + Ok(()) + } + pub(crate) fn remove_mod_clips(&self, owner: &str) { + if let Ok(mut all) = self.mod_clips.write() { all.remove(owner); } + } + pub(crate) fn clear_mod_clips(&self) { + if let Ok(mut all) = self.mod_clips.write() { all.clear(); } + } + /// Executes the ordered stock tree. This uses the native immediate ACS /// arithmetic; the host does not recreate packed animation job commands. pub fn evaluate(&self, commands: &[PoseCommand]) -> Result, String> { + let mod_clips = self.mod_clips.read().map_err(|_| "Animation override lock poisoned")?; let mut stack: Vec> = Vec::new(); for command in commands { match command { @@ -91,7 +113,7 @@ impl PoseEvaluator { loops, } => { let stock = self.frames.clip(name)?; - let clip = self.authored.clip(&stock.name).unwrap_or(stock); + let clip = mod_clips.values().find_map(|c|c.clip(&stock.name)).or_else(||self.authored.clip(&stock.name)).unwrap_or(stock); let mut pose = sample_clip(clip, *time)?; if self.frames.has_trajectory { let previous = sample_bone(clip, *previous_time, 0)?; diff --git a/crates/skate-game/src/animation_pose/authored_clips.rs b/crates/skate-game/src/animation_pose/authored_clips.rs index 7601a2d7..c3e950bc 100644 --- a/crates/skate-game/src/animation_pose/authored_clips.rs +++ b/crates/skate-game/src/animation_pose/authored_clips.rs @@ -4,7 +4,7 @@ use bevy::math::{Mat4, Quat, Vec3}; use serde::Deserialize; use std::collections::BTreeMap; #[derive(Default)] -pub(super) struct Replacements(BTreeMap); +pub(super) struct Replacements(pub(super) BTreeMap); #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct File { @@ -28,7 +28,7 @@ impl Replacements { }; Self::parse(&text, frames).map_err(|e| format!("{}: {e}", path.display())) } - fn parse(text: &str, frames: &AnimationFrames) -> Result { + pub(super) fn parse(text: &str, frames: &AnimationFrames) -> Result { let file: File = serde_json::from_str(text).map_err(|e| e.to_string())?; if file.version != 1 || file.bone_names != frames.bone_names || file.clips.len() > 32 { return Err("Authored clip format or skeleton mismatch".into()); diff --git a/crates/skate-game/src/app.rs b/crates/skate-game/src/app.rs index edae5339..e4c9be8d 100644 --- a/crates/skate-game/src/app.rs +++ b/crates/skate-game/src/app.rs @@ -40,6 +40,8 @@ pub(crate) fn build( let retail_scene = config.map.as_ref().is_some_and(|map| map.materials.iter().any(|m| m.retail_definition.is_some())); let mut app = App::new(); crate::custom_models::register_source(&mut app); + app.register_asset_source("mods", bevy::asset::io::AssetSourceBuilder::platform_default( + &crate::modding::package_root().to_string_lossy(), None)); app.add_plugins( DefaultPlugins .set(AssetPlugin { @@ -111,6 +113,7 @@ pub(crate) fn build( )); app.add_plugins((crate::session_marker::SessionMarkerPlugin, crate::customiser::CustomiserPlugin)); app.add_plugins(crate::custom_models::CustomModelsPlugin); + app.add_plugins(crate::modding::ModdingPlugin); crate::teleport_menu::install(&mut app); app.add_plugins(crate::updater::UpdaterPlugin); app.add_plugins(crate::multiplayer::MultiplayerPlugin); diff --git a/crates/skate-game/src/camera.rs b/crates/skate-game/src/camera.rs index 9664d5f7..dea117c3 100644 --- a/crates/skate-game/src/camera.rs +++ b/crates/skate-game/src/camera.rs @@ -24,7 +24,7 @@ use bevy::prelude::*; use crate::{app::FrameSet, config::Config}; #[derive(Component)] -struct GameplayCamera; +pub(crate) struct GameplayCamera; pub(crate) struct CameraPlugin; impl Plugin for CameraPlugin { @@ -49,9 +49,13 @@ fn spawn(mut commands: Commands, config: Res, retail: Res, windows: Query<&Window>, +#[derive(Default)] +pub(crate) struct VehicleCameraBlend { active:bool, previous:Option, from:Option, elapsed:f32 } + +pub(crate) fn present(vehicles: Res, mut runtime: ResMut, windows: Query<&Window>, history: Res, time: Res>, replay: Res, + virtual_time: Res>, mut vehicle_blend: Local, customiser: Option>, mut cameras: Query<(&mut Camera, &mut Transform, &mut Projection), With>) { if let Ok(window) = windows.single() { @@ -75,6 +79,21 @@ fn present(mut runtime: ResMut, windows: Query<&Window>, *transform = Transform::from_translation(eye).looking_at(center + right * (distance * 0.265625), Vec3::Y); if let Projection::Perspective(p) = &mut *projection { p.fov = 50_f32.to_radians(); } } + let vehicle=vehicles.camera(); + let active=vehicle.is_some(); + if active!=vehicle_blend.active { + vehicle_blend.from=vehicle_blend.previous;vehicle_blend.elapsed=0.;vehicle_blend.active=active; + } + if let Some(pose)=vehicle {*transform=pose;} + vehicle_blend.elapsed+=virtual_time.delta_secs(); + if let Some(from)=vehicle_blend.from { + if vehicle_blend.elapsed<0.5 && from.translation.distance(transform.translation)<50. { + let t=(vehicle_blend.elapsed/0.5).clamp(0.,1.);*transform=crate::presentation::blend(from,*transform,t*t*(3.-2.*t)); + } else {vehicle_blend.from=None;} + } + // Vehicle motion is already interpolated with the rider/chassis. A separate + // follow filter introduces relative motion and makes fixed ticks visible. + vehicle_blend.previous=Some(*transform); camera.is_active = true; } } diff --git a/crates/skate-game/src/graph_host/motion.rs b/crates/skate-game/src/graph_host/motion.rs index 1d93be0e..985beab1 100644 --- a/crates/skate-game/src/graph_host/motion.rs +++ b/crates/skate-game/src/graph_host/motion.rs @@ -1,5 +1,7 @@ #[path = "motion_toggle_execute.rs"] mod toggle_execution; +#[path = "motion_fakie_hold.rs"] +mod fakie_hold; #[path = "motion_air_execute.rs"] mod air_execution; #[path = "motion_execute.rs"] @@ -103,6 +105,8 @@ pub struct MotionHost { pub applying_body_tilt: bool, pub crouching_physical: Option, pub body_tilt_physical: Option, + pub hold_fakie: bool, + automatic_fakie_conditions: Vec, pub fakie_physical: Option, /// PhysOutAnimation158/157, used directly by IsRidingGoofy82BA5AA8. pub physical_stance: Option<(bool, bool)>, @@ -252,6 +256,8 @@ impl MotionHost { applying_body_tilt: false, crouching_physical: None, body_tilt_physical: None, + hold_fakie: false, + automatic_fakie_conditions: fakie_hold::bind(&graph.binding), fakie_physical: None, physical_stance: None, flags: Default::default(), @@ -306,6 +312,12 @@ impl MotionHost { } impl ConditionHost for MotionHost { fn condition_activation(&mut self, condition: usize, frame: &Frame) -> u32 { + // Gate only the authored automatic switch transitions. Other fakie + // conditions (tricks, pushes, dismounts) still see the real stance. + if self.remap.conditions.get(condition).is_some_and(|&id| + fakie_hold::blocked(self.hold_fakie, &self.automatic_fakie_conditions, id)) { + return 0; + } let result = self .remap .conditions diff --git a/crates/skate-game/src/graph_host/motion_fakie_hold.rs b/crates/skate-game/src/graph_host/motion_fakie_hold.rs new file mode 100644 index 00000000..88ddf7aa --- /dev/null +++ b/crates/skate-game/src/graph_host/motion_fakie_hold.rs @@ -0,0 +1,82 @@ +//! Trainer gates for the stock automatic fakie-to-switch animation transitions. +//! FakieTurn is player steering and must never be suppressed by this option. +use skate_data::state_graph::binding::{Binding, Node}; + +pub(super) fn bind(binding: &Binding) -> Vec { + let mut gates = vec![false; binding.operations.len()]; + for transition in &binding.transitions { + let Some(target) = transition.target else { continue; }; + let mut names = Vec::new(); + let mut state = Some(target); + while let Some(id) = state { + names.push(binding.states[id].name.as_str()); + state = binding.states[id].parent; + } + names.reverse(); + if names != ["Motion", "OnBoard", "OnGround", "RidingIdle", "Riding", "Turning", "Switch"] { + continue; + } + if let Some(expression) = transition.expression { + mark(binding, expression, &mut gates); + } + } + gates +} +fn mark(binding: &Binding, expression: usize, gates: &mut [bool]) { + for child in &binding.expressions[expression].children { + match *child { + Node::Operation(id) if binding.operations[id].name == "IsRidingFakie" => gates[id] = true, + Node::Expression(id) => mark(binding, id, gates), + _ => {} + } + } +} +pub(super) fn blocked(enabled: bool, gates: &[bool], operation: usize) -> bool { + enabled && gates.get(operation).copied().unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use skate_data::state_graph::{GraphAttribute, GraphElement, StateGraph}; + struct Tree(&'static str, Vec<(&'static str, &'static str)>, Vec); + fn state(name: &'static str, children: Vec) -> Tree { Tree("state", vec![("name", name)], children) } + fn condition() -> Tree { Tree("condition", vec![("name", "IsRidingFakie")], vec![]) } + fn transition(target: &'static str) -> Tree { + Tree("transition", vec![("target", target)], vec![Tree("expression", vec![("op", "and")], vec![condition()])]) + } + fn add(tree: Tree, graph: &mut StateGraph) -> usize { + let id = graph.elements.len(); + graph.elements.push(GraphElement { source_offset: id, tag: tree.0.into(), + attributes: tree.1.into_iter().map(|(name, text)| GraphAttribute { + name: name.into(), text: text.into(), float_bits: 0, boolean_byte: 0, + }).collect(), children: vec![] }); + let children = tree.2.into_iter().map(|child| add(child, graph)).collect(); + graph.elements[id].children = children; + id + } + #[test] + fn gates_idle_and_bump_switch_but_preserves_other_fakie_conditions() { + let tree = state("Motion", vec![state("OnBoard", vec![state("OnGround", vec![ + state("RidingIdle", vec![ + state("Bumped", vec![transition("Riding.Turning.Switch")]), + state("Riding", vec![state("Turning", vec![ + state("Idle", vec![transition("Switch")]), state("Switch", vec![]), + state("Push", vec![Tree("expression", vec![], vec![condition()])]), + ])]), + ]), + // Same leaf name elsewhere must not be treated as automatic switching. + state("Other", vec![state("Idle", vec![transition("Switch")]), state("Switch", vec![])]), + ])])]); + let mut graph = StateGraph { elements: vec![] }; add(tree, &mut graph); + let binding = Binding::from_graph(&graph).unwrap(); + let gates = bind(&binding); + assert_eq!(gates.iter().filter(|&&gate| gate).count(), 2); + assert_eq!(gates.iter().filter(|&&gate| !gate).count(), 2); + for (id, gate) in gates.iter().enumerate() { + assert!(!blocked(false, &gates, id), "disabled trainer must preserve stock transitions"); + assert_eq!(blocked(true, &gates, id), *gate); + } + assert!(!blocked(true, &gates, usize::MAX)); + } +} diff --git a/crates/skate-game/src/graphics_menu.rs b/crates/skate-game/src/graphics_menu.rs index 9c53d0fe..9734e1da 100644 --- a/crates/skate-game/src/graphics_menu.rs +++ b/crates/skate-game/src/graphics_menu.rs @@ -218,7 +218,7 @@ fn setup( BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| { panel.spawn((Text::new("GAME MENU"),TextFont {font_size:32.,..default()},TextColor(Color::WHITE))); panel.spawn((Text::new("GAMEPLAY & GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85)))); - for i in 0..15 { + for i in 0..16 { panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(32),padding:UiRect::all(px(6)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()}, BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| { row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:18.,..default()},TextColor(Color::WHITE))); @@ -263,6 +263,7 @@ pub(crate) fn interact( mut transition: ResMut, mut customiser: ResMut, mut custom_models: ResMut, + (mut mods, panel): (ResMut, Res), nav: Res, mut physics: ResMut, keys: Res>, @@ -280,7 +281,7 @@ pub(crate) fn interact( time.pause(); return; } - if travel.open || travel.closed_this_frame || customiser.open || custom_models.open { return; } + if mods.open || travel.open || travel.closed_this_frame || customiser.open || custom_models.open { return; } if keys.just_pressed(KeyCode::Escape) || nav.pressed & 0x10 != 0 { menu.open = !menu.open; } @@ -306,7 +307,8 @@ pub(crate) fn interact( } } if menu.open { - let rows = if menu.multiplayer { 11 } else { 15 }; + let rows = if menu.multiplayer { 11 } else { 16 }; + if !panel.focused { if keys.just_pressed(KeyCode::ArrowUp) || nav.pressed & 1 != 0 { menu.selected = (menu.selected + rows - 1) % rows; } @@ -319,7 +321,9 @@ pub(crate) fn interact( if keys.just_pressed(KeyCode::ArrowRight) || keys.just_pressed(KeyCode::Enter) || nav.pressed & (8 | 0x1000) != 0 { action = Some((menu.selected, 1)); } + } for (interaction, row) in &buttons { + if panel.dragging() { continue; } if *interaction == Interaction::Pressed { menu.selected = row.0; action = Some((row.0, 1)); @@ -427,6 +431,7 @@ pub(crate) fn interact( 12 => custom_models.begin(), 13 => menu.status = updater.open(false), 14 => travel.open = true, + 15 => mods.begin(), _ => {} } } @@ -512,6 +517,7 @@ fn labels( customiser: Res, custom_models: Res, travel: Res, + mods: Res, net: Res, window: Single<&Window, With>, mut root: Single<&mut Node, With>, @@ -519,7 +525,7 @@ fn labels( mut status: Single<&mut Text, With>, mut buttons: Query<(&MenuRow, &Interaction, &mut BackgroundColor, &mut Node), Without>, ) { - root.display = if menu.open && !travel.open && !customiser.open && !custom_models.open { + root.display = if menu.open && !mods.open && !travel.open && !customiser.open && !custom_models.open { Display::Flex } else { Display::None @@ -613,6 +619,7 @@ fn labels( 10 => "Character customiser".into(), 12 => "Custom models".into(), 13 => "Updates".into(), + 15 => "Mods".into(), 14 => "Teleport…".into(), _ => "Multiplayer".into(), } diff --git a/crates/skate-game/src/main.rs b/crates/skate-game/src/main.rs index 35f1c7c3..a2b8596f 100644 --- a/crates/skate-game/src/main.rs +++ b/crates/skate-game/src/main.rs @@ -31,6 +31,7 @@ mod skater_animation; mod verification; mod performance; mod graphics_menu; +mod modding; mod customiser; mod customiser_parts; mod customiser_material; diff --git a/crates/skate-game/src/modding/menu.rs b/crates/skate-game/src/modding/menu.rs new file mode 100644 index 00000000..deaeac82 --- /dev/null +++ b/crates/skate-game/src/modding/menu.rs @@ -0,0 +1,356 @@ +use super::Mods; +use bevy::prelude::*; +#[derive(Resource, Default)] +pub(crate) struct ModMenu { + pub open: bool, + just_opened: bool, + id: Option, + selected: usize, + editing: Option<(String, String)>, + status: String, +} +impl ModMenu { + pub fn configure(&mut self, id: String) { + self.begin(); + self.id = Some(id); + } + pub fn begin(&mut self) { + self.open = true; + self.just_opened = true; + self.id = None; + self.selected = 0; + } +} +#[derive(Component)] +struct Root; +#[derive(Component)] +struct Row(usize); +#[derive(Component)] +struct Label(usize); +#[derive(Component)] +struct Badge(usize); +#[derive(Component)] +struct Detail; +#[derive(Clone)] +enum Action { + Select(String), + Enable(String), + Reload(String), + Reset(String), + Setting(String, String), + Scan, + OpenFolder, + Back, +} +fn rows(menu: &ModMenu, mods: &Mods) -> Vec<(String, Action)> { + let manager = &mods.manager; + if let Some(id) = &menu.id { + if let Some(p) = manager.packages.get(id) { + let mut rows = vec![ + ( + if p.running() { + "DISABLE MOD".into() + } else { + "ENABLE MOD".into() + }, + Action::Enable(id.clone()), + ), + ("Reload from disk".into(), Action::Reload(id.clone())), + ( + "Reset settings to defaults".into(), + Action::Reset(id.clone()), + ), + ]; + for (key, s) in &p.manifest.settings { + rows.push(( + format!("{}: {}", s.label, p.settings[key]), + Action::Setting(id.clone(), key.clone()), + )); + } + rows.push(("Back".into(), Action::Back)); + return rows; + } + } + let mut rows: Vec<_> = manager + .packages + .iter() + .map(|(id, p)| (p.manifest.name.clone(), Action::Select(id.clone()))) + .collect(); + rows.push(("Open mods folder".into(), Action::OpenFolder)); + rows.push(("Rescan packages".into(), Action::Scan)); + rows.push(("Back to pause menu".into(), Action::Back)); + rows +} +pub(super) fn install(app: &mut App) { + app.add_systems(PostStartup, setup) + .add_systems( + PreUpdate, + input + .after(crate::graphics_menu::MenuInput) + .before(crate::map_transition::MapTransitionSet), + ) + .add_systems(Update, draw); +} +fn setup(mut commands: Commands) { + commands.spawn((Root,GlobalZIndex(20),Node{display:Display::None,width:percent(100),height:percent(100),position_type:PositionType::Absolute,align_items:AlignItems::Center,justify_content:JustifyContent::Center,..default()},BackgroundColor(Color::srgba(0.015,0.025,0.04,0.98)))).with_children(|root| { + root.spawn((Node{width:px(760),max_width:percent(95),padding:UiRect::all(px(18)),flex_direction:FlexDirection::Column,row_gap:px(5),..default()},BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| { + panel.spawn((Text::new("MODS — Lua SDK 1"),TextFont{font_size:28.,..default()},TextColor(Color::WHITE))); + for i in 0..8 { panel.spawn((Button,Row(i),Node{min_height:px(32),padding:UiRect::all(px(6)),justify_content:JustifyContent::SpaceBetween,..default()},BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {row.spawn((Label(i),Text::new(""),TextFont{font_size:17.,..default()},TextColor(Color::WHITE)));row.spawn((Badge(i),Text::new(""),TextFont{font_size:16.,..default()},TextColor(Color::WHITE)));}); } + panel.spawn((Detail,Text::new(""),TextFont{font_size:15.,..default()},TextColor(Color::srgb(0.65,0.85,0.85)))); + panel.spawn((Text::new("Arrows / D-pad: select & adjust | Enter / A: choose | Esc / B: back\nStrings: Enter then type; Enter saves, Esc cancels. More rows scroll automatically."),TextFont{font_size:14.,..default()},TextColor(Color::WHITE))); + }); + }); +} +fn input( + mut menu: ResMut, + mut mods: ResMut, + keys: Res>, + nav: Res, + buttons: Query<(&Interaction, &Row), Changed>, + mut typing: MessageReader, +) { + if !menu.open { + typing.clear(); + return; + } + if menu.just_opened { + menu.just_opened = false; + typing.clear(); + return; + } + if let Some((key, mut value)) = menu.editing.take() { + if keys.just_pressed(KeyCode::Escape) || nav.pressed & 0x2000 != 0 { + typing.clear(); + return; + } + if keys.just_pressed(KeyCode::Enter) || nav.pressed & 0x1000 != 0 { + if let Some(id) = &menu.id { + menu.status = mods + .manager + .setting(id, &key, value.into()) + .err() + .unwrap_or_else(|| "Saved".into()); + } + typing.clear(); + return; + } + if keys.just_pressed(KeyCode::Backspace) { + value.pop(); + } + for e in typing.read() { + if e.state == bevy::input::ButtonState::Pressed { + if let Some(t) = &e.text { + for c in t.chars().filter(|c| !c.is_control()) { + if value.chars().count() < 128 { + value.push(c); + } + } + } + } + } + menu.editing = Some((key, value)); + return; + } + typing.clear(); + if keys.just_pressed(KeyCode::Escape) || nav.pressed & (0x2000 | 0x10) != 0 { + if menu.id.take().is_none() { + menu.open = false; + } + menu.selected = 0; + return; + } + let entries = rows(&menu, &mods); + let count = entries.len(); + menu.selected = menu.selected.min(count - 1); + if keys.just_pressed(KeyCode::ArrowUp) || nav.pressed & 1 != 0 { + menu.selected = (menu.selected + count - 1) % count; + } + if keys.just_pressed(KeyCode::ArrowDown) || nav.pressed & 2 != 0 { + menu.selected = (menu.selected + 1) % count; + } + let mut direction = 0; + if keys.just_pressed(KeyCode::ArrowLeft) || nav.pressed & 4 != 0 { + direction = -1; + } + if keys.just_pressed(KeyCode::ArrowRight) + || keys.just_pressed(KeyCode::Enter) + || nav.pressed & (8 | 0x1000) != 0 + { + direction = 1; + } + let offset = menu.selected / 8 * 8; + for (interaction, row) in &buttons { + if *interaction == Interaction::Pressed && row.0 + offset < count { + menu.selected = row.0 + offset; + direction = 1; + } + } + if direction == 0 { + return; + } + let action = entries[menu.selected].1.clone(); + let result = match action { + Action::Select(id) => { + menu.id = Some(id); + menu.selected = 0; + Ok(()) + } + Action::Back => { + if menu.id.take().is_none() { + menu.open = false; + } + menu.selected = 0; + Ok(()) + } + Action::OpenFolder => { + #[cfg(target_os = "windows")] + let program = "explorer.exe"; + #[cfg(target_os = "macos")] + let program = "open"; + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + let program = "xdg-open"; + std::process::Command::new(program) + .arg(mods.manager.root()) + .spawn() + .map(|_| ()) + .map_err(|e| format!("Could not open mods folder: {e}")) + } + Action::Scan => { + mods.manager.scan(true); + Ok(()) + } + Action::Enable(id) => { + let enabled = !mods.manager.packages[&id].running(); + mods.manager.enable(&id, enabled) + } + Action::Reload(id) => { + mods.manager.scan(true); + mods.manager.reload(&id); + Ok(()) + } + Action::Reset(id) => mods.manager.reset(&id), + Action::Setting(id, key) => { + let p = &mods.manager.packages[&id]; + let s = &p.manifest.settings[&key]; + let value = &p.settings[&key]; + let next = match s.kind.as_str() { + "boolean" => Some(serde_json::json!(!value.as_bool().unwrap())), + "number" => Some(serde_json::json!( + (value.as_f64().unwrap() + direction as f64 * s.step.unwrap()) + .clamp(s.min.unwrap(), s.max.unwrap()) + )), + "choice" => { + let i = s + .choices + .iter() + .position(|v| Some(v.as_str()) == value.as_str()) + .unwrap_or(0); + Some(serde_json::json!( + s.choices + [(i as i32 + direction).rem_euclid(s.choices.len() as i32) as usize] + )) + } + "string" => { + menu.editing = Some((key.clone(), value.as_str().unwrap().into())); + None + } + _ => None, + }; + if let Some(value) = next { + mods.manager.setting(&id, &key, value) + } else { + Ok(()) + } + } + }; + menu.status = result.err().unwrap_or_default(); +} +fn draw( + menu: Res, + mods: Res, + mut root: Single<&mut Node, With>, + mut labels: Query<(&Label, &mut Text), Without>, + mut badges: Query<(&Badge, &mut Text, &mut TextColor), Without