From 429dc2a2287bc9426c11378048d9539bfa6d3267 Mon Sep 17 00:00:00 2001 From: Nelson Earle Date: Tue, 22 Oct 2024 22:45:05 -0500 Subject: [PATCH] fix: require hat select before ready up (#1037) Fixes #963 ## Changes - [6781542](https://github.com/fishfolk/jumpy/pull/1037/commits/6781542946af7aafdaf0fe69ce0cf7cdd63b0781) `PlayerSlot` is now (sort of) a state machine, starting out `Empty` then progressing through `SelectingPlayer` and `SelectingHat` until becoming `Ready` - Slot for a network player only uses the `SelectingHat` and `Ready` states for simplicity - Angled brackets `<` and `>` show around the thing being selected (player/hat), brackets now don't show when ready - [a2393c4](https://github.com/fishfolk/jumpy/pull/1037/commits/a2393c462d54a45c3b66d92d1b02559b435504aa) fix bug where label has incorrect text for 1 frame when pressing Menu Back to remove player from slot - [f636d33](https://github.com/fishfolk/jumpy/pull/1037/commits/f636d3308832777d6714bceaba18c7e9e169da15) Upgrade bones to latest - Set gitattributes for `*.ftl` files so that they are treated as text for diffs and not binary files - Ignore advisory `RUSTSEC-2024-0370` proc-macro-error is unmaintained (see [related bones issue](https://github.com/fishfolk/bones/issues/479) and [the advisory](https://rustsec.org/advisories/RUSTSEC-2024-0370)) This also fixes a bug I was seeing on `main` where another player in a LAN lobby pressing A/D would behave as if everyone pressed A/D, e.g. pressing D to select the next skin would change to the next skin in both slots. **Rationale for the rough state machine:** Previously there were 3 states for a player slot: Empty, SelectingPlayer, and Ready + SelectingHat. This state was maintained by two booleans, `active` and `confirmed`, which tell you whether the slot is not empty and whether the player has readied-up respectively. Together they represent the 3 states as: (`active/confirmed`) `false/false`, `true/false` and `true/true`; with `false/true` being an invalid state. Now that there are four states to the ready-up process, one way of implementing this is to add a third boolean, but this introduces 3 more invalid states. Let's say the boolean is called `selectedPlayer`, these are the possible boolean combinations and the states they represent (`active/selectedPlayer/confirmed`): ``` false/false/false Empty false/false/true false/true/false false/true/true true/false/false SelectingPlayer true/false/true true/true/false SelectingHat true/true/true Ready ``` The implementation I opted for is a simple state machine. While it added a lot more code to some areas of this module I found it made reasoning about some sections much easier. It also made explicit certain edge cases in the `handle_match_setup_messages` system where e.g the client could theoretically receive a confirmation message from a peer when it had not yet received any player selection messages. A warning log is now generated in the unlikely case this occurs. --- .gitattributes | 2 + Cargo.lock | 487 ++++++++++++----- assets/locales/en-US/player-select.ftl | 5 +- deny.toml | 6 + src/ui/main_menu/map_select.rs | 12 +- src/ui/main_menu/player_select.rs | 729 ++++++++++++++++--------- src/ui/main_menu/settings/controls.rs | 6 +- 7 files changed, 829 insertions(+), 418 deletions(-) diff --git a/.gitattributes b/.gitattributes index 7413aca735..c5490127f1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -21,5 +21,7 @@ web/* linguist-vendored assets/** binary old_assets/** binary +*.ftl diff + # If you have other directories with mostly binary files, add them like this: # other_binary_directory/** binary diff --git a/Cargo.lock b/Cargo.lock index 3a954520ad..fd6c5e1d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,20 @@ dependencies = [ "winit", ] +[[package]] +name = "acto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c372578ce4215ccf94ec3f3585fbb6a902e47d07b064ff8a55d850ffb5025e" +dependencies = [ + "parking_lot", + "pin-project-lite", + "rustc_version", + "smol_str 0.1.24", + "tokio", + "tracing", +] + [[package]] name = "addr2line" version = "0.24.1" @@ -259,9 +273,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.5.2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -275,25 +289,25 @@ dependencies = [ [[package]] name = "asn1-rs-derive" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", - "synstructure 0.12.6", + "syn 2.0.79", + "synstructure", ] [[package]] name = "asn1-rs-impl" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.79", ] [[package]] @@ -307,18 +321,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-executor" version = "1.13.1" @@ -332,6 +334,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "async-task" version = "4.7.1" @@ -475,7 +488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9714af523da4cdf58c42a317e5ed40349708ad954a18533991fd64c8ae0a6f68" dependencies = [ "anyhow", - "async-channel 1.9.0", + "async-channel", "bevy_app", "bevy_diagnostic", "bevy_ecs", @@ -573,7 +586,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266144b36df7e834d5198049e037ecdf2a2310a76ce39ed937d1b0a6a2c4e8c6" dependencies = [ - "async-channel 1.9.0", + "async-channel", "bevy_ecs_macros", "bevy_ptr", "bevy_reflect", @@ -809,7 +822,7 @@ dependencies = [ "parking_lot", "serde", "smallvec", - "smol_str", + "smol_str 0.2.2", "thiserror", ] @@ -834,7 +847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39df4824b760928c27afc7b00fb649c7a63c9d76661ab014ff5c86537ee906cb" dependencies = [ "anyhow", - "async-channel 1.9.0", + "async-channel", "bevy_app", "bevy_asset", "bevy_core", @@ -940,7 +953,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c73bbb847c83990d3927005090df52f8ac49332e1643d2ad9aac3cd2974e66bf" dependencies = [ - "async-channel 1.9.0", + "async-channel", "async-executor", "async-task", "concurrent-queue", @@ -1180,11 +1193,11 @@ dependencies = [ [[package]] name = "bones_asset" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "anyhow", "append-only-vec", - "async-channel 1.9.0", + "async-channel", "bevy_tasks", "bones_schema", "bones_utils", @@ -1214,7 +1227,7 @@ dependencies = [ [[package]] name = "bones_bevy_renderer" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "anyhow", "bevy", @@ -1231,7 +1244,7 @@ dependencies = [ [[package]] name = "bones_ecs" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "anyhow", "atomicell", @@ -1249,7 +1262,7 @@ dependencies = [ [[package]] name = "bones_ecs_macros" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "bones_ecs_macros_core", "proc-macro2", @@ -1258,7 +1271,7 @@ dependencies = [ [[package]] name = "bones_ecs_macros_core" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "proc-macro2", "quote", @@ -1268,10 +1281,10 @@ dependencies = [ [[package]] name = "bones_framework" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "anyhow", - "async-channel 1.9.0", + "async-channel", "bevy_tasks", "bones_asset", "bones_lib", @@ -1325,7 +1338,7 @@ dependencies = [ [[package]] name = "bones_lib" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "bones_ecs", "instant", @@ -1335,7 +1348,7 @@ dependencies = [ [[package]] name = "bones_matchmaker_proto" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "iroh-net", "serde", @@ -1344,7 +1357,7 @@ dependencies = [ [[package]] name = "bones_schema" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "append-only-vec", "bones_schema_macros", @@ -1365,7 +1378,7 @@ dependencies = [ [[package]] name = "bones_schema_macros" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "proc-macro2", "quote", @@ -1375,9 +1388,9 @@ dependencies = [ [[package]] name = "bones_scripting" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ - "async-channel 1.9.0", + "async-channel", "bevy_tasks", "bones_asset", "bones_lib", @@ -1394,7 +1407,7 @@ dependencies = [ [[package]] name = "bones_utils" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "bones_utils_macros", "fxhash", @@ -1409,7 +1422,7 @@ dependencies = [ [[package]] name = "bones_utils_macros" version = "0.4.0" -source = "git+https://github.com/fishfolk/bones#4a7271514812c37fcafd84ea3abf51d118df3da6" +source = "git+https://github.com/fishfolk/bones#965b000bfe2d2b6cad0e6c32281c47d9c6aed3e6" dependencies = [ "quote", "venial", @@ -1788,7 +1801,7 @@ dependencies = [ "core-foundation-sys", "coreaudio-rs", "dasp_sample", - "jni", + "jni 0.21.1", "js-sys", "libc", "mach2", @@ -2036,9 +2049,9 @@ dependencies = [ [[package]] name = "der-parser" -version = "8.2.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs", "displaydoc", @@ -2071,18 +2084,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "1.0.0-beta.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3249c0372e72f5f93b5c0ca54c0ab76bbf6216b6f718925476fd9bc4ffabb4fe" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "1.0.0-beta.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d919ced7590fc17b5d5a3c63b662e8a7d2324212c4e4dbbed975cafd22d16d" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", @@ -2190,6 +2203,12 @@ dependencies = [ "shared_child", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "ecdsa" version = "0.16.9" @@ -2538,27 +2557,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener 5.3.1", - "pin-project-lite", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2946,7 +2944,38 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.79", - "synstructure 0.13.1", + "synstructure", +] + +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "futures-core", + "genawaiter-macro", + "genawaiter-proc-macro", + "proc-macro-hack", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "genawaiter-proc-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" +dependencies = [ + "proc-macro-error", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -3319,15 +3348,41 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-proto" +version = "0.25.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8270a1857fb962b9914aafd46a89a187a4e63d0eb4190c327e7c7b8256a2d055" +dependencies = [ + "async-recursion", + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.5.0", + "ipnet", + "once_cell", + "rand", + "thiserror", + "time", + "tinyvec", + "tokio", + "tracing", + "url", +] + [[package]] name = "hickory-resolver" -version = "0.24.1" +version = "0.25.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243" +checksum = "46c110355b5703070d9e29c344d79818a7cde3de9c27fc35750defea6074b0ad" dependencies = [ "cfg-if", "futures-util", - "hickory-proto", + "hickory-proto 0.25.0-alpha.2", "ipconfig", "lru-cache", "once_cell", @@ -3532,9 +3587,9 @@ dependencies = [ "rustls 0.23.13", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", - "webpki-roots 0.26.6", + "webpki-roots", ] [[package]] @@ -3776,9 +3831,9 @@ checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "iroh-base" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ddb47e8160fb1d563a6f541c813c2f185423a0ad1c9260a6c76891a2300c26" +checksum = "973c0b3c7851fa2e8e6cf4cb81c9f4cab1373848828fafa43dfe25b123a89ff2" dependencies = [ "aead", "anyhow", @@ -3817,9 +3872,9 @@ dependencies = [ [[package]] name = "iroh-metrics" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab017d2786c0b77583371cef016d3e76bdbc7d13b66532023cb7e854f65d15a" +checksum = "02edfa7ca1aa89b0b9793d94671e32b79c97d61e9022f082c5eb2b95b64e90c0" dependencies = [ "anyhow", "erased_set", @@ -3838,12 +3893,11 @@ dependencies = [ [[package]] name = "iroh-net" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372fbf01dc303be5427b6ea33b80411b3cfb6443d6389ce1ffc43231f244a51c" +checksum = "329553be056a92eec7802e1c087eed951eec6e60f0f0740491ef16a295be4ca2" dependencies = [ "anyhow", - "async-channel 2.3.1", "backoff", "base64 0.22.1", "bytes", @@ -3855,9 +3909,10 @@ dependencies = [ "futures-lite 2.3.0", "futures-sink", "futures-util", + "genawaiter", "governor", "hex", - "hickory-proto", + "hickory-proto 0.25.0-alpha.2", "hickory-resolver", "hostname", "http 1.1.0", @@ -3882,23 +3937,24 @@ dependencies = [ "pkarr", "postcard", "rand", - "rand_core", "rcgen", "reqwest", "ring", "rtnetlink", - "rustls 0.21.12", - "rustls-webpki 0.101.7", + "rustls 0.23.13", + "rustls-webpki 0.102.8", "serde", "smallvec", "socket2 0.5.7", "strum 0.26.3", "stun-rs", "surge-ping", + "swarm-discovery", "thiserror", "time", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", + "tokio-stream", "tokio-tungstenite", "tokio-tungstenite-wasm", "tokio-util", @@ -3906,7 +3962,7 @@ dependencies = [ "tungstenite", "url", "watchable", - "webpki-roots 0.25.4", + "webpki-roots", "windows 0.51.1", "wmi", "x509-parser", @@ -3915,16 +3971,17 @@ dependencies = [ [[package]] name = "iroh-quinn" -version = "0.10.5" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "906875956feb75d3d41d708ddaffeb11fdb10cd05f23efbcb17600037e411779" +checksum = "4fd590a39a14cfc168efa4d894de5039d65641e62d8da4a80733018ababe3c33" dependencies = [ "bytes", "iroh-quinn-proto", "iroh-quinn-udp", "pin-project-lite", - "rustc-hash 1.1.0", - "rustls 0.21.12", + "rustc-hash 2.0.0", + "rustls 0.23.13", + "socket2 0.5.7", "thiserror", "tokio", "tracing", @@ -3932,16 +3989,16 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" -version = "0.10.8" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6bf92478805e67f2320459285496e1137edf5171411001a0d4d85f9bbafb792" +checksum = "5fd0538ff12efe3d61ea1deda2d7913f4270873a519d43e6995c6e87a1558538" dependencies = [ "bytes", "rand", "ring", - "rustc-hash 1.1.0", - "rustls 0.21.12", - "rustls-native-certs", + "rustc-hash 2.0.0", + "rustls 0.23.13", + "rustls-platform-verifier", "slab", "thiserror", "tinyvec", @@ -3950,15 +4007,15 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" -version = "0.4.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc7915b3a31f08ee0bc02f73f4d61a5d5be146a1081ef7f70622a11627fd314" +checksum = "d0619b59471fdd393ac8a6c047f640171119c1c8b41f7d2927db91776dcdbc5f" dependencies = [ - "bytes", "libc", + "once_cell", "socket2 0.5.7", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -3982,6 +4039,20 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni" version = "0.21.1" @@ -4032,7 +4103,7 @@ dependencies = [ name = "jumpy" version = "0.12.2" dependencies = [ - "async-channel 1.9.0", + "async-channel", "bevy_dylib", "bevy_tasks", "bitfield", @@ -4313,6 +4384,26 @@ dependencies = [ "libc", ] +[[package]] +name = "mainline" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" +dependencies = [ + "bytes", + "crc", + "ed25519-dalek", + "flume", + "lru", + "rand", + "serde", + "serde_bencode", + "serde_bytes", + "sha1_smol", + "thiserror", + "tracing", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -5097,7 +5188,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni", + "jni 0.21.1", "ndk 0.8.0", "ndk-context", "num-derive", @@ -5116,9 +5207,9 @@ dependencies = [ [[package]] name = "oid-registry" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" dependencies = [ "asn1-rs", ] @@ -5532,11 +5623,13 @@ checksum = "7945a08031b7e14de57e8385cea3bcc7e10a88701595dc11d82551ba07bae13e" dependencies = [ "bytes", "document-features", + "dyn-clone", "ed25519-dalek", "flume", "futures", "js-sys", "lru", + "mainline", "self_cell 1.0.4", "simple-dns", "thiserror", @@ -5774,6 +5867,38 @@ dependencies = [ "toml_edit 0.22.22", ] +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "syn-mid", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.86" @@ -6168,20 +6293,20 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.13", - "rustls-pemfile 2.1.3", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.6", + "webpki-roots", "windows-registry", ] @@ -6364,25 +6489,17 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.4", + "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.3" @@ -6399,6 +6516,33 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +[[package]] +name = "rustls-platform-verifier" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "jni 0.19.0", + "log", + "once_cell", + "rustls 0.23.13", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.8", + "security-framework", + "security-framework-sys", + "webpki-roots", + "winapi", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -6508,6 +6652,7 @@ dependencies = [ "core-foundation 0.9.4", "core-foundation-sys", "libc", + "num-bigint", "security-framework-sys", ] @@ -6569,6 +6714,25 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.210" @@ -6638,6 +6802,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -6781,6 +6951,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" + [[package]] name = "smol_str" version = "0.2.2" @@ -7054,6 +7230,21 @@ dependencies = [ "siphasher", ] +[[package]] +name = "swarm-discovery" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39769914108ae68e261d85ceac7bce7095947130f79c29d4535e9b31fc702a40" +dependencies = [ + "acto", + "anyhow", + "hickory-proto 0.24.1", + "rand", + "socket2 0.5.7", + "tokio", + "tracing", +] + [[package]] name = "symphonia" version = "0.5.4" @@ -7148,24 +7339,23 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "1.0.1" +name = "syn-mid" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" dependencies = [ - "futures-core", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", + "futures-core", ] [[package]] @@ -7366,22 +7556,23 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.21.12", + "rustls 0.23.13", + "rustls-pki-types", "tokio", ] [[package]] -name = "tokio-rustls" -version = "0.26.0" +name = "tokio-stream" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ - "rustls 0.23.13", - "rustls-pki-types", + "futures-core", + "pin-project-lite", "tokio", ] @@ -7424,6 +7615,8 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", + "hashbrown 0.14.5", "pin-project-lite", "tokio", ] @@ -7792,7 +7985,7 @@ dependencies = [ "rustls 0.23.13", "rustls-pki-types", "url", - "webpki-roots 0.26.6", + "webpki-roots", ] [[package]] @@ -8040,7 +8233,7 @@ checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" dependencies = [ "core-foundation 0.9.4", "home", - "jni", + "jni 0.21.1", "log", "ndk-context", "objc", @@ -8049,12 +8242,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "webpki-roots" version = "0.26.6" @@ -8732,9 +8919,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "x509-parser" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ "asn1-rs", "data-encoding", diff --git a/assets/locales/en-US/player-select.ftl b/assets/locales/en-US/player-select.ftl index 88b2007366..91ae10cad8 100644 --- a/assets/locales/en-US/player-select.ftl +++ b/assets/locales/en-US/player-select.ftl @@ -2,12 +2,15 @@ keyboard = Keyboard you-marker = < You > no-hat = No Hat pick-a-fish = Pick a Fish +pick-a-hat = Pick a Hat player-select-ready = Ready! player-select-title = Player Select -player-select-unready = Press { $button } to unready +player-select-unready = Press { $button } to Unready press-button-to-join = Press { $button } to Join +press-button-to-confirm = Press { $button } to Confirm +press-button-to-go-back = Press { $button } to Go Back press-button-to-lock-in = Press { $button } to Lock In press-button-to-remove = Press { $button } to Remove diff --git a/deny.toml b/deny.toml index 62d4950b8a..3cf26e8fa8 100644 --- a/deny.toml +++ b/deny.toml @@ -37,3 +37,9 @@ allow-git = [ [sources.allow-org] github = ["fishfolk"] + +[advisories] +ignore = [ + # proc-macro-error is unmaintained - https://github.com/fishfolk/bones/issues/479 + "RUSTSEC-2024-0370", +] diff --git a/src/ui/main_menu/map_select.rs b/src/ui/main_menu/map_select.rs index 5daec06635..6e22b3340c 100644 --- a/src/ui/main_menu/map_select.rs +++ b/src/ui/main_menu/map_select.rs @@ -129,13 +129,15 @@ pub fn widget( let slot = player_select_state.slots[i]; PlayerInput { - active: slot.active, - selected_player: slot.selected_player, - selected_hat: slot.selected_hat, - control_source: slot.control_source, + active: !slot.is_empty(), + selected_player: slot + .selected_player() + .unwrap_or(player_select_state.players[0]), + selected_hat: slot.selected_hat(), + control_source: slot.user_control_source(), editor_input: default(), control: default(), - is_ai: slot.is_ai, + is_ai: slot.is_ai(), } }), plugins: meta.get_plugins(&assets), diff --git a/src/ui/main_menu/player_select.rs b/src/ui/main_menu/player_select.rs index 51fa611634..f4a3013496 100644 --- a/src/ui/main_menu/player_select.rs +++ b/src/ui/main_menu/player_select.rs @@ -6,9 +6,6 @@ use crate::{ui::player_image::player_image, PackMeta}; use super::*; -// const GAMEPAD_ACTION_IDX: usize = 0; -// const KEYPAD_ACTION_IDX: usize = 1; - #[derive(Default, Clone, Debug, HasSchema)] pub struct PlayerSelectState { pub slots: [PlayerSlot; MAX_PLAYERS as usize], @@ -18,34 +15,193 @@ pub struct PlayerSelectState { pub hats: Vec>>, } +impl PlayerSelectState { + pub fn any_slot_has_source(&self, source: ControlSource) -> bool { + self.slots + .iter() + .any(|slot| slot.user_control_source() == Some(source)) + } +} + #[derive(Default, Clone, Copy, Debug)] -pub struct PlayerSlot { - pub active: bool, - pub confirmed: bool, - pub selected_player: Handle, - pub selected_hat: Option>, - pub control_source: Option, - pub is_ai: bool, +pub enum PlayerSlot { + #[default] + Empty, + SelectingPlayer { + control_source: PlayerSlotControlSource, + current_player: Handle, + }, + SelectingHat { + control_source: PlayerSlotControlSource, + selected_player: Handle, + current_hat: Option>, + }, + Ready { + control_source: PlayerSlotControlSource, + selected_player: Handle, + selected_hat: Option>, + }, } impl PlayerSlot { + pub fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } + + pub fn is_selecting_player(&self) -> bool { + matches!(self, Self::SelectingPlayer { .. }) + } + + pub fn is_selecting_hat(&self) -> bool { + matches!(self, Self::SelectingHat { .. }) + } + + pub fn is_ready(&self) -> bool { + matches!(self, Self::Ready { .. }) + } + + pub fn is_user(&self) -> bool { + match self { + Self::Empty => false, + Self::SelectingPlayer { control_source, .. } + | Self::SelectingHat { control_source, .. } + | Self::Ready { control_source, .. } => control_source.is_user(), + } + } + pub fn is_ai(&self) -> bool { - self.is_ai + match self { + Self::Empty => false, + Self::SelectingPlayer { control_source, .. } + | Self::SelectingHat { control_source, .. } + | Self::Ready { control_source, .. } => control_source.is_ai(), + } } - #[cfg(debug_assertions)] - pub fn set_test_hat(&mut self, asset_server: &AssetServer, hat_handles: &[Handle]) { + pub fn control_source(&self) -> Option { + match self { + Self::Empty => None, + Self::SelectingPlayer { control_source, .. } + | Self::SelectingHat { control_source, .. } + | Self::Ready { control_source, .. } => Some(*control_source), + } + } + + pub fn user_control_source(&self) -> Option { + match self { + Self::Empty => None, + Self::SelectingPlayer { control_source, .. } + | Self::SelectingHat { control_source, .. } + | Self::Ready { control_source, .. } => control_source.user_source(), + } + } + + pub fn selected_player(&self) -> Option> { + match self { + Self::Empty => None, + Self::SelectingPlayer { current_player, .. } => Some(*current_player), + Self::SelectingHat { + selected_player, .. + } => Some(*selected_player), + Self::Ready { + selected_player, .. + } => Some(*selected_player), + } + } + + pub fn selected_hat(&self) -> Option> { + match self { + Self::Empty | Self::SelectingPlayer { .. } => None, + Self::SelectingHat { current_hat, .. } => *current_hat, + Self::Ready { selected_hat, .. } => *selected_hat, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum PlayerSlotControlSource { + User(ControlSource), + Remote, + Ai, +} + +impl PlayerSlotControlSource { + pub fn is_user(self) -> bool { + matches!(self, Self::User(_)) + } + + pub fn is_ai(self) -> bool { + matches!(self, Self::Ai) + } + + pub fn user_source(self) -> Option { + match self { + Self::User(src) => Some(src), + _ => None, + } + } +} + +#[cfg(debug_assertions)] +impl PlayerSlot { + pub fn test_player() -> Option { + match std::env::var("TEST_PLAYER") { + Ok(name) => Some(name), + Err(std::env::VarError::NotUnicode(err)) => { + warn!("Invalid TEST_PLAYER, not unicode: {err:?}"); + Some("Fishy".to_string()) + } + Err(std::env::VarError::NotPresent) => { + let test_vars = [ + std::env::var_os("TEST_MAP"), + std::env::var_os("TEST_HAT"), + std::env::var_os("TEST_CONTROLLER"), + ]; + if test_vars.iter().any(Option::is_some) { + Some("Fishy".to_string()) + } else { + None + } + } + } + } + + pub fn test_control_source() -> ControlSource { + match std::env::var("TEST_CONTROLLER") { + Ok(name) => match &*name { + "Keyboard1" => ControlSource::Keyboard1, + "Keyboard2" => ControlSource::Keyboard2, + "Gamepad" => ControlSource::Gamepad(0), + _ => { + warn!("Invalid TEST_CONTROLLER: {name}"); + warn!(r#"Available controllers: "Keyboard1", "Keyboard2", "Gamepad""#); + ControlSource::Keyboard1 + } + }, + Err(std::env::VarError::NotPresent) => ControlSource::Keyboard1, + Err(std::env::VarError::NotUnicode(err)) => { + warn!("Invalid TEST_CONTROLLER, not unicode: {err:?}"); + ControlSource::Keyboard1 + } + } + } + + pub fn test_hat( + asset_server: &AssetServer, + hat_handles: &[Handle], + ) -> Option> { match std::env::var("TEST_HAT") { - Err(std::env::VarError::NotPresent) => {} + Err(std::env::VarError::NotPresent) => None, Err(std::env::VarError::NotUnicode(err)) => { warn!("Invalid TEST_HAT, not unicode: {err:?}"); + None } Ok(test_hat) => match hat_handles .iter() .copied() .find(|h| asset_server.get(*h).name == test_hat) { - hat_handle @ Some(_) => self.selected_hat = hat_handle, + Some(hat_handle) => Some(hat_handle), None => { warn!("TEST_HAT not found: {test_hat}"); let available_names = @@ -53,6 +209,7 @@ impl PlayerSlot { asset_server.get(h).name.as_str() }); warn!("Available hat names: {available_names}"); + None } }, } @@ -92,50 +249,17 @@ pub fn widget( // Default the player to Jumpy if none is provided. #[cfg(debug_assertions)] 'test_player: { - use std::env::{var, var_os, VarError}; use std::sync::atomic::{AtomicBool, Ordering}; static DEBUG_DID_CHECK_ENV_VARS: AtomicBool = AtomicBool::new(false); if DEBUG_DID_CHECK_ENV_VARS .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_ok() { - let test_player = match var("TEST_PLAYER") { - Ok(name) => name, - Err(VarError::NotUnicode(err)) => { - warn!("Invalid TEST_PLAYER, not unicode: {err:?}"); - "Fishy".to_string() - } - Err(VarError::NotPresent) => { - let test_vars = [ - var_os("TEST_MAP"), - var_os("TEST_HAT"), - var_os("TEST_CONTROLLER"), - ]; - if test_vars.iter().any(Option::is_some) { - "Fishy".to_string() - } else { - break 'test_player; - } - } + let Some(test_player) = PlayerSlot::test_player() else { + break 'test_player; }; - let test_controller = match var("TEST_CONTROLLER") { - Ok(name) => match &*name { - "Keyboard1" => ControlSource::Keyboard1, - "Keyboard2" => ControlSource::Keyboard2, - "Gamepad" => ControlSource::Gamepad(0), - _ => { - warn!("Invalid TEST_CONTROLLER: {name}"); - warn!(r#"Available controllers: "Keyboard1", "Keyboard2", "Gamepad""#); - ControlSource::Keyboard1 - } - }, - Err(VarError::NotPresent) => ControlSource::Keyboard1, - Err(VarError::NotUnicode(err)) => { - warn!("Invalid TEST_CONTROLLER, not unicode: {err:?}"); - ControlSource::Keyboard1 - } - }; + let test_controller = PlayerSlot::test_control_source(); let asset_server = world.resource::(); let game_meta = asset_server.root::(); @@ -156,12 +280,11 @@ pub fn widget( warn!("Available player names: {available_names}"); } Some(player_handle) => { - let slot = &mut state.slots[0]; - slot.active = true; - slot.control_source = Some(test_controller); - slot.selected_player = player_handle; - slot.set_test_hat(&asset_server, core_hat_handles); - slot.confirmed = true; + state.slots[0] = PlayerSlot::Ready { + control_source: PlayerSlotControlSource::User(test_controller), + selected_player: player_handle, + selected_hat: PlayerSlot::test_hat(&asset_server, core_hat_handles), + }; ui.ctx() .set_state(MenuPage::MapSelect { is_waiting: false }); @@ -176,12 +299,12 @@ pub fn widget( let mut at_least_one_non_ai_ready = false; for slot in &state.slots { - if slot.confirmed { + if slot.is_ready() { ready_players += 1; - if !slot.is_ai { + if !slot.is_ai() { at_least_one_non_ai_ready = true; } - } else if slot.active { + } else if !slot.is_empty() { unconfirmed_players += 1; } } @@ -316,15 +439,57 @@ fn handle_match_setup_messages( match postcard::from_bytes::(&data) { Ok(message) => match message { PlayerSelectMessage::SelectPlayer(player_handle) => { - let player_handle = player_handle.into_handle(asset_server); - player_select_state.slots[player as usize].selected_player = player_handle; + let slot = player_select_state.slots[player as usize]; + let control_source = slot + .control_source() + .unwrap_or(PlayerSlotControlSource::Remote); + let selected_player = player_handle.into_handle(asset_server); + let current_hat = slot.selected_hat(); + player_select_state.slots[player as usize] = PlayerSlot::SelectingHat { + control_source, + selected_player, + current_hat, + }; } PlayerSelectMessage::ConfirmSelection(confirmed) => { - player_select_state.slots[player as usize].confirmed = confirmed; + let slot = player_select_state.slots[player as usize]; + let control_source = slot + .control_source() + .unwrap_or(PlayerSlotControlSource::Remote); + let selected_player = slot.selected_player().unwrap_or_else(|| { + warn!("got confirm message while in empty state, falling back to default player"); + player_select_state.players[0] + }); + let hat = slot.selected_hat(); + player_select_state.slots[player as usize] = if confirmed { + PlayerSlot::Ready { + control_source, + selected_player, + selected_hat: hat, + } + } else { + PlayerSlot::SelectingHat { + control_source, + selected_player, + current_hat: hat, + } + }; } - PlayerSelectMessage::SelectHat(hat) => { - let hat = hat.map(|hat| hat.into_handle(asset_server)); - player_select_state.slots[player as usize].selected_hat = hat; + PlayerSelectMessage::SelectHat(hat_handle) => { + let slot = player_select_state.slots[player as usize]; + let control_source = slot + .control_source() + .unwrap_or(PlayerSlotControlSource::Remote); + let selected_player = slot.selected_player().unwrap_or_else(|| { + warn!("got confirm message while in empty state, falling back to default player"); + player_select_state.players[0] + }); + let current_hat = hat_handle.map(|h| h.into_handle(asset_server)); + player_select_state.slots[player as usize] = PlayerSlot::SelectingHat { + control_source, + selected_player, + current_hat, + }; } }, Err(e) => warn!("Ignoring network message that was not understood: {e}"), @@ -343,6 +508,7 @@ fn player_select_panel( #[cfg(not(target_arch = "wasm32"))] network_socket: Option>, ) { let (ui, slot_id, state) = &mut *params; + let slot_id = *slot_id; // Cache the player list if state.players.is_empty() { @@ -371,203 +537,229 @@ fn player_select_panel( } } - #[cfg(target_arch = "wasm32")] - let network_local_player_slot: Option = None; - - #[cfg(target_arch = "wasm32")] - let is_network = false; #[cfg(not(target_arch = "wasm32"))] - let is_network = network_socket.is_some(); + let network_socket = network_socket.as_deref(); #[cfg(not(target_arch = "wasm32"))] - if let Some(socket) = network_socket.as_ref() { + if let Some(socket) = network_socket { // Don't show panels for non-connected players. - if *slot_id + 1 > socket.player_count() { + if slot_id + 1 > socket.player_count() { return; - } else { - state.slots[*slot_id as usize].active = true; } } + #[cfg(target_arch = "wasm32")] + let is_network = false; + #[cfg(not(target_arch = "wasm32"))] + let is_network = network_socket.is_some(); + let is_next_open_slot = state .slots .iter() .enumerate() - .any(|(i, slot)| (!slot.active && u32::try_from(i).unwrap() == *slot_id)); - - #[cfg(not(target_arch = "wasm32"))] - let mut network_local_player_slot: Option = None; + .any(|(i, slot)| (slot.is_empty() && i == slot_id as usize)); + #[cfg(target_arch = "wasm32")] + let (network_local_player_slot, slot_allows_new_player) = (None::, is_next_open_slot); #[cfg(not(target_arch = "wasm32"))] - let slot_allows_new_player = if is_network { - { - network_local_player_slot = Some(network_socket.as_ref().unwrap().player_idx()); - *slot_id == network_local_player_slot.unwrap() + let (network_local_player_slot, slot_allows_new_player) = match network_socket { + Some(socket) => { + let socket_id = socket.player_idx(); + (Some(socket_id), slot_id == socket_id) } - } else { - is_next_open_slot + None => (None, is_next_open_slot), }; - #[cfg(target_arch = "wasm32")] - let slot_allows_new_player = is_next_open_slot; - - // Check if a new player is trying to join - let new_player_join = controls.iter().find_map(|(source, control)| { - ( - // If this control input is pressing the join button - control.menu_confirm_just_pressed && - slot_allows_new_player && - // And this control source is not bound to a player slot already - !state - .slots - .iter() - .any(|s| s.control_source == Some(*source)) - ) - // Return this source - .then_some(*source) - }); - // Input sources that may be used to join a new player - let available_input_sources = { - let mut sources = SmallVec::<[_; 3]>::from_slice(&[ - ControlSource::Keyboard1, - ControlSource::Keyboard2, - ControlSource::Gamepad(0), - ]); + // + // React to user inputs + // - for slot in &state.slots { - if matches!( - slot.control_source, - Some(ControlSource::Keyboard1 | ControlSource::Keyboard2) - ) { - sources.retain(|&mut x| x != slot.control_source.unwrap()); - } + #[cfg(not(target_arch = "wasm32"))] + let net_send_player = |handle: Handle| { + if let Some(socket) = network_socket { + let network_handle = handle.network_handle(&asset_server); + let message = PlayerSelectMessage::SelectPlayer(network_handle); + socket.send_reliable(SocketTarget::All, &postcard::to_allocvec(&message).unwrap()); } - sources }; - let slot = &mut state.slots[*slot_id as usize]; - let player_handle = &mut slot.selected_player; - - // If the handle is empty - if *player_handle == default() { - // Select the first player - *player_handle = state.players[0]; - } - - // Handle player joining - if let Some(control_source) = new_player_join { - slot.active = true; - slot.confirmed = false; - slot.control_source = Some(control_source); - } - - let Some(player_control) = controls.get(&slot.control_source.unwrap_or_default()) else { - return; + #[cfg(not(target_arch = "wasm32"))] + let net_send_hat = |handle: Option>| { + if let Some(socket) = network_socket { + let network_handle = handle.map(|h| h.network_handle(&asset_server)); + let message = PlayerSelectMessage::SelectHat(network_handle); + socket.send_reliable(SocketTarget::All, &postcard::to_allocvec(&message).unwrap()); + } }; - if new_player_join.is_none() { - if player_control.menu_confirm_just_pressed { - slot.confirmed = true; - - #[cfg(not(target_arch = "wasm32"))] - if let Some(socket) = network_socket.as_ref() { - socket.send_reliable( - SocketTarget::All, - &postcard::to_allocvec(&PlayerSelectMessage::ConfirmSelection(slot.confirmed)) - .unwrap(), - ); - } - } else if player_control.menu_back_just_pressed { - if !is_network { - if slot.confirmed { - slot.confirmed = false; - } else if slot.active { - slot.active = false; - slot.control_source = None; - } - } else { - slot.confirmed = false; - } + #[cfg(not(target_arch = "wasm32"))] + let net_send_confirm = |confirm| { + if let Some(socket) = network_socket { + let message = PlayerSelectMessage::ConfirmSelection(confirm); + socket.send_reliable(SocketTarget::All, &postcard::to_allocvec(&message).unwrap()); + } + }; - #[cfg(not(target_arch = "wasm32"))] - if let Some(socket) = network_socket.as_ref() { - socket.send_reliable( - SocketTarget::All, - &postcard::to_allocvec(&PlayerSelectMessage::ConfirmSelection(slot.confirmed)) - .unwrap(), - ); + let mut next_state = None::; + + match state.slots[slot_id as usize] { + PlayerSlot::Empty => { + if slot_allows_new_player { + // Check if a new player is trying to join + let new_player_join = controls.iter().find_map(|(source, control)| { + ( + // If this control input is pressing the join button + control.menu_confirm_just_pressed && + // And this control source is not bound to a player slot already + !state.any_slot_has_source(*source) + ) + // Return this source + .then_some(*source) + }); + next_state = new_player_join.map(|control_source| PlayerSlot::SelectingPlayer { + control_source: PlayerSlotControlSource::User(control_source), + current_player: state.players[0], + }); } - } else if player_control.just_moved { - let direction = player_control.move_direction; + } - // Select a hat if the player has been confirmed - if slot.confirmed { - let current_hat_handle_idx = state - .hats + PlayerSlot::SelectingPlayer { + control_source: control_source @ PlayerSlotControlSource::User(src), + current_player, + } => { + let Some(player_control) = controls.get(&src) else { + return; + }; + if player_control.menu_confirm_just_pressed { + next_state = Some(PlayerSlot::SelectingHat { + control_source, + selected_player: current_player, + current_hat: None, + }); + } else if player_control.menu_back_just_pressed && !is_network { + next_state = Some(PlayerSlot::Empty); + } else if player_control.just_moved { + let current_player_handle_idx = state + .players .iter() .enumerate() - .find(|(_, handle)| **handle == slot.selected_hat) + .find(|(_, handle)| **handle == current_player) .map(|(i, _)| i) .unwrap_or(0); - - let next_idx = if direction.x > 0.0 { - (current_hat_handle_idx + 1) % state.hats.len() + let next_idx = if player_control.move_direction.x > 0.0 { + (current_player_handle_idx + 1) % state.players.len() } else { - let idx = current_hat_handle_idx as i32 - 1; - if idx == -1 { - state.hats.len() - 1 - } else { - idx as usize - } + (current_player_handle_idx + state.players.len() - 1) % state.players.len() }; - slot.selected_hat = state.hats[next_idx]; + let next_player = state.players[next_idx]; #[cfg(not(target_arch = "wasm32"))] - if let Some(socket) = network_socket.as_ref() { - socket.send_reliable( - SocketTarget::All, - &postcard::to_allocvec(&PlayerSelectMessage::SelectHat( - slot.selected_hat.map(|x| x.network_handle(&asset_server)), - )) - .unwrap(), - ); - } + net_send_player(next_player); - // Select player skin if the player has not be confirmed - } else { - let current_player_handle_idx = state - .players + next_state = Some(PlayerSlot::SelectingPlayer { + control_source, + current_player: next_player, + }); + } + } + + PlayerSlot::SelectingHat { + control_source: control_source @ PlayerSlotControlSource::User(src), + selected_player, + current_hat, + } => { + let Some(player_control) = controls.get(&src) else { + return; + }; + if player_control.menu_confirm_just_pressed { + #[cfg(not(target_arch = "wasm32"))] + net_send_confirm(true); + next_state = Some(PlayerSlot::Ready { + control_source, + selected_player, + selected_hat: current_hat, + }); + } else if player_control.menu_back_just_pressed { + next_state = Some(PlayerSlot::SelectingPlayer { + control_source, + current_player: selected_player, + }); + } else if player_control.just_moved { + let current_hat_handle_idx = state + .hats .iter() .enumerate() - .find(|(_, handle)| *handle == player_handle) + .find(|(_, handle)| **handle == current_hat) .map(|(i, _)| i) .unwrap_or(0); - let next_idx = if direction.x > 0.0 { - (current_player_handle_idx + 1) % state.players.len() + + let next_idx = if player_control.move_direction.x > 0.0 { + (current_hat_handle_idx + 1) % state.hats.len() } else { - let idx = current_player_handle_idx as i32 - 1; - if idx == -1 { - state.players.len() - 1 - } else { - idx as usize - } + (current_hat_handle_idx + state.hats.len() - 1) % state.hats.len() }; - *player_handle = state.players[next_idx]; + let next_hat = state.hats[next_idx]; #[cfg(not(target_arch = "wasm32"))] - if let Some(socket) = network_socket.as_ref() { - socket.send_reliable( - SocketTarget::All, - &postcard::to_allocvec(&PlayerSelectMessage::SelectPlayer( - player_handle.network_handle(&asset_server), - )) - .unwrap(), - ); - } + net_send_hat(next_hat); + + next_state = Some(PlayerSlot::SelectingHat { + control_source, + selected_player, + current_hat: next_hat, + }); } } + + PlayerSlot::Ready { + control_source: control_source @ PlayerSlotControlSource::User(src), + selected_player, + selected_hat, + } => { + let Some(player_control) = controls.get(&src) else { + return; + }; + if player_control.menu_back_just_pressed { + #[cfg(not(target_arch = "wasm32"))] + net_send_confirm(false); + next_state = Some(PlayerSlot::SelectingHat { + control_source, + selected_player, + current_hat: selected_hat, + }); + } + } + + _ => {} + } + + if let Some(slot) = next_state.take() { + state.slots[slot_id as usize] = slot; } + // + // Render panel + // + + // Input sources that may be used to join a new player + let available_input_sources = { + let mut sources = SmallVec::<[_; 3]>::from_slice(&[ + ControlSource::Keyboard1, + ControlSource::Keyboard2, + ControlSource::Gamepad(0), + ]); + + for slot in &state.slots { + if matches!( + slot.user_control_source(), + Some(ControlSource::Keyboard1 | ControlSource::Keyboard2) + ) { + sources.retain(|&mut x| Some(x) != slot.user_control_source()); + } + } + sources + }; + let panel = &meta.theme.panel; BorderedFrame::new(&panel.border) .padding(panel.padding) @@ -581,22 +773,21 @@ fn player_select_panel( // Marker for current player in online matches #[cfg(not(target_arch = "wasm32"))] - if let Some(socket) = network_socket { - if socket.player_idx() == *slot_id { + match network_socket { + Some(socket) if socket.player_idx() == slot_id => { ui.vertical_centered(|ui| { ui.label(normal_font.rich(localization.get("you-marker"))); }); - } else { - ui.add_space(normal_font.size); } - } else { - ui.add_space(normal_font.size); + _ => ui.add_space(normal_font.size), } ui.add_space(normal_font.size); - if slot.active { - let confirm_binding = match slot.control_source { + let slot = state.slots[slot_id as usize]; + + if let Some(selected_player) = slot.selected_player() { + let confirm_binding = match slot.user_control_source() { Some(source) => mapping.map_control_source(source).menu_confirm.to_string(), None => available_input_sources .iter() @@ -605,7 +796,7 @@ fn player_select_panel( .join("/"), }; - let back_binding = match slot.control_source { + let back_binding = match slot.user_control_source() { Some(source) => mapping.map_control_source(source).menu_back.to_string(), None => available_input_sources .iter() @@ -614,20 +805,26 @@ fn player_select_panel( .join("/"), }; ui.vertical_centered(|ui| { - let player_meta = asset_server.get(slot.selected_player); - let hat_meta = slot - .selected_hat - .as_ref() - .map(|handle| asset_server.get(*handle)); - - if !slot.confirmed { - if slot.control_source.is_some() { - ui.label(normal_font.rich(localization.get("pick-a-fish"))); + let player_meta = asset_server.get(selected_player); + let hat_meta = slot.selected_hat().map(|h| asset_server.get(h)); + + if !slot.is_ready() { + if slot.is_user() { + if slot.is_selecting_hat() { + ui.label(normal_font.rich(localization.get("pick-a-hat"))); + } else { + ui.label(normal_font.rich(localization.get("pick-a-fish"))); + } } - if network_local_player_slot.is_some_and(|s| s == *slot_id) || !is_network { + if network_local_player_slot.is_some_and(|s| s == slot_id) || !is_network { + let label_id = if slot.is_selecting_hat() { + "press-button-to-lock-in" + } else { + "press-button-to-confirm" + }; ui.label(normal_font.rich(localization.get_with( - "press-button-to-lock-in", + label_id, &fluent_args! { "button" => confirm_binding.as_str() }, @@ -635,8 +832,13 @@ fn player_select_panel( } if !is_network { + let label_id = if slot.is_selecting_hat() { + "press-button-to-go-back" + } else { + "press-button-to-remove" + }; ui.label(normal_font.rich(localization.get_with( - "press-button-to-remove", + label_id, &fluent_args! { "button" => back_binding.as_str() }, @@ -649,7 +851,7 @@ fn player_select_panel( ui.vertical_centered(|ui| { ui.set_height(heading_font.size * 1.5); - if slot.confirmed && !slot.is_ai() && slot.control_source.is_some() { + if slot.is_ready() && !slot.is_ai() && slot.is_user() { ui.label( heading_font .with_color(meta.theme.colors.positive) @@ -663,7 +865,7 @@ fn player_select_panel( }, ))); } - if !is_network && *slot_id != 0 && slot.is_ai() { + if !is_network && slot_id != 0 && slot.is_ai() { ui.label( heading_font .with_color(meta.theme.colors.positive) @@ -677,27 +879,30 @@ fn player_select_panel( .show(ui) .clicked() { - slot.confirmed = false; - slot.active = false; - slot.control_source = None; - slot.is_ai = false; + next_state = Some(PlayerSlot::Empty); } } }); ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - let name_with_arrows = format!("< {} >", player_meta.name); - ui.label(normal_font.rich(if slot.confirmed { - player_meta.name.to_string() - } else { - name_with_arrows - })); - let hat_label = if let Some(hat_meta) = &hat_meta { - format!("< {} >", hat_meta.name) + ui.label(if slot.is_selecting_player() { + normal_font.rich(format!("< {} >", player_meta.name)) } else { - format!("< {} >", localization.get("no-hat")) + normal_font.rich(player_meta.name.as_str()) + }); + + let hat_label = match slot { + PlayerSlot::Empty | PlayerSlot::SelectingPlayer { .. } => String::new(), + PlayerSlot::SelectingHat { .. } => match hat_meta.as_ref() { + Some(hat) => format!("< {} >", hat.name), + None => format!("< {} >", localization.get("no-hat")), + }, + PlayerSlot::Ready { .. } => match hat_meta.as_ref() { + Some(hat) => hat.name.to_string(), + None => localization.get("no-hat").to_string(), + }, }; - ui.label(smaller_font.rich(if slot.confirmed { &hat_label } else { "" })); + ui.label(smaller_font.rich(hat_label)); world.run_system(player_image, (ui, &player_meta, hat_meta.as_deref())); }); @@ -721,7 +926,7 @@ fn player_select_panel( if !is_network { ui.add_space(meta.theme.font_styles.bigger.size); - if *slot_id != 0 + if slot_id != 0 && BorderedButton::themed( &meta.theme.buttons.normal, localization.get("add-ai-player"), @@ -729,14 +934,20 @@ fn player_select_panel( .show(ui) .clicked() { - slot.is_ai = true; - slot.confirmed = true; - slot.active = true; - let rand_idx = THREAD_RNG.with(|rng| rng.usize(0..state.players.len())); - slot.selected_player = state.players[rand_idx]; + let player_idx = + THREAD_RNG.with(|rng| rng.usize(0..state.players.len())); + next_state = Some(PlayerSlot::Ready { + control_source: PlayerSlotControlSource::Ai, + selected_player: state.players[player_idx], + selected_hat: None, + }); } } }); } }); + + if let Some(slot) = next_state { + state.slots[slot_id as usize] = slot; + } } diff --git a/src/ui/main_menu/settings/controls.rs b/src/ui/main_menu/settings/controls.rs index 2850880f74..b819021a20 100644 --- a/src/ui/main_menu/settings/controls.rs +++ b/src/ui/main_menu/settings/controls.rs @@ -356,13 +356,13 @@ fn get_input( .next() .and_then(|event| match event { GamepadEvent::Button(e) => { - (e.value.abs() > 0.1).then(|| InputKind::Button(e.button.clone())) + (e.value.abs() > 0.1).then_some(InputKind::Button(e.button)) } GamepadEvent::Axis(e) => { if e.value > 0.1 { - Some(InputKind::AxisPositive(e.axis.clone())) + Some(InputKind::AxisPositive(e.axis)) } else if e.value < -0.1 { - Some(InputKind::AxisNegative(e.axis.clone())) + Some(InputKind::AxisNegative(e.axis)) } else { None }