diff --git a/.gitignore b/.gitignore index ecd5ba4..294525c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ target output assets_packer/input +# Local config files +client_logging_config.toml +server_logging_config.toml + # RenderDoc *.cap diff --git a/Cargo.lock b/Cargo.lock index c51234d..55a5d66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,9 +8,9 @@ checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" [[package]] name = "ahash" -version = "0.2.18" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3" +checksum = "0989268a37e128d4d7a8028f1c60099430113fdbc70419010601ce51a228e4fe" dependencies = [ "const-random", ] @@ -62,7 +62,7 @@ dependencies = [ [[package]] name = "amethyst" version = "0.14.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_animation", "amethyst_assets", @@ -80,9 +80,9 @@ dependencies = [ "amethyst_ui", "amethyst_utils", "amethyst_window", - "crossbeam-channel", - "derivative", - "dirs 2.0.2", + "crossbeam-channel 0.4.2", + "derivative 1.0.3", + "dirs", "failure", "fern", "glsl-layout", @@ -100,7 +100,7 @@ dependencies = [ [[package]] name = "amethyst_animation" version = "0.9.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_core", @@ -108,7 +108,7 @@ dependencies = [ "amethyst_error", "amethyst_rendy", "amethyst_ui", - "derivative", + "derivative 1.0.3", "fnv", "itertools", "log", @@ -120,13 +120,13 @@ dependencies = [ [[package]] name = "amethyst_assets" version = "0.10.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_core", "amethyst_derive", "amethyst_error", "crossbeam-queue 0.1.2", - "derivative", + "derivative 1.0.3", "derive-new", "erased-serde", "err-derive", @@ -135,7 +135,7 @@ dependencies = [ "lazy_static", "log", "objekt", - "parking_lot", + "parking_lot 0.10.0", "rayon", "ron", "serde", @@ -145,7 +145,7 @@ dependencies = [ [[package]] name = "amethyst_audio" version = "0.9.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_core", @@ -162,7 +162,7 @@ dependencies = [ [[package]] name = "amethyst_config" version = "0.13.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "log", "ron", @@ -173,7 +173,7 @@ dependencies = [ [[package]] name = "amethyst_controls" version = "0.8.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_core", @@ -190,13 +190,13 @@ dependencies = [ [[package]] name = "amethyst_core" version = "0.9.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "alga", "alga_derive", "amethyst_error", "approx 0.3.2", - "derivative", + "derivative 1.0.3", "derive-new", "fnv", "getset", @@ -213,20 +213,20 @@ dependencies = [ [[package]] name = "amethyst_derive" version = "0.7.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "heck", "proc-macro-crate", - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "proc_macro_roids", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] name = "amethyst_error" version = "0.4.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "backtrace", ] @@ -234,17 +234,17 @@ dependencies = [ [[package]] name = "amethyst_input" version = "0.10.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_config", "amethyst_core", "amethyst_error", "amethyst_window", - "derivative", + "derivative 1.0.3", "derive-new", "fnv", "serde", - "smallvec 0.6.13", + "smallvec 1.2.0", "thread_profiler", "winit", ] @@ -252,7 +252,7 @@ dependencies = [ [[package]] name = "amethyst_locale" version = "0.8.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_core", @@ -266,7 +266,7 @@ dependencies = [ [[package]] name = "amethyst_network" version = "0.7.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_core", "amethyst_error", @@ -278,7 +278,7 @@ dependencies = [ [[package]] name = "amethyst_rendy" version = "0.4.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_config", @@ -287,7 +287,7 @@ dependencies = [ "amethyst_error", "amethyst_window", "approx 0.3.2", - "derivative", + "derivative 1.0.3", "derive-new", "failure", "fnv", @@ -299,7 +299,7 @@ dependencies = [ "rendy", "ron", "serde", - "smallvec 0.6.13", + "smallvec 1.2.0", "static_assertions 1.1.0", "thread_profiler", ] @@ -307,7 +307,7 @@ dependencies = [ [[package]] name = "amethyst_tiles" version = "0.2.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_core", @@ -315,7 +315,7 @@ dependencies = [ "amethyst_rendy", "amethyst_window", "bitintr", - "derivative", + "derivative 1.0.3", "err-derive", "failure", "fnv", @@ -325,14 +325,14 @@ dependencies = [ "log", "rayon", "serde", - "smallvec 0.6.13", + "smallvec 1.2.0", "thread_profiler", ] [[package]] name = "amethyst_ui" version = "0.9.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_audio", @@ -343,7 +343,7 @@ dependencies = [ "amethyst_rendy", "amethyst_window", "clipboard", - "derivative", + "derivative 1.0.3", "derive-new", "failure", "fnv", @@ -356,7 +356,7 @@ dependencies = [ "rand 0.7.3", "ron", "serde", - "smallvec 0.6.13", + "smallvec 1.2.0", "thread_profiler", "unicode-normalization", "unicode-segmentation", @@ -366,7 +366,7 @@ dependencies = [ [[package]] name = "amethyst_utils" version = "0.9.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_assets", "amethyst_controls", @@ -386,7 +386,7 @@ dependencies = [ [[package]] name = "amethyst_window" version = "0.4.0" -source = "git+https://github.com/amethyst/amethyst#cf7906e8aed4b5f9561482d6556bbab694adc8f7" +source = "git+https://github.com/mvlabat/amethyst.git?rev=49dc5b6#49dc5b6a43fe75a235b0f777e3fcd45a0158848e" dependencies = [ "amethyst_config", "amethyst_core", @@ -572,7 +572,7 @@ dependencies = [ "clang-sys", "lazy_static", "peeking_take_while", - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", "regex", "rustc-hash", @@ -622,13 +622,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "bytes" -version = "0.4.12" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] +checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" [[package]] name = "c2-chacha" @@ -651,7 +647,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fce5b5fb86b0c57c20c834c1b412fd09c77c8a59b9473f86272709e78874cd1d" dependencies = [ - "nom", + "nom 4.2.3", ] [[package]] @@ -763,8 +759,8 @@ checksum = "1706996401131526e36b3b49f0c4d912639ce110996f3ca144d78946727bce54" dependencies = [ "bitflags", "block", - "core-foundation", - "core-graphics", + "core-foundation 0.6.4", + "core-graphics 0.17.3", "foreign-types", "libc", "objc", @@ -778,9 +774,9 @@ checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" [[package]] name = "colored" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8815e2ab78f3a59928fc32e141fbeece88320a240e43f47b2fd64ea3a88a5b3d" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" dependencies = [ "atty", "lazy_static", @@ -830,11 +826,12 @@ dependencies = [ ] [[package]] -name = "core-foundation-sys" -version = "0.5.1" +name = "core-foundation" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716c271e8613ace48344f723b60b900a93150271e5be206212d052bbc0883efa" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" dependencies = [ + "core-foundation-sys 0.7.0", "libc", ] @@ -844,6 +841,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-graphics" version = "0.17.3" @@ -851,19 +854,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56790968ab1c8a1202a102e6de05fc6e1ec87da99e4e93e9a7d13efbfc1e95a9" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.6.4", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e78b2e0aaf43f08e7ae0d6bc96895ef72ff0921c7d4ff4762201b2dba376dd" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", "foreign-types", "libc", ] [[package]] name = "core-text" -version = "13.3.2" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db84654ad95211c082cf9795f6f83dc17d0ae6c985ac1b906369dc7384ed346d" +checksum = "131b3fd1f8bd5db9f2b398fa4fdb6008c64afc04d447c306ac2c7e98fba2a61d" dependencies = [ - "core-foundation", - "core-graphics", + "core-foundation 0.7.0", + "core-graphics 0.19.0", "foreign-types", "libc", ] @@ -889,15 +904,17 @@ dependencies = [ [[package]] name = "cpal" -version = "0.8.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58ae1ed6536b1b233f5e3aeb6997a046ddb4d05e3f61701b58a92eb254a829e" +checksum = "3ded070249be850b5b59e1e3a44a70b8ae395e0e5c65b487131d8909a8208120" dependencies = [ "alsa-sys", - "core-foundation-sys 0.5.1", + "core-foundation-sys 0.6.2", "coreaudio-rs", + "failure", "lazy_static", "libc", + "num-traits 0.2.11", "stdweb", "winapi", ] @@ -929,26 +946,38 @@ dependencies = [ "crossbeam-utils 0.6.6", ] +[[package]] +name = "crossbeam-channel" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + [[package]] name = "crossbeam-deque" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" dependencies = [ "crossbeam-epoch", - "crossbeam-utils 0.7.0", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] name = "crossbeam-epoch" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ - "autocfg 0.1.7", + "autocfg 1.0.0", "cfg-if", - "crossbeam-utils 0.7.0", + "crossbeam-utils 0.7.2", "lazy_static", + "maybe-uninit", "memoffset", "scopeguard", ] @@ -969,7 +998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" dependencies = [ "cfg-if", - "crossbeam-utils 0.7.0", + "crossbeam-utils 0.7.2", ] [[package]] @@ -984,23 +1013,23 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 0.1.7", + "autocfg 1.0.0", "cfg-if", "lazy_static", ] [[package]] name = "ctor" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc" +checksum = "47c5e5ac752e18207b12e16b10631ae5f7f68f8805f335f9b817ead83d9ffce1" dependencies = [ "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -1024,15 +1053,26 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "derivative" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f" +dependencies = [ + "proc-macro2 1.0.9", + "quote 1.0.2", + "syn 1.0.16", +] + [[package]] name = "derive-new" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71f31892cd5c62e414316f2963c5689242c43d8e7bbcaaeca97e5e28c95d91d9" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -1045,17 +1085,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dirs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dirs" version = "2.0.2" @@ -1137,15 +1166,15 @@ dependencies = [ [[package]] name = "err-derive" -version = "0.1.6" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41487fadaa500d02a819eefcde5f713599a01dd51626ef25d2d72d87115667b" +checksum = "14ce82f12540e448ece75759f36d341dcca4148bb76fb393b087c24e00b15d07" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "rustc_version", - "syn 1.0.14", + "rustversion", + "syn 1.0.16", "synstructure", ] @@ -1184,9 +1213,9 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", "synstructure", ] @@ -1209,9 +1238,9 @@ checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" [[package]] name = "fluent" -version = "0.8.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d560a76b2b0384dfa064a2f99946e5fc83af2165b975a63e8ebe03dfabf89f0" +checksum = "e7720e3feab35a8aacf3927bf0aea2fe9bd59bde9cd396faf5c01ddaa12c258b" dependencies = [ "fluent-bundle", "unic-langid", @@ -1219,34 +1248,33 @@ dependencies = [ [[package]] name = "fluent-bundle" -version = "0.8.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c8a0179a4ab2150b3357b4ee4cb21006d1ad99f5b5563225756b193d14fdc" +checksum = "67fc7bdf8a047142c86627fed5789fda5ffad8ce549e4fc8b8b3bcd08710a1e7" dependencies = [ - "failure", - "failure_derive", - "fluent-locale", + "fluent-langneg", "fluent-syntax", + "intl-memoizer", "intl_pluralrules", "rental", - "smallvec 0.6.13", + "smallvec 1.2.0", "unic-langid", ] [[package]] -name = "fluent-locale" -version = "0.10.1" +name = "fluent-langneg" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f626739113990f6ee64eff9b9e92621688dfd8a5d1b6eab94741bb5eddbc96" +checksum = "fe5815efd5542e40841cd34ef9003822352b04c67a70c595c6758597c72e1f56" dependencies = [ "unic-langid", ] [[package]] name = "fluent-syntax" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be7427364d95bc7b59f3b7cd0b1a74bb70c2ee0ae667b63b853d359470dc85c" +checksum = "9fe937dbd784d0f085f05c15a06f0d5dd06ce31cc823f7ab12ebb3758d948b39" [[package]] name = "fnv" @@ -1256,15 +1284,15 @@ checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" [[package]] name = "font-kit" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b7ff8d2a0a660875d01689807925a45c5843bf90a1ef97ec52ef86ab0cafba" +checksum = "09b6e2b877097ffd0abac6597fea26fccb5ed7eb9da0a4094f11ccc8aba64efb" dependencies = [ "byteorder", - "core-foundation", - "core-graphics", + "core-foundation 0.7.0", + "core-graphics 0.19.0", "core-text", - "dirs 1.0.5", + "dirs", "dwrote", "euclid", "float-ord", @@ -1273,7 +1301,6 @@ dependencies = [ "libc", "log", "lyon_path", - "memmap", "servo-fontconfig", "walkdir", "winapi", @@ -1360,13 +1387,13 @@ dependencies = [ [[package]] name = "getset" -version = "0.0.8" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117a5b13aecd4e10161bb3feb22dda898e8552836c2391d8e4645d5e703ab866" +checksum = "5bb3f5b7d8d70c9bd23cf29b2b38094661418fb0ea79f1b0cc2019a11d6f5429" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -1388,8 +1415,8 @@ checksum = "5facf0b6e4306a6a93682077f85602c89b49aca8ecceb73b9558e7baa37f8c54" dependencies = [ "ash", "byteorder", - "core-graphics", - "derivative", + "core-graphics 0.17.3", + "derivative 1.0.3", "gfx-hal", "lazy_static", "log", @@ -1419,9 +1446,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a36606a68532b5640dc86bb1f33c64b45c4682aad4c50f3937b317ea387f3d6" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -1455,16 +1482,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def9469f08de9930cae4c4e7d88b059cce0765a0ffdf6108ecc96568e801516d" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] name = "glyph_brush" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ec4a0787fffc6692ec1ccef9fe09a8e02df1b7fc8fadd5af96662dc979473f9" +checksum = "5fca6f9d679bff1322c76c9a1ad4b8553b30a94f3f75bea6936e19032c2f2ec3" dependencies = [ "glyph_brush_layout", "log", @@ -1476,10 +1503,11 @@ dependencies = [ [[package]] name = "glyph_brush_layout" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b6d460a62dc8a2d3402689b02824ca03d40ec6f1673b9d2016d1f30ce5cb62" +checksum = "8b70adc570f1dc71b6b32e241cbcc2b42175f5aea71951fbf41e68b04aec24c7" dependencies = [ + "approx 0.3.2", "rusttype 0.8.2", "xi-unicode", ] @@ -1500,7 +1528,7 @@ dependencies = [ "amethyst", "bincode", "clap", - "derivative", + "derivative 2.1.1", "failure", "glsl-layout", "gv_animation_prefabs", @@ -1511,6 +1539,7 @@ dependencies = [ "log", "num", "rand 0.6.5", + "toml", ] [[package]] @@ -1534,6 +1563,7 @@ version = "0.1.0" dependencies = [ "amethyst", "bincode", + "derivative 2.1.1", "log", "nalgebra 0.18.1", "num", @@ -1574,16 +1604,17 @@ dependencies = [ "gv_game", "lazy_static", "log", + "toml", ] [[package]] name = "hashbrown" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead" +checksum = "728e7d31e63d53c436094370f1e6fa249f60a4bb318cc5dfbbbe0aa2bc5a29d7" dependencies = [ "ahash", - "autocfg 0.1.7", + "autocfg 1.0.0", ] [[package]] @@ -1597,18 +1628,18 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" +checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" dependencies = [ "libc", ] [[package]] name = "hibitset" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e7292fd9f7fe89fa35c98048f2d0a69b79ed243604234d18f6f8a1aa6f408d" +checksum = "93a1bb8316a44459a7d14253c4d28dd7395cbd23cc04a68c46e851b8e46d64b1" dependencies = [ "atom", "rayon", @@ -1664,14 +1695,23 @@ dependencies = [ "adler32", ] +[[package]] +name = "intl-memoizer" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff533e7939a956a8b4f328b498b3ac59ee4259f9aaede10a58c28cbeb59e1c0d" +dependencies = [ + "type-map", + "unic-langid", +] + [[package]] name = "intl_pluralrules" -version = "4.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914dfd30afec12b332108e91a892988be4a91ce907b29c59c01348b73f06edce" +checksum = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002" dependencies = [ - "matches", - "phf", + "tinystr", "unic-langid", ] @@ -1692,18 +1732,9 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a8e30575afe28eea36a9a39136b70b2fb6b0dd0a212a5bd1f30a498395c0274" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", -] - -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", + "syn 1.0.16", ] [[package]] @@ -1739,7 +1770,7 @@ checksum = "de96f75f071a80952498ac17613843a2f529188ac053af7d358403aac4e34551" dependencies = [ "byteorder", "crc", - "crossbeam-channel", + "crossbeam-channel 0.3.9", "lazy_static", "log", "rand 0.7.3", @@ -1770,7 +1801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e0d09e60c187a6d0a3fa418aec8587c6a4ae9de872f6126f2134f319b5ed10d" dependencies = [ "cfg-if", - "lexical-core", + "lexical-core 0.4.6", "rustc_version", ] @@ -1787,11 +1818,24 @@ dependencies = [ "static_assertions 0.3.4", ] +[[package]] +name = "lexical-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890" +dependencies = [ + "arrayvec 0.4.12", + "cfg-if", + "rustc_version", + "ryu", + "static_assertions 0.3.4", +] + [[package]] name = "libc" -version = "0.2.66" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" [[package]] name = "libloading" @@ -1871,9 +1915,9 @@ checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" [[package]] name = "mach" -version = "0.2.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" dependencies = [ "libc", ] @@ -1887,12 +1931,6 @@ dependencies = [ "libc", ] -[[package]] -name = "matches" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" - [[package]] name = "matrixmultiply" version = "0.2.3" @@ -1910,9 +1948,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.3.0" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memmap" @@ -1935,9 +1973,9 @@ dependencies = [ [[package]] name = "minimp3" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542e9bed56860c5070a09939eee0e2df6f8f73f60304ddf56d620947e7017239" +checksum = "dce0cff6a0bfd3f8b6b2350819bbddd63bc65cc45e53888bdd0ff49dde16d2d5" dependencies = [ "minimp3-sys", "slice-deque", @@ -1945,9 +1983,9 @@ dependencies = [ [[package]] name = "minimp3-sys" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c109ae05c00ad6e3a53fab101e2f234545bdd010f0fffd399355efaf70817817" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" dependencies = [ "cc", ] @@ -2041,7 +2079,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" dependencies = [ "memchr", - "version_check", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +dependencies = [ + "lexical-core 0.6.2", + "memchr", + "version_check 0.9.1", ] [[package]] @@ -2237,10 +2286,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.6.2", "rustc_version", ] +[[package]] +name = "parking_lot" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc" +dependencies = [ + "lock_api", + "parking_lot_core 0.7.0", +] + [[package]] name = "parking_lot_core" version = "0.6.2" @@ -2256,11 +2315,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec 1.2.0", + "winapi", +] + [[package]] name = "paste" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "423a519e1c6e828f1e73b720f9d9ed2fa643dce8a7737fb43235ce0b41eeaa49" +checksum = "63e1afe738d71b1ebab5f1207c055054015427dbfc7bbe9ee1266894156ec046" dependencies = [ "paste-impl", "proc-macro-hack", @@ -2268,14 +2341,14 @@ dependencies = [ [[package]] name = "paste-impl" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4214c9e912ef61bf42b81ba9a47e8aad1b2ffaf739ab162bf96d1e011f54e6c5" +checksum = "6d4dc4a7f6f743211c5aab239640a65091535d97d43d92a52bca435a640892bb" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -2375,13 +2448,28 @@ dependencies = [ [[package]] name = "proc-macro-error" -version = "0.2.6" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052b3c9af39c7e5e94245f820530487d19eb285faedcb40e0c3275132293f242" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.9", + "quote 1.0.2", + "rustversion", + "syn 1.0.16", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097" +checksum = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "rustversion", + "syn 1.0.16", + "syn-mid", ] [[package]] @@ -2390,9 +2478,9 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -2406,22 +2494,22 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" dependencies = [ "unicode-xid 0.2.0", ] [[package]] name = "proc_macro_roids" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8024b1cbfbb39df5aee6c2d55252d7c943af7c06002a37ec5d2c74daf97335f" +checksum = "06675fa2c577f52bcf77fbb511123927547d154faa08097cc012c66ec3c9611a" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -2451,7 +2539,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", ] [[package]] @@ -2675,7 +2763,7 @@ checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" dependencies = [ "crossbeam-deque", "crossbeam-queue 0.2.1", - "crossbeam-utils 0.7.0", + "crossbeam-utils 0.7.2", "lazy_static", "num_cpus", ] @@ -2775,7 +2863,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94a32282d82bd3ef04c15edf3d6762a8bea38a6575490344188a9bbb110c6c6a" dependencies = [ - "derivative", + "derivative 1.0.3", "failure", "gfx-hal", "relevant", @@ -2790,7 +2878,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca7bcc3cb86a7945ecc5f0d7121e47a0b5979c3c57d3a5e6facc8738338651d8" dependencies = [ - "derivative", + "derivative 1.0.3", "failure", "gfx-hal", "log", @@ -2804,12 +2892,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a733600fa7aa962919999ffe20bbf3f9c1c36230fedd9abf9e78caffe9db7093" dependencies = [ - "derivative", + "derivative 1.0.3", "either", "failure", "gfx-hal", "log", - "parking_lot", + "parking_lot 0.9.0", "relevant", "rendy-command", "rendy-descriptor", @@ -2828,7 +2916,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417485444f959d67f3b1ec5ad7de340e389548ab404df0bb809bc821128660d3" dependencies = [ - "derivative", + "derivative 1.0.3", "either", "failure", "gfx-hal", @@ -2850,7 +2938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b34a289735fd2a15482aaf9fb5ba824678768dd985e7d17545a7354836d5ebd4" dependencies = [ "bitflags", - "derivative", + "derivative 1.0.3", "either", "failure", "gfx-hal", @@ -2877,7 +2965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cf6b3fc8a012b69062419caf086d35f83d9af57bf30a6971691731b4816a47f" dependencies = [ "colorful", - "derivative", + "derivative 1.0.3", "failure", "gfx-hal", "hibitset", @@ -2914,8 +3002,8 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3c94df60f828ae6e0c9f50bf0faeaf0125ef728ec66029c25d59c91fd6d41ee" dependencies = [ - "crossbeam-channel", - "derivative", + "crossbeam-channel 0.3.9", + "derivative 1.0.3", "failure", "gfx-hal", "log", @@ -2932,7 +3020,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dd3a577f3b58cd2ea98f4480d32493d80f8c3aaf1852388efec58c33d3ac222" dependencies = [ - "derivative", + "derivative 1.0.3", "failure", "gfx-hal", "log", @@ -2949,7 +3037,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911dbc17c26ec93c8cfecf6a82e5e288d503183e46ac57c7395826aea2bf259f" dependencies = [ - "derivative", + "derivative 1.0.3", "failure", "gfx-hal", "image 0.22.5", @@ -2969,13 +3057,13 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa01882dc9f3f64684393724d0d49d2315b612eeeb029eded2181385d796c75f" dependencies = [ - "derivative", + "derivative 1.0.3", "gfx-backend-empty", "gfx-backend-vulkan", "gfx-hal", "lazy_static", "log", - "parking_lot", + "parking_lot 0.9.0", "serde", "thread_profiler", ] @@ -2986,7 +3074,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9954dda560d8d5fbf619a46b0619aa6e0f990bd439ab974641bdbe99a5f9c93f" dependencies = [ - "derivative", + "derivative 1.0.3", "failure", "gfx-hal", "log", @@ -3014,16 +3102,16 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] name = "rodio" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0f961b254e66d147a7b550c78b01308934c97d807a34b417fd0f5a0a0f3a2d" +checksum = "1e0e0dfa7c8b17c6428f6e992a22ea595922cc86f946191b6b59e7ce96b77262" dependencies = [ "claxon", "cpal", @@ -3031,7 +3119,6 @@ dependencies = [ "lazy_static", "lewton", "minimp3", - "nalgebra 0.18.1", ] [[package]] @@ -3054,7 +3141,7 @@ dependencies = [ "base64 0.11.0", "blake2b_simd", "constant_time_eq", - "crossbeam-utils 0.7.0", + "crossbeam-utils 0.7.2", ] [[package]] @@ -3106,7 +3193,7 @@ dependencies = [ "approx 0.3.2", "arrayvec 0.5.1", "crossbeam-deque", - "crossbeam-utils 0.7.0", + "crossbeam-utils 0.7.2", "linked-hash-map", "num_cpus", "ordered-float", @@ -3114,6 +3201,17 @@ dependencies = [ "stb_truetype", ] +[[package]] +name = "rustversion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" +dependencies = [ + "proc-macro2 1.0.9", + "quote 1.0.2", + "syn 1.0.16", +] + [[package]] name = "ryu" version = "1.0.2" @@ -3137,9 +3235,9 @@ checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" [[package]] name = "scopeguard" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" @@ -3180,16 +3278,16 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] name = "serde_json" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15913895b61e0be854afd32fd4163fcd2a3df34142cf2cb961b310ce694cbf90" +checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" dependencies = [ "itoa", "ryu", @@ -3265,27 +3363,28 @@ checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" [[package]] name = "shred" -version = "0.9.4" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92472b9bafafbcba21935c6444d924e5332742f6778c49504a49a97eaeff6ccc" +checksum = "c5f08237e667ac94ad20f8878b5943d91a93ccb231428446c57c21c57779016d" dependencies = [ - "arrayvec 0.4.12", + "arrayvec 0.5.1", "hashbrown", "mopa", "rayon", "shred-derive", - "smallvec 0.6.13", + "smallvec 1.2.0", + "tynm", ] [[package]] name = "shred-derive" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c245c03fd923292ae18e01eadf65606fdc995c175433104ef3eee956db7c2d7" +checksum = "a1f37080f2751fbf091dbdebaa95bd6cf9dbf74ad1d50396b1908518a1747fdf" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "proc-macro2 1.0.9", + "quote 1.0.2", + "syn 1.0.16", ] [[package]] @@ -3302,9 +3401,9 @@ checksum = "83695fde96cbe9e08f0e4eb96b1b56fdbd44f2098ee27462dda964c7745fddc7" dependencies = [ "bitflags", "itertools", - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", ] [[package]] @@ -3321,9 +3420,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "slice-deque" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffddf594f5f597f63533d897427a570dbaa9feabaaa06595b74b71b7014507d7" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" dependencies = [ "libc", "mach", @@ -3368,12 +3467,11 @@ dependencies = [ [[package]] name = "specs" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4943fde8c5d3d14c3d19d2a4c7abbd7b626c270a19e6cd35252294a48feb698c" +checksum = "fff28a29366aff703d5da8a7e2c8875dc8453ac1118f842cbc0fa70c7db51240" dependencies = [ - "crossbeam-queue 0.1.2", - "derivative", + "crossbeam-queue 0.2.1", "hashbrown", "hibitset", "log", @@ -3386,24 +3484,22 @@ dependencies = [ [[package]] name = "specs-derive" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a63549fa0d4a6f76e99e6634c328f25d0c9fa8ad6f8498aef74f6c35c0b269e5" +checksum = "3e23e09360f3d2190fec4222cd9e19d3158d5da948c0d1ea362df617dd103511" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "proc-macro2 1.0.9", + "quote 1.0.2", + "syn 1.0.16", ] [[package]] name = "specs-hierarchy" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3199a5c3147995119adcaba09a4c70f390cd8002013ef35a7494a7fb9255e3" +checksum = "8c932b14cb12cd113485208054b19049ef2dd7cfa8b2ef7f64d7e078d384eb42" dependencies = [ "hibitset", - "shred", - "shred-derive", "shrev", "specs", ] @@ -3471,24 +3567,35 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" +checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", "unicode-xid 0.2.0", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2 1.0.9", + "quote 1.0.2", + "syn 1.0.16", +] + [[package]] name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ - "proc-macro2 1.0.8", + "proc-macro2 1.0.9", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", "unicode-xid 0.2.0", ] @@ -3595,6 +3702,24 @@ dependencies = [ "rand 0.7.3", ] +[[package]] +name = "tynm" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e87d8ff35b1a0aea28758ec2e5959f9e5d826cebf2349a3a7fad43b3e78de28" +dependencies = [ + "nom 5.1.1", +] + +[[package]] +name = "type-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717" +dependencies = [ + "fxhash", +] + [[package]] name = "typenum" version = "1.11.2" @@ -3603,9 +3728,9 @@ checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" [[package]] name = "unic-langid" -version = "0.6.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f209d65ab52de48d87c1845576d2d44f2b36553b8daa4c7f7a7383781f5750c" +checksum = "24d81136159f779c35b10655f45210c71cd5ca5a45aadfe9840a61c7071735ed" dependencies = [ "unic-langid-impl", "unic-langid-macros", @@ -3613,18 +3738,18 @@ dependencies = [ [[package]] name = "unic-langid-impl" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b22a3f781a9cb3588f751337fa7989472923973d546020d91da8e5d1cd5544" +checksum = "c43c61e94492eb67f20facc7b025778a904de83d953d8fcb60dd9adfd6e2d0ea" dependencies = [ "tinystr", ] [[package]] name = "unic-langid-macros" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856895dc86bc3a3a341cde3bcc04114bfd37a2e104c61a4a14c520df004851f" +checksum = "49bd90791278634d57e3ed4a4073108e3f79bfb87ab6a7b8664ba097425703df" dependencies = [ "proc-macro-hack", "tinystr", @@ -3634,13 +3759,13 @@ dependencies = [ [[package]] name = "unic-langid-macros-impl" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b80c946a565797c2cc44414b29d54e19103ff9fd51a3dc3aed7523deb3fc68" +checksum = "e0098f77bd754f8fb7850cdf4ab143aa821898c4ac6dc16bcb2aa3e62ce858d1" dependencies = [ "proc-macro-hack", "quote 1.0.2", - "syn 1.0.14", + "syn 1.0.16", "unic-langid-impl", ] @@ -3700,6 +3825,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + [[package]] name = "void" version = "1.0.2" @@ -3832,14 +3963,14 @@ dependencies = [ "backtrace", "bitflags", "cocoa", - "core-foundation", - "core-graphics", + "core-foundation 0.6.4", + "core-graphics 0.17.3", "image 0.21.3", "lazy_static", "libc", "log", "objc", - "parking_lot", + "parking_lot 0.9.0", "percent-encoding", "raw-window-handle 0.3.3", "serde", diff --git a/Cargo.toml b/Cargo.toml index ac33131..58a614c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,3 @@ -cargo-features = ["profile-overrides"] - [workspace] members = [ "assets_packer", @@ -7,8 +5,9 @@ members = [ "bins/server", ] -[profile.dev.overrides."*"] +[profile.dev.package."*"] opt-level = 3 -#[patch.'https://github.com/amethyst/amethyst'] #amethyst = { path = "../amethyst" } +[patch.'https://github.com/amethyst/amethyst'] +amethyst = { git = "https://github.com/mvlabat/amethyst.git", rev = "49dc5b6" } diff --git a/README.md b/README.md index d2f052a..bddd676 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ and play for free. ![Grumpy Visitors screenshot](header_screenshot.png) **Planned gameplay features:** -- Campaign and survival modes +- Campaign and survival modes - Character development system with persistent progress between levels - Co-op multiplayer (up to 4? players) @@ -72,17 +72,14 @@ This project is in its early stage of development. There are only some very basi - [ ] Look for weak spots in performance and optimize them if found and possible - [ ] Add visual indicators for better debugging (mobs health, network state, fps, latency etc) - [x] Refactor UI code (current definition files and the system are huge) -- [ ] Polish UI (transitions, resetting game states and menu screens, input validations) +- [x] Polish UI (transitions, resetting game states and menu screens, input validations) - [x] Better visuals (polishing animations, adding some nice shaders for spells) - [ ] Implement defining spells and monsters with *.ron files (no more hardcoded values) - [ ] Unit tests ### Known issues -- Starting a multiplayer game before the connected peers pop up in the players list will cause a crash +- Starting a multiplayer game before the connected peers pop up in the players list may cause a crash - The multiplayer game will eventually crash because of `ExceededMaxPacketSize` error -- You have only one chance to join or host a game. Anything went wrong? Just restart it. -(A lot of menu transitions polishing still needs to be done.) -- Leaving a multiplayer room will cause redundant players to spawn in single player - In multiplayer missiles are a little bit clunky when launching and sometimes desync ## License diff --git a/assets_packer/Cargo.toml b/assets_packer/Cargo.toml index 17ce70e..d327895 100644 --- a/assets_packer/Cargo.toml +++ b/assets_packer/Cargo.toml @@ -17,4 +17,4 @@ path = "../libs/animation_prefabs" [dependencies.amethyst] git = "https://github.com/amethyst/amethyst" -features = ["nightly", "empty"] +features = ["empty"] diff --git a/bins/client/Cargo.toml b/bins/client/Cargo.toml index eb94ebc..f11312a 100644 --- a/bins/client/Cargo.toml +++ b/bins/client/Cargo.toml @@ -11,13 +11,14 @@ profiler = ["gv_game/profiler"] [dependencies] bincode = "1.1.4" clap = "2.33.0" -derivative = "1.0.3" +derivative = "2.1.1" failure = "0.1.5" glsl-layout = "0.3.2" lazy_static = "1.3.0" log = "0.4.6" num = "0.2.0" rand = "0.6.5" +toml = "0.5.6" [dependencies.gv_animation_prefabs] path = "../../libs/animation_prefabs" @@ -34,7 +35,7 @@ features = ["client"] [dependencies.amethyst] git = "https://github.com/amethyst/amethyst" -features = ["nightly", "shader-compiler"] +features = ["shader-compiler"] # Bad news: https://github.com/rust-lang/cargo/issues/1197 diff --git a/bins/client/src/ecs/resources.rs b/bins/client/src/ecs/resources.rs index 0d792f0..7b030f5 100644 --- a/bins/client/src/ecs/resources.rs +++ b/bins/client/src/ecs/resources.rs @@ -3,7 +3,7 @@ use amethyst::Error; use std::{ env::current_exe, net::SocketAddr, - process::{Child, Command}, + process::{Child, Command, ExitStatus}, }; pub struct LastAcknowledgedUpdate { @@ -11,6 +11,28 @@ pub struct LastAcknowledgedUpdate { pub frame_number: u64, } +#[derive(Default)] +pub struct UiNetworkCommandResource { + pub command: Option, +} + +pub enum UiNetworkCommand { + Host { + nickname: String, + server_addr: SocketAddr, + }, + Connect { + nickname: String, + server_addr: SocketAddr, + }, + Kick { + player_number: usize, + }, + Start, + Leave, + Reset, +} + pub struct ServerCommand { process: Option, } @@ -25,10 +47,22 @@ impl ServerCommand { Ok(()) } - #[allow(dead_code)] - pub fn kill(&mut self) { + pub fn is_started(&self) -> bool { + self.process.is_some() + } + + pub fn stop(&mut self) { self.process = None; } + + pub fn exit_status(&mut self) -> Option { + self.process.as_mut().and_then(|process| { + process + .cmd + .try_wait() + .expect("Expected to get a process status") + }) + } } pub struct ServerProcess { diff --git a/bins/client/src/ecs/system_data/ui.rs b/bins/client/src/ecs/system_data/ui.rs index 8e17254..cd08a90 100644 --- a/bins/client/src/ecs/system_data/ui.rs +++ b/bins/client/src/ecs/system_data/ui.rs @@ -1,16 +1,16 @@ use amethyst::{ - ecs::{Entities, Entity, Join, World, WriteStorage}, + ecs::{storage::GenericReadStorage, Entities, Entity, Join, World, WriteStorage}, shred::{ResourceId, SystemData}, - ui::UiTransform, + ui::{UiText, UiTransform}, }; #[derive(SystemData)] -pub struct UiFinderMut<'a> { - entities: Entities<'a>, - storage: WriteStorage<'a, UiTransform>, +pub struct UiFinderMut<'s> { + entities: Entities<'s>, + storage: WriteStorage<'s, UiTransform>, } -impl<'a> UiFinderMut<'a> { +impl<'s> UiFinderMut<'s> { pub fn find(&self, id: &str) -> Option { (&*self.entities, &self.storage) .join() @@ -31,4 +31,24 @@ impl<'a> UiFinderMut<'a> { .find(|(_, transform)| transform.id == id) .map(|(entity, transform)| (entity, transform)) } + + pub fn get_ui_text<'a>( + &mut self, + ui_texts: &'a impl GenericReadStorage, + id: &str, + ) -> Option<&'a String> { + self.find(id) + .and_then(move |entity| ui_texts.get(entity)) + .map(|ui_text| &ui_text.text) + } + + pub fn get_ui_text_mut<'a>( + &mut self, + ui_texts: &'a mut WriteStorage, + id: &str, + ) -> Option<&'a mut String> { + self.find(id) + .and_then(move |entity| ui_texts.get_mut(entity)) + .map(|ui_text| &mut ui_text.text) + } } diff --git a/bins/client/src/ecs/systems/client_network.rs b/bins/client/src/ecs/systems/client_network.rs index fb1e96c..cf21e1a 100644 --- a/bins/client/src/ecs/systems/client_network.rs +++ b/bins/client/src/ecs/systems/client_network.rs @@ -1,11 +1,16 @@ use amethyst::{ - ecs::{Entities, Join, ReadExpect, System, Write, WriteExpect, WriteStorage}, - network::simulation::TransportResource, + ecs::{Entities, Join, ReadExpect, System, World, Write, WriteExpect, WriteStorage}, + network::simulation::{laminar::LaminarSocketResource, TransportResource}, + shred::{ResourceId, SystemData}, }; -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; -use gv_client_shared::ecs::resources::MultiplayerRoomState; +use gv_client_shared::ecs::resources::{ConnectionStatus, MultiplayerRoomState}; use gv_core::{ actions::monster_spawn::SpawnActions, ecs::{ @@ -14,15 +19,16 @@ use gv_core::{ net::MultiplayerGameState, world::{ FramedUpdates, PlayerActionUpdates, ReceivedPlayerUpdate, - ReceivedServerWorldUpdate, ServerWorldUpdate, LAG_COMPENSATION_FRAMES_LIMIT, + ReceivedServerWorldUpdate, ServerWorldUpdate, PAUSE_FRAME_THRESHOLD, }, GameEngineState, NewGameEngineState, }, system_data::time::GameTimeService, }, net::{ - client_message::ClientMessagePayload, server_message::ServerMessagePayload, NetEvent, - NetIdentifier, INTERPOLATION_FRAME_DELAY, + client_message::ClientMessagePayload, + server_message::{DisconnectReason, ServerMessage, ServerMessagePayload}, + NetEvent, NetIdentifier, INTERPOLATION_FRAME_DELAY, }, }; use gv_game::{ @@ -30,269 +36,526 @@ use gv_game::{ utils::net::{send_message_reliable, send_message_unreliable}, }; -use crate::ecs::resources::LastAcknowledgedUpdate; +use crate::ecs::resources::{ + LastAcknowledgedUpdate, ServerCommand, UiNetworkCommand, UiNetworkCommandResource, +}; -// Pause the game if we haven't received any message from server for the last 180 frames (3 secs). -const PAUSE_FRAME_THRESHOLD: u64 = - (LAG_COMPENSATION_FRAMES_LIMIT + LAG_COMPENSATION_FRAMES_LIMIT / 2) as u64; const HEARTBEAT_FRAME_INTERVAL: u64 = 10; +#[derive(SystemData)] +pub struct ClientNetworkSystemData<'s> { + game_time_service: GameTimeService<'s>, + game_engine_state: ReadExpect<'s, GameEngineState>, + entities: Entities<'s>, + connection_events: WriteExpect<'s, ConnectionEvents>, + multiplayer_room_state: WriteExpect<'s, MultiplayerRoomState>, + multiplayer_game_state: WriteExpect<'s, MultiplayerGameState>, + new_game_engine_sate: WriteExpect<'s, NewGameEngineState>, + last_acknowledged_update: WriteExpect<'s, LastAcknowledgedUpdate>, + framed_updates: WriteExpect<'s, FramedUpdates>, + player_actions_updates: WriteExpect<'s, FramedUpdates>, + spawn_actions: WriteExpect<'s, FramedUpdates>, + server_command: WriteExpect<'s, ServerCommand>, + ui_network_command: WriteExpect<'s, UiNetworkCommandResource>, + net_connection_models: WriteStorage<'s, NetConnectionModel>, + transport: Write<'s, TransportResource>, + laminar_socket: WriteExpect<'s, LaminarSocketResource>, +} + #[derive(Default)] pub struct ClientNetworkSystem { + session_id_autoinc: NetIdentifier, last_heartbeat_frame: u64, + has_sent_join_message: bool, + nickname: String, } -impl<'s> System<'s> for ClientNetworkSystem { - type SystemData = ( - GameTimeService<'s>, - ReadExpect<'s, GameEngineState>, - Entities<'s>, - WriteExpect<'s, ConnectionEvents>, - WriteExpect<'s, MultiplayerRoomState>, - WriteExpect<'s, MultiplayerGameState>, - WriteExpect<'s, NewGameEngineState>, - WriteExpect<'s, LastAcknowledgedUpdate>, - WriteExpect<'s, FramedUpdates>, - WriteExpect<'s, FramedUpdates>, - WriteExpect<'s, FramedUpdates>, - WriteStorage<'s, NetConnectionModel>, - Write<'s, TransportResource>, - ); +impl ClientNetworkSystem { + fn next_session_id(&mut self) -> NetIdentifier { + let id = self.session_id_autoinc; + self.session_id_autoinc = self.session_id_autoinc.wrapping_add(1); + id + } - fn run( + fn process_ui_network_command( &mut self, - ( - game_time_service, - game_engine_state, - entities, - mut connection_events, - mut multiplayer_room_state, - mut multiplayer_game_state, - mut new_game_engine_sate, - mut last_acknowledged_update, - mut framed_updates, - mut player_actions_updates, - mut spawn_actions, - mut net_connection_models, - mut transport, - ): Self::SystemData, + system_data: &mut ClientNetworkSystemData, + ui_network_command: UiNetworkCommand, ) { - // A joining (not hosting) client has to initiate a connection. - if net_connection_models.count() == 0 - && multiplayer_room_state.is_active - && !multiplayer_room_state.is_host - && !multiplayer_room_state.has_sent_join_message - { - multiplayer_room_state.has_sent_join_message = true; - let net_connection_model = - NetConnectionModel::new(0, multiplayer_room_state.server_addr); - send_message_reliable( - &mut transport, - &net_connection_model, - &ClientMessagePayload::JoinRoom { - nickname: multiplayer_room_state.nickname.clone(), + match ui_network_command { + UiNetworkCommand::Host { + nickname, + server_addr, + } => { + self.nickname = nickname; + system_data.multiplayer_room_state.is_active = true; + system_data.multiplayer_room_state.is_host = true; + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::Connecting(Instant::now()); + + let mut host_client_addr = system_data + .laminar_socket + .get_mut() + .expect("Expected a LaminarSocket") + .local_addr() + .expect("Expected a local address for a Laminar socket"); + match &mut host_client_addr { + SocketAddr::V4(addr) => addr.set_ip(Ipv4Addr::new(127, 0, 0, 1)), + SocketAddr::V6(addr) => addr.set_ip(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + }; + if let Err(err) = system_data + .server_command + .start(server_addr, host_client_addr) + { + log::error!("Couldn't start the server: {:?}", err); + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::ServerStartFailed; + } + } + + UiNetworkCommand::Connect { + nickname, + server_addr, + } => { + self.nickname = nickname; + system_data.multiplayer_room_state.is_active = true; + system_data.multiplayer_room_state.is_host = false; + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::Connecting(Instant::now()); + + let net_connection_model = + NetConnectionModel::new(0, self.next_session_id(), server_addr); + + log::info!("Sending a JoinRoom message"); + self.has_sent_join_message = true; + send_message_reliable( + &mut system_data.transport, + &net_connection_model, + ClientMessagePayload::JoinRoom { + sent_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Expected a duration unix timestamp"), + nickname: self.nickname.clone(), + }, + ); + + system_data + .entities + .build_entity() + .with(net_connection_model, &mut system_data.net_connection_models) + .build(); + } + + UiNetworkCommand::Kick { player_number } => send_message_reliable( + &mut system_data.transport, + server_connection(&mut system_data.net_connection_models), + ClientMessagePayload::Kick { + kicked_connection_id: system_data.multiplayer_game_state.players[player_number] + .connection_id, }, - ); - entities - .build_entity() - .with(net_connection_model, &mut net_connection_models) - .build(); - return; + ), + + UiNetworkCommand::Start => { + if system_data.multiplayer_room_state.is_host { + send_message_reliable( + &mut system_data.transport, + server_connection(&mut system_data.net_connection_models), + ClientMessagePayload::StartHostedGame, + ); + } else { + log::error!( + "Client check failed: only host can send a StartHostedGame message" + ); + } + } + + UiNetworkCommand::Leave => { + log::info!("Closing the connection with the server..."); + let net_connection_model = + server_connection(&mut system_data.net_connection_models); + send_message_reliable( + &mut system_data.transport, + net_connection_model, + ClientMessagePayload::Disconnect, + ); + net_connection_model.disconnected = true; + system_data.multiplayer_room_state.connection_status = + if !system_data.multiplayer_room_state.is_host { + ConnectionStatus::Disconnected(DisconnectReason::Closed) + } else { + ConnectionStatus::Disconnecting + } + } + + UiNetworkCommand::Reset => { + self.has_sent_join_message = false; + self.last_heartbeat_frame = 0; + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::NotConnected; + system_data.multiplayer_game_state.reset(); + system_data.multiplayer_room_state.reset(); + } } + } +} - if multiplayer_room_state.is_host - && multiplayer_room_state.has_started - && !multiplayer_room_state.has_sent_start_message - { - multiplayer_room_state.has_sent_start_message = true; - let connection = (&mut net_connection_models) - .join() - .next() - .expect("Expected a server connection"); - send_message_reliable( - &mut transport, - connection, - &ClientMessagePayload::StartHostedGame, - ); +impl<'s> System<'s> for ClientNetworkSystem { + type SystemData = ClientNetworkSystemData<'s>; + + #[allow(clippy::cognitive_complexity)] + fn run(&mut self, mut system_data: Self::SystemData) { + if let Some(ui_network_command) = system_data.ui_network_command.command.take() { + self.process_ui_network_command(&mut system_data, ui_network_command); } - for connection_event in connection_events.0.drain(..) { - match connection_event.event { - NetEvent::Message(ServerMessagePayload::Handshake { - net_id: connection_id, - is_host, - }) => { - log::info!("Received Handshake from a server ({})", connection_id); - let connection = (&mut net_connection_models) - .join() - .next() - .expect("Expected a server connection"); - - // A hosting client won't send a join packet first, as a server initiates - // a connection. - if !multiplayer_room_state.has_sent_join_message { - multiplayer_room_state.has_sent_join_message = true; - send_message_reliable( - &mut transport, - connection, - &ClientMessagePayload::JoinRoom { - nickname: multiplayer_room_state.nickname.clone(), - }, - ); - } + if !system_data.multiplayer_room_state.is_active { + system_data.net_connection_models.clear(); + return; + } - multiplayer_room_state.connection_id = Some(connection_id); - multiplayer_room_state.is_host = is_host; + if system_data.server_command.is_started() { + if let Some(exit_status) = system_data.server_command.exit_status() { + let code = exit_status.code().expect("Expected an exit status code"); + if code == 0 { + log::info!("The server has closed"); + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::Disconnected(DisconnectReason::Closed); + } else { + log::error!("The server crashed with the exit code {}", code); + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::Disconnected(DisconnectReason::ServerCrashed(code)); } - NetEvent::Message(ServerMessagePayload::UpdateRoomPlayers(players)) => { - log::info!("Updated room players"); - *multiplayer_game_state.update_players() = players; + system_data.server_command.stop(); + } + } + + if system_data.net_connection_models.count() == 0 { + if system_data.multiplayer_game_state.is_playing + && *system_data.game_engine_state == GameEngineState::Playing + { + system_data.multiplayer_game_state.is_disconnected = true; + } + return; + } + + // TODO: implement rejecting incoming connections for client, cause this can fail badly. + let net_connection_model = server_connection(&mut system_data.net_connection_models); + for connection_event in system_data.connection_events.0.drain(..) { + // Ignore all the messages for disconnected models, except for Disconnected or Handshake. + if net_connection_model.disconnected { + let ignore_event = match connection_event.event { + NetEvent::Disconnected + | NetEvent::Message(ServerMessage { + payload: ServerMessagePayload::Handshake { .. }, + .. + }) => false, + _ => true, + }; + if ignore_event { + continue; } - NetEvent::Message(ServerMessagePayload::StartGame(entity_net_ids)) => { - for (i, player) in multiplayer_game_state - .update_players() - .iter_mut() - .enumerate() - { - player.entity_net_id = entity_net_ids[i]; - if multiplayer_room_state - .connection_id - .map_or(false, |connection_id| connection_id == player.connection_id) - { - multiplayer_room_state.player_net_id = player.entity_net_id; - } - } - multiplayer_game_state.is_playing = true; - new_game_engine_sate.0 = GameEngineState::Playing; + } + + if let NetEvent::Message(ServerMessage { session_id, .. }) = &connection_event.event { + if *session_id != net_connection_model.session_id { + log::warn!("Ignoring a message with session id {} from a connection {} with session id {}", session_id, net_connection_model.id, net_connection_model.session_id); + continue; } - NetEvent::Message(ServerMessagePayload::UpdateWorld { id, mut updates }) => { - let connection = (&mut net_connection_models) - .join() - .next() - .expect("Expected a server connection"); - send_message_unreliable( - &mut transport, - connection, - &ClientMessagePayload::AcknowledgeWorldUpdate(id), - ); + } - if last_acknowledged_update.id < id { - updates.sort_by(|a, b| a.frame_number.cmp(&b.frame_number)); + if system_data.multiplayer_game_state.is_playing { + let ignore_event = match &connection_event.event { + NetEvent::Message(ServerMessage { + session_id: _, + payload, + }) => match payload { + ServerMessagePayload::Handshake { .. } => true, + ServerMessagePayload::UpdateRoomPlayers(_) => true, + ServerMessagePayload::StartGame(_) => true, + _ => false, + }, + _ => false, + }; + if ignore_event { + continue; + } + } - last_acknowledged_update.id = id; - last_acknowledged_update.frame_number = - last_acknowledged_update.frame_number.max( - updates - .last() - .map(|update| update.frame_number) - .unwrap_or(0), + match connection_event.event { + NetEvent::Message(ServerMessage { + session_id: _, + payload, + }) => { + match payload { + // Are covered by NetConnectionManager. + ServerMessagePayload::Heartbeat + | ServerMessagePayload::Ping(_) + | ServerMessagePayload::Pong { .. } => {} + + ServerMessagePayload::Handshake { + net_id: connection_id, + is_host, + } => { + log::info!( + "Received Handshake from a server ({}), is_host: {}", + connection_id, + is_host + ); + // A hosting client won't send a join packet first, as a server initiates + // a connection. + if !self.has_sent_join_message { + log::info!("Sending a JoinRoom message"); + self.has_sent_join_message = true; + send_message_reliable( + &mut system_data.transport, + net_connection_model, + ClientMessagePayload::JoinRoom { + sent_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Expected a duration unix timestamp"), + nickname: self.nickname.clone(), + }, + ); + } + + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::Connected(connection_id); + system_data.multiplayer_room_state.is_host = is_host; + } + ServerMessagePayload::UpdateRoomPlayers(players) => { + log::info!("Updated room players (player count: {})", players.len()); + *system_data.multiplayer_game_state.update_players() = players; + } + ServerMessagePayload::StartGame(entity_net_ids) => { + system_data.last_acknowledged_update.frame_number = 0; + system_data.last_acknowledged_update.id = 0; + + let connection_id = system_data + .multiplayer_room_state + .connection_status + .connection_id() + .expect( + "Expected to be connected when receiving StartGame message", + ); + + let mut found_ourselves = false; + // Looking for an entity_net_id of a client's player + // and storing it in the MultiplayerRoomState. + for (i, player) in system_data + .multiplayer_game_state + .update_players() + .iter_mut() + .enumerate() + { + player.entity_net_id = entity_net_ids[i]; + if connection_id == player.connection_id { + log::info!( + "Starting a new game as a player with net id {}", + player.entity_net_id + ); + found_ourselves = true; + system_data.multiplayer_room_state.player_net_id = + player.entity_net_id; + } + } + if !found_ourselves { + panic!( + "Couldn't found a player with connection id {}", + connection_id + ); + } + system_data.multiplayer_game_state.is_playing = true; + system_data.new_game_engine_sate.0 = GameEngineState::Playing; + } + ServerMessagePayload::UpdateWorld { id, mut updates } => { + send_message_unreliable( + &mut system_data.transport, + net_connection_model, + ClientMessagePayload::AcknowledgeWorldUpdate(id), ); - let frame_to_reserve = last_acknowledged_update - .frame_number - .max(game_time_service.game_frame_number()); - framed_updates.reserve_updates(frame_to_reserve); - spawn_actions.reserve_updates(frame_to_reserve); - - apply_world_updates( - vec![multiplayer_room_state.player_net_id], - &mut framed_updates, - &mut spawn_actions, - updates, - ); + if system_data.last_acknowledged_update.id < id { + updates.sort_by(|a, b| a.frame_number.cmp(&b.frame_number)); + + system_data.last_acknowledged_update.id = id; + system_data.last_acknowledged_update.frame_number = + system_data.last_acknowledged_update.frame_number.max( + updates + .last() + .map(|update| update.frame_number) + .unwrap_or(0), + ); + + let frame_to_reserve = system_data + .last_acknowledged_update + .frame_number + .max(system_data.game_time_service.game_frame_number()); + system_data.framed_updates.reserve_updates(frame_to_reserve); + system_data.spawn_actions.reserve_updates(frame_to_reserve); + + apply_world_updates( + vec![system_data.multiplayer_room_state.player_net_id], + &mut system_data.framed_updates, + &mut system_data.spawn_actions, + updates, + ); + } + } + ServerMessagePayload::DiscardWalkActions(discarded_actions) => { + discard_walk_actions( + &mut system_data.player_actions_updates, + discarded_actions, + ); + } + ServerMessagePayload::PauseWaitingForPlayers { id, players } => { + if system_data + .multiplayer_game_state + .waiting_for_players_pause_id + < id + { + // We don't always want set `waiting_for_players` to true, as we may need + // to catch up with the server if we're lagging too. See below. + system_data + .multiplayer_game_state + .waiting_for_players_pause_id = id; + system_data.multiplayer_game_state.lagging_players = players; + } + } + ServerMessagePayload::UnpauseWaitingForPlayers(id) => { + if system_data + .multiplayer_game_state + .waiting_for_players_pause_id + <= id + { + system_data.multiplayer_game_state.waiting_for_players = false; + system_data + .multiplayer_game_state + .waiting_for_players_pause_id = id; + system_data.multiplayer_game_state.lagging_players.clear(); + } + } + ServerMessagePayload::Disconnect(disconnect_reason) => { + if !system_data + .multiplayer_room_state + .connection_status + .is_not_connected() + { + log::info!( + "Received a Disconnect message: {:?}", + disconnect_reason + ); + let is_shutting_down_by_host = + if let ConnectionStatus::Disconnecting = + system_data.multiplayer_room_state.connection_status + { + true + } else { + false + }; + + if !is_shutting_down_by_host { + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::Disconnected(disconnect_reason); + } + } + } } } - NetEvent::Message(ServerMessagePayload::DiscardWalkActions(discarded_actions)) => { - discard_walk_actions(&mut player_actions_updates, discarded_actions); - } - NetEvent::Message(ServerMessagePayload::PauseWaitingForPlayers { id, players }) => { - if multiplayer_game_state.waiting_for_players_pause_id < id { - // We don't always want set `waiting_for_players` to true, as we may need - // to catch up with the server if we're lagging too. See below. - multiplayer_game_state.waiting_for_players_pause_id = id; - multiplayer_game_state.lagging_players = players; + + NetEvent::Disconnected => { + let mut is_not_connected = system_data + .multiplayer_room_state + .connection_status + .is_not_connected(); + if let ConnectionStatus::Connecting(started_at) = + system_data.multiplayer_room_state.connection_status + { + // A really ugly way to ignore Disconnected events for previous connections. + is_not_connected = Instant::now() - started_at < Duration::from_secs(1); } - } - NetEvent::Message(ServerMessagePayload::UnpauseWaitingForPlayers(id)) => { - if multiplayer_game_state.waiting_for_players_pause_id <= id { - multiplayer_game_state.waiting_for_players = false; - multiplayer_game_state.waiting_for_players_pause_id = id; - multiplayer_game_state.lagging_players.clear(); + if !is_not_connected { + system_data.multiplayer_room_state.connection_status = + ConnectionStatus::ConnectionFailed(None); } } - // TODO: handle disconnects. _ => {} } } - if game_time_service.engine_time().frame_number() - self.last_heartbeat_frame + if system_data.game_time_service.engine_time().frame_number() - self.last_heartbeat_frame > HEARTBEAT_FRAME_INTERVAL + && !net_connection_model.disconnected { - let net_connection_model = (&net_connection_models).join().next(); - - if let Some(net_connection_model) = net_connection_model { - self.last_heartbeat_frame = game_time_service.engine_time().frame_number(); - send_message_reliable( - &mut transport, - net_connection_model, - &ClientMessagePayload::Heartbeat, - ); - } + self.last_heartbeat_frame = system_data.game_time_service.engine_time().frame_number(); + send_message_reliable( + &mut system_data.transport, + net_connection_model, + ClientMessagePayload::Heartbeat, + ); } // Until the server authorizes to unpause we need to use a chance to catch up with it, // even if it's not us lagging. - if !multiplayer_game_state.lagging_players.is_empty() { - let server_frame = framed_updates + if !system_data + .multiplayer_game_state + .lagging_players + .is_empty() + { + let server_frame = system_data + .framed_updates .updates .back() .map_or(0, |update| update.frame_number); - multiplayer_game_state.waiting_for_players = - game_time_service.game_frame_number() + INTERPOLATION_FRAME_DELAY >= server_frame; + system_data.multiplayer_game_state.waiting_for_players = + system_data.game_time_service.game_frame_number() + INTERPOLATION_FRAME_DELAY + >= server_frame; } - if *game_engine_state == GameEngineState::Playing && multiplayer_game_state.is_playing { + if *system_data.game_engine_state == GameEngineState::Playing + && system_data.multiplayer_game_state.is_playing + { // We always skip first INTERPOLATION_FRAME_DELAY frames on game start. - match game_time_service + match system_data + .game_time_service .game_frame_number_absolute() .cmp(&INTERPOLATION_FRAME_DELAY) { Ordering::Less => { - multiplayer_game_state.waiting_network = true; + system_data.multiplayer_game_state.waiting_network = true; return; } Ordering::Equal => { - multiplayer_game_state.waiting_network = false; + system_data.multiplayer_game_state.waiting_network = false; } _ => {} } // Wait if we a server is lagging behind for PAUSE_FRAME_THRESHOLD frames. - let frames_ahead = game_time_service.game_frame_number().saturating_sub( - last_acknowledged_update - .frame_number - .saturating_sub(INTERPOLATION_FRAME_DELAY), - ); + let frames_ahead = system_data + .game_time_service + .game_frame_number() + .saturating_sub( + system_data + .last_acknowledged_update + .frame_number + .saturating_sub(INTERPOLATION_FRAME_DELAY), + ); log::trace!("Frames ahead: {}", frames_ahead); - if multiplayer_game_state.waiting_network { - multiplayer_game_state.waiting_network = frames_ahead == 0; + if system_data.multiplayer_game_state.waiting_network { + system_data.multiplayer_game_state.waiting_network = frames_ahead != 0; } else if frames_ahead > PAUSE_FRAME_THRESHOLD { - multiplayer_game_state.waiting_network = true; + system_data.multiplayer_game_state.waiting_network = true; } - if multiplayer_game_state.waiting_network || multiplayer_game_state.waiting_for_players + if system_data.multiplayer_game_state.waiting_network + || system_data.multiplayer_game_state.waiting_for_players { - let net_connection_model = (&net_connection_models) - .join() - .next() - .expect("Expected a server connection (NetConnectionModel)"); - log::debug!( "Waiting for server. Frames ahead: {}. Current frame: {}. Last ServerWorldUpdate frame: {}. Estimated server frame: {}", frames_ahead, - game_time_service.game_frame_number(), - last_acknowledged_update.frame_number, + system_data.game_time_service.game_frame_number(), + system_data.last_acknowledged_update.frame_number, net_connection_model.ping_pong_data.last_stored_game_frame(), ); } @@ -300,6 +563,15 @@ impl<'s> System<'s> for ClientNetworkSystem { } } +fn server_connection<'a>( + net_connection_models: &'a mut WriteStorage, +) -> &'a mut NetConnectionModel { + net_connection_models + .join() + .next() + .expect("Expected a server connection") +} + // Expects incoming_updates to be sorted (lowest frame first). fn apply_world_updates( controlled_players: Vec, diff --git a/bins/client/src/ecs/systems/game_updates_broadcasting.rs b/bins/client/src/ecs/systems/game_updates_broadcasting.rs index dce7151..1386796 100644 --- a/bins/client/src/ecs/systems/game_updates_broadcasting.rs +++ b/bins/client/src/ecs/systems/game_updates_broadcasting.rs @@ -43,6 +43,10 @@ impl<'s> System<'s> for GameUpdatesBroadcastingSystem { mut net_connection_models, ): Self::SystemData, ) { + if !game_state_helper.is_multiplayer() { + client_world_updates.clear(); + return; + } if !game_state_helper.multiplayer_is_running() { return; } @@ -56,7 +60,7 @@ impl<'s> System<'s> for GameUpdatesBroadcastingSystem { send_message_reliable( &mut transport, net_connection, - &ClientMessagePayload::WalkActions(ImmediatePlayerActionsUpdates { + ClientMessagePayload::WalkActions(ImmediatePlayerActionsUpdates { frame_number: game_time_service.game_frame_number() + INTERPOLATION_FRAME_DELAY, updates: client_world_updates.walk_action_updates.clone(), }), @@ -68,7 +72,7 @@ impl<'s> System<'s> for GameUpdatesBroadcastingSystem { send_message_reliable( &mut transport, net_connection, - &ClientMessagePayload::CastActions(ImmediatePlayerActionsUpdates { + ClientMessagePayload::CastActions(ImmediatePlayerActionsUpdates { frame_number: game_time_service.game_frame_number() + INTERPOLATION_FRAME_DELAY, updates: client_world_updates.cast_action_updates.clone(), }), @@ -88,7 +92,7 @@ impl<'s> System<'s> for GameUpdatesBroadcastingSystem { send_message_reliable( &mut transport, net_connection, - &ClientMessagePayload::LookActions(PlayerLookActionUpdates { + ClientMessagePayload::LookActions(PlayerLookActionUpdates { updates: Vec::from_iter(client_world_updates.look_actions_updates.drain(..).map( |(frame_number, update)| (frame_number + INTERPOLATION_FRAME_DELAY, update), )), diff --git a/bins/client/src/ecs/systems/menu/hidden.rs b/bins/client/src/ecs/systems/menu/hidden.rs new file mode 100644 index 0000000..f51e643 --- /dev/null +++ b/bins/client/src/ecs/systems/menu/hidden.rs @@ -0,0 +1,68 @@ +use gv_client_shared::ecs::resources::ConnectionStatus; + +use super::*; +use crate::{ecs::resources::UiNetworkCommand, utils::ui::disconnect_reason_title}; + +const DISCONNECTED: &str = "MP_GAME_DISCONNECTED"; + +pub struct HiddenMenuScreen; + +impl MenuScreen for HiddenMenuScreen { + fn elements_to_show(&self, _system_data: &MenuSystemData) -> Vec { + vec![] + } + + fn update( + &mut self, + system_data: &mut MenuSystemData, + button_pressed: Option<&str>, + modal_window_id: Option<&str>, + ) -> StateUpdate { + if system_data.game_level_state.is_over { + return StateUpdate::GameMenuUpdate { + game_engine_state: Some(GameEngineState::Menu), + menu_screen: Some(GameMenuScreen::RestartMenu), + }; + } + + let disconnected_modal_window_is_shown = + modal_window_id.map_or(false, |modal_window_id| modal_window_id == DISCONNECTED); + if !disconnected_modal_window_is_shown { + match system_data.multiplayer_room_state.connection_status { + ConnectionStatus::ConnectionFailed(ref error) => { + return StateUpdate::ShowModalWindow { + id: DISCONNECTED.to_owned(), + title: error + .as_ref() + .map(|error| format!("Disconnected: {:?}", error)) + .unwrap_or_else(|| "Disconnected".to_owned()), + show_confirmation: true, + } + } + ConnectionStatus::Disconnected(disconnect_reason) => { + return StateUpdate::ShowModalWindow { + id: DISCONNECTED.to_owned(), + title: disconnect_reason_title(disconnect_reason), + show_confirmation: true, + } + } + _ => {} + } + } + + if let (Some(UI_MODAL_CONFIRM_BUTTON), Some(DISCONNECTED)) = + (button_pressed, modal_window_id) + { + // TODO (refactor): mutating game_level_state from here is not cool. + system_data.game_level_state.is_over = true; + + system_data.ui_network_command.command = Some(UiNetworkCommand::Reset); + StateUpdate::GameMenuUpdate { + game_engine_state: Some(GameEngineState::Menu), + menu_screen: Some(GameMenuScreen::LobbyMenu), + } + } else { + StateUpdate::None + } + } +} diff --git a/bins/client/src/ecs/systems/menu/lobby.rs b/bins/client/src/ecs/systems/menu/lobby.rs index 1ead7ac..7610fdf 100644 --- a/bins/client/src/ecs/systems/menu/lobby.rs +++ b/bins/client/src/ecs/systems/menu/lobby.rs @@ -1,9 +1,15 @@ -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use gv_client_shared::ecs::resources::ConnectionStatus; use super::*; +use crate::{ecs::resources::UiNetworkCommand, utils::ui::disconnect_reason_title}; pub struct LobbyMenuScreen; +const INVALID_IP_ADDRESS: &str = "LOBBY_INVALID_IP_ADDRESS"; +const SERVER_START_FAILED: &str = "LOBBY_SERVER_START_FAILED"; +const CONNECTING_PROGRESS: &str = "LOBBY_CONNECTING_PROGRESS"; +const CONNECTING_FAILED: &str = "LOBBY_CONNECTING_FAILED"; + impl MenuScreen for LobbyMenuScreen { fn elements_to_show(&self, _system_data: &MenuSystemData) -> Vec { vec![ @@ -20,14 +26,17 @@ impl MenuScreen for LobbyMenuScreen { ] } - fn process_events( + fn update( &mut self, system_data: &mut MenuSystemData, button_pressed: Option<&str>, + modal_window_id: Option<&str>, ) -> StateUpdate { - match button_pressed { - Some(UI_MAIN_MENU_BUTTON) => StateUpdate::new_menu_screen(GameMenuScreen::MainMenu), - Some(b @ UI_LOBBY_HOST_BUTTON) | Some(b @ UI_LOBBY_JOIN_BUTTON) => { + match (button_pressed, modal_window_id) { + (Some(UI_MAIN_MENU_BUTTON), _) => { + StateUpdate::new_menu_screen(GameMenuScreen::MainMenu) + } + (Some(b @ (UI_LOBBY_HOST_BUTTON | UI_LOBBY_JOIN_BUTTON)), _) => { let is_host = b == UI_LOBBY_HOST_BUTTON; let address_field = if is_host { @@ -37,43 +46,103 @@ impl MenuScreen for LobbyMenuScreen { }; let addr = system_data .ui_finder - .find(address_field) - .and_then(|entity| system_data.ui_texts.get(entity)) - .map(|ui_text| ui_text.text.clone()) + .get_ui_text(&system_data.ui_texts, address_field) .unwrap(); let nickname = system_data .ui_finder - .find(UI_LOBBY_NICKNAME_EDITABLE) - .and_then(|entity| system_data.ui_texts.get(entity)) - .map(|ui_text| ui_text.text.clone()) + .get_ui_text(&system_data.ui_texts, UI_LOBBY_NICKNAME_EDITABLE) + .cloned() .unwrap(); - // TODO: error validations. - let server_addr = addr.parse().expect("Expected a valid address"); - if is_host { - let mut host_client_addr = system_data - .laminar_socket - .get_mut() - .expect("Expected a LaminarSocket") - .local_addr() - .expect("Expected a local address for a Laminar socket"); - match &mut host_client_addr { - SocketAddr::V4(addr) => addr.set_ip(Ipv4Addr::new(127, 0, 0, 1)), - SocketAddr::V6(addr) => addr.set_ip(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + let server_addr = addr.parse(); + if server_addr.is_err() { + return StateUpdate::ShowModalWindow { + id: INVALID_IP_ADDRESS.to_owned(), + title: "Server IP address has invalid format".to_owned(), + show_confirmation: true, }; - system_data - .server_command - .start(server_addr, host_client_addr) - .expect("Expected to start a server"); } - system_data.multiplayer_room_state.nickname = nickname; - system_data.multiplayer_room_state.is_active = true; - system_data.multiplayer_room_state.server_addr = server_addr; - system_data.multiplayer_room_state.is_host = is_host; + let server_addr = server_addr.unwrap(); - StateUpdate::new_menu_screen(GameMenuScreen::MultiplayerRoomMenu) + log::info!("Joining {}...", server_addr); + if is_host { + system_data.ui_network_command.command = Some(UiNetworkCommand::Host { + nickname, + server_addr, + }); + } else { + system_data.ui_network_command.command = Some(UiNetworkCommand::Connect { + nickname, + server_addr, + }); + } + StateUpdate::ShowModalWindow { + id: CONNECTING_PROGRESS.to_owned(), + title: "Connecting...".to_owned(), + show_confirmation: false, + } } - _ => StateUpdate::none(), + (Some(UI_MODAL_CONFIRM_BUTTON), Some(CONNECTING_FAILED)) => { + system_data.ui_network_command.command = Some(UiNetworkCommand::Reset); + StateUpdate::None + } + (Some(UI_MODAL_CONFIRM_BUTTON), Some(SERVER_START_FAILED)) => { + system_data.ui_network_command.command = Some(UiNetworkCommand::Reset); + StateUpdate::None + } + (None, modal_window_id) => { + match &system_data.multiplayer_room_state.connection_status { + ConnectionStatus::NotConnected => StateUpdate::None, + ConnectionStatus::Connecting(_) => StateUpdate::None, + ConnectionStatus::Disconnecting => StateUpdate::None, + ConnectionStatus::Connected(_) => { + StateUpdate::new_menu_screen(GameMenuScreen::MultiplayerRoomMenu) + } + ConnectionStatus::ConnectionFailed(error) => { + if is_failed_modal_window(modal_window_id) { + StateUpdate::None + } else { + StateUpdate::ShowModalWindow { + id: CONNECTING_FAILED.to_owned(), + title: error + .as_ref() + .map(|error| format!("Disconnected: {:?}", error)) + .unwrap_or_else(|| "Disconnected".to_owned()), + show_confirmation: true, + } + } + } + ConnectionStatus::Disconnected(disconnect_reason) => { + if is_failed_modal_window(modal_window_id) { + StateUpdate::None + } else { + StateUpdate::ShowModalWindow { + id: CONNECTING_FAILED.to_owned(), + title: disconnect_reason_title(*disconnect_reason), + show_confirmation: true, + } + } + } + ConnectionStatus::ServerStartFailed => { + if is_failed_modal_window(modal_window_id) { + StateUpdate::None + } else { + StateUpdate::ShowModalWindow { + id: SERVER_START_FAILED.to_owned(), + title: "Couldn't start the server, make sure that gv_server is in the same working directory".to_owned(), + show_confirmation: true, + } + } + } + } + } + _ => StateUpdate::None, } } } + +fn is_failed_modal_window(modal_window_id: Option<&str>) -> bool { + modal_window_id.map_or(true, |modal_window_id| { + modal_window_id == CONNECTING_FAILED || modal_window_id == SERVER_START_FAILED + }) +} diff --git a/bins/client/src/ecs/systems/menu/main.rs b/bins/client/src/ecs/systems/menu/main.rs index e98a592..eaf51fb 100644 --- a/bins/client/src/ecs/systems/menu/main.rs +++ b/bins/client/src/ecs/systems/menu/main.rs @@ -11,19 +11,20 @@ impl MenuScreen for MainMenuScreen { ] } - fn process_events( + fn update( &mut self, _system_data: &mut MenuSystemData, button_pressed: Option<&str>, + _modal_window_id: Option<&str>, ) -> StateUpdate { match button_pressed { - Some(UI_SINGLE_PLAYER_BUTTON) => StateUpdate { + Some(UI_SINGLE_PLAYER_BUTTON) => StateUpdate::GameMenuUpdate { game_engine_state: Some(GameEngineState::Playing), menu_screen: Some(GameMenuScreen::Hidden), }, Some(UI_MULTIPLAYER_BUTTON) => StateUpdate::new_menu_screen(GameMenuScreen::LobbyMenu), Some(UI_QUIT_BUTTON) => StateUpdate::new_game_engine_state(GameEngineState::Quit), - _ => StateUpdate::none(), + _ => StateUpdate::None, } } } diff --git a/bins/client/src/ecs/systems/menu/mod.rs b/bins/client/src/ecs/systems/menu/mod.rs index e0cd72c..dd43b77 100644 --- a/bins/client/src/ecs/systems/menu/mod.rs +++ b/bins/client/src/ecs/systems/menu/mod.rs @@ -1,19 +1,19 @@ +mod hidden; mod lobby; mod main; mod multiplayer_room; mod restart; use amethyst::{ - core::{Hidden, HiddenPropagate, ParentHierarchy}, + core::{HiddenPropagate, ParentHierarchy}, ecs::{Entity, ReadExpect, System, SystemData, World, Write, WriteExpect, WriteStorage}, - network::simulation::laminar::LaminarSocketResource, shred::ResourceId, shrev::{EventChannel, ReaderId}, ui::{Interactable, UiEvent, UiEventType, UiImage, UiText}, }; use lazy_static::lazy_static; -use std::time::Duration; +use std::{collections::VecDeque, time::Duration}; use gv_client_shared::ecs::resources::MultiplayerRoomState; use gv_core::ecs::{ @@ -22,11 +22,11 @@ use gv_core::ecs::{ }; use crate::ecs::{ - resources::ServerCommand, + resources::UiNetworkCommandResource, system_data::ui::UiFinderMut, systems::menu::{ - lobby::LobbyMenuScreen, main::MainMenuScreen, multiplayer_room::MultiplayerRoomMenuScreen, - restart::RestartMenuScreen, + hidden::HiddenMenuScreen, lobby::LobbyMenuScreen, main::MainMenuScreen, + multiplayer_room::MultiplayerRoomMenuScreen, restart::RestartMenuScreen, }, }; @@ -35,6 +35,7 @@ type MenuElement = &'static str; const MENU_FADE_OUT_DURATION_MS: u64 = 500; const CONTAINER_TAG: &str = "_container"; const BACKGROUND_TAG: &str = "_bg"; +const MODAL_TAG: &str = "_modal"; const UI_MAIN_CONTAINER: &str = "ui_main_container"; const UI_LOADING_LABEL: &str = "ui_loading_label"; @@ -61,6 +62,7 @@ const UI_MP_ROOM_PLAYER1_CONTAINER: &str = "ui_mp_room_player1_container"; const UI_MP_ROOM_PLAYER1_BG: &str = "ui_mp_room_player1_bg"; const UI_MP_ROOM_PLAYER1_NUMBER: &str = "ui_mp_room_player1_number"; const UI_MP_ROOM_PLAYER1_NICKNAME: &str = "ui_mp_room_player1_nickname"; +const UI_MP_ROOM_PLAYER1_KICK: &str = "ui_mp_room_player1_kick"; const UI_MP_ROOM_PLAYER2_CONTAINER: &str = "ui_mp_room_player2_container"; const UI_MP_ROOM_PLAYER2_BG: &str = "ui_mp_room_player2_bg"; const UI_MP_ROOM_PLAYER2_NUMBER: &str = "ui_mp_room_player2_number"; @@ -77,6 +79,12 @@ const UI_MP_ROOM_PLAYER4_NUMBER: &str = "ui_mp_room_player4_number"; const UI_MP_ROOM_PLAYER4_NICKNAME: &str = "ui_mp_room_player4_nickname"; const UI_MP_ROOM_PLAYER4_KICK: &str = "ui_mp_room_player4_kick"; +const UI_MODAL_BACKDROP_CONTAINER: &str = "ui_modal_backdrop_container"; +const UI_MODAL_WINDOW_BORDER_CONTAINER: &str = "ui_modal_window_border_container"; +const UI_MODAL_WINDOW_CONTAINER: &str = "ui_modal_window_container"; +const UI_MODAL_TITLE: &str = "ui_modal_title"; +const UI_MODAL_CONFIRM_BUTTON: &str = "ui_modal_confirm_button"; + trait MenuScreen { fn elements_to_show(&self, system_data: &MenuSystemData) -> Vec; @@ -84,10 +92,21 @@ trait MenuScreen { self.elements_to_show(system_data) } - fn process_events( + fn show(&mut self, _system_data: &mut MenuSystemData) {} + + fn value_changed( + &mut self, + _system_data: &mut MenuSystemData, + _text_field_id: &str, + _new_value: &str, + ) { + } + + fn update( &mut self, system_data: &mut MenuSystemData, button_pressed: Option<&str>, + modal_window_id: Option<&str>, ) -> StateUpdate; } @@ -119,21 +138,28 @@ lazy_static! { UI_MP_ROOM_PLAYER1_BG, UI_MP_ROOM_PLAYER1_NUMBER, UI_MP_ROOM_PLAYER1_NICKNAME, + UI_MP_ROOM_PLAYER1_KICK, UI_MP_ROOM_PLAYER2_CONTAINER, UI_MP_ROOM_PLAYER2_BG, UI_MP_ROOM_PLAYER2_NUMBER, UI_MP_ROOM_PLAYER2_NICKNAME, - // UI_MP_ROOM_PLAYER2_KICK, + UI_MP_ROOM_PLAYER2_KICK, UI_MP_ROOM_PLAYER3_CONTAINER, UI_MP_ROOM_PLAYER3_BG, UI_MP_ROOM_PLAYER3_NUMBER, UI_MP_ROOM_PLAYER3_NICKNAME, - // UI_MP_ROOM_PLAYER3_KICK, + UI_MP_ROOM_PLAYER3_KICK, UI_MP_ROOM_PLAYER4_CONTAINER, UI_MP_ROOM_PLAYER4_BG, UI_MP_ROOM_PLAYER4_NUMBER, UI_MP_ROOM_PLAYER4_NICKNAME, - // UI_MP_ROOM_PLAYER4_KICK, + UI_MP_ROOM_PLAYER4_KICK, + ]; + static ref MODAL_WINDOW_ELEMENTS: &'static [&'static str] = &[ + UI_MODAL_BACKDROP_CONTAINER, + UI_MODAL_WINDOW_BORDER_CONTAINER, + UI_MODAL_WINDOW_CONTAINER, + UI_MODAL_TITLE, ]; } @@ -144,36 +170,41 @@ pub struct MenuSystemData<'s> { hierarchy: ReadExpect<'s, ParentHierarchy>, game_engine_state: ReadExpect<'s, GameEngineState>, new_game_engine_state: WriteExpect<'s, NewGameEngineState>, - game_level_state: ReadExpect<'s, GameLevelState>, - server_command: WriteExpect<'s, ServerCommand>, - multiplayer_room_state: WriteExpect<'s, MultiplayerRoomState>, - multiplayer_game_state: WriteExpect<'s, MultiplayerGameState>, - laminar_socket: WriteExpect<'s, LaminarSocketResource>, + game_level_state: WriteExpect<'s, GameLevelState>, + ui_network_command: WriteExpect<'s, UiNetworkCommandResource>, + multiplayer_room_state: ReadExpect<'s, MultiplayerRoomState>, + multiplayer_game_state: ReadExpect<'s, MultiplayerGameState>, ui_events: Write<'s, EventChannel>, ui_texts: WriteStorage<'s, UiText>, ui_images: WriteStorage<'s, UiImage>, ui_interactables: WriteStorage<'s, Interactable>, - hidden: WriteStorage<'s, Hidden>, hidden_propagates: WriteStorage<'s, HiddenPropagate>, } pub struct MenuSystem { menu_screens: MenuScreens, - elements_to_hide: Vec<&'static str>, - elements_to_show: Vec<&'static str>, + modal_window_id: Option, mouse_reactive: Vec<&'static str>, - is_transitioning: bool, - transition_began_at: Duration, + menu_screen_animations: VecDeque, event_readers: Option>, menu_screen: GameMenuScreen, transition_state: TransitionState, } +#[derive(Debug)] +struct MenuScreenAnimation { + change_modal_title: Option, + started_at: Option, + elements_to_hide: Vec<&'static str>, + elements_to_show: Vec<&'static str>, +} + struct MenuScreens { lobby_menu_screen: LobbyMenuScreen, main_menu_screen: MainMenuScreen, multiplayer_room_menu_screen: MultiplayerRoomMenuScreen, restart_menu_screen: RestartMenuScreen, + hidden_menu_screen: HiddenMenuScreen, } impl MenuScreens { @@ -183,37 +214,43 @@ impl MenuScreens { GameMenuScreen::MainMenu => Some(&mut self.main_menu_screen), GameMenuScreen::MultiplayerRoomMenu => Some(&mut self.multiplayer_room_menu_screen), GameMenuScreen::RestartMenu => Some(&mut self.restart_menu_screen), - _ => None, + GameMenuScreen::Hidden => Some(&mut self.hidden_menu_screen), + GameMenuScreen::Loading => None, } } } -struct StateUpdate { - game_engine_state: Option, - menu_screen: Option, +enum StateUpdate { + GameMenuUpdate { + game_engine_state: Option, + menu_screen: Option, + }, + ShowModalWindow { + id: String, + title: String, + show_confirmation: bool, + }, + CustomAnimation { + elements_to_hide: Vec<&'static str>, + elements_to_show: Vec<&'static str>, + }, + None, } impl StateUpdate { pub fn new_game_engine_state(game_engine_state: GameEngineState) -> Self { - Self { + Self::GameMenuUpdate { game_engine_state: Some(game_engine_state), menu_screen: None, } } pub fn new_menu_screen(menu_screen: GameMenuScreen) -> Self { - Self { + Self::GameMenuUpdate { game_engine_state: None, menu_screen: Some(menu_screen), } } - - pub fn none() -> Self { - Self { - game_engine_state: None, - menu_screen: None, - } - } } impl MenuSystem { @@ -222,11 +259,11 @@ impl MenuSystem { menu_screens: MenuScreens { lobby_menu_screen: LobbyMenuScreen, main_menu_screen: MainMenuScreen, - multiplayer_room_menu_screen: MultiplayerRoomMenuScreen, + multiplayer_room_menu_screen: MultiplayerRoomMenuScreen::new(), restart_menu_screen: RestartMenuScreen, + hidden_menu_screen: HiddenMenuScreen, }, - elements_to_hide: Vec::new(), - elements_to_show: Vec::new(), + modal_window_id: None, mouse_reactive: vec![ UI_SINGLE_PLAYER_BUTTON, UI_MULTIPLAYER_BUTTON, @@ -240,12 +277,13 @@ impl MenuSystem { UI_LOBBY_JOIN_BUTTON, UI_MP_ROOM_START_BUTTON, UI_MP_ROOM_LOBBY_BUTTON, + UI_MP_ROOM_PLAYER1_KICK, UI_MP_ROOM_PLAYER2_KICK, UI_MP_ROOM_PLAYER3_KICK, UI_MP_ROOM_PLAYER4_KICK, + UI_MODAL_CONFIRM_BUTTON, ], - is_transitioning: false, - transition_began_at: Duration::new(0, 0), + menu_screen_animations: VecDeque::new(), event_readers: None, menu_screen: GameMenuScreen::Loading, transition_state: TransitionState::Still, @@ -269,12 +307,41 @@ enum GameMenuScreen { Hidden, } +#[derive(Debug)] enum TransitionState { FadeIn, FadeOut, Still, } +#[derive(Clone, Copy)] +struct AlphaTransition { + normal_alpha: f32, + current_step: f32, + is_fade_in: bool, +} + +impl AlphaTransition { + fn modify(self, current_alpha: &mut [f32; 4]) -> bool { + let goal = if self.is_fade_in { + self.normal_alpha + } else { + 0.0 + }; + if (current_alpha[3] - goal).abs() < 0.0001 { + return false; + } + current_alpha[3] = if self.is_fade_in { + let new_alpha = self.normal_alpha * self.current_step; + num::Float::max(new_alpha, current_alpha[3]) + } else { + let new_alpha = self.normal_alpha * (1.0 - self.current_step); + num::Float::min(new_alpha, current_alpha[3]) + }; + true + } +} + impl<'s> System<'s> for MenuSystem { type SystemData = MenuSystemData<'s>; @@ -289,10 +356,30 @@ impl<'s> System<'s> for MenuSystem { .get_or_insert_with(|| system_data.ui_events.register_reader()); let mut button_pressed = None; + let mut value_changed = None; for event in system_data.ui_events.read(event_readers) { - if let UiEventType::Click = event.event_type { - button_pressed = system_data.ui_finder.get_id_by_entity(event.target); - } + let target_id = system_data.ui_finder.get_id_by_entity(event.target); + log::trace!("{:?}: {:?}", target_id, event); + + match &event.event_type { + UiEventType::Click => { + button_pressed = target_id; + // Prevent double-clicking. + system_data.ui_interactables.remove(event.target); + } + UiEventType::ValueChange => { + let text_field_id = system_data + .ui_finder + .get_id_by_entity(event.target) + .expect("Expected an edited text field"); + let new_value = system_data + .ui_finder + .get_ui_text(&system_data.ui_texts, &text_field_id) + .expect("Expected an edited text field"); + value_changed = Some((text_field_id, new_value.clone())); + } + _ => {} + }; } if let Some(ui_loading) = system_data.ui_finder.find(UI_LOADING_LABEL) { @@ -304,104 +391,212 @@ impl<'s> System<'s> for MenuSystem { self.run_fade_animation(&mut system_data, now); + // Pass the events to the active menu screen handler. let state_update = match (&*system_data.game_engine_state, self.menu_screen) { (GameEngineState::Menu, GameMenuScreen::Loading) => { StateUpdate::new_menu_screen(GameMenuScreen::LobbyMenu) } (GameEngineState::Menu, menu_screen) => { - let menu_screen = self.menu_screens.menu_screen(menu_screen).unwrap(); - menu_screen.process_events( + let menu_screen = self + .menu_screens + .menu_screen(menu_screen) + .expect("Expected a menu screen for GameEngineState::Menu"); + if let Some((text_field_id, value_changed)) = value_changed { + menu_screen.value_changed(&mut system_data, &text_field_id, &value_changed); + } + menu_screen.update( &mut system_data, button_pressed.as_ref().map(std::string::String::as_str), + self.modal_window_id + .as_ref() + .map(std::string::String::as_str), ) } (GameEngineState::Playing, menu_screen) if menu_screen != GameMenuScreen::Hidden => { StateUpdate::new_menu_screen(GameMenuScreen::Hidden) } - (GameEngineState::Playing, _) if system_data.game_level_state.is_over => StateUpdate { - game_engine_state: Some(GameEngineState::Menu), - menu_screen: Some(GameMenuScreen::RestartMenu), - }, - _ => StateUpdate::none(), + (GameEngineState::Playing, _) => self.menu_screens.hidden_menu_screen.update( + &mut system_data, + button_pressed.as_ref().map(std::string::String::as_str), + self.modal_window_id + .as_ref() + .map(std::string::String::as_str), + ), + _ => StateUpdate::None, }; - if let Some(new_game_engine_state) = state_update.game_engine_state { - *system_data.new_game_engine_state = NewGameEngineState(new_game_engine_state); - } - if let Some(new_menu_screen) = state_update.menu_screen { - let current_menu_screen = self.menu_screen; - let elements_to_hide = if let GameMenuScreen::Loading = current_menu_screen { - vec![UI_LOADING_LABEL] - } else { - self.menu_screens - .menu_screen(current_menu_screen) - .map(|menu_screen| { - let elements = menu_screen.elements_to_hide(&system_data); - if let GameMenuScreen::Hidden = new_menu_screen { - with_background(&elements) - } else { - elements - } - }) - .unwrap_or_default() - }; - let elements_to_show = self - .menu_screens - .menu_screen(new_menu_screen) - .map(|menu_screen| { - let elements = menu_screen.elements_to_show(&system_data); - if let GameMenuScreen::Hidden = current_menu_screen { - with_background(&elements) + let (change_modal_title, mut elements_to_hide, elements_to_show) = match state_update { + StateUpdate::GameMenuUpdate { + game_engine_state, + menu_screen, + } => { + if let Some(new_game_engine_state) = game_engine_state { + *system_data.new_game_engine_state = NewGameEngineState(new_game_engine_state); + } + if let Some(new_menu_screen) = menu_screen { + let current_menu_screen = self.menu_screen; + let mut elements_to_hide = if let GameMenuScreen::Loading = current_menu_screen + { + vec![UI_LOADING_LABEL] } else { - elements + self.menu_screens + .menu_screen(current_menu_screen) + .map(|menu_screen| { + let elements = menu_screen.elements_to_hide(&system_data); + if let GameMenuScreen::Hidden = new_menu_screen { + with_background(&elements) + } else { + elements + } + }) + .unwrap_or_default() + }; + let elements_to_show = self + .menu_screens + .menu_screen(new_menu_screen) + .map(|menu_screen| { + let elements = menu_screen.elements_to_show(&system_data); + if let GameMenuScreen::Hidden = current_menu_screen { + with_background(&elements) + } else { + elements + } + }) + .unwrap_or_default(); + self.menu_screen = new_menu_screen; + if let Some(menu_screen) = self.menu_screens.menu_screen(new_menu_screen) { + menu_screen.show(&mut system_data); } - }) - .unwrap_or_default(); - self.menu_screen = new_menu_screen; - self.set_fade_animation(now, elements_to_hide, elements_to_show); + self.modal_window_id = None; + elements_to_hide.append(&mut modal_window_with_confirmation()); + (None, elements_to_hide, elements_to_show) + } else { + (None, vec![], vec![]) + } + } + StateUpdate::ShowModalWindow { + id, + title, + show_confirmation, + } => { + log::info!( + "Show modal window {}, show confirmation: {}", + id, + show_confirmation + ); + self.modal_window_id = Some(id); + if show_confirmation { + (Some(title), vec![], modal_window_with_confirmation()) + } else { + (Some(title), vec![UI_MODAL_CONFIRM_BUTTON], modal_window()) + } + } + StateUpdate::CustomAnimation { + elements_to_hide, + elements_to_show, + } => (None, elements_to_hide, elements_to_show), + StateUpdate::None => (None, vec![], vec![]), + }; + + if self.modal_window_id.is_some() { + if let Some(UI_MODAL_CONFIRM_BUTTON) = + button_pressed.as_ref().map(std::string::String::as_str) + { + self.modal_window_id = None; + elements_to_hide.append(&mut modal_window_with_confirmation()); + } + } + + if !elements_to_show.is_empty() || !elements_to_hide.is_empty() { + self.add_fade_animation(change_modal_title, elements_to_hide, elements_to_show); } } } impl MenuSystem { - fn set_fade_animation( + fn add_fade_animation( &mut self, - begin_time: Duration, + change_modal_title: Option, elements_to_hide: Vec<&'static str>, elements_to_show: Vec<&'static str>, ) { - if let TransitionState::Still = self.transition_state { - } else { - panic!("Transition state must be Still before new transition"); - } - - if !elements_to_hide.is_empty() { - self.transition_state = TransitionState::FadeOut; - } else if !elements_to_show.is_empty() { - self.transition_state = TransitionState::FadeIn; - } else { - panic!("There's no elements to show or hide"); - } - - self.transition_began_at = begin_time; - self.elements_to_hide = elements_to_hide; - self.elements_to_show = elements_to_show; - self.is_transitioning = true; + self.menu_screen_animations.push_back(MenuScreenAnimation { + change_modal_title, + started_at: None, + elements_to_hide, + elements_to_show, + }) } + #[allow(clippy::cognitive_complexity)] fn run_fade_animation( &mut self, system_data: &mut ::SystemData, now: Duration, ) { + let menu_screen_animation = self.menu_screen_animations.get_mut(0); + if menu_screen_animation.is_none() { + return; + } + let menu_screen_animation = menu_screen_animation.unwrap(); + + if let Some(change_modal_title) = menu_screen_animation.change_modal_title.clone() { + *system_data + .ui_finder + .get_ui_text_mut(&mut system_data.ui_texts, UI_MODAL_TITLE) + .unwrap() = change_modal_title; + } + + if menu_screen_animation.started_at.is_none() { + if let TransitionState::Still = self.transition_state { + } else { + panic!("Transition state must be Still before starting a new transition"); + } + if !menu_screen_animation.elements_to_hide.is_empty() { + self.transition_state = TransitionState::FadeOut; + } else if !menu_screen_animation.elements_to_show.is_empty() { + self.transition_state = TransitionState::FadeIn; + } else { + panic!("There's no elements to show or hide"); + } + log::debug!( + "Starting a new menu screen animation at {}s ({:?}): {:?}", + now.as_secs_f32(), + self.transition_state, + menu_screen_animation + ); + menu_screen_animation.started_at = Some(now); + } + let started_at = menu_screen_animation.started_at.unwrap(); + let transition_completed = - (now - self.transition_began_at).as_millis() as f32 / MENU_FADE_OUT_DURATION_MS as f32; + (now - started_at).as_millis() as f32 / MENU_FADE_OUT_DURATION_MS as f32; match self.transition_state { TransitionState::FadeOut => { - let new_alpha = num::Float::max(0.0, 1.0 - transition_completed); - - for element_to_hide in &self.elements_to_hide { + let mut modified = 0; + for element_to_hide in &menu_screen_animation.elements_to_hide { + let alpha_transition = if element_to_hide.contains(MODAL_TAG) { + if *element_to_hide == UI_MODAL_BACKDROP_CONTAINER { + AlphaTransition { + normal_alpha: 0.7, + current_step: transition_completed, + is_fade_in: false, + } + } else { + AlphaTransition { + normal_alpha: 1.0, + current_step: 1.0, + is_fade_in: false, + } + } + } else { + AlphaTransition { + normal_alpha: 1.0, + current_step: transition_completed, + is_fade_in: false, + } + }; let ui_entity = system_data .ui_finder .find_with_mut_transform(element_to_hide); @@ -409,11 +604,16 @@ impl MenuSystem { let (ui_entity, ui_transform) = if let Some(ui_entity) = ui_entity { ui_entity } else { + log::warn!("Couldn't find a UI entity: {}", element_to_hide); continue; }; if !is_container && !element_to_hide.contains(BACKGROUND_TAG) { ui_transform.local_z = 0.5; + } else if *element_to_hide == UI_MODAL_BACKDROP_CONTAINER + && transition_completed >= 1.0 + { + ui_transform.local_z = 100.0; } system_data.ui_interactables.remove(ui_entity); @@ -422,21 +622,17 @@ impl MenuSystem { } else { Some(&system_data.hierarchy) }; - if transition_completed > 1.0 { - if is_container { - system_data - .hidden - .insert(ui_entity, Hidden) - .expect("Expected to insert Hidden component"); - } else { - system_data - .hidden_propagates - .insert(ui_entity, HiddenPropagate) - .expect("Expected to insert HiddenPropagate component"); - } + if transition_completed >= 1.0 + || (element_to_hide.contains(MODAL_TAG) + && *element_to_hide != UI_MODAL_BACKDROP_CONTAINER) + { + system_data + .hidden_propagates + .insert(ui_entity, HiddenPropagate::new()) + .expect("Expected to insert HiddenPropagate component"); } else { - Self::set_alpha_for( - new_alpha, + modified += Self::set_alpha_for( + alpha_transition, ui_entity, &mut system_data.ui_texts, &mut system_data.ui_images, @@ -445,16 +641,42 @@ impl MenuSystem { } } - if transition_completed > 1.0 { - self.elements_to_hide.clear(); + if transition_completed >= 1.0 || (modified == 0 && transition_completed != 0.0) { + menu_screen_animation.elements_to_hide.clear(); self.transition_state = TransitionState::FadeIn; - self.transition_began_at = now; + menu_screen_animation.started_at = Some(now); + log::debug!( + "Starting a new menu screen animation at {}s ({:?}): {:?}", + now.as_secs_f32(), + self.transition_state, + menu_screen_animation + ); } } TransitionState::FadeIn => { - let new_alpha = num::Float::min(1.0, transition_completed); - - for element_to_show in &self.elements_to_show { + let mut modified = 0; + for element_to_show in &menu_screen_animation.elements_to_show { + let alpha_transition = if element_to_show.contains(MODAL_TAG) { + if *element_to_show == UI_MODAL_BACKDROP_CONTAINER { + AlphaTransition { + normal_alpha: 0.7, + current_step: transition_completed, + is_fade_in: true, + } + } else { + AlphaTransition { + normal_alpha: 1.0, + current_step: 1.0, + is_fade_in: true, + } + } + } else { + AlphaTransition { + normal_alpha: 1.0, + current_step: transition_completed, + is_fade_in: true, + } + }; let ui_entity = system_data .ui_finder .find_with_mut_transform(element_to_show); @@ -462,24 +684,27 @@ impl MenuSystem { let (ui_entity, ui_transform) = if let Some(ui_entity) = ui_entity { ui_entity } else { + log::warn!("Couldn't find a UI entity: {}", element_to_show); continue; }; + system_data.hidden_propagates.remove(ui_entity); let hierarchy = if is_container { - system_data.hidden.remove(ui_entity); None } else { - system_data.hidden_propagates.remove(ui_entity); Some(&system_data.hierarchy) }; - Self::set_alpha_for( - new_alpha, + modified += Self::set_alpha_for( + alpha_transition, ui_entity, &mut system_data.ui_texts, &mut system_data.ui_images, hierarchy, ); - if transition_completed > 1.0 { + if *element_to_show == UI_MODAL_BACKDROP_CONTAINER { + ui_transform.local_z = 150.0; + } + if transition_completed >= 1.0 { if !is_container && !element_to_show.contains(BACKGROUND_TAG) { ui_transform.local_z = 1.0; } @@ -492,41 +717,49 @@ impl MenuSystem { } } - if transition_completed > 1.0 { - self.elements_to_show.clear(); + if transition_completed >= 1.0 || (modified == 0 && transition_completed != 0.0) { + menu_screen_animation.elements_to_show.clear(); self.transition_state = TransitionState::Still; } } TransitionState::Still => {} } - if transition_completed > 1.0 - && self.elements_to_hide.is_empty() - && self.elements_to_show.is_empty() + if menu_screen_animation.elements_to_hide.is_empty() + && menu_screen_animation.elements_to_show.is_empty() { self.transition_state = TransitionState::Still; - self.is_transitioning = false; + self.menu_screen_animations.pop_front(); } } fn set_alpha_for( - new_alpha: f32, + new_alpha: AlphaTransition, ui_entity: Entity, ui_texts: &mut WriteStorage, ui_images: &mut WriteStorage, hierarchy: Option<&ReadExpect>, - ) { + ) -> u16 { + let mut modified = 0; if let Some(ui_text) = ui_texts.get_mut(ui_entity) { - ui_text.color[3] = new_alpha; + modified += new_alpha.modify(&mut ui_text.color) as u16; } else if let Some(UiImage::SolidColor(ref mut color)) = ui_images.get_mut(ui_entity) { - color[3] = new_alpha; + modified += new_alpha.modify(color) as u16; } if let Some(hierarchy) = hierarchy { for ui_entity in hierarchy.children(ui_entity) { - Self::set_alpha_for(new_alpha, *ui_entity, ui_texts, ui_images, Some(hierarchy)) + modified += Self::set_alpha_for( + new_alpha, + *ui_entity, + ui_texts, + ui_images, + Some(hierarchy), + ); } } + + modified } } @@ -535,3 +768,13 @@ fn with_background(menu_elements: &[MenuElement]) -> Vec { elements.push(UI_MAIN_CONTAINER); elements } + +fn modal_window() -> Vec { + MODAL_WINDOW_ELEMENTS.to_vec() +} + +fn modal_window_with_confirmation() -> Vec { + let mut elements = MODAL_WINDOW_ELEMENTS.to_vec(); + elements.push(UI_MODAL_CONFIRM_BUTTON); + elements +} diff --git a/bins/client/src/ecs/systems/menu/multiplayer_room.rs b/bins/client/src/ecs/systems/menu/multiplayer_room.rs index f896f50..4f2ac03 100644 --- a/bins/client/src/ecs/systems/menu/multiplayer_room.rs +++ b/bins/client/src/ecs/systems/menu/multiplayer_room.rs @@ -1,4 +1,11 @@ +use gv_client_shared::ecs::resources::ConnectionStatus; + use super::*; +use crate::{ecs::resources::UiNetworkCommand, utils::ui::disconnect_reason_title}; +use gv_core::ecs::resources::net::MultiplayerRoomPlayer; + +const DISCONNECTED: &str = "MP_DISCONNECTED"; +const DISCONNECTING: &str = "MP_DISCONNECTING"; lazy_static! { static ref MP_ROOM_MENU_ELEMENTS_HOST: &'static [&'static str] = &[ @@ -26,7 +33,19 @@ lazy_static! { ]; } -pub struct MultiplayerRoomMenuScreen; +pub struct MultiplayerRoomMenuScreen { + initiated_disconnecting: bool, + players: Vec, +} + +impl MultiplayerRoomMenuScreen { + pub fn new() -> Self { + Self { + initiated_disconnecting: false, + players: Vec::new(), + } + } +} impl MenuScreen for MultiplayerRoomMenuScreen { fn elements_to_show(&self, system_data: &MenuSystemData) -> Vec { @@ -45,107 +64,162 @@ impl MenuScreen for MultiplayerRoomMenuScreen { UI_MP_ROOM_PLAYER1_BG, UI_MP_ROOM_PLAYER1_NUMBER, UI_MP_ROOM_PLAYER1_NICKNAME, + UI_MP_ROOM_PLAYER1_KICK, UI_MP_ROOM_PLAYER2_CONTAINER, UI_MP_ROOM_PLAYER2_BG, UI_MP_ROOM_PLAYER2_NUMBER, UI_MP_ROOM_PLAYER2_NICKNAME, - // UI_MP_ROOM_PLAYER2_KICK, + UI_MP_ROOM_PLAYER2_KICK, UI_MP_ROOM_PLAYER3_CONTAINER, UI_MP_ROOM_PLAYER3_BG, UI_MP_ROOM_PLAYER3_NUMBER, UI_MP_ROOM_PLAYER3_NICKNAME, - // UI_MP_ROOM_PLAYER3_KICK, + UI_MP_ROOM_PLAYER3_KICK, UI_MP_ROOM_PLAYER4_CONTAINER, UI_MP_ROOM_PLAYER4_BG, UI_MP_ROOM_PLAYER4_NUMBER, UI_MP_ROOM_PLAYER4_NICKNAME, - // UI_MP_ROOM_PLAYER4_KICK, + UI_MP_ROOM_PLAYER4_KICK, ] } - fn process_events( + fn show(&mut self, _system_data: &mut MenuSystemData) { + *self = Self::new(); + } + + fn update( &mut self, system_data: &mut MenuSystemData, button_pressed: Option<&str>, + modal_window_id: Option<&str>, ) -> StateUpdate { - match button_pressed { - Some(UI_MP_ROOM_LOBBY_BUTTON) => { - StateUpdate::new_menu_screen(GameMenuScreen::LobbyMenu) + let disconnected_modal_window_is_shown = + modal_window_id.map_or(false, |modal_window_id| modal_window_id == DISCONNECTED); + if !disconnected_modal_window_is_shown { + match system_data.multiplayer_room_state.connection_status { + ConnectionStatus::ConnectionFailed(ref error) => { + return StateUpdate::ShowModalWindow { + id: DISCONNECTED.to_owned(), + title: error + .as_ref() + .map(|error| format!("Disconnected: {:?}", error)) + .unwrap_or_else(|| "Disconnected".to_owned()), + show_confirmation: true, + } + } + ConnectionStatus::Disconnected(disconnect_reason) => { + if self.initiated_disconnecting { + self.initiated_disconnecting = false; + system_data.ui_network_command.command = Some(UiNetworkCommand::Reset); + return StateUpdate::new_menu_screen(GameMenuScreen::LobbyMenu); + } else { + return StateUpdate::ShowModalWindow { + id: DISCONNECTED.to_owned(), + title: disconnect_reason_title(disconnect_reason), + show_confirmation: true, + }; + } + } + _ => {} + }; + } + + match (button_pressed, modal_window_id) { + (Some(UI_MP_ROOM_PLAYER1_KICK), _) => { + system_data.ui_network_command.command = + Some(UiNetworkCommand::Kick { player_number: 0 }); + StateUpdate::None + } + (Some(UI_MP_ROOM_PLAYER2_KICK), _) => { + system_data.ui_network_command.command = + Some(UiNetworkCommand::Kick { player_number: 1 }); + StateUpdate::None } - Some(UI_MP_ROOM_START_BUTTON) => { - system_data.multiplayer_room_state.has_started = true; - StateUpdate::none() + (Some(UI_MP_ROOM_PLAYER3_KICK), _) => { + system_data.ui_network_command.command = + Some(UiNetworkCommand::Kick { player_number: 2 }); + StateUpdate::None } - _ => { - Self::update_players(system_data); - StateUpdate::none() + (Some(UI_MP_ROOM_PLAYER4_KICK), _) => { + system_data.ui_network_command.command = + Some(UiNetworkCommand::Kick { player_number: 3 }); + StateUpdate::None } + (Some(UI_MP_ROOM_LOBBY_BUTTON), _) => { + self.initiated_disconnecting = true; + system_data.ui_network_command.command = Some(UiNetworkCommand::Leave); + + if system_data.multiplayer_room_state.is_host { + StateUpdate::ShowModalWindow { + id: DISCONNECTING.to_owned(), + title: "Shutting down the server...".to_owned(), + show_confirmation: false, + } + } else { + StateUpdate::ShowModalWindow { + id: DISCONNECTING.to_owned(), + title: "Disconnecting...".to_owned(), + show_confirmation: false, + } + } + } + (Some(UI_MP_ROOM_START_BUTTON), _) => { + system_data.ui_network_command.command = Some(UiNetworkCommand::Start); + StateUpdate::None + } + (Some(UI_MODAL_CONFIRM_BUTTON), Some(DISCONNECTED)) => { + system_data.ui_network_command.command = Some(UiNetworkCommand::Reset); + StateUpdate::new_menu_screen(GameMenuScreen::LobbyMenu) + } + _ => self.update_players(system_data), } } } impl MultiplayerRoomMenuScreen { - fn update_players(system_data: &mut MenuSystemData) { - if let Some(players) = system_data.multiplayer_game_state.read_updated_players() { + fn update_players(&mut self, system_data: &mut MenuSystemData) -> StateUpdate { + let mut elements_to_hide = Vec::new(); + let mut elements_to_show = Vec::new(); + + if self.players != system_data.multiplayer_game_state.players { + self.players = system_data.multiplayer_game_state.players.clone(); + #[rustfmt::skip] let rows = [ - (UI_MP_ROOM_PLAYER1_NUMBER, UI_MP_ROOM_PLAYER1_NICKNAME), - (UI_MP_ROOM_PLAYER2_NUMBER, UI_MP_ROOM_PLAYER2_NICKNAME), - (UI_MP_ROOM_PLAYER3_NUMBER, UI_MP_ROOM_PLAYER3_NICKNAME), - (UI_MP_ROOM_PLAYER4_NUMBER, UI_MP_ROOM_PLAYER4_NICKNAME), + (UI_MP_ROOM_PLAYER1_NUMBER, UI_MP_ROOM_PLAYER1_NICKNAME, UI_MP_ROOM_PLAYER1_KICK), + (UI_MP_ROOM_PLAYER2_NUMBER, UI_MP_ROOM_PLAYER2_NICKNAME, UI_MP_ROOM_PLAYER2_KICK), + (UI_MP_ROOM_PLAYER3_NUMBER, UI_MP_ROOM_PLAYER3_NICKNAME, UI_MP_ROOM_PLAYER3_KICK), + (UI_MP_ROOM_PLAYER4_NUMBER, UI_MP_ROOM_PLAYER4_NICKNAME, UI_MP_ROOM_PLAYER4_KICK), ]; for (i, row) in rows.iter().enumerate() { { - let (ui_number_entity, ui_number_transform) = system_data - .ui_finder - .find_with_mut_transform(row.0) - .unwrap_or_else(|| { - panic!("Expected a player number UiTransform for row {}", i) - }); - let ui_number_text = system_data - .ui_texts - .get_mut(ui_number_entity) - .unwrap_or_else(|| panic!("Expected a player number UiText for row {}", i)); - if players.get(i).is_some() { - system_data.hidden_propagates.remove(ui_number_entity); - ui_number_transform.local_z = 1.0; - ui_number_text.color[3] = 1.0; - } else { - system_data - .hidden_propagates - .insert(ui_number_entity, HiddenPropagate) - .expect("Expected to insert Hidden component"); - ui_number_transform.local_z = 0.5; - ui_number_text.color[3] = 0.0; - } - } - - { - let (ui_text_entity, ui_text_transform) = system_data - .ui_finder - .find_with_mut_transform(row.1) - .unwrap_or_else(|| { - panic!("Expected a player nickname UiTransform for row {}", i) - }); - let ui_nickname_text = system_data - .ui_texts - .get_mut(ui_text_entity) - .unwrap_or_else(|| panic!("Expected a player number UiText for row {}", i)); + if let Some(player) = self.players.get(i) { + let player_nickname_text = system_data + .ui_finder + .get_ui_text_mut(&mut system_data.ui_texts, row.1) + .expect("Expected a player nickname text component"); + *player_nickname_text = player.nickname.clone(); - if let Some(player) = players.get(i) { - system_data.hidden_propagates.remove(ui_text_entity); - ui_text_transform.local_z = 1.0; - ui_nickname_text.color[3] = 1.0; - ui_nickname_text.text = player.nickname.clone(); + elements_to_show.push(row.0); + elements_to_show.push(row.1); + if system_data.multiplayer_room_state.is_host && !player.is_host { + elements_to_show.push(row.2); + } } else { - system_data - .hidden_propagates - .insert(ui_text_entity, HiddenPropagate) - .expect("Expected to insert Hidden component"); - ui_text_transform.local_z = 0.5; - ui_nickname_text.color[3] = 0.0; + elements_to_hide.push(row.0); + elements_to_hide.push(row.1); + elements_to_hide.push(row.2); } } } } + + if elements_to_hide.is_empty() && elements_to_show.is_empty() { + StateUpdate::None + } else { + StateUpdate::CustomAnimation { + elements_to_hide, + elements_to_show, + } + } } } diff --git a/bins/client/src/ecs/systems/menu/restart.rs b/bins/client/src/ecs/systems/menu/restart.rs index 82b767d..020b12d 100644 --- a/bins/client/src/ecs/systems/menu/restart.rs +++ b/bins/client/src/ecs/systems/menu/restart.rs @@ -7,15 +7,16 @@ impl MenuScreen for RestartMenuScreen { vec![UI_RESTART_BUTTON, UI_MAIN_MENU_BUTTON] } - fn process_events( + fn update( &mut self, _system_data: &mut MenuSystemData, button_pressed: Option<&str>, + _modal_window_id: Option<&str>, ) -> StateUpdate { match button_pressed { Some(UI_RESTART_BUTTON) => StateUpdate::new_game_engine_state(GameEngineState::Playing), Some(UI_MAIN_MENU_BUTTON) => StateUpdate::new_menu_screen(GameMenuScreen::MainMenu), - _ => StateUpdate::none(), + _ => StateUpdate::None, } } } diff --git a/bins/client/src/main.rs b/bins/client/src/main.rs index 72e7a4f..e6e3fcd 100644 --- a/bins/client/src/main.rs +++ b/bins/client/src/main.rs @@ -1,4 +1,5 @@ #![feature(clamp)] +#![feature(or_patterns)] #![allow(clippy::too_many_arguments, clippy::type_complexity)] mod ecs; @@ -20,10 +21,10 @@ use amethyst::{ RenderingBundle, SpriteRender, }, ui::{RenderUi, UiBundle}, - LogLevelFilter, Logger, + Logger, LoggerConfig, }; -use std::{env, time::Duration}; +use std::env; use gv_animation_prefabs::{AnimationId, GameSpriteAnimationPrefab}; use gv_client_shared::{ecs::resources::MultiplayerRoomState, settings::Settings}; @@ -38,7 +39,7 @@ use gv_game::{ use crate::{ ecs::{ - resources::{LastAcknowledgedUpdate, ServerCommand}, + resources::{LastAcknowledgedUpdate, ServerCommand, UiNetworkCommandResource}, systems::*, }, rendering::*, @@ -64,17 +65,23 @@ fn main() -> amethyst::Result<()> { let socket_addr = "0.0.0.0:0"; - Logger::from_config(Default::default()) - .level_for("amethyst_assets", LogLevelFilter::Info) - .level_for("gfx_backend_vulkan", LogLevelFilter::Warn) - .level_for("gv_game::ecs::systems", LogLevelFilter::Debug) - .level_for( - "gv_game::ecs::systems::net_connection_manager", - LogLevelFilter::Info, - ) - .level_for("gv_game::utils::net", LogLevelFilter::Info) - .level_for("gv_client", LogLevelFilter::Debug) - .start(); + let logging_config: LoggerConfig = ::std::fs::read_to_string("client_logging_config.toml") + .map_err(|err| { + println!( + "Failed to read client_logging_config.toml, using the defaults: {:?}", + err + ) + }) + .and_then(|config_contents| { + toml::from_str(&config_contents).map_err(|err| { + println!( + "Failed to read client_logging_config.toml, using the defaults: {:?}", + err + ); + }) + }) + .unwrap_or_default(); + Logger::from_config(logging_config).start(); let settings = Settings::new()?; let display_config = settings.display().clone(); @@ -85,6 +92,9 @@ fn main() -> amethyst::Result<()> { let mut builder = Application::build("./", LoadingState::default())?; builder.world.insert(settings); builder.world.insert(ServerCommand::new()); + + // The resources which we need to remember to reset on starting a game. + builder.world.insert(UiNetworkCommandResource::default()); builder.world.insert(MultiplayerRoomState::new()); builder.world.insert(ClientWorldUpdates::default()); builder.world.insert(LastAcknowledgedUpdate { @@ -176,10 +186,7 @@ fn main() -> amethyst::Result<()> { )?; let mut game = builder - .with_frame_limit( - FrameRateLimitStrategy::SleepAndYield(Duration::from_millis(2)), - 60, - ) + .with_frame_limit(FrameRateLimitStrategy::Yield, 60) .build(game_data_builder)?; game.run(); diff --git a/bins/client/src/utils/mod.rs b/bins/client/src/utils/mod.rs index b84959a..9bbc968 100644 --- a/bins/client/src/utils/mod.rs +++ b/bins/client/src/utils/mod.rs @@ -1 +1,2 @@ pub mod camera; +pub mod ui; diff --git a/bins/client/src/utils/ui.rs b/bins/client/src/utils/ui.rs new file mode 100644 index 0000000..e9c1370 --- /dev/null +++ b/bins/client/src/utils/ui.rs @@ -0,0 +1,14 @@ +use gv_core::net::server_message::DisconnectReason; + +pub fn disconnect_reason_title(disconnect_reason: DisconnectReason) -> String { + match disconnect_reason { + DisconnectReason::Uninitialized => "The server is not initialized yet".to_owned(), + DisconnectReason::GameIsStarted => "The server has already started the game".to_owned(), + DisconnectReason::RoomIsFull => "The room is full".to_owned(), + DisconnectReason::Kick => "You've been kicked".to_owned(), + DisconnectReason::Closed => "The host has closed the server".to_owned(), + DisconnectReason::ServerCrashed(exit_code) => { + format!("The server unexpectedly closed: {}", exit_code) + } + } +} diff --git a/bins/server/Cargo.toml b/bins/server/Cargo.toml index 9cfb5ed..61ff2f3 100644 --- a/bins/server/Cargo.toml +++ b/bins/server/Cargo.toml @@ -15,6 +15,7 @@ failure = "0.1.5" fern = "0.5.8" lazy_static = "1.3.0" log = "0.4.6" +toml = "0.5.6" [dependencies.gv_animation_prefabs] path = "../../libs/animation_prefabs" @@ -28,4 +29,4 @@ features = [] [dependencies.amethyst] git = "https://github.com/amethyst/amethyst" -features = ["nightly", "empty"] +features = ["empty"] diff --git a/bins/server/src/ecs/systems/game_updates_broadcasting.rs b/bins/server/src/ecs/systems/game_updates_broadcasting.rs index 5a6add7..343ce9f 100644 --- a/bins/server/src/ecs/systems/game_updates_broadcasting.rs +++ b/bins/server/src/ecs/systems/game_updates_broadcasting.rs @@ -100,7 +100,7 @@ impl<'s> System<'s> for GameUpdatesBroadcastingSystem { send_message_unreliable( &mut transport, &net_connection_model, - &ServerMessagePayload::UpdateWorld { + ServerMessagePayload::UpdateWorld { id: latest_update_number, updates, }, diff --git a/bins/server/src/ecs/systems/server_network.rs b/bins/server/src/ecs/systems/server_network.rs index 87af413..26c3be7 100644 --- a/bins/server/src/ecs/systems/server_network.rs +++ b/bins/server/src/ecs/systems/server_network.rs @@ -15,42 +15,48 @@ use gv_core::{ world::{ FramedUpdates, ImmediatePlayerActionsUpdates, PlayerLookActionUpdates, ReceivedClientActionUpdates, ServerWorldUpdates, LAG_COMPENSATION_FRAMES_LIMIT, + PAUSE_FRAME_THRESHOLD, }, GameEngineState, NewGameEngineState, }, system_data::time::GameTimeService, }, net::{ - client_message::ClientMessagePayload, server_message::ServerMessagePayload, NetEvent, - NetIdentifier, NetUpdate, INTERPOLATION_FRAME_DELAY, + client_message::{ClientMessage, ClientMessagePayload}, + server_message::{DisconnectReason, ServerMessagePayload}, + NetEvent, NetIdentifier, NetUpdate, INTERPOLATION_FRAME_DELAY, }, + PLAYER_COLORS, }; use gv_game::{ ecs::resources::ConnectionEvents, utils::net::{broadcast_message_reliable, send_message_reliable}, - PLAYER_COLORS, }; -use crate::ecs::resources::{HostClientAddress, LastBroadcastedFrame}; +use std::collections::HashSet; -// Pause the game if we have a client that hasn't responded for the last 180 frames (3 secs). -const PAUSE_FRAME_THRESHOLD: u64 = - (LAG_COMPENSATION_FRAMES_LIMIT + LAG_COMPENSATION_FRAMES_LIMIT / 2) as u64; +use crate::ecs::resources::{HostClientAddress, LastBroadcastedFrame}; const HEARTBEAT_FRAME_INTERVAL: u64 = 2; pub struct ServerNetworkSystem { - host_connection_id: NetIdentifier, + host_connection_id: Option, last_heartbeat_frame: u64, } impl ServerNetworkSystem { pub fn new() -> Self { Self { - host_connection_id: 0, + host_connection_id: None, last_heartbeat_frame: 0, } } + + fn is_host(&self, connection_id: NetIdentifier) -> bool { + self.host_connection_id.map_or(false, |host_connection_id| { + host_connection_id == connection_id + }) + } } impl<'s> System<'s> for ServerNetworkSystem { @@ -70,6 +76,7 @@ impl<'s> System<'s> for ServerNetworkSystem { Write<'s, TransportResource>, ); + #[allow(clippy::cognitive_complexity)] fn run( &mut self, ( @@ -89,13 +96,13 @@ impl<'s> System<'s> for ServerNetworkSystem { ): Self::SystemData, ) { if let Some(host_client_address) = host_client_address.0.take() { - let net_connection_model = NetConnectionModel::new(0, host_client_address); - self.host_connection_id = 0; + let net_connection_model = NetConnectionModel::new(0, 0, host_client_address); + self.host_connection_id = Some(0); log::info!("Sending a Handshake message to a hosting client"); send_message_reliable( &mut transport, &net_connection_model, - &ServerMessagePayload::Handshake { + ServerMessagePayload::Handshake { net_id: 0, is_host: true, }, @@ -106,134 +113,338 @@ impl<'s> System<'s> for ServerNetworkSystem { .build(); } + let mut host_disconnected = false; + let mut kicked_players = HashSet::new(); + for connection_event in connection_events.0.drain(..) { let connection_id = connection_event.connection_id; - match connection_event.event { - NetEvent::Connected => { - // TODO: we'll need a more reliable way to determine the host in future. - if multiplayer_game_state.players.is_empty() { - self.host_connection_id = connection_id; + let net_connection_model = (&mut net_connection_models) + .join() + .find(|net_connection_model| net_connection_model.id == connection_id) + .expect("Expected to find a NetConnection"); + + // Handle ignoring outdated messages or setting a new session_id. + if let NetEvent::Message(ClientMessage { + session_id, + payload, + }) = &connection_event.event + { + if *session_id < net_connection_model.session_id { + log::warn!("Ignoring a message with session id {} from a connection {} with session id {}", session_id, net_connection_model.id, net_connection_model.session_id); + continue; + } else if let ClientMessagePayload::JoinRoom { sent_at, .. } = payload { + if net_connection_model.session_created_at < *sent_at { + net_connection_model.session_id = *session_id; + net_connection_model.session_created_at = *sent_at; + // It might be the case that a player reconnects before the connection model + // entity is dropped, so we need to change this flag manually for previously + // existed connections. + net_connection_model.disconnected = false; } - - log::info!("Sending a Handshake message: {}", connection_id); - let net_connection = (&mut net_connection_models) - .join() - .find(|net_connection_model| net_connection_model.id == connection_id) - .expect("Expected to find a NetConnection"); - send_message_reliable( - &mut transport, - net_connection, - &ServerMessagePayload::Handshake { - net_id: connection_id, - is_host: multiplayer_game_state.players.is_empty(), - }, - ); } - NetEvent::Message(ClientMessagePayload::JoinRoom { nickname }) => { - log::info!( - "A client ({}) has joined the room: {}", - connection_id, - nickname - ); - if multiplayer_game_state - .players - .iter() - .any(|player| player.connection_id == connection_id) - { - log::warn!( - "A client has sent a JoinRoom message more than once, discarding" - ); - } else { - let color_index = multiplayer_game_state.players.len(); - multiplayer_game_state - .update_players() - .push(MultiplayerRoomPlayer { + } + + // Handle ignoring messages if the game is already started. + if multiplayer_game_state.is_playing { + if let NetEvent::Message(ClientMessage { + session_id: _, + payload, + }) = &connection_event.event + { + let is_ignored = match payload { + ClientMessagePayload::JoinRoom { .. } => { + let player_is_in_game = multiplayer_game_state + .players + .iter() + .any(|player| player.connection_id == connection_id); + if !player_is_in_game { + log::warn!( + "A new client ({}) {} tried to connect while the game has already started", + connection_id, + net_connection_model.addr + ); + send_message_reliable( + &mut transport, + net_connection_model, + ServerMessagePayload::Disconnect( + DisconnectReason::GameIsStarted, + ), + ); + net_connection_model.disconnected = true; + } + true + } + + ClientMessagePayload::StartHostedGame => { + log::warn!( + "A client ({}) {} tried to start the game while it's already started", connection_id, - entity_net_id: 0, - nickname, - is_host: self.host_connection_id == connection_id, - color: PLAYER_COLORS[color_index], - }); + net_connection_model.addr + ); + true + } + + _ => false, + }; + + if is_ignored { + continue; } } - NetEvent::Message(ClientMessagePayload::StartHostedGame) - if connection_id == self.host_connection_id - && !multiplayer_game_state.is_playing => + } + + // Handle ignoring messages if the game is not started. + if !multiplayer_game_state.is_playing { + if let NetEvent::Message(ClientMessage { + session_id: _, + payload, + }) = &connection_event.event { - multiplayer_game_state.is_playing = true; - new_game_engine_state.0 = GameEngineState::Playing; + let is_ignored = match payload { + ClientMessagePayload::AcknowledgeWorldUpdate(_) => true, + ClientMessagePayload::WalkActions(_) => true, + ClientMessagePayload::CastActions(_) => true, + ClientMessagePayload::LookActions(_) => true, + _ => false, + }; + + if is_ignored { + continue; + } } - NetEvent::Message(ClientMessagePayload::WalkActions(actions)) => { - log::trace!( - "Received WalkAction updates (frame {}): {:?}", - game_time_service.game_frame_number(), - actions - ); - let discarded_actions = add_walk_actions( - &mut *framed_updates, - actions, - game_time_service.game_frame_number(), - ); + } - if !discarded_actions.is_empty() { - log::trace!( - "{} walk actions have been discarded", - discarded_actions.len() + match connection_event.event { + NetEvent::Message(ClientMessage { + session_id: _, + payload, + }) => match payload { + ClientMessagePayload::JoinRoom { + nickname, + sent_at: _, + } => { + let is_host = if multiplayer_game_state.players.is_empty() { + if let Some(host_connection_id) = self.host_connection_id { + if host_connection_id != connection_id { + send_message_reliable( + &mut transport, + net_connection_model, + ServerMessagePayload::Disconnect( + DisconnectReason::Uninitialized, + ), + ); + net_connection_model.disconnected = true; + continue; + } + true + } else { + self.host_connection_id = Some(connection_id); + true + } + } else { + false + }; + + log::info!( + "A client ({}) has joined the room: {}", + connection_id, + nickname ); + if let Some(player) = multiplayer_game_state + .update_players() + .iter_mut() + .find(|player| player.connection_id == connection_id) + { + log::info!("The player already existed, updating the nickname only"); + player.nickname = nickname; + } else { + let new_player_count = multiplayer_game_state.players.len(); + if new_player_count >= 4 { + send_message_reliable( + &mut transport, + net_connection_model, + ServerMessagePayload::Disconnect(DisconnectReason::RoomIsFull), + ); + net_connection_model.disconnected = true; + continue; + } + + multiplayer_game_state + .update_players() + .push(MultiplayerRoomPlayer { + connection_id, + entity_net_id: 0, + nickname, + is_host: self.is_host(connection_id), + color: PLAYER_COLORS[new_player_count], + }); + } - let net_connection = (&mut net_connection_models) - .join() - .find(|net_connection_model| net_connection_model.id == connection_id) - .expect("Expected to find a NetConnection"); + log::info!("Sending a Handshake message: {}", connection_id); send_message_reliable( &mut transport, - net_connection, - &ServerMessagePayload::DiscardWalkActions(discarded_actions), + net_connection_model, + ServerMessagePayload::Handshake { + net_id: connection_id, + is_host, + }, ); } - } - NetEvent::Message(ClientMessagePayload::CastActions(actions)) => { - add_cast_actions( - &mut *framed_updates, - actions, - &mut *action_update_id_provider, - game_time_service.game_frame_number(), - ); - } - NetEvent::Message(ClientMessagePayload::LookActions(actions)) => { - add_look_actions( - &mut *framed_updates, - actions, - game_time_service.game_frame_number(), - ); - } - NetEvent::Message(ClientMessagePayload::AcknowledgeWorldUpdate(frame_number)) => { - let mut connection_model = (&mut net_connection_models) - .join() - .find(|model| model.id == connection_id) - .unwrap_or_else(|| { - panic!( - "Expected to find a connection model with id {}", - connection_id - ) - }); - connection_model.last_acknowledged_update = - Some(frame_number).max(connection_model.last_acknowledged_update); - } + + ClientMessagePayload::StartHostedGame + if self.is_host(connection_id) && !multiplayer_game_state.is_playing => + { + multiplayer_game_state.is_playing = true; + new_game_engine_state.0 = GameEngineState::Playing; + } + ClientMessagePayload::StartHostedGame => { + log::warn!( + "Received an unexpected StartHostedGame message (connection id: {})", + connection_id, + ); + } + + ClientMessagePayload::WalkActions(actions) => { + log::trace!( + "Received WalkAction updates (frame {}): {:?}", + game_time_service.game_frame_number(), + actions + ); + let discarded_actions = add_walk_actions( + &mut *framed_updates, + actions, + game_time_service.game_frame_number(), + ); + + if !discarded_actions.is_empty() { + log::trace!( + "{} walk actions have been discarded", + discarded_actions.len() + ); + send_message_reliable( + &mut transport, + net_connection_model, + ServerMessagePayload::DiscardWalkActions(discarded_actions), + ); + } + } + + ClientMessagePayload::CastActions(actions) => { + add_cast_actions( + &mut *framed_updates, + actions, + &mut *action_update_id_provider, + game_time_service.game_frame_number(), + ); + } + + ClientMessagePayload::LookActions(actions) => { + add_look_actions( + &mut *framed_updates, + actions, + game_time_service.game_frame_number(), + ); + } + + ClientMessagePayload::AcknowledgeWorldUpdate(frame_number) => { + net_connection_model.last_acknowledged_update = + Some(frame_number).max(net_connection_model.last_acknowledged_update); + } + + ClientMessagePayload::Kick { + kicked_connection_id, + } if self.is_host(connection_id) && !multiplayer_game_state.is_playing => { + if self.is_host(kicked_connection_id) { + log::warn!( + "Tried to kick the host (connection id: {})", + kicked_connection_id + ); + continue; + } + + let kicked_player_index = multiplayer_game_state + .players + .iter() + .position(|player| player.connection_id == kicked_connection_id); + if let Some(kicked_player_index) = kicked_player_index { + kicked_players.insert(kicked_player_index); + } else { + log::warn!( + "Tried to kick a player with an unknown connection id: {}", + kicked_connection_id + ); + } + } + ClientMessagePayload::Kick { .. } => { + log::warn!( + "Received an unexpected Kick message (connection id: {})", + connection_id + ); + } + + ClientMessagePayload::Disconnect => { + net_connection_model.disconnected = true; + if self.is_host(connection_id) { + host_disconnected = true; + } + } + + ClientMessagePayload::Heartbeat + | ClientMessagePayload::Ping(_) + | ClientMessagePayload::Pong { .. } => {} + }, + NetEvent::Disconnected => { - multiplayer_game_state - .update_players() - .retain(|player| player.connection_id == connection_id); + // We don't mark the net_connection_model as disconnected here, + // because it should already be done by NetConnectionManagerSystem. + if self.is_host(connection_id) { + host_disconnected = true; + } } + _ => {} } + + if net_connection_model.disconnected && !host_disconnected { + multiplayer_game_state.drop_player_by_connection_id(connection_id); + } + } + + for kicked_player_index in kicked_players.iter().cloned() { + let player_connection_id = + multiplayer_game_state.players[kicked_player_index].connection_id; + multiplayer_game_state.drop_player_by_index(kicked_player_index); + let net_connection_model = (&mut net_connection_models) + .join() + .find(|net_connection_model| net_connection_model.id == player_connection_id) + .expect("Expected a connection model of a kicked player"); + send_message_reliable( + &mut transport, + net_connection_model, + ServerMessagePayload::Disconnect(DisconnectReason::Kick), + ); + net_connection_model.disconnected = true; + } + + if host_disconnected { + log::info!("The host has disconnected. Shutting down the server..."); + broadcast_message_reliable( + &mut transport, + (&net_connection_models).join(), + ServerMessagePayload::Disconnect(DisconnectReason::Closed), + ); + for net_connection_model in (&mut net_connection_models).join() { + net_connection_model.disconnected = true; + } + *new_game_engine_state = NewGameEngineState::shutdown(); + return; } if let Some(players) = multiplayer_game_state.read_updated_players() { broadcast_message_reliable( &mut transport, (&net_connection_models).join(), - &ServerMessagePayload::UpdateRoomPlayers(players.to_owned()), + ServerMessagePayload::UpdateRoomPlayers(players.to_owned()), ); } @@ -244,7 +455,7 @@ impl<'s> System<'s> for ServerNetworkSystem { broadcast_message_reliable( &mut transport, (&net_connection_models).join(), - &ServerMessagePayload::Heartbeat, + ServerMessagePayload::Heartbeat, ); } @@ -252,6 +463,10 @@ impl<'s> System<'s> for ServerNetworkSystem { if *game_engine_state == GameEngineState::Playing && multiplayer_game_state.is_playing { let mut lagging_players = Vec::new(); for net_connection_model in (&net_connection_models).join() { + if net_connection_model.disconnected { + continue; + } + let frames_since_last_pong = game_time_service .engine_time() .frame_number() @@ -303,7 +518,7 @@ impl<'s> System<'s> for ServerNetworkSystem { broadcast_message_reliable( &mut transport, (&net_connection_models).join(), - &ServerMessagePayload::PauseWaitingForPlayers { + ServerMessagePayload::PauseWaitingForPlayers { id: multiplayer_game_state.waiting_for_players_pause_id, players: lagging_players, }, @@ -313,7 +528,7 @@ impl<'s> System<'s> for ServerNetworkSystem { broadcast_message_reliable( &mut transport, (&net_connection_models).join(), - &ServerMessagePayload::UnpauseWaitingForPlayers( + ServerMessagePayload::UnpauseWaitingForPlayers( multiplayer_game_state.waiting_for_players_pause_id, ), ); @@ -347,6 +562,13 @@ fn add_walk_actions( let mut discarded_actions = Vec::new(); let added_actions_frame_number = actions.frame_number; + + // Just ignore these updates, most probably these are lost packages from the previous game, + // or the client is just bonkers. + if added_actions_frame_number.saturating_sub(frame_number) > PAUSE_FRAME_THRESHOLD { + return Vec::new(); + } + let oldest_possible_frame = frame_number.saturating_sub(LAG_COMPENSATION_FRAMES_LIMIT as u64); let are_lag_compensated = added_actions_frame_number > oldest_possible_frame; let actual_frame = if are_lag_compensated { @@ -446,6 +668,16 @@ fn add_look_actions( .filter(|(_, updates)| !updates.is_empty()) .map(|(frame_number, _)| frame_number) .max_by(|prev_frame_number, next_frame_number| prev_frame_number.cmp(next_frame_number)); + + // Just ignore these updates, most probably these are lost packages from the previous game, + // or the client is just bonkers. + let is_outdated_update = frame_to_reserve.map_or(true, |frame_to_reserve| { + frame_to_reserve.saturating_sub(frame_number) > PAUSE_FRAME_THRESHOLD + }); + if is_outdated_update { + return; + } + if let Some(frame_to_reserve) = frame_to_reserve { framed_updates.reserve_updates(*frame_to_reserve); } @@ -510,6 +742,13 @@ fn add_cast_actions( frame_number: u64, ) { let added_actions_frame_number = actions.frame_number; + + // Just ignore these updates, most probably these are lost packages from the previous game, + // or the client is just bonkers. + if added_actions_frame_number.saturating_sub(frame_number) > PAUSE_FRAME_THRESHOLD { + return; + } + let oldest_possible_frame = frame_number.saturating_sub(LAG_COMPENSATION_FRAMES_LIMIT as u64); let are_lag_compensated = added_actions_frame_number > oldest_possible_frame; let actual_frame = if are_lag_compensated { diff --git a/bins/server/src/main.rs b/bins/server/src/main.rs index 9efc835..6fff9f4 100644 --- a/bins/server/src/main.rs +++ b/bins/server/src/main.rs @@ -6,11 +6,9 @@ use amethyst::{ core::{frame_limiter::FrameRateLimitStrategy, transform::TransformBundle}, network::simulation::laminar::{LaminarConfig, LaminarNetworkBundle, LaminarSocket}, prelude::{Application, GameDataBuilder, SystemDesc}, - LogLevelFilter, Logger, + Logger, LoggerConfig, }; -use std::time::Duration; - use gv_core::ecs::resources::world::{ DummyFramedUpdate, FramedUpdates, ReceivedClientActionUpdates, ServerWorldUpdates, }; @@ -59,7 +57,23 @@ fn main() -> amethyst::Result<()> { HostClientAddress(None) }; - Logger::from_config_formatter(Default::default(), |out, message, record| { + let logging_config: LoggerConfig = ::std::fs::read_to_string("server_logging_config.toml") + .map_err(|err| { + log::warn!( + "Failed to read server_logging_config.toml, using the defaults: {:?}", + err + ) + }) + .and_then(|config_contents| { + toml::from_str(&config_contents).map_err(|err| { + log::warn!( + "Failed to read server_logging_config.toml, using the defaults: {:?}", + err + ) + }) + }) + .unwrap_or_default(); + Logger::from_config_formatter(logging_config, |out, message, record| { out.finish(format_args!( "[{level}][SERVER][{target}] {message}", level = record.level(), @@ -67,14 +81,6 @@ fn main() -> amethyst::Result<()> { message = message, )) }) - .level_for("gfx_backend_vulkan", LogLevelFilter::Warn) - .level_for("gv_game::ecs::systems", LogLevelFilter::Debug) - .level_for( - "gv_game::ecs::systems::net_connection_manager", - LogLevelFilter::Info, - ) - .level_for("gv_game::utils::net", LogLevelFilter::Info) - .level_for("gv_server", LogLevelFilter::Debug) .start(); let mut builder = Application::build("./", LoadingState::default())?; @@ -117,12 +123,8 @@ fn main() -> amethyst::Result<()> { .with_bundle(TransformBundle::new().with_dep(&["world_position_transform_system"]))?; let mut game = builder - .with_frame_limit( - FrameRateLimitStrategy::SleepAndYield(Duration::from_millis(2)), - 60, - ) + .with_frame_limit(FrameRateLimitStrategy::Yield, 60) .build(game_data_builder)?; game.run(); - Ok(()) } diff --git a/client_logging_config.toml.example b/client_logging_config.toml.example new file mode 100644 index 0000000..754779b --- /dev/null +++ b/client_logging_config.toml.example @@ -0,0 +1,13 @@ +stdout = "Colored" +level_filter = "Info" +allow_env_override = true +log_gfx_backend_level = "Warn" +log_gfx_rendy_level = "Warn" +module_levels = [ + ["amethyst_assets", "Info"], + ["gfx_backend_vulkan", "Warn"], + ["gv_game::ecs::systems", "Debug"], + ["gv_game::ecs::systems::net_connection_manager", "Info"], + ["gv_game::utils::net", "Info"], + ["gv_client", "Debug"], +] diff --git a/libs/client_shared/src/ecs/factories.rs b/libs/client_shared/src/ecs/factories.rs index b4305ff..1af8d25 100644 --- a/libs/client_shared/src/ecs/factories.rs +++ b/libs/client_shared/src/ecs/factories.rs @@ -78,7 +78,7 @@ impl<'s> PlayerClientFactory<'s> { .insert(player_entity, PlayerColor(color)) .expect("Expected to insert a PlayerColor"); self.hidden_propagates - .insert(player_entity, HiddenPropagate) + .insert(player_entity, HiddenPropagate::new()) .expect("Expected to insert a HiddenPropagate"); if is_controllable { self.health_ui_graphics diff --git a/libs/client_shared/src/ecs/resources.rs b/libs/client_shared/src/ecs/resources.rs index 966008f..8227885 100644 --- a/libs/client_shared/src/ecs/resources.rs +++ b/libs/client_shared/src/ecs/resources.rs @@ -5,10 +5,13 @@ use amethyst::{ ui::FontHandle, }; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::{io, time::Instant}; use gv_animation_prefabs::GameSpriteAnimationPrefab; -use gv_core::{math::Vector3, net::NetIdentifier}; +use gv_core::{ + math::Vector3, + net::{server_message::DisconnectReason, NetIdentifier}, +}; use crate::utils::graphic_helpers::generate_rectangle_vertices; @@ -59,31 +62,25 @@ pub struct EntityGraphics { } pub struct MultiplayerRoomState { - pub nickname: String, pub is_active: bool, - pub has_started: bool, - pub has_sent_join_message: bool, - pub has_sent_start_message: bool, - pub server_addr: SocketAddr, pub is_host: bool, - pub connection_id: Option, + pub connection_status: ConnectionStatus, pub player_net_id: NetIdentifier, } impl MultiplayerRoomState { pub fn new() -> Self { Self { - nickname: "Player".to_owned(), is_active: false, - has_started: false, - has_sent_join_message: false, - has_sent_start_message: false, - server_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 0), 3455)), is_host: false, - connection_id: None, + connection_status: ConnectionStatus::NotConnected, player_net_id: 0, } } + + pub fn reset(&mut self) { + *self = MultiplayerRoomState::new(); + } } impl Default for MultiplayerRoomState { @@ -91,3 +88,50 @@ impl Default for MultiplayerRoomState { Self::new() } } + +#[derive(Debug)] +pub enum ConnectionStatus { + NotConnected, + Connecting(Instant), + Connected(NetIdentifier), + Disconnecting, + Disconnected(DisconnectReason), + ServerStartFailed, + ConnectionFailed(Option), +} + +impl ConnectionStatus { + pub fn is_not_connected(&self) -> bool { + match self { + ConnectionStatus::NotConnected + | ConnectionStatus::Disconnected(_) + | ConnectionStatus::ConnectionFailed(_) + | ConnectionStatus::ServerStartFailed => true, + _ => false, + } + } + + pub fn is_connecting(&self) -> bool { + if let ConnectionStatus::Connecting(_) = self { + true + } else { + false + } + } + + pub fn is_connected(&self) -> bool { + if let ConnectionStatus::Connected(_) = self { + true + } else { + false + } + } + + pub fn connection_id(&self) -> Option { + if let ConnectionStatus::Connected(connection_id) = self { + Some(*connection_id) + } else { + None + } + } +} diff --git a/libs/core/Cargo.toml b/libs/core/Cargo.toml index c00eda6..4d4b6a4 100644 --- a/libs/core/Cargo.toml +++ b/libs/core/Cargo.toml @@ -4,9 +4,6 @@ version = "0.1.0" authors = [] edition = "2018" -[dependencies.amethyst] -git = "https://github.com/amethyst/amethyst" - [dependencies] bincode = "1.1.4" nalgebra = { version = "0.18.0", features = ["serde-serialize"] } @@ -16,3 +13,7 @@ rand = "0.6.5" serde = "1.0.101" serde_derive = "1.0.101" shrinkwraprs = "0.2.1" +derivative = "2.1.1" + +[dependencies.amethyst] +git = "https://github.com/amethyst/amethyst" diff --git a/libs/core/src/ecs/components/mod.rs b/libs/core/src/ecs/components/mod.rs index 018a000..37585b7 100644 --- a/libs/core/src/ecs/components/mod.rs +++ b/libs/core/src/ecs/components/mod.rs @@ -5,7 +5,11 @@ use amethyst::ecs::{Component, DenseVecStorage, Entity, VecStorage}; use serde_derive::{Deserialize, Serialize}; use shrinkwraprs::Shrinkwrap; -use std::{collections::VecDeque, net::SocketAddr, time::Instant}; +use std::{ + collections::VecDeque, + net::SocketAddr, + time::{Duration, Instant}, +}; use crate::{ actions::{ @@ -149,16 +153,22 @@ pub struct NetConnectionModel { pub created_at: Instant, pub last_acknowledged_update: Option, pub ping_pong_data: PingPongData, + pub disconnected: bool, + pub session_created_at: Duration, + pub session_id: NetIdentifier, } impl NetConnectionModel { - pub fn new(id: NetIdentifier, addr: SocketAddr) -> Self { + pub fn new(id: NetIdentifier, session_id: NetIdentifier, addr: SocketAddr) -> Self { Self { id, addr, created_at: Instant::now(), last_acknowledged_update: None, ping_pong_data: PingPongData::new(), + disconnected: false, + session_created_at: Duration::new(0, 0), + session_id, } } } diff --git a/libs/core/src/ecs/resources/mod.rs b/libs/core/src/ecs/resources/mod.rs index 975f537..95706a3 100644 --- a/libs/core/src/ecs/resources/mod.rs +++ b/libs/core/src/ecs/resources/mod.rs @@ -1,7 +1,7 @@ pub mod net; pub mod world; -use std::time::Duration; +use std::time::{Duration, Instant}; use crate::math::Vector2; @@ -57,5 +57,14 @@ pub enum GameEngineState { Loading, Menu, Playing, + ShuttingDown { shutdown_at: Instant }, Quit, } + +impl NewGameEngineState { + pub fn shutdown() -> Self { + NewGameEngineState(GameEngineState::ShuttingDown { + shutdown_at: Instant::now() + Duration::from_secs(1), + }) + } +} diff --git a/libs/core/src/ecs/resources/net.rs b/libs/core/src/ecs/resources/net.rs old mode 100644 new mode 100755 index 5e20891..3e28f07 --- a/libs/core/src/ecs/resources/net.rs +++ b/libs/core/src/ecs/resources/net.rs @@ -1,4 +1,5 @@ use amethyst::ecs::Entity; +use derivative::Derivative; use serde_derive::{Deserialize, Serialize}; use std::{collections::HashMap, ops::Range}; @@ -6,14 +7,17 @@ use std::{collections::HashMap, ops::Range}; use crate::{ actions::{player::PlayerCastAction, IdentifiableAction}, net::NetIdentifier, + PLAYER_COLORS, }; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Derivative, Debug, Clone, Serialize, Deserialize)] +#[derivative(PartialEq)] pub struct MultiplayerRoomPlayer { pub connection_id: NetIdentifier, pub entity_net_id: NetIdentifier, pub nickname: String, pub is_host: bool, + #[derivative(PartialEq = "ignore")] pub color: [f32; 3], } @@ -26,6 +30,7 @@ pub struct MultiplayerGameState { /// This is used on client to make sure that we do not unpause before pausing. pub waiting_for_players_pause_id: u64, pub lagging_players: Vec, + pub is_disconnected: bool, players_updated: bool, } @@ -38,10 +43,15 @@ impl MultiplayerGameState { waiting_for_players: false, waiting_for_players_pause_id: 0, lagging_players: Vec::new(), + is_disconnected: false, players_updated: false, } } + pub fn reset(&mut self) { + *self = MultiplayerGameState::new(); + } + pub fn read_updated_players(&mut self) -> Option<&[MultiplayerRoomPlayer]> { if self.players_updated { self.players_updated = false; @@ -55,6 +65,29 @@ impl MultiplayerGameState { self.players_updated = true; &mut self.players } + + pub fn drop_player_by_connection_id(&mut self, player_connection_id: NetIdentifier) { + let player_index = self + .players + .iter() + .position(|player| player.connection_id == player_connection_id); + if let Some(player_index) = player_index { + self.drop_player_by_index(player_index); + } else { + log::warn!( + "Couldn't find a player with connection id {} to drop", + player_connection_id + ); + } + } + + pub fn drop_player_by_index(&mut self, player_index: usize) { + self.players_updated = true; + self.players.remove(player_index); + for (player_index, player) in self.players.iter_mut().enumerate().skip(player_index) { + player.color = PLAYER_COLORS[player_index]; + } + } } pub struct EntityNetMetadataStorage { diff --git a/libs/core/src/ecs/resources/world.rs b/libs/core/src/ecs/resources/world.rs index 92fc938..1ffd03a 100644 --- a/libs/core/src/ecs/resources/world.rs +++ b/libs/core/src/ecs/resources/world.rs @@ -19,6 +19,8 @@ use crate::{ pub const SAVED_WORLD_STATES_LIMIT: usize = 600; pub const LAG_COMPENSATION_FRAMES_LIMIT: usize = 20; +pub const PAUSE_FRAME_THRESHOLD: u64 = + (LAG_COMPENSATION_FRAMES_LIMIT + LAG_COMPENSATION_FRAMES_LIMIT / 2) as u64; #[derive(Debug)] pub struct OldFrameError { @@ -148,7 +150,7 @@ impl SavedWorldState { let is_the_same_generation = storage.contains(*entity); if is_the_same_generation { storage - .insert(entity.clone(), component.clone()) + .insert(*entity, component.clone()) .expect("Expected to insert a saved component"); } } @@ -344,6 +346,14 @@ pub struct ClientWorldUpdates { pub look_actions_updates: VecDeque<(u64, Vec>>)>, } +impl ClientWorldUpdates { + pub fn clear(&mut self) { + self.walk_action_updates.clear(); + self.cast_action_updates.clear(); + self.look_actions_updates.clear(); + } +} + /// Client uses it to store the updates until it receives their confirmations from a server. #[derive(Debug)] pub struct PlayerActionUpdates { diff --git a/libs/core/src/ecs/system_data/time.rs b/libs/core/src/ecs/system_data/time.rs index 4d8880f..c418afa 100644 --- a/libs/core/src/ecs/system_data/time.rs +++ b/libs/core/src/ecs/system_data/time.rs @@ -18,6 +18,7 @@ impl<'a> GameTimeService<'a> { pub fn set_game_start_time(&mut self) { self.game_time.level_started_at = self.engine_time.absolute_time(); self.game_time.started_at_frame_number = self.engine_time.frame_number(); + self.game_time.frames_skipped = 0; } pub fn engine_time(&self) -> &Time { diff --git a/libs/core/src/lib.rs b/libs/core/src/lib.rs index fb09336..76d44f1 100644 --- a/libs/core/src/lib.rs +++ b/libs/core/src/lib.rs @@ -3,6 +3,14 @@ pub mod ecs; pub mod math; pub mod net; +pub static PLAYER_COLORS: [[f32; 3]; 5] = [ + [0.64, 0.12, 0.11], + [0.04, 0.45, 0.69], + [0.0, 0.49, 0.26], + [0.40, 0.3, 0.55], + [0.57, 0.57, 0.57], +]; + #[macro_export] macro_rules! profile_scope { ($string:expr) => { diff --git a/libs/core/src/net/client_message.rs b/libs/core/src/net/client_message.rs index 1fc36b0..696e3f3 100644 --- a/libs/core/src/net/client_message.rs +++ b/libs/core/src/net/client_message.rs @@ -1,5 +1,7 @@ use serde_derive::{Deserialize, Serialize}; +use std::time::Duration; + use crate::{ actions::{ player::{PlayerCastAction, PlayerWalkAction}, @@ -9,10 +11,20 @@ use crate::{ net::NetIdentifier, }; +#[derive(Debug, Serialize, Deserialize)] +pub struct ClientMessage { + pub session_id: NetIdentifier, + pub payload: ClientMessagePayload, +} + #[derive(Debug, Serialize, Deserialize)] pub enum ClientMessagePayload { Heartbeat, JoinRoom { + // As the server stores session id coming with this message + // (see `ServerMessage::session_id`), `sent_at` is used to filter out outdated handshakes + // in case there are duplicates of reliable messages. + sent_at: Duration, nickname: String, }, StartHostedGame, @@ -25,6 +37,11 @@ pub enum ClientMessagePayload { ping_id: NetIdentifier, frame_number: u64, }, + Kick { + /// Connection id stored by the host process. + kicked_connection_id: NetIdentifier, + }, + Disconnect, } impl ClientMessagePayload { diff --git a/libs/core/src/net/server_message.rs b/libs/core/src/net/server_message.rs index ff1d97b..4e98c93 100644 --- a/libs/core/src/net/server_message.rs +++ b/libs/core/src/net/server_message.rs @@ -5,12 +5,18 @@ use crate::{ net::NetIdentifier, }; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ServerMessage { + pub session_id: NetIdentifier, + pub payload: ServerMessagePayload, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum ServerMessagePayload { Heartbeat, UpdateRoomPlayers(Vec), /// Must have the same length as a last sent UpdateRoomPlayers, - /// contains server ids for corresponding players. + /// contains server (entity) ids for corresponding players. StartGame(Vec), Handshake { net_id: NetIdentifier, @@ -32,6 +38,19 @@ pub enum ServerMessagePayload { players: Vec, }, UnpauseWaitingForPlayers(NetIdentifier), + Disconnect(DisconnectReason), +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum DisconnectReason { + /// For rejecting any connections while the server + /// isn't connected to a host (in case of self-hosting). + Uninitialized, + GameIsStarted, + RoomIsFull, + Kick, + Closed, + ServerCrashed(i32), } impl ServerMessagePayload { diff --git a/libs/game/src/ecs/resources.rs b/libs/game/src/ecs/resources.rs index 24e55a8..c8e5ade 100644 --- a/libs/game/src/ecs/resources.rs +++ b/libs/game/src/ecs/resources.rs @@ -10,9 +10,9 @@ use gv_client_shared::{ utils::graphic_helpers::{create_color_material, create_mesh, generate_circle_vertices}, }; #[cfg(not(feature = "client"))] -use gv_core::net::client_message::ClientMessagePayload; +use gv_core::net::client_message::ClientMessage; #[cfg(feature = "client")] -use gv_core::net::server_message::ServerMessagePayload; +use gv_core::net::server_message::ServerMessage; use gv_core::{actions::mob::MobAttackType, net::ConnectionNetEvent}; #[derive(Clone)] @@ -73,6 +73,6 @@ impl MonsterDefinitions { } #[cfg(feature = "client")] -pub struct ConnectionEvents(pub Vec>); +pub struct ConnectionEvents(pub Vec>); #[cfg(not(feature = "client"))] -pub struct ConnectionEvents(pub Vec>); +pub struct ConnectionEvents(pub Vec>); diff --git a/libs/game/src/ecs/system_data/game_state_helper.rs b/libs/game/src/ecs/system_data/game_state_helper.rs index 4b6f65d..1b74e90 100644 --- a/libs/game/src/ecs/system_data/game_state_helper.rs +++ b/libs/game/src/ecs/system_data/game_state_helper.rs @@ -3,11 +3,12 @@ use amethyst::{ shred::{ResourceId, SystemData}, }; -use gv_core::ecs::resources::{net::MultiplayerGameState, GameEngineState}; +use gv_core::ecs::resources::{net::MultiplayerGameState, GameEngineState, NewGameEngineState}; #[derive(SystemData)] pub struct GameStateHelper<'s> { game_engine_state: ReadExpect<'s, GameEngineState>, + new_game_engine_state: ReadExpect<'s, NewGameEngineState>, multiplayer_game_state: ReadExpect<'s, MultiplayerGameState>, } @@ -18,7 +19,9 @@ impl<'s> GameStateHelper<'s> { || (!self.multiplayer_game_state.waiting_network && !self.multiplayer_game_state.waiting_for_players); - *self.game_engine_state == GameEngineState::Playing && multiplayer_is_unpaused + *self.game_engine_state == GameEngineState::Playing + && self.new_game_engine_state.0 == GameEngineState::Playing + && multiplayer_is_unpaused } pub fn is_multiplayer(&self) -> bool { @@ -36,12 +39,15 @@ impl<'s> GameStateHelper<'s> { } pub fn multiplayer_is_running(&self) -> bool { - *self.game_engine_state == GameEngineState::Playing && self.multiplayer_is_unpaused() + *self.game_engine_state == GameEngineState::Playing + && self.new_game_engine_state.0 == GameEngineState::Playing + && self.multiplayer_is_unpaused() } pub fn multiplayer_is_unpaused(&self) -> bool { self.multiplayer_game_state.is_playing && !self.multiplayer_game_state.waiting_network && !self.multiplayer_game_state.waiting_for_players + && !self.multiplayer_game_state.is_disconnected } } diff --git a/libs/game/src/ecs/systems/damage_subsystem.rs b/libs/game/src/ecs/systems/damage_subsystem.rs index 879a469..970abd9 100644 --- a/libs/game/src/ecs/systems/damage_subsystem.rs +++ b/libs/game/src/ecs/systems/damage_subsystem.rs @@ -132,7 +132,7 @@ impl<'s> DamageSubsystem<'s> { } } else { // If an entity has Dead component for whatever reason, but it has positive health, - // remove it's Dead component. + // remove its Dead component. // TODO: do we really need this code? let will_be_killed = dead .get(entity) @@ -157,9 +157,15 @@ impl<'s> DamageSubsystem<'s> { incoming_net_updates.expect("Expected net updates on client side"); for net_update in incoming_net_updates { assert_eq!(net_update.data.frame_number, frame_number); - let entity = entity_net_metadata_storage - .get_entity(net_update.entity_net_id) - .expect("Expected an updated entity"); + let entity = entity_net_metadata_storage.get_entity(net_update.entity_net_id); + if entity.is_none() { + log::error!( + "Couldn't find an entity (net id: {}) to apply damage entries", + net_update.entity_net_id + ); + return; + } + let entity = entity.unwrap(); let damage_history = damage_histories .get_mut(entity) .expect("Expected DamageHistory component"); diff --git a/libs/game/src/ecs/systems/mod.rs b/libs/game/src/ecs/systems/mod.rs index fda87a7..bcc8a56 100644 --- a/libs/game/src/ecs/systems/mod.rs +++ b/libs/game/src/ecs/systems/mod.rs @@ -63,14 +63,14 @@ pub type OutcomingNetUpdates = ClientWorldUpdates; pub type OutcomingNetUpdates = ServerWorldUpdate; #[cfg(feature = "client")] -type ClientFrameUpdate = PlayerActionUpdates; +pub type ClientFrameUpdate = PlayerActionUpdates; #[cfg(not(feature = "client"))] -type ClientFrameUpdate = DummyFramedUpdate; +pub type ClientFrameUpdate = DummyFramedUpdate; #[cfg(feature = "client")] -type FrameUpdate = ReceivedServerWorldUpdate; +pub type FrameUpdate = ReceivedServerWorldUpdate; #[cfg(not(feature = "client"))] -type FrameUpdate = ReceivedClientActionUpdates; +pub type FrameUpdate = ReceivedClientActionUpdates; type WriteStorageCell<'s, T> = Rc>>; type WriteExpectCell<'s, T> = Rc>>; diff --git a/libs/game/src/ecs/systems/net_connection_manager.rs b/libs/game/src/ecs/systems/net_connection_manager.rs index 1f94ae3..f864ae8 100644 --- a/libs/game/src/ecs/systems/net_connection_manager.rs +++ b/libs/game/src/ecs/systems/net_connection_manager.rs @@ -17,7 +17,8 @@ use std::{ use gv_core::{ ecs::{components::NetConnectionModel, system_data::time::GameTimeService}, net::{ - client_message::ClientMessagePayload, server_message::ServerMessagePayload, + client_message::{ClientMessage, ClientMessagePayload}, + server_message::{ServerMessage, ServerMessagePayload}, ConnectionNetEvent, EncodedMessage, NetEvent, NetIdentifier, }, }; @@ -27,13 +28,22 @@ use crate::ecs::resources::ConnectionEvents; const PING_INTERVAL_MILLIS: u64 = 500; #[cfg(feature = "client")] -type IncomingMessage = ServerMessagePayload; +type IncomingMessage = ServerMessage; #[cfg(not(feature = "client"))] -type IncomingMessage = ClientMessagePayload; +type IncomingMessage = ClientMessage; #[cfg(feature = "client")] -type OutcomingMessage = ClientMessagePayload; +type OutcomingMessage = ClientMessage; #[cfg(not(feature = "client"))] -type OutcomingMessage = ServerMessagePayload; +type OutcomingMessage = ServerMessage; + +#[cfg(feature = "client")] +type IncomingMessagePayload = ServerMessagePayload; +#[cfg(not(feature = "client"))] +type IncomingMessagePayload = ClientMessagePayload; +#[cfg(feature = "client")] +type OutcomingMessagePayload = ClientMessagePayload; +#[cfg(not(feature = "client"))] +type OutcomingMessagePayload = ServerMessagePayload; #[derive(Default)] pub struct NetConnectionManagerDesc; @@ -92,13 +102,12 @@ impl<'s> System<'s> for NetConnectionManagerSystem { game_time_service, mut transport, net_events, - mut incoming_messages, + mut connection_events, mut net_connection_models, entities, ): Self::SystemData, ) { let ping_id = self.next_ping_id(); - let send_ping_packet = ping_message(ping_id); // A hacky way to update connection_id_autoinc if some connections have been already created // (like we do it in ServerNetworkSystem). @@ -115,7 +124,7 @@ impl<'s> System<'s> for NetConnectionManagerSystem { ); if let Some(event) = event { - incoming_messages.0.push(event); + connection_events.0.push(event); } if let Some(response) = response { let addr = event_peer_addr(&net_event) @@ -130,6 +139,10 @@ impl<'s> System<'s> for NetConnectionManagerSystem { } for connection_model in (&mut net_connection_models).join() { + if connection_model.disconnected { + continue; + } + if connection_model.ping_pong_data.last_pinged_at + Duration::from_millis(PING_INTERVAL_MILLIS) < Instant::now() @@ -139,7 +152,7 @@ impl<'s> System<'s> for NetConnectionManagerSystem { .add_ping(ping_id, game_time_service.engine_time().frame_number()); transport.send_with_requirements( connection_model.addr, - &send_ping_packet, + &ping_message(connection_model.session_id, ping_id), DeliveryRequirement::Unreliable, UrgencyRequirement::Immediate, ); @@ -148,16 +161,26 @@ impl<'s> System<'s> for NetConnectionManagerSystem { } } -fn ping_message(ping_id: NetIdentifier) -> EncodedMessage { - bincode::serialize(&OutcomingMessage::Ping(ping_id)) - .expect("Expected to serialize Ping message") - .into() +fn ping_message(session_id: NetIdentifier, ping_id: NetIdentifier) -> EncodedMessage { + bincode::serialize(&OutcomingMessage { + session_id, + payload: OutcomingMessagePayload::Ping(ping_id), + }) + .expect("Expected to serialize Ping message") + .into() } -fn pong_message(ping_id: NetIdentifier, frame_number: u64) -> EncodedMessage { - bincode::serialize(&OutcomingMessage::Pong { - ping_id, - frame_number, +fn pong_message( + session_id: NetIdentifier, + ping_id: NetIdentifier, + frame_number: u64, +) -> EncodedMessage { + bincode::serialize(&OutcomingMessage { + session_id, + payload: OutcomingMessagePayload::Pong { + ping_id, + frame_number, + }, }) .expect("Expected to serialize Pong message") .into() @@ -181,37 +204,34 @@ impl NetConnectionManagerSystem { let peer_addr = peer_addr.unwrap(); if let NetworkSimulationEvent::Connect(socket_addr) = event { - let existing_connection = (net_connection_models) - .join() - .find(|connection_model| connection_model.addr == *socket_addr); - if let Some(connection_model) = existing_connection { - log::warn!( - "Duplicate Connect event for an existing connection ({})", - connection_model.id - ); + log::info!("Detected a new UDP connection: {}", socket_addr); + return (None, None); + } + + let mut connection = (entities, &mut *net_connection_models) + .join() + .find(|(_, connection_model)| connection_model.addr == peer_addr); + if connection.is_none() { + if let NetworkSimulationEvent::Disconnect(_) = event { + log::trace!("Ignoring Disconnect event for an already dropped connection"); return (None, None); } let connection_id = self.next_connection_id(); - log::info!("New connection ({}): {}", connection_id, peer_addr); - let net_connection_model = NetConnectionModel::new(connection_id, peer_addr); - entities + log::info!( + "Creating a new NewConnectionModel ({}) for {}", + connection_id, + peer_addr + ); + let net_connection_model = NetConnectionModel::new(connection_id, 0, peer_addr); + let entity = entities .build_entity() .with(net_connection_model, net_connection_models) .build(); - return ( - Some(ConnectionNetEvent { - connection_id, - event: NetEvent::Connected, - }), - None, - ); + connection = Some((entity, net_connection_models.get_mut(entity).unwrap())) } + let (connection_model_entity, connection_model) = connection.unwrap(); - let (connection_model_entity, connection_model) = (entities, net_connection_models) - .join() - .find(|(_, connection_model)| connection_model.addr == peer_addr) - .unwrap_or_else(|| panic!("Expected a NetConnectionModel for {}", peer_addr)); let connection_id = connection_model.id; match event { NetworkSimulationEvent::Disconnect(_) => { @@ -220,6 +240,7 @@ impl NetConnectionManagerSystem { connection_model.id, connection_model.addr, ); + connection_model.disconnected = true; entities .delete(connection_model_entity) .expect("Expected to delete a NetConnectionModel"); @@ -232,20 +253,31 @@ impl NetConnectionManagerSystem { ) } NetworkSimulationEvent::Message(_, bytes) => { - if let Ok(message) = bincode::deserialize::(bytes.as_ref()) { - match message { - IncomingMessage::Ping(ping_id) => { - log::trace!("Received a new ping message: {:?}", &message); + if let Ok(IncomingMessage { + session_id, + payload, + }) = bincode::deserialize::(bytes.as_ref()) + { + match payload { + IncomingMessagePayload::Ping(ping_id) => { + log::trace!("Received a new ping message: {:?}", &payload); + if connection_model.disconnected { + return (None, None); + } ( None, - Some(pong_message(ping_id, game_time_service.game_frame_number())), + Some(pong_message( + session_id, + ping_id, + game_time_service.game_frame_number(), + )), ) } - IncomingMessage::Pong { + IncomingMessagePayload::Pong { ping_id, frame_number: peer_frame_number, } => { - log::trace!("Received a new pong message: {:?}", &message); + log::trace!("Received a new pong message: {:?}", &payload); connection_model.ping_pong_data.add_pong( ping_id, peer_frame_number, @@ -255,15 +287,25 @@ impl NetConnectionManagerSystem { (None, None) } message if message.is_heartbeat() => { - log::trace!("Received a new Heartbeat message"); + log::trace!( + "Received a new Heartbeat message (connection_id: {})", + connection_id + ); (None, None) } _ => { - log::debug!("Received a new message: {:?}", &message); + log::debug!( + "Received a new message (connection_id: {}): {:?}", + connection_id, + &payload + ); ( Some(ConnectionNetEvent { connection_id, - event: NetEvent::Message(message), + event: NetEvent::Message(IncomingMessage { + session_id, + payload, + }), }), None, ) @@ -273,6 +315,18 @@ impl NetConnectionManagerSystem { (None, None) } } + NetworkSimulationEvent::SendError(err, _) => { + log::error!("(SendError) {:?}", err); + (None, None) + } + NetworkSimulationEvent::RecvError(err) => { + log::error!("(RecvError) {:?}", err); + (None, None) + } + NetworkSimulationEvent::ConnectionError(err, _) => { + log::error!("(ConnectionError) {:?}", err); + (None, None) + } _ => (None, None), } } diff --git a/libs/game/src/ecs/systems/state_switcher.rs b/libs/game/src/ecs/systems/state_switcher.rs index 77a9a0f..99f281c 100644 --- a/libs/game/src/ecs/systems/state_switcher.rs +++ b/libs/game/src/ecs/systems/state_switcher.rs @@ -4,6 +4,8 @@ use amethyst::{ shrev::EventChannel, }; +use std::time::Instant; + use gv_core::ecs::resources::{GameEngineState, NewGameEngineState}; use crate::states::*; @@ -27,6 +29,10 @@ impl<'s> System<'s> for StateSwitcherSystem { GameEngineState::Loading => unreachable!(), GameEngineState::Menu => Trans::Switch(Box::new(MenuState)), GameEngineState::Playing => Trans::Switch(Box::new(PlayingState)), + GameEngineState::ShuttingDown { shutdown_at } if Instant::now() > shutdown_at => { + Trans::Quit + } + GameEngineState::ShuttingDown { .. } => Trans::None, GameEngineState::Quit => Trans::Quit, }); trans_events.single_write(trans); diff --git a/libs/game/src/lib.rs b/libs/game/src/lib.rs index dcc3484..e8b7915 100644 --- a/libs/game/src/lib.rs +++ b/libs/game/src/lib.rs @@ -27,27 +27,21 @@ use crate::ecs::{ systems::{missile::MissileDyingSystem, monster::*, *}, }; -pub static PLAYER_COLORS: [[f32; 3]; 5] = [ - [0.64, 0.12, 0.11], - [0.04, 0.45, 0.69], - [0.0, 0.49, 0.26], - [0.40, 0.3, 0.55], - [0.57, 0.57, 0.57], -]; - pub fn build_game_logic_systems<'a, 'b>( game_data_builder: GameDataBuilder<'a, 'b>, world: &mut World, is_server: bool, ) -> Result, Error> { - world.insert(FramedUpdates::::default()); - world.insert(FramedUpdates::::default()); - world.insert(WorldStates::default()); world.insert(ConnectionEvents(Vec::new())); world.insert(MultiplayerGameState::new()); - world.insert(EntityNetMetadataStorage::new()); world.insert(ActionUpdateIdProvider::default()); + + // The resources which we need to remember to reset on starting a game. + world.insert(FramedUpdates::::default()); + world.insert(FramedUpdates::::default()); + world.insert(WorldStates::default()); world.insert(CastActionsToExecute::default()); + world.insert(EntityNetMetadataStorage::new()); let game_data_builder = game_data_builder .with(PauseSystem, "pause_system", &["game_network_system"]) diff --git a/libs/game/src/states/loading_state.rs b/libs/game/src/states/loading_state.rs index 649c4c2..2278a5d 100644 --- a/libs/game/src/states/loading_state.rs +++ b/libs/game/src/states/loading_state.rs @@ -128,6 +128,7 @@ impl LoadingState { creator.create("resources/ui/lobby_menu.ron", ()), creator.create("resources/ui/multiplayer_menu.ron", ()), creator.create("resources/ui/restart_menu.ron", ()), + creator.create("resources/ui/modal.ron", ()), ) }); diff --git a/libs/game/src/states/menu_state.rs b/libs/game/src/states/menu_state.rs index 04f52e7..114ccd2 100644 --- a/libs/game/src/states/menu_state.rs +++ b/libs/game/src/states/menu_state.rs @@ -4,11 +4,20 @@ use amethyst::{ renderer::Camera, }; -use gv_core::ecs::{ - components::{missile::Missile, Monster, Player}, - resources::{GameEngineState, GameLevelState}, +use gv_core::{ + actions::monster_spawn::SpawnActions, + ecs::{ + components::{missile::Missile, EntityNetMetadata, Monster, Player}, + resources::{ + net::{CastActionsToExecute, EntityNetMetadataStorage}, + world::{FramedUpdates, WorldStates}, + GameEngineState, GameLevelState, + }, + }, }; +use crate::ecs::systems::{ClientFrameUpdate, FrameUpdate}; + pub struct MenuState; impl SimpleState for MenuState { @@ -21,14 +30,28 @@ impl SimpleState for MenuState { if game_level_state.is_over { game_level_state.is_over = false; drop(game_level_state); + + world.insert(FramedUpdates::::default()); + world.insert(FramedUpdates::::default()); + world.insert(FramedUpdates::::default()); + world.insert(WorldStates::default()); + world.insert(CastActionsToExecute::default()); + world.insert(EntityNetMetadataStorage::new()); + world.exec( - |(entities, players, monsters, missiles, cameras): ( + |(entities, entity_net_metadata, players, monsters, missiles, cameras): ( Entities, + ReadStorage, ReadStorage, ReadStorage, ReadStorage, ReadStorage, )| { + for (entity_net_metadata, _) in (&entities, &entity_net_metadata).join() { + entities + .delete(entity_net_metadata) + .expect("Expected to clean up an entity"); + } for (player_entity, _) in (&entities, &players).join() { entities .delete(player_entity) diff --git a/libs/game/src/states/playing_state.rs b/libs/game/src/states/playing_state.rs index 834eb76..61b4404 100644 --- a/libs/game/src/states/playing_state.rs +++ b/libs/game/src/states/playing_state.rs @@ -28,14 +28,14 @@ use gv_core::ecs::{ }, system_data::time::GameTimeService, }; +#[cfg(feature = "client")] +use gv_core::PLAYER_COLORS; #[cfg(not(feature = "client"))] use gv_core::{ecs::components::NetConnectionModel, net::server_message::ServerMessagePayload}; use crate::ecs::factories::{LandscapeFactory, PlayerFactory}; #[cfg(not(feature = "client"))] use crate::utils::net::broadcast_message_reliable; -#[cfg(feature = "client")] -use crate::PLAYER_COLORS; #[derive(Default)] pub struct PlayingState; @@ -163,7 +163,7 @@ fn initialize_players(world: &mut World) { broadcast_message_reliable( &mut transport, (&net_connections).join(), - &ServerMessagePayload::StartGame(player_net_identifiers), + ServerMessagePayload::StartGame(player_net_identifiers), ); }, ); diff --git a/libs/game/src/utils/net.rs b/libs/game/src/utils/net.rs index de8f917..b0f1015 100644 --- a/libs/game/src/utils/net.rs +++ b/libs/game/src/utils/net.rs @@ -2,28 +2,30 @@ use amethyst::network::simulation::{DeliveryRequirement, TransportResource, Urge use gv_core::ecs::components::NetConnectionModel; #[cfg(feature = "client")] -use gv_core::net::client_message::ClientMessagePayload; +use gv_core::net::client_message::{ClientMessage, ClientMessagePayload}; #[cfg(not(feature = "client"))] -use gv_core::net::server_message::ServerMessagePayload; - -// TODO - jstnlef: I made all of the UrgencyRequirements immediate because I wasn't sure if you wanted -// to use the NetworkSimulationTime resource to handle rate of send. +use gv_core::net::server_message::{ServerMessage, ServerMessagePayload}; #[cfg(not(feature = "client"))] pub fn broadcast_message_reliable<'a>( transport: &mut TransportResource, net_connections: impl Iterator, - message: &ServerMessagePayload, + payload: ServerMessagePayload, ) { - let send_message = - bincode::serialize(message).expect("Expected to serialize a broadcasted message"); for connection in net_connections { - transport.send_with_requirements( - connection.addr, - &send_message, - DeliveryRequirement::Reliable, - UrgencyRequirement::Immediate, - ); + let sent_message = bincode::serialize(&ServerMessage { + session_id: connection.session_id, + payload: payload.clone(), + }) + .expect("Expected to serialize a broadcasted message"); + if !connection.disconnected { + transport.send_with_requirements( + connection.addr, + &sent_message, + DeliveryRequirement::Reliable, + UrgencyRequirement::Immediate, + ); + } } } @@ -31,12 +33,19 @@ pub fn broadcast_message_reliable<'a>( pub fn send_message_reliable( transport: &mut TransportResource, net_connection: &NetConnectionModel, - message: &ClientMessagePayload, + payload: ClientMessagePayload, ) { - let send_message = bincode::serialize(message).expect("Expected to serialize a client message"); + if net_connection.disconnected { + return; + } + let sent_message = bincode::serialize(&ClientMessage { + session_id: net_connection.session_id, + payload, + }) + .expect("Expected to serialize a client message"); transport.send_with_requirements( net_connection.addr, - &send_message, + &sent_message, DeliveryRequirement::Reliable, UrgencyRequirement::Immediate, ); @@ -46,12 +55,19 @@ pub fn send_message_reliable( pub fn send_message_reliable( transport: &mut TransportResource, net_connection: &NetConnectionModel, - message: &ServerMessagePayload, + payload: ServerMessagePayload, ) { - let send_message = bincode::serialize(message).expect("Expected to serialize a server message"); + if net_connection.disconnected { + return; + } + let sent_message = bincode::serialize(&ServerMessage { + session_id: net_connection.session_id, + payload, + }) + .expect("Expected to serialize a server message"); transport.send_with_requirements( net_connection.addr, - &send_message, + &sent_message, DeliveryRequirement::Reliable, UrgencyRequirement::Immediate, ); @@ -61,13 +77,21 @@ pub fn send_message_reliable( pub fn send_message_unreliable( transport: &mut TransportResource, net_connection: &NetConnectionModel, - message: &ClientMessagePayload, + payload: ClientMessagePayload, ) { + if net_connection.disconnected { + return; + } + let message = ClientMessage { + session_id: net_connection.session_id, + payload, + }; log::trace!("Sending: {:#?}", message); - let send_message = bincode::serialize(message).expect("Expected to serialize a client message"); + let sent_message = + bincode::serialize(&message).expect("Expected to serialize a client message"); transport.send_with_requirements( net_connection.addr, - &send_message, + &sent_message, DeliveryRequirement::Unreliable, UrgencyRequirement::Immediate, ); @@ -77,14 +101,22 @@ pub fn send_message_unreliable( pub fn send_message_unreliable( transport: &mut TransportResource, net_connection: &NetConnectionModel, - message: &ServerMessagePayload, + payload: ServerMessagePayload, ) { + if net_connection.disconnected { + return; + } + let message = ServerMessage { + session_id: net_connection.session_id, + payload, + }; log::trace!("Sending: {:#?}", message); - let send_message = bincode::serialize(message).expect("Expected to serialize a server message"); - log::trace!("Packet len: {}", send_message.len()); + let sent_message = + bincode::serialize(&message).expect("Expected to serialize a server message"); + log::trace!("Packet len: {}", sent_message.len()); transport.send_with_requirements( net_connection.addr, - &send_message, + &sent_message, DeliveryRequirement::Unreliable, UrgencyRequirement::Immediate, ); diff --git a/resources/ui/modal.ron b/resources/ui/modal.ron new file mode 100644 index 0000000..75e79c6 --- /dev/null +++ b/resources/ui/modal.ron @@ -0,0 +1,83 @@ +#![enable(implicit_some)] +Container( + transform: ( + id: "ui_modal_backdrop_container", + x: 0.0, + y: 0.0, + z: 100.0, + stretch: XY(x_margin: 0.0, y_margin: 0.0, keep_aspect_ratio: false), + hidden: true, + ), + background: SolidColor(0.02, 0.02, 0.02, 0.7), + children: [ + Container( + transform: ( + id: "ui_modal_window_border_container", + anchor: Middle, + pivot: Middle, + width: 400.0, + height: 250.0, + z: 0.5, + hidden: true, + ), + background: SolidColor(0.7, 0.7, 0.7, 1.0), + children: [ + Container( + transform: ( + id: "ui_modal_window_container", + anchor: Middle, + pivot: Middle, + z: 0.5, + stretch: XY(x_margin: 1.0, y_margin: 1.0, keep_aspect_ratio: false), + hidden: true, + ), + background: SolidColor(0.05, 0.05, 0.05, 1.0), + children: [ + Label( + transform: ( + id: "ui_modal_title", + anchor: Middle, + pivot: Middle, + x: 0.0, + y: 0.0, + z: 0.5, + width: 350.0, + height: 100.0, + hidden: true, + ), + text: ( + text: "Default modal window text", + color: (0.9, 0.9, 0.9, 1.0), + font_size: 28.0, + font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), + align: Middle, + line_mode: Wrap, + ), + ), + + Button( + transform: ( + id: "ui_modal_confirm_button", + anchor: BottomMiddle, + pivot: Middle, + x: 0.0, + y: 35.0, + z: 0.5, + width: 100.0, + height: 75.0, + hidden: true, + ), + button: ( + text: "OK", + font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), + font_size: 30., + normal_text_color: (0.972, 0.917, 0.827, 0.0), + hover_text_color: (0.841, 0.670, 0.556, 1.0), + ) + ), + ], + ), + ], + ), + ], +) diff --git a/resources/ui/multiplayer_menu.ron b/resources/ui/multiplayer_menu.ron index cc80a38..fa52292 100644 --- a/resources/ui/multiplayer_menu.ron +++ b/resources/ui/multiplayer_menu.ron @@ -82,13 +82,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 7.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "1", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), @@ -102,16 +103,37 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 9.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "Player", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), + Button( + transform: ( + id: "ui_mp_room_player1_kick", + anchor: MiddleRight, + pivot: MiddleRight, + x: 0.0, + y: 0.0, + z: 0.5, + width: 100.0, + height: 36.0, + hidden: true, + ), + button: ( + text: "Kick", + font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), + font_size: 36., + normal_text_color: (0.972, 0.917, 0.827, 0.0), + hover_text_color: (0.841, 0.670, 0.556, 1.0), + ) + ), ] ), Container( @@ -146,13 +168,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 7.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "2", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), @@ -166,13 +189,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 9.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "Player", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), @@ -230,13 +254,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 7.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "3", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), @@ -250,13 +275,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 9.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "Player", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), @@ -314,13 +340,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 7.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "4", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), @@ -334,13 +361,14 @@ Container( height: 58.0, anchor: Middle, stretch: XY(x_margin: 0.0, y_margin: 9.0, keep_aspect_ratio: false), + opaque: false, hidden: true, ), text: ( text: "Player", font: File("resources/PT_Sans-Web-Regular.ttf", ("TTF", ())), font_size: 58.0, - color: (0.9, 0.9, 0.9, 1.0), + color: (0.9, 0.9, 0.9, 0.0), align: MiddleLeft, ) ), diff --git a/server_logging_config.toml.example b/server_logging_config.toml.example new file mode 100644 index 0000000..8d3e543 --- /dev/null +++ b/server_logging_config.toml.example @@ -0,0 +1,10 @@ +stdout = "Colored" +level_filter = "Info" +allow_env_override = true +module_levels = [ + ["amethyst_assets", "Info"], + ["gv_game::ecs::systems", "Debug"], + ["gv_game::ecs::systems::net_connection_manager", "Info"], + ["gv_game::utils::net", "Info"], + ["gv_server", "Debug"], +]