Compare commits
25 Commits
advanced-a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
102dd7fa6f
|
|||
|
f8996c958c
|
|||
|
f91fcf6b6f
|
|||
|
4593514954
|
|||
|
31a3e31e70
|
|||
|
4873e0298c
|
|||
|
637fb38131
|
|||
|
ae624f90dc
|
|||
|
1d17e6acf0
|
|||
|
a53cf8a8c7
|
|||
|
9007de1a2d
|
|||
|
6df057de17
|
|||
|
4fe2eed922
|
|||
|
e83d0e5ff9
|
|||
|
4587d8161d
|
|||
|
7b56dacb73
|
|||
|
f19e846e0f
|
|||
|
6240b0ae86
|
|||
|
8d1ec94ac2
|
|||
|
f82d860822
|
|||
|
c8eb2f7878
|
|||
|
66cb1fc5ff
|
|||
|
0cb0f6a423
|
|||
|
890e5c1905
|
|||
|
3d8b5a0dfe
|
353
Cargo.lock
generated
353
Cargo.lock
generated
@@ -73,6 +73,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "array-init"
|
||||
version = "2.1.0"
|
||||
@@ -169,6 +219,12 @@ version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@@ -284,6 +340,46 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.12.0"
|
||||
@@ -295,6 +391,12 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -405,6 +507,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@@ -531,6 +644,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "four-cc"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "795cbfc56d419a7ce47ccbb7504dd9a5b7c484c083c356e797de08bd988d9629"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
@@ -649,6 +768,19 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h264-reader"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "036a78b2620d92f0ec57690bc792b3bb87348632ee5225302ba2e66a48021c6c"
|
||||
dependencies = [
|
||||
"bitstream-io",
|
||||
"hex-slice",
|
||||
"log",
|
||||
"memchr",
|
||||
"rfc6381-codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
@@ -681,12 +813,24 @@ dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex-slice"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5491a308e0214554f07a81d8944abe45f552871c12e3c3c6e7e5d354039a6c4c"
|
||||
|
||||
[[package]]
|
||||
name = "hexf-parse"
|
||||
version = "0.2.1"
|
||||
@@ -714,6 +858,21 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "integration-tests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"strafesnet_common",
|
||||
"strafesnet_roblox_bot_file",
|
||||
"strafesnet_roblox_bot_player",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -723,6 +882,12 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -899,6 +1064,35 @@ dependencies = [
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mp4"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"num-rational",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mp4ra-rust"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdbc3d3867085d66ac6270482e66f3dd2c5a18451a3dc9ad7269e94844a536b7"
|
||||
dependencies = [
|
||||
"four-cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpeg4-audio-const"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a1fe2275b68991faded2c80aa4a33dba398b77d276038b8f50701a22e55918"
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "28.0.0"
|
||||
@@ -919,7 +1113,7 @@ dependencies = [
|
||||
"log",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-ident",
|
||||
@@ -955,6 +1149,37 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1205,6 +1430,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.50"
|
||||
@@ -1452,12 +1683,28 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "rfc6381-codec"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed54c20f5c3ec82eab6d998b313dc75ec5d5650d4f57675e61d72489040297fd"
|
||||
dependencies = [
|
||||
"mp4ra-rust",
|
||||
"mpeg4-audio-const",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
@@ -1554,6 +1801,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1632,9 +1892,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_common"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
|
||||
checksum = "fb31424f16d189979d9f5781067ff29169a258c11da6ff46a4196bffd96d61dc"
|
||||
checksum = "ac4eb613a8d86986b61aa6b52bd74ef605d370c149778fe96cfab16dc4377636"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 2.11.0",
|
||||
@@ -1647,9 +1907,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_graphics"
|
||||
version = "0.0.5"
|
||||
version = "0.0.7"
|
||||
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
|
||||
checksum = "786feeee41d2a65707eea824ad928f1dad5be3fc82e80d541bb36e45fda1c9d6"
|
||||
checksum = "5b0afe15871f8205f3387dd5816e797c0576429d3013a33a3daed293bcaeedef"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"ddsfile",
|
||||
@@ -1672,7 +1932,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strafesnet_roblox_bot_player"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"glam",
|
||||
"strafesnet_common",
|
||||
@@ -1729,6 +1989,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -1862,14 +2128,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
@@ -1895,12 +2176,64 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "video-encoder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"glam",
|
||||
"mp4",
|
||||
"strafesnet_common",
|
||||
"strafesnet_graphics",
|
||||
"strafesnet_roblox_bot_file",
|
||||
"strafesnet_roblox_bot_player",
|
||||
"strafesnet_snf",
|
||||
"vk-video",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vk-mem"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cb12b79bcec57a3334d0284f1364c1846f378bb47df9779c6dbfcfc245c9404"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"bitflags 2.11.0",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vk-video"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b2031523b3ed32d99d650ace95b70606bba44fc7a1178ba7bbbe1c17fa0a2b"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"derivative",
|
||||
"h264-reader",
|
||||
"memchr",
|
||||
"rustc-hash 2.1.1",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"vk-mem",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -2160,7 +2493,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"wgpu-core-deps-apple",
|
||||
@@ -2693,3 +3026,9 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -1,7 +1,9 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"integration-tests",
|
||||
"lib",
|
||||
"native-player",
|
||||
"video-encoder",
|
||||
"wasm-module"
|
||||
]
|
||||
resolver = "3"
|
||||
@@ -13,8 +15,16 @@ codegen-units = 1
|
||||
|
||||
[workspace.dependencies]
|
||||
glam = "0.32.0"
|
||||
wgpu = "28.0.0"
|
||||
|
||||
strafesnet_common = { version = "0.8.6", registry = "strafesnet" }
|
||||
strafesnet_graphics = { version = "0.0.5", registry = "strafesnet" }
|
||||
strafesnet_graphics = { version = "0.0.7", registry = "strafesnet" }
|
||||
strafesnet_roblox_bot_file = { version = "0.9.3", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.3.2", registry = "strafesnet" }
|
||||
|
||||
strafesnet_roblox_bot_player = { version = "0.2.0", path = "lib" }
|
||||
|
||||
# strafesnet_common = { path = "../strafe-project/lib/common" }
|
||||
# strafesnet_graphics = { path = "../strafe-project/engine/graphics" }
|
||||
# strafesnet_roblox_bot_file = { path = "../roblox_bot_file" }
|
||||
# strafesnet_snf = { path = "../strafe-project/lib/snf" }
|
||||
|
||||
9
integration-tests/Cargo.toml
Normal file
9
integration-tests/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "integration-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_roblox_bot_file.workspace = true
|
||||
strafesnet_roblox_bot_player.workspace = true
|
||||
25
integration-tests/src/main.rs
Normal file
25
integration-tests/src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
use strafesnet_roblox_bot_player::{bot,bvh,head};
|
||||
use head::Time as PlaybackTime;
|
||||
use strafesnet_common::session::Time as SessionTime;
|
||||
|
||||
fn main(){
|
||||
let bot=include_bytes!("../../web-demo/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d.qbot");
|
||||
let timelines=v0::read_all_to_block(std::io::Cursor::new(bot)).unwrap();
|
||||
let bot=bot::CompleteBot::new(timelines);
|
||||
let bvh=bvh::Bvh::new(&bot);
|
||||
|
||||
// sample the position at 0.24s
|
||||
let mut playback0=head::PlaybackHead::new(&bot,SessionTime::ZERO);
|
||||
for i in 0..10{
|
||||
let sample_time=PlaybackTime::from_millis(6543+1*i);
|
||||
playback0.set_time(&bot,SessionTime::ZERO,sample_time);
|
||||
let pos=playback0.get_position(&bot,SessionTime::ZERO);
|
||||
|
||||
// get the closest time on the timeline (convert to PlaybackTime which starts at 0)
|
||||
let closest_time=bot.playback_time(bvh.closest_time_to_point(&bot,pos).unwrap());
|
||||
println!("time={sample_time} closest_time={closest_time}");
|
||||
}
|
||||
// let mut playback1=head::PlaybackHead::new(&bot,SessionTime::ZERO);
|
||||
// playback1.set_time(&bot,SessionTime::ZERO,sample_time);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "strafesnet_roblox_bot_player"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
glam.workspace = true
|
||||
wgpu.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_graphics.workspace = true
|
||||
strafesnet_roblox_bot_file.workspace = true
|
||||
wgpu = "28.0.0"
|
||||
|
||||
@@ -34,6 +34,10 @@ impl CompleteBot{
|
||||
pub fn time(&self,time:PlaybackTime)->PhysicsTime{
|
||||
self.timer.time(time)
|
||||
}
|
||||
pub fn playback_time(&self,time:PhysicsTime)->PlaybackTime{
|
||||
use strafesnet_common::timer::TimerState;
|
||||
time.coerce()-self.timer.clone().into_state().get_offset().coerce()
|
||||
}
|
||||
pub const fn duration(&self)->PhysicsTime{
|
||||
self.duration
|
||||
}
|
||||
|
||||
162
lib/src/bvh.rs
Normal file
162
lib/src/bvh.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use core::ops::Range;
|
||||
use strafesnet_common::aabb::Aabb;
|
||||
use strafesnet_common::bvh::generate_bvh;
|
||||
use strafesnet_common::integer::vec3;
|
||||
use strafesnet_common::integer::{Fixed,Planar64};
|
||||
use strafesnet_common::physics::Time as PhysicsTime;
|
||||
use crate::bot::CompleteBot;
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
|
||||
const MAX_SLICE_LEN:usize=16;
|
||||
struct EventSlice{
|
||||
slice:Range<usize>,
|
||||
inclusive:bool,
|
||||
}
|
||||
|
||||
pub struct Bvh{
|
||||
bvh:strafesnet_common::bvh::BvhNode<EventSlice>,
|
||||
}
|
||||
|
||||
impl Bvh{
|
||||
pub fn new(bot:&CompleteBot)->Self{
|
||||
let output_events=&bot.timelines().output_events;
|
||||
// iterator over the event timeline and capture slices of contiguous output events.
|
||||
// create an Aabb for each slice and then generate a BVH.
|
||||
let mut bvh_nodes=Vec::new();
|
||||
let it=output_events
|
||||
.array_windows()
|
||||
.enumerate()
|
||||
// find discontinuities
|
||||
.filter(|&(_,[event0,event1])|
|
||||
event0.time==event1.time&&!(
|
||||
event0.event.position.x==event1.event.position.x
|
||||
&&event0.event.position.y==event1.event.position.y
|
||||
&&event0.event.position.z==event1.event.position.z
|
||||
)
|
||||
);
|
||||
|
||||
let mut last_index=0;
|
||||
let mut push_slices=|index:usize|{
|
||||
let len=index-last_index;
|
||||
let count=len.div_ceil(MAX_SLICE_LEN);
|
||||
let slice_len=MAX_SLICE_LEN;
|
||||
bvh_nodes.reserve(count);
|
||||
// 0123456789
|
||||
// split into groups of MAX_SLICE_LEN=4
|
||||
// [0123][4567][89]
|
||||
let mut push_slice=|slice:Range<usize>,inclusive:bool|{
|
||||
let mut aabb=Aabb::default();
|
||||
for event in &output_events[slice.start..slice.end]{
|
||||
aabb.grow(vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap());
|
||||
}
|
||||
if inclusive{
|
||||
let event=&output_events[slice.end];
|
||||
aabb.grow(vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap());
|
||||
}
|
||||
bvh_nodes.push((EventSlice{slice,inclusive},aabb));
|
||||
};
|
||||
// push fixed-size groups
|
||||
for i in 0..count-1{
|
||||
push_slice((last_index+i*slice_len)..(last_index+(i+1)*slice_len),true);
|
||||
}
|
||||
// push last group which may be shorter
|
||||
push_slice((last_index+(count-1)*slice_len)..index,false);
|
||||
last_index=index;
|
||||
};
|
||||
// find discontinuities (teleports) and avoid forming a bvh node across them
|
||||
for (split_index,_) in it{
|
||||
// we want to use the index of event1
|
||||
push_slices(split_index+1);
|
||||
}
|
||||
// there are no more discontinuities, push the remaining slices
|
||||
push_slices(output_events.len());
|
||||
let bvh=generate_bvh(bvh_nodes);
|
||||
Self{bvh}
|
||||
}
|
||||
/// Find the exact timestamp on the bot timeline that is closest to the given point.
|
||||
pub fn closest_time_to_point<'a>(&self,bot:&'a CompleteBot,point:glam::Vec3)->Option<PhysicsTime>{
|
||||
let point=point+bot.world_offset();
|
||||
let start_point=vec3::try_from_f32_array(point.to_array()).unwrap();
|
||||
let output_events=&bot.timelines().output_events;
|
||||
// grow a sphere starting at start_point until we find the closest point on the bot output events
|
||||
let intersect_leaf=|event_slice:&EventSlice|{
|
||||
// calculate the distance to the leaf contents
|
||||
let mut best_distance=output_events[event_slice.slice.start..event_slice.slice.end].iter().map(|event|{
|
||||
let p=event.event.position;
|
||||
let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap();
|
||||
(start_point-p).length_squared()
|
||||
}).min()?;
|
||||
let mut prev_event=&output_events[event_slice.slice.start];
|
||||
let mut f=|event:&'a v0::Timed<v0::OutputEvent>|{
|
||||
let p0=vec3::try_from_f32_array([prev_event.event.position.x,prev_event.event.position.y,prev_event.event.position.z]).unwrap();
|
||||
let p1=vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap();
|
||||
let d=p1-p0;
|
||||
let d0=p0.dot(d);
|
||||
let d1=p1.dot(d);
|
||||
let sp_d=start_point.dot(d);
|
||||
// must be on the segment
|
||||
if d0<sp_d&&sp_d<d1{
|
||||
let t0=d1-sp_d;
|
||||
let t1=sp_d-d0;
|
||||
let dt=d1-d0;
|
||||
let distance=(((p0*t0+p1*t1)/dt).divide().wrap_1()-start_point).length_squared();
|
||||
if distance<best_distance{
|
||||
best_distance=distance;
|
||||
}
|
||||
}
|
||||
prev_event=event;
|
||||
};
|
||||
for event in &output_events[event_slice.slice.start+1..event_slice.slice.end]{
|
||||
f(event);
|
||||
}
|
||||
if event_slice.inclusive{
|
||||
f(&output_events[event_slice.slice.end]);
|
||||
}
|
||||
Some(best_distance)
|
||||
};
|
||||
let intersect_aabb=|aabb:&Aabb|{
|
||||
// calculate the distance to the aabb
|
||||
let clamped_point=start_point.min(aabb.max()).max(aabb.min());
|
||||
Some((start_point-clamped_point).length_squared())
|
||||
};
|
||||
let (_,event_slice)=self.bvh.traverse(start_point,Fixed::ZERO,Fixed::MAX,intersect_leaf,intersect_aabb)?;
|
||||
|
||||
// find time at the closest point
|
||||
let (best_time,mut best_distance)=output_events[event_slice.slice.start..event_slice.slice.end].iter().map(|event|{
|
||||
let p=event.event.position;
|
||||
let p=vec3::try_from_f32_array([p.x,p.y,p.z]).unwrap();
|
||||
(event.time,(start_point-p).length_squared())
|
||||
}).min_by_key(|&(_,distance)|distance)?;
|
||||
let mut best_time=crate::time::from_float(best_time).unwrap();
|
||||
let mut prev_event=&output_events[event_slice.slice.start];
|
||||
let mut f=|event:&'a v0::Timed<v0::OutputEvent>|{
|
||||
let p0=vec3::try_from_f32_array([prev_event.event.position.x,prev_event.event.position.y,prev_event.event.position.z]).unwrap();
|
||||
let p1=vec3::try_from_f32_array([event.event.position.x,event.event.position.y,event.event.position.z]).unwrap();
|
||||
let d=p1-p0;
|
||||
let d0=p0.dot(d);
|
||||
let d1=p1.dot(d);
|
||||
let sp_d=start_point.dot(d);
|
||||
// must be on the segment
|
||||
if d0<sp_d&&sp_d<d1{
|
||||
let t0=d1-sp_d;
|
||||
let t1=sp_d-d0;
|
||||
let dt=d1-d0;
|
||||
let distance=(((p0*t0+p1*t1)/dt).divide().wrap_1()-start_point).length_squared();
|
||||
if distance<best_distance{
|
||||
best_distance=distance;
|
||||
let p0:Planar64=prev_event.time.try_into().unwrap();
|
||||
let p1:Planar64=event.time.try_into().unwrap();
|
||||
best_time=((p0*t0+p1*t1)/dt).into();
|
||||
}
|
||||
}
|
||||
prev_event=event;
|
||||
};
|
||||
for event in &output_events[event_slice.slice.start+1..event_slice.slice.end]{
|
||||
f(event);
|
||||
}
|
||||
if event_slice.inclusive{
|
||||
f(&output_events[event_slice.slice.end]);
|
||||
}
|
||||
Some(best_time)
|
||||
}
|
||||
}
|
||||
@@ -3,54 +3,28 @@ use strafesnet_graphics::graphics::GraphicsState;
|
||||
/// The graphics state, essentially a handle to all the information on the GPU.
|
||||
pub struct Graphics{
|
||||
graphics:GraphicsState,
|
||||
config:wgpu::SurfaceConfiguration,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
start_offset:glam::Vec3,
|
||||
}
|
||||
impl Graphics{
|
||||
pub fn new(device:wgpu::Device,queue:wgpu::Queue,config:wgpu::SurfaceConfiguration)->Self{
|
||||
let graphics=strafesnet_graphics::graphics::GraphicsState::new(&device,&queue,&config);
|
||||
pub fn new(device:&wgpu::Device,queue:&wgpu::Queue,size:glam::UVec2,view_format:wgpu::TextureFormat)->Self{
|
||||
let graphics=strafesnet_graphics::graphics::GraphicsState::new(device,queue,size,view_format);
|
||||
Self{
|
||||
graphics,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
start_offset:glam::Vec3::ZERO,
|
||||
}
|
||||
}
|
||||
pub fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
|
||||
pub fn change_map(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&strafesnet_common::map::CompleteMap){
|
||||
self.graphics.clear();
|
||||
self.graphics.generate_models(&self.device,&self.queue,map);
|
||||
self.graphics.generate_models(device,queue,map);
|
||||
let modes=map.modes.clone().denormalize();
|
||||
let mode=modes.get_mode(strafesnet_common::gameplay_modes::ModeId::MAIN).expect("Map does not have a main mode");
|
||||
let start_zone=map.models.get(mode.get_start().get() as usize).expect("Map does not have a start zone");
|
||||
self.start_offset=glam::Vec3::from_array(start_zone.transform.translation.map(|f|f.into()).to_array());
|
||||
}
|
||||
pub fn resize(&mut self,surface:&wgpu::Surface<'_>,size:glam::UVec2,fov:glam::Vec2){
|
||||
self.config.width=size.x.max(1);
|
||||
self.config.height=size.y.max(1);
|
||||
surface.configure(&self.device,&self.config);
|
||||
self.graphics.resize(&self.device,&self.config,fov);
|
||||
pub fn resize(&mut self,device:&wgpu::Device,size:glam::UVec2,fov:glam::Vec2){
|
||||
self.graphics.resize(device,size,fov);
|
||||
}
|
||||
pub fn render(&mut self,surface:&wgpu::Surface<'_>,pos:glam::Vec3,angles:glam::Vec2){
|
||||
//this has to go deeper somehow
|
||||
let frame=match surface.get_current_texture(){
|
||||
Ok(frame)=>frame,
|
||||
Err(_)=>{
|
||||
surface.configure(&self.device,&self.config);
|
||||
surface
|
||||
.get_current_texture()
|
||||
.expect("Failed to acquire next surface texture!")
|
||||
}
|
||||
};
|
||||
let view=frame.texture.create_view(&wgpu::TextureViewDescriptor{
|
||||
format:Some(self.config.view_formats[0]),
|
||||
..wgpu::TextureViewDescriptor::default()
|
||||
});
|
||||
|
||||
self.graphics.render(&view,&self.device,&self.queue,strafesnet_graphics::graphics::view_inv(pos+self.start_offset,angles));
|
||||
|
||||
frame.present();
|
||||
pub fn encode_commands(&mut self,encoder:&mut wgpu::CommandEncoder,view:&wgpu::TextureView,pos:glam::Vec3,angles:glam::Vec2){
|
||||
self.graphics.encode_commands(encoder,view,strafesnet_graphics::graphics::view_inv(pos+self.start_offset,angles));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ impl PlaybackHead{
|
||||
|
||||
(p-bot.world_offset()+CompleteBot::CAMERA_OFFSET,a.yx())
|
||||
}
|
||||
pub fn get_position(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{
|
||||
let interp=self.interpolate_output(bot,time);
|
||||
interp.position()-bot.world_offset()
|
||||
}
|
||||
pub fn get_velocity(&self,bot:&CompleteBot,time:SessionTime)->glam::Vec3{
|
||||
let interp=self.interpolate_output(bot,time);
|
||||
interp.velocity()
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
pub mod bot;
|
||||
pub mod bvh;
|
||||
pub mod head;
|
||||
pub mod time;
|
||||
pub mod state;
|
||||
// pub mod surface;
|
||||
pub mod graphics;
|
||||
|
||||
// Create Surface
|
||||
// Create Graphics from map file and with surface as sample
|
||||
// Create bot from bot file
|
||||
// Create playback head
|
||||
// loop{
|
||||
// advance head
|
||||
// render frame
|
||||
// }
|
||||
|
||||
@@ -55,6 +55,8 @@ pub struct PlaybackState{
|
||||
mouse_pos:v0::Vector2,
|
||||
// EventType::Output
|
||||
jump_count:u32,
|
||||
angles:v0::Vector3,
|
||||
angles_delta:glam::Vec3,
|
||||
// EventType::Sound
|
||||
// EventType::World
|
||||
// EventType::Gravity
|
||||
@@ -77,6 +79,8 @@ impl PlaybackState{
|
||||
game_controls:v0::GameControls::empty(),
|
||||
mouse_pos:v0::Vector2{x:0.0,y:0.0},
|
||||
jump_count:0,
|
||||
angles:v0::Vector3{x:0.0,y:0.0,z:0.0},
|
||||
angles_delta:glam::Vec3::ZERO,
|
||||
gravity:v0::Vector3{x:0.0,y:0.0,z:0.0},
|
||||
runs:HashMap::new(),
|
||||
style:v0::Style::Autohop,
|
||||
@@ -91,9 +95,15 @@ impl PlaybackState{
|
||||
self.runs.get(&mode)
|
||||
}
|
||||
fn push_output(&mut self,event:&v0::OutputEvent){
|
||||
// Jumps may occur during a substep
|
||||
if event.tick_info.contains(v0::TickInfo::Jump){
|
||||
self.jump_count+=1;
|
||||
}
|
||||
// Game tick "end", i.e. not a sub-step
|
||||
if event.tick_info.contains(v0::TickInfo::TickEnd){
|
||||
self.angles_delta=glam::vec3(event.angles.x,event.angles.y,event.angles.z)-glam::vec3(self.angles.x,self.angles.y,self.angles.z);
|
||||
self.angles=event.angles;
|
||||
}
|
||||
}
|
||||
fn push_input(&mut self,event:&v0::InputEvent){
|
||||
self.game_controls=event.game_controls;
|
||||
@@ -194,22 +204,43 @@ impl PlaybackState{
|
||||
}
|
||||
}
|
||||
pub(crate) fn process_head(&mut self,block:&v0::Block,head:&v0::Head){
|
||||
// The whole point of this is to avoid running the realtime events!
|
||||
/*
|
||||
for event in &block.input_events[0..head.get_event_index(v0::EventType::Input)]{
|
||||
// Avoid running the realtime events from the beginning.
|
||||
// Run the preceding input event to initialize the state.
|
||||
if let Some(index)=head.get_event_index(v0::EventType::Input).checked_sub(1)
|
||||
&&let Some(event)=block.input_events.get(index)
|
||||
{
|
||||
self.push_input(&event.event);
|
||||
}
|
||||
for event in &block.output_events[0..head.get_event_index(v0::EventType::Output)]{
|
||||
|
||||
// Helper function
|
||||
fn is_output_tick_end(&(_,event):&(usize,&v0::Timed<v0::OutputEvent>))->bool{
|
||||
event.event.tick_info.contains(v0::TickInfo::TickEnd)
|
||||
}
|
||||
// Run two preceding output events to flush out the default state.
|
||||
let output_end_index=head.get_event_index(v0::EventType::Output);
|
||||
let mut it=block.output_events[..output_end_index].iter().enumerate().rev();
|
||||
// Find two TickEnd events before output_end_index
|
||||
let _first=it.find(is_output_tick_end);
|
||||
let second=it.find(is_output_tick_end);
|
||||
// Get the index at the second event, if two TickEnd events don't exist then start at 0
|
||||
let output_start_index=second.map_or(0,|(i,_)|i);
|
||||
for event in &block.output_events[output_start_index..output_end_index]{
|
||||
self.push_output(&event.event);
|
||||
}
|
||||
for event in &bot.sound_events[0..head.get_event_index(v0::EventType::Sound)]{
|
||||
self.push_sound(&event.event);
|
||||
}
|
||||
*/
|
||||
|
||||
// for event in &bot.sound_events[0..head.get_event_index(v0::EventType::Sound)]{
|
||||
// self.push_sound(&event.event);
|
||||
// }
|
||||
|
||||
// Offline events have to be run from the beginning because they contain cumulative state.
|
||||
// for event in &bot.world_events[0..head.get_event_index(v0::EventType::World)]{
|
||||
// self.push_world(&event.event);
|
||||
// }
|
||||
for event in &block.gravity_events[0..head.get_event_index(v0::EventType::Gravity)]{
|
||||
|
||||
// Except for gravity, only the most recent event is relevant.
|
||||
if let Some(index)=head.get_event_index(v0::EventType::Gravity).checked_sub(1)
|
||||
&&let Some(event)=block.gravity_events.get(index)
|
||||
{
|
||||
self.push_gravity(&event.event);
|
||||
}
|
||||
for event in &block.run_events[0..head.get_event_index(v0::EventType::Run)]{
|
||||
@@ -255,4 +286,7 @@ impl PlaybackState{
|
||||
pub const fn get_gravity(&self)->v0::Vector3{
|
||||
self.gravity
|
||||
}
|
||||
pub const fn get_angles_delta(&self)->glam::Vec3{
|
||||
self.angles_delta
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/// A render surface configuration, containing information such as resolution and pixel format
|
||||
pub struct Surface{}
|
||||
@@ -4,11 +4,11 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
glam.workspace = true
|
||||
pollster = "0.4.0"
|
||||
wgpu = "28.0.0"
|
||||
winit = "0.30.12"
|
||||
strafesnet_roblox_bot_player = { version = "0.1.0", path = "../lib" }
|
||||
glam.workspace = true
|
||||
wgpu.workspace = true
|
||||
strafesnet_roblox_bot_player.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_graphics.workspace = true
|
||||
strafesnet_roblox_bot_file.workspace = true
|
||||
|
||||
@@ -2,6 +2,7 @@ use strafesnet_common::instruction::TimedInstruction;
|
||||
use strafesnet_common::session::Time as SessionTime;
|
||||
use strafesnet_common::timer::TimerState;
|
||||
use strafesnet_roblox_bot_player::{bot::CompleteBot,graphics::Graphics,head::{PlaybackHead,Time as PlaybackTime}};
|
||||
use strafesnet_graphics::surface::Surface;
|
||||
|
||||
pub enum SessionControlInstruction{
|
||||
SetPaused(bool),
|
||||
@@ -35,22 +36,22 @@ struct Playback{
|
||||
}
|
||||
|
||||
pub struct PlayerWorker<'a>{
|
||||
surface:wgpu::Surface<'a>,
|
||||
graphics_thread:Graphics,
|
||||
surface:Surface<'a>,
|
||||
playback:Option<Playback>,
|
||||
}
|
||||
impl<'a> PlayerWorker<'a>{
|
||||
pub fn new(
|
||||
surface:wgpu::Surface<'a>,
|
||||
graphics_thread:Graphics,
|
||||
surface:Surface<'a>,
|
||||
)->Self{
|
||||
Self{
|
||||
surface,
|
||||
graphics_thread,
|
||||
surface,
|
||||
playback:None,
|
||||
}
|
||||
}
|
||||
pub fn send(&mut self,ins:TimedInstruction<Instruction,SessionTime>){
|
||||
pub fn send(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,ins:TimedInstruction<Instruction,SessionTime>){
|
||||
match ins.instruction{
|
||||
Instruction::SessionControl(SessionControlInstruction::SetPaused(paused))=>if let Some(playback)=&mut self.playback{
|
||||
playback.playback_head.set_paused(ins.time,paused);
|
||||
@@ -77,15 +78,28 @@ impl<'a> PlayerWorker<'a>{
|
||||
Instruction::Render=>if let Some(playback)=&mut self.playback{
|
||||
playback.playback_head.advance_time(&playback.bot,ins.time);
|
||||
let (pos,angles)=playback.playback_head.get_position_angles(&playback.bot,ins.time);
|
||||
self.graphics_thread.render(&self.surface,pos,angles);
|
||||
|
||||
//this has to go deeper somehow
|
||||
let frame=self.surface.new_frame(device);
|
||||
|
||||
let mut encoder=device.create_command_encoder(&wgpu::CommandEncoderDescriptor{label:None});
|
||||
|
||||
self.graphics_thread.encode_commands(&mut encoder,frame.view(),pos,angles);
|
||||
|
||||
queue.submit([encoder.finish()]);
|
||||
|
||||
frame.present();
|
||||
},
|
||||
Instruction::Resize(physical_size)=>if let Some(playback)=&self.playback{
|
||||
let fov_y=playback.playback_head.state().get_fov_y();
|
||||
let fov_x=fov_y*physical_size.width as f64/physical_size.height as f64;
|
||||
self.graphics_thread.resize(&self.surface,glam::uvec2(physical_size.width,physical_size.height),glam::vec2(fov_x as f32,fov_y as f32));
|
||||
let fov=glam::vec2(fov_x as f32,fov_y as f32);
|
||||
let size=glam::uvec2(physical_size.width,physical_size.height);
|
||||
self.surface.configure(device,size);
|
||||
self.graphics_thread.resize(device,size,fov);
|
||||
},
|
||||
Instruction::ChangeMap(complete_map)=>{
|
||||
self.graphics_thread.change_map(&complete_map);
|
||||
self.graphics_thread.change_map(device,queue,&complete_map);
|
||||
},
|
||||
Instruction::LoadReplay(bot)=>{
|
||||
let bot=CompleteBot::new(bot);
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn setup_and_start(title:&str){
|
||||
let (device,queue)=setup::step4::request_device(&adapter).await.unwrap();
|
||||
|
||||
let size=window.inner_size();
|
||||
let config=setup::step5::configure_surface(&adapter,&device,&surface,(size.width,size.height)).unwrap();
|
||||
let surface=setup::step5::configure_surface(&adapter,&device,surface,(size.width,size.height)).unwrap();
|
||||
|
||||
//dedicated thread to ping request redraw back and resize the window doesn't seem logical
|
||||
|
||||
@@ -32,7 +32,6 @@ pub async fn setup_and_start(title:&str){
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
config,
|
||||
);
|
||||
|
||||
for arg in std::env::args().skip(1){
|
||||
|
||||
@@ -13,15 +13,20 @@ pub struct WindowContext<'a>{
|
||||
simulation_paused:bool,
|
||||
window:&'a winit::window::Window,
|
||||
physics_thread:PlayerWorker<'a>,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
}
|
||||
|
||||
impl WindowContext<'_>{
|
||||
fn phys(&mut self,ins:TimedInstruction<crate::player::Instruction,strafesnet_common::session::Time>){
|
||||
self.physics_thread.send(&self.device,&self.queue,ins);
|
||||
}
|
||||
fn window_event(&mut self,time:SessionTime,event:winit::event::WindowEvent){
|
||||
match event{
|
||||
winit::event::WindowEvent::DroppedFile(path)=>{
|
||||
match crate::file::load(path.as_path()){
|
||||
Ok(LoadFormat::Map(map))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}),
|
||||
Ok(LoadFormat::Bot(bot))=>self.physics_thread.send(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}),
|
||||
Ok(LoadFormat::Map(map))=>self.phys(TimedInstruction{time,instruction:PhysicsWorkerInstruction::ChangeMap(map)}),
|
||||
Ok(LoadFormat::Bot(bot))=>self.phys(TimedInstruction{time,instruction:PhysicsWorkerInstruction::LoadReplay(bot)}),
|
||||
Err(e)=>println!("Failed to load file: {e}"),
|
||||
}
|
||||
},
|
||||
@@ -31,7 +36,7 @@ impl WindowContext<'_>{
|
||||
return;
|
||||
}
|
||||
//pause unpause
|
||||
self.physics_thread.send(TimedInstruction{
|
||||
self.phys(TimedInstruction{
|
||||
time,
|
||||
instruction:PhysicsWorkerInstruction::SessionControl(SessionControlInstruction::SetPaused(!state)),
|
||||
});
|
||||
@@ -74,7 +79,7 @@ impl WindowContext<'_>{
|
||||
},
|
||||
_=>None,
|
||||
}{
|
||||
self.physics_thread.send(TimedInstruction{
|
||||
self.phys(TimedInstruction{
|
||||
time,
|
||||
instruction,
|
||||
});
|
||||
@@ -83,7 +88,7 @@ impl WindowContext<'_>{
|
||||
}
|
||||
},
|
||||
winit::event::WindowEvent::Resized(size)=>{
|
||||
self.physics_thread.send(
|
||||
self.phys(
|
||||
TimedInstruction{
|
||||
time,
|
||||
instruction:PhysicsWorkerInstruction::Resize(size)
|
||||
@@ -92,7 +97,7 @@ impl WindowContext<'_>{
|
||||
},
|
||||
winit::event::WindowEvent::RedrawRequested=>{
|
||||
self.window.request_redraw();
|
||||
self.physics_thread.send(
|
||||
self.phys(
|
||||
TimedInstruction{
|
||||
time,
|
||||
instruction:PhysicsWorkerInstruction::Render
|
||||
@@ -121,17 +126,19 @@ impl WindowContext<'_>{
|
||||
window:&'a winit::window::Window,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
surface:wgpu::Surface<'a>,
|
||||
config:wgpu::SurfaceConfiguration,
|
||||
surface:strafesnet_graphics::surface::Surface<'a>,
|
||||
)->WindowContext<'a>{
|
||||
let graphics=strafesnet_roblox_bot_player::graphics::Graphics::new(device,queue,config);
|
||||
let size=surface.size();
|
||||
let graphics=strafesnet_roblox_bot_player::graphics::Graphics::new(&device,&queue,size,surface.view_format());
|
||||
WindowContext{
|
||||
simulation_paused:false,
|
||||
window,
|
||||
physics_thread:crate::player::PlayerWorker::new(
|
||||
surface,
|
||||
graphics,
|
||||
surface,
|
||||
),
|
||||
device,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
video-encoder/Cargo.toml
Normal file
16
video-encoder/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "video-encoder"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
glam.workspace = true
|
||||
wgpu.workspace = true
|
||||
strafesnet_roblox_bot_player.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_graphics.workspace = true
|
||||
strafesnet_roblox_bot_file.workspace = true
|
||||
strafesnet_snf.workspace = true
|
||||
vk-video = "0.2.0"
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
mp4 = "0.14.0"
|
||||
4
video-encoder/README.md
Normal file
4
video-encoder/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
### How it works
|
||||
- Render RGB to graphics_texture
|
||||
- Convert RGB to YUV on video_texture
|
||||
- Encode video frame
|
||||
55
video-encoder/shaders/rgb_to_yuv.wgsl
Normal file
55
video-encoder/shaders/rgb_to_yuv.wgsl
Normal file
@@ -0,0 +1,55 @@
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
// hacky way to draw a large triangle
|
||||
let tmp1 = i32(vertex_index) / 2;
|
||||
let tmp2 = i32(vertex_index) & 1;
|
||||
var result:VertexOutput;
|
||||
result.position=vec4<f32>(
|
||||
f32(tmp1) * 4.0 - 1.0,
|
||||
f32(tmp2) * 4.0 - 1.0,
|
||||
1.0,
|
||||
1.0
|
||||
);
|
||||
result.uv=vec2<f32>(
|
||||
f32(tmp1) * 2.0,
|
||||
1.0 - f32(tmp2) * 2.0
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@group(0)
|
||||
@binding(0)
|
||||
var texture: texture_2d<f32>;
|
||||
@group(0)
|
||||
@binding(1)
|
||||
var texture_sampler: sampler;
|
||||
|
||||
const RGB_TO_Y:vec3<f32> =
|
||||
vec3(0.2126,0.7152,0.0722);
|
||||
const RGB_TO_UV:mat3x2<f32> = mat3x2<f32>(
|
||||
-0.09991,0.615,
|
||||
-0.33609,-0.55861,
|
||||
0.436,-0.05639
|
||||
);
|
||||
const BIAS:vec2<f32> = vec2<f32>(0.5, 0.5);
|
||||
|
||||
@fragment
|
||||
fn fs_main_y(input: VertexOutput) -> @location(0) f32 {
|
||||
let color = textureSample(texture, texture_sampler, input.uv).rgb;
|
||||
let y = dot(RGB_TO_Y,color);
|
||||
let y_limited = mix(16.0/255.0,240.0/255.0,y);
|
||||
return clamp(y_limited, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_uv(input: VertexOutput) -> @location(0) vec2<f32> {
|
||||
let color = textureSample(texture, texture_sampler, input.uv).rgb;
|
||||
let uv = RGB_TO_UV * color + BIAS;
|
||||
let uv_limited = mix(vec2(16.0/255.0),vec2(240.0/255.0),uv);
|
||||
return clamp(uv_limited, vec2(0.0, 0.0), vec2(1.0, 1.0));
|
||||
}
|
||||
509
video-encoder/src/encode.rs
Normal file
509
video-encoder/src/encode.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::PathBuf;
|
||||
use strafesnet_common::session::Time as SessionTime;
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
pub enum Commands{
|
||||
Encode(EncodeSubcommand),
|
||||
}
|
||||
|
||||
impl Commands{
|
||||
pub fn run(self){
|
||||
match self{
|
||||
Commands::Encode(command)=>command.run().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub struct EncodeSubcommand{
|
||||
#[arg(long,short)]
|
||||
map:PathBuf,
|
||||
#[arg(long,short)]
|
||||
bot:PathBuf,
|
||||
#[arg(long,short)]
|
||||
output_file:Option<PathBuf>,
|
||||
#[arg(long,short)]
|
||||
width:Option<NonZeroU32>,
|
||||
#[arg(long,short)]
|
||||
height:Option<NonZeroU32>,
|
||||
#[arg(long)]
|
||||
fps:Option<u32>,
|
||||
#[arg(long)]
|
||||
target_bitrate:Option<u64>,
|
||||
#[arg(long)]
|
||||
max_bitrate:Option<u64>,
|
||||
#[arg(long)]
|
||||
device:Option<String>,
|
||||
}
|
||||
impl EncodeSubcommand{
|
||||
fn run(self)->Result<(),EncodeError>{
|
||||
encode(EncodeParams{
|
||||
width:self.width.unwrap_or(NonZeroU32::new(1920).unwrap()),
|
||||
height:self.width.unwrap_or(NonZeroU32::new(1080).unwrap()),
|
||||
target_framerate:self.fps.unwrap_or(60),
|
||||
average_bitrate:self.target_bitrate.unwrap_or(6_000_000),
|
||||
max_bitrate:self.max_bitrate.unwrap_or(6_000_000),
|
||||
device:self.device,
|
||||
output_file:self.output_file.unwrap_or_else(||{
|
||||
let mut output_file:PathBuf=self.bot.file_stem().unwrap().into();
|
||||
output_file.set_extension("mp4");
|
||||
output_file
|
||||
}),
|
||||
map:self.map,
|
||||
bot:self.bot,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum EncodeError{
|
||||
ReadMap(std::io::Error),
|
||||
ReadBot(std::io::Error),
|
||||
DecodeSNF(strafesnet_snf::Error),
|
||||
DecodeMap(strafesnet_snf::map::Error),
|
||||
DecodeBot(strafesnet_roblox_bot_file::v0::Error),
|
||||
CreateInstance(vk_video::VulkanInitError),
|
||||
CreateAdapter(vk_video::VulkanInitError),
|
||||
NoAdapter,
|
||||
CreateDevice(vk_video::VulkanInitError),
|
||||
VideoEncodeParams(vk_video::VulkanEncoderError),
|
||||
VideoCreateTextures(vk_video::VulkanEncoderError),
|
||||
VideoEncodeFrame(vk_video::VulkanEncoderError),
|
||||
OutputCreateFile(std::io::Error),
|
||||
OutputMp4Start(mp4::Error),
|
||||
OutputMp4AddTrack(mp4::Error),
|
||||
OutputMp4WriteSample(mp4::Error),
|
||||
OutputMp4End(mp4::Error),
|
||||
}
|
||||
|
||||
struct EncodeParams{
|
||||
width:NonZeroU32,
|
||||
height:NonZeroU32,
|
||||
target_framerate:u32,
|
||||
average_bitrate:u64,
|
||||
max_bitrate:u64,
|
||||
device:Option<String>,
|
||||
map:PathBuf,
|
||||
bot:PathBuf,
|
||||
output_file:PathBuf,
|
||||
}
|
||||
|
||||
fn encode(params:EncodeParams)->Result<(),EncodeError>{
|
||||
let size = glam::uvec2(params.width.get(),params.height.get());
|
||||
let target_framerate = params.target_framerate;
|
||||
let average_bitrate = params.average_bitrate;
|
||||
let max_bitrate = params.max_bitrate;
|
||||
|
||||
let map_file=std::fs::read(params.map).map_err(EncodeError::ReadMap)?;
|
||||
let bot_file=std::fs::read(params.bot).map_err(EncodeError::ReadBot)?;
|
||||
|
||||
// read files
|
||||
let map=strafesnet_snf::read_map(std::io::Cursor::new(map_file))
|
||||
.map_err(EncodeError::DecodeSNF)?
|
||||
.into_complete_map()
|
||||
.map_err(EncodeError::DecodeMap)?;
|
||||
let timelines=strafesnet_roblox_bot_file::v0::read_all_to_block(std::io::Cursor::new(bot_file))
|
||||
.map_err(EncodeError::DecodeBot)?;
|
||||
|
||||
// vulkan init
|
||||
let vulkan_instance = vk_video::VulkanInstance::new().map_err(EncodeError::CreateInstance)?;
|
||||
let vulkan_adapter = if let Some(filter)=params.device.as_deref(){
|
||||
vulkan_instance.iter_adapters(None)
|
||||
.map_err(EncodeError::CreateAdapter)?
|
||||
.find(|adapter|adapter.info().name.contains(filter))
|
||||
.ok_or(EncodeError::NoAdapter)?
|
||||
}else{
|
||||
vulkan_instance.create_adapter(None).map_err(EncodeError::CreateAdapter)?
|
||||
};
|
||||
let vulkan_device = vulkan_adapter
|
||||
.create_device(
|
||||
wgpu::Features::TEXTURE_COMPRESSION_BC,
|
||||
wgpu::ExperimentalFeatures::disabled(),
|
||||
wgpu::Limits::defaults(),
|
||||
)
|
||||
.map_err(EncodeError::CreateDevice)?;
|
||||
|
||||
// playback
|
||||
let bot=strafesnet_roblox_bot_player::bot::CompleteBot::new(timelines);
|
||||
let mut playback_head=strafesnet_roblox_bot_player::head::PlaybackHead::new(&bot,SessionTime::ZERO);
|
||||
|
||||
let mut wgpu_state = WgpuState::new(
|
||||
vulkan_device.wgpu_device(),
|
||||
vulkan_device.wgpu_queue(),
|
||||
size,
|
||||
);
|
||||
|
||||
wgpu_state.change_map(&map);
|
||||
|
||||
let mut encoder = vulkan_device
|
||||
.create_wgpu_textures_encoder(
|
||||
vulkan_device
|
||||
.encoder_parameters_high_quality(
|
||||
vk_video::parameters::VideoParameters {
|
||||
width:params.width,
|
||||
height:params.height,
|
||||
target_framerate:target_framerate.into(),
|
||||
},
|
||||
vk_video::parameters::RateControl::VariableBitrate {
|
||||
average_bitrate,
|
||||
max_bitrate,
|
||||
virtual_buffer_size: std::time::Duration::from_secs(2),
|
||||
},
|
||||
)
|
||||
.map_err(EncodeError::VideoEncodeParams)?,
|
||||
)
|
||||
.map_err(EncodeError::VideoCreateTextures)?;
|
||||
|
||||
let output_file=std::fs::File::create(params.output_file)
|
||||
.map_err(EncodeError::OutputCreateFile)?;
|
||||
|
||||
let mp4_config=mp4::Mp4Config{
|
||||
major_brand: str::parse("isom").unwrap(),
|
||||
minor_version: 512,
|
||||
compatible_brands: vec![
|
||||
str::parse("isom").unwrap(),
|
||||
str::parse("iso2").unwrap(),
|
||||
str::parse("avc1").unwrap(),
|
||||
str::parse("mp41").unwrap(),
|
||||
],
|
||||
timescale:target_framerate,
|
||||
};
|
||||
let mut mp4=mp4::Mp4Writer::write_start(output_file,&mp4_config)
|
||||
.map_err(EncodeError::OutputMp4Start)?;
|
||||
|
||||
let avc_config=mp4::AvcConfig{
|
||||
width:params.width.get() as u16,
|
||||
height:params.height.get() as u16,
|
||||
// make up some data to prevent this underdeveloped library from crashing
|
||||
seq_param_set:vec![0,0,0,0],
|
||||
pic_param_set:vec![],
|
||||
};
|
||||
let track_config=mp4::TrackConfig{
|
||||
track_type:mp4::TrackType::Video,
|
||||
timescale:target_framerate,
|
||||
language:"eng".to_owned(),
|
||||
media_conf:mp4::MediaConfig::AvcConfig(avc_config),
|
||||
};
|
||||
|
||||
const TRACK_ID:u32=1;
|
||||
mp4.add_track(&track_config)
|
||||
.map_err(EncodeError::OutputMp4AddTrack)?;
|
||||
|
||||
let duration = bot.duration();
|
||||
for i in 0..duration.get()*target_framerate as i64/SessionTime::ONE_SECOND.get() {
|
||||
let time=SessionTime::raw(i*SessionTime::ONE_SECOND.get()/target_framerate as i64);
|
||||
playback_head.advance_time(&bot,time);
|
||||
let (pos,angles)=playback_head.get_position_angles(&bot,time);
|
||||
wgpu_state.render(pos,angles);
|
||||
|
||||
let frame=vk_video::Frame{
|
||||
data:wgpu_state.video_texture.clone(),
|
||||
pts:None,
|
||||
};
|
||||
let res=unsafe{encoder.encode(frame,false)}
|
||||
.map_err(EncodeError::VideoEncodeFrame)?;
|
||||
|
||||
let mp4_sample=mp4::Mp4Sample{
|
||||
start_time:i as u64,
|
||||
duration:1,
|
||||
rendering_offset:0,
|
||||
is_sync:false,
|
||||
bytes:res.data.into(),
|
||||
};
|
||||
mp4.write_sample(TRACK_ID,&mp4_sample)
|
||||
.map_err(EncodeError::OutputMp4WriteSample)?;
|
||||
}
|
||||
|
||||
mp4.write_end()
|
||||
.map_err(EncodeError::OutputMp4End)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct WgpuState {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
// graphics output
|
||||
graphics:strafesnet_roblox_bot_player::graphics::Graphics,
|
||||
// not sure if this needs to stay bound to keep the TextureView valid
|
||||
#[expect(unused)]
|
||||
graphics_texture: wgpu::Texture,
|
||||
graphics_texture_view: wgpu::TextureView,
|
||||
// video output
|
||||
video_texture: wgpu::Texture,
|
||||
y_renderer: PlaneRenderer,
|
||||
uv_renderer: PlaneRenderer,
|
||||
}
|
||||
|
||||
impl WgpuState {
|
||||
fn new(
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
size: glam::UVec2,
|
||||
) -> WgpuState {
|
||||
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
|
||||
let graphics = strafesnet_roblox_bot_player::graphics::Graphics::new(&device,&queue,size,FORMAT);
|
||||
|
||||
let shader = wgpu::include_wgsl!("../shaders/rgb_to_yuv.wgsl");
|
||||
let shader = device.create_shader_module(shader);
|
||||
|
||||
let graphics_texture_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
|
||||
label:Some("RGB Bind Group Layout"),
|
||||
entries:&[
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:0,
|
||||
visibility:wgpu::ShaderStages::FRAGMENT,
|
||||
ty:wgpu::BindingType::Texture{
|
||||
sample_type:wgpu::TextureSampleType::Float{filterable:true},
|
||||
multisampled:false,
|
||||
view_dimension:wgpu::TextureViewDimension::D2,
|
||||
},
|
||||
count:None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry{
|
||||
binding:1,
|
||||
visibility:wgpu::ShaderStages::FRAGMENT,
|
||||
ty:wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count:None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let graphics_texture=device.create_texture(&wgpu::TextureDescriptor{
|
||||
label:Some("RGB texture"),
|
||||
format:FORMAT,
|
||||
size:wgpu::Extent3d{
|
||||
width:size.x,
|
||||
height:size.y,
|
||||
depth_or_array_layers:1,
|
||||
},
|
||||
mip_level_count:1,
|
||||
sample_count:1,
|
||||
dimension:wgpu::TextureDimension::D2,
|
||||
usage:wgpu::TextureUsages::RENDER_ATTACHMENT|wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats:&[],
|
||||
});
|
||||
let graphics_texture_view = graphics_texture.create_view(&wgpu::TextureViewDescriptor {
|
||||
label: Some("RGB texture view"),
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
usage: Some(wgpu::TextureUsages::RENDER_ATTACHMENT|wgpu::TextureUsages::TEXTURE_BINDING),
|
||||
..Default::default()
|
||||
});
|
||||
let clamp_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
|
||||
label:Some("Clamp Sampler"),
|
||||
address_mode_u:wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v:wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w:wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter:wgpu::FilterMode::Linear,
|
||||
min_filter:wgpu::FilterMode::Linear,
|
||||
mipmap_filter:wgpu::MipmapFilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
let graphics_texture_bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
|
||||
layout:&graphics_texture_bind_group_layout,
|
||||
entries:&[
|
||||
wgpu::BindGroupEntry{
|
||||
binding:0,
|
||||
resource:wgpu::BindingResource::TextureView(&graphics_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry{
|
||||
binding:1,
|
||||
resource:wgpu::BindingResource::Sampler(&clamp_sampler),
|
||||
},
|
||||
],
|
||||
label:Some("Graphics Texture"),
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("wgpu pipeline layout"),
|
||||
bind_group_layouts: &[
|
||||
&graphics_texture_bind_group_layout
|
||||
],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let video_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("wgpu render target"),
|
||||
format: wgpu::TextureFormat::NV12,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
sample_count: 1,
|
||||
view_formats: &[],
|
||||
mip_level_count: 1,
|
||||
size: wgpu::Extent3d {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
});
|
||||
|
||||
let y_renderer = PlaneRenderer::new(
|
||||
&device,
|
||||
&pipeline_layout,
|
||||
&shader,
|
||||
"fs_main_y",
|
||||
&video_texture,
|
||||
wgpu::TextureAspect::Plane0,
|
||||
graphics_texture_bind_group.clone(),
|
||||
);
|
||||
let uv_renderer = PlaneRenderer::new(
|
||||
&device,
|
||||
&pipeline_layout,
|
||||
&shader,
|
||||
"fs_main_uv",
|
||||
&video_texture,
|
||||
wgpu::TextureAspect::Plane1,
|
||||
graphics_texture_bind_group,
|
||||
);
|
||||
|
||||
WgpuState {
|
||||
device,
|
||||
queue,
|
||||
graphics,
|
||||
graphics_texture,
|
||||
graphics_texture_view,
|
||||
video_texture,
|
||||
y_renderer,
|
||||
uv_renderer,
|
||||
}
|
||||
}
|
||||
|
||||
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
|
||||
self.graphics.change_map(&self.device,&self.queue,map);
|
||||
}
|
||||
|
||||
fn render(&mut self,pos:glam::Vec3,angles:glam::Vec2) {
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("wgpu encoder"),
|
||||
});
|
||||
|
||||
self.graphics.encode_commands(&mut encoder,&self.graphics_texture_view,pos,angles);
|
||||
|
||||
self.y_renderer.render(&mut encoder);
|
||||
self.uv_renderer.render(&mut encoder);
|
||||
|
||||
encoder.transition_resources(
|
||||
[].into_iter(),
|
||||
[wgpu::TextureTransition {
|
||||
texture: &self.video_texture,
|
||||
state: wgpu::TextureUses::COPY_SRC,
|
||||
selector: None,
|
||||
}]
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let buffer = encoder.finish();
|
||||
|
||||
self.queue.submit([buffer]);
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaneRenderer {
|
||||
graphics_texture_bind_group: wgpu::BindGroup,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
plane: wgpu::TextureAspect,
|
||||
plane_view: wgpu::TextureView,
|
||||
}
|
||||
|
||||
impl PlaneRenderer {
|
||||
fn new(
|
||||
device: &wgpu::Device,
|
||||
pipeline_layout: &wgpu::PipelineLayout,
|
||||
shader: &wgpu::ShaderModule,
|
||||
fragment_entry_point: &str,
|
||||
texture: &wgpu::Texture,
|
||||
plane: wgpu::TextureAspect,
|
||||
graphics_texture_bind_group: wgpu::BindGroup,
|
||||
) -> Self {
|
||||
let format = texture.format().aspect_specific_format(plane).unwrap();
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("wgpu pipeline"),
|
||||
layout: Some(pipeline_layout),
|
||||
cache: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: shader,
|
||||
buffers: &[],
|
||||
entry_point: None,
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some(fragment_entry_point),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
blend: None,
|
||||
format,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
front_face: wgpu::FrontFace::Cw,
|
||||
conservative: false,
|
||||
unclipped_depth: false,
|
||||
strip_index_format: None,
|
||||
},
|
||||
multiview_mask: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
});
|
||||
|
||||
let plane_view = texture.create_view(&wgpu::TextureViewDescriptor {
|
||||
label: Some("wgpu render target plane view"),
|
||||
aspect: plane,
|
||||
usage: Some(wgpu::TextureUsages::RENDER_ATTACHMENT),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Self {
|
||||
graphics_texture_bind_group,
|
||||
pipeline,
|
||||
plane,
|
||||
plane_view,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, encoder: &mut wgpu::CommandEncoder) {
|
||||
let clear_color = match self.plane {
|
||||
wgpu::TextureAspect::Plane0 => wgpu::Color::BLACK,
|
||||
wgpu::TextureAspect::Plane1 => wgpu::Color {
|
||||
r: 0.5,
|
||||
g: 0.5,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("wgpu render pass"),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
depth_stencil_attachment: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.plane_view,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(clear_color),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
})],
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
render_pass.set_bind_group(0,&self.graphics_texture_bind_group,&[]);
|
||||
render_pass.set_pipeline(&self.pipeline);
|
||||
render_pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
24
video-encoder/src/main.rs
Normal file
24
video-encoder/src/main.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use clap::{Parser,Subcommand};
|
||||
|
||||
mod encode;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author,version,about,long_about=None)]
|
||||
#[command(propagate_version=true)]
|
||||
struct Cli{
|
||||
#[command(subcommand)]
|
||||
command:Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands{
|
||||
#[command(flatten)]
|
||||
Encode(encode::Commands),
|
||||
}
|
||||
|
||||
fn main(){
|
||||
let cli=Cli::parse();
|
||||
match cli.command{
|
||||
Commands::Encode(commands)=>commands.run(),
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,8 @@ webgl = ["wgpu/webgl"]
|
||||
|
||||
[dependencies]
|
||||
glam.workspace = true
|
||||
strafesnet_roblox_bot_player = { version = "0.1.0", path = "../lib" }
|
||||
wgpu.workspace = true
|
||||
strafesnet_roblox_bot_player.workspace = true
|
||||
strafesnet_common.workspace = true
|
||||
strafesnet_graphics.workspace = true
|
||||
strafesnet_roblox_bot_file.workspace = true
|
||||
@@ -20,7 +21,6 @@ strafesnet_snf.workspace = true
|
||||
wasm-bindgen = "0.2.108"
|
||||
wasm-bindgen-futures = "0.4.58"
|
||||
web-sys = { version = "0.3.85", features = ["HtmlCanvasElement"] }
|
||||
wgpu = { version = "28.0.0" }
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-Oz", "--enable-bulk-memory","--enable-nontrapping-float-to-int"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::JsError;
|
||||
use strafesnet_roblox_bot_file::v0;
|
||||
use strafesnet_roblox_bot_player::{bot,head,time,graphics};
|
||||
use strafesnet_graphics::setup;
|
||||
use strafesnet_roblox_bot_player::{bot,bvh,head,time,graphics};
|
||||
use strafesnet_graphics::{setup,surface};
|
||||
|
||||
// Hack to keep the code compiling,
|
||||
// SurfaceTarget::Canvas is not available in IDE for whatever reason.
|
||||
@@ -22,11 +22,13 @@ impl From<ToSurfaceTarget> for wgpu::SurfaceTarget<'static>{
|
||||
#[wasm_bindgen]
|
||||
pub struct Graphics{
|
||||
graphics:graphics::Graphics,
|
||||
surface:wgpu::Surface<'static>,
|
||||
surface:surface::Surface<'static>,
|
||||
device:wgpu::Device,
|
||||
queue:wgpu::Queue,
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub async fn setup_graphics(canvas:web_sys::HtmlCanvasElement)->Result<Graphics,JsError>{
|
||||
let size=(canvas.width(),canvas.height());
|
||||
let size=glam::uvec2(canvas.width(),canvas.height());
|
||||
|
||||
let instance_desc=wgpu::InstanceDescriptor::from_env_or_default();
|
||||
let instance=wgpu::util::new_instance_with_webgpu_detection(&instance_desc).await;
|
||||
@@ -37,10 +39,12 @@ pub async fn setup_graphics(canvas:web_sys::HtmlCanvasElement)->Result<Graphics,
|
||||
compatible_surface:Some(&surface),
|
||||
}).await.map_err(|e|JsError::new(&e.to_string()))?;
|
||||
let (device,queue)=setup::step4::request_device(&adapter).await.map_err(|e|JsError::new(&e.to_string()))?;
|
||||
let config=setup::step5::configure_surface(&adapter,&device,&surface,size).map_err(|e|JsError::new(&e.to_string()))?;
|
||||
let surface=setup::step5::configure_surface(&adapter,&device,surface,(size.x,size.y)).map_err(|e|JsError::new(&e.to_string()))?;
|
||||
Ok(Graphics{
|
||||
graphics:graphics::Graphics::new(device,queue,config),
|
||||
surface:surface,
|
||||
graphics:graphics::Graphics::new(&device,&queue,size,surface.view_format()),
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
})
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
@@ -49,15 +53,21 @@ impl Graphics{
|
||||
pub fn render(&mut self,bot:&CompleteBot,head:&PlaybackHead,time:f64){
|
||||
let time=time::from_float(time).unwrap();
|
||||
let (pos,angles)=head.head.get_position_angles(&bot.bot,time);
|
||||
self.graphics.render(&self.surface,pos,angles);
|
||||
let frame=self.surface.new_frame(&self.device);
|
||||
let mut encoder=self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor{label:None});
|
||||
self.graphics.encode_commands(&mut encoder,frame.view(),pos,angles);
|
||||
self.queue.submit([encoder.finish()]);
|
||||
frame.present();
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub fn resize(&mut self,width:u32,height:u32,fov_slope_x:f32,fov_slope_y:f32){
|
||||
self.graphics.resize(&self.surface,[width,height].into(),[fov_slope_x as f32,fov_slope_y as f32].into());
|
||||
let size=[width,height].into();
|
||||
self.surface.configure(&self.device,size);
|
||||
self.graphics.resize(&self.device,size,[fov_slope_x as f32,fov_slope_y as f32].into());
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub fn change_map(&mut self,map:&CompleteMap){
|
||||
self.graphics.change_map(&map.map);
|
||||
self.graphics.change_map(&self.device,&self.queue,&map.map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,11 +193,43 @@ impl PlaybackHead{
|
||||
pub fn get_game_controls(&self)->u32{
|
||||
self.head.state().get_controls().bits()
|
||||
}
|
||||
/// Returns an array of [pitch, yaw, roll] in radians. Yaw is not restricted to any particular range.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_angles(&self,bot:&CompleteBot,time:f64)->Vec<f32>{
|
||||
pub fn get_position(&self,bot:&CompleteBot,time:f64)->Vector3{
|
||||
let time=time::from_float(time).unwrap();
|
||||
let angles=self.head.get_angles(&bot.bot,time);
|
||||
angles.to_array().to_vec()
|
||||
let position=self.head.get_position(&bot.bot,time);
|
||||
Vector3(position)
|
||||
}
|
||||
/// Returns the camera angles yaw delta between the last game tick and the most recent game tick.
|
||||
#[wasm_bindgen]
|
||||
pub fn get_angles_yaw_delta(&self)->f32{
|
||||
self.head.state().get_angles_delta().y
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Vector3(glam::Vec3);
|
||||
#[wasm_bindgen]
|
||||
impl Vector3{
|
||||
#[wasm_bindgen]
|
||||
pub fn to_array(&self)->Vec<f32>{
|
||||
self.0.to_array().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Bvh{
|
||||
bvh:bvh::Bvh,
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
impl Bvh{
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(bot:&CompleteBot)->Self{
|
||||
Self{
|
||||
bvh:bvh::Bvh::new(&bot.bot),
|
||||
}
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub fn closest_time_to_point(&self,bot:&CompleteBot,point:&Vector3)->Option<f64>{
|
||||
Some(bot.bot.playback_time(self.bvh.closest_time_to_point(&bot.bot,point.0)?).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
<div class="hud">
|
||||
<div id="hud_duration" class="timer">00:00:00</div>
|
||||
<div id="hud_timer" class="timer">00:00:00</div>
|
||||
<div id="diff_velocity" class="timer">-0.000 u/s</div>
|
||||
<div id="diff_time" class="timer">-0.000s</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="control_reset">↪️</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import init, {
|
||||
CompleteBot,
|
||||
CompleteMap,
|
||||
PlaybackHead,
|
||||
Bvh,
|
||||
} from "./pkg/strafesnet_roblox_bot_player_wasm_module.js";
|
||||
|
||||
// Loading
|
||||
@@ -17,12 +18,16 @@ const graphics = await setup_graphics(canvas);
|
||||
const bot = new CompleteBot(new Uint8Array(await b.arrayBuffer()));
|
||||
const map = new CompleteMap(new Uint8Array(await m.arrayBuffer()));
|
||||
const playback = new PlaybackHead(bot, 0);
|
||||
const bvh_wr = new Bvh(bot);
|
||||
const playback_wr = new PlaybackHead(bot, 0);
|
||||
|
||||
graphics.change_map(map);
|
||||
|
||||
// HUD
|
||||
const hud_timer = document.getElementById("hud_timer");
|
||||
const hud_duration = document.getElementById("hud_duration");
|
||||
const diff_velocity = document.getElementById("diff_velocity");
|
||||
const diff_time = document.getElementById("diff_time");
|
||||
const MODE_MAIN = 0;
|
||||
|
||||
function timer_text(t) {
|
||||
@@ -106,6 +111,22 @@ function animate(now) {
|
||||
const time = playback.get_run_time(bot, elapsedSec, MODE_MAIN);
|
||||
hud_timer.textContent = timer_text(time);
|
||||
|
||||
// show diff
|
||||
const pos = playback.get_position(bot, elapsedSec);
|
||||
const wr_playback_time = bvh_wr.closest_time_to_point(bot, pos);
|
||||
playback_wr.set_head_time(bot, elapsedSec, wr_playback_time);
|
||||
const wr_time = playback_wr.get_run_time(bot, elapsedSec, MODE_MAIN);
|
||||
const run_speed = playback.get_speed(bot, elapsedSec);
|
||||
const wr_speed = playback_wr.get_speed(bot, elapsedSec);
|
||||
const v_diff = run_speed - wr_speed;
|
||||
const wholespeed = Math.floor(Math.abs(v_diff));
|
||||
const millispeed = Math.floor((Math.abs(v_diff) % 1) * 1000);
|
||||
diff_velocity.textContent = `${v_diff<0?"-":"+"}${String(wholespeed)}.${String(millispeed).padStart(3, "0")} u/s`;
|
||||
const t_diff = time - wr_time;
|
||||
const s = Math.floor(Math.abs(t_diff));
|
||||
const ms = Math.floor((Math.abs(t_diff) % 1) * 1000);
|
||||
diff_time.textContent = `${t_diff<0?"-":"+"}${String(s)}.${String(ms).padStart(3, "0")}s`;
|
||||
|
||||
// Render the frame that the bot is at that time
|
||||
graphics.render(bot, playback, elapsedSec);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user