diff --git a/.gitignore b/.gitignore index 5487562ee7..68ff898e06 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ src/.DS_Store .DS_Store /.idea/ book/book -.vscode \ No newline at end of file +.vscode +trace-*.json +perf.data* +callgrind.out* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9f3a9798bf..7064bdfdba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "ab_glyph" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a9283dace1c41c265496614998d5b9c4a97b3eb770e804f007c5144bf03f2b" +checksum = "4dcdbc68024b653943864d436fe8a24b028095bc1cf91a8926f8241e4aaffe59" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -28,6 +28,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330223a1aecc308757b9926e9391c9b47f8ef2dbd8aea9df88312aea18c5e8d6" +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -53,9 +62,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e6e951cfbb2db8de1828d49073a113a29fd7117b1596caa781a258c7e38d72" +checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107" dependencies = [ "cfg-if 1.0.0", "getrandom", @@ -196,37 +205,37 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.4", "event-listener", "futures-core", ] [[package]] name = "async-executor" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", "async-task", - "concurrent-queue", + "concurrent-queue 2.0.0", "fastrand", "futures-lite", - "once_cell", "slab", ] [[package]] name = "async-io" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" +checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" dependencies = [ + "async-lock", "autocfg", - "concurrent-queue", + "concurrent-queue 1.2.4", "futures-lite", "libc", "log", - "once_cell", "parking", "polling", "slab", @@ -235,6 +244,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + [[package]] name = "async-task" version = "4.3.0" @@ -276,6 +295,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.5.4", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.11.0" @@ -284,9 +318,9 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "better_scoped_tls" @@ -480,7 +514,7 @@ dependencies = [ [[package]] name = "bevy_ecs_dynamic" version = "0.1.0" -source = "git+https://github.com/jakobhellermann/bevy_ecs_dynamic#aa7a051a49bd134cfd8c169542e16526bf2b36b3" +source = "git+https://github.com/jakobhellermann/bevy_ecs_dynamic?rev=aa7a051a49bd134cfd8c169542e16526bf2b36b3#aa7a051a49bd134cfd8c169542e16526bf2b36b3" dependencies = [ "bevy_ecs", "bevy_reflect", @@ -555,6 +589,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "bevy_ggrs" +version = "0.10.0" +source = "git+https://github.com/zicklag/bevy_ggrs.git?branch=jumpy#306683bbad3ed12e81175879a1ecb020d1873ea1" +dependencies = [ + "bevy", + "bytemuck", + "ggrs", + "instant", + "log", + "parking_lot 0.12.1", + "tracing", +] + [[package]] name = "bevy_gilrs" version = "0.8.1" @@ -616,6 +664,7 @@ dependencies = [ "bevy_ptr", "bevy_reflect", "bevy_render", + "bevy_scene", "bevy_sprite", "bevy_tasks", "bevy_text", @@ -688,7 +737,7 @@ dependencies = [ [[package]] name = "bevy_mod_js_scripting" version = "0.1.0" -source = "git+https://github.com/zicklag/bevy_mod_js_scripting.git?branch=jumpy#c86212a29be7475b0c2b1ffeaa60d13b0f6f2706" +source = "git+https://github.com/zicklag/bevy_mod_js_scripting.git?branch=jumpy#9781be0f8035ba43fa4498d88f1efba658ab4ed5" dependencies = [ "anyhow", "bevy", @@ -697,11 +746,13 @@ dependencies = [ "bevy_reflect_fns", "deno_core", "fixedbitset", + "indexmap", "js-sys", "pollster", "serde", "serde-wasm-bindgen", "serde_json", + "serde_v8", "slotmap", "swc_atoms", "swc_common", @@ -788,7 +839,7 @@ dependencies = [ [[package]] name = "bevy_reflect_fns" version = "0.1.0" -source = "git+https://github.com/jakobhellermann/bevy_reflect_fns#22e85021851e0cfe27b87e428f6313dd8c26abde" +source = "git+https://github.com/jakobhellermann/bevy_reflect_fns?rev=22e85021851e0cfe27b87e428f6313dd8c26abde#22e85021851e0cfe27b87e428f6313dd8c26abde" dependencies = [ "bevy_reflect", "thiserror", @@ -825,7 +876,7 @@ dependencies = [ "futures-lite", "hex", "hexasphere", - "image 0.24.4", + "image 0.24.5", "naga", "once_cell", "parking_lot 0.12.1", @@ -849,6 +900,28 @@ dependencies = [ "syn", ] +[[package]] +name = "bevy_scene" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a045d575d2c8f776d8ea965363c81660243fefbfc3712ead938b00dfd6797216" +dependencies = [ + "anyhow", + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_hierarchy", + "bevy_reflect", + "bevy_render", + "bevy_transform", + "bevy_utils", + "ron", + "serde", + "thiserror", + "uuid", +] + [[package]] name = "bevy_sprite" version = "0.8.1" @@ -1026,11 +1099,20 @@ dependencies = [ "winit", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" -version = "0.59.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +checksum = "8a022e58a142a46fea340d68012b9201c094e93ec3d033a944a24f8fd4a4f09a" dependencies = [ "bitflags", "cexpr", @@ -1043,6 +1125,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", + "syn", ] [[package]] @@ -1060,6 +1143,22 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitfield-rle" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8acc105b7bd3ed61e4bb7ad3e3b3f2a8da72205b2e0408cf71a499e8f57dd0" +dependencies = [ + "failure", + "varinteger", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1106,24 +1205,24 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytemuck" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9e1f5fa78f69496407a27ae9ed989e3c3b072310286f5ef385525e4cbc24a9" +checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" dependencies = [ "proc-macro2", "quote", @@ -1138,9 +1237,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cache-padded" @@ -1150,9 +1249,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" dependencies = [ "jobserver", ] @@ -1203,9 +1302,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.18" +version = "4.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" +checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e" dependencies = [ "atty", "bitflags", @@ -1218,9 +1317,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.18" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", @@ -1257,9 +1356,9 @@ checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "cocoa" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ "bitflags", "block", @@ -1321,6 +1420,15 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1443,9 +1551,9 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" +checksum = "1a9444b94b8024feecc29e01a9706c69c1e26bfee480221c90764200cfd778fb" dependencies = [ "bindgen", ] @@ -1506,9 +1614,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "422f23e724af1240ec469ea1e834d87a4b59ce2efe2c6a96256b0c47e2fd86aa" dependencies = [ "cfg-if 1.0.0", ] @@ -1523,16 +1631,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cstr_core" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd98742e4fdca832d40cab219dc2e3048de17d873248f83f17df47c1bea70956" -dependencies = [ - "cty", - "memchr", -] - [[package]] name = "cty" version = "0.2.2" @@ -1595,7 +1693,7 @@ dependencies = [ "hashbrown", "lock_api", "once_cell", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -1669,9 +1767,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -1703,6 +1801,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "downcast-rs" version = "1.2.0" @@ -1715,7 +1824,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc9fcd393c3daaaf5909008a1d948319d538b79c51871e4df0993260260a94e4" dependencies = [ - "ahash 0.8.0", + "ahash 0.8.2", "epaint", "nohash-hasher", ] @@ -1814,7 +1923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ba04741be7f6602b1a1b28f1082cce45948a7032961c52814f8946b28493300" dependencies = [ "ab_glyph", - "ahash 0.8.0", + "ahash 0.8.2", "atomic_refcell", "bytemuck", "emath", @@ -1856,6 +1965,28 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -1867,14 +1998,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -2148,9 +2279,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2159,6 +2290,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ggrs" +version = "0.9.2" +source = "git+https://github.com/gschup/ggrs#99f8a139d599d94db7397e65c33d6fabe45c4acf" +dependencies = [ + "bincode", + "bitfield-rle", + "bytemuck", + "instant", + "js-sys", + "parking_lot 0.11.2", + "rand", + "serde", +] + [[package]] name = "gilrs" version = "0.9.0" @@ -2193,6 +2339,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + [[package]] name = "glam" version = "0.21.3" @@ -2401,23 +2553,23 @@ dependencies = [ [[package]] name = "image" -version = "0.24.4" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" dependencies = [ "bytemuck", "byteorder", "color_quant", "num-rational 0.4.1", "num-traits", - "png 0.17.6", + "png 0.17.7", ] [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -2480,11 +2632,10 @@ dependencies = [ [[package]] name = "intl_pluralrules" -version = "7.0.1" +version = "7.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18f988384267d7066cc2be425e6faf352900652c046b6971d2e228d3b1c5ecf" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" dependencies = [ - "tinystr", "unic-langid", ] @@ -2528,9 +2679,9 @@ checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "iyes_loopless" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec251a82c60be9e282aec12056fa153666d5730b21d124655d7c22114d342c8" +checksum = "20f42b3a59033b3372129b84850a6d39e02c25f3f170c4f8b84232b775602bb0" dependencies = [ "bevy_app", "bevy_ecs", @@ -2588,7 +2739,7 @@ version = "0.4.3" dependencies = [ "anyhow", "async-channel", - "base64 0.13.0", + "base64 0.13.1", "bevy", "bevy-has-load-progress", "bevy-inspector-egui", @@ -2597,11 +2748,14 @@ dependencies = [ "bevy_ecs_tilemap", "bevy_egui", "bevy_fluent", + "bevy_ggrs", "bevy_kira_audio", "bevy_mod_js_scripting", "bevy_prototype_lyon", "bevy_tweening", + "bitfield", "blocking", + "bytemuck", "bytes", "clap", "directories", @@ -2614,7 +2768,10 @@ dependencies = [ "iyes_loopless", "jumpy-matchmaker-proto", "leafwing-input-manager", + "log", + "mimalloc", "normalize-path", + "numquant", "once_cell", "postcard 1.0.2 (git+https://github.com/zicklag/postcard.git?branch=custom-error-messages)", "quinn", @@ -2625,7 +2782,8 @@ dependencies = [ "serde_yaml", "sys-locale", "thiserror", - "ulid", + "tracing", + "turborand", "unic-langid", "wasm-bindgen", "web-sys", @@ -2639,16 +2797,17 @@ dependencies = [ "async-executor", "async-io", "bevy_tasks", - "blocking", + "bytes", "clap", "either", + "futures", "futures-lite", - "jumpy", "jumpy-matchmaker-proto", "once_cell", "postcard 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "quinn", "quinn-bevy", + "rand", "rcgen", "rustls", "scc", @@ -2691,9 +2850,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" dependencies = [ "kqueue-sys", "libc", @@ -2723,9 +2882,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "leafwing-input-manager" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e2dd6c5d8c0bc64951036855bb3fb8f4ed88442cb6c02ed490b29cae5c186" +checksum = "32953f440c0c48698cf354a78ccdb4fda4cb6ca1846f326e1280021fa333e158" dependencies = [ "bevy", "derive_more", @@ -2737,9 +2896,9 @@ dependencies = [ [[package]] name = "leafwing_input_manager_macros" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38676bbe26f336c5554150be0050a7033c72e882f8df4be5a2b3b1e6b2929cd" +checksum = "d98664cb644020e9c60d50c49a4630eb0a44aa15008d859208c538d75a4216b9" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2822,20 +2981,30 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libloading" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if 1.0.0", "winapi", ] +[[package]] +name = "libmimalloc-sys" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d1c67deb83e6b75fa4fe3309e09cfeade12e7721d95322af500d3814ea60c9" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libudev-sys" version = "0.1.4" @@ -2951,6 +3120,15 @@ dependencies = [ "objc", ] +[[package]] +name = "mimalloc" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2374e2999959a7b583e1811a1ddbf1d3a4b9496eceb9746f1192a59d871eca" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2985,16 +3163,25 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -3280,9 +3467,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -3310,13 +3497,10 @@ dependencies = [ ] [[package]] -name = "num_threads" -version = "0.1.6" +name = "numquant" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] +checksum = "54809e43d79aa532432c0d03c6adf62fdd96f2e152b90cef6cd9a316c3da4d99" [[package]] name = "objc" @@ -3357,6 +3541,15 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + [[package]] name = "oboe" version = "0.4.6" @@ -3382,9 +3575,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "openssl-probe" @@ -3394,9 +3587,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "overload" @@ -3406,9 +3599,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" -version = "0.15.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb" +checksum = "18904d3c65493a9f0d7542293d1a7f69bfdc309a6b9ef4f46dc3e58b0577edc5" dependencies = [ "ttf-parser", ] @@ -3437,7 +3630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -3456,15 +3649,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -3479,7 +3672,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] @@ -3585,9 +3778,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "pmutil" @@ -3614,21 +3807,21 @@ dependencies = [ [[package]] name = "png" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ "bitflags", "crc32fast", "flate2", - "miniz_oxide 0.5.4", + "miniz_oxide 0.6.2", ] [[package]] name = "polling" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" dependencies = [ "autocfg", "cfg-if 1.0.0", @@ -3657,7 +3850,7 @@ dependencies = [ [[package]] name = "postcard" version = "1.0.2" -source = "git+https://github.com/zicklag/postcard.git?branch=custom-error-messages#c6c081107909bb84a901210d6aa6b3d83a3eadc8" +source = "git+https://github.com/zicklag/postcard.git?branch=custom-error-messages#b7d417ce3dffa67d8fc3a7128100962701a6b6e2" dependencies = [ "cobs", "serde", @@ -3674,9 +3867,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precomputed-hash" @@ -3748,8 +3941,9 @@ checksum = "74605f360ce573babfe43964cbe520294dcb081afbf8c108fc6e23036b4da2df" [[package]] name = "quinn" -version = "0.8.0" -source = "git+https://github.com/quinn-rs/quinn.git#feca8abebf2b358141a675931b98043c355d4271" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57659b2d8340fa1a8b9358d14597b0b62ea8b9f6cd77cde07895079d619d2d4e" dependencies = [ "bytes", "futures-io", @@ -3780,8 +3974,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.8.0" -source = "git+https://github.com/quinn-rs/quinn.git#feca8abebf2b358141a675931b98043c355d4271" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57098b1a3d2159d13dc3a98c0e3a5f8ab91ac3dd2471e52b1d712ea0c1085555" dependencies = [ "bytes", "rand", @@ -3798,8 +3993,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.2.0" -source = "git+https://github.com/quinn-rs/quinn.git#feca8abebf2b358141a675931b98043c355d4271" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47085d44ed35b0f4499a88e47d689a411d483845966124e187a8f3cbb39eeebb" dependencies = [ "libc", "quinn-proto", @@ -3908,9 +4104,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -3928,9 +4124,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "renderdoc-sys" @@ -3968,11 +4164,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bitflags", "serde", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4027,7 +4229,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] @@ -4058,9 +4260,9 @@ dependencies = [ [[package]] name = "scc" -version = "0.11.1" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054da6411906a6cfa45a4b5cc922a4fdfd395f4fd17d98ef4603d334b83d3639" +checksum = "05e2cbd3b93f18dc6ec37f938c5f1914c072675a7f20a5cae7cfd8ec2294f0e3" dependencies = [ "scopeguard", ] @@ -4077,9 +4279,9 @@ dependencies = [ [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" @@ -4189,9 +4391,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" dependencies = [ "indexmap", "itoa", @@ -4215,9 +4417,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8613d593412a0deb7bbd8de9d908efff5a0cb9ccd8f62c641e7b2ed2f57291d1" +checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da" dependencies = [ "indexmap", "itoa", @@ -4436,9 +4638,9 @@ dependencies = [ [[package]] name = "swc_atoms" -version = "0.4.21" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ffecde9d1a937a61b4799478d598121b539eb7da6127c16af86a044017e1e5" +checksum = "63b8033a868fbebf5829797ac0c543499622b657e2d33a08ca6ab12547b8bafc" dependencies = [ "once_cell", "rustc-hash", @@ -4611,7 +4813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b57461fea819904faf5aeac39e49229995701a31fa5041929b7909885a69cc0a" dependencies = [ "ahash 0.7.6", - "base64 0.13.0", + "base64 0.13.1", "dashmap", "indexmap", "once_cell", @@ -4811,23 +5013,33 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "sys-locale" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658ee915b6c7b73ec4c1ffcd838506b5c5a4087eadc1ec8f862f1066cf2c8132" +checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee" dependencies = [ - "cc", - "cstr_core", "js-sys", "libc", "wasm-bindgen", @@ -4899,19 +5111,28 @@ dependencies = [ [[package]] name = "time" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "libc", - "num_threads", + "serde", + "time-core", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "tinystr" -version = "0.3.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1" +checksum = "f8aeafdfd935e4a7fe16a91ab711fa52d54df84f9c8f7ca5837a9d1d902ef4c2" +dependencies = [ + "displaydoc", +] [[package]] name = "tinyvec" @@ -4930,9 +5151,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg", "bytes", @@ -5053,9 +5274,20 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.15.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" +checksum = "375812fa44dab6df41c195cd2f7fecb488f6c09fbaafb62807488cefab642bff" + +[[package]] +name = "turborand" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d9ba4859e1ed0ef7788f3c8473529cbf69ec83abb6ad9ffab63ff790576d05" +dependencies = [ + "getrandom", + "instant", + "serde", +] [[package]] name = "type-map" @@ -5099,18 +5331,18 @@ dependencies = [ [[package]] name = "unic-langid" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73328fcd730a030bdb19ddf23e192187a6b01cd98be6d3140622a89129459ce5" +checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" dependencies = [ "unic-langid-impl", ] [[package]] name = "unic-langid-impl" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a4a8eeaf0494862c1404c95ec2f4c33a2acff5076f64314b465e3ddae1b934d" +checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" dependencies = [ "serde", "tinystr", @@ -5190,9 +5422,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom", "serde", @@ -5218,6 +5450,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "varinteger" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea29db9f94ff08bb619656b8120878f280526f71dc88b5262c958a510181812" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 120f971d89..5359f06d9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,69 +23,74 @@ render = [ "bevy/bevy_gilrs", ] -[dependencies.bevy] -version = "0.8" -default-features = false -features = [ - "x11", - "png", - "filesystem_watcher", - "bevy_gilrs" -] - [dependencies] -blocking = "1.2.0" +tracing = { version = "0.1.37", features = ["release_max_level_debug"]} +log = { version = "0.4.17", features = ["release_max_level_debug"] } anyhow = "1.0.58" +async-channel = "1.7.1" +base64 = "0.13.0" +bevy-has-load-progress = { path = "crates/bevy-has-load-progress", features = ["bevy_egui"] } +bevy-inspector-egui = { version = "0.13.0" } bevy-parallax = "0.2.0" +bevy_ecs_dynamic = { git = "https://github.com/jakobhellermann/bevy_ecs_dynamic", rev = "aa7a051a49bd134cfd8c169542e16526bf2b36b3" } +bevy_ecs_tilemap = { version = "0.7.0", features = ["atlas"] } bevy_egui = "0.16.1" -egui_extras = "0.19.0" -bevy_kira_audio = { version = "0.12.0", features = ["mp3"] } -iyes_loopless = "0.7.0" -serde = { version = "1.0.137", features = ["derive"] } -serde_yaml = "0.9.2" -thiserror = "1.0.31" -clap = { version = "4.0.18", features = ["derive", "env"] } -rand = "0.8.5" -getrandom = { version = "0.2", features = ["js"] } -bevy-has-load-progress = { path = "crates/bevy-has-load-progress", features = ["bevy_egui"] } -leafwing-input-manager = { version = "0.5.2", default-features = false } -unic-langid = "0.9.0" bevy_fluent = "0.4.0" -sys-locale = "0.2.1" -fluent = "0.16.0" -directories = "4.0.1" -async-channel = "1.7.1" -once_cell = "1.13.0" +bevy_ggrs = { git = "https://github.com/zicklag/bevy_ggrs.git", branch = "jumpy" } +bevy_kira_audio = { version = "0.12.0", features = ["mp3"] } bevy_mod_js_scripting = { git = "https://github.com/zicklag/bevy_mod_js_scripting.git", branch = "jumpy" } -bevy_ecs_tilemap = { version = "0.7.0", features = ["atlas"] } bevy_prototype_lyon = "0.6.0" +bevy_tweening = { version = "0.5", default-features = false } +bitfield = "0.14.0" +blocking = "1.2.0" +bytemuck = "1.12.3" +bytes = "1.2.1" +clap = { version = "4.0.18", features = ["derive", "env"] } +directories = "4.0.1" +egui_extras = "0.19.0" +either = "1.8.0" +fluent = "0.16.0" fnv = "1.0.7" -base64 = "0.13.0" -bevy_ecs_dynamic = { git = "https://github.com/jakobhellermann/bevy_ecs_dynamic" } -postcard = { git = "https://github.com/zicklag/postcard.git", branch = "custom-error-messages", default-features = false, features = ["alloc", "use-std"] } futures-lite = "1.12.0" -either = "1.8.0" +getrandom = { version = "0.2", features = ["js"] } +iyes_loopless = "0.8.0" jumpy-matchmaker-proto = { path = "crates/matchmaker-proto" } -bevy_tweening = { version= "0.5", default-features = false } - -# Debug tools -bevy-inspector-egui = { version = "0.13.0" } -ulid = { version = "1.0.0", features = ["serde"] } -bytes = "1.2.1" -rustls = { version = "0.20.7", features = ["dangerous_configuration", "quic"] } +leafwing-input-manager = { version = "0.6.1", default-features = false } normalize-path = "0.2.0" +numquant = "0.2.0" +once_cell = "1.13.0" +postcard = { git = "https://github.com/zicklag/postcard.git", branch = "custom-error-messages", default-features = false, features = ["alloc", "use-std"] } +rand = "0.8.5" +rustls = { version = "0.20.7", features = ["dangerous_configuration", "quic"] } +serde = { version = "1.0.137", features = ["derive"] } +serde_yaml = "0.9.2" +sys-locale = "0.2.1" +thiserror = "1.0.31" +unic-langid = "0.9.0" +turborand = { version = "0.8.0", features = ["atomic", "serialize"] } +mimalloc = "0.1.32" + +[dependencies.bevy] +version = "0.8" +default-features = false +features = [ + "x11", + "png", + "filesystem_watcher", + "bevy_gilrs" +] [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2.83" web-sys = { version = "0.3", features = ["Window","Location","Storage"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -quinn = { git = "https://github.com/quinn-rs/quinn.git", default-features = false, features = ["futures-io", "native-certs", "tls-rustls"] } +quinn = { version = "0.9", default-features = false, features = ["futures-io", "native-certs", "tls-rustls"] } quinn-bevy = { path = "crates/quinn-bevy" } [profile.dev.package."*"] -opt-level = 1 # Set this to 3 if the game becomes slow to respond during gameplay -debug = false +opt-level = 3 # Set this to 3 if the game becomes slow to respond during gameplay +debug = true [profile.release] lto = true diff --git a/assets/default.game.yaml b/assets/default.game.yaml index 051a2d7112..d1dc771ba9 100644 --- a/assets/default.game.yaml +++ b/assets/default.game.yaml @@ -1,7 +1,7 @@ camera_height: 448 clear_color: 27233B physics: - terminal_velocity: 20 + terminal_velocity: 30 friction_lerp: 0.96 stop_threshold: 1.0 @@ -30,15 +30,6 @@ maps: scripts: - map/scripts/kill_out_of_bounds.ts - - player/states/default.ts - - player/states/dead.ts - - player/states/idle.ts - - player/states/walk.ts - - player/states/midair.ts - - player/states/crouch.ts - -client_scripts: - - game/camera_controller.ts - ui/menu-background-zoom.ts main_menu: diff --git a/assets/map/elements/environment/crab/crab.element.yaml b/assets/map/elements/environment/crab/crab.element.yaml index 4457b44bc0..8e818d5ea9 100644 --- a/assets/map/elements/environment/crab/crab.element.yaml +++ b/assets/map/elements/environment/crab/crab.element.yaml @@ -1,7 +1,7 @@ name: Crab category: Critters scripts: - - ./crab.ts + # - ./crab.ts editor_size: [17, 12] preload_assets: - ./crab.atlas.yaml diff --git a/assets/map/elements/environment/crab/crab.ts b/assets/map/elements/environment/crab/crab.ts index 0c3d104a0b..9b0448186a 100644 --- a/assets/map/elements/environment/crab/crab.ts +++ b/assets/map/elements/environment/crab/crab.ts @@ -1,26 +1,22 @@ -const initState: { crabs: JsEntity[] } = { - crabs: [], -}; - const MapMeta: BevyType = { typeName: "jumpy::metadata::map::MapMeta", }; let i = 0; -const state = Script.state(initState); +const CRABS = "crabs"; export default { preUpdateInGame() { const mapQuery = world.query(MapMeta)[0]; if (!mapQuery) { - state.crabs = []; + Script.clearEntityList(CRABS); return; } const spawnedEntities = MapElement.getSpawnedEntities(); if (spawnedEntities.length > 0) { - state.crabs = []; + Script.clearEntityList(CRABS); } // Handle newly spawned map entities @@ -30,8 +26,8 @@ export default { .get(spanwer_entity); // Spawn a new entity for the crab and copy the transform and visibility from the map element - const entity = world.spawn(); - state.crabs.push(EntityRef.toJs(entity)); + const entity = WorldTemp.spawn(); + Script.addEntityToList(CRABS, entity); world.insert(entity, Value.create(EntityName, ["Critter: Crab"])); world.insert(entity, transform); @@ -73,15 +69,15 @@ export default { i++; const query = world.query(KinematicBody); - for (const crab of state.crabs) { - const components = query.get(EntityRef.fromJs(crab)); + for (const crab of Script.getEntityList(CRABS)) { + const components = query.get(crab); if (!components) continue; const [kinematicBody] = components; if (i % 100 == 0) { i = 0; kinematicBody.velocity.x = - Math.random() * 3 * (Math.random() >= 0.5 ? -1 : 1); + Random.gen() * 3 * (Random.gen() >= 0.5 ? -1 : 1); } } }, diff --git a/assets/map/elements/environment/fish_school/fish_school.element.yaml b/assets/map/elements/environment/fish_school/fish_school.element.yaml index 7534ee65c5..b40f5763aa 100644 --- a/assets/map/elements/environment/fish_school/fish_school.element.yaml +++ b/assets/map/elements/environment/fish_school/fish_school.element.yaml @@ -1,4 +1,4 @@ name: Fish School category: Critters scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/environment/player_spawner/player_spawner.element.yaml b/assets/map/elements/environment/player_spawner/player_spawner.element.yaml index 573eb54f84..7a69159725 100644 --- a/assets/map/elements/environment/player_spawner/player_spawner.element.yaml +++ b/assets/map/elements/environment/player_spawner/player_spawner.element.yaml @@ -1,5 +1,4 @@ name: Player Spawner category: Gampelay -scripts: - - ./player_spawner.ts +builtin: !PlayerSpawner editor_size: [16, 32] diff --git a/assets/map/elements/environment/player_spawner/player_spawner.ts b/assets/map/elements/environment/player_spawner/player_spawner.ts index eb1dbb9f3f..27fc1e6255 100644 --- a/assets/map/elements/environment/player_spawner/player_spawner.ts +++ b/assets/map/elements/environment/player_spawner/player_spawner.ts @@ -1,25 +1,24 @@ -const initState: { spawners: JsEntity[]; currentSpawner: number } = { +const initState: { currentSpawner: number } = { currentSpawner: 0, - spawners: [], }; const state = Script.state(initState); export default { preUpdate() { - if (NetInfo.get().is_client) return; - const player_inputs = world.resource(PlayerInputs); const mapQuery = world.query(MapMeta)[0]; if (!mapQuery) { - state.spawners = []; + Script.clearEntityList("playerSpawners"); return; } const spawnedEntities = MapElement.getSpawnedEntities(); if (spawnedEntities.length > 0) { - state.spawners = spawnedEntities.map((e) => EntityRef.toJs(e)); + spawnedEntities.forEach((e) => + Script.addEntityToList("playerSpawners", e) + ); } // Collect all the alive players on the map @@ -30,19 +29,21 @@ export default { // Get the player input const player = player_inputs.players[i]; + const spawners = Script.getEntityList("playerSpawners"); + // If the player is active, but not alive if (player.active && !alive_players.includes(i)) { // Get the next spawner state.currentSpawner += 1; - state.currentSpawner %= state.spawners.length; + state.currentSpawner %= spawners.length; - const spawner = EntityRef.fromJs(state.spawners[state.currentSpawner]); + const spawner = spawners[state.currentSpawner]; // Get the spawner transform const [spawnerTransform] = world.query(Transform).get(spawner); // Spawn the player - const player = world.spawn(); + const player = WorldTemp.spawn(); world.insert(player, Value.create(PlayerIdx, [i])); world.insert(player, spawnerTransform); } diff --git a/assets/map/elements/environment/sproinger/sproinger.element.yaml b/assets/map/elements/environment/sproinger/sproinger.element.yaml index f42a17486f..65ea0d519e 100644 --- a/assets/map/elements/environment/sproinger/sproinger.element.yaml +++ b/assets/map/elements/environment/sproinger/sproinger.element.yaml @@ -1,6 +1,4 @@ name: Sproinger category: Gameplay -scripts: - - ./sproinger.ts -preload_assets: - - ./sproinger.atlas.yaml \ No newline at end of file +builtin: !Sproinger + atlas: ./sproinger.atlas.yaml \ No newline at end of file diff --git a/assets/map/elements/environment/sproinger/sproinger.ts b/assets/map/elements/environment/sproinger/sproinger.ts index bb49f57745..10d48d3650 100644 --- a/assets/map/elements/environment/sproinger/sproinger.ts +++ b/assets/map/elements/environment/sproinger/sproinger.ts @@ -4,14 +4,6 @@ type SproingerState = { frame: number; }; -// Initialize script state. -// -// This will be used to keep track of which entities in the world are sproingers. -const initState: { sproingers: JsEntity[] } = { - sproingers: [], -}; -const state = Script.state(initState); - // Add our constants const FORCE = 25; @@ -21,8 +13,8 @@ export default { const map = world.query(MapMeta)[0]; // If there is no map if (!map) { - // lear our sproinger list - state.sproingers = []; + // clear our sproinger list + Script.clearEntityList("sproingers"); return; } @@ -31,7 +23,7 @@ export default { // If there are spawned entities, that means the map was loaded or reloaded. if (spawnedEntities.length > 0) { // So clear our sproinger list - state.sproingers = []; + Script.clearEntityList("sproingers"); } // For every new sproinger entity @@ -40,7 +32,7 @@ export default { // // Note: Because we cannot persist entity refs across frames, // we must first convert the entity to a JSON representation. - state.sproingers.push(EntityRef.toJs(entity)); + Script.addEntityToList("sproingers", entity); // Add the sprite world.insert( @@ -77,10 +69,7 @@ export default { const animatedSprites = world.query(AnimatedSprite); // Loop over all our sproingers - for (const jsEntity of state.sproingers) { - // We must convert our JsEntities to entity refs to use in world queries. - const entity = EntityRef.fromJs(jsEntity); - + for (const entity of Script.getEntityList("sproingers")) { // Get our sproinger sprite const [sprite] = animatedSprites.get(entity); diff --git a/assets/map/elements/item/blunderbass/blunderbass.element.yaml b/assets/map/elements/item/blunderbass/blunderbass.element.yaml index 1dc32194b5..8f6760fb95 100644 --- a/assets/map/elements/item/blunderbass/blunderbass.element.yaml +++ b/assets/map/elements/item/blunderbass/blunderbass.element.yaml @@ -1,4 +1,4 @@ name: Blunderbass category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/cannon/cannon.element.yaml b/assets/map/elements/item/cannon/cannon.element.yaml index c1a53b802a..ee74a4a546 100644 --- a/assets/map/elements/item/cannon/cannon.element.yaml +++ b/assets/map/elements/item/cannon/cannon.element.yaml @@ -1,4 +1,4 @@ name: Canon category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/crate/crate.element.yaml b/assets/map/elements/item/crate/crate.element.yaml index a546ff8855..5db25a2c33 100644 --- a/assets/map/elements/item/crate/crate.element.yaml +++ b/assets/map/elements/item/crate/crate.element.yaml @@ -1,4 +1,4 @@ name: Crate category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/grenades/grenades.element.yaml b/assets/map/elements/item/grenades/grenades.element.yaml index cdaac718c9..339fa41466 100644 --- a/assets/map/elements/item/grenades/grenades.element.yaml +++ b/assets/map/elements/item/grenades/grenades.element.yaml @@ -1,4 +1,4 @@ name: Grenades category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/kick_bomb/kick_bomb.element.yaml b/assets/map/elements/item/kick_bomb/kick_bomb.element.yaml index ee9f04b385..9d92f99c02 100644 --- a/assets/map/elements/item/kick_bomb/kick_bomb.element.yaml +++ b/assets/map/elements/item/kick_bomb/kick_bomb.element.yaml @@ -1,8 +1,8 @@ name: Kick Bomb category: Weapons scripts: - - ./kick_bomb_spawner.ts - - ./kick_bomb.ts + # - ./kick_bomb_spawner.ts + # - ./kick_bomb.ts preload_assets: - ./kick_bomb.atlas.yaml - ./explosion.atlas.yaml \ No newline at end of file diff --git a/assets/map/elements/item/kick_bomb/kick_bomb.ts b/assets/map/elements/item/kick_bomb/kick_bomb.ts index dbf6d9c22b..4911b17ca9 100644 --- a/assets/map/elements/item/kick_bomb/kick_bomb.ts +++ b/assets/map/elements/item/kick_bomb/kick_bomb.ts @@ -3,12 +3,8 @@ const scriptPath = Script.getInfo().path; type LitBombState = { frames: number; }; -type ScriptState = { - litBombs: JsEntity[]; -}; -const scriptState = Script.state({ - litBombs: [], -}); + +const LIT_BOMBS = "kickBombsLit"; export default { preUpdateInGame() { @@ -57,6 +53,7 @@ export default { const players = world.query( AnimatedSprite, Transform, + KinematicBody, PlayerIdx, GlobalTransform, ComputedVisibility @@ -75,6 +72,8 @@ export default { Visibility, ComputedVisibility ); + const usedItems = world.query(ItemUsed); + const droppedItems = world.query(ItemDropped); // Update items that are being held // @@ -106,74 +105,72 @@ export default { x: 13 * flipFactor, y: 0, }); - } - - // For every item that is being used - for (const event of Items.useEvents()) { - let parentComponents = parents.get(event.item); - // If this item isn't being held, skip the item - if (!parentComponents) continue; - // Get the player info - const [parent] = parentComponents; - const [ - playerSprite, - transform, - _idx, - globalTransform, - computedVisibility, - ] = players.get(parent[0]); - const flip = playerSprite.flip_x; - const flipFactor = flip ? -1 : 1; + // For every item that is being used + if (!!usedItems.get(itemEnt)) { + // Get the player info + const [parent] = parentComponents; + const playerEnt = parent[0]; + const [ + playerSprite, + transform, + _body, + _idx, + globalTransform, + computedVisibility, + ] = players.get(playerEnt); + const flip = playerSprite.flip_x; + const flipFactor = flip ? -1 : 1; - // Despawn the item from the player's hand - WorldTemp.despawnRecursive(event.item); + // Despawn the item from the player's hand + Player.setInventory(playerEnt, null); + // WorldTemp.despawnRecursive(itemEnt); - // Spawn a new, lit bomb to the map - const entity = world.spawn(); - scriptState.litBombs.push(EntityRef.toJs(entity)); - world.insert(entity, transform); - world.insert(entity, globalTransform); - world.insert(entity, computedVisibility); - world.insert(entity, Value.create(Visibility)); + // Spawn a new, lit bomb to the map + const entity = WorldTemp.spawn(); + Script.addEntityToList(LIT_BOMBS, entity); + world.insert(entity, Value.create(EntityName, ["Kick Bomb ( Lit )"])); + world.insert(entity, transform); + world.insert(entity, globalTransform); + world.insert(entity, computedVisibility); + world.insert(entity, Value.create(Visibility)); - // Add the animated sprite - world.insert( - entity, - Value.create(AnimatedSprite, { - start: 3, - end: 5, - repeat: true, - fps: 8, - atlas: { - id: Assets.getHandleId("kick_bomb.atlas.yaml"), - }, - }) - ); - // And the kinematic body - world.insert( - entity, - Value.create(KinematicBody, { - size: { - x: 26, - y: 26, - }, - velocity: { - x: 10 * flipFactor, - }, - gravity: 1, - has_friction: true, - has_mass: true, - }) - ); + // Add the animated sprite + world.insert( + entity, + Value.create(AnimatedSprite, { + start: 3, + end: 5, + repeat: true, + fps: 8, + atlas: { + id: Assets.getHandleId("kick_bomb.atlas.yaml"), + }, + }) + ); + // And the kinematic body + world.insert( + entity, + Value.create(KinematicBody, { + size: { + x: 26, + y: 26, + }, + velocity: { + x: 10 * flipFactor, + }, + gravity: 1, + has_friction: true, + has_mass: true, + }) + ); + } } // Handle lit bombs - const litBombs = scriptState.litBombs; - scriptState.litBombs = []; - for (const jsEntity of litBombs) { - const bombEntity = EntityRef.fromJs(jsEntity); - + const litBombs = Script.getEntityList(LIT_BOMBS); + Script.clearEntityList(LIT_BOMBS); + for (const bombEntity of litBombs) { // Get the bomb's state const state = Script.getEntityState(bombEntity, { frames: 0, @@ -183,7 +180,7 @@ export default { if (state.frames >= 60) { // Spawn damage region entity - const damageRegionEnt = world.spawn(); + const damageRegionEnt = WorldTemp.spawn(); world.insert(damageRegionEnt, transform); world.insert(damageRegionEnt, globalTransform); world.insert(damageRegionEnt, visibility); @@ -200,11 +197,11 @@ export default { world.insert( damageRegionEnt, Value.create(Lifetime, { - lifetime: 1 / 9 * 4, + lifetime: (1 / 9) * 4, }) ); // Spawn explosion sprite entity - const explosionSpriteEnt = world.spawn(); + const explosionSpriteEnt = WorldTemp.spawn(); world.insert(explosionSpriteEnt, transform); world.insert(explosionSpriteEnt, globalTransform); world.insert(explosionSpriteEnt, visibility); @@ -224,7 +221,7 @@ export default { world.insert( explosionSpriteEnt, Value.create(Lifetime, { - lifetime: 1 / 9 * 11, + lifetime: (1 / 9) * 11, }) ); @@ -232,26 +229,39 @@ export default { WorldTemp.despawnRecursive(bombEntity); } else { state.frames += 1; - scriptState.litBombs.push(jsEntity); + Script.addEntityToList(LIT_BOMBS, bombEntity); } } // Update dropped items - for (const event of Items.dropEvents()) { - const [_item, itemTransform, body, sprite] = items.get(event.item); - let flip = sprite.flip_x; - let flipFactor = flip ? -1 : 1; + for (const { + entity: itemEnt, + components: [item], + } of items) { + if (item.script != scriptPath) continue; - // Re-activate physics body on the item - body.is_deactivated = false; - // Make sure item maintains player velocity - body.velocity = event.velocity; - body.is_spawning = true; + const droppedItemComponents = droppedItems.get(itemEnt); + if (!!droppedItemComponents) { + const [droppedItem] = droppedItemComponents; + const [_item, itemTransform, body, sprite] = items.get(itemEnt); + const [_, playerTransform, playerBody] = players.get( + droppedItem.player + ); + let flip = sprite.flip_x; + let flipFactor = flip ? -1 : 1; - // Drop item at the middle of the player - itemTransform.translation.y = event.position.y; - itemTransform.translation.x = event.position.x + 13 * flipFactor; - itemTransform.translation.z = event.position.z; + // Re-activate physics body on the item + body.is_deactivated = false; + // Make sure item maintains player velocity + body.velocity = playerBody.velocity; + body.is_spawning = true; + + // Drop item at the middle of the player + itemTransform.translation.y = playerTransform.translation.y; + itemTransform.translation.x = + playerTransform.translation.x + 13 * flipFactor; + itemTransform.translation.z = playerTransform.translation.z; + } } }, }; diff --git a/assets/map/elements/item/kick_bomb/kick_bomb_spawner.ts b/assets/map/elements/item/kick_bomb/kick_bomb_spawner.ts index 993455b857..5a16d6fbc4 100644 --- a/assets/map/elements/item/kick_bomb/kick_bomb_spawner.ts +++ b/assets/map/elements/item/kick_bomb/kick_bomb_spawner.ts @@ -1,8 +1,5 @@ export default { preUpdate() { - // Clients may not spawn items - if (NetInfo.get().is_client) return; - const spawnedEntities = MapElement.getSpawnedEntities(); // Handle newly spawned map entities @@ -12,7 +9,7 @@ export default { .get(spanwer_entity); // Spawn a new entity for the bomb item and copy the transform from the map element - const entity = world.spawn(); + const entity = WorldTemp.spawn(); world.insert( entity, Value.create(Item, { diff --git a/assets/map/elements/item/machine_gun/machine_gun.element.yaml b/assets/map/elements/item/machine_gun/machine_gun.element.yaml index a9d409eea8..e27ed529f1 100644 --- a/assets/map/elements/item/machine_gun/machine_gun.element.yaml +++ b/assets/map/elements/item/machine_gun/machine_gun.element.yaml @@ -1,4 +1,4 @@ name: Machine Gun category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/mines/mines.element.yaml b/assets/map/elements/item/mines/mines.element.yaml index 1f2ecac688..9ec2ea0d61 100644 --- a/assets/map/elements/item/mines/mines.element.yaml +++ b/assets/map/elements/item/mines/mines.element.yaml @@ -1,4 +1,4 @@ name: Mines category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/musket/musket.element.yaml b/assets/map/elements/item/musket/musket.element.yaml index e0917b5f8d..f0b0a09c39 100644 --- a/assets/map/elements/item/musket/musket.element.yaml +++ b/assets/map/elements/item/musket/musket.element.yaml @@ -1,4 +1,4 @@ name: Musket category: Weapons scripts: - - ./musket.ts + # - ./musket.ts diff --git a/assets/map/elements/item/sniper_rifle/sniper_rifle.element.yaml b/assets/map/elements/item/sniper_rifle/sniper_rifle.element.yaml index 2985d0bf15..58680c0180 100644 --- a/assets/map/elements/item/sniper_rifle/sniper_rifle.element.yaml +++ b/assets/map/elements/item/sniper_rifle/sniper_rifle.element.yaml @@ -1,4 +1,4 @@ name: Sniper Rifle category: Weapons scripts: - - ./sproinger.ts + # - ./sproinger.ts diff --git a/assets/map/elements/item/sword/sword.element.yaml b/assets/map/elements/item/sword/sword.element.yaml index cad9647d32..a8c2c9e06a 100644 --- a/assets/map/elements/item/sword/sword.element.yaml +++ b/assets/map/elements/item/sword/sword.element.yaml @@ -1,7 +1,5 @@ name: Sword category: Weapons -scripts: - - ./sword_spawner.ts - - ./sword.ts -preload_assets: - - ./sword.atlas.yaml +builtin: !Sword + atlas: ./sword.atlas.yaml + \ No newline at end of file diff --git a/assets/map/elements/item/sword/sword.ts b/assets/map/elements/item/sword/sword.ts index b9ebbff3f5..22f82660ca 100644 --- a/assets/map/elements/item/sword/sword.ts +++ b/assets/map/elements/item/sword/sword.ts @@ -56,15 +56,16 @@ export default { }, updateInGame() { - const players = world.query(AnimatedSprite, Transform, PlayerIdx); - const parents = world.query(Parent); - const items = world.query( - Item, + const players = world.query( + AnimatedSprite, Transform, KinematicBody, - AnimatedSprite, - GlobalTransform + PlayerIdx ); + const parents = world.query(Parent); + const items = world.query(Item, Transform, KinematicBody, AnimatedSprite); + const usedItems = world.query(ItemUsed); + const droppedItems = world.query(ItemDropped); // Helper to spawn a damage region const spawnDamageRegion = ( @@ -76,16 +77,11 @@ export default { ) => { /// This is a hack to get a global transform because scripts can't construct it with /// Value.create(). ( Fixed in Bevy 0.9 ) - const [ - _item, - _transform, - _kinematicBody, - _animatedSprite, - globalTransform, - ] = items[0].components; + const [_item, _transform, _kinematicBody, _animatedSprite] = + items[0].components; // Spawn damage region entity - let entity = world.spawn(); + let entity = WorldTemp.spawn(); world.insert( entity, Value.create(Transform, { @@ -95,7 +91,6 @@ export default { }, }) ); - world.insert(entity, globalTransform); world.insert( entity, Value.create(DamageRegion, { @@ -210,46 +205,58 @@ export default { state.frame += 1; } } - } - // For every item that is being used - for (const event of Items.useEvents()) { - // Get the current item state - const state = Script.getEntityState(event.item, itemStateInit); + // If the item is being used + if (!!usedItems.get(itemEnt)) { + // Get the current item state + const state = Script.getEntityState(itemEnt, itemStateInit); - if (state.status == "idle") { - const [_item, _itemTransform, _body, sprite] = items.get(event.item); + if (state.status == "idle") { + const [_item, _itemTransform, _body, sprite] = items.get(itemEnt); - // Start attacking animation - sprite.index = 0; - sprite.start = 8; - sprite.end = 12; - - // And move to an attacking state - Script.setEntityState(event.item, { - status: "swinging", - frame: 0, - }); + // Start attacking animation + sprite.index = 0; + sprite.start = 8; + sprite.end = 12; + + // And move to an attacking state + Script.setEntityState(itemEnt, { + status: "swinging", + frame: 0, + }); + } } } // Update dropped items - for (const event of Items.dropEvents()) { - const [_item, itemTransform, body, sprite] = items.get(event.item); - - // Re-activate physics body on the item - body.is_deactivated = false; - // Put sword in rest position - sprite.start = 0; - sprite.end = 0; - // Make sure item maintains player velocity - body.velocity = event.velocity; - body.is_spawning = true; - - // Drop item at the middle of the player - itemTransform.translation.y = event.position.y - 30; - itemTransform.translation.x = event.position.x; - itemTransform.translation.z = event.position.z; + for (const { + entity: itemEnt, + components: [item], + } of items) { + if (item.script != scriptPath) continue; + + const droppedItemComponents = droppedItems.get(itemEnt); + if (!!droppedItemComponents) { + const [droppedItem] = droppedItemComponents; + const [_item, itemTransform, body, sprite] = items.get(itemEnt); + const [_, playerTransform, playerBody] = players.get( + droppedItem.player + ); + + // Re-activate physics body on the item + body.is_deactivated = false; + // Put sword in rest position + sprite.start = 0; + sprite.end = 0; + // Make sure item maintains player velocity + body.velocity = playerBody.velocity; + body.is_spawning = true; + + // Drop item at the middle of the player + itemTransform.translation.y = playerTransform.translation.y - 30; + itemTransform.translation.x = playerTransform.translation.x; + itemTransform.translation.z = playerTransform.translation.z; + } } }, }; diff --git a/assets/map/elements/item/sword/sword_spawner.ts b/assets/map/elements/item/sword/sword_spawner.ts index 469d1363d7..77d916b47c 100644 --- a/assets/map/elements/item/sword/sword_spawner.ts +++ b/assets/map/elements/item/sword/sword_spawner.ts @@ -1,8 +1,5 @@ export default { preUpdate() { - // Clients may not spawn items - if (NetInfo.get().is_client) return; - const spawnedEntities = MapElement.getSpawnedEntities(); // Handle newly spawned map entities @@ -12,7 +9,7 @@ export default { .get(spanwer_entity); // Spawn a new entity for the sword and copy the transform and visibility from the map element - const entity = world.spawn(); + const entity = WorldTemp.spawn(); world.insert( entity, Value.create(Item, { diff --git a/assets/map/scripts/kill_out_of_bounds.ts b/assets/map/scripts/kill_out_of_bounds.ts index d8c8750c01..4569b7c234 100644 --- a/assets/map/scripts/kill_out_of_bounds.ts +++ b/assets/map/scripts/kill_out_of_bounds.ts @@ -14,19 +14,14 @@ export default { for (const { entity, components } of world.query(PlayerIdx, Transform)) { let [player_idx, transform] = components; + let pos = transform.translation; + if ( - (netInfo.is_client && player_idx[0] == netInfo.player_idx) || - !netInfo.is_client + pos.x < leftKillZone || + pos.x > rightKillZone || + pos.y < bottomKillZone ) { - let pos = transform.translation; - - if ( - pos.x < leftKillZone || - pos.x > rightKillZone || - pos.y < bottomKillZone - ) { - Player.kill(entity); - } + Player.kill(entity); } } }, diff --git a/assets/player/states/README.md b/assets/player/states/README.md new file mode 100644 index 0000000000..c016d744a4 --- /dev/null +++ b/assets/player/states/README.md @@ -0,0 +1,3 @@ +# Player States + +These are the TypeScript implementations of the player states. For now they are unused as we migrated them to Rust for performance reasons. \ No newline at end of file diff --git a/assets/player/states/dead.ts b/assets/player/states/dead.ts index aa8e4258bb..737269b19e 100644 --- a/assets/player/states/dead.ts +++ b/assets/player/states/dead.ts @@ -1,14 +1,19 @@ const scriptId = Script.getInfo().path; +const DYING_PLAYERS = "dyingPlayers"; + /** Responsible for transitioning players to the dead state whenever they are killed */ export default { playerStateTransition() { - const players = world.query(PlayerState); + const players = world.query(PlayerState, PlayerKilled); - // Transition all players tht have been killed to this state - for (const event of Player.killEvents()) { - const [playerState] = players.get(event.player); - playerState.id = Assets.getAbsolutePath("./dead.ts"); + // Transition all players that have been killed to this state + for (const { entity } of players) { + if (!Script.entityListContains(DYING_PLAYERS, entity)) { + const [playerState] = players.get(entity); + playerState.id = Assets.getAbsolutePath("./dead.ts"); + Script.addEntityToList(DYING_PLAYERS, entity); + } } }, handlePlayerState() { @@ -28,6 +33,7 @@ export default { // Despawn player after 1.5 seconds ( 90 frames ) if (playerState.age >= 90) { + Script.removeEntityFromList(DYING_PLAYERS, entity); Player.despawn(entity); Script.setEntityState(entity, undefined); } diff --git a/crates/bevy-has-load-progress/src/lib.rs b/crates/bevy-has-load-progress/src/lib.rs index 0c34eba97b..0ce9228a53 100644 --- a/crates/bevy-has-load-progress/src/lib.rs +++ b/crates/bevy-has-load-progress/src/lib.rs @@ -5,6 +5,8 @@ //! inside, maybe deeply nested or stored in vectors, etc., and you need to be able to get the load //! progress of _all_ of the handles inside that struct. +#![allow(clippy::bool_to_int_with_if)] + use std::marker::PhantomData; use bevy::{ diff --git a/crates/matchmaker-proto/src/lib.rs b/crates/matchmaker-proto/src/lib.rs index aabce1eb46..3fc75256cc 100644 --- a/crates/matchmaker-proto/src/lib.rs +++ b/crates/matchmaker-proto/src/lib.rs @@ -1,5 +1,13 @@ use serde::{Deserialize, Serialize}; +// +// === Matchmaking Mode === +// +// These are messages sent when first establishing a connecting to the matchmaker and waiting for a +// match. +// + +/// Requests that may be made in matchmaking mode #[derive(Serialize, Deserialize, Debug, Clone)] pub enum MatchmakerRequest { /// Request a match ID from the server @@ -9,12 +17,62 @@ pub enum MatchmakerRequest { /// Information about a match that is being requested #[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] pub struct MatchInfo { - pub player_count: u8, + /// The number of clients to have in a match + pub client_count: u8, + /// This is an arbitrary set of bytes that must match exactly for clients to end up in the same + /// match. + /// + /// This allows us to support matchmaking for different games/modes with the same matchmaking + /// server. + pub match_data: Vec, } +/// Responses that may be returned in matchmaking mode #[derive(Serialize, Deserialize, Debug, Clone)] pub enum MatchmakerResponse { + /// The conneciton has been accepted Accepted, - PlayerCount(u8), - Success, + /// This is the current number of connected clients + ClientCount(u8), + /// The desired client count has been reached, and you may start the match. + Success { + /// The random seed that each client should use. + random_seed: u64, + /// The client idx of the current client + player_idx: u8, + /// The number of connected clients in the match + client_count: u8, + }, +} + +// +// === Proxy mode === +// +// These are messages sent after the match has been made and the clients are sending messages to +// each-other. +// + +/// The format of a message sent by a client to the proxy, so the proxy can send it to another +/// client. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SendProxyMessage { + /// The client that the message should go to. + pub target_client: TargetClient, + /// The message data. + pub message: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum TargetClient { + All, + One(u8), +} + +/// The format of a message forwarded by the proxy to a client. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RecvProxyMessage { + /// The client that the message came from. + pub from_client: u8, + /// The message data. + pub message: Vec, } diff --git a/crates/matchmaker/Cargo.toml b/crates/matchmaker/Cargo.toml index 07b2e146c2..adfd6ddf6a 100644 --- a/crates/matchmaker/Cargo.toml +++ b/crates/matchmaker/Cargo.toml @@ -5,13 +5,12 @@ edition = "2021" authors = ["The Fish Fight Game & Spicy Lobster Developers"] [dependencies] -jumpy = { path = "../../", default-features = false } jumpy-matchmaker-proto = { path = "../matchmaker-proto" } anyhow = "1.0.66" async-executor = "1.4.1" futures-lite = "1.12.0" once_cell = "1.15.0" -quinn = { git = "https://github.com/quinn-rs/quinn.git", default-features = false, features = ["futures-io", "native-certs", "tls-rustls"] } +quinn = { version = "0.9", default-features = false, features = ["futures-io", "native-certs", "tls-rustls"] } quinn-bevy = { path = "../quinn-bevy" } bevy_tasks = "0.8.1" rcgen = "0.10.0" @@ -23,8 +22,10 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } clap = { version = "4.0.18", features = ["derive", "env"] } tokio = { version = "1.0", features = ["full"] } either = "1.8.0" -blocking = "1.2.0" scc = "0.11.1" +bytes = "1.2.1" +futures = { version = "0.3.25", default-features = false, features = ["std", "async-await"] } +rand = "0.8.5" [dev-dependencies] async-io = "1.9.0" diff --git a/crates/matchmaker/examples/matchmaker_client.rs b/crates/matchmaker/examples/matchmaker_client.rs index beab74470a..a9a3b2961a 100644 --- a/crates/matchmaker/examples/matchmaker_client.rs +++ b/crates/matchmaker/examples/matchmaker_client.rs @@ -2,10 +2,10 @@ use std::{net::SocketAddr, sync::Arc, time::Duration}; use bevy_tasks::{IoTaskPool, TaskPool}; use certs::SkipServerVerification; -use jumpy::networking::proto::{Ping, Pong}; use jumpy_matchmaker_proto::{MatchInfo, MatchmakerRequest, MatchmakerResponse}; use quinn::{ClientConfig, Endpoint, EndpointConfig}; use quinn_bevy::BevyIoTaskPoolExecutor; +use serde::{Deserialize, Serialize}; static SERVER_NAME: &str = "localhost"; @@ -17,6 +17,11 @@ fn server_addr() -> SocketAddr { "127.0.0.1:8943".parse::().unwrap() } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Hello { + i_am: String, +} + mod certs { use std::sync::Arc; @@ -72,10 +77,11 @@ async fn client() -> anyhow::Result<()> { None, socket, BevyIoTaskPoolExecutor, - )? - .0; + )?; - println!("Opened client on {}", endpoint.local_addr()?); + let i_am = std::env::args().nth(2).unwrap(); + let hello = Hello { i_am }; + println!("o Opened client on {}. {hello:?}", endpoint.local_addr()?); // Connect to the server passing in the server name which is supposed to be in the server certificate. let conn = endpoint @@ -86,26 +92,27 @@ async fn client() -> anyhow::Result<()> { let (mut send, recv) = conn.open_bi().await?; let message = MatchmakerRequest::RequestMatch(MatchInfo { - player_count: std::env::args() + client_count: std::env::args() .nth(1) .map(|x| x.parse().unwrap()) .unwrap_or(0), + match_data: b"example-client".to_vec(), }); - println!("Sending match request: {message:?}"); + println!("=> Sending match request: {message:?}"); let message = postcard::to_allocvec(&message)?; send.write_all(&message).await?; send.finish().await?; - println!("Waiting for response"); + println!("o Waiting for response"); let message = recv.read_to_end(256).await?; let message: MatchmakerResponse = postcard::from_bytes(&message)?; if let MatchmakerResponse::Accepted = message { - println!("Request accepted, waiting for match"); + println!("<= Request accepted, waiting for match"); } else { - panic!("Unexpected message from server!"); + panic!("<= Unexpected message from server!"); } loop { @@ -114,37 +121,68 @@ async fn client() -> anyhow::Result<()> { let message: MatchmakerResponse = postcard::from_bytes(&message)?; match message { - MatchmakerResponse::PlayerCount(count) => { - println!("{count} players in lobby"); + MatchmakerResponse::ClientCount(count) => { + println!("<= {count} players in lobby"); } - MatchmakerResponse::Success => { - println!("Match is ready!"); + MatchmakerResponse::Success { + random_seed, + player_idx, + client_count, + } => { + println!("<= Match is ready! Random seed: {random_seed}. Player IDX: {player_idx}. Client count: {client_count}"); break; } - _ => panic!("Unexpected message from server"), + _ => panic!("<= Unexpected message from server"), } } - async_io::Timer::after(Duration::from_secs(2)).await; - - for _ in 0..3 { - let mut sender = conn.open_uni().await?; - - println!("Sending ping to server"); - sender.write_all(&postcard::to_allocvec(&Ping)?).await?; - sender.write_all(&u32::to_le_bytes(0)).await?; - sender.finish().await?; - - println!("Waiting for pong"); - let recv = conn.accept_uni().await?; - let mut incomming = recv.read_to_end(256).await?; - let type_idx_bytes: [u8; 4] = incomming.split_off(incomming.len() - 4).try_into().unwrap(); - let type_idx = u32::from_le_bytes(type_idx_bytes); - assert_eq!(type_idx, 1, "Invalid type"); - let message: Pong = postcard::from_bytes(&incomming).unwrap(); + let task_pool = IoTaskPool::get(); - println!("Got message: {:?}", message); - } + let conn_ = conn.clone(); + task_pool + .spawn(async move { + let result = async move { + for _ in 0..3 { + println!("=> {hello:?}"); + let mut sender = conn_.open_uni().await?; + sender + .write_all(&postcard::to_allocvec(&hello.clone())?) + .await?; + sender.finish().await?; + + async_io::Timer::after(Duration::from_secs(1)).await; + } + + Ok::<_, anyhow::Error>(()) + }; + + if let Err(e) = result.await { + eprintln!("<= Error: {e:?}"); + } + }) + .detach(); + + let conn_ = conn.clone(); + task_pool + .spawn(async move { + loop { + let result = async { + let recv = conn_.accept_uni().await?; + + let incomming = recv.read_to_end(256).await?; + let message: Hello = postcard::from_bytes(&incomming).unwrap(); + + println!("<= {message:?}"); + + Ok::<_, anyhow::Error>(()) + }; + if let Err(e) = result.await { + eprintln!("Error: {e:?}"); + break; + } + } + }) + .detach(); async_io::Timer::after(Duration::from_secs(4)).await; diff --git a/crates/matchmaker/src/game_server.rs b/crates/matchmaker/src/game_server.rs deleted file mode 100644 index 3cc703ef83..0000000000 --- a/crates/matchmaker/src/game_server.rs +++ /dev/null @@ -1,24 +0,0 @@ -use blocking::unblock; -use jumpy::networking::server::NetServer; -use jumpy_matchmaker_proto::MatchInfo; -use quinn::Connection; - -pub async fn start_game_server(match_info: MatchInfo, clients: Vec) { - info!(?match_info, "Starting match"); - let client_ids = clients.iter().map(|x| x.stable_id()).collect::>(); - - if let Err(e) = impl_game_server(match_info.clone(), clients).await { - error!(?match_info, ?client_ids, "Error running match: {e}"); - } -} - -async fn impl_game_server(match_info: MatchInfo, clients: Vec) -> anyhow::Result<()> { - unblock(|| { - jumpy::build_app(Some(NetServer::new(clients))).run(); - }) - .await; - - info!(?match_info, "Game server finished"); - - Ok(()) -} diff --git a/crates/matchmaker/src/lib.rs b/crates/matchmaker/src/lib.rs index b5338dc4f3..5504277e51 100644 --- a/crates/matchmaker/src/lib.rs +++ b/crates/matchmaker/src/lib.rs @@ -10,8 +10,8 @@ use quinn_bevy::BevyIoTaskPoolExecutor; pub mod cli; mod certs; -mod game_server; mod matchmaker; +mod proxy; #[derive(clap::Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -19,19 +19,11 @@ struct Config { /// The server address to listen on #[clap(short, long = "listen", default_value = "0.0.0.0:8943")] listen_addr: SocketAddr, - - /// The directory containing the bevy assets - #[clap(short, long, env = "JUMPY_ASSET_DIR")] - asset_dir: String, } async fn server(args: Config) -> anyhow::Result<()> { let task_pool = IoTaskPool::get(); - // Put Jumpy in server mode - std::env::set_var(jumpy::config::SERVER_MODE_ENV_VAR, "true"); - std::env::set_var(jumpy::config::ASSET_DIR_ENV_VAR, args.asset_dir); - // Set allowed threads for blocking thread pool. This value represents the maxiumum number of // matches that can run at the same time. // @@ -49,7 +41,7 @@ async fn server(args: Config) -> anyhow::Result<()> { // Open Socket and create endpoint let socket = std::net::UdpSocket::bind(args.listen_addr)?; - let (endpoint, mut incoming) = Endpoint::new( + let endpoint = Endpoint::new( EndpointConfig::default(), Some(server_config), socket, @@ -58,7 +50,7 @@ async fn server(args: Config) -> anyhow::Result<()> { info!(address=%endpoint.local_addr()?, "Started server"); // Listen for incomming connections - while let Some(connecting) = incoming.next().await { + while let Some(connecting) = endpoint.accept().await { let connection = connecting.await; match connection { diff --git a/crates/matchmaker/src/matchmaker.rs b/crates/matchmaker/src/matchmaker.rs index bb201a9046..e5b6c56f6a 100644 --- a/crates/matchmaker/src/matchmaker.rs +++ b/crates/matchmaker/src/matchmaker.rs @@ -5,7 +5,7 @@ use once_cell::sync::Lazy; use quinn::{Connection, ConnectionError}; use scc::HashMap; -use crate::game_server::start_game_server; +use crate::proxy::start_proxy; pub async fn handle_connection(conn: Connection) { let connection_id = conn.stable_id(); @@ -72,7 +72,7 @@ async fn impl_matchmaker(conn: Connection) -> anyhow::Result<()> { send.write_all(&message).await?; send.finish().await?; - let player_count = match_info.player_count; + let player_count = match_info.client_count; let mut members_to_join = Vec::new(); let mut members_to_notify = Vec::new(); @@ -120,7 +120,7 @@ async fn impl_matchmaker(conn: Connection) -> anyhow::Result<()> { if let Some(members) = members { let result = async { let message = postcard::to_allocvec( - &MatchmakerResponse::PlayerCount( + &MatchmakerResponse::ClientCount( members.len() as u8 ), )?; @@ -154,7 +154,7 @@ async fn impl_matchmaker(conn: Connection) -> anyhow::Result<()> { .await; if !members_to_notify.is_empty() { - let message = postcard::to_allocvec(&MatchmakerResponse::PlayerCount( + let message = postcard::to_allocvec(&MatchmakerResponse::ClientCount( members_to_notify.len() as u8, ))?; for conn in members_to_notify { @@ -165,12 +165,17 @@ async fn impl_matchmaker(conn: Connection) -> anyhow::Result<()> { } if !members_to_join.is_empty() { - // Respond with success - let message = postcard::to_allocvec(&MatchmakerResponse::Success)?; - // Send the match ID to all of the clients in the room let mut clients = Vec::with_capacity(player_count as usize); - for conn in members_to_join.drain(..) { + let random_seed = rand::random(); + for (idx, conn) in members_to_join.drain(..).enumerate() { + // Respond with success + let message = + postcard::to_allocvec(&MatchmakerResponse::Success { + random_seed, + client_count: player_count, + player_idx: idx as u8, + })?; let mut send = conn.open_uni().await?; send.write_all(&message).await?; send.finish().await?; @@ -179,7 +184,7 @@ async fn impl_matchmaker(conn: Connection) -> anyhow::Result<()> { } // Hand the clients off to the game manager - start_game_server(match_info, clients).await; + start_proxy(match_info, clients).await; } } } diff --git a/crates/matchmaker/src/proxy.rs b/crates/matchmaker/src/proxy.rs new file mode 100644 index 0000000000..4447fe5632 --- /dev/null +++ b/crates/matchmaker/src/proxy.rs @@ -0,0 +1,141 @@ +use anyhow::format_err; +use bevy_tasks::IoTaskPool; +use bytes::Bytes; +use jumpy_matchmaker_proto::{MatchInfo, RecvProxyMessage, SendProxyMessage}; +use quinn::Connection; + +pub async fn start_proxy(match_info: MatchInfo, clients: Vec) { + info!(?match_info, "Starting match"); + let client_ids = clients.iter().map(|x| x.stable_id()).collect::>(); + + if let Err(e) = impl_proxy(match_info.clone(), clients).await { + error!(?match_info, ?client_ids, "Error running match: {e}"); + } +} + +async fn impl_proxy(match_info: MatchInfo, clients: Vec) -> anyhow::Result<()> { + let task_pool = IoTaskPool::get(); + + // For each connected client + for (i, conn) in clients.iter().enumerate() { + // Get the client connection + let conn = conn.clone(); + + // And all the connections to it's peers + let peers = clients.clone(); + + // Spawn a task for handling this client's reliable connections + let conn_ = conn.clone(); + let peers_ = peers.clone(); + task_pool + .spawn(async move { + let result = async { + loop { + // Wait for an incomming connection + let accept = conn_.accept_uni().await?; + + // Parse the message + let message = accept.read_to_end(1024).await?; + let message = postcard::from_bytes::(&message)?; + let target_client = message.target_client; + let message = message.message; + + let targets = match target_client { + jumpy_matchmaker_proto::TargetClient::All => peers_.clone(), + jumpy_matchmaker_proto::TargetClient::One(i) => vec![peers_ + .get(i as usize) + .cloned() + .ok_or_else(|| format_err!("Invalid target client: {i}"))?], + }; + + // Send message to all target clients + let mut send_tasks = Vec::with_capacity(targets.len()); + for target_client in targets { + let message_ = message.clone(); + let task = task_pool.spawn(async move { + // Send the message to the target client + let mut send = target_client.open_uni().await?; + let send_message = RecvProxyMessage { + from_client: i as u8, + message: message_, + }; + let send_message = postcard::to_allocvec(&send_message)?; + send.write_all(&send_message).await?; + send.finish().await?; + + Ok::<_, anyhow::Error>(()) + }); + + send_tasks.push(task); + } + futures::future::try_join_all(send_tasks).await?; + } + + #[allow(unreachable_code)] + Ok::<_, anyhow::Error>(()) + }; + + if let Err(e) = result.await { + warn!("Error in client connection: {e:?}"); + } + }) + .detach(); + + // Spawn task for handling the client's unreliable messages + // TODO: De-duplicate this code a little? + task_pool + .spawn(async move { + let result = async { + loop { + // Wait for an incomming connection + let bytes = conn.read_datagram().await?; + + // Parse the message + let message = postcard::from_bytes::(&bytes)?; + let target_client = message.target_client; + let message = message.message; + + let targets = match target_client { + jumpy_matchmaker_proto::TargetClient::All => peers.clone(), + jumpy_matchmaker_proto::TargetClient::One(i) => vec![peers + .get(i as usize) + .cloned() + .ok_or_else(|| format_err!("Invalid target client: {i}"))?], + }; + + // Send message to all target clients + let mut send_tasks = Vec::with_capacity(targets.len()); + // Send the message to the target client + let send_message = RecvProxyMessage { + from_client: i as u8, + message, + }; + let send_message = Bytes::from(postcard::to_allocvec(&send_message)?); + for target_client in targets { + let send_message = send_message.clone(); + let task = task_pool.spawn(async move { + target_client.send_datagram(send_message)?; + + Ok::<_, anyhow::Error>(()) + }); + + send_tasks.push(task); + } + futures::future::try_join_all(send_tasks).await?; + } + + #[allow(unreachable_code)] + Ok::<_, anyhow::Error>(()) + }; + + if let Err(e) = result.await { + warn!("Error in client connection: {e:?}"); + } + }) + .detach(); + } + + info!(?match_info, "Match finished"); + + Ok(()) +} diff --git a/crates/quinn-bevy/Cargo.toml b/crates/quinn-bevy/Cargo.toml index 07fa758f8a..895d034d29 100644 --- a/crates/quinn-bevy/Cargo.toml +++ b/crates/quinn-bevy/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -quinn = { git = "https://github.com/quinn-rs/quinn.git", default-features = false, features = ["native-certs", "tls-rustls"] } -quinn-udp = { git = "https://github.com/quinn-rs/quinn.git", default-features = false } -quinn-proto = { git = "https://github.com/quinn-rs/quinn.git", default-features = false } +quinn = { version = "0.9.1", default-features = false, features = ["native-certs", "tls-rustls"] } +quinn-udp = { version = "0.3.0", default-features = false } +quinn-proto = { version = "0.9", default-features = false } bevy_tasks = "0.8.1" async-executor = "1.4.1" async-io = "1.9.0" diff --git a/lib.jumpy.d.ts b/lib.jumpy.d.ts index 5793ae4b48..c618b22576 100644 --- a/lib.jumpy.d.ts +++ b/lib.jumpy.d.ts @@ -17,7 +17,14 @@ declare namespace Assets { function getAbsolutePath(relative_path: string): string; } +/** + * TODO: These are functions that need to be moved to a different namespace, or built into + * bevy_mod_js_scripting. + * + * Sorry for the weird naming! + */ declare namespace WorldTemp { + function spawn(): Entity; function despawnRecursive(entity: Entity): void; } @@ -33,6 +40,11 @@ declare interface ScriptInfo { declare namespace Script { function getInfo(): ScriptInfo; function state(init?: T): T; + function addEntityToList(listName: string, entity: Entity): void; + function getEntityList(listName: string): Entity[]; + function entityListContains(listName: string, entity: Entity): boolean; + function removeEntityFromList(listName: string, entity: Entity): void; + function clearEntityList(listName: string): void; function entityStates(): object; function getEntityState(entity: Entity, init?: T): T; function setEntityState(entity: Entity, value: T): void; @@ -53,9 +65,8 @@ declare namespace EntityRef { } interface NetInfo { - is_client: boolean; - is_server: boolean; player_idx: usize; + player_count: usize; } declare namespace NetInfo { @@ -75,33 +86,13 @@ declare namespace Player { function setInventory(player: Entity, item: Entity): void; function useItem(player: Entity): void; } +declare namespace Random { + function gen(): number; +} declare namespace CollisionWorld { function actorCollisions(entity: Entity): Entity[]; } - -type ItemGrabEvent = { - player: Entity; - item: Entity; - position: Vec3; -}; -type ItemDropEvent = { - player: Entity; - item: Entity; - position: Vec3; - velocity: Vec2; -}; -type ItemUseEvent = { - player: Entity; - item: Entity; - position: Vec3; -}; -declare namespace Items { - function grabEvents(): ItemGrabEvent[]; - function dropEvents(): ItemDropEvent[]; - function useEvents(): ItemUseEvent[]; -} - // // Jumpy component types // @@ -151,6 +142,19 @@ type Item = { script: string; }; declare const Item: BevyType; +type ItemGrabbed = { + player: Entity; +}; +declare const ItemGrabbed: BevyType; +type ItemDropped = { + player: Entity; +}; +declare const ItemDropped: BevyType; +type ItemUsed = { + player: Entity; +}; +declare const ItemUsed: BevyType; + type DamageRegion = { size: Vec2; diff --git a/lib.jumpy.js b/lib.jumpy.js index c6c5e5d149..016c204481 100644 --- a/lib.jumpy.js +++ b/lib.jumpy.js @@ -22,6 +22,15 @@ const PlayerInputs = { const Item = { typeName: "jumpy::item::Item" } +const ItemDropped = { + typeName: "jumpy::item::ItemDropped" +} +const ItemUsed = { + typeName: "jumpy::item::ItemUsed" +} +const ItemGrabbed = { + typeName: "jumpy::item::ItemGrabbed" +} const DamageRegion = { typeName: "jumpy::damage::DamageRegion" } @@ -39,4 +48,4 @@ const AnimationBankSprite = { } const AnimatedSprite = { typeName: "jumpy::animation::AnimatedSprite" -} \ No newline at end of file +} diff --git a/rust-toolchain b/rust-toolchain index 7573329311..6680fdbf12 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.64.0 \ No newline at end of file +1.65.0 \ No newline at end of file diff --git a/src.bkup/networking/client/player_input.rs b/src.bkup/networking/client/player_input.rs index 5896d6e110..9773278f3b 100644 --- a/src.bkup/networking/client/player_input.rs +++ b/src.bkup/networking/client/player_input.rs @@ -27,7 +27,7 @@ impl Plugin for ClientPlayerInputPlugin { } fn recv_player_input_from_server( - mut client: ResMut, + mut client: ResMut, mut player_inputs: ResMut, ) { // FIXME: Unordered packets not handled correctly. We need a network tick. diff --git a/src.bkup/networking/commands.rs b/src.bkup/networking/commands.rs index 16468c8fb5..9a7d84bc52 100644 --- a/src.bkup/networking/commands.rs +++ b/src.bkup/networking/commands.rs @@ -135,7 +135,7 @@ pub fn client_handle_net_commands( entities: &Entities, mut commands: Commands, type_registry: Res, - mut client: ResMut, + mut client: Res, mut net_ids: ResMut, type_names: Res, mut reset_controller: ResetController, diff --git a/src.bkup/networking/frame_sync.rs b/src.bkup/networking/frame_sync.rs index 1995f28ae2..f7de0bacbc 100644 --- a/src.bkup/networking/frame_sync.rs +++ b/src.bkup/networking/frame_sync.rs @@ -249,9 +249,7 @@ fn impl_server_send_sync_data( // Loop over all entities matching the query let mut serialized_items = Vec::new(); for item in query.iter_mut(world) { - let net_id = if let Some(id) = net_ids.get_net_id(item.entity) { - id - } else { + let Some(net_id) = net_ids.get_net_id(item.entity) else { warn!("Skipping sync of entity without known NetId"); continue; }; diff --git a/src/animation.rs b/src/animation.rs index d24c8e36b9..93c5acddd2 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,6 +1,4 @@ -use std::time::Duration; - -use bevy::{ecs::system::AsSystemLabel, reflect::FromReflect, time::FixedTimestep, utils::HashMap}; +use bevy::{ecs::system::AsSystemLabel, reflect::FromReflect, utils::HashMap}; use crate::prelude::*; @@ -16,31 +14,46 @@ pub enum AnimationStage { impl Plugin for AnimationPlugin { fn build(&self, app: &mut App) { + // Pre-initialize components so that the scripting engine doesn't throw an error if a script + // tries to access the component before it has been added to the world by a Rust system. + app.world.init_component::(); + app.register_type::() .register_type::() - .add_stage_after( - CoreStage::PostUpdate, - AnimationStage::Hydrate, - SystemStage::single_threaded() - .with_system(hydrate_animation_bank_sprites) - .with_system(hydrate_animated_sprites.after(hydrate_animation_bank_sprites)), - ) - .add_stage_after( - AnimationStage::Hydrate, - AnimationStage::Animate, - SystemStage::single_threaded() - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)) - .with_system(update_animation_bank_sprites) - .with_system( - update_animated_sprite_components.after(update_animation_bank_sprites), + .extend_rollback_plugin(|plugin| { + plugin + .register_rollback_type::() + .register_rollback_type::() + .register_rollback_type::() + }) + .extend_rollback_schedule(|schedule| { + schedule + .add_stage_after( + RollbackStage::PostUpdate, + AnimationStage::Hydrate, + SystemStage::single_threaded() + .with_system(hydrate_animation_bank_sprites) + .with_system( + hydrate_animated_sprites.after(hydrate_animation_bank_sprites), + ), ) - .with_system( - animate_sprites - .run_in_state(GameState::InGame) - .run_not_in_state(InGameState::Paused) - .after(update_animated_sprite_components.as_system_label()), - ), - ); + .add_stage_after( + AnimationStage::Hydrate, + AnimationStage::Animate, + SystemStage::single_threaded() + .with_system(update_animation_bank_sprites) + .with_system( + update_animated_sprite_components + .after(update_animation_bank_sprites), + ) + .with_system( + animate_sprites + .run_in_state(GameState::InGame) + .run_not_in_state(InGameState::Paused) + .after(update_animated_sprite_components.as_system_label()), + ), + ); + }); } } @@ -59,13 +72,9 @@ pub struct AnimatedSprite { pub flip_y: bool, pub repeat: bool, pub fps: f32, - #[reflect(ignore)] - pub timer: Timer, + pub timer: f32, } -#[derive(Reflect, Component, Debug, Default, Deref, DerefMut)] -pub struct LastAnimatedSprite(pub Option); - impl Clone for AnimatedSprite { fn clone(&self) -> Self { Self { @@ -77,7 +86,7 @@ impl Clone for AnimatedSprite { repeat: self.repeat, fps: self.fps, atlas: self.atlas.clone_weak(), - timer: self.timer.clone(), + timer: self.timer, } } } @@ -100,11 +109,11 @@ pub struct AnimationBank { fn animate_sprites(mut animated_sprites: Query<(&mut AnimatedSprite, &mut TextureAtlasSprite)>) { for (mut animated_sprite, mut atlas_sprite) in &mut animated_sprites { - animated_sprite - .timer - .tick(Duration::from_secs_f64(crate::FIXED_TIMESTEP)); + animated_sprite.timer += 1.0 / crate::FPS as f32; + atlas_sprite.flip_x = animated_sprite.flip_x; - if animated_sprite.timer.just_finished() { + if animated_sprite.timer > 1.0 / animated_sprite.fps { + animated_sprite.timer = 0.0; if animated_sprite.index >= animated_sprite .end @@ -129,7 +138,6 @@ fn hydrate_animated_sprites( for entity in &animated_sprites { commands .entity(entity) - .insert(LastAnimatedSprite(None)) .insert(Handle::::default()) .insert(TextureAtlasSprite::default()); } @@ -146,57 +154,18 @@ fn hydrate_animation_bank_sprites( fn update_animated_sprite_components( mut animated_sprites: Query<( - &mut AnimatedSprite, - &mut LastAnimatedSprite, + &AnimatedSprite, &mut Handle, &mut TextureAtlasSprite, )>, ) { - for (mut animated_sprite, mut last_animated_sprite, mut atlas_handle, mut atlas_sprite) in - &mut animated_sprites - { + for (animated_sprite, mut atlas_handle, mut atlas_sprite) in &mut animated_sprites { if *atlas_handle != animated_sprite.atlas { *atlas_handle = animated_sprite.atlas.clone_weak(); } atlas_sprite.flip_x = animated_sprite.flip_x; atlas_sprite.flip_y = animated_sprite.flip_y; - - let fps = animated_sprite.fps; - let repeat = animated_sprite.repeat; - - // If the FPS or repeat mode changed - if last_animated_sprite - .as_ref() - .as_ref() - .map(|last| fps != last.fps || repeat != last.repeat) - .unwrap_or(true) - { - // Restart the animation - animated_sprite.index = 0; - atlas_sprite.index = animated_sprite.start; - - // Reset the timer - animated_sprite - .timer - .set_duration(Duration::from_secs_f32(1.0 / fps.max(0.0001))); - animated_sprite.timer.set_repeating(true); - animated_sprite.timer.reset(); - } - - // If the animation changed - if last_animated_sprite - .as_ref() - .as_ref() - .map(|last| animated_sprite.start != last.start || animated_sprite.end != last.end) - .unwrap_or(true) - { - // Restart the animation - animated_sprite.index = 0; - atlas_sprite.index = animated_sprite.start; - } - - **last_animated_sprite = Some(animated_sprite.clone()); } } diff --git a/src/assets.rs b/src/assets.rs index 2daa83d908..2904562d6b 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -6,14 +6,12 @@ use std::{ use bevy::{ asset::{Asset, AssetLoader, AssetPath, LoadedAsset}, reflect::TypeUuid, - render::texture::ImageTextureLoader, }; use bevy_egui::egui; use bevy_mod_js_scripting::{serde_json, JsScript}; use normalize_path::NormalizePath; use crate::{ - config::ENGINE_CONFIG, metadata::{ BorderImageMeta, GameMeta, MapElementMeta, MapLayerKind, MapMeta, PlayerMeta, TextureAtlasMeta, @@ -41,11 +39,6 @@ impl Plugin for AssetPlugin { .add_asset_loader(TextureAtlasLoader) .add_jumpy_asset::() .add_asset_loader(EguiFontLoader); - - if ENGINE_CONFIG.server_mode { - let image_loader = ImageTextureLoader::from_world(&mut app.world); - app.add_asset::().add_asset_loader(image_loader); - } } } @@ -212,24 +205,6 @@ impl AssetLoader for GameMetaLoader { .push(AssetHandle::new(script_path, script_handle.typed())); } - // Load the client_script handles - for script_relative_path in &meta.client_scripts { - let (script_path, script_handle) = - get_relative_asset(load_context, self_path, script_relative_path); - dependencies.push(script_path.clone()); - meta.client_script_handles - .push(AssetHandle::new(script_path, script_handle.typed())); - } - - // Load the serer_script handles - for script_relative_path in &meta.server_scripts { - let (script_path, script_handle) = - get_relative_asset(load_context, self_path, script_relative_path); - dependencies.push(script_path.clone()); - meta.server_script_handles - .push(AssetHandle::new(script_path, script_handle.typed())); - } - load_context.set_default_asset(LoadedAsset::new(meta).with_dependencies(dependencies)); Ok(()) @@ -376,6 +351,28 @@ impl AssetLoader for MapElementMetaLoader { dependencies.push(script_path); } + // Load assets for built-in types + match &mut meta.builtin { + crate::metadata::BuiltinElementKind::None => (), + crate::metadata::BuiltinElementKind::PlayerSpawner => (), + crate::metadata::BuiltinElementKind::Sproinger { + atlas, + atlas_handle, + } => { + let (path, handle) = get_relative_asset(load_context, self_path, atlas); + *atlas_handle = AssetHandle::new(path.clone(), handle.typed()); + dependencies.push(path); + } + crate::metadata::BuiltinElementKind::Sword { + atlas, + atlas_handle, + } => { + let (path, handle) = get_relative_asset(load_context, self_path, atlas); + *atlas_handle = AssetHandle::new(path.clone(), handle.typed()); + dependencies.push(path); + } + } + // Load preloaded assets for asset in &meta.preload_assets { let (path, handle) = get_relative_asset(load_context, self_path, asset); diff --git a/src/assets/asset_handle.rs b/src/assets/asset_handle.rs index 9226dfd708..a600bbb25d 100644 --- a/src/assets/asset_handle.rs +++ b/src/assets/asset_handle.rs @@ -103,6 +103,7 @@ impl HasLoadProgress for AssetHandle { loading_resources.asset_server.get_load_state(&self.inner) == LoadState::Loaded; LoadProgress { + #[allow(clippy::bool_to_int_with_if)] loaded: if loaded { 1 } else { 0 }, total: 1, } diff --git a/src/camera.rs b/src/camera.rs index 127d9d6e91..9620d773fb 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,20 +1,19 @@ use bevy::render::view::RenderLayers; -use bevy_parallax::{ParallaxCameraComponent, ParallaxResource}; +use bevy_parallax::ParallaxCameraComponent; -use crate::{config::ENGINE_CONFIG, prelude::*}; +use crate::{player::PlayerIdx, prelude::*}; pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { app.register_type::() - .register_type::(); + .register_type::() + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage(RollbackStage::Update, camera_controller); + }); - if !ENGINE_CONFIG.server_mode { - app.add_plugin(bevy_parallax::ParallaxPlugin); - } else { - app.init_resource::(); - } + app.add_plugin(bevy_parallax::ParallaxPlugin); } } @@ -87,3 +86,41 @@ pub fn spawn_editor_camera(commands: &mut Commands) -> Entity { }) .id() } + +fn camera_controller( + players: Query<&Transform, With>, + mut camera: Query< + (&mut Transform, &mut OrthographicProjection), + (With, Without), + >, +) { + const LERP_FACTOR: f32 = 0.1; + + let Ok((mut camera_transform, mut projection)) = camera.get_single_mut() else { + return; + }; + + let mut middle_point = Vec2::ZERO; + let mut min = Vec2::new(100000.0, 100000.0); + let mut max = Vec2::new(-100000.0, -100000.0); + + let player_count = players.iter().len(); + + for player_transform in &players { + let pos = player_transform.translation.truncate(); + middle_point += pos; + + min.x = pos.x.min(min.x); + min.y = pos.y.min(min.y); + max.x = pos.x.max(max.x); + max.y = pos.y.max(max.y); + } + + middle_point /= player_count.max(1) as f32; + + let delta = camera_transform.translation.truncate() - middle_point; + let dist = delta * LERP_FACTOR; + camera_transform.translation -= dist.extend(0.0); + + projection.scale = 1.25; +} diff --git a/src/config.rs b/src/config.rs index 9e95611d02..f7a6953cd5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,12 +17,6 @@ pub static ENGINE_CONFIG: Lazy = Lazy::new(|| { #[derive(Clone, Debug, clap::Parser)] #[command(author, version, about)] pub struct EngineConfig { - /// Whether or not to run the game headless server mode. - /// - /// Useful only for development. - #[arg(hide = true, long, env = SERVER_MODE_ENV_VAR)] - pub server_mode: bool, - /// Hot reload assets #[arg(short = 'R', long)] pub hot_reload: bool, diff --git a/src/damage.rs b/src/damage.rs index a07edd9424..19b404f321 100644 --- a/src/damage.rs +++ b/src/damage.rs @@ -1,7 +1,6 @@ //! Systems and components related to damage/kill zones use crate::{ - networking::proto::ClientMatchInfo, physics::{collisions::Rect, KinematicBody}, player::{PlayerIdx, PlayerKillCommand}, prelude::*, @@ -13,10 +12,15 @@ impl Plugin for DamagePlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() - .add_system_to_stage( - FixedUpdateStage::PostUpdate, - eliminate_players_in_damage_region, - ); + .extend_rollback_plugin(|plugin| { + plugin + .register_rollback_type::() + .register_rollback_type::() + }) + .extend_rollback_schedule(|schedule| { + schedule + .add_system_to_stage(RollbackStage::PostUpdate, kill_players_in_damage_region); + }); } } @@ -61,23 +65,18 @@ impl Default for DamageRegionOwner { } /// System that will eliminate players that are intersecting with a damage region. -fn eliminate_players_in_damage_region( +fn kill_players_in_damage_region( mut commands: Commands, - players: Query<(Entity, &PlayerIdx, &GlobalTransform, &KinematicBody)>, - damage_regions: Query<(&DamageRegion, &GlobalTransform, Option<&DamageRegionOwner>)>, - client_match_info: Option>, + players: Query<(Entity, &GlobalTransform, &KinematicBody), With>, + // FIXME: We should technically be using the GlobalTransform of the damage region, but after + // adding rollback for some reason we run into a scenario where the GlobalTransform updates are + // not propagated for several frames. Using only the transform avoids the need for the + // propagation and temporarily works around the issue, but it's not a good final solution. + damage_regions: Query<(&DamageRegion, &Transform, Option<&DamageRegionOwner>)>, ) { - for (player_ent, player_idx, player_global_transform, kinematic_body) in &players { - // For network games, only consider the local player. We're not allowed to kill the other - // players. - if let Some(info) = &client_match_info { - if player_idx.0 != info.player_idx { - continue; - } - } - + for (player_ent, player_global_transform, kinematic_body) in &players { let player_rect = kinematic_body.collider_rect(player_global_transform.translation()); - for (damage_region, global_transform, damage_region_owner) in &damage_regions { + for (damage_region, transform, damage_region_owner) in &damage_regions { // Don't damage the player that owns this damage region if let Some(owner) = damage_region_owner { if owner.0 == player_ent { @@ -85,7 +84,7 @@ fn eliminate_players_in_damage_region( } } - let damage_rect = damage_region.collider_rect(global_transform.translation()); + let damage_rect = damage_region.collider_rect(transform.translation); if player_rect.overlaps(&damage_rect) { commands.add(PlayerKillCommand::new(player_ent)); diff --git a/src/item.rs b/src/item.rs index 4db5a1dbb6..51243b34f1 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,20 +1,27 @@ -use crate::{ - networking::{proto::game::GameEventFromServer, server::NetServer, NetId, NetIdMap}, - prelude::*, -}; +use crate::{prelude::*, utils::invalid_entity}; pub struct ItemPlugin; impl Plugin for ItemPlugin { fn build(&self, app: &mut App) { + // Pre-initialize components so that the scripting engine doesn't throw an error if a script + // tries to access the component before it has been added to the world by a Rust system. + app.world.init_component::(); + app.world.init_component::(); + app.world.init_component::(); + app.world.init_component::(); + app.register_type::() - .add_fixed_update_event::() - .add_fixed_update_event::() - .add_fixed_update_event::() - .add_system_to_stage( - CoreStage::PreUpdate, - send_net_item_spawns_from_server.run_if_resource_exists::(), - ); + .register_type::() + .register_type::() + .register_type::() + .extend_rollback_plugin(|plugin| { + plugin + .register_rollback_type::() + .register_rollback_type::() + .register_rollback_type::() + .register_rollback_type::() + }); } } @@ -23,49 +30,63 @@ impl Plugin for ItemPlugin { #[derive(Component, Reflect, Default, Serialize, Deserialize, Debug)] #[reflect(Default, Component)] pub struct Item { - /// The path to the item's script + /// The path to the item's script, or if the item is built-in a string like `core:sword`. pub script: String, } -/// An event triggered when an item is grabbed -#[derive(Reflect, Clone, Debug)] -pub struct ItemGrabEvent { +/// Marker component added to items that have been drop in the current frame. +/// +/// This component will be removed from the item at the end of the frame. +#[derive(Component, Reflect, Debug)] +#[reflect(Default, Component)] +#[component(storage = "SparseSet")] +pub struct ItemDropped { + /// The player that dropped the item pub player: Entity, - pub item: Entity, - pub position: Vec3, } -/// An event triggered when an item is dropped -#[derive(Reflect, Clone, Debug)] -pub struct ItemDropEvent { - pub player: Entity, - pub item: Entity, - pub position: Vec3, - pub velocity: Vec2, +impl Default for ItemDropped { + fn default() -> Self { + Self { + player: invalid_entity(), + } + } } -/// An event triggered when an item is used -#[derive(Reflect, Clone, Debug)] -pub struct ItemUseEvent { +/// Marker component indicating the item has been used this frame. +/// +/// This component will be removed from the item at the end of the frame. +#[derive(Component, Reflect, Debug)] +#[reflect(Default, Component)] +#[component(storage = "SparseSet")] +pub struct ItemUsed { + /// The player that dropped the item pub player: Entity, - pub item: Entity, - pub position: Vec3, } -/// System to send send net messages for item spawns when running as the server -fn send_net_item_spawns_from_server( - server: Res, - new_items: Query<(Entity, &Transform, &Item), Added>, - mut net_ids: ResMut, -) { - for (entity, transform, item) in &new_items { - let net_id = NetId::new(); - net_ids.insert(entity, net_id); +impl Default for ItemUsed { + fn default() -> Self { + Self { + player: invalid_entity(), + } + } +} + +/// Marker component indicating the item has been grabbed this frame. +/// +/// This component will be removed from the item at the end of the frame. +#[derive(Component, Reflect, Debug)] +#[reflect(Default, Component)] +#[component(storage = "SparseSet")] +pub struct ItemGrabbed { + /// The player that dropped the item + pub player: Entity, +} - server.broadcast_reliable(&GameEventFromServer::SpawnItem { - net_id, - script: item.script.clone(), - pos: transform.translation, - }); +impl Default for ItemGrabbed { + fn default() -> Self { + Self { + player: invalid_entity(), + } } } diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 39556e3fde..0000000000 --- a/src/lib.rs +++ /dev/null @@ -1,266 +0,0 @@ -//! Jumpy is a pixel-style, tactical 2D shooter with a fishy theme. -//! -//! This is the project's internal developer API documentation. API documentation is usually meant -//! for libraries with public APIs, but this is a game, so we use it to document the internal game -//! architecture for contributors. -//! -//! TODO: Write essentially an Architecture.md type of document here, and fill out the other game -//! module's documentation. - -#![allow(clippy::type_complexity)] -#![allow(clippy::forget_non_drop)] -#![allow(clippy::too_many_arguments)] - -use std::time::Duration; - -use bevy::{ - app::{RunMode, ScheduleRunnerPlugin, ScheduleRunnerSettings}, - asset::AssetServerSettings, - log::{LogPlugin, LogSettings}, - pbr::PbrPlugin, - render::{texture::ImageSettings, RenderPlugin}, - sprite::SpritePlugin, - text::TextPlugin, - time::FixedTimestep, - window::WindowPlugin, - winit::WinitPlugin, -}; -use bevy_parallax::ParallaxResource; - -pub mod animation; -pub mod assets; -pub mod camera; -pub mod config; -pub mod damage; -pub mod debug; -pub mod item; -pub mod lifetime; -pub mod lines; -pub mod loading; -pub mod localization; -pub mod map; -pub mod metadata; -pub mod name; -pub mod networking; -pub mod physics; -pub mod platform; -pub mod player; -pub mod prelude; -pub mod run_criteria; -pub mod scripting; -pub mod ui; -pub mod utils; -pub mod workarounds; - -use crate::{ - animation::AnimationPlugin, - assets::AssetPlugin, - camera::CameraPlugin, - damage::DamagePlugin, - debug::DebugPlugin, - item::ItemPlugin, - lifetime::LifetimePlugin, - lines::LinesPlugin, - loading::LoadingPlugin, - localization::LocalizationPlugin, - map::MapPlugin, - metadata::{GameMeta, MetadataPlugin}, - name::NamePlugin, - networking::{proto, server::NetServer, NetworkingPlugin}, - physics::PhysicsPlugin, - platform::PlatformPlugin, - player::PlayerPlugin, - prelude::*, - scripting::ScriptingPlugin, - ui::UiPlugin, - workarounds::WorkaroundsPlugin, -}; - -/// The timestep used for fixed update systems -pub const FIXED_TIMESTEP: f64 = 1.0 / 60.; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum GameState { - LoadingPlatformStorage, - LoadingGameData, - MainMenu, - InGame, - ServerPlayerSelect, - ServerInGame, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum InGameState { - Playing, - Editing, - Paused, -} - -#[derive(StageLabel)] -pub enum FixedUpdateStage { - First, - PreUpdate, - Update, - PostUpdate, - Last, -} - -pub fn build_app(net_server: Option) -> App { - // Load engine config. This will parse CLI arguments or web query string so we want to do it - // before we create the app to make sure everything is in order. - let engine_config = &*config::ENGINE_CONFIG; - - let mut app = App::new(); - - if !engine_config.server_mode { - app.insert_resource(WindowDescriptor { - title: "Fish Folk: Jumpy".to_string(), - fit_canvas_to_parent: true, - ..default() - }) - .insert_resource(ImageSettings::default_nearest()); - } - - // Configure log level - app.insert_resource(LogSettings { - filter: engine_config.log_level.clone(), - ..default() - }); - - // Configure asset server - let mut asset_server_settings = AssetServerSettings { - watch_for_changes: engine_config.hot_reload, - ..default() - }; - if let Some(asset_dir) = &engine_config.asset_dir { - asset_server_settings.asset_folder = asset_dir.clone(); - } - app.insert_resource(asset_server_settings); - - if !engine_config.server_mode { - // Initialize resources - app.insert_resource(ClearColor(Color::BLACK)) - .init_resource::(); - } - - // Set initial game state - app.add_loopless_state(GameState::LoadingPlatformStorage) - .add_loopless_state(InGameState::Playing); - - // Add fixed update stages - app.add_stage_after( - CoreStage::First, - FixedUpdateStage::First, - SystemStage::parallel().with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - CoreStage::PreUpdate, - FixedUpdateStage::PreUpdate, - SystemStage::parallel().with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - CoreStage::Update, - FixedUpdateStage::Update, - SystemStage::parallel().with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - CoreStage::PostUpdate, - FixedUpdateStage::PostUpdate, - SystemStage::parallel().with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - CoreStage::Last, - FixedUpdateStage::Last, - SystemStage::parallel().with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ); - - // Install game plugins - - // Server mode requires special configuration to disable rendering, etc. - if engine_config.server_mode { - if let Some(net_server) = net_server { - // Send each client their player index - let player_count = net_server.client_count(); - for i in 0..player_count { - info!("Sending net idx for player {i}"); - net_server.send_reliable( - &proto::ClientMatchInfo { - player_idx: i, - player_count, - }, - i, - ); - } - - app.insert_resource(net_server); - } else { - panic!("Net server required when in server mode"); - } - - app.add_plugins_with(DefaultPlugins, |group| { - group - .disable::() - .disable::() - .disable::() - .disable::() - .disable::() - .disable::() - .disable::() - .disable::() - }) - .init_resource::() - .add_asset::() - .insert_resource(ScheduleRunnerSettings { - run_mode: RunMode::Loop { - wait: Some(Duration::from_secs_f64(FIXED_TIMESTEP)), - }, - }) - .add_plugin(ScheduleRunnerPlugin) - .register_type::() - .register_type::(); - - // If we're not in server mode - } else { - app.add_plugins_with(DefaultPlugins, |group| { - // TODO: We should figure out how to not include these dependencies, so we can remove - // this disable section, too. - group - .disable::() - .disable::() - }) - .add_plugin(LinesPlugin) - .add_plugin(UiPlugin); - } - - app.add_plugin(bevy_tweening::TweeningPlugin) - .add_plugin(MetadataPlugin) - .add_plugin(PlatformPlugin) - .add_plugin(LoadingPlugin) - .add_plugin(AssetPlugin) - .add_plugin(LocalizationPlugin) - .add_plugin(NamePlugin) - .add_plugin(AnimationPlugin) - .add_plugin(PlayerPlugin) - .add_plugin(ItemPlugin) - .add_plugin(PhysicsPlugin) - .add_plugin(CameraPlugin) - .add_plugin(MapPlugin) - .add_plugin(DamagePlugin) - .add_plugin(LifetimePlugin) - .add_plugin(WorkaroundsPlugin) - .add_plugin(DebugPlugin) - .add_plugin(ScriptingPlugin) - .add_plugin(NetworkingPlugin); - - debug!(?engine_config, "Starting game"); - - // Get the game handle - let asset_server = app.world.get_resource::().unwrap(); - let game_asset = &engine_config.game_asset; - let game_handle: Handle = asset_server.load(game_asset); - - // Insert game handle resource - app.world.insert_resource(game_handle); - - app -} diff --git a/src/lifetime.rs b/src/lifetime.rs index 47ef435afc..18a300c893 100644 --- a/src/lifetime.rs +++ b/src/lifetime.rs @@ -1,13 +1,16 @@ //! Module providing entity lifetime components and systems -use crate::{prelude::*, FIXED_TIMESTEP}; +use crate::{prelude::*, FPS}; pub struct LifetimePlugin; impl Plugin for LifetimePlugin { fn build(&self, app: &mut App) { app.register_type::() - .add_system_to_stage(FixedUpdateStage::PostUpdate, lifetime_system); + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()) + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage(RollbackStage::PostUpdate, lifetime_system); + }); } } @@ -31,11 +34,20 @@ pub struct Lifetime { pub non_recursive_despawn: bool, } +impl Lifetime { + pub fn new(lifetime: f32) -> Self { + Self { + lifetime, + ..default() + } + } +} + /// Despawns entities that have an expired lifetime fn lifetime_system(mut commands: Commands, mut entities: Query<(Entity, &mut Lifetime)>) { for (entity, mut lifetime) in &mut entities { - lifetime.age += FIXED_TIMESTEP as f32; - if lifetime.age >= lifetime.lifetime { + lifetime.age += 1.0 / FPS as f32; + if lifetime.age > lifetime.lifetime { if lifetime.non_recursive_despawn { commands.entity(entity).despawn(); } else { diff --git a/src/loading.rs b/src/loading.rs index 5718d9ac28..b1da6724df 100644 --- a/src/loading.rs +++ b/src/loading.rs @@ -179,13 +179,8 @@ impl<'w, 's> GameLoader<'w, 's> { player.selected_player = game.player_handles[0].clone_weak(); } - if ENGINE_CONFIG.server_mode { - // Go to server player selection state - commands.insert_resource(NextState(GameState::ServerPlayerSelect)); - } else { - // Transition to the main menu when we are done - commands.insert_resource(NextState(GameState::MainMenu)); - } + // Transition to the main menu when we are done + commands.insert_resource(NextState(GameState::MainMenu)); } // Update camera scaling mode @@ -247,15 +242,6 @@ impl<'w, 's> GameLoader<'w, 's> { for script_handle in &game.script_handles { active_scripts.insert(script_handle.inner.clone_weak()); } - if ENGINE_CONFIG.server_mode { - for script_handle in &game.server_script_handles { - active_scripts.insert(script_handle.inner.clone_weak()); - } - } else { - for script_handle in &game.client_script_handles { - active_scripts.insert(script_handle.inner.clone_weak()); - } - } // Insert the game resource commands.insert_resource(game.clone()); diff --git a/src/main.rs b/src/main.rs index 2ad17b529b..4d9b4d651a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,260 @@ -fn main() { - let mut app = jumpy::build_app(None); +//! Jumpy is a pixel-style, tactical 2D shooter with a fishy theme. +//! +//! This is the project's internal developer API documentation. API documentation is usually meant +//! for libraries with public APIs, but this is a game, so we use it to document the internal game +//! architecture for contributors. +//! +//! TODO: Write essentially an Architecture.md type of document here, and fill out the other game +//! module's documentation. - app.run(); +#![allow(clippy::type_complexity)] +#![allow(clippy::forget_non_drop)] +#![allow(clippy::too_many_arguments)] + +use bevy::{ + asset::AssetServerSettings, log::LogSettings, render::texture::ImageSettings, text::TextPlugin, +}; +use bevy_ggrs::{ + ggrs::{self}, + GGRSPlugin, +}; +use bevy_parallax::ParallaxResource; +use mimalloc::MiMalloc; + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +pub mod animation; +pub mod assets; +pub mod camera; +pub mod config; +pub mod damage; +pub mod debug; +pub mod item; +pub mod lifetime; +pub mod lines; +pub mod loading; +pub mod localization; +pub mod map; +pub mod metadata; +pub mod name; +pub mod networking; +pub mod physics; +pub mod platform; +pub mod player; +pub mod prelude; +pub mod random; +pub mod run_criteria; +pub mod schedule; +pub mod scripting; +pub mod session; +pub mod ui; +pub mod utils; +pub mod workarounds; + +use crate::{ + animation::AnimationPlugin, + assets::AssetPlugin, + camera::CameraPlugin, + damage::DamagePlugin, + debug::DebugPlugin, + item::ItemPlugin, + lifetime::LifetimePlugin, + lines::LinesPlugin, + loading::LoadingPlugin, + localization::LocalizationPlugin, + map::MapPlugin, + metadata::{GameMeta, MetadataPlugin}, + name::NamePlugin, + networking::NetworkingPlugin, + physics::PhysicsPlugin, + platform::PlatformPlugin, + player::PlayerPlugin, + prelude::*, + random::RandomPlugin, + scripting::ScriptingPlugin, + session::SessionPlugin, + ui::UiPlugin, + utils::{is_in_game_run_criteria, UtilsPlugin}, + workarounds::WorkaroundsPlugin, +}; + +/// The game logic frames per second, aka. the fixed updates per second ( UPS/FPS ). +pub const FPS: usize = 45; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum GameState { + LoadingPlatformStorage, + LoadingGameData, + MainMenu, + InGame, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum InGameState { + Playing, + Editing, + Paused, +} + +#[derive(StageLabel)] +pub enum RollbackStage { + First, + PreUpdate, + PreUpdateInGame, + Update, + UpdateInGame, + PostUpdate, + Last, +} + +#[derive(Debug)] +pub struct GgrsConfig; +impl ggrs::Config for GgrsConfig { + type Input = player::input::DensePlayerControl; + type State = u8; + /// Addresses are the same as the player handle for our custom socket. + type Address = usize; +} + +pub fn main() { + // Load engine config. This will parse CLI arguments or web query string so we want to do it + // before we create the app to make sure everything is in order. + let engine_config = &*config::ENGINE_CONFIG; + + let mut app = App::new(); + + app.insert_resource(WindowDescriptor { + title: "Fish Folk: Jumpy".to_string(), + fit_canvas_to_parent: true, + ..default() + }) + .insert_resource(ImageSettings::default_nearest()); + + // Configure log level + app.insert_resource(LogSettings { + filter: engine_config.log_level.clone(), + ..default() + }); + + // Configure asset server + let mut asset_server_settings = AssetServerSettings { + watch_for_changes: engine_config.hot_reload, + ..default() + }; + if let Some(asset_dir) = &engine_config.asset_dir { + asset_server_settings.asset_folder = asset_dir.clone(); + } + app.insert_resource(asset_server_settings); + + // Initialize resources + app.insert_resource(ClearColor(Color::BLACK)) + .init_resource::(); + + // Set initial game state + app.add_loopless_state(GameState::LoadingPlatformStorage) + .add_loopless_state(InGameState::Playing); + + // Create the GGRS rollback schedule and plugin + let mut rollback_schedule = Schedule::default(); + let rollback_plugin = GGRSPlugin::::new(); + + // Add fixed update stagesrefs/branchless/2fd80952e26d905aa258ebb7e6175a7cfc4cb76f + rollback_schedule + .add_stage(RollbackStage::First, SystemStage::parallel()) + .add_stage_after( + RollbackStage::First, + RollbackStage::PreUpdate, + SystemStage::parallel(), + ) + .add_stage_after( + RollbackStage::PreUpdate, + RollbackStage::Update, + SystemStage::parallel(), + ) + .add_stage_after( + RollbackStage::PreUpdate, + RollbackStage::PreUpdateInGame, + SystemStage::parallel().with_run_criteria(is_in_game_run_criteria), + ) + .add_stage_after( + RollbackStage::Update, + RollbackStage::PostUpdate, + SystemStage::parallel(), + ) + .add_stage_after( + RollbackStage::Update, + RollbackStage::UpdateInGame, + SystemStage::parallel().with_run_criteria(is_in_game_run_criteria), + ) + .add_stage_after( + RollbackStage::PostUpdate, + RollbackStage::Last, + SystemStage::parallel(), + ); + + // Add the rollback schedule and plugin as resources, temporarily. + // This allows plugins to modify them using `crate::schedule::RollbackScheduleAppExt`. + app.insert_resource(rollback_schedule); + app.insert_resource(rollback_plugin); + + // Install game plugins + + app.add_plugins_with(DefaultPlugins, |group| { + // TODO: We should figure out how to not include these dependencies, so we can remove + // this disable section. + group + .disable::() + .disable::() + }) + .add_plugin(LinesPlugin) + .add_plugin(UiPlugin); + + app.add_plugin(bevy_tweening::TweeningPlugin) + .add_plugin(UtilsPlugin) + .add_plugin(MetadataPlugin) + .add_plugin(PlatformPlugin) + .add_plugin(LoadingPlugin) + .add_plugin(AssetPlugin) + .add_plugin(LocalizationPlugin) + .add_plugin(NamePlugin) + .add_plugin(AnimationPlugin) + .add_plugin(PlayerPlugin) + .add_plugin(ItemPlugin) + .add_plugin(PhysicsPlugin) + .add_plugin(CameraPlugin) + .add_plugin(MapPlugin) + .add_plugin(DamagePlugin) + .add_plugin(LifetimePlugin) + .add_plugin(WorkaroundsPlugin) + .add_plugin(DebugPlugin) + .add_plugin(RandomPlugin) + .add_plugin(ScriptingPlugin) + .add_plugin(NetworkingPlugin) + .add_plugin(SessionPlugin); + + // Pull the schedule back out of the world + let rollback_schedule: Schedule = app.world.remove_resource().unwrap(); + let ggrs_plugin: GGRSPlugin = app.world.remove_resource().unwrap(); + + // Build the GGRS plugin + ggrs_plugin + .with_input_system(player::input::input_system) + .with_update_frequency(crate::FPS) + .with_rollback_schedule(rollback_schedule) + .register_rollback_type::() + .register_rollback_type::>() + .build(&mut app); + + debug!(?engine_config, "Starting game"); + + // Get the game handle + let asset_server = app.world.get_resource::().unwrap(); + let game_asset = &engine_config.game_asset; + let game_handle: Handle = asset_server.load(game_asset); + + // Insert game handle resource + app.world.insert_resource(game_handle); + + app.run() } diff --git a/src/map.rs b/src/map.rs index 7f4107bf5c..4e141729b6 100644 --- a/src/map.rs +++ b/src/map.rs @@ -6,27 +6,34 @@ use bevy_prototype_lyon::{prelude::*, shapes::Rectangle}; use crate::{ camera::GameRenderLayers, - config::ENGINE_CONFIG, metadata::{MapElementMeta, MapLayerKind, MapLayerMeta, MapMeta}, name::EntityName, physics::collisions::{CollisionLayerTag, TileCollision}, prelude::*, + utils::Sort, }; +pub mod elements; pub mod grid; pub struct MapPlugin; impl Plugin for MapPlugin { fn build(&self, app: &mut App) { - if !ENGINE_CONFIG.server_mode { - app.add_plugin(TilemapPlugin); - } - - app.init_resource::().add_system(hydrate_maps); + app.add_plugin(TilemapPlugin) + .init_resource::() + .add_system(hydrate_maps) + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()) + .add_plugin(elements::MapElementsPlugin); } } +/// Marker component indicating that a map element load event has been handled by the map element's +/// script. +#[derive(Reflect, Component, Default)] +#[reflect(Component, Default)] +pub struct MapElementHydrated; + /// Contains the scripts that have been added for the currently loaded map #[derive(Deref, DerefMut, Default)] pub struct MapScripts(pub HashSet>); @@ -45,6 +52,7 @@ pub fn hydrate_maps( element_assets: ResMut>, mut active_scripts: ResMut, mut map_scripts: ResMut, + mut rids: ResMut, unspawned_maps: Query<(Entity, &AssetHandle), Without>, ) { for (map_entity, map_handle) in &unspawned_maps { @@ -58,14 +66,12 @@ pub fn hydrate_maps( default(), ); - if !ENGINE_CONFIG.server_mode { - let window = windows.primary(); - *parallax = map.get_parallax_resource(); - parallax.window_size = Vec2::new(window.width(), window.height()); - parallax.create_layers(&mut commands, &asset_server, &mut texture_atlas_assets); + let window = windows.primary(); + *parallax = map.get_parallax_resource(); + parallax.window_size = Vec2::new(window.width(), window.height()); + parallax.create_layers(&mut commands, &asset_server, &mut texture_atlas_assets); - commands.insert_resource(ClearColor(map.background_color.into())); - } + commands.insert_resource(ClearColor(map.background_color.into())); let tilemap_size = TilemapSize { x: map.grid_size.x, @@ -96,6 +102,8 @@ pub fn hydrate_maps( active_scripts.remove(&script); } + let mut current_map_element_idx = 0; + // Spawn map layers for (i, layer) in map.layers.iter().enumerate() { let layer: &MapLayerMeta = layer; @@ -190,7 +198,9 @@ pub fn hydrate_maps( let element_name = &element_meta.name; - // Note we use NetCommands to spawn the entity, because it needs to have + let sort = Sort(current_map_element_idx); + current_map_element_idx += 1; + let entity = commands .spawn() .insert(EntityName(format!( @@ -204,6 +214,7 @@ pub fn hydrate_maps( -100.0 + i as f32, )) .insert(GlobalTransform::default()) + .insert(sort) .with_children(|parent| { parent .spawn() @@ -223,6 +234,7 @@ pub fn hydrate_maps( )); }) .insert(element_meta) + .insert(Rollback::new(rids.next_id())) .id(); map_children.push(entity) } diff --git a/src/map/elements.rs b/src/map/elements.rs new file mode 100644 index 0000000000..1f6d5a38ba --- /dev/null +++ b/src/map/elements.rs @@ -0,0 +1,22 @@ +use crate::{ + animation::AnimatedSprite, + map::MapElementHydrated, + metadata::{BuiltinElementKind, MapElementMeta}, + physics::{collisions::CollisionWorld, KinematicBody}, + player::{input::PlayerInputs, PlayerIdx, MAX_PLAYERS}, + prelude::*, +}; + +pub mod player_spawner; +pub mod sproinger; +pub mod sword; + +pub struct MapElementsPlugin; + +impl Plugin for MapElementsPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(player_spawner::PlayerSpawnerPlugin) + .add_plugin(sproinger::SproingerPlugin) + .add_plugin(sword::SwordPlugin); + } +} diff --git a/src/map/elements/player_spawner.rs b/src/map/elements/player_spawner.rs new file mode 100644 index 0000000000..4a72ef5d8c --- /dev/null +++ b/src/map/elements/player_spawner.rs @@ -0,0 +1,75 @@ +use crate::{metadata::BuiltinElementKind, utils::Sort}; + +use super::*; + +pub struct PlayerSpawnerPlugin; +impl Plugin for PlayerSpawnerPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage(RollbackStage::PreUpdateInGame, pre_update_in_game); + }) + .extend_rollback_plugin(|plugin| { + plugin + .register_rollback_type::() + .register_rollback_type::() + }); + } +} + +/// Marker component for player spawners +#[derive(Component, Reflect, Default)] +#[reflect(Component, Default)] +pub struct PlayerSpawner; + +#[derive(Component, Reflect, Default, Deref, DerefMut)] +#[reflect(Component, Default)] +pub struct CurrentPlayerSpawner(pub usize); + +fn pre_update_in_game( + mut commands: Commands, + player_inputs: Res, + players: Query<&PlayerIdx>, + player_spawners: Query<(&Sort, &Transform), With>, + non_hydrated_map_elements: Query< + (Entity, &Sort, &Transform, &MapElementMeta), + Without, + >, + mut ridp: ResMut, + mut current_spawner: ResMut, +) { + let mut spawn_points = player_spawners.iter().collect::>(); + // Hydrate any newly-spawned spawn points + for (entity, sort, transform, map_element) in &non_hydrated_map_elements { + // TODO: Better way to tie the behavior to the map element? + if matches!(map_element.builtin, BuiltinElementKind::PlayerSpawner) { + commands + .entity(entity) + .insert(MapElementHydrated) + .insert(PlayerSpawner); + spawn_points.push((sort, transform)); + } + } + spawn_points.sort_by_key(|x| x.0); + + // For every player + for i in 0..MAX_PLAYERS { + let player = &player_inputs.players[i]; + + // If the player is active, but not alive + if player.active && !players.iter().any(|x| x.0 == i) { + **current_spawner += 1; + **current_spawner %= spawn_points.len().max(1); + + let Some((_, spawn_point)) = spawn_points.get(**current_spawner) else { + break; + }; + + commands + .spawn() + .insert(PlayerIdx(i)) + .insert(**spawn_point) + .insert(Rollback::new(ridp.next_id())); + } + } +} diff --git a/src/map/elements/sproinger.rs b/src/map/elements/sproinger.rs new file mode 100644 index 0000000000..88927ee09f --- /dev/null +++ b/src/map/elements/sproinger.rs @@ -0,0 +1,87 @@ +use crate::metadata::BuiltinElementKind; + +use super::*; + +const FORCE: f32 = 30.0; + +pub struct SproingerPlugin; +impl Plugin for SproingerPlugin { + fn build(&self, app: &mut App) { + app.extend_rollback_schedule(|schedule| { + schedule + .add_system_to_stage(RollbackStage::PreUpdateInGame, pre_update_in_game) + .add_system_to_stage(RollbackStage::UpdateInGame, update_in_game); + }) + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()); + } +} + +#[derive(Component, Reflect, Default)] +#[reflect(Component, Default)] +pub struct Sproinger { + frame: u32, + sproinging: bool, +} + +fn pre_update_in_game( + mut commands: Commands, + non_hydrated_map_elements: Query<(Entity, &MapElementMeta), Without>, +) { + // Hydrate any newly-spawned sproingers + for (entity, map_element) in &non_hydrated_map_elements { + if let BuiltinElementKind::Sproinger { atlas_handle, .. } = &map_element.builtin { + commands + .entity(entity) + .insert(MapElementHydrated) + .insert(Sproinger::default()) + .insert(AnimatedSprite { + start: 0, + end: 6, + atlas: atlas_handle.inner.clone(), + repeat: false, + fps: 0.0, + ..default() + }) + .insert(KinematicBody { + size: Vec2::new(32.0, 8.0), + offset: Vec2::new(0.0, -6.0), + has_mass: false, + ..default() + }); + } + } +} + +fn update_in_game( + mut sproingers: Query<(Entity, &mut Sproinger, &mut AnimatedSprite)>, + mut bodies: Query<&mut KinematicBody>, + collision_world: CollisionWorld, +) { + for (sproinger_ent, mut sproinger, mut sprite) in &mut sproingers { + if sproinger.sproinging { + match sproinger.frame { + 1 => sprite.index = 2, + 4 => sprite.index = 3, + 8 => sprite.index = 4, + 12 => sprite.index = 5, + 20 => { + sprite.index = 0; + sproinger.sproinging = false; + sproinger.frame = 0; + } + _ => (), + } + sproinger.frame += 1; + } + + for collider_ent in collision_world.actor_collisions(sproinger_ent) { + let mut body = bodies.get_mut(collider_ent).unwrap(); + + if !sproinger.sproinging { + body.velocity.y = FORCE; + + sproinger.sproinging = true; + } + } + } +} diff --git a/src/map/elements/sword.rs b/src/map/elements/sword.rs new file mode 100644 index 0000000000..a2a764547a --- /dev/null +++ b/src/map/elements/sword.rs @@ -0,0 +1,248 @@ +use crate::{ + damage::{DamageRegion, DamageRegionOwner}, + item::{Item, ItemDropped, ItemUsed}, + lifetime::Lifetime, + name::EntityName, + utils::Sort, +}; + +use super::*; + +pub struct SwordPlugin; +impl Plugin for SwordPlugin { + fn build(&self, app: &mut App) { + app.extend_rollback_schedule(|schedule| { + schedule + .add_system_to_stage(RollbackStage::PreUpdateInGame, pre_update_in_game) + .add_system_to_stage(RollbackStage::UpdateInGame, update_in_game); + }) + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()); + } +} + +#[derive(Component, Reflect, Default, Clone)] +#[reflect(Component, Default)] +pub enum SwordState { + #[default] + Idle, + Swinging { + frame: usize, + }, + Cooldown { + frame: usize, + }, +} + +const COOLDOWN_FRAMES: usize = 13; +const ATTACK_FPS: f32 = 10.0; + +fn pre_update_in_game( + mut commands: Commands, + non_hydrated_map_elements: Query< + (Entity, &Sort, &MapElementMeta, &Transform), + Without, + >, + mut ridp: ResMut, +) { + // Hydrate any newly-spawned swords + let mut elements = non_hydrated_map_elements.iter().collect::>(); + elements.sort_by_key(|x| x.1); + for (entity, _sort, map_element, transform) in elements { + if let BuiltinElementKind::Sword { atlas_handle, .. } = &map_element.builtin { + commands.entity(entity).insert(MapElementHydrated); + + commands + .spawn() + .insert(Rollback::new(ridp.next_id())) + .insert(Item { + script: "core:sword".into(), + }) + .insert(EntityName("Item: Sword".into())) + .insert(SwordState::default()) + .insert(AnimatedSprite { + start: 0, + end: 0, + atlas: atlas_handle.inner.clone(), + repeat: false, + fps: ATTACK_FPS, + ..default() + }) + .insert_bundle(VisibilityBundle::default()) + .insert_bundle(TransformBundle { + local: *transform, + ..default() + }) + .insert(KinematicBody { + size: Vec2::new(64.0, 16.0), + offset: Vec2::new(0.0, 38.0), + gravity: 1.0, + has_mass: true, + has_friction: true, + ..default() + }); + } + } +} + +fn update_in_game( + mut commands: Commands, + players: Query<(&AnimatedSprite, &Transform, &KinematicBody), With>, + mut swords: Query< + ( + Entity, + &mut Transform, + &mut SwordState, + &mut AnimatedSprite, + &mut KinematicBody, + Option<&Parent>, + Option<&ItemUsed>, + Option<&ItemDropped>, + ), + Without, + >, + mut ridp: ResMut, +) { + // Helper to spawn damage regions + let mut spawn_damage_region = + |commands: &mut Commands, pos: Vec2, size: Vec2, owner: Entity| { + commands + .spawn() + .insert(Rollback::new(ridp.next_id())) + .insert(Transform::from_translation(pos.extend(0.0))) + .insert(GlobalTransform::default()) + .insert(DamageRegion { size }) + .insert(DamageRegionOwner(owner)) + .insert(Lifetime::new(2.0 / 60.0)); + }; + + for ( + item_ent, + mut transform, + mut state, + mut sprite, + mut body, + parent, + item_used, + item_dropped, + ) in &mut swords + { + // For all tiems that are being held + if let Some(parent) = parent { + let (player_sprite, player_transform, ..) = + players.get(parent.get()).expect("Parent is not a player"); + + // Deactivate collisions while being held + body.is_deactivated = true; + + // Flip the sprite to match the player orientation + let flip = player_sprite.flip_x; + sprite.flip_x = flip; + let flip_factor = if flip { -1.0 } else { 1.0 }; + transform.translation.x = 13.0 * flip_factor; + transform.translation.y = 21.0; + transform.translation.z = 0.0; + + // Reset the sword animation if we're not swinging it + if !matches!(&*state, SwordState::Swinging { .. }) { + sprite.start = 4; + sprite.end = 4; + sprite.index = 0; + sprite.repeat = false; + } + + let mut next_state = None; + match &mut *state { + SwordState::Idle => (), + SwordState::Swinging { frame } => { + // If we're at the end of the swinging animation + if sprite.index >= sprite.end - sprite.start - 1 { + // Go to cooldown frames + next_state = Some(SwordState::Cooldown { frame: 0 }); + + // If we're still swinging + } else { + // Set the current attack frame to the animation index + *frame = sprite.index; + } + + // TODO: Move all these constants to the builtin item config + match frame { + 0 => spawn_damage_region( + &mut commands, + Vec2::new( + player_transform.translation.x + 20.0 * flip_factor, + player_transform.translation.y + 20.0, + ), + Vec2::new(30.0, 70.0), + parent.get(), + ), + 1 => spawn_damage_region( + &mut commands, + Vec2::new( + player_transform.translation.x + 25.0 * flip_factor, + player_transform.translation.y + 20.0, + ), + Vec2::new(40.0, 50.0), + parent.get(), + ), + 2 => spawn_damage_region( + &mut commands, + Vec2::new( + player_transform.translation.x + 20.0 * flip_factor, + player_transform.translation.y, + ), + Vec2::new(40.0, 50.0), + parent.get(), + ), + _ => (), + } + + *frame += 1; + } + SwordState::Cooldown { frame } => { + if *frame >= COOLDOWN_FRAMES { + next_state = Some(SwordState::Idle); + } else { + *frame += 1; + } + } + } + + if let Some(next) = next_state { + *state = next; + } + if item_used.is_some() { + commands.entity(item_ent).remove::(); + } + + // If the item is being used + if item_used.is_some() && matches!(*state, SwordState::Idle) { + sprite.index = 0; + sprite.start = 8; + sprite.end = 12; + *state = SwordState::Swinging { frame: 0 }; + } + } + + // If the item was dropped + if let Some(dropped) = item_dropped { + commands.entity(item_ent).remove::(); + let (.., player_transform, player_body) = + players.get(dropped.player).expect("Parent is not a player"); + + // Re-activate physics + body.is_deactivated = false; + + // Put sword in rest position + sprite.start = 0; + sprite.end = 0; + body.velocity = player_body.velocity; + body.is_spawning = true; + + // Drop item at middle of player + transform.translation.y = player_transform.translation.y - 30.0; + transform.translation.x = player_transform.translation.x; + transform.translation.z = player_transform.translation.z; + } + } +} diff --git a/src/metadata.rs b/src/metadata.rs index 592eb20e63..29b90b6695 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -50,18 +50,6 @@ pub struct GameMeta { pub scripts: Vec, #[serde(skip)] pub script_handles: Vec>, - - /// Scripts that run only on the client - #[serde(default)] - pub client_scripts: Vec, - #[serde(skip)] - pub client_script_handles: Vec>, - - /// Scripts that run only on the server - #[serde(default)] - pub server_scripts: Vec, - #[serde(skip)] - pub server_script_handles: Vec>, } #[derive(HasLoadProgress, Deserialize, Clone, Debug)] diff --git a/src/metadata/map.rs b/src/metadata/map.rs index 24406fda1e..2b8d810a23 100644 --- a/src/metadata/map.rs +++ b/src/metadata/map.rs @@ -173,7 +173,11 @@ impl From for ParallaxLayerData { pub struct MapElementMeta { pub name: String, pub category: String, + #[serde(default)] pub scripts: Vec, + #[serde(default)] + #[has_load_progress(none)] + pub builtin: BuiltinElementKind, /// The size of the bounding rect for the element in the editor #[serde(default = "editor_size_default")] @@ -192,3 +196,27 @@ pub struct MapElementMeta { fn editor_size_default() -> Vec2 { Vec2::splat(16.0) } + +/// The kind of built-in +#[derive(Reflect, Component, Deserialize, Serialize, Clone, Debug, Default)] +#[reflect(Default, Component)] +#[serde(deny_unknown_fields)] +pub enum BuiltinElementKind { + /// This is not a built-in item + #[default] + None, + /// Player spawner + PlayerSpawner, + /// This is a sproinger + Sproinger { + atlas: String, + #[serde(skip)] + atlas_handle: AssetHandle, + }, + /// This is a sword + Sword { + atlas: String, + #[serde(skip)] + atlas_handle: AssetHandle, + }, +} diff --git a/src/metadata/player.rs b/src/metadata/player.rs index 047b0705d3..327386c02a 100644 --- a/src/metadata/player.rs +++ b/src/metadata/player.rs @@ -15,7 +15,7 @@ impl Plugin for PlayerMetadataPlugin { } } -#[derive(Reflect, TypeUuid, Deserialize, Clone, Debug, Component)] +#[derive(Reflect, TypeUuid, Deserialize, Clone, Debug, Component, Default)] #[serde(deny_unknown_fields)] #[uuid = "a939278b-901a-47d4-8ee8-6ac97881cf4d"] pub struct PlayerMeta { @@ -23,7 +23,7 @@ pub struct PlayerMeta { pub spritesheet: PlayerSpritesheetMeta, } -#[derive(Reflect, Deserialize, Clone, Debug)] +#[derive(Reflect, Deserialize, Clone, Debug, Default)] #[serde(deny_unknown_fields)] pub struct PlayerSpritesheetMeta { pub image: String, @@ -56,7 +56,7 @@ impl PlayerSpritesheetMeta { flip_y: false, repeat: clip.repeat, fps: self.animation_fps, - timer: Timer::from_seconds(1.0 / self.animation_fps, true), + timer: 0.0, index: 0, }, ) @@ -106,17 +106,13 @@ impl<'de> serde::de::Visitor<'de> for RangeVisitor { where A: SeqAccess<'de>, { - let start: usize = if let Some(start) = seq.next_element()? { - start - } else { + let Some(start) = seq.next_element()? else { return Err(serde::de::Error::invalid_length( 0, &"a sequence with a length of 2", )); }; - let end: usize = if let Some(end) = seq.next_element()? { - end - } else { + let Some(end) = seq.next_element()? else { return Err(serde::de::Error::invalid_length( 1, &"a sequence with a length of 2", diff --git a/src/name.rs b/src/name.rs index d383dc50d4..a229ea9acf 100644 --- a/src/name.rs +++ b/src/name.rs @@ -5,7 +5,8 @@ pub struct NamePlugin; impl Plugin for NamePlugin { fn build(&self, app: &mut App) { app.register_type::() - .add_system(update_entity_names); + .add_system_to_stage(CoreStage::Last, update_entity_names) + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()); } } diff --git a/src/networking.rs b/src/networking.rs index fd5903ded2..27e8513166 100644 --- a/src/networking.rs +++ b/src/networking.rs @@ -1,119 +1,11 @@ -use std::any::TypeId; - -use bevy::{reflect::FromReflect, utils::HashMap}; -use once_cell::sync::Lazy; -use ulid::Ulid; - -use crate::{config::ENGINE_CONFIG, prelude::*}; +use crate::prelude::*; pub mod client; pub mod proto; -pub mod server; +// pub mod server; pub struct NetworkingPlugin; impl Plugin for NetworkingPlugin { - fn build(&self, app: &mut App) { - app.init_resource::() - .add_plugin(client::ClientPlugin); - - if ENGINE_CONFIG.server_mode { - app.add_plugin(server::ServerPlugin); - } - } -} - -pub static NET_MESSAGE_TYPES: Lazy> = Lazy::new(|| { - [ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ] - .to_vec() -}); - -#[derive( - Reflect, - FromReflect, - Component, - Deref, - DerefMut, - Debug, - Clone, - Copy, - Serialize, - Deserialize, - Ord, - PartialOrd, - Eq, - PartialEq, - Hash, -)] -#[reflect_value(PartialEq, Deserialize, Serialize, Hash)] -pub struct NetId(pub Ulid); - -impl NetId { - pub fn new() -> Self { - Self(Ulid::new()) - } -} - -impl Default for NetId { - fn default() -> Self { - Self::new() - } -} - -impl From for NetId { - fn from(u: Ulid) -> Self { - Self(u) - } -} - -#[derive(Clone, Debug, Default)] -pub struct NetIdMap { - ent_to_net: HashMap, - net_to_ent: HashMap, -} - -impl NetIdMap { - pub fn insert(&mut self, entity: Entity, net_id: NetId) { - self.ent_to_net.insert(entity, net_id); - self.net_to_ent.insert(net_id, entity); - } - - pub fn remove_entity(&mut self, entity: Entity) -> Option { - if let Some(net_id) = self.ent_to_net.remove(&entity) { - self.net_to_ent.remove(&net_id); - - Some(net_id) - } else { - None - } - } - - pub fn remove_net_id(&mut self, net_id: NetId) -> Option { - if let Some(entity) = self.net_to_ent.remove(&net_id) { - self.ent_to_net.remove(&entity); - - Some(entity) - } else { - None - } - } - - pub fn get_entity(&self, net_id: NetId) -> Option { - self.net_to_ent.get(&net_id).cloned() - } - - pub fn get_net_id(&self, entity: Entity) -> Option { - self.ent_to_net.get(&entity).cloned() - } + fn build(&self, _app: &mut App) {} } diff --git a/src/networking/client.rs b/src/networking/client.rs index df22c70b6d..ca5547b5f0 100644 --- a/src/networking/client.rs +++ b/src/networking/client.rs @@ -1,59 +1,31 @@ -use std::{any::TypeId, collections::VecDeque, net::SocketAddr, sync::Arc}; +use std::{net::SocketAddr, sync::Arc}; use async_channel::{Receiver, RecvError, Sender}; -use bevy::{tasks::IoTaskPool, utils::HashMap}; +use bevy::tasks::IoTaskPool; use futures_lite::future; +use jumpy_matchmaker_proto::{RecvProxyMessage, SendProxyMessage, TargetClient}; use quinn::{ClientConfig, Connection, Endpoint, EndpointConfig}; use quinn_bevy::BevyIoTaskPoolExecutor; -use serde::de::DeserializeOwned; -use crate::{metadata::GameMeta, player::input::PlayerInputs, prelude::*}; +use crate::prelude::*; -use super::{proto::ClientMatchInfo, NET_MESSAGE_TYPES}; - -pub mod game; +use super::proto::{ + ClientMatchInfo, RecvReliableGameMessage, RecvUnreliableGameMessage, ReliableGameMessageKind, + UnreliableGameMessageKind, +}; pub struct ClientPlugin; impl Plugin for ClientPlugin { fn build(&self, app: &mut App) { - app.add_plugin(game::ClientGamePlugin) - .add_system_to_stage( - CoreStage::First, - remove_closed_client.run_if_resource_exists::(), - ) - .add_system_to_stage( - CoreStage::First, - recv_client_match_info - .run_if_resource_exists::() - .run_unless_resource_exists::(), - ) - .add_exit_system( - GameState::InGame, - close_connection_when_leaving_game.run_if_resource_exists::(), - ); - } -} - -fn recv_client_match_info( - mut commands: Commands, - mut client: ResMut, - mut player_inputs: ResMut, - game: Res, -) { - if let Some(match_info) = client.recv_reliable::() { - info!("Got match info: {:?}", match_info); - - for (i, player) in player_inputs.players.iter_mut().enumerate() { - player.active = i < match_info.player_count; - player.selected_player = game - .player_handles - .get(0) - .expect("No players in .game.yaml") - .clone_weak(); - } - - commands.insert_resource(match_info); + app.add_system_to_stage( + CoreStage::First, + remove_closed_client.run_if_resource_exists::(), + ) + .add_exit_system( + GameState::InGame, + close_connection_when_leaving_game.run_if_resource_exists::(), + ); } } @@ -115,8 +87,7 @@ pub async fn open_connection( None, socket, BevyIoTaskPoolExecutor, - )? - .0; + )?; let conn = endpoint .connect_with(client_config, server_addr.into(), "server")? @@ -125,19 +96,21 @@ pub async fn open_connection( Ok((endpoint, conn)) } -pub struct NetClient { +/// Networking client. Can be cloned to get another handle to the same network client. +#[derive(Clone)] +pub struct NetClient(Arc); + +struct NetClientInner { endpoint: Endpoint, conn: Connection, - outgoing_reliable_sender: Sender>, - outgoing_reliable_receiver: Receiver>, - outgoing_unreliable_sender: Sender>, - outgoing_unreliable_receiver: Receiver>, - incomming_reliable_sender: Sender>, - incomming_reliable_receiver: Receiver>, - incomming_unreliable_sender: Sender>, - incomming_unreliable_receiver: Receiver>, - incomming_reliable_queue: HashMap>>, - incomming_unreliable_queue: HashMap>>, + outgoing_reliable_sender: Sender, + outgoing_reliable_receiver: Receiver, + outgoing_unreliable_sender: Sender, + outgoing_unreliable_receiver: Receiver, + incomming_reliable_sender: Sender, + incomming_reliable_receiver: Receiver, + incomming_unreliable_sender: Sender, + incomming_unreliable_receiver: Receiver, } impl NetClient { @@ -148,7 +121,7 @@ impl NetClient { let (incomming_unreliable_sender, incomming_unreliable_receiver) = async_channel::unbounded(); - let client = Self { + let client = Self(Arc::new(NetClientInner { endpoint, conn, outgoing_reliable_sender, @@ -159,9 +132,7 @@ impl NetClient { incomming_reliable_receiver, incomming_unreliable_sender, incomming_unreliable_receiver, - incomming_reliable_queue: default(), - incomming_unreliable_queue: default(), - }; + })); spawn_message_recv_task(&client); spawn_message_send_task(&client); @@ -169,99 +140,83 @@ impl NetClient { client } - fn update_queue(&mut self) { - while let Ok(mut incomming) = self.incomming_reliable_receiver.try_recv() { - let type_idx_bytes: [u8; 4] = - incomming.split_off(incomming.len() - 4).try_into().unwrap(); - let type_idx = u32::from_le_bytes(type_idx_bytes); - let type_id = NET_MESSAGE_TYPES[type_idx as usize]; - self.incomming_reliable_queue - .entry(type_id) - .or_default() - .push_back(incomming); - } - while let Ok(mut incomming) = self.incomming_unreliable_receiver.try_recv() { - let type_idx_bytes: [u8; 4] = - incomming.split_off(incomming.len() - 4).try_into().unwrap(); - let type_idx = u32::from_le_bytes(type_idx_bytes); - let type_id = NET_MESSAGE_TYPES[type_idx as usize]; - self.incomming_unreliable_queue - .entry(type_id) - .or_default() - .push_back(incomming); - } - } - - pub fn send_reliable(&self, message: &S) { - let type_id = TypeId::of::(); - let type_idx = NET_MESSAGE_TYPES - .iter() - .position(|x| x == &type_id) - .expect("Net message type not registered") as u32; - let mut message = postcard::to_allocvec(message).expect("Serialize net message"); - message.extend_from_slice(&(type_idx as u32).to_le_bytes()); - - self.outgoing_reliable_sender.try_send(message).ok(); + pub fn send_reliable>( + &self, + message: M, + target_client: TargetClient, + ) { + let message = message.into(); + let message = postcard::to_allocvec(&message).expect("Serialize net message"); + let proxy_message = SendProxyMessage { + target_client, + message, + }; + self.0.outgoing_reliable_sender.try_send(proxy_message).ok(); } - pub fn send_unreliable(&self, message: &S) { - let type_id = TypeId::of::(); - let type_idx = NET_MESSAGE_TYPES - .iter() - .position(|x| x == &type_id) - .expect("Net message not registered") as u32; - let mut message = postcard::to_allocvec(message).expect("Serialize net message"); - message.extend_from_slice(&(type_idx as u32).to_le_bytes()); - - self.outgoing_unreliable_sender.try_send(message).ok(); + pub fn send_unreliable>( + &self, + message: M, + target_client: TargetClient, + ) { + let message = message.into(); + let message = postcard::to_allocvec(&message).expect("Serialize net message"); + let proxy_message = SendProxyMessage { + target_client, + message, + }; + self.0 + .outgoing_unreliable_sender + .try_send(proxy_message) + .ok(); } - pub fn recv_reliable(&mut self) -> Option { - let type_id = TypeId::of::(); - if !NET_MESSAGE_TYPES.contains(&type_id) { - panic!("Attempt to receive unregistered message type"); - } - self.update_queue(); - self.incomming_reliable_queue - .get_mut(&type_id) - .and_then(|queue| queue.pop_front()) - .map(|message| postcard::from_bytes(&message).expect("Deserialize net message")) + pub fn recv_reliable(&self) -> Option { + self.0 + .incomming_reliable_receiver + .try_recv() + .map(|message| RecvReliableGameMessage { + from_player_idx: message.from_client as usize, + kind: postcard::from_bytes(&message.message) + .expect("TODO: Handle error: Net deserialize error"), + }) + .ok() } - pub fn recv_unreliable(&mut self) -> Option { - let type_id = TypeId::of::(); - if !NET_MESSAGE_TYPES.contains(&type_id) { - panic!("Attempt to receive unregistered message type"); - } - self.update_queue(); - self.incomming_unreliable_queue - .get_mut(&type_id) - .and_then(|queue| queue.pop_front()) - .map(|message| postcard::from_bytes(&message).expect("Deserialize net message")) + pub fn recv_unreliable(&self) -> Option { + self.0 + .incomming_unreliable_receiver + .try_recv() + .map(|message| RecvUnreliableGameMessage { + from_player_idx: message.from_client as usize, + kind: postcard::from_bytes(&message.message) + .expect("TODO: Handle error: Net deserialize error"), + }) + .ok() } pub fn conn(&self) -> &Connection { - &self.conn + &self.0.conn } pub fn endpoint(&self) -> &Endpoint { - &self.endpoint + &self.0.endpoint } pub fn close(&self) { - self.conn.close(0u8.into(), b"NetClient::close()"); + self.0.conn.close(0u8.into(), b"NetClient::close()"); } pub fn is_closed(&self) -> bool { - self.conn.close_reason().is_some() + self.0.conn.close_reason().is_some() } } fn spawn_message_recv_task(client: &NetClient) { let io_pool = IoTaskPool::get(); - let reliable_sender = client.incomming_reliable_sender.clone(); - let unreliable_sender = client.incomming_unreliable_sender.clone(); - let conn = client.conn.clone(); + let reliable_sender = client.0.incomming_reliable_sender.clone(); + let unreliable_sender = client.0.incomming_unreliable_sender.clone(); + let conn = client.0.conn.clone(); io_pool .spawn(async move { @@ -271,6 +226,7 @@ fn spawn_message_recv_task(client: &NetClient) { async { while let Ok(recv) = conn.accept_uni().await { let message = recv.read_to_end(1024 * 1024).await?; + let message = postcard::from_bytes::(&message)?; reliable_sender.try_send(message).ok(); } @@ -279,7 +235,10 @@ fn spawn_message_recv_task(client: &NetClient) { }, async { while let Ok(message) = conn.read_datagram().await { - unreliable_sender.try_send(message.to_vec()).ok(); + let message = postcard::from_bytes::(&message) + .expect("TODO: Handle error: deserialize net message."); + + unreliable_sender.try_send(message).ok(); } }, ) @@ -315,9 +274,9 @@ fn spawn_message_recv_task(client: &NetClient) { fn spawn_message_send_task(client: &NetClient) { let io_pool = IoTaskPool::get(); - let conn = client.conn.clone(); - let outgoing_reliable_receiver = client.outgoing_reliable_receiver.clone(); - let outgoing_unreliable_receiver = client.outgoing_unreliable_receiver.clone(); + let conn = client.0.conn.clone(); + let outgoing_reliable_receiver = client.0.outgoing_reliable_receiver.clone(); + let outgoing_unreliable_receiver = client.0.outgoing_unreliable_receiver.clone(); io_pool .spawn(async move { @@ -325,6 +284,9 @@ fn spawn_message_send_task(client: &NetClient) { let handle_reliable_message = async { loop { let message = outgoing_reliable_receiver.recv().await?; + let message = + postcard::to_allocvec(&message).expect("Serialize net message"); + let result = async { let mut sender = conn.open_uni().await?; @@ -347,6 +309,9 @@ fn spawn_message_send_task(client: &NetClient) { let handle_unreliable_message = async { loop { let message = outgoing_unreliable_receiver.recv().await?; + let message = + postcard::to_allocvec(&message).expect("Serialize net message"); + let result = conn.send_datagram(message.into()); if let Err(e) = result { diff --git a/src/networking/client/game.rs b/src/networking/client/game.rs deleted file mode 100644 index fa0fcd20c0..0000000000 --- a/src/networking/client/game.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::time::Duration; - -use bevy_tweening::{lens::TransformPositionLens, Animator, EaseMethod, Tween, TweeningType}; - -use crate::{ - animation::AnimationBankSprite, - item::{Item, ItemDropEvent, ItemGrabEvent, ItemUseEvent}, - networking::{ - proto::{ - game::{ - GameEventFromServer, PlayerEvent, PlayerEventFromServer, PlayerState, - PlayerStateFromServer, - }, - tick::{ClientTicks, Tick}, - ClientMatchInfo, - }, - NetIdMap, - }, - physics::KinematicBody, - player::{ - PlayerDespawnCommand, PlayerDespawnEvent, PlayerIdx, PlayerKillCommand, PlayerKillEvent, - PlayerSetInventoryCommand, PlayerUseItemCommand, - }, - prelude::*, - FIXED_TIMESTEP, -}; - -use super::NetClient; - -pub struct ClientGamePlugin; - -impl Plugin for ClientGamePlugin { - fn build(&self, app: &mut App) { - app.add_system_to_stage( - FixedUpdateStage::Last, - send_game_events - .chain(send_player_state) - .run_if_resource_exists::() - .run_if_resource_exists::(), - ) - .add_system_to_stage( - FixedUpdateStage::First, - handle_game_events_from_server - .chain(handle_player_state) - .run_if_resource_exists::() - .run_if_resource_exists::(), - ); - } -} - -fn send_game_events( - mut player_kill_events: EventReader, - mut player_despawn_events: EventReader, - mut item_grab_events: EventReader, - mut item_drop_events: EventReader, - mut item_use_events: EventReader, - players: Query<(&PlayerIdx, &Transform, &KinematicBody)>, - client: Res, - client_info: Res, - net_ids: Res, -) { - for event in item_grab_events.iter() { - if let Ok((player_idx, ..)) = players.get(event.player) { - // As the client, we're only allowed to drop and pick up items for our own player. - if client_info.player_idx == player_idx.0 { - let net_id = net_ids - .get_net_id(event.item) - .expect("Item in network game without NetId"); - client.send_reliable(&PlayerEvent::GrabItem(net_id)); - } - } - } - - for event in item_drop_events.iter() { - if let Ok((player_idx, player_transform, body)) = players.get(event.player) { - // As the client, we're only allowed to drop and pick up items for our own player. - if client_info.player_idx == player_idx.0 { - client.send_reliable(&PlayerEvent::DropItem { - position: player_transform.translation, - velocity: body.velocity, - }); - } - } - } - - for event in item_use_events.iter() { - if let Ok((player_idx, ..)) = players.get(event.player) { - // As the client, we're only allowed to drop and pick up items for our own player. - if client_info.player_idx == player_idx.0 { - let item_id = net_ids - .get_net_id(event.item) - .expect("Item in network game without NetId"); - client.send_reliable(&PlayerEvent::UseItem { - position: event.position, - item: item_id, - }); - } - } - } - - for event in player_kill_events.iter() { - if let Ok((player_idx, ..)) = players.get(event.player) { - // As the client, we're only allowed to kill our own player - if client_info.player_idx == player_idx.0 { - client.send_reliable(&PlayerEvent::KillPlayer { - position: event.position, - velocity: event.velocity, - }); - } - } else { - warn!("Received kill event for player that isn't found"); - } - } - - for event in player_despawn_events.iter() { - // As the client, we're only able to despawn our own player - if client_info.player_idx == event.player_idx { - client.send_reliable(&PlayerEvent::DespawnPlayer); - } - } -} - -fn send_player_state( - client: Res, - players: Query<(&PlayerIdx, &Transform, &AnimationBankSprite)>, - match_info: Res, -) { - for (player_idx, transform, sprite) in &players { - if player_idx.0 == match_info.player_idx { - client.send_unreliable(&PlayerState { - tick: Tick::next(), - pos: transform.translation, - sprite: sprite.clone(), - }); - } - } -} - -fn handle_game_events_from_server( - mut commands: Commands, - mut client: ResMut, - players: Query<(Entity, &PlayerIdx)>, - mut net_ids: ResMut, -) { - while let Some(event) = client.recv_reliable::() { - let player_ent = players - .iter() - .find(|x| x.1 .0 == event.player_idx as usize) - .map(|x| x.0); - - match event.kind { - PlayerEvent::SpawnPlayer(pos) => { - commands - .spawn() - .insert(PlayerIdx(event.player_idx as usize)) - .insert(Transform::from_translation(pos)); - } - PlayerEvent::KillPlayer { position, velocity } => { - if let Some(player_ent) = player_ent { - commands.add(PlayerKillCommand { - player: player_ent, - position: Some(position), - velocity: Some(velocity), - }); - } else { - warn!(?event.player_idx, "Net event to kill player that doesn't exist locally"); - } - } - PlayerEvent::DespawnPlayer => { - if let Some(player_ent) = player_ent { - commands.add(PlayerDespawnCommand::new(player_ent)); - } else { - warn!(?event.player_idx, "Net event to despawn player that doesn't exist locally"); - } - } - PlayerEvent::GrabItem(net_id) => { - if let Some(player_ent) = player_ent { - if let Some(item_ent) = net_ids.get_entity(net_id) { - commands.add(PlayerSetInventoryCommand { - player: player_ent, - item: Some(item_ent), - position: None, - velocity: None, - }); - } else { - warn!( - "Trying to grab item but could not find local item with given net ID" - ); - } - } else { - warn!(?event.player_idx, "Net event to kill player that doesn't exist locally"); - } - } - PlayerEvent::DropItem { position, velocity } => { - if let Some(player_ent) = player_ent { - commands.add(PlayerSetInventoryCommand { - player: player_ent, - item: None, - position: Some(position), - velocity: Some(velocity), - }); - } else { - warn!(?event.player_idx, "Net event to kill player that doesn't exist locally"); - } - } - PlayerEvent::UseItem { position, item } => { - if let Some(player_ent) = player_ent { - if let Some(item_ent) = net_ids.get_entity(item) { - commands.add(PlayerUseItemCommand { - player: player_ent, - position: Some(position), - item: Some(item_ent), - }); - } else { - warn!( - "Trying to use item but could not find entity for item with given ID" - ); - } - } else { - warn!(?event.player_idx, "Net event to kill player that doesn't exist locally"); - } - } - } - } - while let Some(event) = client.recv_reliable::() { - match event { - GameEventFromServer::SpawnItem { - net_id, - script, - pos, - } => { - let mut item = commands.spawn(); - net_ids.insert(item.id(), net_id); - item.insert(Transform::from_translation(pos)) - .insert(GlobalTransform::default()) - .insert(Item { script }) - .insert_bundle(VisibilityBundle::default()); - } - } - } -} - -fn handle_player_state( - mut client_ticks: Local, - mut client: ResMut, - mut players: Query<( - Entity, - &PlayerIdx, - &Transform, - &mut Animator, - &mut AnimationBankSprite, - )>, -) { - while let Some(message) = client.recv_unreliable::() { - if client_ticks.is_latest(message.player_idx as usize, message.state.tick) { - for (_, idx, transform, mut animator, mut sprite) in &mut players { - if idx.0 == message.player_idx as usize { - animator.set_tweenable(Tween::new( - EaseMethod::Linear, - TweeningType::Once, - Duration::from_secs_f64(FIXED_TIMESTEP * 2.0), - TransformPositionLens { - start: transform.translation, - end: message.state.pos, - }, - )); - *sprite = message.state.sprite; - break; - } - } - } - } -} diff --git a/src/networking/proto.rs b/src/networking/proto.rs index 76bd73ad94..814c001d4e 100644 --- a/src/networking/proto.rs +++ b/src/networking/proto.rs @@ -1,8 +1,40 @@ use crate::prelude::*; -pub mod game; pub mod match_setup; -pub mod tick; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ReliableGameMessageKind { + MatchSetup(match_setup::MatchSetupMessage), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RecvReliableGameMessage { + pub from_player_idx: usize, + pub kind: ReliableGameMessageKind, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum UnreliableGameMessageKind { + Ggrs(bevy_ggrs::ggrs::Message), +} + +impl From for UnreliableGameMessageKind { + fn from(m: bevy_ggrs::ggrs::Message) -> Self { + Self::Ggrs(m) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RecvUnreliableGameMessage { + pub from_player_idx: usize, + pub kind: UnreliableGameMessageKind, +} + +impl From for ReliableGameMessageKind { + fn from(x: match_setup::MatchSetupMessage) -> Self { + Self::MatchSetup(x) + } +} /// A resource indicating which player this game client represents, and how many players there are /// in the match.j @@ -11,9 +43,3 @@ pub struct ClientMatchInfo { pub player_idx: usize, pub player_count: usize, } - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Ping; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Pong; diff --git a/src/networking/proto/game.rs b/src/networking/proto/game.rs deleted file mode 100644 index d56b217453..0000000000 --- a/src/networking/proto/game.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::{animation::AnimationBankSprite, networking::NetId, prelude::*}; - -use super::tick::Tick; - -#[derive(Serialize, Deserialize)] -pub struct PlayerEventFromServer { - pub player_idx: u8, - pub kind: PlayerEvent, -} - -#[derive(Serialize, Deserialize)] -pub enum GameEventFromServer { - SpawnItem { - net_id: NetId, - script: String, - pos: Vec3, - }, -} - -#[derive(Serialize, Deserialize)] -pub struct PlayerStateFromServer { - pub player_idx: u8, - pub state: PlayerState, -} - -#[derive(Serialize, Deserialize)] -pub enum PlayerEvent { - SpawnPlayer(Vec3), - KillPlayer { position: Vec3, velocity: Vec2 }, - DespawnPlayer, - GrabItem(NetId), - DropItem { position: Vec3, velocity: Vec2 }, - UseItem { position: Vec3, item: NetId }, -} - -#[derive(Serialize, Deserialize)] -pub struct PlayerState { - pub tick: Tick, - pub pos: Vec3, - pub sprite: AnimationBankSprite, -} diff --git a/src/networking/proto/match_setup.rs b/src/networking/proto/match_setup.rs index 7307687993..7e5da894ca 100644 --- a/src/networking/proto/match_setup.rs +++ b/src/networking/proto/match_setup.rs @@ -5,19 +5,8 @@ use crate::{ /// Network message sent by client to select a player #[derive(Serialize, Deserialize, Clone, Debug)] -pub enum MatchSetupFromClient { +pub enum MatchSetupMessage { SelectPlayer(AssetHandle), ConfirmSelection(bool), SelectMap(AssetHandle), } - -/// Network message sent by server to notify clients of selected players -#[derive(Serialize, Deserialize, Clone, Debug)] -pub enum MatchSetupFromServer { - ClientMessage { - player_idx: u8, - message: MatchSetupFromClient, - }, - SelectMap, - WaitForMapSelect, -} diff --git a/src/networking/proto/tick.rs b/src/networking/proto/tick.rs deleted file mode 100644 index 97ee207402..0000000000 --- a/src/networking/proto/tick.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::sync::atomic::{AtomicU32, Ordering::Relaxed}; - -use crate::prelude::*; - -/// Internal counter used for provisioning ticks -static COUNTER: AtomicU32 = AtomicU32::new(0); - -/// A tick that can be compared to other ticks from the same server. -/// -/// It's essentially a [`u32`] but one that increments every time you create a new [`Tick`] and will -/// loop around to `0` when it gets to `u32::MAX`. -/// -/// One tick can be compared with another to find out which one is newer, but because of the -/// wrapping, you can not accurately compare to [`Tick`]'s that are more than `u32::MAX / 2` ticks -/// appart. -/// -/// [`AtomicU32`]: std::sync::atomic::AtomicU32 -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone, Copy)] -pub struct Tick(u32); - -/// A collection of ticks for other clients -/// -/// It's a helper containing the `is_latest()` function to check if a given tick is the latest we've -/// seen for a given client, and update the latest tick for that client if it is. -#[derive(Default)] -pub struct ClientTicks(Vec); - -impl ClientTicks { - pub fn is_latest(&mut self, client_idx: usize, tick: Tick) -> bool { - if client_idx >= self.0.len() { - let extra_space = client_idx + 1 - self.0.len(); - self.0 - .extend(std::iter::once(Tick::default()).cycle().take(extra_space)); - } - - let current_tick = self.0[client_idx]; - if tick > current_tick { - self.0[client_idx] = tick; - true - } else { - false - } - } -} - -impl Tick { - pub fn next() -> Self { - COUNTER.fetch_add(1, Relaxed); - Tick(COUNTER.load(Relaxed)) - } - - pub fn as_u32(&self) -> u32 { - self.0 - } -} - -impl PartialOrd for Tick { - fn partial_cmp(&self, other: &Self) -> Option { - Some(if self == other { - std::cmp::Ordering::Equal - - // If `self` is greater than `other`, than we have two scenarios: - // - `self` was made later than `other` - // - `other` was made later than `self`, but wrapped around to a lower number To account - // - // for the second scenario we check what the distance between the two numbers is if we wrapp - // around from `self` to the `other` across `u32::MAX`. If the difference between the - // numbers wrapped is greater than the distance between them without wrapping, then we - // assume that they aren't wrapped, and `other` is actually less than `self. - } else if self.0 > other.0 && u32::MAX - self.0 + other.0 > u32::MAX / 2 { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Less - }) - } -} - -impl Ord for Tick { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other).unwrap() - } -} diff --git a/src/networking/server.rs b/src/networking/server.rs deleted file mode 100644 index 6c6a29bad6..0000000000 --- a/src/networking/server.rs +++ /dev/null @@ -1,398 +0,0 @@ -use std::{any::TypeId, collections::VecDeque}; - -use crate::{networking::proto, player::input::PlayerInputs}; -use async_channel::{Receiver, RecvError, Sender}; -use bevy::{app::AppExit, tasks::IoTaskPool, utils::HashMap}; -use bytes::Bytes; -use futures_lite::future; -use quinn::Connection; -use serde::de::DeserializeOwned; - -use crate::prelude::*; - -use super::NET_MESSAGE_TYPES; - -pub mod game; -pub mod match_setup; - -pub struct ServerPlugin; - -impl Plugin for ServerPlugin { - fn build(&self, app: &mut App) { - app.add_plugin(match_setup::ServerPlayerSelectPlugin) - .add_plugin(game::ServerGamePlugin) - .add_startup_system(spawn_message_recv_tasks) - .add_startup_system(spawn_message_send_task) - .add_system_to_stage(CoreStage::First, exit_on_disconnect) - .add_system(reply_to_ping); - - app.world.resource_scope(|world, server: Mut| { - let mut player_inputs = world.resource_mut::(); - - for i in 0..server.client_count() { - player_inputs.players[i].active = true; - } - }); - } -} - -fn reply_to_ping(mut server: ResMut) { - while let Some(incomming) = server.recv_reliable::() { - let client_idx = incomming.client_idx; - info!("Ping from client {client_idx}"); - server.send_reliable(&proto::Pong, client_idx); - } -} - -#[derive(Debug, Clone)] -pub struct NetServer { - clients: Vec, - outgoing_reliable_sender: Sender, - outgoing_reliable_receiver: Receiver, - outgoing_unreliable_sender: Sender, - outgoing_unreliable_receiver: Receiver, - incomming_reliable_sender: Sender, - incomming_reliable_receiver: Receiver, - incomming_unreliable_sender: Sender, - incomming_unreliable_receiver: Receiver, - incomming_reliable_queue: HashMap>, - incomming_unreliable_queue: HashMap>, -} - -#[derive(Debug)] -struct Outgoing { - data: Vec, - target: MessageTarget, -} - -#[derive(Debug, Clone)] -struct Incomming { - data: Vec, - client_idx: usize, -} - -#[derive(Debug, Clone)] -pub enum MessageTarget { - All, - AllExcept(usize), - Client(usize), -} - -impl NetServer { - pub fn new(clients: Vec) -> Self { - let (outgoing_reliable_sender, outgoing_reliable_receiver) = async_channel::unbounded(); - let (outgoing_unreliable_sender, outgoing_unreliable_receiver) = async_channel::unbounded(); - let (incomming_reliable_sender, incomming_reliable_receiver) = async_channel::unbounded(); - let (incomming_unreliable_sender, incomming_unreliable_receiver) = - async_channel::unbounded(); - - Self { - clients, - outgoing_reliable_sender, - outgoing_reliable_receiver, - outgoing_unreliable_sender, - outgoing_unreliable_receiver, - incomming_reliable_sender, - incomming_reliable_receiver, - incomming_unreliable_sender, - incomming_unreliable_receiver, - incomming_reliable_queue: default(), - incomming_unreliable_queue: default(), - } - } - - pub fn client_count(&self) -> usize { - self.clients.len() - } - - /// Update the incomming message queue - fn update_queue(&mut self) { - while let Ok(mut incomming) = self.incomming_reliable_receiver.try_recv() { - let type_idx_bytes: [u8; 4] = incomming - .data - .split_off(incomming.data.len() - 4) - .try_into() - .unwrap(); - let type_idx = u32::from_le_bytes(type_idx_bytes); - let type_id = NET_MESSAGE_TYPES[type_idx as usize]; - self.incomming_reliable_queue - .entry(type_id) - .or_default() - .push_back(incomming); - } - while let Ok(mut incomming) = self.incomming_unreliable_receiver.try_recv() { - let type_idx_bytes: [u8; 4] = incomming - .data - .split_off(incomming.data.len() - 4) - .try_into() - .unwrap(); - let type_idx = u32::from_le_bytes(type_idx_bytes); - let type_id = NET_MESSAGE_TYPES[type_idx as usize]; - self.incomming_unreliable_queue - .entry(type_id) - .or_default() - .push_back(incomming); - } - } - - pub fn send_reliable_to(&self, message: &S, target: MessageTarget) { - let type_id = TypeId::of::(); - let type_idx = NET_MESSAGE_TYPES - .iter() - .position(|x| x == &type_id) - .expect("Net message not registered") as u32; - let mut message = postcard::to_allocvec(message).expect("Serialize net message"); - message.extend_from_slice(&(type_idx as u32).to_le_bytes()); - self.outgoing_reliable_sender - .try_send(Outgoing { - data: message, - target, - }) - .ok(); - } - - pub fn send_unreliable_to(&self, message: &S, target: MessageTarget) { - let type_id = TypeId::of::(); - let type_idx = NET_MESSAGE_TYPES - .iter() - .position(|x| x == &type_id) - .expect("Net message not registered") as u32; - let mut message = postcard::to_allocvec(message).expect("Serialize net message"); - message.extend_from_slice(&(type_idx as u32).to_le_bytes()); - - self.outgoing_unreliable_sender - .try_send(Outgoing { - data: message, - target, - }) - .ok(); - } - - pub fn send_reliable(&self, message: &S, client_idx: usize) { - self.send_reliable_to(message, MessageTarget::Client(client_idx)); - } - - pub fn send_unreliable(&self, message: &S, client_idx: usize) { - self.send_unreliable_to(message, MessageTarget::Client(client_idx)); - } - - pub fn broadcast_reliable(&self, message: &S) { - self.send_reliable_to(message, MessageTarget::All); - } - - pub fn broadcast_unreliable(&self, message: &S) { - self.send_unreliable_to(message, MessageTarget::All); - } - - pub fn recv_reliable(&mut self) -> Option> { - let type_id = TypeId::of::(); - if !NET_MESSAGE_TYPES.contains(&type_id) { - panic!("Attempt to receive unregistered message type"); - } - self.update_queue(); - self.incomming_reliable_queue - .get_mut(&type_id) - .and_then(|queue| queue.pop_front()) - .map(|incomming| IncommingMessage { - message: postcard::from_bytes(&incomming.data).expect("Deserialize net message"), - client_idx: incomming.client_idx, - }) - } - - pub fn recv_unreliable( - &mut self, - ) -> Option> { - let type_id = TypeId::of::(); - if !NET_MESSAGE_TYPES.contains(&type_id) { - panic!("Attempt to receive unregistered message type"); - } - self.update_queue(); - self.incomming_unreliable_queue - .get_mut(&type_id) - .and_then(|queue| queue.pop_front()) - .map(|incomming| IncommingMessage { - message: postcard::from_bytes(&incomming.data).expect("Deserialize net message"), - client_idx: incomming.client_idx, - }) - } -} - -pub struct IncommingMessage { - pub message: T, - pub client_idx: usize, -} - -fn spawn_message_send_task(server: Res) { - let io_pool = IoTaskPool::get(); - - let clients = server.clients.clone(); - let outgoing_reliable_receiver = server.outgoing_reliable_receiver.clone(); - let outgoing_unreliable_receiver = server.outgoing_unreliable_receiver.clone(); - io_pool - .spawn(async move { - loop { - let handle_reliable_message = async { - loop { - let message = outgoing_reliable_receiver.recv().await?; - let data = Bytes::from(message.data); - - let targets = match message.target { - MessageTarget::All => clients.iter().collect::>(), - MessageTarget::AllExcept(idx) => clients - .iter() - .enumerate() - .filter(|(i, _)| i != &idx) - .map(|(_, x)| x) - .collect::>(), - MessageTarget::Client(idx) => { - [&clients[idx]].into_iter().collect::>() - } - }; - - // Broadcast reliable messages to clients - for conn in targets { - let message_ = data.clone(); - - let result = async { - let mut sender = conn.open_uni().await?; - - sender.write_all(&message_).await?; - sender.finish().await?; - - Ok::<(), anyhow::Error>(()) - }; - - if let Err(e) = result.await { - error!("Error sending reliable message: {e:?}"); - } - } - } - - // This is needed to annotate the return type of the block - #[allow(unreachable_code)] - Ok::<(), RecvError>(()) - }; - - let handle_unreliable_message = async { - loop { - let message = outgoing_unreliable_receiver.recv().await?; - let data = Bytes::from(message.data); - - let targets = match message.target { - MessageTarget::All => clients.iter().collect::>(), - MessageTarget::AllExcept(idx) => clients - .iter() - .enumerate() - .filter(|(i, _)| i != &idx) - .map(|(_, x)| x) - .collect::>(), - MessageTarget::Client(idx) => { - [&clients[idx]].into_iter().collect::>() - } - }; - - // Broadcast unreliable messages to clients - for conn in targets { - let message_ = data.clone(); - let result = conn.send_datagram(message_); - - if let Err(e) = result { - error!("Error sending unreliable message: {e:?}"); - } - } - } - - // This is needed to annotate the return type of the block - #[allow(unreachable_code)] - Ok::<(), RecvError>(()) - }; - - if future::race(handle_reliable_message, handle_unreliable_message) - .await - .is_err() - { - break; - } - } - }) - .detach(); -} - -fn spawn_message_recv_tasks(server: Res) { - let io_pool = IoTaskPool::get(); - - for (client_idx, conn) in server.clients.iter().enumerate() { - let reliable_sender = server.incomming_reliable_sender.clone(); - let unreliable_sender = server.incomming_unreliable_sender.clone(); - let conn = conn.clone(); - - io_pool - .spawn(async move { - 'connection: loop { - let receive_message_result = async { - future::zip( - async { - while let Ok(recv) = conn.accept_uni().await { - let message = recv.read_to_end(usize::MAX).await?; - reliable_sender - .try_send(Incomming { - data: message, - client_idx, - }) - .ok(); - } - - Ok::<(), anyhow::Error>(()) - }, - async { - while let Ok(message) = conn.read_datagram().await { - unreliable_sender - .try_send(Incomming { - data: message.to_vec(), - client_idx, - }) - .ok(); - } - }, - ) - .await - .0?; - - Ok::<(), anyhow::Error>(()) - }; - - let connection_closed = conn.closed(); - - let event = future::or( - async move { either::Left(connection_closed.await) }, - async move { either::Right(receive_message_result.await) }, - ) - .await; - - match event { - either::Either::Left(closed) => { - debug!("Client connection closed: {closed:?}"); - break 'connection; - } - either::Either::Right(message_result) => { - if let Err(e) = message_result { - error!("Error receiving net messages: {e:?}"); - } - } - } - } - }) - .detach(); - } -} - -fn exit_on_disconnect(mut server: ResMut, mut exit_sender: EventWriter) { - // Remove disconnected clients - server.clients.retain(|conn| conn.close_reason().is_none()); - - // If all clients have disconnected, exit the app - if server.clients.is_empty() { - info!("All clients disconnected from match"); - exit_sender.send_default(); - } -} diff --git a/src/networking/server/game.rs b/src/networking/server/game.rs deleted file mode 100644 index 488a2fc645..0000000000 --- a/src/networking/server/game.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::{ - networking::proto::game::{ - PlayerEvent, PlayerEventFromServer, PlayerState, PlayerStateFromServer, - }, - player::{PlayerDespawnCommand, PlayerIdx, PlayerKillCommand}, - prelude::*, -}; - -use super::{MessageTarget, NetServer}; - -pub struct ServerGamePlugin; - -impl Plugin for ServerGamePlugin { - fn build(&self, app: &mut App) { - app.add_system( - handle_client_messages - .run_if_resource_exists::() - .run_in_state(GameState::ServerInGame), - ); - } -} - -fn handle_client_messages( - mut server: ResMut, - players: Query<(Entity, &PlayerIdx)>, - mut commands: Commands, -) { - while let Some(incomming) = server.recv_reliable::() { - if let PlayerEvent::KillPlayer { position, velocity } = incomming.message { - for (entity, player_idx) in &players { - if player_idx.0 == incomming.client_idx { - commands.add(PlayerKillCommand { - player: entity, - position: Some(position), - velocity: Some(velocity), - }); - break; - } - } - } else if let PlayerEvent::DespawnPlayer = incomming.message { - for (entity, player_idx) in &players { - if player_idx.0 == incomming.client_idx { - commands.add(PlayerDespawnCommand::new(entity)); - break; - } - } - } - - server.send_reliable_to( - &PlayerEventFromServer { - player_idx: incomming.client_idx.try_into().unwrap(), - kind: incomming.message, - }, - MessageTarget::AllExcept(incomming.client_idx), - ) - } - while let Some(incomming) = server.recv_unreliable::() { - server.send_unreliable_to( - &PlayerStateFromServer { - player_idx: incomming.client_idx.try_into().unwrap(), - state: incomming.message, - }, - MessageTarget::AllExcept(incomming.client_idx), - ) - } -} diff --git a/src/networking/server/match_setup.rs b/src/networking/server/match_setup.rs deleted file mode 100644 index 95b9b3e5d1..0000000000 --- a/src/networking/server/match_setup.rs +++ /dev/null @@ -1,92 +0,0 @@ -use rand::{thread_rng, Rng}; - -use crate::{ - networking::proto::match_setup::{MatchSetupFromClient, MatchSetupFromServer}, - player::{input::PlayerInputs, MAX_PLAYERS}, - prelude::*, -}; - -use super::NetServer; - -pub struct ServerPlayerSelectPlugin; - -impl Plugin for ServerPlayerSelectPlugin { - fn build(&self, app: &mut App) { - app.add_system( - handle_client_messages - .run_if_resource_exists::() - .run_in_state(GameState::ServerPlayerSelect), - ); - } -} - -#[derive(Default, Deref, DerefMut)] -struct PlayerConfirmations([bool; MAX_PLAYERS]); - -impl PlayerConfirmations { - fn count(&self) -> usize { - let mut count = 0; - for confirmed in &self.0 { - if *confirmed { - count += 1; - } - } - - count - } -} - -fn handle_client_messages( - mut players_selected: Local, - mut player_selecting_map: Local>, - mut commands: Commands, - mut server: ResMut, - mut player_inputs: ResMut, -) { - while let Some(incomming) = server.recv_reliable::() { - match &incomming.message { - MatchSetupFromClient::SelectPlayer(handle) => { - player_inputs.players[incomming.client_idx].selected_player = handle.clone_weak(); - } - MatchSetupFromClient::ConfirmSelection(confirmed) => { - players_selected[incomming.client_idx] = *confirmed; - } - MatchSetupFromClient::SelectMap(map_handle) => { - if let Some(player_selecting_map) = &*player_selecting_map { - if *player_selecting_map == incomming.client_idx { - // Spawn the map - commands.spawn().insert(map_handle.clone_weak()); - - // Start the game - commands.insert_resource(NextState(GameState::ServerInGame)); - } - } - } - } - - // If the players have finished selecting their fish - if players_selected.count() == server.client_count() && player_selecting_map.is_none() { - // Select a random player to pick the map - let idx = thread_rng().gen_range(0..server.client_count()); - *player_selecting_map = Some(idx); - - server.send_reliable_to( - &MatchSetupFromServer::WaitForMapSelect, - super::MessageTarget::AllExcept(idx), - ); - server.send_reliable(&MatchSetupFromServer::SelectMap, idx); - - // If we are still waiting for players to select fish, map, or get ready - } else { - // Forward client message to other clients - let message = MatchSetupFromServer::ClientMessage { - player_idx: incomming.client_idx.try_into().unwrap(), - message: incomming.message, - }; - server.send_reliable_to( - &message, - super::MessageTarget::AllExcept(incomming.client_idx), - ); - } - } -} diff --git a/src/physics.rs b/src/physics.rs index f11a996661..e75c463f46 100644 --- a/src/physics.rs +++ b/src/physics.rs @@ -1,4 +1,4 @@ -use bevy::{math::vec2, time::FixedTimestep}; +use bevy::math::vec2; use crate::{config::ENGINE_CONFIG, metadata::GameMeta, prelude::*}; @@ -17,24 +17,32 @@ pub enum PhysicsStages { impl Plugin for PhysicsPlugin { fn build(&self, app: &mut App) { + app.world.init_component::(); app.register_type::() .register_type::() - .add_stage_after( - CoreStage::PostUpdate, - PhysicsStages::Hydrate, - SystemStage::parallel().with_system(hydrate_physics_bodies), - ) - .add_stage_after( - PhysicsStages::Hydrate, - PhysicsStages::UpdatePhysics, - SystemStage::parallel() - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)) - .with_system( - update_kinematic_bodies - .run_in_state(GameState::InGame) - .run_not_in_state(InGameState::Paused), - ), - ); + .extend_rollback_plugin(|plugin| { + plugin + .register_rollback_type::() + .register_rollback_type::() + .register_rollback_type::() + }) + .extend_rollback_schedule(|schedule| { + schedule + .add_stage_after( + RollbackStage::PostUpdate, + PhysicsStages::Hydrate, + SystemStage::parallel().with_system(hydrate_physics_bodies), + ) + .add_stage_after( + PhysicsStages::Hydrate, + PhysicsStages::UpdatePhysics, + SystemStage::parallel().with_system( + update_kinematic_bodies + .run_in_state(GameState::InGame) + .run_not_in_state(InGameState::Paused), + ), + ); + }); if ENGINE_CONFIG.debug_tools { app.add_plugin(debug::PhysicsDebugRenderPlugin); @@ -219,7 +227,7 @@ fn apply_rotation( ) { let mut angle = transform.rotation.to_euler(EulerRot::XYZ).2; if angular_velocity != 0.0 { - angle += (angular_velocity * crate::FIXED_TIMESTEP as f32).to_radians(); + angle += (angular_velocity * crate::FPS as f32).to_radians(); } else if !is_on_ground { angle += velocity.x.abs() * 0.00045 + velocity.y.abs() * 0.00015; } else { diff --git a/src/physics/collisions.rs b/src/physics/collisions.rs index ddcc8d37b6..9de7bde63c 100644 --- a/src/physics/collisions.rs +++ b/src/physics/collisions.rs @@ -223,14 +223,16 @@ impl<'w, 's> CollisionWorld<'w, 's> { pub fn actor_collisions(&self, entity: Entity) -> Vec { let mut collisions = Vec::new(); - let collider = if let Ok((_, collider)) = self.actors.get(entity) { - collider.clone() - } else { + let Ok((_, collider)) = self.actors.get(entity) else { return collisions; }; let rect = collider.rect(); - for (other_entity, collider) in &self.actors { + let mut actors = self.actors.iter().collect::>(); + // Sort for determinism's sake. ( Maybe entity's aren't deterministic, though...? ) + actors.sort_by_key(|x| x.0); + + for (other_entity, collider) in actors { if entity == other_entity { continue; } diff --git a/src/physics/debug.rs b/src/physics/debug.rs index 8839a4ace6..3244d38e30 100644 --- a/src/physics/debug.rs +++ b/src/physics/debug.rs @@ -6,7 +6,7 @@ use bevy::{ }; use bevy_prototype_lyon::{entity::ShapeBundle, prelude::*}; -use crate::damage::DamageRegion; +use crate::{damage::DamageRegion, prelude::RollbackScheduleAppExt}; use super::{collisions::Collider, KinematicBody, PhysicsStages}; @@ -30,11 +30,13 @@ struct PhysicsDebugRenderStage; impl Plugin for PhysicsDebugRenderPlugin { fn build(&self, app: &mut App) { app.init_resource::() - .add_stage_after( - PhysicsStages::UpdatePhysics, - PhysicsDebugRenderStage, - SystemStage::single(render_collision_shapes), - ); + .extend_rollback_schedule(|schedule| { + schedule.add_stage_after( + PhysicsStages::UpdatePhysics, + PhysicsDebugRenderStage, + SystemStage::single(render_collision_shapes), + ); + }); } } @@ -87,7 +89,7 @@ struct DebugRenderer<'w, 's> { Query<'w, 's, (&'static mut Path, &'static mut DrawMode), With>, custom_colors: Query<'w, 's, &'static ColliderDebugColor>, colliders: Query<'w, 's, (Entity, &'static Collider)>, - damage_regions: Query<'w, 's, (Entity, &'static DamageRegion, &'static GlobalTransform)>, + damage_regions: Query<'w, 's, (Entity, &'static DamageRegion, &'static Transform)>, kinematic_bodies: Query<'w, 's, &'static KinematicBody>, } @@ -124,7 +126,7 @@ impl<'w, 's> DebugRenderer<'w, 's> { let shape_path = std::mem::take(shape_path_ref); *shape_path_ref = shape_path.add(&ColliderRect { - pos: transform.translation().truncate(), + pos: transform.translation.truncate(), size: vec2(damage_region.size.x, damage_region.size.y), }); } diff --git a/src/player.rs b/src/player.rs index 0abd8f4ffd..b6468178f9 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,16 +1,9 @@ use bevy::ecs::system::Command; -use bevy_tweening::Animator; use crate::{ - item::{Item, ItemDropEvent, ItemGrabEvent, ItemUseEvent}, + item::{Item, ItemDropped, ItemGrabbed, ItemUsed}, metadata::{GameMeta, PlayerMeta, Settings}, - networking::{ - proto::{ - game::{PlayerEvent, PlayerEventFromServer}, - ClientMatchInfo, - }, - server::NetServer, - }, + networking::proto::ClientMatchInfo, physics::KinematicBody, platform::Storage, prelude::*, @@ -28,15 +21,23 @@ pub struct PlayerPlugin; impl Plugin for PlayerPlugin { fn build(&self, app: &mut App) { + app.world.init_component::(); app.add_plugin(input::PlayerInputPlugin) .add_plugin(state::PlayerStatePlugin) - .add_fixed_update_event::() - .add_fixed_update_event::() .register_type::() - .add_system_to_stage( - FixedUpdateStage::PreUpdate, - hydrate_players.run_if_resource_exists::(), - ); + .register_type::() + .extend_rollback_plugin(|plugin| { + plugin + .register_rollback_type::() + .register_rollback_type::() + .register_rollback_type::() + }) + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage( + RollbackStage::PreUpdate, + hydrate_players.run_if_resource_exists::(), + ); + }); } } @@ -54,26 +55,6 @@ pub struct PlayerIdx(pub usize); #[reflect(Default, Component)] pub struct PlayerKilled; -/// An event sent when a player is killed. -/// -/// > **Note:** This is different than when the player is despawned. A player is usually killed to -/// > play their death animation before they are despawned. -#[derive(Reflect, Clone, Debug)] -pub struct PlayerKillEvent { - /// The index of the player that was killed. - pub player: Entity, - pub velocity: Vec2, - pub position: Vec3, -} - -/// An event sent when a player is despawned. -/// -/// > **Note:** This is different than when the player is killed. A player is usually killed to -/// > play their death animation before they are despawned. -pub struct PlayerDespawnEvent { - pub player_idx: usize, -} - /// A [`Command`] to kill a player. /// /// The command will perform any actions needed for the player kill sequence, including sending @@ -119,42 +100,36 @@ impl Command for PlayerKillCommand { return; } - world.resource_scope(|world, mut kill_events: Mut>| { - // If the entity is a player - if world.get::(self.player).is_some() { - let position = self.position.unwrap_or_else(|| { - world - .get::(self.player) - .expect("Player without kinematic body") - .translation - }); - let velocity = self.velocity.unwrap_or_else(|| { - world - .get::(self.player) - .expect("Player without kinematic body") - .velocity - }); - - // Send kill event - kill_events.send(PlayerKillEvent { - player: self.player, - velocity, - position, - }); - - // Drop any items player was carrying - PlayerSetInventoryCommand { - player: self.player, - item: None, - position: Some(position), - velocity: Some(velocity), - } - .write(world); - world.entity_mut(self.player).insert(PlayerKilled); - } else { - warn!("Tried to kill non-player entity") + // If the entity is a player + if let Some(idx) = world.get::(self.player) { + debug!("Killing player {}", idx.0); + let position = self.position.unwrap_or_else(|| { + world + .get::(self.player) + .expect("Player without kinematic body") + .translation + }); + let velocity = self.velocity.unwrap_or_else(|| { + world + .get::(self.player) + .expect("Player without kinematic body") + .velocity + }); + + // Drop any items player was carrying + PlayerSetInventoryCommand { + player: self.player, + item: None, + position: Some(position), + velocity: Some(velocity), } - }); + .write(world); + + // Add the maker component + world.entity_mut(self.player).insert(PlayerKilled); + } else { + warn!("Tried to kill non-player entity") + } } } @@ -180,22 +155,13 @@ impl PlayerDespawnCommand { impl Command for PlayerDespawnCommand { fn write(self, world: &mut World) { - world.resource_scope( - |world, mut despawn_events: Mut>| { - // If the entity is a player - if let Some(player_idx) = world.get::(self.player) { - // Send despawn event - despawn_events.send(PlayerDespawnEvent { - player_idx: player_idx.0, - }); - - // Despawn the player entity - despawn_with_children_recursive(world, self.player); - } else { - warn!("Tried to despawn non-player entity") - } - }, - ); + // If the entity is a player + if world.get::(self.player).is_some() { + // Despawn the player entity + despawn_with_children_recursive(world, self.player); + } else { + warn!("Tried to despawn non-player entity") + } } } @@ -235,57 +201,28 @@ impl PlayerSetInventoryCommand { impl Command for PlayerSetInventoryCommand { fn write(self, world: &mut World) { - // Get the effective drop/grab position - let position = self.position.unwrap_or_else(|| { - world - .get::(self.player) - .expect("Player missing transform") - .translation - }); - let current_inventory = get_player_inventory(world, self.player); - world.resource_scope(|world, mut grab_events: Mut>| { - world.resource_scope(|world, mut drop_events: Mut>| { - // If there was a previous item in the inventory, drop it - if let Some(current_item) = current_inventory { - let velocity = self.velocity.unwrap_or_else(|| { - world - .get::(self.player) - .map(|x| x.velocity) - // If we get network notified to kill a player before we get notified to - // drop the item they are carrying, we may end up calling this on a - // remote player with no kinematic body. - // - // In this case, because we don't have a velocity for the drop action, - // just do no movement on the drop. - // - // We may need to re-visit this if we find that this causes a de-sync. - .unwrap_or_default() - }); - - drop_events.send(ItemDropEvent { - player: self.player, - item: current_item, - position, - velocity, - }); - world - .entity_mut(self.player) - .remove_children(&[current_item]); - } + // If there was a previous item in the inventory, drop it + if let Some(current_item) = current_inventory { + // Add the drop marker + world.entity_mut(current_item).insert(ItemDropped { + player: self.player, + }); - // If there is a new item in the inventory, add it - if let Some(item) = self.item { - grab_events.send(ItemGrabEvent { - player: self.player, - item, - position, - }); - world.entity_mut(self.player).push_children(&[item]); - } + world + .entity_mut(self.player) + .remove_children(&[current_item]); + } + + // If there is a new item in the inventory, add it + if let Some(item) = self.item { + // Add the grab marker + world.entity_mut(item).insert(ItemGrabbed { + player: self.player, }); - }); + world.entity_mut(self.player).push_children(&[item]); + } } } @@ -326,22 +263,13 @@ impl PlayerUseItemCommand { impl Command for PlayerUseItemCommand { fn write(self, world: &mut World) { - let position = self.position.unwrap_or_else(|| { - let transform = world - .get::(self.player) - .expect("Player missing transform"); - transform.translation - }); let item = self .item .or_else(|| get_player_inventory(world, self.player)); if let Some(item) = item { - let mut use_events = world.resource_mut::>(); - use_events.send(ItemUseEvent { + world.entity_mut(item).insert(ItemUsed { player: self.player, - item, - position, }); } else { warn!("Tried to use item when not carrying one"); @@ -376,7 +304,6 @@ fn hydrate_players( game: Res, player_inputs: Res, player_meta_assets: Res>, - server: Option>, client_match_info: Option>, ) { let settings = storage.get(Settings::STORAGE_KEY); @@ -388,14 +315,6 @@ fn hydrate_players( // it may be required. player_transform.set_changed(); - // If we are the server, broadcast a spawn event for each hydrated player - if let Some(server) = &server { - server.broadcast_reliable(&PlayerEventFromServer { - player_idx: player_idx.0.try_into().unwrap(), - kind: PlayerEvent::SpawnPlayer(player_transform.translation), - }); - } - let input = &player_inputs.players[player_idx.0]; let meta = player_meta_assets .get(&input.selected_player) @@ -409,7 +328,6 @@ fn hydrate_players( entity_commands .insert(Name::new(format!("Player {}", player_idx.0))) .insert(PlayerState::default()) - .insert(meta.clone()) .insert(animation_bank) .insert(animation_bank_sprite) .insert(GlobalTransform::default()) @@ -419,7 +337,7 @@ fn hydrate_players( size: Vec2::new(32.0, 48.0), // FIXME: Don't hardcode! Load from player meta. has_mass: true, has_friction: true, - gravity: 1.0, + gravity: 1.5, ..default() }; let input_manager_for_player = |player_idx| InputManagerBundle { @@ -427,26 +345,18 @@ fn hydrate_players( ..default() }; - if let Some(match_info) = &client_match_info { - // Only add physics and input bundle for non-remote players if we are a multiplayer client - if match_info.player_idx == player_idx.0 { - // If we are a client in a multiplayer game, use the first player's controls - let player_input_idx = if client_match_info.is_some() { - 0 - // Otherwise, use the corresponding player's controls - } else { - player_idx.0 - }; - - entity_commands - .insert(kinematic_body) - .insert_bundle(input_manager_for_player(player_input_idx)); - - // For remote players we add an `Animator` that will be used to tween it's transform for - // smoothing player movement. + if let Some(_match_info) = &client_match_info { + // If we are a client in a multiplayer game, use the first player's controls + let player_input_idx = if client_match_info.is_some() { + 0 + // Otherwise, use the corresponding player's controls } else { - entity_commands.insert(Animator::::default()); - } + player_idx.0 + }; + + entity_commands + .insert(kinematic_body) + .insert_bundle(input_manager_for_player(player_input_idx)); // If this is a local game } else { diff --git a/src/player/input.rs b/src/player/input.rs index 2767799ee4..9effcbeb62 100644 --- a/src/player/input.rs +++ b/src/player/input.rs @@ -1,17 +1,18 @@ use super::*; use bevy::reflect::{FromReflect, Reflect}; +use bevy_ggrs::ggrs::{InputStatus, PlayerHandle}; +use leafwing_input_manager::plugin::InputManagerSystem; +use numquant::{IntRange, Quantized}; -use crate::{ - metadata::PlayerMeta, - networking::{proto::ClientMatchInfo, server::NetServer}, -}; +use crate::metadata::PlayerMeta; pub struct PlayerInputPlugin; impl Plugin for PlayerInputPlugin { fn build(&self, app: &mut App) { app.init_resource::() + .init_resource::() .register_type::() .register_type::() .register_type::>() @@ -19,16 +20,72 @@ impl Plugin for PlayerInputPlugin { .add_plugin(InputManagerPlugin::::default()) .add_system_to_stage( CoreStage::PreUpdate, - update_user_input.run_unless_resource_exists::(), + update_input_buffer.after(InputManagerSystem::Update), ) - .add_system_to_stage( - FixedUpdateStage::Last, - reset_input.run_unless_resource_exists::(), - ); + .add_system_to_stage(CoreStage::Last, clear_input_buffer) + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()) + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage(RollbackStage::PreUpdate, update_user_input); + // .add_system_to_stage(FixedUpdateStage::Last, reset_input); + }); + } +} + +/// A buffer holding the player inputs until they are read by the game simulation. +#[derive(Reflect, Default)] +pub struct LocalPlayerInputBuffer { + /// The buffers for each player. Non-local players will have empty buffers. + pub players: [DensePlayerControl; MAX_PLAYERS], + /// Indicates that the buffer has been read and should be reset at the end of the render frame. + pub has_been_read: bool, +} + +/// Update the player input buffer. This makes sure that if the frame rate exceeds the simulation +/// updates per second that any inputs pressed in between frames will be detected. +fn update_input_buffer( + mut buffer: ResMut, + players: Query<(&PlayerIdx, &ActionState)>, +) { + for (player_idx, action_state) in &players { + let control = &mut buffer.players[player_idx.0]; + + control.set_move_direction(DenseMoveDirection( + action_state + .axis_pair(PlayerAction::Move) + .unwrap_or_default() + .xy(), + )); + + control + .set_jump_pressed(action_state.pressed(PlayerAction::Jump) || control.jump_pressed()); + control.set_shoot_pressed( + action_state.pressed(PlayerAction::Shoot) || control.shoot_pressed(), + ); + control.set_slide_pressed( + action_state.pressed(PlayerAction::Slide) || control.slide_pressed(), + ); + control + .set_grab_pressed(action_state.pressed(PlayerAction::Grab) || control.grab_pressed()); + } +} + +/// Clear the input buffer if it has been read this frame +fn clear_input_buffer(mut buffer: ResMut) { + if buffer.has_been_read { + *buffer = default() } } -/// The control inputs that a player may make +/// The GGRS input system +pub fn input_system( + player_handle: In, + mut buffer: ResMut, +) -> DensePlayerControl { + buffer.has_been_read = true; + buffer.players[player_handle.0] +} + +/// The control inputs that a player may make. #[derive(Debug, Copy, Clone, Actionlike, Deserialize, Eq, PartialEq, Hash)] pub enum PlayerAction { Move, @@ -38,21 +95,18 @@ pub enum PlayerAction { Slide, } -#[derive(Reflect, Clone, Debug)] +/// The inputs for each player in this simulation frame. +#[derive(Reflect, Clone, Debug, Component)] #[reflect(Default, Resource)] pub struct PlayerInputs { pub players: Vec, - - /// This field indicates whether or not the user input has been updated since the last run of - /// the `reset_input` system. - pub has_updated: bool, } impl Default for PlayerInputs { fn default() -> Self { Self { players: vec![default(); MAX_PLAYERS], - has_updated: false, + // has_updated: false, } } } @@ -93,62 +147,103 @@ pub struct PlayerControl { pub slide_just_pressed: bool, } +bitfield::bitfield! { + /// A player's controller inputs densely packed into a single u16. + /// + /// This is used when sending player inputs across the network. + #[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, PartialEq, Eq, Reflect)] + #[repr(transparent)] + pub struct DensePlayerControl(u16); + impl Debug; + jump_pressed, set_jump_pressed: 0; + shoot_pressed, set_shoot_pressed: 1; + grab_pressed, set_grab_pressed: 2; + slide_pressed, set_slide_pressed: 3; + from into DenseMoveDirection, move_direction, set_move_direction: 15, 4; +} + +impl Default for DensePlayerControl { + fn default() -> Self { + let mut control = Self(0); + + control.set_move_direction(default()); + + control + } +} + +/// A newtype around [`Vec2`] that implements [`From`] and [`Into`] as a way to compress +/// user stick input for use in [`DensePlayerControl`]. +#[derive(Debug, Deref, DerefMut, Default)] +struct DenseMoveDirection(pub Vec2); + +/// This is the specific [`Quantized`] type that we use to represent movement directions in +/// [`DenseMoveDirection`]. +type MoveDirQuant = Quantized>; + +impl From for DenseMoveDirection { + fn from(bits: u16) -> Self { + // maximum movement value representable, we use 6 bits to represent each movement direction. + let max = 0b111111; + // The first six bits represent the x movement + let x_move_bits = bits & max; + // The second six bits represents the y movement + let y_move_bits = (bits >> 6) & max; + + // Round near-zero values to zero + let mut x = MoveDirQuant::from_raw(x_move_bits).to_f32(); + if x.abs() < 0.02 { + x = 0.0; + } + let mut y = MoveDirQuant::from_raw(y_move_bits).to_f32(); + if y.abs() < 0.02 { + y = 0.0; + } + + DenseMoveDirection(Vec2::new(x, y)) + } +} + +impl From for u16 { + fn from(dir: DenseMoveDirection) -> Self { + let x_bits = MoveDirQuant::from_f32(dir.x).raw(); + let y_bits = MoveDirQuant::from_f32(dir.y).raw(); + + x_bits | (y_bits << 6) + } +} + +/// Updates the [`PlayerInputs`] resource from input collected from GGRS. fn update_user_input( + inputs: Res>, mut player_inputs: ResMut, - players: Query<(&PlayerIdx, &ActionState)>, - client_match_info: Option>, ) { - for (player_idx, action_state) in &players { - // Nuance: during a network game, this allows the player to use any of the control methods - // for any local player to control themselves. - let actual_player_idx = if let Some(match_info) = &client_match_info { - match_info.player_idx - } else { - player_idx.0 - }; - + for (player_idx, (input, _)) in inputs.iter().enumerate() { let PlayerInput { control, previous_control, .. - } = &mut player_inputs.players[actual_player_idx]; + } = &mut player_inputs.players[player_idx]; - control.moving = action_state.pressed(PlayerAction::Move); + let move_direction = input.move_direction(); + + control.moving = move_direction.0 != Vec2::ZERO; control.just_moved = control.moving && !previous_control.moving; - control.move_direction = action_state - .axis_pair(PlayerAction::Move) - .unwrap_or_default() - .xy(); - - if action_state.pressed(PlayerAction::Jump) { - control.jump_pressed = true; - control.jump_just_pressed = !previous_control.jump_pressed; - } - if action_state.pressed(PlayerAction::Grab) { - control.grab_pressed = true; - control.grab_just_pressed = !previous_control.grab_pressed; - } - if action_state.pressed(PlayerAction::Shoot) { - control.shoot_pressed = true; - control.shoot_just_pressed = !previous_control.shoot_pressed; - } - if action_state.pressed(PlayerAction::Slide) { - control.slide_pressed = true; - control.slide_just_pressed = !previous_control.slide_pressed; - } - } - player_inputs.has_updated = true; -} + control.move_direction = move_direction.0; -/// Reset player inputs to prepare for the next update -fn reset_input(mut player_inputs: ResMut) { - if player_inputs.has_updated { - for player in &mut player_inputs.players { - player.previous_control = player.control.clone(); - player.control = default(); - } + control.jump_pressed = input.jump_pressed(); + control.jump_just_pressed = control.jump_pressed && !previous_control.jump_pressed; + + control.shoot_pressed = input.shoot_pressed(); + control.shoot_just_pressed = control.shoot_pressed && !previous_control.shoot_pressed; + + control.grab_pressed = input.grab_pressed(); + control.grab_just_pressed = control.grab_pressed && !previous_control.grab_pressed; + + control.slide_pressed = input.slide_pressed(); + control.slide_just_pressed = control.slide_pressed && !previous_control.slide_pressed; - player_inputs.has_updated = false; + *previous_control = control.clone(); } } diff --git a/src/player/state.rs b/src/player/state.rs index b347a4e66e..a7ba0ba871 100644 --- a/src/player/state.rs +++ b/src/player/state.rs @@ -1,10 +1,9 @@ -use std::time::Duration; - -use bevy::{ecs::schedule::ShouldRun, time::FixedTimestep}; -use bevy_mod_js_scripting::run_script_fn_system; +use bevy::ecs::schedule::ShouldRun; use crate::prelude::*; +mod states; + pub struct PlayerStatePlugin; #[derive(StageLabel)] @@ -36,52 +35,46 @@ impl Plugin for PlayerStatePlugin { // run_script_fn_system("playerStateCollectTransitions".into()).at_end(), // ), // ) - .add_stage_after( - CoreStage::PreUpdate, - PlayerStateStage::PerformTransitions, - // Note: We use the iyes_loopless FixedTimestepStage here, instead of the FixedTimestep - // run critera that we use elsewhere, because it is much easier to compose it with our - // state_transition_run_critera. - // - // The reason we don't _always_ use `FixedTimestepStage` is because it doesn't work with - // the `app.add_system_to_stage()` method. - FixedTimestepStage::from_stage( - Duration::from_secs_f64(crate::FIXED_TIMESTEP), - SystemStage::single_threaded() - .with_run_criteria(state_transition_run_criteria) - .with_system( - run_script_fn_system("playerStateTransition".into()) - .with_run_criteria(in_game_not_paused) - .at_end(), - ), - ), - ) - .add_stage_after( - PlayerStateStage::PerformTransitions, - PlayerStateStage::HandleState, - SystemStage::parallel() - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)) - .with_system( - run_script_fn_system("handlePlayerState".into()) - .with_run_criteria(in_game_not_paused) - .at_end(), - ), - ) - .add_system_to_stage(FixedUpdateStage::Last, update_player_state_age); + .extend_rollback_schedule(|schedule| { + schedule + .add_stage_after( + RollbackStage::PreUpdate, + PlayerStateStage::PerformTransitions, + SystemStage::single_threaded() + .with_run_criteria(state_transition_run_criteria), + // .with_system( + // run_script_fn_system("playerStateTransition".into()) + // .with_run_criteria(in_game_not_paused) + // .at_end(), + // ), + ) + .add_stage_after( + PlayerStateStage::PerformTransitions, + PlayerStateStage::HandleState, + SystemStage::parallel(), + // .with_system( + // run_script_fn_system("handlePlayerState".into()) + // .with_run_criteria(in_game_not_paused) + // .at_end(), + // ), + ) + .add_system_to_stage(RollbackStage::Last, update_player_state_age); + }) + .add_plugin(states::StatesPlugin); } } -/// Bevy run criteria for when the game is not paused -fn in_game_not_paused( - game_state: Res>, - in_game_state: Res>, -) -> ShouldRun { - if game_state.0 == GameState::InGame && in_game_state.0 != InGameState::Paused { - return ShouldRun::Yes; - } +// /// Bevy run criteria for when the game is not paused +// fn in_game_not_paused( +// game_state: Res>, +// in_game_state: Res>, +// ) -> ShouldRun { +// if game_state.0 == GameState::InGame && in_game_state.0 != InGameState::Paused { +// return ShouldRun::Yes; +// } - ShouldRun::No -} +// ShouldRun::No +// } fn state_transition_run_criteria( mut changed_states: Query<&mut PlayerState, Changed>, diff --git a/src/player/state/states.rs b/src/player/state/states.rs new file mode 100644 index 0000000000..7cbdf76e7d --- /dev/null +++ b/src/player/state/states.rs @@ -0,0 +1,74 @@ +use crate::prelude::*; + +use crate::{ + animation::AnimationBankSprite, + item::Item, + physics::collisions::CollisionWorld, + physics::KinematicBody, + player::{ + input::PlayerInputs, state::PlayerState, PlayerIdx, PlayerSetInventoryCommand, + PlayerUseItemCommand, + }, +}; + +use super::PlayerStateStage; + +pub const JUMP_SPEED: f32 = 17.0; + +pub mod crouch; +pub mod dead; +pub mod idle; +pub mod midair; +pub mod walk; + +/// Helper macro that adds the `player_state_transition` and `handle_player_state` systems from +/// `module` to the appropriate stages in `app`. +macro_rules! add_state_module { + ($app:ident, $module:ident) => { + $app.extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage( + PlayerStateStage::PerformTransitions, + $module::player_state_transition, + ); + schedule + .add_system_to_stage(PlayerStateStage::HandleState, $module::handle_player_state); + }); + }; +} + +/// Implements built-in player states +pub struct StatesPlugin; + +impl Plugin for StatesPlugin { + fn build(&self, app: &mut App) { + // Add default state + app.extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage( + PlayerStateStage::PerformTransitions, + default::player_state_transition, + ); + }); + + // Add other states + add_state_module!(app, idle); + add_state_module!(app, midair); + add_state_module!(app, walk); + add_state_module!(app, crouch); + add_state_module!(app, dead); + } +} + +/// The meaningless default state that players start at when spawned. +pub mod default { + use super::*; + + pub fn player_state_transition(mut states: Query<&mut PlayerState>) { + for mut state in &mut states { + // If the current state is the default, meaningless state + if state.id.is_empty() { + // Transition to idle + state.id = idle::ID.into(); + } + } + } +} diff --git a/src/player/state/states/crouch.rs b/src/player/state/states/crouch.rs new file mode 100644 index 0000000000..79675a33f7 --- /dev/null +++ b/src/player/state/states/crouch.rs @@ -0,0 +1,47 @@ +use super::*; + +pub const ID: &str = "core:crouch"; + +pub fn player_state_transition( + player_inputs: Res, + mut players: Query<(&mut PlayerState, &PlayerIdx, &KinematicBody)>, +) { + for (mut player_state, player_idx, body) in &mut players { + if player_state.id != ID { + continue; + } + + let control = &player_inputs.players[player_idx.0].control; + + if !body.is_on_ground || control.move_direction.y > -0.5 { + player_state.id = idle::ID.into(); + } + } +} + +pub fn handle_player_state( + player_inputs: Res, + mut players: Query<( + &PlayerState, + &PlayerIdx, + &mut AnimationBankSprite, + &mut KinematicBody, + )>, +) { + for (player_state, player_idx, mut sprite, mut body) in &mut players { + if player_state.id != ID { + continue; + } + + // Set animation + if player_state.age == 0 { + sprite.current_animation = "crouch".into(); + } + + let control = &player_inputs.players[player_idx.0].control; + + if control.jump_just_pressed { + body.fall_through = true; + } + } +} diff --git a/src/player/state/states/dead.rs b/src/player/state/states/dead.rs new file mode 100644 index 0000000000..5228ad5cbb --- /dev/null +++ b/src/player/state/states/dead.rs @@ -0,0 +1,29 @@ +use crate::player::{PlayerDespawnCommand, PlayerKilled}; + +use super::*; + +pub const ID: &str = "core:dead"; + +pub fn player_state_transition(mut players: Query<&mut PlayerState, With>) { + // Transition all killed players to this state + for mut player_state in &mut players { + if player_state.id != ID { + player_state.id = ID.into(); + } + } +} + +pub fn handle_player_state( + mut commands: Commands, + mut players: Query<(Entity, &PlayerState, &mut AnimationBankSprite), With>, +) { + for (entity, state, mut sprite) in &mut players { + if state.age == 0 { + sprite.current_animation = "death_1".into(); + } + + if state.age >= 80 { + commands.add(PlayerDespawnCommand::new(entity)); + } + } +} diff --git a/src/player/state/states/idle.rs b/src/player/state/states/idle.rs new file mode 100644 index 0000000000..86691d79f8 --- /dev/null +++ b/src/player/state/states/idle.rs @@ -0,0 +1,95 @@ +use super::*; + +pub const ID: &str = "core:idle"; + +pub fn player_state_transition( + player_inputs: Res, + mut players: Query<(&mut PlayerState, &PlayerIdx, &KinematicBody)>, +) { + for (mut player_state, player_idx, body) in &mut players { + if player_state.id != ID { + continue; + } + + let control = &player_inputs.players[player_idx.0].control; + + if !body.is_on_ground { + player_state.id = midair::ID.into(); + } else if control.move_direction.y < -0.5 { + player_state.id = crouch::ID.into(); + } else if control.move_direction.x != 0.0 { + player_state.id = walk::ID.into(); + } + } +} + +pub fn handle_player_state( + mut commands: Commands, + player_inputs: Res, + items: Query, With>, + mut players: Query<( + Entity, + &PlayerState, + &PlayerIdx, + &mut AnimationBankSprite, + &mut KinematicBody, + )>, + collision_world: CollisionWorld, +) { + for (player_ent, player_state, player_idx, mut sprite, mut body) in &mut players { + if player_state.id != ID { + continue; + } + + // If this is the first frame of this state + if player_state.age == 0 { + // set our animation to idle + sprite.current_animation = "idle".into(); + } + + let control = &player_inputs.players[player_idx.0].control; + + // Check for item in player inventory + let mut has_item = false; + 'items: for item_parent in &items { + if item_parent.filter(|x| x.get() == player_ent).is_some() { + has_item = true; + break 'items; + } + } + + // If we are grabbing + if control.grab_just_pressed { + // If we don't have an item + if !has_item { + // For each actor colliding with the player + 'colliders: for collider in collision_world.actor_collisions(player_ent) { + // If this is an item + if items.contains(collider) { + commands.add(PlayerSetInventoryCommand::new(player_ent, Some(collider))); + break 'colliders; + } + } + + // If we are already carrying an item + } else { + // Drop it + commands.add(PlayerSetInventoryCommand::new(player_ent, None)); + } + } + + // If we are using an item + if control.shoot_just_pressed && has_item { + commands.add(PlayerUseItemCommand::new(player_ent)); + } + + // If we are jumping + if control.jump_just_pressed { + // Move up + body.velocity.y = JUMP_SPEED; + } + + // Since we are idling, don't move + body.velocity.x = 0.0; + } +} diff --git a/src/player/state/states/midair.rs b/src/player/state/states/midair.rs new file mode 100644 index 0000000000..b2adec2d3f --- /dev/null +++ b/src/player/state/states/midair.rs @@ -0,0 +1,92 @@ +use super::*; + +pub const ID: &str = "core:midair"; + +pub const AIR_MOVE_SPEED: f32 = 7.0; + +pub fn player_state_transition(mut players: Query<(&mut PlayerState, &KinematicBody)>) { + for (mut player_state, body) in &mut players { + if player_state.id != ID { + continue; + } + + if body.is_on_ground { + player_state.id = idle::ID.into(); + } + } +} + +pub fn handle_player_state( + mut commands: Commands, + player_inputs: Res, + items: Query, With>, + mut players: Query<( + Entity, + &PlayerState, + &PlayerIdx, + &mut AnimationBankSprite, + &mut KinematicBody, + )>, + collision_world: CollisionWorld, +) { + for (player_ent, player_state, player_idx, mut sprite, mut body) in &mut players { + if player_state.id != ID { + continue; + } + + if body.velocity.y > 0.0 { + sprite.current_animation = "rise".into(); + } else { + sprite.current_animation = "fall".into(); + } + + let control = &player_inputs.players[player_idx.0].control; + + // Check for item in player inventory + let mut has_item = false; + 'items: for item_parent in &items { + if item_parent.filter(|x| x.get() == player_ent).is_some() { + has_item = true; + break 'items; + } + } + + // If we are grabbing + if control.grab_just_pressed { + // If we don't have an item + if !has_item { + // For each actor colliding with the player + 'colliders: for collider in collision_world.actor_collisions(player_ent) { + // If this is an item + if items.contains(collider) { + commands.add(PlayerSetInventoryCommand::new(player_ent, Some(collider))); + break 'colliders; + } + } + + // If we are already carrying an item + } else { + // Drop it + commands.add(PlayerSetInventoryCommand::new(player_ent, None)); + } + } + + // If we are using an item + if control.shoot_just_pressed && has_item { + commands.add(PlayerUseItemCommand::new(player_ent)); + } + + // Add controls + body.velocity.x = control.move_direction.x * AIR_MOVE_SPEED; + + // Fall through platforms + body.fall_through = control.move_direction.y < -0.5 && control.jump_pressed; + + // Point in movement direction + if control.move_direction.x > 0.0 { + sprite.flip_x = false; + } else if control.move_direction.x < 0.0 { + sprite.flip_x = true; + } + } +} diff --git a/src/player/state/states/walk.rs b/src/player/state/states/walk.rs new file mode 100644 index 0000000000..e31f7c8bf3 --- /dev/null +++ b/src/player/state/states/walk.rs @@ -0,0 +1,103 @@ +use super::*; + +pub const ID: &str = "core:walk"; +pub const WALK_SPEED: f32 = 7.0; + +pub fn player_state_transition( + player_inputs: Res, + mut players: Query<(&mut PlayerState, &PlayerIdx, &KinematicBody)>, +) { + for (mut player_state, player_idx, body) in &mut players { + if player_state.id != ID { + continue; + } + + let control = &player_inputs.players[player_idx.0].control; + + if !body.is_on_ground { + player_state.id = midair::ID.into(); + } else if control.move_direction.y < -0.5 { + player_state.id = crouch::ID.into(); + } else if control.move_direction.x == 0.0 { + player_state.id = idle::ID.into(); + } + } +} + +pub fn handle_player_state( + mut commands: Commands, + player_inputs: Res, + items: Query, With>, + mut players: Query<( + Entity, + &PlayerState, + &PlayerIdx, + &mut AnimationBankSprite, + &mut KinematicBody, + )>, + collision_world: CollisionWorld, +) { + for (player_ent, player_state, player_idx, mut sprite, mut body) in &mut players { + if player_state.id != ID { + continue; + } + + // If this is the first frame of this state + if player_state.age == 0 { + // set our animation + sprite.current_animation = "walk".into(); + } + + let control = &player_inputs.players[player_idx.0].control; + + // Check for item in player inventory + let mut has_item = false; + 'items: for item_parent in &items { + if item_parent.filter(|x| x.get() == player_ent).is_some() { + has_item = true; + break 'items; + } + } + + // If we are grabbing + if control.grab_just_pressed { + // If we don't have an item + if !has_item { + // For each actor colliding with the player + 'colliders: for collider in collision_world.actor_collisions(player_ent) { + // If this is an item + if items.contains(collider) { + commands.add(PlayerSetInventoryCommand::new(player_ent, Some(collider))); + break 'colliders; + } + } + + // If we are already carrying an item + } else { + // Drop it + commands.add(PlayerSetInventoryCommand::new(player_ent, None)); + } + } + + // If we are using an item + if control.shoot_just_pressed && has_item { + commands.add(PlayerUseItemCommand::new(player_ent)); + } + + // If we are jumping + if control.jump_just_pressed { + // Move up + body.velocity.y = JUMP_SPEED; + } + + // Walk in movement direction + body.velocity.x = control.move_direction.x * WALK_SPEED; + + // Point in movement direction + if control.move_direction.x > 0.0 { + sprite.flip_x = false; + } else if control.move_direction.x < 0.0 { + sprite.flip_x = true; + } + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 1504140262..9448a141bf 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,8 +1,10 @@ pub use crate::{ - assets::AssetHandle, utils::event::FixedUpdateEventAppExt, FixedUpdateStage, GameState, - InGameState, + assets::AssetHandle, schedule::RollbackScheduleAppExt, utils::event::FixedUpdateEventAppExt, + GameState, InGameState, RollbackStage, }; pub use bevy::prelude::*; +pub use bevy_ggrs::{Rollback, RollbackIdProvider}; pub use iyes_loopless::prelude::*; pub use leafwing_input_manager::prelude::*; pub use serde::{Deserialize, Serialize}; +pub use turborand::prelude::*; diff --git a/src/random.rs b/src/random.rs new file mode 100644 index 0000000000..39da7acc98 --- /dev/null +++ b/src/random.rs @@ -0,0 +1,27 @@ +use crate::prelude::*; +pub use turborand::prelude::*; + +pub struct RandomPlugin; + +impl Plugin for RandomPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()); + } +} + +#[derive(Reflect, Component, Serialize, Deserialize, Debug, Deref, DerefMut)] +#[reflect_value(Component, Resource, Default, Serialize, Deserialize)] +pub struct GlobalRng(AtomicRng); + +impl Clone for GlobalRng { + fn clone(&self) -> Self { + Self(postcard::from_bytes(&postcard::to_allocvec(&self.0).unwrap()).unwrap()) + } +} + +impl Default for GlobalRng { + fn default() -> Self { + Self(AtomicRng::with_seed(7)) + } +} diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000000..0e686fa471 --- /dev/null +++ b/src/schedule.rs @@ -0,0 +1,33 @@ +//! Utilities related to system scheduling and, in particular, the netcode rollback schedule. + +use bevy_ggrs::GGRSPlugin; + +use crate::prelude::*; + +pub trait RollbackScheduleAppExt { + fn extend_rollback_schedule(&mut self, f: F) -> &mut Self + where + F: FnOnce(&mut Schedule); + fn extend_rollback_plugin(&mut self, f: F) -> &mut Self + where + F: FnOnce(GGRSPlugin) -> GGRSPlugin; +} + +impl RollbackScheduleAppExt for App { + fn extend_rollback_schedule(&mut self, f: F) -> &mut Self + where + F: FnOnce(&mut Schedule), + { + let mut schedule = self.world.resource_mut(); + f(&mut schedule); + self + } + fn extend_rollback_plugin(&mut self, f: F) -> &mut Self + where + F: FnOnce(GGRSPlugin) -> GGRSPlugin, + { + let plugin = self.world.remove_resource().unwrap(); + self.world.insert_resource(f(plugin)); + self + } +} diff --git a/src/scripting.rs b/src/scripting.rs index 303e4d4165..d13627f1fb 100644 --- a/src/scripting.rs +++ b/src/scripting.rs @@ -3,18 +3,17 @@ use std::{ hash::{Hash, Hasher}, }; -use crate::{prelude::*, run_criteria::ShouldRunExt}; -use bevy::{ - asset::HandleId, ecs::schedule::ShouldRun, reflect::TypeRegistryArc, time::FixedTimestep, -}; +use crate::prelude::*; +use bevy::{asset::HandleId, ecs::entity::EntityMap, reflect::TypeRegistryArc}; +use bevy_ggrs::{ggrs::Frame, RollbackEventHook}; use bevy_mod_js_scripting::{ bevy_reflect_fns::{ PassMode, ReflectArg, ReflectFunction, ReflectFunctionError, ReflectMethods, }, - run_script_fn_system, JsRuntimeConfig, JsScriptingPlugin, + serde_json, JsRuntime, JsRuntimeApi, JsRuntimeConfig, JsScriptingPlugin, }; -mod ops; +pub mod ops; pub struct ScriptingPlugin; @@ -49,6 +48,73 @@ impl From for u64 { } } +#[derive(Serialize, Deserialize)] +struct JsEntity(JsU64); + +impl From for JsEntity { + fn from(e: Entity) -> Self { + Self(e.to_bits().into()) + } +} +impl From for Entity { + fn from(e: JsEntity) -> Self { + Entity::from_bits(e.0.into()) + } +} + +struct ScriptingRollbackHooks; + +impl RollbackEventHook for ScriptingRollbackHooks { + fn pre_save(&mut self, frame: Frame, max_snapshots: usize, world: &mut World) { + let runtime = world.remove_non_send_resource::().unwrap(); + + // We use extremely brief keys here to avoid encoding more string across the FFI + let args = serde_json::json!({ + "f": frame, + "m": max_snapshots, + }); + if let Err(e) = runtime.eval(&format!("globalThis.saveSnapshot({})", args), world) { + error!("Error running JS save snapshot hook: {e:?}"); + } + + world.insert_non_send_resource(runtime); + } + + fn post_load( + &mut self, + frame: Frame, + max_snapshots: usize, + entity_map: &EntityMap, + world: &mut World, + ) { + let runtime = world.remove_non_send_resource::().unwrap(); + + let mut entity_map_json = Vec::new(); + + for from in entity_map.keys() { + let to = entity_map.get(from).unwrap(); + if from != to { + entity_map_json.push(serde_json::json!({ + "f": JsEntity::from(from), + "t": JsEntity::from(to), + })); + } + } + + // We use extremely brief keys here to avoid encoding more string across the FFI + let args = serde_json::json!({ + "f": frame, + "m": max_snapshots, + "e": entity_map_json, + }); + if let Err(e) = runtime.eval(&format!("globalThis.loadSnapshot({})", args), world) { + error!("Error running JS save snapshot hook: {e:?}"); + } + + world.insert_non_send_resource(runtime); + } +} + impl Plugin for ScriptingPlugin { fn build(&self, app: &mut App) { let custom_ops = ops::get_ops(); @@ -57,7 +123,8 @@ impl Plugin for ScriptingPlugin { .insert_non_send_resource(JsRuntimeConfig { custom_ops }) .add_plugin(JsScriptingPlugin { skip_core_stage_setup: true, - }); + }) + .extend_rollback_plugin(|plugin| plugin.add_rollback_hook(ScriptingRollbackHooks)); { let type_registry = app.world.resource::(); @@ -77,92 +144,67 @@ impl Plugin for ScriptingPlugin { )])); } - // Add fixed update stages - app.add_stage_after( - FixedUpdateStage::First, - ScriptUpdateStage::First, - SystemStage::single(run_script_fn_system("first".into())) - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - FixedUpdateStage::First, - ScriptUpdateStage::FirstInGame, - SystemStage::single(run_script_fn_system("firstInGame".into())).with_run_criteria( - FixedTimestep::step(crate::FIXED_TIMESTEP).chain(is_in_game_run_criteria), - ), - ) - .add_stage_after( - FixedUpdateStage::PreUpdate, - ScriptUpdateStage::PreUpdate, - SystemStage::single(run_script_fn_system("preUpdate".into())) - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - FixedUpdateStage::PreUpdate, - ScriptUpdateStage::PreUpdateInGame, - SystemStage::single(run_script_fn_system("preUpdateInGame".into())).with_run_criteria( - FixedTimestep::step(crate::FIXED_TIMESTEP).chain(is_in_game_run_criteria), - ), - ) - .add_stage_after( - FixedUpdateStage::Update, - ScriptUpdateStage::Update, - SystemStage::single(run_script_fn_system("update".into())) - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - FixedUpdateStage::Update, - ScriptUpdateStage::UpdateInGame, - SystemStage::single(run_script_fn_system("updateInGame".into())).with_run_criteria( - FixedTimestep::step(crate::FIXED_TIMESTEP).chain(is_in_game_run_criteria), - ), - ) - .add_stage_after( - FixedUpdateStage::PostUpdate, - ScriptUpdateStage::PostUpdate, - SystemStage::single(run_script_fn_system("postUpdate".into())) - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - FixedUpdateStage::PostUpdate, - ScriptUpdateStage::PostUpdateInGame, - SystemStage::single(run_script_fn_system("postUpdateInGame".into())).with_run_criteria( - FixedTimestep::step(crate::FIXED_TIMESTEP).chain(is_in_game_run_criteria), - ), - ) - .add_stage_after( - FixedUpdateStage::Last, - ScriptUpdateStage::Last, - SystemStage::single(run_script_fn_system("last".into())) - .with_run_criteria(FixedTimestep::step(crate::FIXED_TIMESTEP)), - ) - .add_stage_after( - FixedUpdateStage::Last, - ScriptUpdateStage::LastInGame, - SystemStage::single(run_script_fn_system("lastInGame".into())).with_run_criteria( - FixedTimestep::step(crate::FIXED_TIMESTEP).chain(is_in_game_run_criteria), - ), - ); - } -} + // TODO: For now scripting is disabled for performance reasons. -/// Heper stage run criteria that only runs if we are in a gameplay state. -fn is_in_game_run_criteria( - should_run: In, - game_state: Option>>, - in_game_state: Option>>, -) -> ShouldRun { - if should_run.0.should_run() { - let is_in_game = game_state - .map(|x| x.0 == GameState::InGame) - .unwrap_or(false) - && in_game_state - .map(|x| x.0 != InGameState::Paused) - .unwrap_or(false); - - ShouldRun::new(is_in_game, should_run.0.check_again()) - } else { - should_run.0 + // Add fixed update stages + // app.extend_rollback_schedule(|schedule| { + // schedule + // .add_stage_after( + // RollbackStage::First, + // ScriptUpdateStage::First, + // SystemStage::single(run_script_fn_system("first".into())), + // ) + // .add_stage_after( + // RollbackStage::First, + // ScriptUpdateStage::FirstInGame, + // SystemStage::single(run_script_fn_system("firstInGame".into())) + // .with_run_criteria(is_in_game_run_criteria), + // ) + // .add_stage_after( + // RollbackStage::PreUpdate, + // ScriptUpdateStage::PreUpdate, + // SystemStage::single(run_script_fn_system("preUpdate".into())), + // ) + // .add_stage_after( + // RollbackStage::PreUpdate, + // ScriptUpdateStage::PreUpdateInGame, + // SystemStage::single(run_script_fn_system("preUpdateInGame".into())) + // .with_run_criteria(is_in_game_run_criteria), + // ) + // .add_stage_after( + // RollbackStage::Update, + // ScriptUpdateStage::Update, + // SystemStage::single(run_script_fn_system("update".into())), + // ) + // .add_stage_after( + // RollbackStage::Update, + // ScriptUpdateStage::UpdateInGame, + // SystemStage::single(run_script_fn_system("updateInGame".into())) + // .with_run_criteria(is_in_game_run_criteria), + // ) + // .add_stage_after( + // RollbackStage::PostUpdate, + // ScriptUpdateStage::PostUpdate, + // SystemStage::single(run_script_fn_system("postUpdate".into())), + // ) + // .add_stage_after( + // RollbackStage::PostUpdate, + // ScriptUpdateStage::PostUpdateInGame, + // SystemStage::single(run_script_fn_system("postUpdateInGame".into())) + // .with_run_criteria(is_in_game_run_criteria), + // ) + // .add_stage_after( + // RollbackStage::Last, + // ScriptUpdateStage::Last, + // SystemStage::single(run_script_fn_system("last".into())), + // ) + // .add_stage_after( + // RollbackStage::Last, + // ScriptUpdateStage::LastInGame, + // SystemStage::single(run_script_fn_system("lastInGame".into())) + // .with_run_criteria(is_in_game_run_criteria), + // ); + // }); } } diff --git a/src/scripting/ops.rs b/src/scripting/ops.rs index 230a3edd41..12735b6eb6 100644 --- a/src/scripting/ops.rs +++ b/src/scripting/ops.rs @@ -3,24 +3,23 @@ use bevy_mod_js_scripting::{JsRuntimeOp, OpMap}; pub mod asset; pub mod collision_world; pub mod entity; -pub mod item; pub mod map; pub mod net; pub mod player; +pub mod random; +pub mod rollback; pub mod script; pub mod world; pub fn get_ops() -> OpMap { let mut ops = OpMap::default(); + ops.insert("_rollback_hooks", Box::new(rollback::RollbackHooks)); ops.insert("_component_types_include", Box::new(ComponentTypesInclude)); ops.insert( "jumpy_element_get_spawned_entities", Box::new(map::ElementGetSpawnedEntities), ); - ops.insert("jumpy_item_grab_events", Box::new(item::ItemGrabEvents)); - ops.insert("jumpy_item_drop_events", Box::new(item::ItemDropEvents)); - ops.insert("jumpy_item_use_events", Box::new(item::ItemUseEvents)); ops.insert( "jumpy_asset_get_handle_id", Box::new(asset::AssetGetHandleId), @@ -35,10 +34,6 @@ pub fn get_ops() -> OpMap { ops.insert("jumpy_net_info_get", Box::new(net::NetInfoGet)); ops.insert("jumpy_player_kill", Box::new(player::PlayerKill)); ops.insert("jumpy_player_despawn", Box::new(player::PlayerDespawn)); - ops.insert( - "jumpy_player_kill_events", - Box::new(player::PlayerKillEvents), - ); ops.insert("jumpy_player_use_item", Box::new(player::PlayerUseItem)); ops.insert( "jumpy_player_get_inventory", @@ -57,6 +52,8 @@ pub fn get_ops() -> OpMap { "jumpy_world_despawn_recursive", Box::new(world::WorldDespawnRecursive), ); + ops.insert("jumpy_world_spawn", Box::new(world::WorldSpawn)); + ops.insert("jumpy_random", Box::new(random::Random)); ops } diff --git a/src/scripting/ops/asset.rs b/src/scripting/ops/asset.rs index 1db0f1381d..012b5121f6 100644 --- a/src/scripting/ops/asset.rs +++ b/src/scripting/ops/asset.rs @@ -90,6 +90,6 @@ impl JsRuntimeOp for AssetGetAbsolutePath { let absolute_path = absolute_path.normalize(); let path_str = absolute_path.to_str().expect("Non-unicode-path"); - Ok(serde_json::to_value(&path_str)?) + Ok(serde_json::to_value(path_str)?) } } diff --git a/src/scripting/ops/collision_world.rs b/src/scripting/ops/collision_world.rs index d9c186b011..baf492c8ac 100644 --- a/src/scripting/ops/collision_world.rs +++ b/src/scripting/ops/collision_world.rs @@ -6,12 +6,6 @@ use bevy::ecs::system::SystemState; use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, JsValueRef, JsValueRefs, OpContext}; use once_cell::sync::OnceCell; -#[derive(Serialize)] -struct JsNetInfo { - is_server: bool, - is_client: bool, -} - pub struct CollisionWorldActorCollisions; impl JsRuntimeOp for CollisionWorldActorCollisions { fn js(&self) -> Option<&'static str> { diff --git a/src/scripting/ops/entity.rs b/src/scripting/ops/entity.rs index 412ec406d1..c747c7d555 100644 --- a/src/scripting/ops/entity.rs +++ b/src/scripting/ops/entity.rs @@ -5,23 +5,8 @@ use anyhow::Context; use bevy::prelude::Entity; use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, JsValueRef, JsValueRefs}; -use serde::{Deserialize, Serialize}; -use crate::scripting::JsU64; - -#[derive(Serialize, Deserialize)] -struct JsEntity(JsU64); - -impl From for JsEntity { - fn from(e: Entity) -> Self { - Self(e.to_bits().into()) - } -} -impl From for Entity { - fn from(e: JsEntity) -> Self { - Entity::from_bits(e.0.into()) - } -} +use crate::scripting::JsEntity; pub struct EntityRefToJs; impl JsRuntimeOp for EntityRefToJs { diff --git a/src/scripting/ops/item.rs b/src/scripting/ops/item.rs deleted file mode 100644 index 80c4c2ac7f..0000000000 --- a/src/scripting/ops/item.rs +++ /dev/null @@ -1,180 +0,0 @@ -use std::{path::PathBuf, sync::Mutex}; - -use crate::{ - item::{Item, ItemDropEvent, ItemGrabEvent, ItemUseEvent}, - prelude::*, -}; -use bevy::{ecs::system::SystemState, utils::HashMap}; -use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, JsValueRef, OpContext}; -use once_cell::sync::OnceCell; - -pub struct ItemGrabEvents; -impl JsRuntimeOp for ItemGrabEvents { - fn js(&self) -> Option<&'static str> { - Some( - r#" - if (!globalThis.Items) { - globalThis.Items = {} - } - - globalThis.Items.grabEvents = () => { - return bevyModJsScriptingOpSync('jumpy_item_grab_events') - .map(x => globalThis.Value.wrapValueRef(x)); - } - "#, - ) - } - - fn run( - &self, - ctx: OpContext, - world: &mut World, - _args: serde_json::Value, - ) -> anyhow::Result { - let script_path = ctx.script_info.path.to_str().expect("non-unicode path"); - - type Param<'w, 's> = ( - Query<'w, 's, &'static Item>, - EventReader<'w, 's, ItemGrabEvent>, - ); - static STATE: OnceCell>>> = OnceCell::new(); - let mut states = STATE - .get_or_init(|| Mutex::new(HashMap::default())) - .lock() - .unwrap(); - let state = states - .entry(ctx.script_info.path.clone()) - .or_insert_with(|| SystemState::new(world)); - - let value_refs = ctx.op_state.get_mut().unwrap(); - - let (items, mut grab_events) = state.get_mut(world); - - let events = grab_events - .iter() - .filter(|event| { - items - .get(event.item) - .map(|item| item.script == script_path) - .unwrap_or(false) - }) - .map(|x| JsValueRef::new_free(Box::new(x.clone()), value_refs)) - .collect::>(); - - Ok(serde_json::to_value(&events)?) - } -} - -pub struct ItemDropEvents; -impl JsRuntimeOp for ItemDropEvents { - fn js(&self) -> Option<&'static str> { - Some( - r#" - if (!globalThis.Items) { - globalThis.Items = {} - } - - globalThis.Items.dropEvents = () => { - return bevyModJsScriptingOpSync('jumpy_item_drop_events') - .map(x => globalThis.Value.wrapValueRef(x)); - } - "#, - ) - } - - fn run( - &self, - ctx: OpContext, - world: &mut World, - _args: serde_json::Value, - ) -> anyhow::Result { - let script_path = ctx.script_info.path.to_str().expect("non-unicode path"); - - type Param<'w, 's> = ( - Query<'w, 's, &'static Item>, - EventReader<'w, 's, ItemDropEvent>, - ); - static STATE: OnceCell>>> = OnceCell::new(); - let mut states = STATE - .get_or_init(|| Mutex::new(HashMap::default())) - .lock() - .unwrap(); - let state = states - .entry(ctx.script_info.path.clone()) - .or_insert_with(|| SystemState::new(world)); - - let value_refs = ctx.op_state.get_mut().unwrap(); - - let (items, mut drop_events) = state.get_mut(world); - - let events = drop_events - .iter() - .filter(|event| { - items - .get(event.item) - .map(|item| item.script == script_path) - .unwrap_or(false) - }) - .map(|x| JsValueRef::new_free(Box::new(x.clone()), value_refs)) - .collect::>(); - - Ok(serde_json::to_value(&events)?) - } -} - -pub struct ItemUseEvents; -impl JsRuntimeOp for ItemUseEvents { - fn js(&self) -> Option<&'static str> { - Some( - r#" - if (!globalThis.Items) { - globalThis.Items = {} - } - - globalThis.Items.useEvents = () => { - return bevyModJsScriptingOpSync('jumpy_item_use_events') - .map(x => globalThis.Value.wrapValueRef(x)); - } - "#, - ) - } - - fn run( - &self, - ctx: OpContext, - world: &mut World, - _args: serde_json::Value, - ) -> anyhow::Result { - let script_path = ctx.script_info.path.to_str().expect("non-unicode path"); - - type Param<'w, 's> = ( - Query<'w, 's, &'static Item>, - EventReader<'w, 's, ItemUseEvent>, - ); - static STATE: OnceCell>>> = OnceCell::new(); - let mut states = STATE - .get_or_init(|| Mutex::new(HashMap::default())) - .lock() - .unwrap(); - let state = states - .entry(ctx.script_info.path.clone()) - .or_insert_with(|| SystemState::new(world)); - - let value_refs = ctx.op_state.get_mut().unwrap(); - - let (items, mut use_events) = state.get_mut(world); - - let events = use_events - .iter() - .filter(|event| { - items - .get(event.item) - .map(|item| item.script == script_path) - .unwrap_or(false) - }) - .map(|x| JsValueRef::new_free(Box::new(x.clone()), value_refs)) - .collect::>(); - - Ok(serde_json::to_value(&events)?) - } -} diff --git a/src/scripting/ops/map.rs b/src/scripting/ops/map.rs index 3c7fe36889..de35c114cb 100644 --- a/src/scripting/ops/map.rs +++ b/src/scripting/ops/map.rs @@ -1,10 +1,6 @@ -use crate::{metadata::MapElementMeta, prelude::*}; +use crate::{map::MapElementHydrated, metadata::MapElementMeta, prelude::*}; use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, JsValueRef, OpContext}; -#[derive(Component)] -#[component(storage = "SparseSet")] -pub struct MapElementLoaded; - pub struct ElementGetSpawnedEntities; impl JsRuntimeOp for ElementGetSpawnedEntities { fn js(&self) -> Option<&'static str> { @@ -31,7 +27,7 @@ impl JsRuntimeOp for ElementGetSpawnedEntities { let value_refs = ctx.op_state.get_mut().unwrap(); let entities = world - .query_filtered::<(Entity, &MapElementMeta), Without>() + .query_filtered::<(Entity, &MapElementMeta), Without>() .iter(world) .filter(|(_, meta)| { meta.script_handles @@ -42,7 +38,7 @@ impl JsRuntimeOp for ElementGetSpawnedEntities { .collect::>() .into_iter() .map(|entity| { - world.entity_mut(entity).insert(MapElementLoaded); + world.entity_mut(entity).insert(MapElementHydrated); JsValueRef::new_free(Box::new(entity), value_refs) }) .collect::>(); diff --git a/src/scripting/ops/net.rs b/src/scripting/ops/net.rs index accaba0bf7..c28a85c704 100644 --- a/src/scripting/ops/net.rs +++ b/src/scripting/ops/net.rs @@ -1,16 +1,6 @@ -use crate::{ - networking::{client::NetClient, proto::ClientMatchInfo, server::NetServer}, - prelude::*, -}; +use crate::{networking::proto::ClientMatchInfo, prelude::*}; use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, OpContext}; -#[derive(Serialize)] -struct JsNetInfo { - is_server: bool, - is_client: bool, - player_idx: usize, -} - pub struct NetInfoGet; impl JsRuntimeOp for NetInfoGet { fn js(&self) -> Option<&'static str> { @@ -33,15 +23,7 @@ impl JsRuntimeOp for NetInfoGet { world: &mut World, _args: serde_json::Value, ) -> anyhow::Result { - let is_server = world.contains_resource::(); - let is_client = world.contains_resource::(); let match_info = world.get_resource::(); - let player_idx = match_info.map(|info| info.player_idx).unwrap_or(0); - - Ok(serde_json::to_value(&JsNetInfo { - is_server, - is_client, - player_idx, - })?) + Ok(serde_json::to_value(match_info)?) } } diff --git a/src/scripting/ops/player.rs b/src/scripting/ops/player.rs index a918552826..0e8add3fca 100644 --- a/src/scripting/ops/player.rs +++ b/src/scripting/ops/player.rs @@ -1,16 +1,12 @@ -use std::sync::Mutex; - use crate::{ player::{ - PlayerDespawnCommand, PlayerKillCommand, PlayerKillEvent, PlayerSetInventoryCommand, - PlayerUseItemCommand, + PlayerDespawnCommand, PlayerKillCommand, PlayerSetInventoryCommand, PlayerUseItemCommand, }, prelude::*, }; use anyhow::Context; -use bevy::ecs::system::{Command, SystemState}; +use bevy::ecs::system::Command; use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, JsValueRef, JsValueRefs, OpContext}; -use once_cell::sync::OnceCell; pub struct PlayerKill; impl JsRuntimeOp for PlayerKill { @@ -112,50 +108,6 @@ impl JsRuntimeOp for PlayerGetInventory { } } -pub struct PlayerKillEvents; -impl JsRuntimeOp for PlayerKillEvents { - fn js(&self) -> Option<&'static str> { - Some( - r#" - if (!globalThis.Player) { - globalThis.Player = {} - } - - globalThis.Player.killEvents = () => { - return bevyModJsScriptingOpSync('jumpy_player_kill_events') - .map(x => globalThis.Value.wrapValueRef(x)); - } - "#, - ) - } - - fn run( - &self, - ctx: OpContext, - world: &mut World, - _args: serde_json::Value, - ) -> anyhow::Result { - type Param<'w, 's> = EventReader<'w, 's, PlayerKillEvent>; - - static STATE: OnceCell>> = OnceCell::new(); - let mut state = STATE - .get_or_init(|| Mutex::new(SystemState::new(world))) - .lock() - .unwrap(); - - let value_refs = ctx.op_state.get_mut().unwrap(); - - let events = state - .get_mut(world) - .iter() - .cloned() - .map(|x| JsValueRef::new_free(Box::new(x), value_refs)) - .collect::>(); - - Ok(serde_json::to_value(&events)?) - } -} - pub struct PlayerSetInventory; impl JsRuntimeOp for PlayerSetInventory { fn js(&self) -> Option<&'static str> { diff --git a/src/scripting/ops/random.rs b/src/scripting/ops/random.rs new file mode 100644 index 0000000000..04e019f3a3 --- /dev/null +++ b/src/scripting/ops/random.rs @@ -0,0 +1,29 @@ +use crate::{prelude::*, random::GlobalRng}; +use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, OpContext}; + +pub struct Random; +impl JsRuntimeOp for Random { + fn js(&self) -> Option<&'static str> { + Some( + r#" + if (!globalThis.Random) { + globalThis.Random = {} + } + + globalThis.Random.gen = () => { + return bevyModJsScriptingOpSync('jumpy_random'); + } + "#, + ) + } + + fn run( + &self, + _ctx: OpContext, + world: &mut World, + _args: serde_json::Value, + ) -> anyhow::Result { + let rng = world.resource::(); + Ok(serde_json::to_value(rng.f32())?) + } +} diff --git a/src/scripting/ops/rollback.rs b/src/scripting/ops/rollback.rs new file mode 100644 index 0000000000..254a44a055 --- /dev/null +++ b/src/scripting/ops/rollback.rs @@ -0,0 +1,9 @@ +use bevy_mod_js_scripting::JsRuntimeOp; + +pub struct RollbackHooks; + +impl JsRuntimeOp for RollbackHooks { + fn js(&self) -> Option<&'static str> { + Some(include_str!("./rollback/rollback_hooks.js")) + } +} diff --git a/src/scripting/ops/rollback/rollback_hooks.js b/src/scripting/ops/rollback/rollback_hooks.js new file mode 100644 index 0000000000..1dc16a5318 --- /dev/null +++ b/src/scripting/ops/rollback/rollback_hooks.js @@ -0,0 +1,22 @@ +globalThis.snapshots = []; + +globalThis.saveSnapshot = ({ f: frame, m: maxSnapshots }) => { + globalThis.snapshots[frame % maxSnapshots] = JSON.stringify(globalThis.jsState || {}); +} +globalThis.loadSnapshot = ({ f: frame, m: maxSnapshots, e: entityMap }) => { + globalThis.jsState = JSON.parse(globalThis.snapshots[frame % maxSnapshots] || '{}'); + + if (globalThis.jsState.entityLists && entityMap.length > 0) { + for (const listName in globalThis.jsState.entityLists) { + const entityList = globalThis.jsState.entityLists[listName]; + for (const i in entityList) { + const entity = entityList[i]; + for (const { t: to, f: from } of entityMap) { + if (from[0] == entity[0] && from[1] == entity[1]) { + entityList[i] = to; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/scripting/ops/script.rs b/src/scripting/ops/script.rs index 355e56e9e1..df470893d7 100644 --- a/src/scripting/ops/script.rs +++ b/src/scripting/ops/script.rs @@ -14,49 +14,7 @@ struct JsScriptInfo { pub struct ScriptGetInfo; impl JsRuntimeOp for ScriptGetInfo { fn js(&self) -> Option<&'static str> { - Some( - r#" - const cloneObj = x => JSON.parse(JSON.stringify(x)); - if (!globalThis.Script) { - globalThis.Script = {} - } - - globalThis.Script.getInfo = () => { - return bevyModJsScriptingOpSync('jumpy_script_get_info'); - } - - globalThis.Script.state = (init) => { - const scriptId = Script.getInfo().path; - if (!globalThis.scriptState) globalThis.scriptState = {}; - if (!globalThis.scriptState[scriptId]) globalThis.scriptState[scriptId] = cloneObj(init) || {}; - return globalThis.scriptState[scriptId]; - } - - globalThis.Script.entityStates = () => { - if (!globalThis.scriptEntityState) globalThis.scriptState = {}; - if (!globalThis.scriptEntityState[scriptId]) globalThis.scriptState[scriptId] = {}; - return globalThis.scriptEntityState[scriptId]; - } - - globalThis.Script.getEntityState = (entity, init) => { - const jsEntity = EntityRef.toJs(entity); - const entityKey = JSON.stringify(jsEntity); - const scriptId = Script.getInfo().path; - if (!globalThis.scriptEntityState) globalThis.scriptEntityState = {}; - if (!globalThis.scriptEntityState[scriptId]) globalThis.scriptEntityState[scriptId] = {}; - if (!globalThis.scriptEntityState[scriptId][entityKey]) globalThis.scriptEntityState[scriptId][entityKey] = cloneObj(init) || {}; - return globalThis.scriptEntityState[scriptId][entityKey]; - } - globalThis.Script.setEntityState = (entity, state) => { - const jsEntity = EntityRef.toJs(entity); - const entityKey = JSON.stringify(jsEntity); - const scriptId = Script.getInfo().path; - if (!globalThis.scriptEntityState) globalThis.scriptEntityState = {}; - if (!globalThis.scriptEntityState[scriptId]) globalThis.scriptEntityState[scriptId] = {}; - globalThis.scriptEntityState[scriptId][entityKey] = state; - } - "#, - ) + Some(include_str!("./script/script.js")) } fn run( diff --git a/src/scripting/ops/script/script.js b/src/scripting/ops/script/script.js new file mode 100644 index 0000000000..a051f0397d --- /dev/null +++ b/src/scripting/ops/script/script.js @@ -0,0 +1,93 @@ +const cloneObj = x => JSON.parse(JSON.stringify(x)); +const jsEntityEq = (ent1, ent2) => { + return ent1[0] == ent2[0] && ent1[1] == ent2[1]; +} +if (!globalThis.Script) { + globalThis.Script = {} +} + +globalThis.Script.getInfo = () => { + return bevyModJsScriptingOpSync('jumpy_script_get_info'); +} + +globalThis.Script.state = (init) => { + const scriptId = Script.getInfo().path; + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.script) globalThis.jsState.script = {}; + if (!globalThis.jsState.script[scriptId]) globalThis.jsState.script[scriptId] = cloneObj(init) || {}; + return globalThis.jsState.script[scriptId]; +} + +globalThis.Script.getEntityList = (listName) => { + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.entityLists) globalThis.jsState.entityLists = {}; + if (!globalThis.jsState.entityLists[listName]) globalThis.jsState.entityLists[listName] = []; + return globalThis.jsState.entityLists[listName].map(e => EntityRef.fromJs(e)); +} + +globalThis.Script.addEntityToList = (listName, entity) => { + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.entityLists) globalThis.jsState.entityLists = {}; + if (!globalThis.jsState.entityLists[listName]) globalThis.jsState.entityLists[listName] = []; + let list = globalThis.jsState.entityLists[listName]; + const jsEntity = EntityRef.toJs(entity); + list.push(jsEntity); +} + +globalThis.Script.entityListContains = (listName, entity) => { + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.entityLists) globalThis.jsState.entityLists = {}; + if (!globalThis.jsState.entityLists[listName]) globalThis.jsState.entityLists[listName] = []; + let list = globalThis.jsState.entityLists[listName]; + const jsEntity = EntityRef.toJs(entity); + + // Look for entity in list + for (const item of list) { + if (jsEntityEq(item, jsEntity)) { + return true; + } + } + + return false; +} + +globalThis.Script.removeEntityFromList = (listName, entity) => { + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.entityLists) globalThis.jsState.entityLists = {}; + if (!globalThis.jsState.entityLists[listName]) globalThis.jsState.entityLists[listname] = []; + let list = globalThis.jsState.entityLists[listName]; + const jsEntity = EntityRef.toJs(entity); + globalThis.jsState.entityLists[listName] = list.filter(x => !jsEntityEq(x, jsEntity)); +} + +globalThis.Script.clearEntityList = (listName) => { + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.entityLists) globalThis.jsState.entityLists = {}; + globalThis.jsState.entityLists[listName] = []; +} + +globalThis.Script.entityStates = () => { + if (!globalThis.jsState) globalThis.jsState = {}; + if (!globalThis.jsState.entity) globalThis.jsState.script = {}; + if (!globalThis.jsState.entity[scriptId]) globalThis.jsState.script[scriptId] = {}; + return globalThis.jsState.entity[scriptId]; +} + +globalThis.Script.getEntityState = (entity, init) => { + const jsEntity = EntityRef.toJs(entity); + const entityKey = JSON.stringify(jsEntity); + const scriptId = Script.getInfo().path; + if (!globalThis.jsState.entity) globalThis.jsState.entity = {}; + if (!globalThis.jsState.entity[scriptId]) globalThis.jsState.entity[scriptId] = {}; + if (!globalThis.jsState.entity[scriptId][entityKey]) globalThis.jsState.entity[scriptId][entityKey] = cloneObj(init) || {}; + return globalThis.jsState.entity[scriptId][entityKey]; +} + +globalThis.Script.setEntityState = (entity, state) => { + const jsEntity = EntityRef.toJs(entity); + const entityKey = JSON.stringify(jsEntity); + const scriptId = Script.getInfo().path; + if (!globalThis.jsState.entity) globalThis.jsState.entity = {}; + if (!globalThis.jsState.entity[scriptId]) globalThis.jsState.entity[scriptId] = {}; + globalThis.jsState.entity[scriptId][entityKey] = state; +} \ No newline at end of file diff --git a/src/scripting/ops/world.rs b/src/scripting/ops/world.rs index ccdfc693af..75a351d103 100644 --- a/src/scripting/ops/world.rs +++ b/src/scripting/ops/world.rs @@ -1,7 +1,3 @@ -//! Extensions to the script environment `world` global. -//! -//! TODO: These ops should be migrated to the `bevy_mod_js_scripting` crate. - use anyhow::Context; use bevy_mod_js_scripting::{serde_json, JsRuntimeOp, JsValueRef, JsValueRefs, OpContext}; @@ -41,3 +37,39 @@ impl JsRuntimeOp for WorldDespawnRecursive { Ok(serde_json::Value::Null) } } + +pub struct WorldSpawn; +impl JsRuntimeOp for WorldSpawn { + fn js(&self) -> Option<&'static str> { + Some( + r#" + if (!globalThis.WorldTemp) { + globalThis.WorldTemp = {} + } + + globalThis.WorldTemp.spawn = () => { + return Value.wrapValueRef(bevyModJsScriptingOpSync( + 'jumpy_world_spawn', + )); + } + "#, + ) + } + + fn run( + &self, + ctx: OpContext, + world: &mut World, + _args: serde_json::Value, + ) -> anyhow::Result { + let value_refs = ctx.op_state.get_mut::().unwrap(); + + let id = world.resource_scope(|world, mut rids: Mut| { + world.spawn().insert(Rollback::new(rids.next_id())).id() + }); + + let entity_ref = JsValueRef::new_free(Box::new(id), value_refs); + + Ok(serde_json::to_value(&entity_ref)?) + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000000..e3016ada88 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,120 @@ +//! Game session handling. +//! +//! A "session" in this context means either a local or networked game session, which for network +//! games will be synced with peers. + +use bevy::ecs::system::SystemParam; +use bevy_ggrs::{ + ggrs::{self, NonBlockingSocket, SessionBuilder}, + SessionType, +}; +use jumpy_matchmaker_proto::TargetClient; + +use crate::{ + networking::{client::NetClient, proto::ClientMatchInfo}, + player, + prelude::*, + GgrsConfig, +}; + +pub struct SessionPlugin; + +impl Plugin for SessionPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()) + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage( + RollbackStage::Last, + |mut frame_idx: ResMut| { + frame_idx.0 = frame_idx.0.wrapping_add(1); + trace!("End of simulation frame {}", frame_idx.0); + }, + ); + }); + } +} + +/// The current game logic frame, as distict from a render frame, in the presence of rollback. +/// +/// Primarily used for diagnostics. +#[derive(Reflect, Component, Default)] +#[reflect(Default)] +pub struct FrameIdx(pub u32); + +#[derive(SystemParam)] +pub struct SessionManager<'w, 's> { + commands: Commands<'w, 's>, + client_match_info: Option>, + client: Option>, +} + +impl NonBlockingSocket for NetClient { + fn send_to(&mut self, msg: &ggrs::Message, addr: &usize) { + self.send_unreliable(msg.clone(), TargetClient::One(*addr as u8)); + } + + fn receive_all_messages(&mut self) -> Vec<(usize, ggrs::Message)> { + let mut messages = Vec::new(); + while let Some(message) = self.recv_unreliable() { + match message.kind { + crate::networking::proto::UnreliableGameMessageKind::Ggrs(msg) => { + messages.push((message.from_player_idx, msg)); + } + } + } + messages + } +} + +impl<'w, 's> SessionManager<'w, 's> { + /// Setup the game session + pub fn start_session(&mut self) { + const INPUT_DELAY: usize = 1; + + if let Some((info, client)) = self.client_match_info.as_ref().zip(self.client.as_ref()) { + let client = (*client).clone(); + + let mut builder = SessionBuilder::::new(); + builder = builder + .with_input_delay(INPUT_DELAY) + .with_num_players(info.player_count); + + for i in 0..info.player_count { + builder = builder + .add_player( + if i == info.player_idx { + ggrs::PlayerType::Local + } else { + ggrs::PlayerType::Remote(i) + }, + i, + ) + .expect("Invalid player handle"); + } + + let session = builder.start_p2p_session(client).unwrap(); + self.commands.insert_resource(session); + self.commands.insert_resource(SessionType::P2PSession); + info!("Started P2P session"); + } else { + let mut builder = SessionBuilder::::new(); + + builder = builder + .with_input_delay(INPUT_DELAY) + .with_num_players(player::MAX_PLAYERS) + // TODO: Add some flag to enable the non-zero check distance, instead of wasting + // processing power for local games. + .with_check_distance(7); + + for i in 0..player::MAX_PLAYERS { + builder = builder.add_player(ggrs::PlayerType::Local, i).unwrap(); + } + + let session = builder.start_synctest_session().unwrap(); + self.commands.insert_resource(session); + self.commands.insert_resource(SessionType::SyncTestSession); + info!("Started Local session"); + } + } +} diff --git a/src/ui/debug_tools.rs b/src/ui/debug_tools.rs index 148d69e784..963255be8d 100644 --- a/src/ui/debug_tools.rs +++ b/src/ui/debug_tools.rs @@ -143,7 +143,7 @@ fn frame_diagnostic_window( egui::Window::new(&localization.get("frame-diagnostics")) .id(egui::Id::new("frame_diagnostics")) .default_width(500.0) - .open(&mut **show) + .open(&mut show) .show(ctx, |ui| { if ui.button(&localization.get("reset-min-max")).clicked() { *state = default(); diff --git a/src/ui/editor.rs b/src/ui/editor.rs index 0040b3ea1f..6efb20c192 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -29,12 +29,11 @@ impl Plugin for EditorPlugin { .run_in_state(InGameState::Editing), ) .add_system( - iyes_loopless::condition::IntoConditionalExclusiveSystem::run_in_state( - editor_ui_system, - GameState::InGame, - ) - .run_in_state(InGameState::Editing) - .at_end(), + editor_ui_system + .into_conditional_exclusive() + .run_in_state(GameState::InGame) + .run_in_state(InGameState::Editing) + .at_end(), ) .add_enter_system(InGameState::Editing, setup_editor) .add_exit_system(InGameState::Editing, cleanup_editor) @@ -149,6 +148,7 @@ struct EditorTopBar<'w, 's> { ResetManager<'w, 's>, ), >, + rids: ResMut<'w, RollbackIdProvider>, show_map_export_window: Local<'s, bool>, localization: Res<'w, Localization>, map_meta: Query<'w, 's, &'static MapMeta>, @@ -246,7 +246,8 @@ impl<'w, 's> WidgetSystem for EditorTopBar<'w, 's> { .camera_commands_resetcontroller .p1() .spawn() - .insert(handle); + .insert(handle) + .insert(Rollback::new(params.rids.next_id())); } } }); diff --git a/src/ui/main_menu.rs b/src/ui/main_menu.rs index 58d62aa039..498f6b0bf2 100644 --- a/src/ui/main_menu.rs +++ b/src/ui/main_menu.rs @@ -6,7 +6,6 @@ use bevy::{ }; use bevy_egui::*; use bevy_fluent::Localization; -use iyes_loopless::condition::IntoConditionalExclusiveSystem; use crate::{ localization::LocalizationExt, @@ -46,7 +45,12 @@ impl Plugin for MainMenuPlugin { .init_resource::() .init_resource::() .init_resource::() - .add_system(main_menu_system.run_in_state(GameState::MainMenu).at_end()) + .add_system( + main_menu_system + .into_conditional_exclusive() + .run_in_state(GameState::MainMenu) + .at_end(), + ) .add_enter_system(GameState::MainMenu, setup_main_menu) .add_exit_system(GameState::MainMenu, clean_up_main_menu); } diff --git a/src/ui/main_menu/map_select.rs b/src/ui/main_menu/map_select.rs index da985713f0..5adca7f160 100644 --- a/src/ui/main_menu/map_select.rs +++ b/src/ui/main_menu/map_select.rs @@ -1,21 +1,27 @@ +use bevy_ggrs::RollbackIdProvider; +use jumpy_matchmaker_proto::TargetClient; + use crate::{ metadata::MapMeta, networking::{ client::NetClient, - proto::match_setup::{MatchSetupFromClient, MatchSetupFromServer}, + proto::{match_setup::MatchSetupMessage, ReliableGameMessageKind}, }, + session::SessionManager, }; use super::*; #[derive(SystemParam)] pub struct MapSelectMenu<'w, 's> { - client: Option>, + client: Option>, menu_page: ResMut<'w, MenuPage>, game: Res<'w, GameMeta>, commands: Commands<'w, 's>, localization: Res<'w, Localization>, map_assets: Res<'w, Assets>, + rids: ResMut<'w, RollbackIdProvider>, + session_manager: SessionManager<'w, 's>, #[system_param(ignore)] _phantom: PhantomData<(&'w (), &'s ())>, } @@ -82,6 +88,7 @@ impl<'w, 's> WidgetSystem for MapSelectMenu<'w, 's> { .show(ui) .clicked() { + info!("Selected map, starting game"); *params.menu_page = MenuPage::Home; params.commands.spawn().insert(map_handle.clone_weak()); params @@ -90,11 +97,13 @@ impl<'w, 's> WidgetSystem for MapSelectMenu<'w, 's> { params .commands .insert_resource(NextState(InGameState::Playing)); + params.session_manager.start_session(); if let Some(client) = &mut params.client { - client.send_reliable(&MatchSetupFromClient::SelectMap( - map_handle, - )); + client.send_reliable( + MatchSetupMessage::SelectMap(map_handle), + TargetClient::All, + ); } } } @@ -107,24 +116,27 @@ impl<'w, 's> WidgetSystem for MapSelectMenu<'w, 's> { fn handle_match_setup_messages(params: &mut MapSelectMenu) { if let Some(client) = &mut params.client { - while let Some(message) = client.recv_reliable::() { - match message { - MatchSetupFromServer::ClientMessage { - player_idx: _, - message: MatchSetupFromClient::SelectMap(map_handle), - } => { - *params.menu_page = MenuPage::Home; - params.commands.spawn().insert(map_handle); - params - .commands - .insert_resource(NextState(GameState::InGame)); - params - .commands - .insert_resource(NextState(InGameState::Playing)); - } - message => { - warn!("Unexpected message in map select: {message:?}"); - } + while let Some(message) = client.recv_reliable() { + match message.kind { + ReliableGameMessageKind::MatchSetup(setup) => match setup { + MatchSetupMessage::SelectMap(map_handle) => { + info!("Other player selected map, starting game"); + *params.menu_page = MenuPage::Home; + params + .commands + .spawn() + .insert(map_handle) + .insert(Rollback::new(params.rids.next_id())); + params + .commands + .insert_resource(NextState(GameState::InGame)); + params + .commands + .insert_resource(NextState(InGameState::Playing)); + params.session_manager.start_session(); + } + other => warn!("Unexpected message: {other:?}"), + }, } } } diff --git a/src/ui/main_menu/matchmaking.rs b/src/ui/main_menu/matchmaking.rs index bcb3750da0..c4c3a2e5dc 100644 --- a/src/ui/main_menu/matchmaking.rs +++ b/src/ui/main_menu/matchmaking.rs @@ -7,7 +7,11 @@ use futures_lite::future; use jumpy_matchmaker_proto::{MatchInfo, MatchmakerRequest, MatchmakerResponse}; use quinn::{Connection, Endpoint}; -use crate::{networking::client::NetClient, player::MAX_PLAYERS}; +use crate::{ + networking::{client::NetClient, proto::ClientMatchInfo}, + player::MAX_PLAYERS, + random::GlobalRng, +}; use super::*; @@ -20,6 +24,8 @@ pub struct MatchmakingMenu<'w, 's> { localization: Res<'w, Localization>, state: Local<'s, State>, menu_input: Query<'w, 's, &'static mut ActionState>, + global_rng: Res<'w, GlobalRng>, + player_inputs: ResMut<'w, PlayerInputs>, } pub struct State { @@ -38,7 +44,12 @@ enum Status { WaitingForPlayers { players: usize, }, - MatchReady(Endpoint, Connection), + MatchReady { + endpoint: Endpoint, + conn: Connection, + random_seed: u64, + client_info: ClientMatchInfo, + }, Errored(String), } @@ -47,7 +58,7 @@ impl Status { matches!(self, Status::NotConnected) || matches!(self, Status::Errored(_)) } fn is_match_ready(&self) -> bool { - matches!(self, Status::MatchReady(_, _)) + matches!(self, Status::MatchReady { .. }) } } @@ -89,13 +100,24 @@ impl<'w, 's> WidgetSystem for MatchmakingMenu<'w, 's> { if params.state.status.is_match_ready() { let status = std::mem::take(&mut params.state.status); - if let Status::MatchReady(endpoint, conn) = status { + if let Status::MatchReady { + endpoint, + conn, + random_seed, + client_info, + } = status + { + for i in 0..client_info.player_count { + params.player_inputs.players[i].active = true; + } let client = NetClient::new(endpoint, conn); params.commands.insert_resource(client); + params.commands.insert_resource(client_info); *params.menu_page = MenuPage::PlayerSelect; params.state.status_receiver = default(); params.state.status = default(); params.state.cancel_sender = default(); + params.global_rng.reseed(random_seed); } else { unreachable!("Programmer error in is_match_ready() helper method"); } @@ -238,7 +260,7 @@ impl<'w, 's> WidgetSystem for MatchmakingMenu<'w, 's> { )), ); } - Status::MatchReady(_, _) => { + Status::MatchReady { .. } => { // We shouldn't get here because we check for a ready match above } Status::Errored(e) => { @@ -310,7 +332,10 @@ async fn impl_start_matchmaking( let (mut send, recv) = conn.open_bi().await?; - let message = MatchmakerRequest::RequestMatch(MatchInfo { player_count }); + let message = MatchmakerRequest::RequestMatch(MatchInfo { + client_count: player_count, + match_data: b"jumpy_default_game".to_vec(), + }); let message = postcard::to_allocvec(&message)?; send.write_all(&message).await?; @@ -336,16 +361,30 @@ async fn impl_start_matchmaking( let message: MatchmakerResponse = postcard::from_bytes(&message)?; match message { - MatchmakerResponse::PlayerCount(count) => { + MatchmakerResponse::ClientCount(count) => { status .try_send(Status::WaitingForPlayers { players: count as usize, }) .ok(); } - MatchmakerResponse::Success => { + MatchmakerResponse::Success { + random_seed, + player_idx, + client_count, + } => { + info!(%random_seed, %player_idx, %client_count, "Match established"); + let client_info = ClientMatchInfo { + player_idx: player_idx as usize, + player_count: client_count as usize, + }; status - .try_send(Status::MatchReady(endpoint, conn.clone())) + .try_send(Status::MatchReady { + endpoint, + conn: conn.clone(), + random_seed, + client_info, + }) .ok(); break; } diff --git a/src/ui/main_menu/player_select.rs b/src/ui/main_menu/player_select.rs index ccfc61ff93..be40c3b127 100644 --- a/src/ui/main_menu/player_select.rs +++ b/src/ui/main_menu/player_select.rs @@ -1,15 +1,15 @@ +use jumpy_matchmaker_proto::TargetClient; + use crate::{ loading::PlayerInputCollector, metadata::PlayerMeta, networking::{ client::NetClient, - proto::{ - match_setup::{MatchSetupFromClient, MatchSetupFromServer}, - ClientMatchInfo, - }, + proto::{match_setup::MatchSetupMessage, ClientMatchInfo}, }, player::input::PlayerAction, player::{input::PlayerInputs, MAX_PLAYERS}, + random::GlobalRng, }; use super::*; @@ -33,7 +33,9 @@ pub struct PlayerSelectMenu<'w, 's> { player_select_state: ResMut<'w, PlayerSelectState>, keyboard_input: Res<'w, Input>, localization: Res<'w, Localization>, - client: Option>, + client: Option>, + client_info: Option>, + global_rng: Res<'w, GlobalRng>, #[system_param(ignore)] _phantom: PhantomData<&'s ()>, } @@ -63,8 +65,24 @@ impl<'w, 's> WidgetSystem for PlayerSelectMenu<'w, 's> { unconfirmed_players += 1; } } - let may_continue = - ready_players >= 1 && unconfirmed_players == 0 && params.client.is_none(); + let may_continue = ready_players >= 1 && unconfirmed_players == 0; + + if let Some(client_info) = params.client_info { + if may_continue { + let player_to_select_map = *params + .global_rng + .sample( + &(0usize..client_info.player_count) + .into_iter() + .collect::>(), + ) + .unwrap(); + info!(%player_to_select_map, %client_info.player_idx); + let is_waiting = player_to_select_map != client_info.player_idx; + + *params.menu_page = MenuPage::MapSelect { is_waiting }; + } + } ui.vertical_centered(|ui| { let params: PlayerSelectMenu = state.get_mut(world); @@ -173,30 +191,22 @@ impl<'w, 's> WidgetSystem for PlayerSelectMenu<'w, 's> { fn handle_match_setup_messages(params: &mut PlayerSelectMenu) { if let Some(client) = &mut params.client { - while let Some(message) = client.recv_reliable::() { - match message { - MatchSetupFromServer::ClientMessage { - player_idx, - message, - } => match message { - MatchSetupFromClient::SelectPlayer(player_handle) => { - params.player_inputs.players[player_idx as usize].selected_player = - player_handle; + while let Some(message) = client.recv_reliable() { + match message.kind { + crate::networking::proto::ReliableGameMessageKind::MatchSetup(setup) => match setup + { + MatchSetupMessage::SelectPlayer(player_handle) => { + params.player_inputs.players[message.from_player_idx as usize] + .selected_player = player_handle } - MatchSetupFromClient::ConfirmSelection(confirmed) => { - params.player_select_state.player_slots[player_idx as usize].confirmed = - confirmed; + MatchSetupMessage::ConfirmSelection(confirmed) => { + params.player_select_state.player_slots[message.from_player_idx as usize] + .confirmed = confirmed; } - message => { - warn!("Unexpected message in player select menu: {message:?}"); + MatchSetupMessage::SelectMap(_) => { + warn!("Unexpected map select message: player selection not yet confirmed"); } }, - MatchSetupFromServer::SelectMap => { - *params.menu_page = MenuPage::MapSelect { is_waiting: false }; - } - MatchSetupFromServer::WaitForMapSelect => { - *params.menu_page = MenuPage::MapSelect { is_waiting: true }; - } } } } @@ -276,7 +286,10 @@ impl<'w, 's> WidgetSystem for PlayerSelectPanel<'w, 's> { } if let Some(client) = params.client { - client.send_reliable(&MatchSetupFromClient::ConfirmSelection(slot.confirmed)); + client.send_reliable( + MatchSetupMessage::ConfirmSelection(slot.confirmed), + TargetClient::All, + ); } } else if player_actions.just_pressed(PlayerAction::Grab) { if params.client.is_none() { @@ -289,7 +302,10 @@ impl<'w, 's> WidgetSystem for PlayerSelectPanel<'w, 's> { slot.confirmed = false; } if let Some(client) = params.client { - client.send_reliable(&MatchSetupFromClient::ConfirmSelection(slot.confirmed)); + client.send_reliable( + MatchSetupMessage::ConfirmSelection(slot.confirmed), + TargetClient::All, + ); } } else if player_actions.just_pressed(PlayerAction::Move) && !slot.confirmed { let direction = player_actions @@ -332,9 +348,10 @@ impl<'w, 's> WidgetSystem for PlayerSelectPanel<'w, 's> { } if let Some(client) = params.client { - client.send_reliable(&MatchSetupFromClient::SelectPlayer( - player_handle.clone_weak(), - )); + client.send_reliable( + MatchSetupMessage::SelectPlayer(player_handle.clone_weak()), + TargetClient::All, + ); } } @@ -362,12 +379,7 @@ impl<'w, 's> WidgetSystem for PlayerSelectPanel<'w, 's> { if player_input.active { ui.vertical_centered(|ui| { - let player_meta = - if let Some(meta) = params.player_meta_assets.get(player_handle) { - meta - } else { - return; - }; + let Some(player_meta) = params.player_meta_assets.get(player_handle) else { return; }; ui.themed_label(normal_font, ¶ms.localization.get("pick-a-fish")); diff --git a/src/ui/pause_menu.rs b/src/ui/pause_menu.rs index 2e825fcd27..3e439c299e 100644 --- a/src/ui/pause_menu.rs +++ b/src/ui/pause_menu.rs @@ -6,6 +6,7 @@ use crate::{ metadata::{GameMeta, MapMeta}, networking::client::NetClient, prelude::*, + session::SessionManager, ui::input::MenuAction, utils::ResetManager, GameState, @@ -61,6 +62,7 @@ pub fn pause_menu( map_handle: Query<&AssetHandle>, mut reset_controller: ResetManager, client: Option>, + mut session_manager: SessionManager, ) { let is_online = client.is_some(); let ui_theme = &game.ui_theme; @@ -125,6 +127,8 @@ pub fn pause_menu( if let Some(handle) = map_handle { commands.spawn().insert(handle); } + + session_manager.start_session(); } }); diff --git a/src/utils.rs b/src/utils.rs index 2f803d3b97..be21264711 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,22 @@ -use bevy::ecs::system::SystemParam; +use bevy::ecs::{schedule::ShouldRun, system::SystemParam}; +use bevy_ggrs::{ggrs::SyncTestSession, ResetGGRSSession, SessionType}; -use crate::{loading::PlayerInputCollector, prelude::*, ui::input::MenuAction}; +use crate::{ + loading::PlayerInputCollector, map::elements::player_spawner::CurrentPlayerSpawner, prelude::*, + run_criteria::ShouldRunExt, ui::input::MenuAction, GgrsConfig, +}; pub mod event; +pub struct UtilsPlugin; + +impl Plugin for UtilsPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .extend_rollback_plugin(|plugin| plugin.register_rollback_type::()); + } +} + /// Cache a string using [`wasm_bindgen::intern`] when running on web platforms. /// /// [`wasm_bindgen::intern`]: https://docs.rs/wasm-bindgen/latest/wasm_bindgen/fn.intern.html @@ -14,6 +27,27 @@ pub fn cache_str(s: &str) { wasm_bindgen::intern(s); } +/// A [`Component`] that is simply an index that may be used to sort elements for deterministic +/// iteration. +#[derive( + Deref, DerefMut, Component, Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Reflect, Default, +)] +#[reflect(Component)] +pub struct Sort(pub u32); + +/// Returns the hypothetical "invalid entity" ( `Entity::from_raw(u32::MAX)` ). +/// +/// This serves as a workaround for the fact that [`Entity`] does not implement [`Default`], but +/// [`Default`] is required to reflect [`Component`]. +/// +/// It would be best to find a way to get rid of this, but I ( @zicklag ) belive that the chances of +/// the invalid entity turning out to be a real entity that happened to get all the way up to +/// index [`u32::MAX`] is highly unlikely. +#[inline] +pub fn invalid_entity() -> Entity { + Entity::from_raw(u32::MAX) +} + /// System parameter that can be used to reset the game world. /// /// Currently this just means de-spawning all of the entities other than the camera and resetting @@ -42,6 +76,7 @@ pub struct ResetManager<'w, 's> { Without>, ), >, + current_player_spawner: ResMut<'w, CurrentPlayerSpawner>, } impl<'w, 's> ResetManager<'w, 's> { @@ -60,5 +95,28 @@ impl<'w, 's> ResetManager<'w, 's> { transform.translation.y = 0.0; projection.scale = 1.0; } + + **self.current_player_spawner = 0; + + // Clear the game session + self.commands.insert_resource(ResetGGRSSession); + self.commands.remove_resource::(); + self.commands + .remove_resource::>(); } } + +/// Heper stage run criteria that only runs if we are in a gameplay state. +pub fn is_in_game_run_criteria( + game_state: Option>>, + in_game_state: Option>>, +) -> ShouldRun { + let is_in_game = game_state + .map(|x| x.0 == GameState::InGame) + .unwrap_or(false) + && in_game_state + .map(|x| x.0 != InGameState::Paused) + .unwrap_or(false); + + ShouldRun::new(is_in_game, false) +} diff --git a/src/utils/event.rs b/src/utils/event.rs index e623e63142..e2d23bc1b7 100644 --- a/src/utils/event.rs +++ b/src/utils/event.rs @@ -9,7 +9,9 @@ pub trait FixedUpdateEventAppExt { impl FixedUpdateEventAppExt for bevy::app::App { fn add_fixed_update_event(&mut self) -> &mut Self { self.init_resource::>() - .add_system_to_stage(FixedUpdateStage::First, Events::::update_system); + .extend_rollback_schedule(|schedule| { + schedule.add_system_to_stage(RollbackStage::First, Events::::update_system); + }); self } diff --git a/src/workarounds.rs b/src/workarounds.rs index 945b54d7df..6b8b40301c 100644 --- a/src/workarounds.rs +++ b/src/workarounds.rs @@ -3,18 +3,17 @@ use bevy::render::camera::CameraUpdateSystem; -use crate::{config::ENGINE_CONFIG, prelude::*}; +use crate::prelude::*; pub struct WorkaroundsPlugin; impl Plugin for WorkaroundsPlugin { fn build(&self, app: &mut App) { - if !ENGINE_CONFIG.server_mode { - app.add_system_to_stage( - CoreStage::PostUpdate, - update_camera_projection_when_camera_changes.before(CameraUpdateSystem), - ); - } + // TODO: This is fixed in Bevy 0.9 + app.add_system_to_stage( + CoreStage::PostUpdate, + update_camera_projection_when_camera_changes.before(CameraUpdateSystem), + ); } }