Add Theater editing and chat, improve controller UI, audio, combat and network recovery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -158,7 +158,7 @@ dependencies = [
|
||||
"android-properties",
|
||||
"bitflags 2.13.1",
|
||||
"cc",
|
||||
"jni 0.22.4",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -2206,6 +2206,7 @@ dependencies = [
|
||||
"session",
|
||||
"sha2",
|
||||
"sim",
|
||||
"theater",
|
||||
"ui",
|
||||
]
|
||||
|
||||
@@ -2312,12 +2313,6 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -2560,14 +2555,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.17.3"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
|
||||
checksum = "6f02e8d0327b42d3e2e4ab2119af397344eb9fc54a34bf0ddeaa1277af8681f1"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"block2 0.6.2",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
@@ -2582,10 +2578,9 @@ dependencies = [
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2700,6 +2695,12 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -3027,6 +3028,12 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -3560,6 +3567,7 @@ dependencies = [
|
||||
"net",
|
||||
"playerstate_iw4",
|
||||
"render_material",
|
||||
"resvg",
|
||||
"sim",
|
||||
"structured_data_iw4",
|
||||
"weapon_iw4",
|
||||
@@ -3819,6 +3827,12 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -3921,22 +3935,6 @@ dependencies = [
|
||||
"tower-http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
@@ -4020,6 +4018,18 @@ dependencies = [
|
||||
name = "killcam_iw4"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"euclid",
|
||||
"polycool",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lalrpop-util"
|
||||
version = "0.23.1"
|
||||
@@ -4291,12 +4301,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
|
||||
|
||||
[[package]]
|
||||
name = "marks_iw4"
|
||||
@@ -4684,6 +4691,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
@@ -5157,6 +5165,12 @@ dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
@@ -5247,6 +5261,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "polycool"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.14.0"
|
||||
@@ -5753,6 +5776,7 @@ dependencies = [
|
||||
"hud_iw4",
|
||||
"lighting_iw4",
|
||||
"math_iw4",
|
||||
"render_frame",
|
||||
"render_material",
|
||||
"sim",
|
||||
"xmodel_runtime",
|
||||
@@ -5817,6 +5841,21 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.48.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67e3803f97b999e80cbf7c6ecdd07a8102204d92e1633cf48783720c521196bd"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"log",
|
||||
"pico-args",
|
||||
"rgb",
|
||||
"svgtypes",
|
||||
"tiny-skia 0.12.0",
|
||||
"usvg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.17.2"
|
||||
@@ -5844,6 +5883,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -5872,6 +5920,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
@@ -5972,7 +6029,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
@@ -6054,7 +6111,7 @@ dependencies = [
|
||||
"log",
|
||||
"memmap2",
|
||||
"smithay-client-toolkit",
|
||||
"tiny-skia",
|
||||
"tiny-skia 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6269,6 +6326,21 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.44.0"
|
||||
@@ -6388,6 +6460,9 @@ name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structured_data_iw4"
|
||||
@@ -6408,6 +6483,16 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swash"
|
||||
version = "0.2.10"
|
||||
@@ -6600,6 +6685,26 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theater"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"console",
|
||||
"frame",
|
||||
"net",
|
||||
"render",
|
||||
"render_anim",
|
||||
"render_scene",
|
||||
"replay",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"session",
|
||||
"sha2",
|
||||
"sim",
|
||||
"ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -6660,7 +6765,22 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"tiny-skia-path",
|
||||
"tiny-skia-path 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png",
|
||||
"tiny-skia-path 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6674,6 +6794,17 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -7124,6 +7255,25 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.48.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "977d0a4abdef933f424a99fe09f95576e089b90aebc6f016a3bc813762493e91"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"imagesize",
|
||||
"kurbo",
|
||||
"log",
|
||||
"pico-args",
|
||||
"roxmltree",
|
||||
"simplecss",
|
||||
"siphasher",
|
||||
"strict-num",
|
||||
"svgtypes",
|
||||
"tiny-skia-path 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -7783,22 +7933,13 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7807,7 +7948,7 @@ version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7819,35 +7960,20 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7859,36 +7985,18 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[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.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[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.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -7901,48 +8009,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[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.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[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.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[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.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -63,6 +63,7 @@ members = [
|
||||
"crates/render_material",
|
||||
"crates/render_scene",
|
||||
"crates/replay",
|
||||
"crates/theater",
|
||||
"crates/session",
|
||||
"crates/sim",
|
||||
"crates/structured_data_iw4",
|
||||
@@ -169,6 +170,7 @@ render_material = { path = "crates/render_material" }
|
||||
render_scene = { path = "crates/render_scene" }
|
||||
session = { path = "crates/session" }
|
||||
replay = { path = "crates/replay" }
|
||||
theater = { path = "crates/theater" }
|
||||
sim = { path = "crates/sim" }
|
||||
trace_iw4 = { path = "crates/trace_iw4" }
|
||||
ui = { path = "crates/ui" }
|
||||
|
||||
@@ -920,7 +920,7 @@ Apache License 2.0 (Apache-2.0)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
codespan-reporting 0.13.1 — https://github.com/brendanzab/codespan
|
||||
cpal 0.17.3 — https://github.com/RustAudio/cpal
|
||||
cpal 0.18.2 — https://github.com/RustAudio/cpal
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
@@ -1604,6 +1604,7 @@ Used by:
|
||||
session 0.1.0
|
||||
sim 0.1.0
|
||||
structured_data_iw4 0.1.0
|
||||
theater 0.1.0
|
||||
trace_iw4 0.1.0
|
||||
ui 0.1.0
|
||||
updater 0.1.0
|
||||
@@ -1787,6 +1788,45 @@ pub(crate) static GB18030_2022_OVERRIDE_BYTES: [[u8; 2]; 18] = [
|
||||
];
|
||||
|
||||
|
||||
================================================================================
|
||||
BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
tiny-skia-path 0.12.0 — https://github.com/linebender/tiny-skia/tree/master/path
|
||||
tiny-skia 0.12.0 — https://github.com/linebender/tiny-skia
|
||||
|
||||
Copyright (c) 2011 Google Inc. All rights reserved.
|
||||
Copyright (c) 2020 Yevhenii Reizner All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
================================================================================
|
||||
BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -2713,6 +2753,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
data-url 0.3.2 — https://github.com/servo/rust-url
|
||||
idna 1.1.0 — https://github.com/servo/rust-url/
|
||||
percent-encoding 2.3.2 — https://github.com/servo/rust-url/
|
||||
url 2.5.8 — https://github.com/servo/rust-url
|
||||
@@ -2914,6 +2955,33 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
float-cmp 0.9.0 — https://github.com/mikedilger/float-cmp
|
||||
|
||||
Copyright (c) 2014-2020 Optimal Computing (NZ) Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -3898,6 +3966,40 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
kurbo 0.13.1 — https://github.com/linebender/kurbo
|
||||
polycool 0.4.0 — https://github.com/linebender/kurbo
|
||||
|
||||
Copyright (c) 2018 Raph Levien
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -3948,6 +4050,34 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
svgtypes 0.16.1 — https://github.com/linebender/svgtypes
|
||||
|
||||
Copyright (c) 2018 Yevhenii Reizner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -4466,6 +4596,34 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
pico-args 0.5.0 — https://github.com/RazrFalcon/pico-args
|
||||
|
||||
Copyright (c) 2019 Yevhenii Reizner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -4957,6 +5115,33 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
strict-num 0.1.1 — https://github.com/RazrFalcon/strict-num
|
||||
|
||||
Copyright (c) 2022 Yevhenii Reizner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -5330,6 +5515,40 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
resvg 0.48.1 — https://github.com/linebender/resvg
|
||||
usvg 0.48.1 — https://github.com/linebender/resvg
|
||||
|
||||
Copyright 2017 the Resvg Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -5627,6 +5846,35 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
imagesize 0.15.0 — https://github.com/Roughsketch/imagesize
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Maiddog
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -5763,6 +6011,35 @@ The above copyright notice and this permission notice (including the next paragr
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
rgb 0.8.53 — https://github.com/kornelski/rust-rgb
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Kornel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -6764,6 +7041,31 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
siphasher 1.0.4 — https://github.com/jedisct1/rust-siphash
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -7015,6 +7317,64 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
simplecss 0.2.2 — https://github.com/linebender/simplecss
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Reizner Evgeniy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
Used by:
|
||||
roxmltree 0.21.1 — https://github.com/RazrFalcon/roxmltree
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Yevhenii Reizner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
MIT License (MIT)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -24,6 +24,8 @@ nowhere else: maps, seed rule, scenes, positions, angles, weapons, durations
|
||||
and input. To pin a scene to an owner-chosen position, replace its `Place`
|
||||
there; the runner does not change.
|
||||
|
||||
The heavy scenario uses a 2560×1440 window with vsync off, recorded alongside the observed render target in `run.json`.
|
||||
|
||||
The heavy scenario also plants a claymore, drops a weapon through player death,
|
||||
restarts the script round in place, and captures entity and resource ownership at gameplay and teardown boundaries.
|
||||
Both scenarios use isolated accounts and wait for native progression content and run `unlock all` from the menu before
|
||||
|
||||
@@ -328,6 +328,18 @@ fn run(root: &Path, args: &Args) -> Result<bool, String> {
|
||||
});
|
||||
}
|
||||
|
||||
let (width, height) = hgl::RESOLUTION;
|
||||
std::fs::write(
|
||||
run_dir.join("settings.cfg"),
|
||||
format!("resolution={width}x{height}\nfullscreen=false\nvsync=false\n"),
|
||||
)
|
||||
.map_err(|error| format!("write scenario settings: {error}"))?;
|
||||
manifest["requested_render"] = json!({
|
||||
"width": width,
|
||||
"height": height,
|
||||
"fullscreen": false,
|
||||
"present_mode": "AutoNoVsync",
|
||||
});
|
||||
let cache = prepare_cache(root, &run_dir, args.cache)?;
|
||||
manifest["cache"] = cache;
|
||||
let bin = stage_binary(&args.bin, &run_dir)?;
|
||||
@@ -353,6 +365,7 @@ fn run(root: &Path, args: &Args) -> Result<bool, String> {
|
||||
args: child_args,
|
||||
env: vec![
|
||||
("IW4L_PERF", "1".into()),
|
||||
("IW4L_PRESENT_MODE", "AutoNoVsync".into()),
|
||||
("IW4L_GAMETYPE", SCENARIO.gametype.into()),
|
||||
(
|
||||
"IW4L_SETTINGS_PATH",
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::scenario::{
|
||||
MapB, Phase, Place, ResolvedScene, Scenario, Scene, Weapon, capture_commands,
|
||||
};
|
||||
|
||||
pub const RESOLUTION: (u32, u32) = (2560, 1440);
|
||||
|
||||
pub const SCENARIO: Scenario = Scenario {
|
||||
name: "heavy_gameplay_lifecycle",
|
||||
gametype: "dm",
|
||||
|
||||
@@ -95,7 +95,7 @@ pub enum SecondaryActivation {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SecondaryPolicySource {
|
||||
T5IndependentCompatibility,
|
||||
IndependentLayers,
|
||||
PrimaryPreparedCompatibility,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ impl CueCompiler for T5CueCompiler {
|
||||
streamed_decode: crate::StreamedDecodePolicy::WmaContainerWithWaveCompatibility,
|
||||
secondary: (
|
||||
SecondaryActivation::OnResolution,
|
||||
SecondaryPolicySource::T5IndependentCompatibility,
|
||||
SecondaryPolicySource::IndependentLayers,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ impl CueCompiler for T6CueCompiler {
|
||||
zero_volume_unity: false,
|
||||
streamed_decode: crate::StreamedDecodePolicy::Detected,
|
||||
secondary: (
|
||||
SecondaryActivation::OnPrimaryPrepared,
|
||||
SecondaryPolicySource::PrimaryPreparedCompatibility,
|
||||
SecondaryActivation::OnResolution,
|
||||
SecondaryPolicySource::IndependentLayers,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +464,13 @@ impl MenuCatalog {
|
||||
seen.len()
|
||||
}
|
||||
|
||||
pub fn insert_string_table(&mut self, table: CapturedStringTable) {
|
||||
self.string_tables
|
||||
.retain(|name, _| !name.eq_ignore_ascii_case(&table.name));
|
||||
self.string_tables
|
||||
.insert(table.name.to_ascii_lowercase(), table);
|
||||
}
|
||||
|
||||
pub fn string_table(&self, name: &str) -> Option<&CapturedStringTable> {
|
||||
if let Some(table) = self.string_tables.get(name) {
|
||||
return Some(table);
|
||||
|
||||
@@ -1329,6 +1329,7 @@ pub struct T6AttachmentModel {
|
||||
struct WeaponRow {
|
||||
preparation: WeaponPreparationRecipe,
|
||||
name: String,
|
||||
native_script_name: String,
|
||||
alternate_weapon: Option<String>,
|
||||
impact_payload: Option<String>,
|
||||
alternate_index: u32,
|
||||
@@ -1469,6 +1470,7 @@ impl Default for WeaponRow {
|
||||
Self {
|
||||
preparation: WeaponPreparationRecipe::native(crate::AssetNamespace::Iw4),
|
||||
name: String::new(),
|
||||
native_script_name: String::new(),
|
||||
alternate_weapon: None,
|
||||
impact_payload: None,
|
||||
alternate_index: 0,
|
||||
|
||||
@@ -115,11 +115,12 @@ impl WeaponCatalog {
|
||||
dpad_icon_image: None,
|
||||
dpad_icon_atlas: None,
|
||||
dpad_icon_ratio: 0,
|
||||
kill_icon: leftover_t5_material_name_opt(
|
||||
stream,
|
||||
geometry.weap_def,
|
||||
fastfile_t5::size::WEAPON_DEF_KILL_ICON_OFF,
|
||||
),
|
||||
kill_icon: geometry
|
||||
.kill_icon_name
|
||||
.and_then(|ptr| stream.cstr(ptr).ok())
|
||||
.map(asset_core::AssetRef::bare_name)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(str::to_owned),
|
||||
kill_icon_slot: leftover_t5_asset_slot(
|
||||
stream,
|
||||
geometry.weap_def,
|
||||
|
||||
@@ -349,6 +349,7 @@ impl WeaponBuild {
|
||||
rows.push(WeaponRow {
|
||||
preparation: WeaponPreparationRecipe::for_capture(entry.namespace, &name),
|
||||
name,
|
||||
native_script_name: entry.name,
|
||||
alternate_weapon: entry.alternate_weapon,
|
||||
impact_payload: entry.impact_payload,
|
||||
alternate_index: 0,
|
||||
|
||||
@@ -231,7 +231,17 @@ impl WeaponRegistry {
|
||||
.describe_configuration(id)
|
||||
.ok_or("unlock.missing_family")?;
|
||||
let family = selection.family.as_ref().ok_or("unlock.missing_family")?;
|
||||
self.item_unlock_requirement(family.namespace, &family.base)
|
||||
match self.item_unlock_requirement(family.namespace, &family.base) {
|
||||
Err("unlock.missing_item")
|
||||
if matches!(
|
||||
family.namespace,
|
||||
crate::AssetNamespace::Iw4 | crate::AssetNamespace::Iw5
|
||||
) =>
|
||||
{
|
||||
Ok(Default::default())
|
||||
}
|
||||
requirement => requirement,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vehicle_compass(&self) -> impl Iterator<Item = (&str, &[String; 2], [i32; 2])> {
|
||||
@@ -920,6 +930,18 @@ impl WeaponRegistry {
|
||||
gsc_weapon_script_name(self.name_of(index))
|
||||
}
|
||||
|
||||
pub fn native_script_aliases(&self) -> impl Iterator<Item = (String, u32)> + '_ {
|
||||
self.rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(1)
|
||||
.filter_map(|(id, row)| {
|
||||
(normalize_weapon_name(&row.native_script_name) == row.name
|
||||
&& row.native_script_name != gsc_weapon_script_name(&row.name))
|
||||
.then(|| (row.native_script_name.clone(), id as u32))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn configuration_supported(&self, id: u32) -> bool {
|
||||
let Some(row) = self.rows.get(id as usize) else {
|
||||
return false;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use std::{
|
||||
collections::{HashMap, hash_map::DefaultHasher},
|
||||
hash::{Hash, Hasher},
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use asset_iw4::size as sz;
|
||||
use bevy::prelude::{Quat, Vec3};
|
||||
use fastfile_iw4::{Ptr, ScriptStrings, XModelGeometry, ZonePtr, ZoneStream};
|
||||
@@ -59,11 +65,8 @@ pub struct ModelSkel {
|
||||
pub tag_weapon: Option<usize>,
|
||||
|
||||
pub pose: Option<ModelPoseSrc>,
|
||||
pub positions: Vec<[f32; 3]>,
|
||||
pub normals: Vec<[f32; 3]>,
|
||||
pub colors: Vec<[f32; 4]>,
|
||||
pub uvs: Vec<[f32; 2]>,
|
||||
pub indices: Vec<u32>,
|
||||
pub positions: Arc<[[f32; 3]]>,
|
||||
pub indices: Arc<[u32]>,
|
||||
|
||||
pub surface_materials: Vec<Option<crate::WalkLocalMaterialIndex>>,
|
||||
|
||||
@@ -77,11 +80,11 @@ pub struct ModelSkel {
|
||||
|
||||
pub surface_vert_list_count: Vec<Option<u32>>,
|
||||
|
||||
pub vert_skin: Vec<VertSkin>,
|
||||
pub vert_skin: Arc<[VertSkin]>,
|
||||
pub rigid_verts: usize,
|
||||
pub blend_verts: usize,
|
||||
|
||||
pub packed_vertices: Vec<[u8; sz::GFX_PACKED_VERTEX]>,
|
||||
pub packed_vertices: Arc<[[u8; sz::GFX_PACKED_VERTEX]]>,
|
||||
|
||||
pub radius: Option<f32>,
|
||||
|
||||
@@ -106,7 +109,57 @@ pub struct ModelSkel {
|
||||
pub lod_surf_span: [(u16, u16); 4],
|
||||
}
|
||||
|
||||
fn vertex_word(vertex: &[u8; sz::GFX_PACKED_VERTEX], offset: usize) -> u32 {
|
||||
u32::from_le_bytes(vertex[offset..offset + 4].try_into().unwrap())
|
||||
}
|
||||
|
||||
fn vertex_normal(vertex: &[u8; sz::GFX_PACKED_VERTEX]) -> [f32; 3] {
|
||||
normalize_or_up(unpack_unit_vec(vertex_word(vertex, 24)))
|
||||
}
|
||||
|
||||
impl ModelSkel {
|
||||
pub fn has_same_vertex_attributes(&self, other: &Self) -> bool {
|
||||
self.packed_vertices.len() == other.packed_vertices.len()
|
||||
&& self
|
||||
.packed_vertices
|
||||
.iter()
|
||||
.zip(other.packed_vertices.iter())
|
||||
.all(|(a, b)| {
|
||||
let uv = vertex_word(a, 20);
|
||||
let other_uv = vertex_word(b, 20);
|
||||
let half_is_nan = |bits: u32| bits & 0x7c00 == 0x7c00 && bits & 0x03ff != 0;
|
||||
vertex_word(a, 16) == vertex_word(b, 16)
|
||||
&& (vertex_word(a, 24) == vertex_word(b, 24)
|
||||
|| vertex_normal(a) == vertex_normal(b))
|
||||
&& if uv == other_uv {
|
||||
// Identical NaN encodings must still fail decoded float equality.
|
||||
!half_is_nan(uv) && !half_is_nan(uv >> 16)
|
||||
} else {
|
||||
unpack_packed_tex_coords(uv) == unpack_packed_tex_coords(other_uv)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normal(&self, index: usize) -> Option<[f32; 3]> {
|
||||
self.packed_vertices.get(index).map(vertex_normal)
|
||||
}
|
||||
|
||||
pub fn normals(&self) -> impl ExactSizeIterator<Item = [f32; 3]> + '_ {
|
||||
self.packed_vertices.iter().map(vertex_normal)
|
||||
}
|
||||
|
||||
pub fn colors(&self) -> impl ExactSizeIterator<Item = [f32; 4]> + '_ {
|
||||
self.packed_vertices
|
||||
.iter()
|
||||
.map(|v| unpack_color(vertex_word(v, 16)))
|
||||
}
|
||||
|
||||
pub fn uvs(&self) -> impl ExactSizeIterator<Item = [f32; 2]> + '_ {
|
||||
self.packed_vertices
|
||||
.iter()
|
||||
.map(|v| unpack_packed_tex_coords(vertex_word(v, 20)))
|
||||
}
|
||||
|
||||
pub fn retained_capability(&self) -> Option<xmodel_runtime::RetainedModelCapability> {
|
||||
Some(xmodel_runtime::RetainedModelCapability {
|
||||
key: self.name.clone(),
|
||||
@@ -160,12 +213,74 @@ impl ModelSkel {
|
||||
|
||||
pub type FpvSkel = ModelSkel;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct VertSkin {
|
||||
pub bones: [u16; 4],
|
||||
pub weights: [f32; 4],
|
||||
pub bones: [u8; 4],
|
||||
extra_weights: [u16; 3],
|
||||
weight_flags: u8,
|
||||
}
|
||||
|
||||
pub weight_u16: [u16; 4],
|
||||
impl Default for VertSkin {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bones: [0; 4],
|
||||
extra_weights: [0; 3],
|
||||
weight_flags: 1 << 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VertSkin {
|
||||
pub(crate) fn rigid(bone: u8) -> Self {
|
||||
Self {
|
||||
bones: [bone, 0, 0, 0],
|
||||
weight_flags: 1 << 5,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_extra_weight(&self, index: usize) -> bool {
|
||||
self.weight_flags & (1 << index) != 0
|
||||
}
|
||||
|
||||
pub fn primary_is_zero(&self) -> bool {
|
||||
self.weight_flags & (1 << 4) != 0
|
||||
}
|
||||
|
||||
fn weight_is_nonzero(&self, index: usize) -> bool {
|
||||
if index == 0 {
|
||||
!self.primary_is_zero()
|
||||
} else {
|
||||
self.has_extra_weight(index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_only_primary_weight(&self) -> bool {
|
||||
self.weight_flags & 0x2e == 0x20
|
||||
}
|
||||
|
||||
pub(crate) fn is_rigid(&self) -> bool {
|
||||
self.weight_flags & (1 << 5) != 0 && !self.has_extra_weight(1)
|
||||
}
|
||||
|
||||
pub fn extra_weight(&self, index: usize) -> u16 {
|
||||
self.extra_weights[index - 1]
|
||||
}
|
||||
|
||||
pub(crate) fn set_extra_weight(&mut self, index: usize, raw: u16) {
|
||||
self.extra_weights[index - 1] = raw;
|
||||
}
|
||||
|
||||
pub(crate) fn set_weight(&mut self, index: usize, weight: f32) {
|
||||
if index == 0 {
|
||||
self.weight_flags = (self.weight_flags & 0x0f)
|
||||
| (u8::from(weight == 0.0) << 4)
|
||||
| (u8::from(weight == 1.0) << 5);
|
||||
} else {
|
||||
self.weight_flags =
|
||||
(self.weight_flags & !(1 << index)) | (u8::from(weight > 0.0) << index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -193,9 +308,38 @@ impl From<BoneBind> for TagViewBind {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SharedXModelSurfaces(
|
||||
pub std::collections::BTreeMap<String, (std::sync::Arc<ModelSkel>, usize)>,
|
||||
HashMap<u64, Vec<Weak<ModelSkel>>>,
|
||||
);
|
||||
|
||||
impl SharedXModelSurfaces {
|
||||
pub fn share_geometry(&mut self, mut skel: ModelSkel) -> Arc<ModelSkel> {
|
||||
let mut hash = DefaultHasher::new();
|
||||
skel.packed_vertices.as_ref().as_flattened().hash(&mut hash);
|
||||
let candidates = self.1.entry(hash.finish()).or_default();
|
||||
candidates.retain(|candidate| candidate.strong_count() != 0);
|
||||
for candidate in candidates.iter().filter_map(Weak::upgrade) {
|
||||
if skel.packed_vertices == candidate.packed_vertices
|
||||
&& skel.indices == candidate.indices
|
||||
&& skel.vert_skin == candidate.vert_skin
|
||||
&& skel.positions.len() == candidate.positions.len()
|
||||
&& skel
|
||||
.positions
|
||||
.iter()
|
||||
.zip(candidate.positions.iter())
|
||||
.all(|(a, b)| a.map(f32::to_bits) == b.map(f32::to_bits))
|
||||
{
|
||||
skel.packed_vertices = candidate.packed_vertices.clone();
|
||||
skel.positions = candidate.positions.clone();
|
||||
skel.indices = candidate.indices.clone();
|
||||
skel.vert_skin = candidate.vert_skin.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
let skel = Arc::new(skel);
|
||||
candidates.push(Arc::downgrade(&skel));
|
||||
skel
|
||||
}
|
||||
|
||||
pub fn retain(
|
||||
&mut self,
|
||||
stream: &ZoneStream<'_>,
|
||||
@@ -344,9 +488,6 @@ fn capture_model_skel_iw4(
|
||||
let bone_collision = capture_bone_collision(stream, geometry, geometry.num_bones);
|
||||
|
||||
let mut positions = Vec::new();
|
||||
let mut normals = Vec::new();
|
||||
let mut colors = Vec::new();
|
||||
let mut uvs = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
let mut surface_materials = Vec::new();
|
||||
let mut surface_vertex_ranges = Vec::new();
|
||||
@@ -382,8 +523,8 @@ fn capture_model_skel_iw4(
|
||||
let (v, vn) = source.surface_vertex_ranges[n];
|
||||
let (ix, ixn) = source.surface_index_ranges[n];
|
||||
if source.vert_skin[v..v + vn].iter().any(|skin| {
|
||||
skin.bones.iter().zip(skin.weights).any(|(bone, weight)| {
|
||||
weight != 0.0 && usize::from(*bone) >= geometry.num_bones
|
||||
skin.bones.iter().enumerate().any(|(index, bone)| {
|
||||
skin.weight_is_nonzero(index) && usize::from(*bone) >= geometry.num_bones
|
||||
})
|
||||
}) {
|
||||
return None;
|
||||
@@ -391,12 +532,10 @@ fn capture_model_skel_iw4(
|
||||
let base = positions.len();
|
||||
let index_start = indices.len();
|
||||
positions.extend_from_slice(&source.positions[v..v + vn]);
|
||||
normals.extend_from_slice(&source.normals[v..v + vn]);
|
||||
colors.extend_from_slice(&source.colors[v..v + vn]);
|
||||
uvs.extend_from_slice(&source.uvs[v..v + vn]);
|
||||
|
||||
packed_vertices.extend_from_slice(&source.packed_vertices[v..v + vn]);
|
||||
for skin in &source.vert_skin[v..v + vn] {
|
||||
if skin.weights[1] == 0.0 && skin.weights[0] == 1.0 {
|
||||
if skin.is_rigid() {
|
||||
rigid_verts += 1;
|
||||
} else {
|
||||
blend_verts += 1;
|
||||
@@ -465,13 +604,9 @@ fn capture_model_skel_iw4(
|
||||
stream.f32_at(vertex, 4).ok()?,
|
||||
stream.f32_at(vertex, 8).ok()?,
|
||||
]);
|
||||
colors.push(unpack_color(stream.u32_at(vertex, 16).ok()?));
|
||||
uvs.push(unpack_packed_tex_coords(stream.u32_at(vertex, 20).ok()?));
|
||||
normals.push(normalize_or_up(unpack_unit_vec(
|
||||
stream.u32_at(vertex, 24).ok()?,
|
||||
)));
|
||||
|
||||
let skin = surface_skins[vertex_index];
|
||||
if skin.weights[1] == 0.0 && skin.weights[0] == 1.0 {
|
||||
if skin.is_rigid() {
|
||||
rigid_verts += 1;
|
||||
} else {
|
||||
blend_verts += 1;
|
||||
@@ -515,21 +650,18 @@ fn capture_model_skel_iw4(
|
||||
tag_view,
|
||||
tag_weapon,
|
||||
pose,
|
||||
positions,
|
||||
normals,
|
||||
colors,
|
||||
uvs,
|
||||
indices,
|
||||
positions: positions.into(),
|
||||
indices: indices.into(),
|
||||
surface_materials,
|
||||
surface_vertex_ranges,
|
||||
surface_index_ranges,
|
||||
surface_part_bits,
|
||||
surface_deformed,
|
||||
surface_vert_list_count,
|
||||
vert_skin,
|
||||
vert_skin: vert_skin.into(),
|
||||
rigid_verts,
|
||||
blend_verts,
|
||||
packed_vertices,
|
||||
packed_vertices: packed_vertices.into(),
|
||||
radius: geometry.radius,
|
||||
bounds: match (geometry.bounds_mid, geometry.bounds_half) {
|
||||
(Some(mid), Some(half))
|
||||
@@ -772,12 +904,14 @@ fn decode_surface_skin(
|
||||
num_bones: usize,
|
||||
) -> Option<Vec<VertSkin>> {
|
||||
let mut skins = vec![VertSkin::default(); vertex_count];
|
||||
let bone_at = |raw: u16| -> Option<u16> {
|
||||
let bone_at = |raw: u16| -> Option<u8> {
|
||||
if raw % BONE_STRIDE != 0 {
|
||||
return None;
|
||||
}
|
||||
let bone = raw / BONE_STRIDE;
|
||||
((bone as usize) < num_bones).then_some(bone)
|
||||
((bone as usize) < num_bones)
|
||||
.then(|| u8::try_from(bone).ok())
|
||||
.flatten()
|
||||
};
|
||||
|
||||
if vert_list_count > 0 {
|
||||
@@ -793,11 +927,7 @@ fn decode_surface_skin(
|
||||
if vertex >= vertex_count {
|
||||
break;
|
||||
}
|
||||
skins[vertex] = VertSkin {
|
||||
bones: [bone, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
skins[vertex] = VertSkin::rigid(bone);
|
||||
vertex += 1;
|
||||
}
|
||||
}
|
||||
@@ -825,11 +955,7 @@ fn decode_surface_skin(
|
||||
);
|
||||
}
|
||||
for skin in &mut skins {
|
||||
*skin = VertSkin {
|
||||
bones: [0, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
*skin = VertSkin::rigid(0);
|
||||
}
|
||||
return Some(skins);
|
||||
}
|
||||
@@ -858,11 +984,11 @@ fn decode_surface_skin(
|
||||
let raw = stream.u16_at(blend_ptr, off * 2 + 2).ok()?;
|
||||
let w = dpvs_iw4::skin_blend_weight(raw);
|
||||
skin.bones[extra] = bone_at(b)?;
|
||||
skin.weights[extra] = w;
|
||||
skin.weight_u16[extra] = raw;
|
||||
skin.set_weight(extra, w);
|
||||
skin.set_extra_weight(extra, raw);
|
||||
remaining -= w;
|
||||
}
|
||||
skin.weights[0] = remaining;
|
||||
skin.set_weight(0, remaining);
|
||||
if vertex >= vertex_count {
|
||||
return None;
|
||||
}
|
||||
@@ -975,9 +1101,6 @@ fn capture_model_skel_t5(
|
||||
let pose = capture_pose_src_t5(stream, &geometry, &bone_name_strs, &bones, num_child);
|
||||
|
||||
let mut positions = Vec::new();
|
||||
let mut normals = Vec::new();
|
||||
let mut colors = Vec::new();
|
||||
let mut uvs = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
let mut surface_materials = Vec::with_capacity(geometry.surface_count);
|
||||
let mut surface_vertex_ranges = Vec::with_capacity(geometry.surface_count);
|
||||
@@ -1032,13 +1155,9 @@ fn capture_model_skel_t5(
|
||||
stream.f32_at(vertex, 4).ok()?,
|
||||
stream.f32_at(vertex, 8).ok()?,
|
||||
]);
|
||||
normals.push(normalize_or_up(unpack_unit_vec(
|
||||
stream.u32_at(vertex, 24).ok()?,
|
||||
)));
|
||||
colors.push(unpack_color(stream.u32_at(vertex, 16).ok()?));
|
||||
uvs.push(unpack_packed_tex_coords(stream.u32_at(vertex, 20).ok()?));
|
||||
|
||||
let skin = surface_skins[vertex_index];
|
||||
if skin.weights[1] == 0.0 && skin.weights[0] == 1.0 {
|
||||
if skin.is_rigid() {
|
||||
rigid_verts += 1;
|
||||
} else {
|
||||
blend_verts += 1;
|
||||
@@ -1075,21 +1194,18 @@ fn capture_model_skel_t5(
|
||||
tag_view,
|
||||
tag_weapon,
|
||||
pose,
|
||||
positions,
|
||||
normals,
|
||||
colors,
|
||||
uvs,
|
||||
indices,
|
||||
positions: positions.into(),
|
||||
indices: indices.into(),
|
||||
surface_materials,
|
||||
surface_vertex_ranges,
|
||||
surface_index_ranges,
|
||||
surface_part_bits,
|
||||
surface_deformed,
|
||||
surface_vert_list_count,
|
||||
vert_skin,
|
||||
vert_skin: vert_skin.into(),
|
||||
rigid_verts,
|
||||
blend_verts,
|
||||
packed_vertices,
|
||||
packed_vertices: packed_vertices.into(),
|
||||
radius: geometry.radius,
|
||||
bounds: None,
|
||||
contents: Some(geometry.contents),
|
||||
@@ -1355,12 +1471,14 @@ fn decode_surface_skin_t5(
|
||||
rigid_stride: usize,
|
||||
) -> Option<Vec<VertSkin>> {
|
||||
let mut skins = vec![VertSkin::default(); vertex_count];
|
||||
let bone_at = |raw: u16| -> Option<u16> {
|
||||
let bone_at = |raw: u16| -> Option<u8> {
|
||||
if raw % BONE_STRIDE != 0 {
|
||||
return None;
|
||||
}
|
||||
let bone = raw / BONE_STRIDE;
|
||||
((bone as usize) < num_bones).then_some(bone)
|
||||
((bone as usize) < num_bones)
|
||||
.then(|| u8::try_from(bone).ok())
|
||||
.flatten()
|
||||
};
|
||||
|
||||
if vert_list_count > 0 {
|
||||
@@ -1376,11 +1494,7 @@ fn decode_surface_skin_t5(
|
||||
if vertex >= vertex_count {
|
||||
break;
|
||||
}
|
||||
skins[vertex] = VertSkin {
|
||||
bones: [bone, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
skins[vertex] = VertSkin::rigid(bone);
|
||||
vertex += 1;
|
||||
}
|
||||
}
|
||||
@@ -1401,11 +1515,7 @@ fn decode_surface_skin_t5(
|
||||
fastfile_t5::ZonePtr::Offset(p) => stream.resolve_alias(p),
|
||||
fastfile_t5::ZonePtr::Null if counts.iter().all(|&c| c == 0) => {
|
||||
for skin in &mut skins {
|
||||
*skin = VertSkin {
|
||||
bones: [0, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
*skin = VertSkin::rigid(0);
|
||||
}
|
||||
return Some(skins);
|
||||
}
|
||||
@@ -1434,11 +1544,11 @@ fn decode_surface_skin_t5(
|
||||
let raw = stream.u16_at(blend_ptr, off * 2 + 2).ok()?;
|
||||
let w = dpvs_iw4::skin_blend_weight(raw);
|
||||
skin.bones[extra] = bone_at(b)?;
|
||||
skin.weights[extra] = w;
|
||||
skin.weight_u16[extra] = raw;
|
||||
skin.set_weight(extra, w);
|
||||
skin.set_extra_weight(extra, raw);
|
||||
remaining -= w;
|
||||
}
|
||||
skin.weights[0] = remaining;
|
||||
skin.set_weight(0, remaining);
|
||||
if vertex >= vertex_count {
|
||||
return None;
|
||||
}
|
||||
@@ -1582,10 +1692,9 @@ fn capture_model_skel_iw5(
|
||||
let (source, _) = shared?.0.get(surfaces_name)?;
|
||||
if source.surface_vertex_ranges.len() != geometry.surface_count
|
||||
|| source.vert_skin.iter().any(|skin| {
|
||||
skin.bones
|
||||
.iter()
|
||||
.zip(skin.weights)
|
||||
.any(|(bone, weight)| weight != 0.0 && usize::from(*bone) >= geometry.num_bones)
|
||||
skin.bones.iter().enumerate().any(|(index, bone)| {
|
||||
skin.weight_is_nonzero(index) && usize::from(*bone) >= geometry.num_bones
|
||||
})
|
||||
})
|
||||
{
|
||||
return None;
|
||||
@@ -1605,9 +1714,6 @@ fn capture_model_skel_iw5(
|
||||
};
|
||||
|
||||
let mut positions = Vec::new();
|
||||
let mut normals = Vec::new();
|
||||
let mut colors = Vec::new();
|
||||
let mut uvs = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
let mut packed_vertices = Vec::new();
|
||||
let mut surface_materials = Vec::with_capacity(geometry.surface_count);
|
||||
@@ -1673,13 +1779,9 @@ fn capture_model_skel_iw5(
|
||||
stream.f32_at(vertex, 8).ok()?,
|
||||
]);
|
||||
packed_vertices.push(copy_packed_vertex_iw5(stream, vertex)?);
|
||||
normals.push(normalize_or_up(unpack_unit_vec(
|
||||
stream.u32_at(vertex, 24).ok()?,
|
||||
)));
|
||||
colors.push(unpack_color(stream.u32_at(vertex, 16).ok()?));
|
||||
uvs.push(unpack_packed_tex_coords(stream.u32_at(vertex, 20).ok()?));
|
||||
|
||||
let skin = surface_skins[vertex_index];
|
||||
if skin.weights[1] == 0.0 && skin.weights[0] == 1.0 {
|
||||
if skin.is_rigid() {
|
||||
rigid_verts += 1;
|
||||
} else {
|
||||
blend_verts += 1;
|
||||
@@ -1706,21 +1808,18 @@ fn capture_model_skel_iw5(
|
||||
tag_view,
|
||||
tag_weapon,
|
||||
pose,
|
||||
positions,
|
||||
normals,
|
||||
colors,
|
||||
uvs,
|
||||
indices,
|
||||
positions: positions.into(),
|
||||
indices: indices.into(),
|
||||
surface_materials,
|
||||
surface_vertex_ranges,
|
||||
surface_index_ranges,
|
||||
surface_part_bits,
|
||||
surface_deformed,
|
||||
surface_vert_list_count,
|
||||
vert_skin,
|
||||
vert_skin: vert_skin.into(),
|
||||
rigid_verts,
|
||||
blend_verts,
|
||||
packed_vertices,
|
||||
packed_vertices: packed_vertices.into(),
|
||||
radius: geometry.radius,
|
||||
bounds: None,
|
||||
contents: None,
|
||||
@@ -1861,12 +1960,14 @@ fn decode_surface_skin_iw5(
|
||||
use fastfile_iw5::size as iw5sz;
|
||||
|
||||
let mut skins = vec![VertSkin::default(); vertex_count];
|
||||
let bone_at = |raw: u16| -> Option<u16> {
|
||||
let bone_at = |raw: u16| -> Option<u8> {
|
||||
if raw % BONE_STRIDE != 0 {
|
||||
return None;
|
||||
}
|
||||
let bone = raw / BONE_STRIDE;
|
||||
((bone as usize) < num_bones).then_some(bone)
|
||||
((bone as usize) < num_bones)
|
||||
.then(|| u8::try_from(bone).ok())
|
||||
.flatten()
|
||||
};
|
||||
|
||||
let mut rigid_assigned = 0usize;
|
||||
@@ -1885,11 +1986,7 @@ fn decode_surface_skin_iw5(
|
||||
if vertex >= vertex_count {
|
||||
break;
|
||||
}
|
||||
skins[vertex] = VertSkin {
|
||||
bones: [bone, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
skins[vertex] = VertSkin::rigid(bone);
|
||||
vertex += 1;
|
||||
}
|
||||
}
|
||||
@@ -1921,11 +2018,7 @@ fn decode_surface_skin_iw5(
|
||||
);
|
||||
}
|
||||
for skin in &mut skins {
|
||||
*skin = VertSkin {
|
||||
bones: [0, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
*skin = VertSkin::rigid(0);
|
||||
}
|
||||
return Some(skins);
|
||||
}
|
||||
@@ -1954,11 +2047,11 @@ fn decode_surface_skin_iw5(
|
||||
let raw = stream.u16_at(blend_ptr, off * 2 + 2).ok()?;
|
||||
let w = dpvs_iw4::skin_blend_weight(raw);
|
||||
skin.bones[extra] = bone_at(b)?;
|
||||
skin.weights[extra] = w;
|
||||
skin.weight_u16[extra] = raw;
|
||||
skin.set_weight(extra, w);
|
||||
skin.set_extra_weight(extra, raw);
|
||||
remaining -= w;
|
||||
}
|
||||
skin.weights[0] = remaining;
|
||||
skin.set_weight(0, remaining);
|
||||
if vertex >= vertex_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ use fastfile_t6::{Ptr, ZoneLoad};
|
||||
|
||||
use crate::dobj::ModelPoseSrc;
|
||||
use crate::model_skel::{BoneBind, ModelSkel, VertSkin};
|
||||
use crate::packed_vertex::{
|
||||
normalize_or_up, repack_vertex_t6, unpack_color, unpack_packed_tex_coords, unpack_unit_vec,
|
||||
};
|
||||
use crate::packed_vertex::repack_vertex_t6;
|
||||
use asset_core::WalkLocalMaterialIndex;
|
||||
pub use xmodel_runtime::BoneCollision;
|
||||
|
||||
@@ -174,9 +172,6 @@ pub fn capture_model_skel_t6(
|
||||
let pose = capture_pose_src(&r, &model, &name, &bone_name_strs, &bones, num_child);
|
||||
|
||||
let mut positions = Vec::new();
|
||||
let mut normals = Vec::new();
|
||||
let mut colors = Vec::new();
|
||||
let mut uvs = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
let mut surface_materials = Vec::with_capacity(surface_count);
|
||||
let mut surface_vertex_ranges = Vec::with_capacity(surface_count);
|
||||
@@ -217,13 +212,10 @@ pub fn capture_model_skel_t6(
|
||||
.try_into()
|
||||
.ok()?,
|
||||
);
|
||||
let word = |o: usize| u32::from_le_bytes(packed[o..o + 4].try_into().unwrap());
|
||||
packed_vertices.push(packed);
|
||||
positions.push([r.f32(vertex)?, r.f32(vertex.at(4))?, r.f32(vertex.at(8))?]);
|
||||
normals.push(normalize_or_up(unpack_unit_vec(word(24))));
|
||||
colors.push(unpack_color(word(16)));
|
||||
uvs.push(unpack_packed_tex_coords(word(20)));
|
||||
if skin.weights[1] == 0.0 && skin.weights[0] == 1.0 {
|
||||
|
||||
if skin.is_rigid() {
|
||||
rigid_verts += 1;
|
||||
} else {
|
||||
blend_verts += 1;
|
||||
@@ -260,21 +252,18 @@ pub fn capture_model_skel_t6(
|
||||
tag_view,
|
||||
tag_weapon,
|
||||
pose,
|
||||
positions,
|
||||
normals,
|
||||
colors,
|
||||
uvs,
|
||||
indices,
|
||||
positions: positions.into(),
|
||||
indices: indices.into(),
|
||||
surface_materials,
|
||||
surface_vertex_ranges,
|
||||
surface_index_ranges,
|
||||
surface_part_bits,
|
||||
surface_deformed,
|
||||
surface_vert_list_count,
|
||||
vert_skin,
|
||||
vert_skin: vert_skin.into(),
|
||||
rigid_verts,
|
||||
blend_verts,
|
||||
packed_vertices,
|
||||
packed_vertices: packed_vertices.into(),
|
||||
radius: Some(model.h_f32(off::RADIUS)),
|
||||
bounds: None,
|
||||
contents: Some(model.h_u32(off::CONTENTS)),
|
||||
@@ -355,18 +344,16 @@ fn decode_surface_skin(
|
||||
num_bones: usize,
|
||||
) -> Option<Vec<VertSkin>> {
|
||||
let mut skins = vec![VertSkin::default(); vertex_count];
|
||||
let bone_at = |raw: u16| -> Option<u16> {
|
||||
let bone_at = |raw: u16| -> Option<u8> {
|
||||
if !raw.is_multiple_of(BONE_STRIDE) {
|
||||
return None;
|
||||
}
|
||||
let bone = raw / BONE_STRIDE;
|
||||
(usize::from(bone) < num_bones).then_some(bone)
|
||||
};
|
||||
let rigid = |bone| VertSkin {
|
||||
bones: [bone, 0, 0, 0],
|
||||
weights: [1.0, 0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
(usize::from(bone) < num_bones)
|
||||
.then(|| u8::try_from(bone).ok())
|
||||
.flatten()
|
||||
};
|
||||
let rigid = |bone| VertSkin::rigid(bone);
|
||||
|
||||
if vert_list_count > 0
|
||||
&& let Some(Some(list)) = r.ptr(surface.at(40))
|
||||
@@ -420,10 +407,10 @@ fn decode_surface_skin(
|
||||
let o = start + 1 + (extra as u32 - 1) * 2;
|
||||
skin.bones[extra] = bone_at(r.u16(blend.at(o * 2))?)?;
|
||||
let w = f32::from(r.u16(blend.at(o * 2 + 2))?) * WEIGHT_SCALE;
|
||||
skin.weights[extra] = w;
|
||||
skin.set_weight(extra, w);
|
||||
remaining -= w;
|
||||
}
|
||||
skin.weights[0] = remaining;
|
||||
skin.set_weight(0, remaining);
|
||||
if vertex >= vertex_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -250,9 +250,6 @@ fn game_files(root: &Path) -> impl Iterator<Item = Result<PathBuf, String>> {
|
||||
files_under(search_roots(root))
|
||||
}
|
||||
|
||||
/// Folders the game never keeps zones in: cargo's output when the games root is the build's own
|
||||
/// `target/play` (tens of thousands of files, walked on every discovery), and our own
|
||||
/// artifacts.
|
||||
const NOT_GAME_DIRS: [&str; 6] = [
|
||||
"incremental",
|
||||
"deps",
|
||||
|
||||
@@ -1022,9 +1022,24 @@ pub fn model_mesh(skel: asset_model::ModelSkel) -> Result<crate::ModelMesh, Stri
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
skel.positions[first..end].to_vec(),
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, skel.normals[first..end].to_vec());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, skel.colors[first..end].to_vec());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, skel.uvs[first..end].to_vec());
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_NORMAL,
|
||||
skel.normals()
|
||||
.skip(first)
|
||||
.take(end - first)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_COLOR,
|
||||
skel.colors()
|
||||
.skip(first)
|
||||
.take(end - first)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_UV_0,
|
||||
skel.uvs().skip(first).take(end - first).collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_indices(Indices::U32(indices));
|
||||
surfaces.push(crate::model_mesh::ModelSurfaceDraw {
|
||||
mesh,
|
||||
|
||||
@@ -1469,7 +1469,7 @@ impl AssetLinkSink for CommonWalkSink {
|
||||
geometry,
|
||||
Some(&self.materials),
|
||||
)
|
||||
.map(std::sync::Arc::new);
|
||||
.map(|skel| self.shared_surfaces.share_geometry(skel));
|
||||
if let Some(skel) = &skel {
|
||||
self.shared_surfaces.retain(stream, geometry, skel.clone());
|
||||
self.fx_models.capture_shared(skel.clone(), &self.materials);
|
||||
|
||||
@@ -24,7 +24,7 @@ playerstate_iw4.workspace = true
|
||||
gamemode_iw4.workspace = true
|
||||
perf.workspace = true
|
||||
symphonia = { version = "0.5", default-features = false, features = ["mp3", "pcm", "wav", "adpcm", "flac"] }
|
||||
cpal = "=0.17.3"
|
||||
cpal = "=0.18.2"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -366,6 +366,7 @@ impl CueWork {
|
||||
}
|
||||
_ => (None, volume),
|
||||
};
|
||||
media.bus = self.request.namespace;
|
||||
let start = CueStart {
|
||||
looping: self.source.is_some()
|
||||
|| (matches!(intent.class, SoundClass::Music | SoundClass::Ambience)
|
||||
|
||||
@@ -92,7 +92,7 @@ pub(crate) fn supervise(shared: Arc<RenderShared>, shutdown: Arc<AtomicBool>) {
|
||||
pub(crate) fn open(
|
||||
shared: Arc<RenderShared>,
|
||||
failed: Arc<AtomicU8>,
|
||||
error_tx: SyncSender<cpal::StreamError>,
|
||||
error_tx: SyncSender<cpal::Error>,
|
||||
) -> Result<cpal::Stream, String> {
|
||||
let device = cpal::default_host()
|
||||
.default_output_device()
|
||||
@@ -157,8 +157,8 @@ fn build<T: SizedSample + FromSample<f32>>(
|
||||
config: &cpal::StreamConfig,
|
||||
shared: Arc<RenderShared>,
|
||||
failed: Arc<AtomicU8>,
|
||||
error_tx: SyncSender<cpal::StreamError>,
|
||||
) -> Result<cpal::Stream, cpal::BuildStreamError> {
|
||||
error_tx: SyncSender<cpal::Error>,
|
||||
) -> Result<cpal::Stream, cpal::Error> {
|
||||
let channels = usize::from(config.channels);
|
||||
let step = f64::from(SAMPLE_RATE) / f64::from(config.sample_rate);
|
||||
let mut block = [[0.0; 2]; QUANTUM];
|
||||
@@ -169,7 +169,7 @@ fn build<T: SizedSample + FromSample<f32>>(
|
||||
let mut primed = false;
|
||||
let errors = shared.clone();
|
||||
device.build_output_stream(
|
||||
config,
|
||||
config.clone(),
|
||||
move |output: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
shared.device_callbacks.fetch_add(1, Ordering::Release);
|
||||
let mut next = || {
|
||||
@@ -211,14 +211,21 @@ fn build<T: SizedSample + FromSample<f32>>(
|
||||
}
|
||||
frames.into_remainder().fill(T::EQUILIBRIUM);
|
||||
},
|
||||
move |error| match error {
|
||||
cpal::StreamError::BufferUnderrun => {
|
||||
move |error| match error.kind() {
|
||||
cpal::ErrorKind::Xrun => {
|
||||
errors.device_underruns.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
error => {
|
||||
let code = match &error {
|
||||
cpal::StreamError::DeviceNotAvailable => 1,
|
||||
cpal::StreamError::StreamInvalidated => 2,
|
||||
cpal::ErrorKind::DeviceChanged => {
|
||||
// The backend follows the default device and keeps this stream alive.
|
||||
diag::info!(Audio, "audio: output device changed: {error}");
|
||||
}
|
||||
cpal::ErrorKind::RealtimeDenied => {
|
||||
diag::warn!(Audio, "audio: realtime scheduling unavailable: {error}");
|
||||
}
|
||||
kind => {
|
||||
let code = match kind {
|
||||
cpal::ErrorKind::DeviceNotAvailable => 1,
|
||||
cpal::ErrorKind::StreamInvalidated => 2,
|
||||
_ => 3,
|
||||
};
|
||||
let _ = error_tx.try_send(error);
|
||||
|
||||
@@ -19,7 +19,7 @@ fn selected_alias<'a>(
|
||||
event: EntityEventKind,
|
||||
player_view: bool,
|
||||
knife: bool,
|
||||
) -> Option<(asset_core::AssetNamespace, &'a str)> {
|
||||
) -> Result<Option<(asset_core::AssetNamespace, &'a str)>, String> {
|
||||
let weapon = if matches!(
|
||||
event,
|
||||
EntityEventKind::MELEE_SWIPE | EntityEventKind::MELEE_HIT | EntityEventKind::MELEE_MISS
|
||||
@@ -74,25 +74,30 @@ fn selected_alias<'a>(
|
||||
WeaponSoundSlot::MeleeSwipePlayer,
|
||||
),
|
||||
EntityEventKind::MELEE_HIT => {
|
||||
return weapons.melee_impact_sound_key(
|
||||
weapon,
|
||||
knife,
|
||||
asset_game::MeleeImpact::Hit,
|
||||
bank,
|
||||
);
|
||||
return weapons
|
||||
.melee_impact_sound_key(weapon, knife, asset_game::MeleeImpact::Hit, bank)
|
||||
.map(Some)
|
||||
.ok_or_else(|| "melee hit sound is not in the sound bank".to_owned());
|
||||
}
|
||||
EntityEventKind::MELEE_MISS => {
|
||||
return weapons.melee_impact_sound_key(
|
||||
weapon,
|
||||
knife,
|
||||
asset_game::MeleeImpact::Miss,
|
||||
bank,
|
||||
);
|
||||
return weapons
|
||||
.melee_impact_sound_key(weapon, knife, asset_game::MeleeImpact::Miss, bank)
|
||||
.map(Some)
|
||||
.ok_or_else(|| "melee miss sound is not in the sound bank".to_owned());
|
||||
}
|
||||
_ => return None,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
weapons.weapon_sound_key(weapon, if player_view { player } else { world }, bank)
|
||||
let slot = if player_view { player } else { world };
|
||||
// Weapon sound slots are optional. An empty slot is authored silence;
|
||||
// sounds can also come from animation notetracks rather than this event.
|
||||
let Some(alias) = weapons.authored_weapon_sound(weapon, slot) else {
|
||||
return Ok(None);
|
||||
};
|
||||
weapons
|
||||
.weapon_sound_key(weapon, slot, bank)
|
||||
.map(Some)
|
||||
.ok_or_else(|| format!("{slot:?} alias `{alias}` is not in the sound bank"))
|
||||
}
|
||||
|
||||
fn entity_event_sound(
|
||||
@@ -190,39 +195,31 @@ fn entity_event_sound(
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Offhand view sounds may be authored entirely as animation notetracks.
|
||||
// A missing player alias must not fall back to the world throw sound.
|
||||
if event == EntityEventKind::USE_OFFHAND
|
||||
&& weapons
|
||||
.registry()
|
||||
.authored_weapon_sound(
|
||||
let selected = bank
|
||||
.as_deref()
|
||||
.ok_or_else(|| "sound bank is unavailable".to_owned())
|
||||
.and_then(|bank| {
|
||||
selected_alias(
|
||||
&weapons.registry(),
|
||||
&bank.0,
|
||||
weapon,
|
||||
if player_view {
|
||||
WeaponSoundSlot::FirePlayer
|
||||
} else {
|
||||
WeaponSoundSlot::Fire
|
||||
},
|
||||
event,
|
||||
player_view,
|
||||
sound.event.payload.event_parm == 1,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some((namespace, alias)) = bank.as_deref().and_then(|bank| {
|
||||
selected_alias(
|
||||
&weapons.registry(),
|
||||
&bank.0,
|
||||
weapon,
|
||||
event,
|
||||
player_view,
|
||||
sound.event.payload.event_parm == 1,
|
||||
)
|
||||
}) else {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: entity sound event {} has no authored alias",
|
||||
event.0
|
||||
);
|
||||
return;
|
||||
});
|
||||
let (namespace, alias) = match selected {
|
||||
Ok(Some(selected)) => selected,
|
||||
Ok(None) => return,
|
||||
Err(error) => {
|
||||
diag::warn!(
|
||||
Audio,
|
||||
"audio: entity sound event {} refused: weapon={} player_view={player_view}: {error}",
|
||||
event.0,
|
||||
weapons.registry().name_of(weapon)
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
output.write(WeaponSound {
|
||||
event: Some(crate::AudioEvent::from_entity(
|
||||
|
||||
@@ -67,6 +67,7 @@ pub use render_core::InstanceStatus;
|
||||
pub use runtime::{AudioDiagnostics, AudioRuntime};
|
||||
|
||||
mod media;
|
||||
mod mix_bus;
|
||||
|
||||
mod offline;
|
||||
pub use decode_budget::{DecodeMemory, decode_memory};
|
||||
|
||||
@@ -204,6 +204,7 @@ pub(crate) struct RenderMedia {
|
||||
pub gain: Option<LiveGain>,
|
||||
pub channel_gain: Option<LiveGain>,
|
||||
pub release: Option<Arc<CueRelease>>,
|
||||
pub bus: asset_core::AssetNamespace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -410,6 +411,7 @@ impl RenderMedia {
|
||||
gain: None,
|
||||
channel_gain: None,
|
||||
release: None,
|
||||
bus: asset_core::AssetNamespace::Iw4,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use asset_core::AssetNamespace;
|
||||
|
||||
use crate::render_core::{QUANTUM, SAMPLE_RATE};
|
||||
|
||||
const BUSES: usize = 4;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Compressor {
|
||||
threshold_db: f32,
|
||||
ratio: f32,
|
||||
makeup_db: f32,
|
||||
attack_s: f32,
|
||||
release_s: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct BusStage {
|
||||
trim_db: f32,
|
||||
compressor: Option<Compressor>,
|
||||
}
|
||||
|
||||
fn stage(namespace: AssetNamespace) -> BusStage {
|
||||
match namespace {
|
||||
AssetNamespace::Iw4 => BusStage {
|
||||
trim_db: 0.0,
|
||||
compressor: None,
|
||||
},
|
||||
AssetNamespace::Iw5 => BusStage {
|
||||
trim_db: -2.5,
|
||||
compressor: None,
|
||||
},
|
||||
AssetNamespace::T5 => BusStage {
|
||||
trim_db: 7.0,
|
||||
compressor: None,
|
||||
},
|
||||
AssetNamespace::T6 => BusStage {
|
||||
trim_db: 0.0,
|
||||
compressor: Some(Compressor {
|
||||
threshold_db: -40.0,
|
||||
ratio: 4.0,
|
||||
makeup_db: 29.5,
|
||||
attack_s: 0.002,
|
||||
release_s: 0.212,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn coefficient(seconds: f32) -> f32 {
|
||||
1.0 - (-1.0 / (seconds * SAMPLE_RATE as f32)).exp()
|
||||
}
|
||||
|
||||
fn db_to_gain(db: f32) -> f32 {
|
||||
10f32.powf(db / 20.0)
|
||||
}
|
||||
|
||||
pub(crate) struct MixBuses {
|
||||
buffers: [[[f32; 2]; QUANTUM]; BUSES],
|
||||
used: [bool; BUSES],
|
||||
envelope: [f32; BUSES],
|
||||
}
|
||||
|
||||
impl Default for MixBuses {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffers: [[[0.0; 2]; QUANTUM]; BUSES],
|
||||
used: [false; BUSES],
|
||||
envelope: [0.0; BUSES],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MixBuses {
|
||||
pub(crate) fn bus(&mut self, namespace: AssetNamespace) -> &mut [[f32; 2]; QUANTUM] {
|
||||
let index = namespace as usize;
|
||||
if !self.used[index] {
|
||||
self.used[index] = true;
|
||||
self.buffers[index] = [[0.0; 2]; QUANTUM];
|
||||
}
|
||||
&mut self.buffers[index]
|
||||
}
|
||||
|
||||
pub(crate) fn mix_into(&mut self, output: &mut [[f32; 2]; QUANTUM]) {
|
||||
for namespace in AssetNamespace::ALL {
|
||||
let index = namespace as usize;
|
||||
let stage = stage(namespace);
|
||||
let Some(compressor) = stage.compressor else {
|
||||
if self.used[index] {
|
||||
let trim = db_to_gain(stage.trim_db);
|
||||
for (target, frame) in output.iter_mut().zip(&self.buffers[index]) {
|
||||
target[0] += frame[0] * trim;
|
||||
target[1] += frame[1] * trim;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
};
|
||||
if !self.used[index] {
|
||||
if self.envelope[index] < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
self.buffers[index] = [[0.0; 2]; QUANTUM];
|
||||
}
|
||||
let attack = coefficient(compressor.attack_s);
|
||||
let release = coefficient(compressor.release_s);
|
||||
let slope = 1.0 - 1.0 / compressor.ratio;
|
||||
let mut envelope = self.envelope[index];
|
||||
for (target, frame) in output.iter_mut().zip(&self.buffers[index]) {
|
||||
let power = (frame[0] * frame[0] + frame[1] * frame[1]) * 0.5;
|
||||
let power = if power.is_finite() { power } else { 0.0 };
|
||||
envelope += (power - envelope) * if power > envelope { attack } else { release };
|
||||
let level_db = 10.0 * envelope.max(1e-12).log10();
|
||||
let over = (level_db - compressor.threshold_db).max(0.0);
|
||||
let gain = db_to_gain(stage.trim_db + compressor.makeup_db - over * slope);
|
||||
target[0] += frame[0] * gain;
|
||||
target[1] += frame[1] * gain;
|
||||
}
|
||||
self.envelope[index] = envelope;
|
||||
}
|
||||
self.used = [false; BUSES];
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MasterLimiter {
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl Default for MasterLimiter {
|
||||
fn default() -> Self {
|
||||
Self { gain: 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
const LIMITER_CEILING: f32 = 0.95;
|
||||
const LIMITER_RELEASE_S: f32 = 0.08;
|
||||
|
||||
impl MasterLimiter {
|
||||
pub(crate) fn apply(&mut self, output: &mut [[f32; 2]; QUANTUM]) {
|
||||
let release = coefficient(LIMITER_RELEASE_S);
|
||||
for frame in output {
|
||||
let peak = frame[0].abs().max(frame[1].abs());
|
||||
let target = if peak > LIMITER_CEILING {
|
||||
LIMITER_CEILING / peak
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
self.gain = if target < self.gain {
|
||||
target
|
||||
} else {
|
||||
self.gain + (target - self.gain) * release
|
||||
};
|
||||
frame[0] *= self.gain;
|
||||
frame[1] *= self.gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,15 +60,15 @@ impl Plugin for PlayerSoundPlugin {
|
||||
.scoped::<crate::ambient::SoundBankLoadAttempted>(frame::MatchScope::Loading)
|
||||
.scoped::<crate::ambient::ResidentSoundBank>(frame::MatchScope::Loading)
|
||||
.scoped::<crate::BobCycleTracker>(frame::MatchScope::Live)
|
||||
.scoped::<crate::shellshock::ShellshockSources>(frame::MatchScope::Live)
|
||||
.scoped::<crate::breath::BreathSources>(frame::MatchScope::Live)
|
||||
.scoped_message::<AliasCommand>(frame::MatchScope::Live)
|
||||
.scoped_message::<Footstep>(frame::MatchScope::Live)
|
||||
.scoped_message::<WeaponSound>(frame::MatchScope::Live)
|
||||
.scoped_message::<BoundWeaponSound>(frame::MatchScope::Live)
|
||||
.scoped_message::<ViewmodelNotetracks>(frame::MatchScope::Live)
|
||||
.scoped::<crate::shellshock::ShellshockSources>(frame::Timeline::Running)
|
||||
.scoped::<crate::breath::BreathSources>(frame::Timeline::Running)
|
||||
.scoped_message::<AliasCommand>(frame::Timeline::Running)
|
||||
.scoped_message::<Footstep>(frame::Timeline::Running)
|
||||
.scoped_message::<WeaponSound>(frame::Timeline::Running)
|
||||
.scoped_message::<BoundWeaponSound>(frame::Timeline::Running)
|
||||
.scoped_message::<ViewmodelNotetracks>(frame::Timeline::Running)
|
||||
.scoped::<crate::entity_events::NotetrackSoundTable>(frame::MatchScope::Live)
|
||||
.scoped_message::<LandSound>(frame::MatchScope::Live)
|
||||
.scoped_message::<LandSound>(frame::Timeline::Running)
|
||||
.add_systems(
|
||||
Update,
|
||||
play_weapon_sound_messages
|
||||
|
||||
@@ -144,6 +144,8 @@ struct Cursor {
|
||||
|
||||
struct RenderState {
|
||||
cursors: [Cursor; PHYSICAL_VOICES],
|
||||
buses: crate::mix_bus::MixBuses,
|
||||
limiter: crate::mix_bus::MasterLimiter,
|
||||
}
|
||||
|
||||
pub(crate) struct RenderShared {
|
||||
@@ -187,6 +189,8 @@ impl RenderShared {
|
||||
position: 0.0,
|
||||
gains: [crate::media::GainEnvelope::default(); 3],
|
||||
}; PHYSICAL_VOICES],
|
||||
buses: Default::default(),
|
||||
limiter: Default::default(),
|
||||
}),
|
||||
frame: AtomicU64::new(0),
|
||||
match_epoch: AtomicU64::new(0),
|
||||
@@ -241,7 +245,7 @@ impl RenderShared {
|
||||
epoch,
|
||||
cancelled,
|
||||
hold_one_shots,
|
||||
output,
|
||||
state.buses.bus(assignment.media.bus),
|
||||
);
|
||||
match device {
|
||||
Some(true) => {
|
||||
@@ -277,15 +281,16 @@ impl RenderShared {
|
||||
slot.phase.store(RETIRED, Ordering::Release);
|
||||
}
|
||||
}
|
||||
state.buses.mix_into(output);
|
||||
let master = f32::from_bits(self.master.load(Ordering::Relaxed));
|
||||
let mut peak = f32::from_bits(self.peak.load(Ordering::Relaxed));
|
||||
for sample in output.iter_mut().flatten() {
|
||||
let value = *sample * master;
|
||||
*sample = if value.is_finite() {
|
||||
value.clamp(-1.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
*sample = if value.is_finite() { value } else { 0.0 };
|
||||
}
|
||||
state.limiter.apply(output);
|
||||
let mut peak = f32::from_bits(self.peak.load(Ordering::Relaxed));
|
||||
for sample in output.iter_mut().flatten() {
|
||||
*sample = sample.clamp(-1.0, 1.0);
|
||||
peak = peak.max(sample.abs());
|
||||
}
|
||||
self.peak.store(peak.to_bits(), Ordering::Relaxed);
|
||||
|
||||
@@ -38,6 +38,7 @@ sim.workspace = true
|
||||
net.workspace = true
|
||||
ui.workspace = true
|
||||
replay.workspace = true
|
||||
theater.workspace = true
|
||||
session.workspace = true
|
||||
bots = { path = "../bots" }
|
||||
hud = { path = "../hud" }
|
||||
|
||||
@@ -87,7 +87,7 @@ fn install_class_catalog(
|
||||
mut commands: Commands,
|
||||
shell: Option<ResMut<ShellCommonTask>>,
|
||||
mut strings: ResMut<asset_game::LocalizeCatalog>,
|
||||
menus: Res<asset_game::MenuCatalog>,
|
||||
mut menus: ResMut<asset_game::MenuCatalog>,
|
||||
account: Option<Res<net::LocalAccount>>,
|
||||
mut player_data: ResMut<sim::LocalPlayerData>,
|
||||
) {
|
||||
@@ -148,6 +148,7 @@ fn install_class_catalog(
|
||||
("mp/rankicontable.csv", common.weapons.shared_rank_icons()),
|
||||
] {
|
||||
if let Ok(table) = table {
|
||||
menus.insert_string_table(table.clone());
|
||||
player_data.install_rank_table(
|
||||
name,
|
||||
sim::script::StringTable {
|
||||
@@ -191,6 +192,7 @@ fn install_class_catalog(
|
||||
strings.absorb(common.strings);
|
||||
commands.insert_resource(class_catalog);
|
||||
commands.remove_resource::<ShellCommonTask>();
|
||||
commands.remove_resource::<frame::FrontendContentLoading>();
|
||||
}
|
||||
|
||||
fn launch_identity(config: &LaunchConfig) -> LaunchIdentity {
|
||||
@@ -405,6 +407,9 @@ fn run_menu(
|
||||
artifacts,
|
||||
};
|
||||
let mut app = App::new();
|
||||
if std::env::var_os("IW4L_PRESENT_MODE").is_some() {
|
||||
app.insert_resource(ui::PresentModeOverride(launch_present_mode(false)));
|
||||
}
|
||||
let namespace_trees = NamespaceTrees::discover(&games);
|
||||
let have = content_flags(&namespace_trees);
|
||||
let master_intent = if have.0 & net::CONTENT_IW4 == 0 {
|
||||
@@ -435,6 +440,7 @@ fn run_menu(
|
||||
perk_table: menus.string_table("mp/perkTable.csv").cloned(),
|
||||
started: std::time::Instant::now(),
|
||||
})
|
||||
.insert_resource(frame::FrontendContentLoading)
|
||||
.add_systems(
|
||||
Update,
|
||||
(install_class_catalog, adopt_shell_ui_images)
|
||||
|
||||
@@ -44,6 +44,7 @@ pub fn add_runtime_plugins_with_role(app: &mut App, role: RuntimeRole) {
|
||||
.add_plugins((BotsPlugin, HudPlugin))
|
||||
.add_plugins(AudioPlugin)
|
||||
.add_plugins(ReplayPlugin)
|
||||
.add_plugins(theater::TheaterPlugin)
|
||||
.add_plugins(RenderPlugin)
|
||||
.add_plugins(SessionPlugin);
|
||||
|
||||
|
||||
@@ -26,3 +26,6 @@ bind TAB +scores
|
||||
bind 2 +left
|
||||
bind 3 +right
|
||||
bind 4 +actionslot 4
|
||||
|
||||
bind T chatmodepublic
|
||||
bind Y chatmodeteam
|
||||
|
||||
@@ -140,33 +140,34 @@ impl PadButton {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn prompt(self, style: frame::PromptStyle) -> &'static str {
|
||||
use frame::PromptStyle::*;
|
||||
match (style, self) {
|
||||
(Xbox, _) => self.label(),
|
||||
(PlayStation, Self::South) => "×",
|
||||
(PlayStation, Self::East) => "○",
|
||||
(PlayStation, Self::West) => "□",
|
||||
(PlayStation, Self::North) => "△",
|
||||
(PlayStation, Self::LeftBumper) => "L1",
|
||||
(PlayStation, Self::RightBumper) => "R1",
|
||||
(PlayStation, Self::LeftTrigger) => "L2",
|
||||
(PlayStation, Self::RightTrigger) => "R2",
|
||||
(PlayStation, Self::LeftStick) => "L3",
|
||||
(PlayStation, Self::RightStick) => "R3",
|
||||
(PlayStation, Self::Select) => "SHARE",
|
||||
(PlayStation, Self::Start) => "OPTIONS",
|
||||
(Generic, Self::South) => "SOUTH",
|
||||
(Generic, Self::East) => "EAST",
|
||||
(Generic, Self::West) => "WEST",
|
||||
(Generic, Self::North) => "NORTH",
|
||||
(Generic, Self::LeftBumper) => "L-BUMPER",
|
||||
(Generic, Self::RightBumper) => "R-BUMPER",
|
||||
(Generic, Self::LeftTrigger) => "L-TRIGGER",
|
||||
(Generic, Self::RightTrigger) => "R-TRIGGER",
|
||||
(Generic, Self::LeftStick) => "L-STICK",
|
||||
(Generic, Self::RightStick) => "R-STICK",
|
||||
(Generic, Self::Select) => "SELECT",
|
||||
pub const fn icon_prompt(self) -> &'static str {
|
||||
use frame::ControllerIcon;
|
||||
match self {
|
||||
Self::South => ControllerIcon::A.prompt(),
|
||||
Self::East => ControllerIcon::B.prompt(),
|
||||
Self::West => ControllerIcon::X.prompt(),
|
||||
Self::North => ControllerIcon::Y.prompt(),
|
||||
Self::DpadUp => ControllerIcon::DpadUp.prompt(),
|
||||
Self::DpadDown => ControllerIcon::DpadDown.prompt(),
|
||||
Self::DpadLeft => ControllerIcon::DpadLeft.prompt(),
|
||||
Self::DpadRight => ControllerIcon::DpadRight.prompt(),
|
||||
_ => self.label(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn generic_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::South => "SOUTH",
|
||||
Self::East => "EAST",
|
||||
Self::West => "WEST",
|
||||
Self::North => "NORTH",
|
||||
Self::LeftBumper => "L-BUMPER",
|
||||
Self::RightBumper => "R-BUMPER",
|
||||
Self::LeftTrigger => "L-TRIGGER",
|
||||
Self::RightTrigger => "R-TRIGGER",
|
||||
Self::LeftStick => "L-STICK",
|
||||
Self::RightStick => "R-STICK",
|
||||
Self::Select => "SELECT",
|
||||
_ => self.label(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ macro_rules! key_catalog {
|
||||
super::PadButton::ALL
|
||||
.into_iter()
|
||||
.find(|pad| pad.console_name() == upper || pad.label() == upper
|
||||
|| format!("BUTTON_{}", pad.prompt(frame::PromptStyle::Generic)) == upper)
|
||||
|| format!("BUTTON_{}", pad.generic_label()) == upper)
|
||||
.map(|pad| vec![BindButton::Pad(pad)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
use crate::{BindButton, ConsoleCommand, ConsoleSettings, ConsoleState, KeyBinds};
|
||||
use bevy::ecs::system::SystemParam;
|
||||
use bevy::{
|
||||
input::{ButtonState, keyboard::KeyboardInput},
|
||||
prelude::*,
|
||||
};
|
||||
use frame::ScopeApp;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct ChatState {
|
||||
pub open: bool,
|
||||
team_only: bool,
|
||||
line: String,
|
||||
caret: usize,
|
||||
captured: bool,
|
||||
error: Option<String>,
|
||||
recent: std::collections::VecDeque<(f64, String, String, i32)>,
|
||||
}
|
||||
|
||||
impl ChatState {
|
||||
pub fn captures_input(&self) -> bool {
|
||||
self.open || self.captured
|
||||
}
|
||||
fn begin(&mut self, team_only: bool) {
|
||||
self.open = true;
|
||||
self.team_only = team_only;
|
||||
self.line.clear();
|
||||
self.caret = 0;
|
||||
self.captured = true;
|
||||
self.error = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ChatText(usize);
|
||||
|
||||
#[derive(Component)]
|
||||
struct ChatBody(usize);
|
||||
|
||||
#[derive(Component)]
|
||||
struct ChatPrompt;
|
||||
|
||||
pub(crate) fn install(app: &mut App) {
|
||||
app.scoped::<ChatState>(frame::MatchScope::Live)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
Update,
|
||||
(commands, display).chain().in_set(net::ClientSet::Diag),
|
||||
);
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, font: Res<ui::GameUiFont>) {
|
||||
commands
|
||||
.spawn((
|
||||
ui::UiLayer::Hud,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: px(12),
|
||||
top: percent(25),
|
||||
width: px(300),
|
||||
flex_direction: FlexDirection::Column,
|
||||
..default()
|
||||
},
|
||||
GlobalZIndex(20),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
for index in 0..5 {
|
||||
parent
|
||||
.spawn((
|
||||
ChatText(index),
|
||||
Text::new(""),
|
||||
ui::game_text_font(&font.0, 12.0),
|
||||
TextColor(Color::WHITE),
|
||||
TextShadow {
|
||||
offset: Vec2::splat(1.0),
|
||||
color: Color::BLACK,
|
||||
},
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
ChatBody(index),
|
||||
TextSpan::new(""),
|
||||
ui::game_text_font(&font.0, 12.0),
|
||||
TextColor(Color::WHITE),
|
||||
));
|
||||
});
|
||||
}
|
||||
parent.spawn((
|
||||
ChatPrompt,
|
||||
Text::new(""),
|
||||
ui::game_text_font(&font.0, 12.0),
|
||||
TextColor(Color::WHITE),
|
||||
TextShadow {
|
||||
offset: Vec2::splat(1.0),
|
||||
color: Color::BLACK,
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(SystemParam)]
|
||||
pub(crate) struct ChatOutbox<'w> {
|
||||
local: Res<'w, net::LocalPresentClient>,
|
||||
inbox: ResMut<'w, net::ClientActionInbox>,
|
||||
ids: ResMut<'w, net::ActionRequestIds>,
|
||||
}
|
||||
|
||||
fn submit(state: &mut ChatState, outbox: &mut ChatOutbox) {
|
||||
if !state.line.trim().is_empty()
|
||||
&& let Err(error) = outbox.inbox.push(
|
||||
outbox.local.0,
|
||||
sim::ClientAction::Chat {
|
||||
request_id: outbox.ids.allocate(),
|
||||
team_only: state.team_only,
|
||||
text: sim::chat::payload(&state.line),
|
||||
},
|
||||
)
|
||||
{
|
||||
state.error = Some(error.to_string());
|
||||
return;
|
||||
}
|
||||
state.open = false;
|
||||
state.line.clear();
|
||||
state.caret = 0;
|
||||
}
|
||||
|
||||
pub(crate) fn keyboard(
|
||||
mut events: MessageReader<KeyboardInput>,
|
||||
state: Option<ResMut<ChatState>>,
|
||||
binds: Res<KeyBinds>,
|
||||
(console, menus, screen, playback): (
|
||||
Res<ConsoleState>,
|
||||
hud::MenuState<'_>,
|
||||
Res<frame::AppScreen>,
|
||||
Option<Res<replay::ReplayPlayback>>,
|
||||
),
|
||||
(keys, mouse, pads, active, mut wheel): (
|
||||
Res<ButtonInput<KeyCode>>,
|
||||
Res<ButtonInput<MouseButton>>,
|
||||
Query<&bevy::input::gamepad::Gamepad>,
|
||||
Res<frame::ActivePad>,
|
||||
MessageReader<bevy::input::mouse::MouseWheel>,
|
||||
),
|
||||
mut outbox: ChatOutbox,
|
||||
) {
|
||||
let Some(mut state) = state else {
|
||||
events.clear();
|
||||
return;
|
||||
};
|
||||
state.captured = false;
|
||||
let available = *screen == frame::AppScreen::InGame && playback.is_none();
|
||||
let modal = console.open || menus.is_some_and(|menus| menus.captures_input());
|
||||
if !available || modal {
|
||||
state.open = false;
|
||||
state.line.clear();
|
||||
events.clear();
|
||||
return;
|
||||
}
|
||||
let wheel_direction = wheel.read().find_map(|event| {
|
||||
if event.y > 0.0 {
|
||||
Some(BindButton::WheelUp)
|
||||
} else if event.y < 0.0 {
|
||||
Some(BindButton::WheelDown)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if !state.open {
|
||||
let inputs = crate::BindInputs::new(&keys, &mouse)
|
||||
.with_pad(active.0.and_then(|id| pads.get(id).ok()));
|
||||
for (button, id) in binds
|
||||
.iter()
|
||||
.filter(|(button, _)| !matches!(button, BindButton::Key(_)))
|
||||
{
|
||||
let team_only = match input_iw4::command_name(id) {
|
||||
Some("chatmodepublic") => false,
|
||||
Some("chatmodeteam") => true,
|
||||
_ => continue,
|
||||
};
|
||||
if inputs.just_pressed(button) || wheel_direction == Some(button) {
|
||||
state.begin(team_only);
|
||||
events.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for event in events.read().filter(|e| e.state == ButtonState::Pressed) {
|
||||
if !state.open {
|
||||
if !event.repeat {
|
||||
match binds
|
||||
.get(BindButton::Key(event.key_code))
|
||||
.and_then(input_iw4::command_name)
|
||||
{
|
||||
Some("chatmodepublic") => state.begin(false),
|
||||
Some("chatmodeteam") => state.begin(true),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
state.captured = true;
|
||||
match event.key_code {
|
||||
KeyCode::Escape | KeyCode::Backquote => {
|
||||
state.open = false;
|
||||
state.line.clear();
|
||||
}
|
||||
KeyCode::Enter | KeyCode::NumpadEnter => submit(&mut state, &mut outbox),
|
||||
KeyCode::ArrowLeft => {
|
||||
state.caret = state.line[..state.caret]
|
||||
.char_indices()
|
||||
.last()
|
||||
.map_or(0, |(i, _)| i);
|
||||
}
|
||||
KeyCode::ArrowRight => {
|
||||
state.caret += state.line[state.caret..]
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(0, char::len_utf8);
|
||||
}
|
||||
KeyCode::Home => state.caret = 0,
|
||||
KeyCode::End => state.caret = state.line.len(),
|
||||
KeyCode::Backspace if state.caret > 0 => {
|
||||
let end = state.caret;
|
||||
let start = state.line[..end].char_indices().last().unwrap().0;
|
||||
state.line.replace_range(start..end, "");
|
||||
state.caret = start;
|
||||
}
|
||||
KeyCode::Delete if state.caret < state.line.len() => {
|
||||
let start = state.caret;
|
||||
let end = start + state.line[start..].chars().next().unwrap().len_utf8();
|
||||
state.line.replace_range(start..end, "");
|
||||
}
|
||||
_ => {
|
||||
if let Some(text) = &event.text {
|
||||
for ch in text.chars().filter(|c| !c.is_control()) {
|
||||
if state.line.len() + ch.len_utf8() < sim::chat::CHAT_BYTES {
|
||||
let caret = state.caret;
|
||||
state.line.insert(caret, ch);
|
||||
state.caret += ch.len_utf8();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn commands(
|
||||
mut events: MessageReader<ConsoleCommand>,
|
||||
state: Option<ResMut<ChatState>>,
|
||||
(screen, playback): (Res<frame::AppScreen>, Option<Res<replay::ReplayPlayback>>),
|
||||
mut outbox: ChatOutbox,
|
||||
mut console: ResMut<ConsoleState>,
|
||||
settings: Res<ConsoleSettings>,
|
||||
) {
|
||||
let Some(mut state) = state else {
|
||||
for cmd in events.read().filter(|c| {
|
||||
matches!(
|
||||
c.name.as_str(),
|
||||
"chatmodepublic" | "chatmodeteam" | "say" | "say_team"
|
||||
)
|
||||
}) {
|
||||
console.echo(
|
||||
format!("{}: join a match first", cmd.name),
|
||||
settings.log_capacity,
|
||||
);
|
||||
}
|
||||
return;
|
||||
};
|
||||
for cmd in events.read().filter(|c| {
|
||||
matches!(
|
||||
c.name.as_str(),
|
||||
"chatmodepublic" | "chatmodeteam" | "say" | "say_team"
|
||||
)
|
||||
}) {
|
||||
if *screen != frame::AppScreen::InGame || playback.is_some() {
|
||||
console.echo("chat: requires a live match", settings.log_capacity);
|
||||
continue;
|
||||
}
|
||||
let team = matches!(cmd.name.as_str(), "chatmodeteam" | "say_team");
|
||||
state.begin(team);
|
||||
if matches!(cmd.name.as_str(), "say" | "say_team") {
|
||||
state.line = sim::chat::clean(&cmd.args.join(" "));
|
||||
submit(&mut state, &mut outbox);
|
||||
} else {
|
||||
console.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display(
|
||||
mut messages: MessageReader<net::SvcGameNotify>,
|
||||
state: Option<ResMut<ChatState>>,
|
||||
time: Res<Time<Real>>,
|
||||
(screen, local, presented, strings): (
|
||||
Res<frame::AppScreen>,
|
||||
Res<net::LocalPresentClient>,
|
||||
Res<net::PresentedSnapshot>,
|
||||
Option<Res<assets::PreparedLocalizedStrings>>,
|
||||
),
|
||||
mut names: Query<(&ChatText, &mut Text, &mut TextColor), Without<ChatPrompt>>,
|
||||
mut bodies: Query<(&ChatBody, &mut TextSpan)>,
|
||||
mut prompt: Query<&mut Text, (With<ChatPrompt>, Without<ChatText>)>,
|
||||
) {
|
||||
let Some(mut state) = state else {
|
||||
messages.clear();
|
||||
for (_, mut text, _) in &mut names {
|
||||
if !text.0.is_empty() {
|
||||
text.0.clear();
|
||||
}
|
||||
}
|
||||
for (_, mut body) in &mut bodies {
|
||||
if !body.0.is_empty() {
|
||||
body.0.clear();
|
||||
}
|
||||
}
|
||||
if let Ok(mut text) = prompt.single_mut()
|
||||
&& !text.0.is_empty()
|
||||
{
|
||||
text.0.clear();
|
||||
}
|
||||
return;
|
||||
};
|
||||
let now = time.elapsed_secs_f64();
|
||||
for msg in messages.read().filter(|m| m.tag == net::SVC_CHAT) {
|
||||
let (name, body) = msg
|
||||
.name
|
||||
.split_once(sim::HUD_PRINT_ARG_SEPARATOR)
|
||||
.unwrap_or(("", &msg.name));
|
||||
let (team, localized) = msg
|
||||
.key
|
||||
.split_once(':')
|
||||
.map_or((msg.key.as_str(), false), |(team, kind)| {
|
||||
(team, kind == "localized")
|
||||
});
|
||||
let body = if localized {
|
||||
let key = body.strip_prefix(": ").unwrap_or(body);
|
||||
format!(
|
||||
": {}",
|
||||
strings.as_ref().and_then(|s| s.0.text(key)).unwrap_or(key)
|
||||
)
|
||||
} else {
|
||||
body.to_owned()
|
||||
};
|
||||
state
|
||||
.recent
|
||||
.push_back((now, name.to_owned(), body, team.parse().unwrap_or(0)));
|
||||
while state.recent.len() > 5 {
|
||||
state.recent.pop_front();
|
||||
}
|
||||
}
|
||||
while state.recent.front().is_some_and(|(at, ..)| now - at > 8.0) {
|
||||
state.recent.pop_front();
|
||||
}
|
||||
let in_game = *screen == frame::AppScreen::InGame;
|
||||
let local_team = presented
|
||||
.snapshot()
|
||||
.and_then(|s| s.meta.for_client(local.0))
|
||||
.map_or(0, |m| m.client_state_team);
|
||||
for (index, mut text, mut color) in &mut names {
|
||||
let row = state.recent.get(index.0).filter(|_| in_game);
|
||||
let name = row.map_or("", |(_, name, ..)| name.as_str());
|
||||
if text.0 != name {
|
||||
text.0 = name.to_owned();
|
||||
}
|
||||
let rgba = row.map_or([1.0; 4], |(_, _, _, team)| {
|
||||
presented.snapshot().map_or([1.0; 4], |s| {
|
||||
hud::team_name_color(local_team, *team, s.meta.script_dvars(local.0))
|
||||
})
|
||||
});
|
||||
let next = Color::srgba(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
if color.0 != next {
|
||||
color.0 = next;
|
||||
}
|
||||
}
|
||||
for (index, mut body) in &mut bodies {
|
||||
let next = state
|
||||
.recent
|
||||
.get(index.0)
|
||||
.filter(|_| in_game)
|
||||
.map_or("", |(_, _, body, _)| body.as_str());
|
||||
if body.0 != next {
|
||||
body.0 = next.to_owned();
|
||||
}
|
||||
}
|
||||
let Ok(mut text) = prompt.single_mut() else {
|
||||
return;
|
||||
};
|
||||
let next = if in_game && state.open {
|
||||
let label = if state.team_only { "Team" } else { "Say" };
|
||||
format!(
|
||||
"{label}: {}|{}{}",
|
||||
&state.line[..state.caret],
|
||||
&state.line[state.caret..],
|
||||
state
|
||||
.error
|
||||
.as_ref()
|
||||
.map_or(String::new(), |e| format!("\n{e}"))
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if text.0 != next {
|
||||
text.0 = next;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ fn finish_replay_playback(
|
||||
let Some(playback) = playback.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if !playback.ended || playback.exit_queued {
|
||||
if !playback.ended || playback.exit_queued || playback.retain_at_end {
|
||||
return;
|
||||
}
|
||||
playback.exit_queued = true;
|
||||
|
||||
@@ -214,6 +214,10 @@ pub fn register_feature_commands(registry: &mut crate::ConsoleRegistry, maps: &[
|
||||
}
|
||||
for (name, usage) in [
|
||||
("help", "help — list registered commands"),
|
||||
("say", "say <message> — public chat"),
|
||||
("say_team", "say_team <message> — team chat"),
|
||||
("chatmodepublic", "chatmodepublic — open public chat"),
|
||||
("chatmodeteam", "chatmodeteam — open team chat"),
|
||||
("clear", "clear — clear console scrollback"),
|
||||
("screenshot", "screenshot [name] — capture the backbuffer"),
|
||||
(
|
||||
|
||||
@@ -8,8 +8,7 @@ use ::replay::{
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use frame::{LaunchIdentity, RuntimeRole};
|
||||
use net::{AuthorityClock, AuthorityInputGate, AuthorityWorld, PresentedSnapshot};
|
||||
use render_frontend::prepare::scene::camera::SimCamera;
|
||||
use net::{AuthorityClock, AuthorityWorld, PresentedSnapshot};
|
||||
|
||||
use crate::{ConsoleCommand, ConsoleDispatch, ConsoleLine, ConsoleSettings, ConsoleState};
|
||||
|
||||
@@ -31,11 +30,7 @@ pub(crate) fn route_replay_commands(
|
||||
Option<Res<audio::ClipStore>>,
|
||||
Option<Res<audio::AudioRuntime>>,
|
||||
),
|
||||
replay_inputs: (
|
||||
Option<Res<AuthorityWorld>>,
|
||||
Res<SimCamera>,
|
||||
Res<AuthorityInputGate>,
|
||||
),
|
||||
authority: Option<Res<AuthorityWorld>>,
|
||||
mut lifecycle: (
|
||||
ResMut<::replay::PendingReplayArm>,
|
||||
ResMut<::session::SessionSwapRequest>,
|
||||
@@ -56,7 +51,6 @@ pub(crate) fn route_replay_commands(
|
||||
line.0 = msg.clone();
|
||||
console.echo(msg, capacity);
|
||||
};
|
||||
let (authority, sim_cam, input_gate) = replay_inputs;
|
||||
let (theater, map_transition, dispatch) = &mut lifecycle;
|
||||
let (ring, role, prediction, clip_clock, clip_presented) = clip;
|
||||
|
||||
@@ -104,12 +98,17 @@ pub(crate) fn route_replay_commands(
|
||||
};
|
||||
theater.playback = Some(playback);
|
||||
theater.quit_on_end = false;
|
||||
theater.in_place = false;
|
||||
match map_transition.request_demo(name.clone(), zone.clone(), false) {
|
||||
Ok(id) => echo(
|
||||
format!("demo: `{name}` zone `{zone}` (swap #{id})"),
|
||||
console,
|
||||
line,
|
||||
),
|
||||
Ok(id) => {
|
||||
theater.request_id = Some(id);
|
||||
theater.control = None;
|
||||
echo(
|
||||
format!("demo: `{name}` zone `{zone}` (swap #{id})"),
|
||||
console,
|
||||
line,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
theater.playback = None;
|
||||
dispatch.release();
|
||||
@@ -146,12 +145,10 @@ pub(crate) fn route_replay_commands(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if !sim_cam.enabled || !input_gate.local_cmds_enabled {
|
||||
if *role == RuntimeRole::Replay || ring.as_ref().is_none_or(|ring| ring.is_empty())
|
||||
{
|
||||
echo(
|
||||
"record: the simulation does not own the camera yet \
|
||||
(no clip brushes, or the authored intermission view is active); \
|
||||
there are no ticks to record"
|
||||
.into(),
|
||||
"record: no live match ticks to record".into(),
|
||||
console,
|
||||
line,
|
||||
);
|
||||
@@ -165,9 +162,13 @@ pub(crate) fn route_replay_commands(
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Some(authority) = authority.as_ref() else {
|
||||
let Some(world) = authority
|
||||
.as_ref()
|
||||
.map(|authority| &authority.0)
|
||||
.or_else(|| prediction.as_ref().map(|prediction| prediction.0.world()))
|
||||
else {
|
||||
echo(
|
||||
"record: authority world missing (match identity unavailable)".into(),
|
||||
"record: match world missing (match identity unavailable)".into(),
|
||||
console,
|
||||
line,
|
||||
);
|
||||
@@ -175,7 +176,7 @@ pub(crate) fn route_replay_commands(
|
||||
};
|
||||
let requested = cmd.args.first().map(String::as_str).unwrap_or("");
|
||||
let record_identity =
|
||||
MatchRecordIdentity::from_world_on_zone(&authority.0, &identity.zone);
|
||||
MatchRecordIdentity::from_world_on_zone(world, &identity.zone);
|
||||
match Recording::start_with_identity(
|
||||
&identity.artifacts,
|
||||
requested,
|
||||
@@ -237,6 +238,7 @@ pub(crate) fn route_replay_commands(
|
||||
);
|
||||
match save_clip_package(
|
||||
identity,
|
||||
*role,
|
||||
authority.as_deref().filter(|_| role.runs_authority()),
|
||||
prediction.0.world(),
|
||||
ring.as_ref(),
|
||||
@@ -307,6 +309,7 @@ fn allocate_clip_dir(artifacts: &Path) -> Result<(String, PathBuf), String> {
|
||||
|
||||
fn save_clip_package(
|
||||
identity: &LaunchIdentity,
|
||||
role: RuntimeRole,
|
||||
world: Option<&AuthorityWorld>,
|
||||
prediction: &sim::SimWorld,
|
||||
ring: &ClipRing,
|
||||
@@ -348,7 +351,7 @@ fn save_clip_package(
|
||||
&identity.zone,
|
||||
captured_unix_ns,
|
||||
);
|
||||
let source = if world.is_some() {
|
||||
let source = if role == RuntimeRole::Dedicated {
|
||||
"authority"
|
||||
} else {
|
||||
"received"
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{CommandSpec, ConsoleCommand, ConsoleRegistry};
|
||||
|
||||
const PAGE_SIZE: usize = 10;
|
||||
const PACK_SLOTS: usize = 16;
|
||||
const LOBBY_REOPEN_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct FrontendState {
|
||||
@@ -24,7 +23,6 @@ pub(crate) struct FrontendState {
|
||||
password_joining: bool,
|
||||
lobby_password: String,
|
||||
rules_seeded: bool,
|
||||
reopen_lobby_until: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
#[derive(SystemParam)]
|
||||
@@ -145,10 +143,9 @@ pub(crate) fn route(
|
||||
&& !state.public
|
||||
&& services.menus.is_some()
|
||||
{
|
||||
state.reopen_lobby_until = Some(std::time::Instant::now() + LOBBY_REOPEN_WAIT);
|
||||
menus.write(UiMenuRequest::RestoreLobby);
|
||||
dvars.set("ui_frontend_status", "");
|
||||
} else {
|
||||
state.reopen_lobby_until = None;
|
||||
*party = UiPartyState::default();
|
||||
state.public = false;
|
||||
}
|
||||
@@ -168,35 +165,11 @@ pub(crate) fn route(
|
||||
dvars.set("ui_frontend_status", reason);
|
||||
} else if returned_in_menu && party.in_lobby && party.is_host {
|
||||
if !left_session && services.menus.is_some() {
|
||||
state.reopen_lobby_until = Some(std::time::Instant::now() + LOBBY_REOPEN_WAIT);
|
||||
menus.write(UiMenuRequest::RestoreLobby);
|
||||
} else {
|
||||
*party = UiPartyState::default();
|
||||
}
|
||||
}
|
||||
// The return resets the menu stack: the lobby opened before the main menu would be dropped.
|
||||
if let Some(deadline) = state.reopen_lobby_until {
|
||||
let open = services
|
||||
.menus
|
||||
.as_ref()
|
||||
.map(|script_menus| script_menus.open_names());
|
||||
let is_open = |name: &str| {
|
||||
open.iter()
|
||||
.flatten()
|
||||
.any(|menu| menu.eq_ignore_ascii_case(name))
|
||||
};
|
||||
if !party.in_lobby {
|
||||
state.reopen_lobby_until = None;
|
||||
} else if is_open("iw4l_main") {
|
||||
if !is_open("game_lobby") {
|
||||
menus.write(UiMenuRequest::Open("game_lobby".into()));
|
||||
diag::info!(Ui, "frontend: private lobby reopened");
|
||||
}
|
||||
state.reopen_lobby_until = None;
|
||||
} else if std::time::Instant::now() >= deadline {
|
||||
state.reopen_lobby_until = None;
|
||||
*party = UiPartyState::default();
|
||||
}
|
||||
}
|
||||
if dvars.get("ui_mapname").is_none()
|
||||
&& let Some(map) = maps.maps().find(|map| map.starts_with("iw4:"))
|
||||
{
|
||||
@@ -239,7 +212,6 @@ pub(crate) fn route(
|
||||
}
|
||||
"ui_create_lobby" => {
|
||||
selected_game(&dvars, &maps)?;
|
||||
state.reopen_lobby_until = None;
|
||||
state.lobby_password.clear();
|
||||
party.active = true;
|
||||
party.in_lobby = true;
|
||||
@@ -283,7 +255,6 @@ pub(crate) fn route(
|
||||
if !party.in_lobby || !party.is_host {
|
||||
return Err("Only the lobby host can start a match".into());
|
||||
}
|
||||
state.reopen_lobby_until = None;
|
||||
let (map, mode) = selected_game(&dvars, &maps)?;
|
||||
commands.insert_resource(host_rules(&dvars));
|
||||
if state.public {
|
||||
@@ -589,6 +560,7 @@ pub(crate) fn route(
|
||||
}
|
||||
}
|
||||
let mut lobby_names = vec![settings.player_name.clone()];
|
||||
let mut local_lobby_row = Some(0);
|
||||
if state.public
|
||||
&& let Some(bridge) = services.bridge.as_ref()
|
||||
{
|
||||
@@ -597,6 +569,7 @@ pub(crate) fn route(
|
||||
map,
|
||||
mode,
|
||||
members,
|
||||
identity,
|
||||
member_names,
|
||||
max_players,
|
||||
..
|
||||
@@ -605,10 +578,12 @@ pub(crate) fn route(
|
||||
map,
|
||||
mode,
|
||||
members,
|
||||
identity,
|
||||
member_names,
|
||||
max_players,
|
||||
..
|
||||
} => {
|
||||
local_lobby_row = members.iter().position(|id| *id == identity.member_id);
|
||||
lobby_names = members
|
||||
.iter()
|
||||
.map(|id| {
|
||||
@@ -645,6 +620,14 @@ pub(crate) fn route(
|
||||
&format!("ui_lobby_member_{row}"),
|
||||
name.map(String::as_str).unwrap_or(""),
|
||||
);
|
||||
dvars.set(
|
||||
&format!("ui_lobby_member_{row}_local"),
|
||||
if local_lobby_row == Some(row) {
|
||||
"1"
|
||||
} else {
|
||||
"0"
|
||||
},
|
||||
);
|
||||
dvars.set(
|
||||
&format!("ui_lobby_member_{row}_visible"),
|
||||
if name.is_some() { "1" } else { "0" },
|
||||
|
||||
@@ -74,7 +74,7 @@ pub(crate) struct PadActivity {
|
||||
}
|
||||
|
||||
pub(crate) fn track_active_pad(
|
||||
gamepads: Query<(Entity, &Gamepad, Option<&Name>)>,
|
||||
gamepads: Query<&Gamepad>,
|
||||
mut events: MessageReader<GamepadEvent>,
|
||||
mut active: ResMut<frame::ActivePad>,
|
||||
mut devices: ResMut<frame::InputDevices>,
|
||||
@@ -104,7 +104,7 @@ pub(crate) fn track_active_pad(
|
||||
}
|
||||
}
|
||||
if matches!(event.connection, GamepadConnection::Connected { .. })
|
||||
&& let Ok((_, pad, _)) = gamepads.get(event.gamepad)
|
||||
&& let Ok(pad) = gamepads.get(event.gamepad)
|
||||
{
|
||||
for axis in [
|
||||
GamepadAxis::LeftStickX,
|
||||
@@ -148,31 +148,6 @@ pub(crate) fn track_active_pad(
|
||||
if active.0.is_none() {
|
||||
devices.pad_prompts = false;
|
||||
}
|
||||
devices.style = match settings.pad_prompts {
|
||||
1 => frame::PromptStyle::Xbox,
|
||||
2 => frame::PromptStyle::PlayStation,
|
||||
3 => frame::PromptStyle::Generic,
|
||||
_ => active
|
||||
.0
|
||||
.and_then(|entity| gamepads.get(entity).ok())
|
||||
.map_or(frame::PromptStyle::Generic, |(_, pad, name)| {
|
||||
let name = name.map_or("", |name| name.as_str()).to_ascii_lowercase();
|
||||
if pad.vendor_id() == Some(0x054c)
|
||||
|| name.contains("dualshock")
|
||||
|| name.contains("dualsense")
|
||||
|| name == "wireless controller"
|
||||
{
|
||||
frame::PromptStyle::PlayStation
|
||||
} else if pad.vendor_id() == Some(0x045e)
|
||||
|| name.contains("xbox")
|
||||
|| name.contains("xinput")
|
||||
{
|
||||
frame::PromptStyle::Xbox
|
||||
} else {
|
||||
frame::PromptStyle::Generic
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const REPEAT_DELAY: f32 = 0.4;
|
||||
@@ -181,6 +156,7 @@ const REPEAT_EVERY: f32 = 0.12;
|
||||
pub(crate) fn drive_menus_with_pad(
|
||||
gamepads: Query<&Gamepad>,
|
||||
active: Res<frame::ActivePad>,
|
||||
overlay: Res<frame::UiOverlayInput>,
|
||||
script_menus: hud::MenuState<'_>,
|
||||
(devices, console, capture): (
|
||||
Res<frame::InputDevices>,
|
||||
@@ -191,7 +167,7 @@ pub(crate) fn drive_menus_with_pad(
|
||||
mut requests: MessageWriter<UiMenuRequest>,
|
||||
mut repeat: Local<Option<(UiMenuKey, f32)>>,
|
||||
) {
|
||||
if !devices.focused || console.open {
|
||||
if !devices.focused || console.open || overlay.0 {
|
||||
*repeat = None;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod barracks_menu;
|
||||
pub mod binds;
|
||||
mod chat;
|
||||
mod class_dispatch;
|
||||
mod class_menu;
|
||||
mod command;
|
||||
|
||||
@@ -17,7 +17,6 @@ use bevy::{
|
||||
window::{CursorEntered, CursorGrabMode, CursorOptions, PrimaryWindow, WindowFocused},
|
||||
};
|
||||
use frame::AppScreen;
|
||||
use frame::ScopeApp;
|
||||
use input_iw4::{SCRIPT_KEYNUM, command_names, input_cmd, key_event, key_up_command_id};
|
||||
use net::{ClientActionInput, ClientSet, PresentedSnapshot, frame_time_msec, key_frame_msec};
|
||||
use render_frontend::prepare::scene::world::WorldScene;
|
||||
@@ -84,6 +83,7 @@ pub struct ConsolePlugin;
|
||||
impl Plugin for ConsolePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
crate::startup::install_stdin(app);
|
||||
crate::chat::install(app);
|
||||
app.init_resource::<ConsoleSettings>()
|
||||
.init_resource::<ConsoleState>()
|
||||
.init_resource::<frame::HudInputView>()
|
||||
@@ -94,7 +94,11 @@ impl Plugin for ConsolePlugin {
|
||||
.init_resource::<ConsoleRegistry>()
|
||||
.init_resource::<crate::ConsoleQueue>()
|
||||
.init_resource::<crate::ConsoleLine>()
|
||||
.published::<crate::weapon_dispatch::WeaponArgCompletions>(frame::MatchScope::Live)
|
||||
.init_resource::<crate::weapon_dispatch::CompletionSlot>()
|
||||
.add_systems(
|
||||
OnExit(frame::MatchScope::Live),
|
||||
crate::weapon_dispatch::clear_weapon_arg_completions,
|
||||
)
|
||||
.init_resource::<crate::user_settings::PendingMenuBinding>()
|
||||
.init_resource::<crate::user_settings::UserSettingsPersistence>()
|
||||
.init_resource::<crate::game_folders::FolderPicks>()
|
||||
@@ -118,6 +122,8 @@ impl Plugin for ConsolePlugin {
|
||||
.add_systems(PreUpdate, feed_console_keyboard.before(InputSystems))
|
||||
.init_resource::<frame::ActivePad>()
|
||||
.init_resource::<frame::InputDevices>()
|
||||
.init_resource::<frame::UiPointerFree>()
|
||||
.init_resource::<frame::UiOverlayInput>()
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
@@ -138,10 +144,12 @@ impl Plugin for ConsolePlugin {
|
||||
PreUpdate,
|
||||
(
|
||||
handle_console_input,
|
||||
crate::chat::keyboard,
|
||||
handle_scrollback_pointer,
|
||||
copy_console_selection_on_release,
|
||||
isolate_gameplay_input,
|
||||
expire_pressed_inputs,
|
||||
publish_ui_input_capture,
|
||||
publish_client_action_input.in_set(frame::InMatch),
|
||||
sync_cursor_grab,
|
||||
)
|
||||
@@ -298,10 +306,11 @@ fn dispatch_menu_commands(
|
||||
|
||||
fn isolate_gameplay_input(
|
||||
console: Res<ConsoleState>,
|
||||
chat: Option<Res<crate::chat::ChatState>>,
|
||||
mut keys: ResMut<ButtonInput<KeyCode>>,
|
||||
mut mouse: MessageReader<MouseMotion>,
|
||||
) {
|
||||
if !console.open {
|
||||
if !console.open && !chat.as_ref().is_some_and(|chat| chat.captures_input()) {
|
||||
return;
|
||||
}
|
||||
keys.reset_all();
|
||||
@@ -316,7 +325,16 @@ struct PhysicalInputState {
|
||||
look_ready: bool,
|
||||
mouse_activity: f32,
|
||||
mouse_activity_start: f32,
|
||||
prompts: Option<(bool, frame::PromptStyle)>,
|
||||
prompts: Option<bool>,
|
||||
}
|
||||
|
||||
fn publish_ui_input_capture(
|
||||
console: Res<ConsoleState>,
|
||||
chat: Option<Res<crate::chat::ChatState>>,
|
||||
mut hud_input: ResMut<frame::HudInputView>,
|
||||
) {
|
||||
hud_input.console_open =
|
||||
console.open || chat.as_ref().is_some_and(|chat| chat.captures_input());
|
||||
}
|
||||
|
||||
fn publish_client_action_input(
|
||||
@@ -331,7 +349,9 @@ fn publish_client_action_input(
|
||||
binds: Res<KeyBinds>,
|
||||
mut scripted: ResMut<ConsoleInputState>,
|
||||
console: Res<ConsoleState>,
|
||||
chat: Option<Res<crate::chat::ChatState>>,
|
||||
script_menus: hud::MenuState<'_>,
|
||||
overlay: Res<frame::UiOverlayInput>,
|
||||
mut hud_input: ResMut<frame::HudInputView>,
|
||||
settings: Res<frame::GameSettings>,
|
||||
mut out: ResMut<ClientActionInput>,
|
||||
@@ -384,12 +404,12 @@ fn publish_client_action_input(
|
||||
if binds.is_changed() {
|
||||
*wheel_carry = 0.0;
|
||||
}
|
||||
let modal_captured = console.open
|
||||
let modal_captured = chat.as_ref().is_some_and(|chat| chat.captures_input())
|
||||
|| console.open
|
||||
|| script_menus.is_some_and(|menus| menus.captures_input())
|
||||
|| keys.just_pressed(KeyCode::Escape)
|
||||
|| pad.is_some_and(|pad| pad.just_pressed(bevy::input::gamepad::GamepadButton::Start));
|
||||
let captured = !devices.focused || modal_captured;
|
||||
hud_input.console_open = console.open;
|
||||
let captured = !devices.focused || modal_captured || overlay.0;
|
||||
physical.blocked.retain(|button| inputs.pressed(*button));
|
||||
|
||||
let akimbo = prediction
|
||||
@@ -555,7 +575,7 @@ fn publish_client_action_input(
|
||||
devices.pad_prompts = false;
|
||||
physical.mouse_activity = 0.0;
|
||||
}
|
||||
let prompts = (devices.pad_prompts, devices.style);
|
||||
let prompts = devices.pad_prompts;
|
||||
if physical.prompts == Some(prompts) && !binds.is_changed() {
|
||||
return;
|
||||
}
|
||||
@@ -570,7 +590,7 @@ fn publish_client_action_input(
|
||||
hud_input.action_slot_keys = [None, None, None, None];
|
||||
for (button, id) in labels {
|
||||
let label = match button {
|
||||
BindButton::Pad(button) => button.prompt(devices.style).to_owned(),
|
||||
BindButton::Pad(button) => button.icon_prompt().to_owned(),
|
||||
_ => crate::binds::display_button(button).to_uppercase(),
|
||||
};
|
||||
if let Some(command) = binds.binding_name(button) {
|
||||
@@ -604,7 +624,9 @@ fn publish_client_action_input(
|
||||
/// only reports relative motion to a held pointer the view stops turning while
|
||||
/// every key, and the console, still work.
|
||||
fn sync_cursor_grab(
|
||||
free_pointer: Res<frame::UiPointerFree>,
|
||||
console: Res<ConsoleState>,
|
||||
chat: Option<Res<crate::chat::ChatState>>,
|
||||
script_menus: hud::MenuState<'_>,
|
||||
screen: Option<Res<AppScreen>>,
|
||||
mut focused: MessageReader<WindowFocused>,
|
||||
@@ -621,7 +643,11 @@ fn sync_cursor_grab(
|
||||
let in_game = screen
|
||||
.as_ref()
|
||||
.is_some_and(|s| matches!(**s, AppScreen::InGame));
|
||||
let grab = in_game && !console.open && !menu_open;
|
||||
let grab = in_game
|
||||
&& !console.open
|
||||
&& !chat.as_ref().is_some_and(|chat| chat.open)
|
||||
&& !menu_open
|
||||
&& !free_pointer.0;
|
||||
let Ok(mut cursor) = windows.single_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -650,7 +676,7 @@ fn setup_console(
|
||||
settings: Res<ConsoleSettings>,
|
||||
mut registry: ResMut<ConsoleRegistry>,
|
||||
mut binds: ResMut<KeyBinds>,
|
||||
weapon_completions: Res<frame::Published<crate::weapon_dispatch::WeaponArgCompletions>>,
|
||||
weapon_completions: Res<crate::weapon_dispatch::CompletionSlot>,
|
||||
) {
|
||||
binds.apply_defaults();
|
||||
if registry.resolve("hold").is_none() {
|
||||
|
||||
@@ -152,20 +152,17 @@ pub(crate) fn consume_menu_binding(
|
||||
|
||||
pub(crate) fn sync_binding_view(
|
||||
binds: Res<KeyBinds>,
|
||||
devices: Res<frame::InputDevices>,
|
||||
mut view: ResMut<ui::BindingView>,
|
||||
mut dvars: ResMut<frame::UiMenuDvars>,
|
||||
mut style: Local<Option<frame::PromptStyle>>,
|
||||
) {
|
||||
if !binds.is_changed() && *style == Some(devices.style) {
|
||||
if !binds.is_changed() {
|
||||
return;
|
||||
}
|
||||
*style = Some(devices.style);
|
||||
let mut chords = std::collections::BTreeMap::<u32, (Vec<String>, Vec<String>)>::new();
|
||||
for (button, id) in binds.iter() {
|
||||
let (keys, buttons) = chords.entry(id).or_default();
|
||||
match button {
|
||||
BindButton::Pad(pad) => buttons.push(pad.prompt(devices.style).to_owned()),
|
||||
BindButton::Pad(pad) => buttons.push(pad.icon_prompt().to_owned()),
|
||||
_ => keys.push(display_button(button)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use frame::Published;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use asset_game::{LoadoutRules, WeaponSelection};
|
||||
use assets::PreparedWeapons;
|
||||
@@ -20,14 +20,29 @@ pub struct WeaponArgCompletions {
|
||||
pub bots: Vec<String>,
|
||||
}
|
||||
|
||||
type CompletionSlot = Published<WeaponArgCompletions>;
|
||||
#[derive(Resource, Clone, Default)]
|
||||
pub(crate) struct CompletionSlot(Arc<RwLock<Option<WeaponArgCompletions>>>);
|
||||
|
||||
impl CompletionSlot {
|
||||
fn read<R>(&self, read: impl FnOnce(Option<&WeaponArgCompletions>) -> R) -> R {
|
||||
read(self.0.read().expect("completion slot poisoned").as_ref())
|
||||
}
|
||||
|
||||
fn publish(&self, value: Option<WeaponArgCompletions>) {
|
||||
*self.0.write().expect("completion slot poisoned") = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_weapon_arg_completions(completions: Res<CompletionSlot>) {
|
||||
completions.publish(None);
|
||||
}
|
||||
|
||||
struct LiveListCompleter(CompletionSlot, fn(&WeaponArgCompletions) -> &[String]);
|
||||
|
||||
impl ArgCompleter for LiveListCompleter {
|
||||
fn complete(&self, prefix: &str) -> Vec<String> {
|
||||
self.0.read(|value| {
|
||||
value.map_or_else(Vec::new, |(_, value)| {
|
||||
value.map_or_else(Vec::new, |value| {
|
||||
StaticCompleter::new((self.1)(value).iter().cloned()).complete(prefix)
|
||||
})
|
||||
})
|
||||
@@ -110,20 +125,17 @@ pub(crate) fn refresh_weapon_arg_completions(
|
||||
)
|
||||
.collect()
|
||||
};
|
||||
completions.publish(
|
||||
generation.0.expect("live match generation"),
|
||||
WeaponArgCompletions {
|
||||
give: weapon_completions(&weapons),
|
||||
attach: if held == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
attach_completions(&weapons, held)
|
||||
},
|
||||
camos,
|
||||
killstreaks: killstreaks.map_or_else(Vec::new, |value| value.0.clone()),
|
||||
bots,
|
||||
completions.publish(Some(WeaponArgCompletions {
|
||||
give: weapon_completions(&weapons),
|
||||
attach: if held == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
attach_completions(&weapons, held)
|
||||
},
|
||||
);
|
||||
camos,
|
||||
killstreaks: killstreaks.map_or_else(Vec::new, |value| value.0.clone()),
|
||||
bots,
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn route_weapon_commands(
|
||||
@@ -448,8 +460,8 @@ fn grant_killstreak(
|
||||
seq: &mut net::ActionRequestIds,
|
||||
mut echo: impl FnMut(String),
|
||||
) {
|
||||
let names = completions
|
||||
.read(|value| value.map_or_else(Vec::new, |(_, value)| value.killstreaks.clone()));
|
||||
let names =
|
||||
completions.read(|value| value.map_or_else(Vec::new, |value| value.killstreaks.clone()));
|
||||
let requested = name.to_ascii_lowercase().replace(['-', ' '], "_");
|
||||
let requested = KILLSTREAK_ALIASES
|
||||
.iter()
|
||||
@@ -518,7 +530,7 @@ impl ArgCompleter for KillstreakCompleter {
|
||||
fn complete(&self, prefix: &str) -> Vec<String> {
|
||||
let names = self
|
||||
.0
|
||||
.read(|value| value.map_or_else(Vec::new, |(_, value)| value.killstreaks.clone()));
|
||||
.read(|value| value.map_or_else(Vec::new, |value| value.killstreaks.clone()));
|
||||
let aliases = KILLSTREAK_ALIASES
|
||||
.iter()
|
||||
.filter(|(_, name)| names.iter().any(|value| value == name))
|
||||
@@ -533,7 +545,7 @@ impl ArgCompleter for GiveCompleter {
|
||||
fn complete(&self, prefix: &str) -> Vec<String> {
|
||||
let mut items = vec!["ammo".to_owned(), "camo".to_owned()];
|
||||
self.0.read(|value| {
|
||||
if let Some((_, value)) = value {
|
||||
if let Some(value) = value {
|
||||
items.extend(value.give.iter().map(|name| format!("weapon/{name}")));
|
||||
}
|
||||
});
|
||||
@@ -573,7 +585,7 @@ impl ArgCompleter for BotCompleter {
|
||||
("give" | "fire" | "tp", 1) => {
|
||||
let mut ids = self
|
||||
.0
|
||||
.read(|value| value.map_or_else(Vec::new, |(_, value)| value.bots.clone()));
|
||||
.read(|value| value.map_or_else(Vec::new, |value| value.bots.clone()));
|
||||
if *verb != "give" {
|
||||
ids.insert(0, "all".into());
|
||||
}
|
||||
@@ -582,7 +594,7 @@ impl ArgCompleter for BotCompleter {
|
||||
("give", 2) => {
|
||||
let names = self
|
||||
.0
|
||||
.read(|value| value.map_or_else(Vec::new, |(_, value)| value.give.clone()));
|
||||
.read(|value| value.map_or_else(Vec::new, |value| value.give.clone()));
|
||||
StaticCompleter::new(names).complete(prefix)
|
||||
}
|
||||
("tp", 2) => StaticCompleter::new(["above"]).complete(prefix),
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
|
||||
read_body_combat(s, body)?,
|
||||
Some(body),
|
||||
WeaponAssets {
|
||||
kill_icon: material_name_at(s, links, body.at(sz::WEAPON_DEF_KILL_ICON_OFF)),
|
||||
reticle_center: material_name_at(s, links, body.at(0x1a0)),
|
||||
reticle_side: material_name_at(s, links, body.at(0x1a4)),
|
||||
..WeaponAssets::default()
|
||||
@@ -65,6 +66,7 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
|
||||
_ => None,
|
||||
};
|
||||
s.record_weapon(WeaponGeometry {
|
||||
kill_icon_name: assets.kill_icon,
|
||||
overlay_material_name,
|
||||
overlay_material_lowres_name,
|
||||
reticle_center_name: assets.reticle_center,
|
||||
@@ -155,7 +157,7 @@ fn load_weapon_def(
|
||||
for off in [0x394, 0x398, 0x39c, 0x3a0, 0x3a4, 0x3a8, 0x48c] {
|
||||
follow_name(s, p, off)?;
|
||||
}
|
||||
asset_ptr_at(s, links, AssetType::Material, p.at(0x578))?;
|
||||
let kill_icon = follow_material_name(s, links, p.at(sz::WEAPON_DEF_KILL_ICON_OFF))?;
|
||||
asset_ptr_at(s, links, AssetType::Material, p.at(0x328))?;
|
||||
follow_name(s, p, 0x58c)?;
|
||||
follow_name(s, p, 0x590)?;
|
||||
@@ -220,6 +222,7 @@ fn load_weapon_def(
|
||||
asset_ptr_at(s, links, AssetType::Fx, p.at(0x7f8))?;
|
||||
asset_ptr_at(s, links, AssetType::Fx, p.at(0x7fc))?;
|
||||
Ok(WeaponAssets {
|
||||
kill_icon,
|
||||
gun: gun0,
|
||||
hand: hand0,
|
||||
rocket,
|
||||
@@ -397,6 +400,7 @@ fn follow_material_name(
|
||||
|
||||
#[derive(Default)]
|
||||
struct WeaponAssets {
|
||||
kill_icon: Option<Ptr>,
|
||||
gun: Option<Ptr>,
|
||||
hand: Option<Ptr>,
|
||||
rocket: Option<Ptr>,
|
||||
|
||||
@@ -384,6 +384,7 @@ pub struct FxEffectDefGeometry {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct WeaponGeometry {
|
||||
pub kill_icon_name: Option<Ptr>,
|
||||
pub overlay_material_name: Option<Ptr>,
|
||||
pub overlay_material_lowres_name: Option<Ptr>,
|
||||
pub reticle_center_name: Option<Ptr>,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
pub mod scope;
|
||||
pub use scope::{
|
||||
InMatch, InRound, MatchScope, Published, RoundPhase, ScopeApp, ScopeEpoch, ScopePlugin,
|
||||
ScopeRegistry, ScopeSet, Staging,
|
||||
InMatch, InRound, MatchScope, RoundPhase, ScopeApp, ScopeEpoch, ScopePlugin, ScopeRegistry,
|
||||
ScopeSet, Staging, Timeline,
|
||||
};
|
||||
mod screen_effects;
|
||||
pub use screen_effects::{ScreenEffectsDvars, ScreenEffectsPublished, ScreenEffectsView};
|
||||
pub use screen_effects::{
|
||||
ScreenBlurRequest, ScreenEffectsDvars, ScreenEffectsPublished, ScreenEffectsView,
|
||||
};
|
||||
pub mod class_presets;
|
||||
pub mod pad;
|
||||
pub mod retire;
|
||||
@@ -16,7 +18,7 @@ pub mod settings;
|
||||
pub mod ui;
|
||||
|
||||
pub use class_presets::{ClassPreset, pick_showcase, showcase_classes};
|
||||
pub use pad::{ActivePad, InputDevices, PromptStyle, TestControllerRumble};
|
||||
pub use pad::{ActivePad, ControllerIcon, InputDevices, TestControllerRumble};
|
||||
pub use retire::Retiring;
|
||||
pub use schedule::{
|
||||
AUTHORITY_TOC, AuthorityBookkeeping, AuthorityEdge, AuthoritySet, CLIENT_TOC,
|
||||
@@ -30,14 +32,15 @@ pub use script_entity_notify::{AbortKillcam, BeginKillcam, KillcamEnded, Spawned
|
||||
pub use script_notify::{ExitLevelCalled, register_script_notify};
|
||||
pub use session::{
|
||||
AdmissionKey, AppScreen, BotNavigationReady, CacWeaponOffer, ClassSelectHandoff,
|
||||
DisplayEncodedCamera, Headless, HostClassLoadouts, HostClassSlot, HudInputView, InstalledMatch,
|
||||
LaunchIdentity, LaunchReport, LifeEndCause, LifeEnded, LifeStartReason, LifeStarted,
|
||||
LocalLoadKey, LocalSpawnArmed, MapLoadApproved, MapLoadFailed, MatchKey, ReadinessState,
|
||||
RuntimeRole, TeardownReason, UiCamera, UiDraw, ViewSubject, WorldGeneration, WorldProducts,
|
||||
WorldReadiness, WorldStamp,
|
||||
DisplayEncodedCamera, Headless, HostClassLoadouts, HostClassSlot, HudDraw, HudInputView,
|
||||
InstalledMatch, LaunchIdentity, LaunchReport, LifeEndCause, LifeEnded, LifeStartReason,
|
||||
LifeStarted, LocalLoadKey, LocalSpawnArmed, MapLoadApproved, MapLoadFailed, MatchKey,
|
||||
ReadinessState, RuntimeRole, TeardownReason, UiCamera, UiDraw, UiOverlayInput, UiPointerFree,
|
||||
ViewSubject, WorldGeneration, WorldProducts, WorldReadiness, WorldStamp,
|
||||
};
|
||||
pub use settings::{DisplayResolution, GameSettings, OtherGame};
|
||||
pub use ui::{
|
||||
HostMatchRules, UiBindRequest, UiBindingCapture, UiExecCommand, UiMenuDvars, UiMenuKey,
|
||||
UiMenuRequest, UiPartyState, UiPlayMusic, UiPlaySound, UiStopMusic, register_ui_contracts,
|
||||
FrontendContentLoading, HostMatchRules, UiBindRequest, UiBindingCapture, UiExecCommand,
|
||||
UiMenuDvars, UiMenuKey, UiMenuRequest, UiPartyState, UiPlayMusic, UiPlaySound, UiStopMusic,
|
||||
register_ui_contracts,
|
||||
};
|
||||
|
||||
@@ -3,21 +3,69 @@ use bevy::prelude::{Entity, Message, Resource};
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ActivePad(pub Option<Entity>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum PromptStyle {
|
||||
Xbox,
|
||||
PlayStation,
|
||||
#[default]
|
||||
Generic,
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct InputDevices {
|
||||
pub pad_prompts: bool,
|
||||
pub style: PromptStyle,
|
||||
pub focused: bool,
|
||||
pub aiming_with_pad: bool,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct TestControllerRumble;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ControllerIcon {
|
||||
A,
|
||||
B,
|
||||
X,
|
||||
Y,
|
||||
DpadUp,
|
||||
DpadDown,
|
||||
DpadLeft,
|
||||
DpadRight,
|
||||
}
|
||||
|
||||
impl ControllerIcon {
|
||||
pub const ALL: [Self; 8] = [
|
||||
Self::A,
|
||||
Self::B,
|
||||
Self::X,
|
||||
Self::Y,
|
||||
Self::DpadUp,
|
||||
Self::DpadDown,
|
||||
Self::DpadLeft,
|
||||
Self::DpadRight,
|
||||
];
|
||||
|
||||
pub const fn prompt(self) -> &'static str {
|
||||
match self {
|
||||
Self::A => "\u{e000}",
|
||||
Self::B => "\u{e001}",
|
||||
Self::X => "\u{e002}",
|
||||
Self::Y => "\u{e003}",
|
||||
Self::DpadUp => "\u{e004}",
|
||||
Self::DpadDown => "\u{e005}",
|
||||
Self::DpadLeft => "\u{e006}",
|
||||
Self::DpadRight => "\u{e007}",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_char(c: char) -> Option<Self> {
|
||||
Self::ALL
|
||||
.get((c as u32).checked_sub(0xe000)? as usize)
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub const fn material(self) -> &'static str {
|
||||
match self {
|
||||
Self::A => "iw4l_controller_a",
|
||||
Self::B => "iw4l_controller_b",
|
||||
Self::X => "iw4l_controller_x",
|
||||
Self::Y => "iw4l_controller_y",
|
||||
Self::DpadUp => "iw4l_controller_dpad_up",
|
||||
Self::DpadDown => "iw4l_controller_dpad_down",
|
||||
Self::DpadLeft => "iw4l_controller_dpad_left",
|
||||
Self::DpadRight => "iw4l_controller_dpad_right",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use bevy::prelude::*;
|
||||
use bevy::state::{app::StatesPlugin, state::StateTransitionSystems};
|
||||
use std::any::{TypeId, type_name};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
|
||||
pub enum MatchScope {
|
||||
@@ -22,6 +21,13 @@ pub enum RoundPhase {
|
||||
Playing,
|
||||
}
|
||||
|
||||
#[derive(SubStates, Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
|
||||
#[source(MatchScope = MatchScope::Live)]
|
||||
pub enum Timeline {
|
||||
#[default]
|
||||
Running,
|
||||
}
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ScopeCommit;
|
||||
|
||||
@@ -71,37 +77,10 @@ impl<R: Resource> Staging<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct Published<T: Send + Sync + 'static>(Arc<RwLock<Option<(u64, T)>>>);
|
||||
|
||||
impl<T: Send + Sync + 'static> Clone for Published<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Default for Published<T> {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(RwLock::new(None)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Published<T> {
|
||||
pub fn read<R>(&self, read: impl FnOnce(Option<(u64, &T)>) -> R) -> R {
|
||||
let guard = self.0.read().expect("published slot poisoned");
|
||||
read(guard.as_ref().map(|(epoch, value)| (*epoch, value)))
|
||||
}
|
||||
|
||||
pub fn publish(&self, epoch: u64, value: T) {
|
||||
*self.0.write().expect("published slot poisoned") = Some((epoch, value));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ScopeKind {
|
||||
Resource,
|
||||
Staged,
|
||||
Published,
|
||||
Message,
|
||||
}
|
||||
|
||||
@@ -151,13 +130,19 @@ impl Plugin for ScopePlugin {
|
||||
}
|
||||
app.init_state::<MatchScope>()
|
||||
.add_sub_state::<RoundPhase>()
|
||||
.add_sub_state::<Timeline>()
|
||||
.init_resource::<ScopeRegistry>()
|
||||
.init_resource::<crate::Retiring>()
|
||||
.init_resource::<ScopeEpoch<MatchScope>>()
|
||||
.init_resource::<ScopeEpoch<RoundPhase>>()
|
||||
.init_resource::<ScopeEpoch<Timeline>>()
|
||||
.add_systems(
|
||||
StateTransition,
|
||||
(retire_state::<RoundPhase>, retire_state::<MatchScope>)
|
||||
(
|
||||
retire_state::<Timeline>,
|
||||
retire_state::<RoundPhase>,
|
||||
retire_state::<MatchScope>,
|
||||
)
|
||||
.chain()
|
||||
.in_set(ScopeRetired)
|
||||
.in_set(StateTransitionSystems::TransitionSchedules),
|
||||
@@ -197,6 +182,7 @@ impl Plugin for ScopePlugin {
|
||||
configure_scope(app, MatchScope::Loading);
|
||||
configure_scope(app, MatchScope::Live);
|
||||
configure_scope(app, RoundPhase::Playing);
|
||||
configure_scope(app, Timeline::Running);
|
||||
if cfg!(debug_assertions) {
|
||||
app.add_systems(Last, verify_registry.after(ScopeCommit));
|
||||
}
|
||||
@@ -254,6 +240,12 @@ fn retire_state<S: States>(
|
||||
}
|
||||
world.resource_mut::<ScopeEpoch<S>>().0 =
|
||||
world.resource::<ScopeEpoch<S>>().0.wrapping_add(1);
|
||||
// Match-epoch consumers (audio voices) must also drop what a restarted
|
||||
// timeline played.
|
||||
if TypeId::of::<S>() == TypeId::of::<Timeline>() {
|
||||
world.resource_mut::<ScopeEpoch<MatchScope>>().0 =
|
||||
world.resource::<ScopeEpoch<MatchScope>>().0.wrapping_add(1);
|
||||
}
|
||||
if !actions.is_empty() {
|
||||
world.resource_mut::<crate::Retiring>().mark_teardown();
|
||||
let mut registry = world.resource_mut::<ScopeRegistry>();
|
||||
@@ -309,7 +301,6 @@ pub trait ScopeApp {
|
||||
fn init_app<R: Resource + FromWorld>(&mut self) -> &mut Self;
|
||||
fn scoped<R: Resource + FromWorld>(&mut self, scope: impl States) -> &mut Self;
|
||||
fn staged<R: Resource>(&mut self, scope: impl States) -> &mut Self;
|
||||
fn published<T: Send + Sync + 'static>(&mut self, scope: impl States) -> &mut Self;
|
||||
fn scoped_message<M: Message>(&mut self, scope: impl States) -> &mut Self;
|
||||
}
|
||||
|
||||
@@ -363,22 +354,6 @@ fn staging_empty<R: Resource>(world: &World) -> bool {
|
||||
.get_resource::<Staging<R>>()
|
||||
.is_none_or(Staging::is_empty)
|
||||
}
|
||||
fn published_present<T: Send + Sync + 'static>(world: &World) -> bool {
|
||||
world
|
||||
.get_resource::<Published<T>>()
|
||||
.is_some_and(|slot| slot.read(|value| value.is_some()))
|
||||
}
|
||||
fn clear_published<T: Send + Sync + 'static>(world: &mut World) {
|
||||
let value = world
|
||||
.resource::<Published<T>>()
|
||||
.0
|
||||
.write()
|
||||
.expect("published slot poisoned")
|
||||
.take();
|
||||
if let Some(value) = value {
|
||||
world.resource_mut::<crate::Retiring>().hand_over(value);
|
||||
}
|
||||
}
|
||||
fn messages_present<M: Message>(world: &World) -> bool {
|
||||
world
|
||||
.get_resource::<Messages<M>>()
|
||||
@@ -406,7 +381,8 @@ fn register<R: 'static, S: States>(
|
||||
assert!(
|
||||
(TypeId::of::<S>() == TypeId::of::<MatchScope>()
|
||||
&& matches!(label.as_str(), "Loading" | "Live"))
|
||||
|| (TypeId::of::<S>() == TypeId::of::<RoundPhase>() && label == "Playing"),
|
||||
|| (TypeId::of::<S>() == TypeId::of::<RoundPhase>() && label == "Playing")
|
||||
|| (TypeId::of::<S>() == TypeId::of::<Timeline>() && label == "Running"),
|
||||
"unsupported resource scope {label}"
|
||||
);
|
||||
if !app.is_plugin_added::<ScopePlugin>() {
|
||||
@@ -474,19 +450,6 @@ impl ScopeApp for App {
|
||||
);
|
||||
self.init_resource::<Staging<R>>()
|
||||
}
|
||||
fn published<T: Send + Sync + 'static>(&mut self, scope: impl States) -> &mut Self {
|
||||
register::<Published<T>, _>(
|
||||
self,
|
||||
scope,
|
||||
ScopeKind::Published,
|
||||
noop,
|
||||
clear_published::<T>,
|
||||
published_present::<T>,
|
||||
empty,
|
||||
noop,
|
||||
);
|
||||
self.init_resource::<Published<T>>()
|
||||
}
|
||||
fn scoped_message<M: Message>(&mut self, scope: impl States) -> &mut Self {
|
||||
register::<Messages<M>, _>(
|
||||
self,
|
||||
|
||||
@@ -15,6 +15,9 @@ pub struct ScreenEffectsView {
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ScreenEffectsPublished;
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct ScreenBlurRequest(pub f32);
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ScreenEffectsDvars {
|
||||
pub draw_shellshock: bool,
|
||||
|
||||
@@ -45,6 +45,21 @@ impl Default for UiDraw {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug, Clone, Copy)]
|
||||
pub struct UiPointerFree(pub bool);
|
||||
|
||||
#[derive(Resource, Default, Debug, Clone, Copy)]
|
||||
pub struct UiOverlayInput(pub bool);
|
||||
|
||||
#[derive(Resource, Debug, Clone, Copy)]
|
||||
pub struct HudDraw(pub bool);
|
||||
|
||||
impl Default for HudDraw {
|
||||
fn default() -> Self {
|
||||
Self(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Debug, Default)]
|
||||
pub struct LaunchIdentity {
|
||||
pub role_label: String,
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Default for GameSettings {
|
||||
pad_curve: 0,
|
||||
pad_acceleration: true,
|
||||
pad_aim_assist: 0,
|
||||
pad_prompts: 0,
|
||||
pad_prompts: 1,
|
||||
pad_vibration: true,
|
||||
pad_deadzone_left: 0.12,
|
||||
pad_deadzone_right: 0.12,
|
||||
@@ -177,7 +177,7 @@ impl GameSettings {
|
||||
self.pad_stick_layout = self.pad_stick_layout.min(3);
|
||||
self.pad_curve = self.pad_curve.min(2);
|
||||
self.pad_aim_assist = 0;
|
||||
self.pad_prompts = self.pad_prompts.min(3);
|
||||
self.pad_prompts = 1;
|
||||
let finite = |v: f32, lo: f32, hi: f32, default: f32| {
|
||||
if v.is_finite() {
|
||||
v.clamp(lo, hi)
|
||||
|
||||
@@ -30,6 +30,9 @@ impl UiMenuDvars {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug)]
|
||||
pub struct FrontendContentLoading;
|
||||
|
||||
#[derive(Resource, Clone, Default, Debug)]
|
||||
pub struct HostMatchRules(pub Vec<(String, String)>);
|
||||
|
||||
@@ -41,6 +44,7 @@ pub struct UiExecCommand {
|
||||
#[derive(Message, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum UiMenuRequest {
|
||||
Toggle,
|
||||
RestoreLobby,
|
||||
Open(String),
|
||||
Close(String),
|
||||
Focus { menu: String, item: String },
|
||||
|
||||
@@ -7,7 +7,8 @@ pub mod menu;
|
||||
pub use audio::{UiPlayMusic, UiPlaySound, UiStopMusic};
|
||||
pub use input::{UiBindRequest, UiBindingCapture};
|
||||
pub use menu::{
|
||||
HostMatchRules, UiExecCommand, UiMenuDvars, UiMenuKey, UiMenuRequest, UiPartyState,
|
||||
FrontendContentLoading, HostMatchRules, UiExecCommand, UiMenuDvars, UiMenuKey, UiMenuRequest,
|
||||
UiPartyState,
|
||||
};
|
||||
|
||||
pub fn register_ui_contracts(app: &mut App) {
|
||||
|
||||
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
description = "Draws what hud_iw4 computes / Snapshot presents."
|
||||
|
||||
[dependencies]
|
||||
resvg = { version = "0.48.1", default-features = false }
|
||||
structured_data_iw4.workspace = true
|
||||
diag.workspace = true
|
||||
bevy.workspace = true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="rgb(32,37,43)" stroke="rgb(112,207,104)" stroke-width="2"/><path d="M10 22 16 9 22 22M12 18h8" fill="none" stroke="rgb(112,207,104)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="rgb(32,37,43)" stroke="rgb(240,116,110)" stroke-width="2"/><path d="M12 10h5a3 3 0 0 1 0 6h-5m5 0a3 3 0 0 1 0 6h-5V10" fill="none" stroke="rgb(240,116,110)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M11 2h10v9h9v10h-9v9H11v-9H2V11h9Z" fill="rgb(32,37,43)" stroke="rgb(146,155,165)" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 4h8v8h-8Z" fill="rgb(245,247,250)" transform="rotate(180 16 16)"/></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M11 2h10v9h9v10h-9v9H11v-9H2V11h9Z" fill="rgb(32,37,43)" stroke="rgb(146,155,165)" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 4h8v8h-8Z" fill="rgb(245,247,250)" transform="rotate(270 16 16)"/></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M11 2h10v9h9v10h-9v9H11v-9H2V11h9Z" fill="rgb(32,37,43)" stroke="rgb(146,155,165)" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 4h8v8h-8Z" fill="rgb(245,247,250)" transform="rotate(90 16 16)"/></svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M11 2h10v9h9v10h-9v9H11v-9H2V11h9Z" fill="rgb(32,37,43)" stroke="rgb(146,155,165)" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 4h8v8h-8Z" fill="rgb(245,247,250)" transform="rotate(0 16 16)"/></svg>
|
||||
|
After Width: | Height: | Size: 280 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="rgb(32,37,43)" stroke="rgb(117,185,246)" stroke-width="2"/><path d="m11 10 10 12m0-12L11 22" fill="none" stroke="rgb(117,185,246)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 302 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="rgb(32,37,43)" stroke="rgb(240,211,106)" stroke-width="2"/><path d="m11 10 5 7 5-7m-5 7v5" fill="none" stroke="rgb(240,211,106)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 300 B |
@@ -17,6 +17,8 @@ pub(crate) struct HudRootVisible(pub Option<i32>);
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct BloodOverlayLatch {
|
||||
intensity: f32,
|
||||
subject: Option<ViewSubject>,
|
||||
last_time: Option<i32>,
|
||||
pub(crate) packed: PackedList,
|
||||
last_gpu_intensity: Option<f32>,
|
||||
last_w: Option<f32>,
|
||||
@@ -72,6 +74,12 @@ pub(crate) fn update_blood_overlay(
|
||||
latch.intensity = 0.0;
|
||||
}
|
||||
}
|
||||
let now = cg_clock.time();
|
||||
if latch.subject != Some(*view) || latch.last_time.is_some_and(|time| now < time) {
|
||||
latch.intensity = 0.0;
|
||||
}
|
||||
latch.subject = Some(*view);
|
||||
latch.last_time = Some(now);
|
||||
let in_game = matches!(*screen, AppScreen::InGame);
|
||||
let Some(ps) = presented.player(local.0) else {
|
||||
latch.intensity = 0.0;
|
||||
|
||||
@@ -739,10 +739,16 @@ pub(crate) fn text_width(font: &FontDef, text: &str) -> i32 {
|
||||
width = 0;
|
||||
continue;
|
||||
}
|
||||
let Some(glyph) = font.glyph(letter) else {
|
||||
let dx = if char::from_u32(letter)
|
||||
.and_then(frame::ControllerIcon::from_char)
|
||||
.is_some()
|
||||
{
|
||||
font.pixel_height
|
||||
} else if let Some(glyph) = font.glyph(letter) {
|
||||
i32::from(glyph.dx)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let dx = i32::from(glyph.dx);
|
||||
width += dx;
|
||||
if max_width < width {
|
||||
max_width = width;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use asset_game::MenuCatalog;
|
||||
use bevy::prelude::*;
|
||||
use net::LastAdoptedSnapshot;
|
||||
use net::transport::udp_session::UdpClientLink;
|
||||
|
||||
use crate::draw2d::{Draw2dCmd, Draw2dList, Draw2dOp, Draw2dProvenance, tessellate_fonts};
|
||||
use crate::gpu_list::{HudTessPass, TessJob};
|
||||
use crate::images::HUD_CHROME_NAMESPACE;
|
||||
use crate::images::HudImages;
|
||||
|
||||
const SHOW_AFTER: Duration = Duration::from_millis(1000);
|
||||
|
||||
#[derive(Component)]
|
||||
pub(crate) struct ConnectionNoticeRaster;
|
||||
|
||||
pub(crate) fn update(
|
||||
surface: Res<crate::surface::Hud2dSurface>,
|
||||
catalog: Option<Res<MenuCatalog>>,
|
||||
link: Option<Res<UdpClientLink>>,
|
||||
adopted: Option<Res<LastAdoptedSnapshot>>,
|
||||
mut pass: ResMut<HudTessPass>,
|
||||
mut hud_images: ResMut<HudImages>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
) {
|
||||
pass.connection_notice = TessJob::Hide;
|
||||
if !surface.is_ready() || !link.is_some_and(|link| link.has_entered_match()) {
|
||||
return;
|
||||
}
|
||||
let Some(silent) = adopted
|
||||
.and_then(|adopted| adopted.since_adopted())
|
||||
.filter(|d| *d >= SHOW_AFTER)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(catalog) = catalog.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(item) = catalog
|
||||
.get("hud_fullscreen")
|
||||
.and_then(|m| m.items.iter().find(|i| i.owner_draw == 113))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let font_name = hud_iw4::ui_get_font_handle(
|
||||
item.font_enum,
|
||||
surface.scale_virtual_to_real()[1],
|
||||
item.text_scale,
|
||||
);
|
||||
let Some(font) = catalog.font(font_name) else {
|
||||
return;
|
||||
};
|
||||
let text = format!("Connection interrupted {:.1}s", silent.as_secs_f32());
|
||||
let nscale = hud_iw4::normalized_text_scale(font.pixel_height, item.text_scale);
|
||||
let material = asset_core::AssetRef::bare_name(&font.material).to_owned();
|
||||
let width = crate::chrome::ui_text_width(font, &text, item.text_scale);
|
||||
let rect = surface.apply_rect(
|
||||
-width * 0.5,
|
||||
-120.0,
|
||||
nscale,
|
||||
nscale,
|
||||
hud_iw4::ALIGN_CENTER,
|
||||
hud_iw4::ALIGN_CENTER,
|
||||
);
|
||||
let list = Draw2dList {
|
||||
cmds: vec![Draw2dCmd {
|
||||
material_namespace: HUD_CHROME_NAMESPACE,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
w: rect.w,
|
||||
h: rect.h,
|
||||
s0: 0.0,
|
||||
t0: 0.0,
|
||||
s1: 1.0,
|
||||
t1: 1.0,
|
||||
color: [1.0, 0.3, 0.3, 1.0],
|
||||
material: material.clone(),
|
||||
op: Draw2dOp::TextRun {
|
||||
font: font_name.to_owned(),
|
||||
scale: nscale,
|
||||
text,
|
||||
loc_key: String::new(),
|
||||
style: item.text_style,
|
||||
fx: None,
|
||||
glow: None,
|
||||
},
|
||||
provenance: Draw2dProvenance::CgDraw {
|
||||
site: "connection_notice",
|
||||
},
|
||||
layer: 1,
|
||||
}],
|
||||
};
|
||||
let mut fonts = HashMap::new();
|
||||
fonts.insert(font_name.to_owned(), font);
|
||||
let (quads, _) = tessellate_fonts(&list, &fonts);
|
||||
if !quads.is_empty()
|
||||
&& hud_images
|
||||
.get(HUD_CHROME_NAMESPACE, &material, &mut images)
|
||||
.is_some()
|
||||
{
|
||||
pass.connection_notice = TessJob::Quads(quads);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use frame::ControllerIcon;
|
||||
|
||||
pub(crate) const SIZE: u32 = 64;
|
||||
|
||||
pub(crate) fn rasterize(icon: ControllerIcon) -> Vec<u8> {
|
||||
let svg = match icon {
|
||||
ControllerIcon::A => include_str!("../icons/xbox/a.svg"),
|
||||
ControllerIcon::B => include_str!("../icons/xbox/b.svg"),
|
||||
ControllerIcon::X => include_str!("../icons/xbox/x.svg"),
|
||||
ControllerIcon::Y => include_str!("../icons/xbox/y.svg"),
|
||||
ControllerIcon::DpadUp => include_str!("../icons/xbox/dpad_up.svg"),
|
||||
ControllerIcon::DpadDown => include_str!("../icons/xbox/dpad_down.svg"),
|
||||
ControllerIcon::DpadLeft => include_str!("../icons/xbox/dpad_left.svg"),
|
||||
ControllerIcon::DpadRight => include_str!("../icons/xbox/dpad_right.svg"),
|
||||
};
|
||||
let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default())
|
||||
.expect("embedded controller SVG");
|
||||
let mut pixmap = resvg::tiny_skia::Pixmap::new(SIZE, SIZE).expect("controller icon pixmap");
|
||||
resvg::render(
|
||||
&tree,
|
||||
resvg::tiny_skia::Transform::from_scale(
|
||||
SIZE as f32 / tree.size().width(),
|
||||
SIZE as f32 / tree.size().height(),
|
||||
),
|
||||
&mut pixmap.as_mut(),
|
||||
);
|
||||
let mut rgba = pixmap.take();
|
||||
for pixel in rgba.chunks_exact_mut(4) {
|
||||
let alpha = u32::from(pixel[3]);
|
||||
if alpha != 0 {
|
||||
for channel in &mut pixel[..3] {
|
||||
*channel = ((u32::from(*channel) * 255 + alpha / 2) / alpha).min(255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use bevy::prelude::*;
|
||||
use frame::{LifeStarted, UiDraw, ViewSubject};
|
||||
use net::{FrameClock, LocalPresentClient, PresentedSnapshot};
|
||||
|
||||
use crate::draw2d::{Draw2dProvenance, Draw2dQuad};
|
||||
use crate::gpu_list::{HudTessPass, TessJob};
|
||||
|
||||
#[derive(Component)]
|
||||
pub(crate) struct DamageDirectionRaster;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct Hit {
|
||||
time: i32,
|
||||
duration: i32,
|
||||
yaw: f32,
|
||||
stun: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct DamageDirections {
|
||||
hits: [Hit; 12],
|
||||
event: Option<u32>,
|
||||
subject: Option<ViewSubject>,
|
||||
time: Option<i32>,
|
||||
weapon_frac: f32,
|
||||
ads_in: bool,
|
||||
random: u32,
|
||||
}
|
||||
|
||||
pub(crate) fn spawn(root: &mut ChildSpawnerCommands) {
|
||||
crate::font_overlay::spawn_overlay(root, DamageDirectionRaster);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn update(
|
||||
surface: Res<crate::surface::Hud2dSurface>,
|
||||
presented: Res<PresentedSnapshot>,
|
||||
local: Res<LocalPresentClient>,
|
||||
view: Res<ViewSubject>,
|
||||
clock: Res<FrameClock>,
|
||||
ui_draw: Res<UiDraw>,
|
||||
aim: Res<net::ViewweaponAim>,
|
||||
weapons: Option<Res<assets::PreparedWeapons>>,
|
||||
mut started: MessageReader<LifeStarted>,
|
||||
mut directions: ResMut<DamageDirections>,
|
||||
mut pass: ResMut<HudTessPass>,
|
||||
) {
|
||||
pass.damage_direction = TessJob::Hide;
|
||||
let now = clock.time();
|
||||
let reset = started.read().filter(|ev| ev.client == local.0.0).count() != 0
|
||||
|| directions.subject != Some(*view)
|
||||
|| directions.time.is_some_and(|time| now < time);
|
||||
if reset {
|
||||
*directions = DamageDirections::default();
|
||||
}
|
||||
directions.subject = Some(*view);
|
||||
directions.time = Some(now);
|
||||
let Some(ps) = presented.player(local.0) else {
|
||||
*directions = DamageDirections::default();
|
||||
return;
|
||||
};
|
||||
let dvars = presented
|
||||
.snapshot()
|
||||
.map(|snapshot| snapshot.meta.script_dvars(local.0));
|
||||
if directions
|
||||
.event
|
||||
.is_some_and(|event| event != ps.damage_event)
|
||||
&& ps.damage_count > 0
|
||||
&& !(ps.damage_yaw == 255 && ps.damage_pitch == 255)
|
||||
{
|
||||
let duration = dvars
|
||||
.and_then(|d| d.int("cg_hudDamageIconTime"))
|
||||
.unwrap_or(6000)
|
||||
.max(0);
|
||||
let slot = directions
|
||||
.hits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, hit)| hit.time)
|
||||
.map(|(index, _)| index)
|
||||
.unwrap_or(0);
|
||||
directions.random = directions.random.wrapping_mul(214013).wrapping_add(2531011);
|
||||
let jitter = (((directions.random >> 16) & 0x7fff) as f32 / 32768.0 - 0.5) * 20.0;
|
||||
directions.hits[slot] = Hit {
|
||||
time: now,
|
||||
duration,
|
||||
yaw: ps.damage_yaw as f32 * (360.0 / 256.0) + jitter,
|
||||
stun: ps.damage_flags & 1 != 0,
|
||||
};
|
||||
}
|
||||
directions.event = Some(ps.damage_event);
|
||||
if ps.f_weapon_pos_frac != directions.weapon_frac {
|
||||
directions.ads_in = ps.f_weapon_pos_frac > directions.weapon_frac;
|
||||
}
|
||||
directions.weapon_frac = ps.f_weapon_pos_frac;
|
||||
let scoped = weapons.as_ref().is_some_and(|weapons| {
|
||||
let index = playerstate_iw4::get_viewmodel_weapon_index(ps);
|
||||
weapons
|
||||
.snapshot_weapon(presented.weapon_epoch(), index)
|
||||
.ok()
|
||||
.and_then(|weapon| weapon.hud_facts())
|
||||
.is_some_and(|facts| {
|
||||
hud_iw4::get_weap_reticle_zoom(
|
||||
ps.f_weapon_pos_frac,
|
||||
directions.ads_in,
|
||||
&hud_iw4::WeaponAdsOverlayFacts {
|
||||
ads_zoom_in_frac: facts.ads_zoom_in_frac,
|
||||
ads_zoom_out_frac: facts.ads_zoom_out_frac,
|
||||
overlay_material: u32::from(
|
||||
weapons.registry().overlay_material_of(index).is_some(),
|
||||
),
|
||||
overlay_reticle: facts.overlay_reticle,
|
||||
..default()
|
||||
},
|
||||
)
|
||||
.active
|
||||
})
|
||||
});
|
||||
let flashed = hud_iw4::is_flashbanged(
|
||||
now,
|
||||
ps.shellshock_time,
|
||||
ps.shellshock_duration,
|
||||
presented
|
||||
.shellshock(local.0)
|
||||
.map_or(hud_iw4::SCREEN_BLEND_BLURRED, |shock| shock.screen_type),
|
||||
) != 0;
|
||||
if !surface.is_ready()
|
||||
|| !ui_draw.0
|
||||
|| flashed
|
||||
|| ps.pm_flags & playerstate_iw4::pm_flags::MANTLE != 0
|
||||
|| (scoped
|
||||
&& dvars
|
||||
.and_then(|d| d.int("cg_hudDamageIconInScope"))
|
||||
.unwrap_or(0)
|
||||
== 0)
|
||||
|| dvars
|
||||
.and_then(|d| d.int("cg_drawDamageDirection"))
|
||||
.unwrap_or(1)
|
||||
== 0
|
||||
|| (view.in_killcam() && ps.kill_cam_entity != playerstate_iw4::ENTITYNUM_NONE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let value = |name, default| {
|
||||
dvars
|
||||
.and_then(|d| d.float(name))
|
||||
.filter(|value| value.is_finite())
|
||||
.unwrap_or(default)
|
||||
.clamp(0.0, 512.0)
|
||||
};
|
||||
let scale = surface.scale_virtual_to_real();
|
||||
let half_width = value("cg_hudDamageIconWidth", 128.0) * scale[0] * 0.5;
|
||||
let offset = value("cg_hudDamageIconOffset", 128.0) * scale[1];
|
||||
let outer = offset + value("cg_hudDamageIconHeight", 64.0) * scale[1];
|
||||
let mut center = [surface.width() * 0.5, surface.height() * 0.5];
|
||||
if scoped && aim.live {
|
||||
let projection = crate::presentation_scale::PresentationScale::from_window(
|
||||
surface.width(),
|
||||
surface.height(),
|
||||
);
|
||||
let factor = projection.factor(crate::presentation_scale::ScaleClass::ProjectionBound);
|
||||
center[0] += aim.xhair_x * factor;
|
||||
center[1] += aim.xhair_y * factor;
|
||||
}
|
||||
let mut quads = Vec::new();
|
||||
for hit in &directions.hits {
|
||||
let age = now.saturating_sub(hit.time);
|
||||
if age <= 0 || age >= hit.duration {
|
||||
continue;
|
||||
}
|
||||
let (sin, cos) = (ps.viewangles[1] - hit.yaw).to_radians().sin_cos();
|
||||
let xy = [
|
||||
[half_width, outer],
|
||||
[-half_width, outer],
|
||||
[-half_width, offset],
|
||||
[half_width, offset],
|
||||
]
|
||||
.map(|[x, y]| [center[0] + cos * x - sin * y, center[1] + sin * x + cos * y]);
|
||||
quads.push(Draw2dQuad {
|
||||
xy,
|
||||
st: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
|
||||
color: [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
(2.0 - 2.0 * age as f32 / hit.duration as f32).min(1.0),
|
||||
],
|
||||
material: if hit.stun {
|
||||
"hit_direction_stun"
|
||||
} else {
|
||||
"hit_direction"
|
||||
}
|
||||
.into(),
|
||||
material_namespace: crate::images::HUD_CHROME_NAMESPACE,
|
||||
provenance: Draw2dProvenance::CgDraw {
|
||||
site: "damage_direction",
|
||||
},
|
||||
layer: 0,
|
||||
clip: None,
|
||||
});
|
||||
}
|
||||
pass.damage_direction = TessJob::Quads(quads);
|
||||
}
|
||||
@@ -648,6 +648,43 @@ fn text_run_quads(
|
||||
hud_iw4::rand_with_seed(&mut temp)
|
||||
});
|
||||
|
||||
if let Some(icon) = frame::ControllerIcon::from_char(c) {
|
||||
let size = font.pixel_height as f32;
|
||||
let (xy, st) = hud_iw4::draw_stretch_pic_corners(
|
||||
cursor_x,
|
||||
y - size * y_scale * 0.8,
|
||||
size * x_scale,
|
||||
size * y_scale,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
);
|
||||
let alpha = if decaying || (draw_rand_char_at_end && max_length_remaining == 1) {
|
||||
f32::from(hud_iw4::modulate_byte_colors(
|
||||
(color[3] * 255.0) as u8,
|
||||
fade_alpha,
|
||||
)) / 255.0
|
||||
} else {
|
||||
color[3]
|
||||
};
|
||||
if !skip_drawing {
|
||||
out.push(Draw2dQuad {
|
||||
xy,
|
||||
st,
|
||||
color: [1.0, 1.0, 1.0, alpha],
|
||||
material: icon.material().to_owned(),
|
||||
material_namespace,
|
||||
provenance: provenance.clone(),
|
||||
layer,
|
||||
clip,
|
||||
});
|
||||
}
|
||||
cursor_x += size * x_scale;
|
||||
max_length_remaining = max_length_remaining.saturating_sub(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(glyph) = font.glyph(letter) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -191,7 +191,9 @@ pub struct HudTessPass {
|
||||
pub targetmap: TessJob,
|
||||
pub emp_static: TessJob,
|
||||
pub mantle_hint: TessJob,
|
||||
pub damage_direction: TessJob,
|
||||
pub breath_hint: TessJob,
|
||||
pub connection_notice: TessJob,
|
||||
pub use_hint: TessJob,
|
||||
pub hud_elems: TessJob,
|
||||
pub hud_elems_back: TessJob,
|
||||
|
||||
@@ -133,6 +133,7 @@ fn make_image(
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct HudImages {
|
||||
controller_icons: HashMap<String, Handle<Image>>,
|
||||
ui_images: asset_material::UiImagePublication,
|
||||
games_root: PathBuf,
|
||||
|
||||
@@ -385,6 +386,24 @@ impl HudImages {
|
||||
sampler: Option<u8>,
|
||||
images: &mut Assets<Image>,
|
||||
) -> Option<Handle<Image>> {
|
||||
if let Some(icon) = frame::ControllerIcon::ALL
|
||||
.into_iter()
|
||||
.find(|icon| icon.material() == name)
|
||||
{
|
||||
let handle = self
|
||||
.controller_icons
|
||||
.entry(name.to_owned())
|
||||
.or_insert_with(|| {
|
||||
images.add(make_image(
|
||||
crate::controller_icons::SIZE,
|
||||
crate::controller_icons::SIZE,
|
||||
crate::controller_icons::rasterize(icon),
|
||||
HudSampling::Color,
|
||||
None,
|
||||
))
|
||||
});
|
||||
return Some(handle.clone());
|
||||
}
|
||||
let key = iwd_key(ns, name, sampling, sampler);
|
||||
if ns == HUD_CHROME_NAMESPACE && self.zone_states.get(&key.1) == Some(&None) {
|
||||
return None;
|
||||
|
||||
@@ -174,7 +174,7 @@ fn snapshot_client_team(presented: &net::PresentedSnapshot, client: i32) -> i32
|
||||
.map_or(0, |meta| meta.client_state_team)
|
||||
}
|
||||
|
||||
fn obituary_name_color(local_team: i32, team: i32, dvars: sim::ScriptDvars<'_>) -> [f32; 4] {
|
||||
pub fn team_name_color(local_team: i32, team: i32, dvars: sim::ScriptDvars<'_>) -> [f32; 4] {
|
||||
if !matches!(local_team, 1 | 2) || !matches!(team, 1 | 2) {
|
||||
return [1.0, 1.0, 1.0, 1.0];
|
||||
}
|
||||
@@ -341,6 +341,9 @@ pub(crate) fn update_killfeed(
|
||||
|
||||
let now = clock.time();
|
||||
for cmd in notifies.read() {
|
||||
if cmd.tag == net::SVC_CHAT {
|
||||
continue;
|
||||
}
|
||||
if !window.seen.insert(cmd.id) {
|
||||
continue;
|
||||
}
|
||||
@@ -577,7 +580,7 @@ pub(crate) fn update_killfeed(
|
||||
applied.h,
|
||||
font_material.clone(),
|
||||
attacker.clone(),
|
||||
obituary_name_color(local_team, *attacker_team, dvars),
|
||||
team_name_color(local_team, *attacker_team, dvars),
|
||||
"killfeed_obituary",
|
||||
));
|
||||
x_virtual += attacker_w + icon_spacing;
|
||||
@@ -641,7 +644,7 @@ pub(crate) fn update_killfeed(
|
||||
applied.h,
|
||||
font_material.clone(),
|
||||
victim.clone(),
|
||||
obituary_name_color(local_team, *victim_team, dvars),
|
||||
team_name_color(local_team, *victim_team, dvars),
|
||||
"killfeed_obituary",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ mod blood;
|
||||
mod breath_hint;
|
||||
mod chrome;
|
||||
mod compass;
|
||||
mod connection_notice;
|
||||
mod controller_icons;
|
||||
mod damage_direction;
|
||||
mod draw2d;
|
||||
mod emp_static;
|
||||
mod expr_cache;
|
||||
@@ -55,3 +58,5 @@ pub use splash::PendingSplash;
|
||||
pub use surface::Hud2dSurface;
|
||||
|
||||
mod use_hint;
|
||||
|
||||
pub use killfeed::team_name_color;
|
||||
|
||||
@@ -48,6 +48,28 @@ impl MenuHost<'_> {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
let flag = |on: bool| String::from(if on { "1" } else { "0" });
|
||||
match lower.as_str() {
|
||||
"ui_profile_rank_icon" | "ui_profile_level" => self
|
||||
.world
|
||||
.player_data
|
||||
.and_then(|data| {
|
||||
let progression = data.progression.as_ref()?;
|
||||
let rank = progression.rank.to_string();
|
||||
if lower == "ui_profile_rank_icon" {
|
||||
data.table_lookup(
|
||||
"mp/rankicontable.csv",
|
||||
0,
|
||||
&rank,
|
||||
progression
|
||||
.prestige
|
||||
.checked_add(1)
|
||||
.filter(|column| *column > 0)?,
|
||||
)
|
||||
} else {
|
||||
data.table_lookup("mp/ranktable.csv", 0, &rank, 14)
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
"cl_ingame" => flag(self.world.in_game),
|
||||
"gamemode" => "mp".to_owned(),
|
||||
"ui_multiplayer" | "widescreen" | "hidef" => flag(true),
|
||||
|
||||
@@ -8,7 +8,10 @@ use state::OpenMenu;
|
||||
pub use state::ScriptMenus;
|
||||
|
||||
#[derive(bevy::prelude::Resource, Default)]
|
||||
pub struct FrontendMenus(pub ScriptMenus);
|
||||
pub struct FrontendMenus {
|
||||
menus: ScriptMenus,
|
||||
restore_lobby: bool,
|
||||
}
|
||||
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub struct MenuState<'w> {
|
||||
@@ -19,7 +22,7 @@ impl MenuState<'_> {
|
||||
pub fn as_ref(&self) -> Option<&ScriptMenus> {
|
||||
self.scoped
|
||||
.as_deref()
|
||||
.or_else(|| self.frontend.as_deref().map(|menus| &menus.0))
|
||||
.or_else(|| self.frontend.as_deref().map(|menus| &menus.menus))
|
||||
}
|
||||
pub fn as_deref(&self) -> Option<&ScriptMenus> {
|
||||
self.as_ref()
|
||||
@@ -65,6 +68,7 @@ pub(super) const MAX_SCRIPT_DEPTH: u32 = 16;
|
||||
const MAIN_MENU_DVAR: &str = "g_scriptMainMenu";
|
||||
const FALLBACK_MAIN_MENU: &str = "class";
|
||||
const SCOREBOARD_MENU: &str = "scoreboard";
|
||||
const FRONTEND_LOADING_MENU: &str = "iw4l_frontend_loading";
|
||||
|
||||
pub(super) fn is_focusable(item: &asset_game::MenuItem) -> bool {
|
||||
if item.static_flags & WINDOW_DECORATION != 0 {
|
||||
@@ -180,6 +184,7 @@ pub(crate) fn spawn_script_menus(root: &mut ChildSpawnerCommands) {
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct MenuInputs<'w, 's> {
|
||||
keys: Res<'w, ButtonInput<KeyCode>>,
|
||||
overlay: Res<'w, frame::UiOverlayInput>,
|
||||
keyboard: MessageReader<'w, 's, bevy::input::keyboard::KeyboardInput>,
|
||||
mouse: Res<'w, ButtonInput<MouseButton>>,
|
||||
windows: Query<'w, 's, &'static Window, With<bevy::window::PrimaryWindow>>,
|
||||
@@ -193,6 +198,7 @@ pub(crate) struct MenuInputs<'w, 's> {
|
||||
party: Res<'w, frame::UiPartyState>,
|
||||
frontend_strings: Option<Res<'w, asset_game::LocalizeCatalog>>,
|
||||
player_data: Option<Res<'w, sim::LocalPlayerData>>,
|
||||
content_loading: Option<Res<'w, frame::FrontendContentLoading>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -245,7 +251,19 @@ pub(crate) fn update_script_menus(
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut pass: ResMut<HudTessPass>,
|
||||
) {
|
||||
let mut menus = menu_states.0.as_deref_mut().unwrap_or(&mut menu_states.1.0);
|
||||
let requests: Vec<UiMenuRequest> = input.requests.read().cloned().collect();
|
||||
let FrontendMenus {
|
||||
menus: frontend_menus,
|
||||
restore_lobby,
|
||||
} = &mut *menu_states.1;
|
||||
for request in &requests {
|
||||
match request {
|
||||
UiMenuRequest::RestoreLobby => *restore_lobby = true,
|
||||
UiMenuRequest::Close(name) if name == "game_lobby" => *restore_lobby = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut menus = menu_states.0.as_deref_mut().unwrap_or(frontend_menus);
|
||||
let mut locals = local_states
|
||||
.0
|
||||
.as_deref_mut()
|
||||
@@ -258,7 +276,6 @@ pub(crate) fn update_script_menus(
|
||||
out.binding.command = None;
|
||||
*menus = ScriptMenus::default();
|
||||
}
|
||||
let requests: Vec<UiMenuRequest> = input.requests.read().cloned().collect();
|
||||
let Some(catalog) = catalog.as_deref() else {
|
||||
return;
|
||||
};
|
||||
@@ -342,8 +359,27 @@ pub(crate) fn update_script_menus(
|
||||
binding: &mut out.binding,
|
||||
};
|
||||
|
||||
let main_menu = if frontend && input.content_loading.is_some() {
|
||||
FRONTEND_LOADING_MENU
|
||||
} else {
|
||||
"iw4l_main"
|
||||
};
|
||||
if frontend
|
||||
&& main_menu == "iw4l_main"
|
||||
&& let Some(at) = runner.menus.position(FRONTEND_LOADING_MENU)
|
||||
{
|
||||
let above: Vec<String> = runner.menus.stack[at + 1..]
|
||||
.iter()
|
||||
.map(|open| open.name.clone())
|
||||
.collect();
|
||||
runner.close(FRONTEND_LOADING_MENU);
|
||||
runner.open(main_menu);
|
||||
for name in above {
|
||||
runner.open(&name);
|
||||
}
|
||||
}
|
||||
if frontend && screen_changed {
|
||||
runner.open("iw4l_main");
|
||||
runner.open(main_menu);
|
||||
if let Some(def) = catalog.get("main")
|
||||
&& !def.sound_name.is_empty()
|
||||
{
|
||||
@@ -353,6 +389,14 @@ pub(crate) fn update_script_menus(
|
||||
}
|
||||
}
|
||||
|
||||
if frontend && main_menu == "iw4l_main" && *restore_lobby {
|
||||
*restore_lobby = false;
|
||||
if input.party.in_lobby && input.party.is_host {
|
||||
runner.open("game_lobby");
|
||||
diag::info!(Ui, "frontend: private lobby reopened");
|
||||
}
|
||||
}
|
||||
|
||||
for command in meta
|
||||
.into_iter()
|
||||
.filter(|_| in_game)
|
||||
@@ -378,6 +422,7 @@ pub(crate) fn update_script_menus(
|
||||
for request in requests {
|
||||
match request {
|
||||
UiMenuRequest::Toggle => pressed.escape = true,
|
||||
UiMenuRequest::RestoreLobby => {}
|
||||
UiMenuRequest::Open(name) => runner.open(&name),
|
||||
UiMenuRequest::Close(name) => runner.close(&name),
|
||||
UiMenuRequest::Focus { menu, item } => {
|
||||
@@ -411,7 +456,7 @@ pub(crate) fn update_script_menus(
|
||||
if frontend && runner.dvars.get("ui_map_error") == Some("1") {
|
||||
runner.open("iw4l_map_error");
|
||||
}
|
||||
let pointer = !console_open;
|
||||
let pointer = !console_open && !input.overlay.0;
|
||||
if pointer {
|
||||
let keys = &input.keys;
|
||||
pressed.escape |= keys.just_pressed(KeyCode::Escape);
|
||||
@@ -453,6 +498,10 @@ pub(crate) fn update_script_menus(
|
||||
}
|
||||
}
|
||||
|
||||
if input.overlay.0 && in_game {
|
||||
return;
|
||||
}
|
||||
|
||||
let scoreboard = snapshot
|
||||
.filter(|s| in_game && crate::scoreboard::displayed(world.scores_open, s, local.0))
|
||||
.and_then(|_| catalog.get(SCOREBOARD_MENU));
|
||||
|
||||
@@ -41,27 +41,29 @@ impl Plugin for HudPlugin {
|
||||
);
|
||||
crate::gpu_list::register(app);
|
||||
let _ = crate::scorebar::milliseconds();
|
||||
app.init_resource::<HudImages>()
|
||||
app.init_resource::<frame::HudDraw>()
|
||||
.init_resource::<HudImages>()
|
||||
.init_resource::<HudPresentationGaps>()
|
||||
.init_resource::<ReticleAdsLatch>()
|
||||
.init_resource::<IrisLetterboxFill>()
|
||||
.init_resource::<BloodOverlayLatch>()
|
||||
.scoped::<crate::damage_direction::DamageDirections>(frame::Timeline::Running)
|
||||
.init_resource::<BloodGpuJob>()
|
||||
.init_resource::<FlashWhiteoutLatch>()
|
||||
.init_resource::<FlashGpuJob>()
|
||||
.scoped::<PendingSplash>(frame::MatchScope::Live)
|
||||
.scoped::<SplashSlots>(frame::MatchScope::Live)
|
||||
.scoped::<KillfeedWindow>(frame::MatchScope::Live)
|
||||
.scoped::<PendingSplash>(frame::Timeline::Running)
|
||||
.scoped::<SplashSlots>(frame::Timeline::Running)
|
||||
.scoped::<KillfeedWindow>(frame::Timeline::Running)
|
||||
.scoped::<PlayerCardCache>(frame::MatchScope::Live)
|
||||
.scoped::<UiLocalVars>(frame::MatchScope::Live)
|
||||
.init_resource::<crate::playercard::FrontendLocalVars>()
|
||||
.init_resource::<HudRootVisible>()
|
||||
.scoped::<crate::compass::CompassPingLatch>(frame::MatchScope::Live)
|
||||
.scoped::<crate::compass::CompassPingLatch>(frame::Timeline::Running)
|
||||
.init_resource::<crate::surface::Hud2dSurface>()
|
||||
.init_resource::<HudPresentStamp>()
|
||||
.init_resource::<HudStageStamp>()
|
||||
.init_resource::<crate::expr_cache::MenuExprCache>()
|
||||
.scoped::<crate::hudelem::HudElemSoundLatch>(frame::MatchScope::Live)
|
||||
.scoped::<crate::hudelem::HudElemSoundLatch>(frame::Timeline::Running)
|
||||
.scoped::<crate::menus::ScriptMenus>(frame::MatchScope::Live)
|
||||
.init_resource::<crate::menus::FrontendMenus>()
|
||||
.add_message::<net::SvcCardSlotCmd>()
|
||||
@@ -126,7 +128,9 @@ impl Plugin for HudPlugin {
|
||||
crate::use_hint::update.in_set(frame::InMatch),
|
||||
update_hud_elems.in_set(frame::InMatch),
|
||||
update_targetmap,
|
||||
crate::damage_direction::update.in_set(frame::InMatch),
|
||||
crate::menus::update_script_menus,
|
||||
crate::connection_notice::update.in_set(frame::InMatch),
|
||||
hud_stage_close::<7>,
|
||||
)
|
||||
.chain(),
|
||||
@@ -154,7 +158,9 @@ impl Plugin for HudPlugin {
|
||||
flush_hud_elems_tess,
|
||||
flush_targetmap_tess,
|
||||
flush_blood_tess,
|
||||
flush_damage_direction_tess,
|
||||
flush_script_menus_tess,
|
||||
flush_connection_notice_tess,
|
||||
)
|
||||
.chain()
|
||||
.after(hud_stage_close::<7>)
|
||||
@@ -340,6 +346,7 @@ fn ensure_hud_root(mut commands: Commands, existing: Query<Entity, With<HudRoot>
|
||||
.with_children(|root| {
|
||||
crate::font_overlay::spawn_overlay(root, crate::overhead_names::OverheadNamesRaster);
|
||||
spawn_reticle(root);
|
||||
crate::damage_direction::spawn(root);
|
||||
spawn_iris(root);
|
||||
spawn_hud_elems_back(root);
|
||||
spawn_compass(root);
|
||||
@@ -357,17 +364,22 @@ fn ensure_hud_root(mut commands: Commands, existing: Query<Entity, With<HudRoot>
|
||||
spawn_hud_elems(root);
|
||||
spawn_targetmap(root);
|
||||
crate::menus::spawn_script_menus(root);
|
||||
crate::font_overlay::spawn_overlay(
|
||||
root,
|
||||
crate::connection_notice::ConnectionNoticeRaster,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn sync_hud_visibility(
|
||||
screen: Res<AppScreen>,
|
||||
ui_draw: Option<Res<UiDraw>>,
|
||||
hud_draw: Res<frame::HudDraw>,
|
||||
mut roots: Query<&mut Visibility, With<HudRoot>>,
|
||||
mut visible: ResMut<HudRootVisible>,
|
||||
) {
|
||||
let ui_on = ui_draw.is_some_and(|d| d.0);
|
||||
let show = hud_root_should_show(*screen, ui_on);
|
||||
let show = hud_draw.0 && hud_root_should_show(*screen, ui_on);
|
||||
visible.0 = Some(i32::from(show));
|
||||
for mut vis in &mut roots {
|
||||
let want = if show {
|
||||
@@ -681,6 +693,36 @@ fn flush_mantle_hint_tess(
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_damage_direction_tess(
|
||||
surface: Res<crate::surface::Hud2dSurface>,
|
||||
mut pass: ResMut<HudTessPass>,
|
||||
mut hud_images: ResMut<HudImages>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut frame: ResMut<crate::gpu_list::HudTessGpuFrame>,
|
||||
mut hint: Query<
|
||||
(Entity, &mut Node, &mut crate::gpu_list::GpuListLatch),
|
||||
With<crate::damage_direction::DamageDirectionRaster>,
|
||||
>,
|
||||
) {
|
||||
let _body = gpu_list::TessBody::open();
|
||||
if !surface.is_ready() {
|
||||
return;
|
||||
}
|
||||
let job = std::mem::take(&mut pass.damage_direction);
|
||||
if let Ok((_, mut host, mut latch)) = hint.single_mut() {
|
||||
gpu_list::apply_tess_job(
|
||||
job,
|
||||
&mut host,
|
||||
&mut latch,
|
||||
&mut hud_images,
|
||||
&mut images,
|
||||
&mut frame,
|
||||
surface.width(),
|
||||
surface.height(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_scoreboard_tess(
|
||||
surface: Res<crate::surface::Hud2dSurface>,
|
||||
mut pass: ResMut<HudTessPass>,
|
||||
@@ -897,6 +939,36 @@ fn flush_breath_hint_tess(
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_connection_notice_tess(
|
||||
surface: Res<crate::surface::Hud2dSurface>,
|
||||
mut pass: ResMut<HudTessPass>,
|
||||
mut hud_images: ResMut<HudImages>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut frame: ResMut<crate::gpu_list::HudTessGpuFrame>,
|
||||
mut hint: Query<
|
||||
(Entity, &mut Node, &mut crate::gpu_list::GpuListLatch),
|
||||
With<crate::connection_notice::ConnectionNoticeRaster>,
|
||||
>,
|
||||
) {
|
||||
let _body = gpu_list::TessBody::open();
|
||||
if !surface.is_ready() {
|
||||
return;
|
||||
}
|
||||
let job = std::mem::take(&mut pass.connection_notice);
|
||||
if let Ok((_, mut host, mut latch)) = hint.single_mut() {
|
||||
gpu_list::apply_tess_job(
|
||||
job,
|
||||
&mut host,
|
||||
&mut latch,
|
||||
&mut hud_images,
|
||||
&mut images,
|
||||
&mut frame,
|
||||
surface.width(),
|
||||
surface.height(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn release_hud_publication(
|
||||
mut latches: Query<&mut crate::gpu_list::GpuListLatch>,
|
||||
mut frame: ResMut<crate::gpu_list::HudTessGpuFrame>,
|
||||
|
||||
@@ -356,26 +356,41 @@ pub(crate) fn update_splash(
|
||||
return;
|
||||
};
|
||||
|
||||
// Promotion uses this match's authoritative rank. The authored expressions
|
||||
// read a stored profile's experience/prestige, which is not the match state.
|
||||
let mut promotion;
|
||||
let menu = if menu_name.eq_ignore_ascii_case("promotion") {
|
||||
promotion = menu.clone();
|
||||
if let Some(meta) = presented
|
||||
let rank = presented
|
||||
.snapshot()
|
||||
.and_then(|snap| snap.meta.for_client(local.0))
|
||||
{
|
||||
let key = meta.rank.to_string();
|
||||
let icon = catalog
|
||||
.as_ref()
|
||||
.and_then(|c| c.string_table("mp/rankIconTable.csv"))
|
||||
.map(|t| t.lookup_col(&key, meta.prestige.saturating_add(1)))
|
||||
.unwrap_or("");
|
||||
for item in &mut promotion.items {
|
||||
if item.name.starts_with("promotion_rank_icon") {
|
||||
item.material_exp.clear();
|
||||
item.background = icon.to_owned();
|
||||
}
|
||||
.and_then(|snap| snap.meta.for_client(local.0));
|
||||
let icon = rank
|
||||
.and_then(|meta| {
|
||||
let column = meta.prestige.checked_add(1).filter(|column| *column > 0)?;
|
||||
catalog
|
||||
.as_ref()?
|
||||
.string_table("mp/rankIconTable.csv")
|
||||
.map(|table| table.lookup_col(&meta.rank.to_string(), column))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let title = rank
|
||||
.and_then(|meta| {
|
||||
catalog
|
||||
.as_ref()?
|
||||
.string_table("mp/rankTable.csv")
|
||||
.map(|table| table.lookup_col(&meta.rank.to_string(), 5))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for item in &mut promotion.items {
|
||||
if item.name.starts_with("promotion_rank_icon") {
|
||||
item.material_exp.clear();
|
||||
item.background = icon.to_owned();
|
||||
} else if item.name.starts_with("promotion_rank_text") {
|
||||
item.text_exp.clear();
|
||||
item.text_key = if title.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("@{}", title.trim_start_matches('@'))
|
||||
};
|
||||
item.text_literal = false;
|
||||
}
|
||||
}
|
||||
&promotion
|
||||
|
||||
@@ -45,6 +45,9 @@ const CLOSE_SERVICE_DONE: u32 = 0;
|
||||
|
||||
const MAX_CLOSE_REASON_BYTES: usize = 120;
|
||||
|
||||
const DATAGRAM_SEND_BUFFER: usize = 128 * 1024;
|
||||
const RELAY_BACKLOG_BYTES: usize = 16 * 1024;
|
||||
|
||||
struct AddressSlot {
|
||||
counts: Arc<std::sync::Mutex<HashMap<IpAddr, usize>>>,
|
||||
ip: IpAddr,
|
||||
@@ -543,7 +546,7 @@ async fn serve(bind: SocketAddr, cert_path: &Path, key_path: &Path) -> Result<()
|
||||
));
|
||||
transport.keep_alive_interval(Some(SESSION_KEEP_ALIVE));
|
||||
transport.datagram_receive_buffer_size(Some(128 * 1024));
|
||||
transport.datagram_send_buffer_size(128 * 1024);
|
||||
transport.datagram_send_buffer_size(DATAGRAM_SEND_BUFFER);
|
||||
|
||||
let endpoint = quinn::Endpoint::server(config, bind)?;
|
||||
let state = Arc::new(Mutex::new(ServiceState::default()));
|
||||
@@ -678,6 +681,7 @@ async fn run_connection(
|
||||
connection.clone(),
|
||||
control_tx,
|
||||
);
|
||||
let mut relay_gate = RelayGate::default();
|
||||
let mut children = tokio::task::JoinSet::new();
|
||||
children.spawn(async move {
|
||||
let result = async {
|
||||
@@ -739,7 +743,7 @@ async fn run_connection(
|
||||
}
|
||||
datagram = connection.read_datagram() => {
|
||||
let bytes = datagram?;
|
||||
if let Err(error) = relay_datagram(state, connection_id, &bytes).await {
|
||||
if let Err(error) = relay_datagram(state, connection_id, &bytes, &mut relay_gate).await {
|
||||
let _ = writeln!(std::io::stderr(), "relay datagram: {error}");
|
||||
}
|
||||
}
|
||||
@@ -1344,17 +1348,63 @@ async fn bootstrap_forward(
|
||||
ConnTask::Bootstrap
|
||||
}
|
||||
|
||||
// Gameplay packets arrive split into fragments, and one lost fragment loses
|
||||
// the packet. Quinn sheds the oldest datagrams of a full buffer one at a time,
|
||||
// which on a slow target leaves almost no packet whole.
|
||||
#[derive(Default)]
|
||||
struct RelayGate {
|
||||
shedding: Option<u32>,
|
||||
}
|
||||
|
||||
impl RelayGate {
|
||||
fn admit(&mut self, payload: &[u8], target: &quinn::Connection, wire_bytes: usize) -> bool {
|
||||
let Some((index, count, packet_id)) = fragment_header(payload) else {
|
||||
return true;
|
||||
};
|
||||
if index != 0 {
|
||||
return self.shedding != Some(packet_id);
|
||||
}
|
||||
let queued = DATAGRAM_SEND_BUFFER.saturating_sub(target.datagram_send_buffer_space());
|
||||
let admitted =
|
||||
queued < wire_bytes || queued + wire_bytes * usize::from(count) <= RELAY_BACKLOG_BYTES;
|
||||
self.shedding = (!admitted).then_some(packet_id);
|
||||
admitted
|
||||
}
|
||||
}
|
||||
|
||||
fn fragment_header(payload: &[u8]) -> Option<(u16, u16, u32)> {
|
||||
let header = payload.get(..12)?;
|
||||
if header[..3] != *b"RF\x02" {
|
||||
return None;
|
||||
}
|
||||
let index = u16::from_le_bytes([header[4], header[5]]);
|
||||
let count = u16::from_le_bytes([header[6], header[7]]);
|
||||
let packet_id = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
|
||||
Some((index, count, packet_id))
|
||||
}
|
||||
|
||||
async fn relay_datagram(
|
||||
state: &Mutex<ServiceState>,
|
||||
connection_id: u64,
|
||||
bytes: &[u8],
|
||||
gate: &mut RelayGate,
|
||||
) -> Result<()> {
|
||||
let relay = decode_relay(bytes)?;
|
||||
let (target, outgoing) = {
|
||||
let current = state.lock().await;
|
||||
current.route_target(connection_id, &relay)?
|
||||
};
|
||||
target.send_datagram(outgoing.into())?;
|
||||
let payload = match relay {
|
||||
RelayDatagram::ClientToHost(payload) | RelayDatagram::HostToMember { payload, .. } => {
|
||||
payload
|
||||
}
|
||||
RelayDatagram::ServiceToHost { payload, .. } | RelayDatagram::ServiceToMember(payload) => {
|
||||
payload
|
||||
}
|
||||
};
|
||||
if gate.admit(payload, &target, outgoing.len()) {
|
||||
target.send_datagram(outgoing.into())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ pub const MAX_BUILD_BYTES: usize = 64;
|
||||
pub const MAX_PLAYER_NAME_BYTES: usize = 64;
|
||||
pub const MAX_LIST_ADVERTS: usize = 128;
|
||||
|
||||
pub const SESSION_IDLE: core::time::Duration = core::time::Duration::from_secs(8);
|
||||
pub const SESSION_IDLE: core::time::Duration = core::time::Duration::from_secs(40);
|
||||
pub const SESSION_KEEP_ALIVE: core::time::Duration = core::time::Duration::from_secs(1);
|
||||
|
||||
const MAGIC: [u8; 4] = *b"IW4M";
|
||||
|
||||
@@ -39,14 +39,16 @@ pub(crate) const COMMAND_TIME_STEP_MS: i32 = 200;
|
||||
|
||||
pub const MAX_COMMAND_STALL_MS: i32 = 1000;
|
||||
|
||||
pub const LOST_COMMAND_HOLD_MS: i32 = 250;
|
||||
|
||||
pub const MAX_COMMAND_LEAD_MS: i32 = 200;
|
||||
|
||||
pub const MAX_COMMANDS_PER_PEER_PER_FRAME: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum InputBacklogFault {
|
||||
QueueCount { queued: usize },
|
||||
|
||||
QueueDuration { queued_ms: i32 },
|
||||
|
||||
NoProgress { stalled_ms: i32 },
|
||||
}
|
||||
|
||||
@@ -58,11 +60,6 @@ impl core::fmt::Display for InputBacklogFault {
|
||||
"InputBacklogExceeded: {queued} unique commands queued (max \
|
||||
{MAX_QUEUED_COMMANDS_PER_PEER})"
|
||||
),
|
||||
Self::QueueDuration { queued_ms } => write!(
|
||||
f,
|
||||
"InputBacklogExceeded: {queued_ms} ms of command time queued (max \
|
||||
{MAX_QUEUED_COMMAND_MS})"
|
||||
),
|
||||
Self::NoProgress { stalled_ms } => write!(
|
||||
f,
|
||||
"InputBacklogExceeded: command queue made no progress for {stalled_ms} ms (max \
|
||||
@@ -186,6 +183,22 @@ impl ClientCommandInbox {
|
||||
if sequenced > MAX_QUEUED_COMMANDS_PER_PEER {
|
||||
return Some(InputBacklogFault::QueueCount { queued: sequenced });
|
||||
}
|
||||
let head = queue.iter().find_map(|(seq, _, _)| *seq)?;
|
||||
let blocked = self.blocked_head.entry(id).or_insert((head, time_ms));
|
||||
if blocked.0 != head {
|
||||
*blocked = (head, time_ms);
|
||||
}
|
||||
let stalled_ms = time_ms.saturating_sub(blocked.1);
|
||||
if stalled_ms > MAX_COMMAND_STALL_MS {
|
||||
return Some(InputBacklogFault::NoProgress { stalled_ms });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn queued_ms(&self, id: ClientId, time_ms: i32) -> i32 {
|
||||
let Some(queue) = self.queued.get(&id) else {
|
||||
return 0;
|
||||
};
|
||||
let mut queued_ms: i32 = 0;
|
||||
let mut previous: Option<i32> = self
|
||||
.last_consumed
|
||||
@@ -207,19 +220,22 @@ impl ClientCommandInbox {
|
||||
}
|
||||
previous = Some(cmd.server_time);
|
||||
}
|
||||
if queued_ms > MAX_QUEUED_COMMAND_MS {
|
||||
return Some(InputBacklogFault::QueueDuration { queued_ms });
|
||||
queued_ms
|
||||
}
|
||||
|
||||
// A burst after a stalled relay can carry more command time than the
|
||||
// budget. Running it would let the peer outpace real time, so the oldest
|
||||
// commands are dropped as lost instead of retiring the peer.
|
||||
fn shed_excess(&mut self, id: ClientId, time_ms: i32) {
|
||||
while self.queued_ms(id, time_ms) > MAX_QUEUED_COMMAND_MS {
|
||||
let Some((seq, cmd, _)) = self.queued.get_mut(&id).and_then(VecDeque::pop_front) else {
|
||||
return;
|
||||
};
|
||||
if let Some(seq) = seq {
|
||||
self.last_acked_seq.insert(id, seq.0);
|
||||
self.last_consumed.insert(id, cmd);
|
||||
}
|
||||
}
|
||||
let head = queue.iter().find_map(|(seq, _, _)| *seq)?;
|
||||
let blocked = self.blocked_head.entry(id).or_insert((head, time_ms));
|
||||
if blocked.0 != head {
|
||||
*blocked = (head, time_ms);
|
||||
}
|
||||
let stalled_ms = time_ms.saturating_sub(blocked.1);
|
||||
if stalled_ms > MAX_COMMAND_STALL_MS {
|
||||
return Some(InputBacklogFault::NoProgress { stalled_ms });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn last_acked_seq(&self, id: ClientId) -> Option<CmdSeq> {
|
||||
@@ -245,12 +261,19 @@ impl ClientCommandInbox {
|
||||
pub fn take_for_tick(&mut self, time_ms: i32) -> GatheredCommands {
|
||||
let mut out = GatheredCommands::default();
|
||||
for id in self.known_clients() {
|
||||
// After an authority hitch the client's monotonic clock stays
|
||||
// ahead; holding its commands until their stamp starves the peer.
|
||||
if let Some(queue) = self.queued.get_mut(&id) {
|
||||
for (_, cmd, _) in queue.iter_mut() {
|
||||
if cmd.server_time > time_ms.saturating_add(MAX_COMMAND_LEAD_MS) {
|
||||
cmd.server_time = time_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A datagram burst can contain more than one frame of runnable work.
|
||||
// Consume the bounded frame budget before measuring remaining debt.
|
||||
// Count and blocked-head limits still apply before any simulation.
|
||||
if let Some(fault) = self.backlog_fault(id, time_ms)
|
||||
&& !matches!(fault, InputBacklogFault::QueueDuration { .. })
|
||||
{
|
||||
if let Some(fault) = self.backlog_fault(id, time_ms) {
|
||||
out.backlog_faults.push((id, fault));
|
||||
continue;
|
||||
}
|
||||
@@ -269,10 +292,16 @@ impl ClientCommandInbox {
|
||||
.iter()
|
||||
.take_while(|(seq, cmd, _)| {
|
||||
if let Some(seq) = seq {
|
||||
if seq.0 != expected || cmd.server_time > time_ms {
|
||||
// The client resends only its oldest unacked
|
||||
// command plus a small rotation, so on a lossy
|
||||
// relay a hole may never fill. Once the command
|
||||
// after it is overdue, the hole is lost.
|
||||
let lost = seq.0 > expected
|
||||
&& cmd.server_time <= time_ms - LOST_COMMAND_HOLD_MS;
|
||||
if (seq.0 != expected && !lost) || cmd.server_time > time_ms {
|
||||
return false;
|
||||
}
|
||||
expected = expected.wrapping_add(1);
|
||||
expected = seq.0.wrapping_add(1);
|
||||
}
|
||||
true
|
||||
})
|
||||
@@ -290,6 +319,7 @@ impl ClientCommandInbox {
|
||||
}
|
||||
drained.sort_by_key(|(seq, _, _)| *seq);
|
||||
if drained.is_empty() {
|
||||
self.shed_excess(id, time_ms);
|
||||
if let Some(fault) = self.backlog_fault(id, time_ms) {
|
||||
out.backlog_faults.push((id, fault));
|
||||
continue;
|
||||
@@ -330,6 +360,7 @@ impl ClientCommandInbox {
|
||||
if let Some(seq) = ack {
|
||||
out.acks.push((id, seq));
|
||||
}
|
||||
self.shed_excess(id, time_ms);
|
||||
if let Some(fault) = self.backlog_fault(id, time_ms) {
|
||||
out.backlog_faults.push((id, fault));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub const CLIENT_ENTITY_SLOT_COUNT: usize = ENTITYNUM_NONE as usize;
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[component(immutable)]
|
||||
#[require(bevy::prelude::DespawnOnExit<frame::MatchScope> = bevy::prelude::DespawnOnExit(frame::MatchScope::Live))]
|
||||
#[require(bevy::prelude::DespawnOnExit<frame::Timeline> = bevy::prelude::DespawnOnExit(frame::Timeline::Running))]
|
||||
pub struct CEntity {
|
||||
number: u16,
|
||||
client: Option<ClientId>,
|
||||
@@ -397,9 +397,9 @@ fn sync_snapshot_entities(
|
||||
}
|
||||
|
||||
pub(crate) fn register_authority_entities(app: &mut App) {
|
||||
app.scoped::<CEntitySlots>(frame::MatchScope::Live)
|
||||
app.scoped::<CEntitySlots>(frame::Timeline::Running)
|
||||
.init_resource::<NetIdentityGaps>()
|
||||
.scoped::<CEntityBirthCensus>(frame::MatchScope::Live)
|
||||
.scoped::<CEntityBirthCensus>(frame::Timeline::Running)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
sync_authority_entities
|
||||
@@ -409,9 +409,9 @@ pub(crate) fn register_authority_entities(app: &mut App) {
|
||||
}
|
||||
|
||||
pub fn register_client_entities(app: &mut App) {
|
||||
app.scoped::<CEntitySlots>(frame::MatchScope::Live)
|
||||
app.scoped::<CEntitySlots>(frame::Timeline::Running)
|
||||
.init_resource::<NetIdentityGaps>()
|
||||
.scoped::<CEntityBirthCensus>(frame::MatchScope::Live)
|
||||
.scoped::<CEntityBirthCensus>(frame::Timeline::Running)
|
||||
.add_systems(
|
||||
Update,
|
||||
sync_client_entities
|
||||
|
||||
@@ -731,7 +731,7 @@ fn set_ads_from_reset(
|
||||
}
|
||||
|
||||
pub fn register_entity_event_dispatch(app: &mut App) {
|
||||
app.scoped::<EntityEventCursor>(frame::MatchScope::Live)
|
||||
app.scoped::<EntityEventCursor>(frame::Timeline::Running)
|
||||
.init_resource::<AppliedEntityEventWalk>()
|
||||
.init_resource::<UnsupportedEntityEvents>()
|
||||
.init_resource::<NetIdentityGaps>()
|
||||
|
||||
@@ -46,6 +46,7 @@ pub struct PresentLocalCensus {
|
||||
|
||||
#[derive(Resource, Clone, Debug, Default)]
|
||||
pub struct PresentedSnapshot {
|
||||
external_camera: Option<bool>,
|
||||
inner: Option<Arc<Snapshot>>,
|
||||
previous_inner: Option<Arc<Snapshot>>,
|
||||
frame_interpolation: f32,
|
||||
@@ -60,12 +61,29 @@ pub struct PresentedSnapshot {
|
||||
}
|
||||
|
||||
impl PresentedSnapshot {
|
||||
pub fn external_camera(&self) -> bool {
|
||||
self.external_camera == Some(true)
|
||||
}
|
||||
|
||||
pub fn first_person_camera(&self) -> bool {
|
||||
self.external_camera == Some(false)
|
||||
}
|
||||
|
||||
pub fn set_first_person_camera(&mut self) {
|
||||
self.external_camera = Some(false);
|
||||
}
|
||||
|
||||
pub fn set_external_camera(&mut self, external: bool) {
|
||||
self.external_camera = external.then_some(true);
|
||||
}
|
||||
|
||||
pub fn weapon_epoch(&self) -> Option<u32> {
|
||||
self.snapshot()
|
||||
.map(|snapshot| snapshot.meta.world_objects.map_round_epoch)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.external_camera = None;
|
||||
self.inner = None;
|
||||
self.previous_inner = None;
|
||||
self.frame_interpolation = 0.0;
|
||||
|
||||
@@ -150,9 +150,15 @@ pub struct LastAdoptedSnapshot {
|
||||
pub next_snap: Option<Arc<Snapshot>>,
|
||||
|
||||
pub applied_this_frame: bool,
|
||||
|
||||
pub adopted_at: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl LastAdoptedSnapshot {
|
||||
pub fn since_adopted(&self) -> Option<std::time::Duration> {
|
||||
self.adopted_at.map(|at| at.elapsed())
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Option<&Snapshot> {
|
||||
self.next_snap.as_deref()
|
||||
}
|
||||
@@ -408,6 +414,7 @@ pub fn advance_cls_realtime(mut cls: ResMut<ClientRealtime>, time: Res<Time<Real
|
||||
|
||||
pub fn advance_cg_frame_clock(
|
||||
mut clock: ResMut<FrameClock>,
|
||||
client_clock: Res<ClientClock>,
|
||||
mut prediction: ResMut<ClientPredictionState>,
|
||||
mut join: ResMut<GameJoinCensus>,
|
||||
time: Res<Time>,
|
||||
@@ -427,6 +434,17 @@ pub fn advance_cg_frame_clock(
|
||||
if *role == RuntimeRole::Listen && cgame_active.0 && was_started {
|
||||
let server = authority.as_ref().expect("listen authority clock").time_ms;
|
||||
clock.assign_server_time(server.wrapping_add(fixed.overstep().as_millis() as i32));
|
||||
} else if *role == RuntimeRole::Replay {
|
||||
if cgame_active.0
|
||||
&& let Some(server) = server_time
|
||||
{
|
||||
clock.assign_server_time(
|
||||
server
|
||||
.ms()
|
||||
.wrapping_add(AUTHORITY_MS)
|
||||
.wrapping_add(client_clock.accumulator_ms as i32),
|
||||
);
|
||||
}
|
||||
} else if *role == RuntimeRole::Client {
|
||||
clock.advance_remote(time.delta_secs(), cgame_active.0, server_time);
|
||||
} else {
|
||||
@@ -456,6 +474,7 @@ pub fn receive_ticks(
|
||||
mut link: Option<ResMut<crate::transport::udp_session::UdpClientLink>>,
|
||||
loopback: Option<ResMut<ListenLoopback>>,
|
||||
mut received: ResMut<ReceivedTicks>,
|
||||
adopted: Res<LastAdoptedSnapshot>,
|
||||
mut local: ResMut<LocalPresentClient>,
|
||||
mut prediction: ResMut<ClientPredictionState>,
|
||||
trace: Option<ResMut<ClientPhaseTrace>>,
|
||||
@@ -490,7 +509,9 @@ pub fn receive_ticks(
|
||||
reliable.fail(&error);
|
||||
}
|
||||
for payload in link.take_controls() {
|
||||
reliable.apply(link.assigned_client.unwrap_or(local.0), &payload);
|
||||
reliable.apply(link.assigned_client.unwrap_or(local.0), &payload, |index| {
|
||||
adopted.sound_alias_name(index).is_some()
|
||||
});
|
||||
}
|
||||
note_join_link(&mut watch, link);
|
||||
|
||||
@@ -573,12 +594,15 @@ pub fn reconcile_prediction(
|
||||
)
|
||||
}));
|
||||
}
|
||||
reliable.apply(local.0, &tick.frame.reliable);
|
||||
reliable.apply(local.0, &tick.frame.reliable, |index| {
|
||||
sim::name_in_occupied(&tick.snapshot.meta.sound_aliases, index).is_some()
|
||||
});
|
||||
let ack = tick.ack_for(local.0);
|
||||
if let Some(acked) = ack {
|
||||
pending.clear_acknowledged(acked);
|
||||
}
|
||||
last_adopted.applied_this_frame = true;
|
||||
last_adopted.adopted_at = Some(std::time::Instant::now());
|
||||
if let Some(old_next) = last_adopted.next_snap.take() {
|
||||
last_adopted.snap = Some(old_next);
|
||||
}
|
||||
@@ -675,7 +699,12 @@ impl ReliableInbound<'_> {
|
||||
));
|
||||
}
|
||||
|
||||
fn apply(&mut self, local: sim::ClientId, payload: &crate::ReliablePayload) {
|
||||
fn apply(
|
||||
&mut self,
|
||||
local: sim::ClientId,
|
||||
payload: &crate::ReliablePayload,
|
||||
sound_ready: impl Fn(u8) -> bool,
|
||||
) {
|
||||
if payload.dropped_oldest != 0 {
|
||||
self.fail("ReliableControlOverflow: outcome unknown");
|
||||
return;
|
||||
@@ -717,6 +746,12 @@ impl ReliableInbound<'_> {
|
||||
self.svc.audio.write(crate::SvcScriptAudio(cmd.clone()));
|
||||
}
|
||||
crate::ReliableRow::Sound(cmd) => {
|
||||
// Reliable controls can overtake the snapshot that publishes
|
||||
// this configuration string. Keep the row unacknowledged so
|
||||
// retransmission preserves it until its dependency is ready.
|
||||
if !sound_ready(cmd.index) {
|
||||
break;
|
||||
}
|
||||
self.svc.sound.write(crate::SvcLocalSound {
|
||||
stop: cmd.stop,
|
||||
index: cmd.index,
|
||||
@@ -1783,7 +1818,7 @@ pub fn publish_presented(
|
||||
.map(|(_, ps)| ps);
|
||||
let predicted_ps = prediction.0.predicted_local().copied();
|
||||
let previous = presented.player(local.0).copied();
|
||||
let armed = prediction.0.is_armed();
|
||||
let armed = *role != RuntimeRole::Replay && prediction.0.is_armed();
|
||||
let frame_interpolation = clock.accumulator_ms / AUTHORITY_MS as f32;
|
||||
let interpolation_previous = if *role == RuntimeRole::Replay {
|
||||
last_adopted
|
||||
@@ -1938,6 +1973,14 @@ pub fn publish_presented(
|
||||
);
|
||||
}
|
||||
|
||||
fn release_timeline(
|
||||
mut presented: ResMut<PresentedSnapshot>,
|
||||
mut events: ResMut<Messages<ReliableControlEvent>>,
|
||||
) {
|
||||
presented.clear();
|
||||
events.clear();
|
||||
}
|
||||
|
||||
fn release_client(
|
||||
mut actions: ResMut<ClientActionInbox>,
|
||||
mut presented: ResMut<PresentedSnapshot>,
|
||||
@@ -1964,6 +2007,10 @@ pub fn register_client_runtime(app: &mut App) {
|
||||
.in_set(frame::ScopeSet::Release)
|
||||
.after(crate::signon::release_match_protocol),
|
||||
);
|
||||
app.add_systems(
|
||||
OnExit(frame::Timeline::Running),
|
||||
release_timeline.in_set(frame::ScopeSet::Release),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
enforce_client_work_limits
|
||||
@@ -1975,15 +2022,15 @@ pub fn register_client_runtime(app: &mut App) {
|
||||
.init_resource::<crate::ClientAdmission>()
|
||||
.scoped::<ClientPredictionState>(frame::MatchScope::Live)
|
||||
.scoped::<ClientClock>(frame::MatchScope::Live)
|
||||
.scoped::<FrameClock>(frame::MatchScope::Live)
|
||||
.scoped::<GameActive>(frame::MatchScope::Live)
|
||||
.scoped::<GameJoinCensus>(frame::MatchScope::Live)
|
||||
.scoped::<FrameClock>(frame::Timeline::Running)
|
||||
.scoped::<GameActive>(frame::Timeline::Running)
|
||||
.scoped::<GameJoinCensus>(frame::Timeline::Running)
|
||||
.init_resource::<ClientRealtime>()
|
||||
.scoped::<ReceivedTicks>(frame::MatchScope::Live)
|
||||
.scoped::<LastAdoptedSnapshot>(frame::MatchScope::Live)
|
||||
.scoped::<PresentLocalCensus>(frame::MatchScope::Live)
|
||||
.scoped::<PendingPresentedEntityEvents>(frame::MatchScope::Live)
|
||||
.scoped::<PendingPelletFx>(frame::MatchScope::Live)
|
||||
.scoped::<ReceivedTicks>(frame::Timeline::Running)
|
||||
.scoped::<LastAdoptedSnapshot>(frame::Timeline::Running)
|
||||
.scoped::<PresentLocalCensus>(frame::Timeline::Running)
|
||||
.scoped::<PendingPresentedEntityEvents>(frame::Timeline::Running)
|
||||
.scoped::<PendingPelletFx>(frame::Timeline::Running)
|
||||
.scoped::<PendingClientSends>(frame::MatchScope::Live)
|
||||
.init_resource::<ClientCmdTemplate>()
|
||||
.scoped::<WeaponSelect>(frame::MatchScope::Live)
|
||||
@@ -1993,15 +2040,15 @@ pub fn register_client_runtime(app: &mut App) {
|
||||
.add_message::<ReliableControlEvent>()
|
||||
.scoped_message::<FireCommandVerdicts>(frame::MatchScope::Live)
|
||||
.init_resource::<crate::FireVerdictState>()
|
||||
.scoped::<RemoteProxyState>(frame::MatchScope::Live)
|
||||
.scoped::<RemoteProxyState>(frame::Timeline::Running)
|
||||
.scoped::<ClientShotSamples>(frame::MatchScope::Live)
|
||||
.scoped::<crate::Scoreboard>(frame::MatchScope::Live)
|
||||
.scoped_message::<crate::SvcLocalSound>(frame::MatchScope::Live)
|
||||
.scoped_message::<crate::SvcScriptAudio>(frame::MatchScope::Live)
|
||||
.scoped_message::<crate::SvcCardSlotCmd>(frame::MatchScope::Live)
|
||||
.scoped_message::<crate::SvcOpenMenuCmd>(frame::MatchScope::Live)
|
||||
.scoped_message::<crate::SvcHudSplash>(frame::MatchScope::Live)
|
||||
.scoped_message::<crate::SvcGameNotify>(frame::MatchScope::Live);
|
||||
.scoped::<crate::Scoreboard>(frame::Timeline::Running)
|
||||
.scoped_message::<crate::SvcLocalSound>(frame::Timeline::Running)
|
||||
.scoped_message::<crate::SvcScriptAudio>(frame::Timeline::Running)
|
||||
.scoped_message::<crate::SvcCardSlotCmd>(frame::Timeline::Running)
|
||||
.scoped_message::<crate::SvcOpenMenuCmd>(frame::Timeline::Running)
|
||||
.scoped_message::<crate::SvcHudSplash>(frame::Timeline::Running)
|
||||
.scoped_message::<crate::SvcGameNotify>(frame::Timeline::Running);
|
||||
crate::client::frame_census::register_update_phase_census(app);
|
||||
app.configure_sets(
|
||||
Update,
|
||||
|
||||
@@ -117,7 +117,7 @@ pub use signon::{
|
||||
phase_from_udp_link,
|
||||
};
|
||||
pub use svc_gamenotify::{
|
||||
PendingGameNotify, SVC_DISCONNECT_NOTIFY, SVC_PRINT, SVC_PRINT_BOLD, SvcGameNotify,
|
||||
PendingGameNotify, SVC_CHAT, SVC_DISCONNECT_NOTIFY, SVC_PRINT, SVC_PRINT_BOLD, SvcGameNotify,
|
||||
client_name_string,
|
||||
};
|
||||
pub use svc_playercard::{
|
||||
@@ -174,6 +174,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter};
|
||||
|
||||
pub use svc_script_audio::SvcScriptAudio;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 114;
|
||||
pub const PROTOCOL_VERSION: u32 = 116;
|
||||
|
||||
pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus};
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::transport::wire::{WireError, WireReader, WireWriter};
|
||||
|
||||
pub const SVC_DISCONNECT_NOTIFY: u8 = 0x65;
|
||||
|
||||
pub const SVC_CHAT: u8 = 0x68;
|
||||
|
||||
pub const SVC_PRINT: u8 = 0x66;
|
||||
|
||||
pub const SVC_PRINT_BOLD: u8 = 0x67;
|
||||
@@ -49,7 +51,9 @@ impl PendingGameNotify {
|
||||
|
||||
pub fn adopt_prints(&mut self, effects: &[sim::PendingPrint]) {
|
||||
for print in effects.iter().cloned() {
|
||||
let tag = if print.bold {
|
||||
let tag = if print.chat {
|
||||
SVC_CHAT
|
||||
} else if print.bold {
|
||||
SVC_PRINT_BOLD
|
||||
} else {
|
||||
SVC_PRINT
|
||||
@@ -116,10 +120,11 @@ pub fn decode_svc_game_notifies(
|
||||
for _ in 0..count {
|
||||
let id = input.get_u32()?;
|
||||
let tag = input.get_u8()?;
|
||||
if !matches!(tag, SVC_DISCONNECT_NOTIFY | SVC_PRINT | SVC_PRINT_BOLD) {
|
||||
return Err(WireError::Malformed(
|
||||
"gamenotify tag is not 'e', 'f' or 'g'",
|
||||
));
|
||||
if !matches!(
|
||||
tag,
|
||||
SVC_DISCONNECT_NOTIFY | SVC_PRINT | SVC_PRINT_BOLD | SVC_CHAT
|
||||
) {
|
||||
return Err(WireError::Malformed("unknown game notification tag"));
|
||||
}
|
||||
let mut name = vec![0; input.get_u8()? as usize];
|
||||
input.get_bytes(&mut name)?;
|
||||
|
||||
@@ -119,7 +119,7 @@ impl SnapshotEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SnapshotDecoder {
|
||||
baseline: Vec<(ClientId, PlayerState)>,
|
||||
projectile_baseline: HashMap<ProjectileId, ProjectileState>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::fmt;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::PathBuf;
|
||||
@@ -2690,17 +2690,18 @@ fn apply_peer_event(
|
||||
now_ms: now_ms(),
|
||||
},
|
||||
);
|
||||
// A peer that reported its map and drew no effect is a peer
|
||||
// that will wait out the match in the loading screen. Name the
|
||||
// gate that owes it an offer.
|
||||
if applied.effects.is_empty() {
|
||||
// MapLoaded can arrive before AuthorityReady. The core retains
|
||||
// that report and offers its bootstrap when the host is ready.
|
||||
// Repeated reports from an admitted peer also need no effect.
|
||||
if !applied.accepted {
|
||||
diag::warn!(
|
||||
Net,
|
||||
"peer map loaded admits nothing: epoch={epoch} host_phase={:?} host_match={:?} host_world={:?} peer={:?} — no bootstrap offer will follow",
|
||||
"peer map loaded rejected: epoch={epoch} host_phase={:?} host_match={:?} host_world={:?} peer={:?} transition={}",
|
||||
host_match.phase(),
|
||||
host_match.match_key(),
|
||||
host_match.host_world(),
|
||||
host_match.peer(member_id).map(|peer| peer.phase),
|
||||
applied.transition,
|
||||
);
|
||||
}
|
||||
execute_host_match_effects(
|
||||
@@ -3051,6 +3052,9 @@ async fn gameplay_egress(
|
||||
is_host: bool,
|
||||
) -> std::result::Result<(), TransportFault> {
|
||||
let mut fragmenter = relay_fragmenter();
|
||||
// Quinn reserves bookkeeping space even for an empty datagram buffer.
|
||||
let empty_buffer_space = connection.datagram_send_buffer_space();
|
||||
let mut pending: VecDeque<(MemberId, Vec<u8>)> = VecDeque::new();
|
||||
let mut tick = tokio::time::interval(QUEUE_POLL);
|
||||
let mut sampled = tokio::time::Instant::now();
|
||||
loop {
|
||||
@@ -3066,29 +3070,65 @@ async fn gameplay_egress(
|
||||
stats.path.cwnd,
|
||||
stats.path.lost_packets,
|
||||
stats.path.congestion_events,
|
||||
(DATAGRAM_SEND_BUFFER - connection.datagram_send_buffer_space()) as u64,
|
||||
empty_buffer_space.saturating_sub(connection.datagram_send_buffer_space()) as u64,
|
||||
);
|
||||
}
|
||||
for (member, packet) in mailbox.take_outbound() {
|
||||
let fragments = match fragmenter.split(&packet) {
|
||||
// Gameplay packets carry the newest input/snapshot plus
|
||||
// unacknowledged reliable rows. Replace unsent older state
|
||||
// for this peer rather than building a latency backlog.
|
||||
if let Some((_, previous)) = pending.iter_mut().find(|(id, _)| *id == member) {
|
||||
*previous = packet;
|
||||
} else {
|
||||
pending.push_back((member, packet));
|
||||
}
|
||||
}
|
||||
while let Some((member, packet)) = pending.front() {
|
||||
let fragments = match fragmenter.split(packet) {
|
||||
Ok(fragments) => fragments,
|
||||
Err(error) => {
|
||||
diag::warn!(Net, "master {role} packet not splittable: {error}");
|
||||
pending.pop_front();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for fragment in fragments {
|
||||
let mut datagrams = Vec::with_capacity(fragments.len());
|
||||
for fragment in &fragments {
|
||||
let datagram = if is_host {
|
||||
RelayDatagram::HostToMember {
|
||||
member_id: member,
|
||||
payload: &fragment,
|
||||
member_id: *member,
|
||||
payload: fragment,
|
||||
}
|
||||
} else {
|
||||
RelayDatagram::ClientToHost(&fragment)
|
||||
RelayDatagram::ClientToHost(fragment)
|
||||
};
|
||||
let bytes = encode_relay(datagram).map_err(|error| {
|
||||
datagrams.push(encode_relay(datagram).map_err(|error| {
|
||||
TransportFault::with_connection("encode_relay", role, error, &connection)
|
||||
})?;
|
||||
})?);
|
||||
}
|
||||
let wire_bytes = datagrams.iter().map(Vec::len).sum::<usize>();
|
||||
if wire_bytes > DATAGRAM_SEND_BUFFER {
|
||||
return Err(TransportFault::with_connection(
|
||||
"send_datagram",
|
||||
role,
|
||||
"fragmented gameplay packet exceeds the send buffer",
|
||||
&connection,
|
||||
));
|
||||
}
|
||||
let free = connection.datagram_send_buffer_space();
|
||||
let queued = empty_buffer_space.saturating_sub(free);
|
||||
let window = connection.stats().path.cwnd as usize;
|
||||
// Queue complete packets together. Quinn discards oldest
|
||||
// datagrams when its buffer fills, which can otherwise leave
|
||||
// every fragmented snapshot incomplete. One packet may exceed
|
||||
// the congestion window, but never enqueue a history behind it.
|
||||
if wire_bytes > free
|
||||
|| (queued != 0 && queued.saturating_add(wire_bytes) > window)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pending.pop_front();
|
||||
for bytes in datagrams {
|
||||
if let Err(error) = connection.send_datagram(bytes.into()) {
|
||||
return Err(TransportFault::with_connection(
|
||||
"send_datagram",
|
||||
@@ -3200,6 +3240,7 @@ async fn connect(
|
||||
quinn::IdleTimeout::try_from(SESSION_IDLE).expect("session idle fits QUIC VarInt"),
|
||||
));
|
||||
transport.keep_alive_interval(Some(SESSION_KEEP_ALIVE));
|
||||
transport.datagram_send_buffer_size(DATAGRAM_SEND_BUFFER);
|
||||
transport.max_concurrent_uni_streams(MAX_RELAY_UNI_STREAMS.into());
|
||||
client_config.transport_config(Arc::new(transport));
|
||||
|
||||
|
||||
@@ -178,6 +178,16 @@ pub(crate) fn encode_action(out: &mut WireWriter, action: &ClientAction) {
|
||||
out.put_u32(request_id);
|
||||
out.put_bytes(&name);
|
||||
}
|
||||
ClientAction::Chat {
|
||||
request_id,
|
||||
team_only,
|
||||
text,
|
||||
} => {
|
||||
out.put_u8(24);
|
||||
out.put_u32(request_id);
|
||||
out.put_u8(u8::from(team_only));
|
||||
out.put_bytes(&text);
|
||||
}
|
||||
ClientAction::UseCopycat { request_id } => {
|
||||
out.put_u8(13);
|
||||
out.put_u32(request_id);
|
||||
@@ -293,6 +303,21 @@ pub(crate) fn decode_action(input: &mut WireReader<'_>) -> Result<ClientAction,
|
||||
killstreaks: [input.get_u32()?, input.get_u32()?, input.get_u32()?],
|
||||
},
|
||||
}),
|
||||
24 => {
|
||||
let request_id = input.get_u32()?;
|
||||
let team_only = match input.get_u8()? {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => return Err(WireError::Malformed("invalid chat audience")),
|
||||
};
|
||||
let mut text = [0; sim::chat::CHAT_BYTES];
|
||||
input.get_bytes(&mut text)?;
|
||||
Ok(ClientAction::Chat {
|
||||
request_id,
|
||||
team_only,
|
||||
text,
|
||||
})
|
||||
}
|
||||
13 => Ok(ClientAction::UseCopycat {
|
||||
request_id: input.get_u32()?,
|
||||
}),
|
||||
@@ -2623,6 +2648,14 @@ fn encode_entity_dobjs(
|
||||
out.put_u8(u8::from(model.ignore_collision));
|
||||
}
|
||||
out.put_u32(state.pose_revision);
|
||||
debug_assert!(state.tag_rotations.len() <= u16::MAX as usize);
|
||||
out.put_u16(state.tag_rotations.len() as u16);
|
||||
for rotation in &state.tag_rotations {
|
||||
put_text(out, &rotation.tag);
|
||||
for angle in rotation.angles {
|
||||
out.put_f32(angle);
|
||||
}
|
||||
}
|
||||
put_optional_part_bits(out, state.requested_parts);
|
||||
for word in state.hide_part_bits.words() {
|
||||
out.put_u32(*word);
|
||||
@@ -2685,6 +2718,19 @@ fn decode_entity_dobjs(
|
||||
});
|
||||
}
|
||||
let pose_revision = input.get_u32()?;
|
||||
let rotation_count = input.get_u16()? as usize;
|
||||
if rotation_count > anim_iw4::PartBits::CAPACITY {
|
||||
return Err(WireError::Malformed("too many tag rotations"));
|
||||
}
|
||||
let mut tag_rotations = Vec::with_capacity(rotation_count);
|
||||
for _ in 0..rotation_count {
|
||||
let tag = get_text(input)?;
|
||||
let angles = [input.get_f32()?, input.get_f32()?, input.get_f32()?];
|
||||
if tag.is_empty() || !angles.iter().all(|angle| angle.is_finite()) {
|
||||
return Err(WireError::Malformed("invalid tag rotation"));
|
||||
}
|
||||
tag_rotations.push(xmodel_runtime::TagRotation { tag, angles });
|
||||
}
|
||||
let requested_parts = get_optional_part_bits(input)?;
|
||||
let mut hide_words = [0u32; anim_iw4::PartBits::WORDS];
|
||||
for word in &mut hide_words {
|
||||
@@ -2746,6 +2792,7 @@ fn decode_entity_dobjs(
|
||||
models,
|
||||
},
|
||||
pose_revision,
|
||||
tag_rotations,
|
||||
tree,
|
||||
requested_parts,
|
||||
hide_part_bits: xmodel_runtime::HidePartBits::from_words(hide_words),
|
||||
|
||||
@@ -208,6 +208,52 @@ struct PeerReplicationState {
|
||||
last_control_seq: Option<u16>,
|
||||
control_drops_sent: u32,
|
||||
account: Option<PeerAccount>,
|
||||
pace: SnapshotPace,
|
||||
held_ack: Option<(ClientId, CmdSeq)>,
|
||||
}
|
||||
|
||||
const SNAPSHOT_RATE_BYTES_PER_SEC: i64 = 25_000;
|
||||
const SNAPSHOT_PACKET_OVERHEAD: i64 = 48;
|
||||
const SNAPSHOT_CREDIT_PER_TICK: i64 =
|
||||
SNAPSHOT_RATE_BYTES_PER_SEC * crate::authority::inbox::AUTHORITY_MS as i64 / 1000;
|
||||
const STALE_BASELINE_SNAPSHOTS: u32 = 29;
|
||||
const MAX_STALE_BACKOFF: u32 = 4;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SnapshotPace {
|
||||
credit: i64,
|
||||
refilled: Option<Tick>,
|
||||
backoff: u32,
|
||||
stale_until: Option<Tick>,
|
||||
}
|
||||
|
||||
impl SnapshotPace {
|
||||
fn ready(&mut self, tick: Tick, unacked: Option<u32>) -> bool {
|
||||
let elapsed = self
|
||||
.refilled
|
||||
.map_or(1, |last| i64::from(tick.0.saturating_sub(last.0)));
|
||||
self.refilled = Some(tick);
|
||||
self.credit =
|
||||
(self.credit + elapsed * SNAPSHOT_CREDIT_PER_TICK).min(SNAPSHOT_CREDIT_PER_TICK);
|
||||
if self.credit <= 0 {
|
||||
return false;
|
||||
}
|
||||
if unacked.is_none_or(|age| age < STALE_BASELINE_SNAPSHOTS) {
|
||||
self.backoff = 0;
|
||||
self.stale_until = None;
|
||||
return true;
|
||||
}
|
||||
if self.stale_until.is_some_and(|until| tick.0 < until.0) {
|
||||
return false;
|
||||
}
|
||||
self.stale_until = Some(Tick(tick.0.saturating_add(1 << self.backoff)));
|
||||
self.backoff = (self.backoff + 1).min(MAX_STALE_BACKOFF);
|
||||
true
|
||||
}
|
||||
|
||||
fn charge(&mut self, bytes: usize) {
|
||||
self.credit -= bytes as i64 + SNAPSHOT_PACKET_OVERHEAD;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -240,6 +286,8 @@ impl PeerReplicationState {
|
||||
last_control_seq: None,
|
||||
control_drops_sent: 0,
|
||||
account: None,
|
||||
pace: SnapshotPace::default(),
|
||||
held_ack: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,6 +1018,17 @@ impl UdpAuthorityHub {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(ack) = acks.iter().rev().find(|(id, _)| *id == client) {
|
||||
peer.held_ack = Some(*ack);
|
||||
}
|
||||
let via_bootstrap = baseline_seq == 0
|
||||
&& self.bootstrap.is_some()
|
||||
&& !matches!(peer.admission, PeerAdmission::Committed { .. });
|
||||
let unacked =
|
||||
(baseline_seq != 0).then(|| peer.next_snap_seq.wrapping_sub(baseline_seq));
|
||||
if !via_bootstrap && !peer.pace.ready(snapshot.tick, unacked) {
|
||||
continue;
|
||||
}
|
||||
let mut encoder = std::mem::take(&mut peer.encoder);
|
||||
if baseline_seq == 0 {
|
||||
peer.last_full = Some(snapshot.tick);
|
||||
@@ -977,11 +1036,7 @@ impl UdpAuthorityHub {
|
||||
} else if let Some(baseline) = peer.baseline.baseline_for_encode() {
|
||||
encoder.adopt_baseline(baseline);
|
||||
}
|
||||
let peer_acks: Vec<(ClientId, CmdSeq)> = acks
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|(id, _)| *id == client)
|
||||
.collect();
|
||||
let peer_acks: Vec<(ClientId, CmdSeq)> = peer.held_ack.take().into_iter().collect();
|
||||
let (peer_snap, meta) = match for_peer(client, snapshot) {
|
||||
Some(own) => {
|
||||
let meta = Arc::new(WireMeta::without_reliable_events(&own.meta));
|
||||
@@ -1116,6 +1171,7 @@ impl UdpAuthorityHub {
|
||||
if let Err(e) = self.send_outgoing(target, &bytes) {
|
||||
last_err = Some(e);
|
||||
} else if let Some(peer) = self.replication.get_mut(&conn) {
|
||||
peer.pace.charge(bytes.len());
|
||||
peer.sent_ticks.push_back(snapshot.tick);
|
||||
while peer.sent_ticks.len() > 32 {
|
||||
peer.sent_ticks.pop_front();
|
||||
|
||||
@@ -39,7 +39,7 @@ impl WorldObjectSyncEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct WorldObjectSyncDecoder {
|
||||
state: WorldObjectSnapshot,
|
||||
}
|
||||
|
||||
@@ -389,10 +389,6 @@ pub fn seal_render_frame(
|
||||
1u32,
|
||||
),
|
||||
};
|
||||
let xmodel_packed_segments = xmodel
|
||||
.as_ref()
|
||||
.map(|plan| plan.packed_segments)
|
||||
.unwrap_or_default();
|
||||
let xmodel_revision = xmodel.as_ref().map(|plan| plan.revision).unwrap_or(0);
|
||||
let xmodel_topology_revision = xmodel
|
||||
.as_ref()
|
||||
@@ -615,7 +611,6 @@ pub fn seal_render_frame(
|
||||
fx_revision,
|
||||
xmodel_revision,
|
||||
xmodel_topology_revision,
|
||||
xmodel_packed_segments,
|
||||
particle_cloud_vertices,
|
||||
particle_cloud_indices,
|
||||
particle_cloud_surface_ranges,
|
||||
|
||||
@@ -499,9 +499,7 @@ impl FpvPreparationJob {
|
||||
|| camo.skel.surface_vertex_ranges != base.skel.surface_vertex_ranges
|
||||
|| camo.skel.surface_index_ranges != base.skel.surface_index_ranges
|
||||
|| camo.skel.positions != base.skel.positions
|
||||
|| camo.skel.normals != base.skel.normals
|
||||
|| camo.skel.uvs != base.skel.uvs
|
||||
|| camo.skel.colors != base.skel.colors
|
||||
|| !camo.skel.has_same_vertex_attributes(&base.skel)
|
||||
|| camo.skel.indices != base.skel.indices
|
||||
{
|
||||
return Err("appearance topology mismatch".to_owned());
|
||||
|
||||
@@ -90,10 +90,12 @@ fn blend_skel_vertices_inner<const DECODED: bool>(
|
||||
) -> BlendedSkel {
|
||||
let (active_vert, rigid_vert) =
|
||||
vertex_stream_masks(skel, lod, surface_is_visible, surface_rigid);
|
||||
let mut positions = DECODED.then(|| skel.positions.clone()).unwrap_or_default();
|
||||
let mut normals = DECODED.then(|| skel.normals.clone()).unwrap_or_default();
|
||||
let mut positions = DECODED.then(|| skel.positions.to_vec()).unwrap_or_default();
|
||||
let mut normals = DECODED
|
||||
.then(|| skel.normals().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
let mut posed_packed =
|
||||
(skel.packed_vertices.len() == skel.positions.len()).then(|| skel.packed_vertices.clone());
|
||||
(skel.packed_vertices.len() == skel.positions.len()).then(|| skel.packed_vertices.to_vec());
|
||||
let max_bone = skel
|
||||
.vert_skin
|
||||
.iter()
|
||||
@@ -102,7 +104,7 @@ fn blend_skel_vertices_inner<const DECODED: bool>(
|
||||
.flat_map(|(skin, _)| {
|
||||
std::iter::once(skin.bones[0]).chain(
|
||||
(1..4)
|
||||
.filter(|&extra| skin.weights[extra] > 0.0)
|
||||
.filter(|&extra| skin.has_extra_weight(extra))
|
||||
.map(|extra| skin.bones[extra]),
|
||||
)
|
||||
})
|
||||
@@ -120,7 +122,7 @@ fn blend_skel_vertices_inner<const DECODED: bool>(
|
||||
continue;
|
||||
}
|
||||
let p = Vec3::from_array(skel.positions[i]);
|
||||
let n = DECODED.then(|| Vec3::from_array(skel.normals[i]));
|
||||
let n = DECODED.then(|| Vec3::from_array(normals[i]));
|
||||
|
||||
let (mut posed_position, mut posed_normal, primary_basis) =
|
||||
if rigid_vert.get(i).copied().unwrap_or(false) {
|
||||
@@ -135,12 +137,12 @@ fn blend_skel_vertices_inner<const DECODED: bool>(
|
||||
let mut extras = [(primary_esi, 0u16); 3];
|
||||
let mut extra_n = 0usize;
|
||||
for extra in 1..4 {
|
||||
if skin.weights[extra] <= 0.0 {
|
||||
if !skin.has_extra_weight(extra) {
|
||||
continue;
|
||||
}
|
||||
extras[extra_n] = (
|
||||
&bone_cache[usize::from(skin.bones[extra])].1,
|
||||
skin.weight_u16[extra],
|
||||
skin.extra_weight(extra),
|
||||
);
|
||||
extra_n += 1;
|
||||
}
|
||||
@@ -154,7 +156,7 @@ fn blend_skel_vertices_inner<const DECODED: bool>(
|
||||
*primary,
|
||||
)
|
||||
};
|
||||
if posed_position == Vec3::ZERO && skin.weights[0] == 0.0 {
|
||||
if posed_position == Vec3::ZERO && skin.primary_is_zero() {
|
||||
posed_position = p;
|
||||
posed_normal = n;
|
||||
}
|
||||
@@ -247,9 +249,9 @@ pub(crate) fn meshes_from_blended(
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, skel.uvs.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, skel.colors.clone());
|
||||
mesh.insert_indices(Indices::U32(skel.indices.clone()));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, skel.uvs().collect::<Vec<_>>());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, skel.colors().collect::<Vec<_>>());
|
||||
mesh.insert_indices(Indices::U32(skel.indices.to_vec()));
|
||||
return Some(vec![PosedModelSurface {
|
||||
surface_index: 0,
|
||||
mesh,
|
||||
@@ -290,8 +292,7 @@ pub(crate) fn meshes_from_blended(
|
||||
let vertex_end = first_vertex.checked_add(vertex_count)?;
|
||||
let index_end = index_start.checked_add(index_count)?;
|
||||
if vertex_end > positions.len()
|
||||
|| vertex_end > skel.uvs.len()
|
||||
|| vertex_end > skel.colors.len()
|
||||
|| vertex_end > skel.packed_vertices.len()
|
||||
|| vertex_end > normals.len()
|
||||
|| index_end > skel.indices.len()
|
||||
{
|
||||
@@ -328,11 +329,17 @@ pub(crate) fn meshes_from_blended(
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_UV_0,
|
||||
skel.uvs[first_vertex..vertex_end].to_vec(),
|
||||
skel.uvs()
|
||||
.skip(first_vertex)
|
||||
.take(vertex_count)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_COLOR,
|
||||
skel.colors[first_vertex..vertex_end].to_vec(),
|
||||
skel.colors()
|
||||
.skip(first_vertex)
|
||||
.take(vertex_count)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_indices(Indices::U32(local_indices));
|
||||
let packed_vertices = posed_packed
|
||||
@@ -380,7 +387,7 @@ pub fn build_skin_layout(
|
||||
return Some(SkinLayout {
|
||||
surfaces: Vec::new(),
|
||||
dest_vertex_n: skel.packed_vertices.len(),
|
||||
indices: skel.indices.clone(),
|
||||
indices: skel.indices.to_vec(),
|
||||
});
|
||||
}
|
||||
if skel.surface_index_ranges.len() != surface_count {
|
||||
@@ -459,7 +466,7 @@ pub fn skin_packed_into(
|
||||
.flat_map(|(skin, _)| {
|
||||
std::iter::once(skin.bones[0]).chain(
|
||||
(1..4)
|
||||
.filter(|&extra| skin.weights[extra] > 0.0)
|
||||
.filter(|&extra| skin.has_extra_weight(extra))
|
||||
.map(|extra| skin.bones[extra]),
|
||||
)
|
||||
})
|
||||
@@ -495,12 +502,12 @@ pub fn skin_packed_into(
|
||||
let mut extras = [(primary_esi, 0u16); 3];
|
||||
let mut extra_n = 0usize;
|
||||
for extra in 1..4 {
|
||||
if skin.weights[extra] <= 0.0 {
|
||||
if !skin.has_extra_weight(extra) {
|
||||
continue;
|
||||
}
|
||||
extras[extra_n] = (
|
||||
&bone_cache[usize::from(skin.bones[extra])].1,
|
||||
skin.weight_u16[extra],
|
||||
skin.extra_weight(extra),
|
||||
);
|
||||
extra_n += 1;
|
||||
}
|
||||
@@ -513,7 +520,7 @@ pub fn skin_packed_into(
|
||||
*primary,
|
||||
)
|
||||
};
|
||||
let posed_position = if posed_position == Vec3::ZERO && skin.weights[0] == 0.0 {
|
||||
let posed_position = if posed_position == Vec3::ZERO && skin.primary_is_zero() {
|
||||
p
|
||||
} else {
|
||||
posed_position
|
||||
@@ -546,7 +553,7 @@ pub fn smodel_surfaces_from_blended(
|
||||
return Some(vec![PosedSmodelSurface {
|
||||
surface_index: 0,
|
||||
vert_n: vertex_n,
|
||||
indices: skel.indices.clone(),
|
||||
indices: skel.indices.to_vec(),
|
||||
packed_vertices: posed_packed.unwrap_or_default(),
|
||||
}]);
|
||||
}
|
||||
@@ -567,8 +574,7 @@ pub fn smodel_surfaces_from_blended(
|
||||
let vertex_end = first_vertex.checked_add(vertex_count)?;
|
||||
let index_end = index_start.checked_add(index_count)?;
|
||||
if vertex_end > vertex_n
|
||||
|| vertex_end > skel.uvs.len()
|
||||
|| vertex_end > skel.colors.len()
|
||||
|| vertex_end > skel.packed_vertices.len()
|
||||
|| index_end > skel.indices.len()
|
||||
{
|
||||
return None;
|
||||
|
||||
@@ -1273,10 +1273,10 @@ pub fn register_fpv_present_systems(app: &mut App) {
|
||||
.scoped::<SessionViewmodel>(frame::MatchScope::Live)
|
||||
.scoped::<PreparedFpv>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::model_materials::PreparedModelMaterials>(frame::MatchScope::Live)
|
||||
.scoped::<SessionViewKick>(frame::MatchScope::Live)
|
||||
.scoped::<SessionViewKick>(frame::Timeline::Running)
|
||||
.scoped::<GunOffset>(frame::MatchScope::Live)
|
||||
.scoped::<ViewweaponAim>(frame::MatchScope::Live)
|
||||
.scoped::<PendingViewHurt>(frame::MatchScope::Live)
|
||||
.scoped::<PendingViewHurt>(frame::Timeline::Running)
|
||||
.scoped::<FpvStatusGap>(frame::MatchScope::Live)
|
||||
.init_app::<RenderPresentationGaps>()
|
||||
.add_systems(
|
||||
|
||||
@@ -76,7 +76,7 @@ struct ScriptModelPoseLocals<'w, 's> {
|
||||
remap: Local<'s, Vec<Option<usize>>>,
|
||||
lod_skinned: Res<'w, render_scene::LodRampSkinnedDvar>,
|
||||
gfx_scene: Res<'w, HostGfxScene>,
|
||||
sun_casters: Res<'w, render_scene::SunShadowCasterVolume>,
|
||||
sun_casters: Res<'w, render_scene::PreparedSunShadowFrame>,
|
||||
}
|
||||
|
||||
#[derive(SystemParam)]
|
||||
@@ -636,9 +636,6 @@ fn pose_script_models(
|
||||
let entnum = owner.gentity_number.map(u32::from);
|
||||
|
||||
let slot = entnum.map(|entnum| script_scene_slot(&gfx_scene.scene, entnum));
|
||||
// Out of the camera's walk, a model can still shade what the camera sees (laundry
|
||||
// above the view on the wall below it): it stays, for the sun shadow only, while it is
|
||||
// inside a sun partition.
|
||||
let walk_hidden = entnum.is_some_and(|entnum| gfx_scene.scene.scene_ent_hidden(entnum));
|
||||
if let Some(why) = slot.as_ref().and_then(|slot| slot.skip)
|
||||
&& !walk_hidden
|
||||
|
||||
@@ -17,6 +17,12 @@ pub fn presented_is_third_person(
|
||||
in_killcam: bool,
|
||||
third_person: bool,
|
||||
) -> bool {
|
||||
if presented.external_camera() {
|
||||
return true;
|
||||
}
|
||||
if presented.first_person_camera() {
|
||||
return false;
|
||||
}
|
||||
let Some(ps) = presented.player(local) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -230,6 +230,27 @@ pub fn sync_camera_from_presented(
|
||||
death_cam_clip: Res<crate::occupancy::dyn_ent::DynEntPhysClip>,
|
||||
) {
|
||||
kick.killcam_focus_distance = None;
|
||||
if let Some(view) = sim_cam.view_override {
|
||||
for mut transform in &mut q {
|
||||
*transform = view.transform;
|
||||
}
|
||||
for mut projection in &mut lenses {
|
||||
if let Projection::Perspective(perspective) = &mut *projection {
|
||||
perspective.fov = horizontal_to_vertical_fov_deg(view.horizontal_fov).to_radians();
|
||||
}
|
||||
}
|
||||
kick.horiz_fov_deg = view.horizontal_fov;
|
||||
kick.refdef_vieworg = view.transform.translation.to_array();
|
||||
let forward = *view.transform.forward();
|
||||
let right = *view.transform.right();
|
||||
let up = *view.transform.up();
|
||||
kick.refdef_view_angles = [
|
||||
-forward.z.clamp(-1.0, 1.0).asin().to_degrees(),
|
||||
forward.y.atan2(forward.x).to_degrees(),
|
||||
(-right.z).atan2(up.z).to_degrees(),
|
||||
];
|
||||
return;
|
||||
}
|
||||
if !sim_cam.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ pub struct RenderAnimPlugin;
|
||||
impl Plugin for RenderAnimPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.scoped::<crate::anim::scene_submission::AnimDObjSceneSkels>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::remote_body::RemoteBodyTrees>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::remote_body::RemoteSkinPoseHashes>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::remote_body::RemoteBodySkinnedQueue>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::fpv_host::FpvPresentCursor>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::fpv_host::PendingFpvSpawn>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::fpv_host::PendingFpvNotetracks>(frame::MatchScope::Live)
|
||||
@@ -17,7 +14,6 @@ impl Plugin for RenderAnimPlugin {
|
||||
.scoped::<crate::anim::fpv_host::FpvBoltTargets>(frame::MatchScope::Live)
|
||||
.scoped::<crate::anim::fpv_host::FpvPoseProduct>(frame::MatchScope::Live)
|
||||
.scoped::<crate::draw::FpvDrawPlan>(frame::MatchScope::Live)
|
||||
.scoped::<crate::draw::RemoteBodyDrawPlan>(frame::MatchScope::Live)
|
||||
.scoped::<crate::draw::ScriptModelDrawPlan>(frame::MatchScope::Live)
|
||||
.scoped::<crate::draw::MissileDrawPlan>(frame::MatchScope::Live)
|
||||
.scoped::<crate::draw::ItemDrawPlan>(frame::MatchScope::Live)
|
||||
|
||||
@@ -550,6 +550,20 @@ fn overlay_draw_obj_only(
|
||||
);
|
||||
}
|
||||
}
|
||||
RetainedDrawKind::Glass {
|
||||
lighting_handle, ..
|
||||
}
|
||||
| RetainedDrawKind::MarkMesh {
|
||||
lighting_handle, ..
|
||||
} => {
|
||||
overlay_smodel_tess_code_constants(
|
||||
scratch,
|
||||
lighting_handle,
|
||||
inv_image_height,
|
||||
None,
|
||||
need,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||