Live location tracking and playback for the game "manhunt"

Better testing, better transport handling

bwc9876.dev d903af34 4d771113

verified
+805 -373
+4
.rustfmt.toml
··· 1 + edition = "2024" 2 + reorder_imports = true 3 + # imports_granularity = "Crate" 4 +
+109 -115
Cargo.lock
··· 540 540 "http-body-util", 541 541 "hyper", 542 542 "hyper-util", 543 - "itoa 1.0.15", 543 + "itoa", 544 544 "matchit", 545 545 "memchr", 546 546 "mime", ··· 730 730 731 731 [[package]] 732 732 name = "brotli" 733 - version = "7.0.0" 733 + version = "8.0.1" 734 734 source = "registry+https://github.com/rust-lang/crates.io-index" 735 - checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" 735 + checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" 736 736 dependencies = [ 737 737 "alloc-no-stdlib", 738 738 "alloc-stdlib", ··· 741 741 742 742 [[package]] 743 743 name = "brotli-decompressor" 744 - version = "4.0.3" 744 + version = "5.0.0" 745 745 source = "registry+https://github.com/rust-lang/crates.io-index" 746 - checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" 746 + checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 747 747 dependencies = [ 748 748 "alloc-no-stdlib", 749 749 "alloc-stdlib", ··· 751 751 752 752 [[package]] 753 753 name = "bumpalo" 754 - version = "3.18.1" 754 + version = "3.19.0" 755 755 source = "registry+https://github.com/rust-lang/crates.io-index" 756 - checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" 756 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 757 757 758 758 [[package]] 759 759 name = "byte-unit" ··· 1167 1167 1168 1168 [[package]] 1169 1169 name = "cssparser" 1170 - version = "0.27.2" 1170 + version = "0.29.6" 1171 1171 source = "registry+https://github.com/rust-lang/crates.io-index" 1172 - checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" 1172 + checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" 1173 1173 dependencies = [ 1174 1174 "cssparser-macros", 1175 1175 "dtoa-short", 1176 - "itoa 0.4.8", 1176 + "itoa", 1177 1177 "matches", 1178 - "phf 0.8.0", 1178 + "phf 0.10.1", 1179 1179 "proc-macro2", 1180 1180 "quote", 1181 1181 "smallvec", ··· 2243 2243 "futures-core", 2244 2244 "futures-sink", 2245 2245 "http", 2246 - "indexmap 2.9.0", 2246 + "indexmap 2.10.0", 2247 2247 "slab", 2248 2248 "tokio", 2249 2249 "tokio-util", ··· 2309 2309 2310 2310 [[package]] 2311 2311 name = "html5ever" 2312 - version = "0.26.0" 2312 + version = "0.29.1" 2313 2313 source = "registry+https://github.com/rust-lang/crates.io-index" 2314 - checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" 2314 + checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" 2315 2315 dependencies = [ 2316 2316 "log", 2317 2317 "mac", 2318 2318 "markup5ever", 2319 - "proc-macro2", 2320 - "quote", 2321 - "syn 1.0.109", 2319 + "match_token", 2322 2320 ] 2323 2321 2324 2322 [[package]] ··· 2329 2327 dependencies = [ 2330 2328 "bytes", 2331 2329 "fnv", 2332 - "itoa 1.0.15", 2330 + "itoa", 2333 2331 ] 2334 2332 2335 2333 [[package]] ··· 2381 2379 "http-body", 2382 2380 "httparse", 2383 2381 "httpdate", 2384 - "itoa 1.0.15", 2382 + "itoa", 2385 2383 "pin-project-lite", 2386 2384 "smallvec", 2387 2385 "tokio", ··· 2591 2589 2592 2590 [[package]] 2593 2591 name = "indexmap" 2594 - version = "2.9.0" 2592 + version = "2.10.0" 2595 2593 source = "registry+https://github.com/rust-lang/crates.io-index" 2596 - checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 2594 + checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 2597 2595 dependencies = [ 2598 2596 "equivalent", 2599 2597 "hashbrown 0.15.4", ··· 2682 2680 2683 2681 [[package]] 2684 2682 name = "itoa" 2685 - version = "0.4.8" 2686 - source = "registry+https://github.com/rust-lang/crates.io-index" 2687 - checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 2688 - 2689 - [[package]] 2690 - name = "itoa" 2691 2683 version = "1.0.15" 2692 2684 source = "registry+https://github.com/rust-lang/crates.io-index" 2693 2685 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" ··· 2806 2798 2807 2799 [[package]] 2808 2800 name = "kuchikiki" 2809 - version = "0.8.2" 2801 + version = "0.8.8-speedreader" 2810 2802 source = "registry+https://github.com/rust-lang/crates.io-index" 2811 - checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" 2803 + checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" 2812 2804 dependencies = [ 2813 2805 "cssparser", 2814 2806 "html5ever", 2815 - "indexmap 1.9.3", 2816 - "matches", 2807 + "indexmap 2.10.0", 2817 2808 "selectors", 2818 2809 ] 2819 2810 ··· 2874 2865 2875 2866 [[package]] 2876 2867 name = "libredox" 2877 - version = "0.1.3" 2868 + version = "0.1.4" 2878 2869 source = "registry+https://github.com/rust-lang/crates.io-index" 2879 - checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 2870 + checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" 2880 2871 dependencies = [ 2881 2872 "bitflags 2.9.1", 2882 2873 "libc", ··· 2974 2965 "serde", 2975 2966 "specta", 2976 2967 "tokio", 2968 + "tokio-util", 2977 2969 "uuid", 2978 2970 ] 2979 2971 ··· 3014 3006 3015 3007 [[package]] 3016 3008 name = "markup5ever" 3017 - version = "0.11.0" 3009 + version = "0.14.1" 3018 3010 source = "registry+https://github.com/rust-lang/crates.io-index" 3019 - checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" 3011 + checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" 3020 3012 dependencies = [ 3021 3013 "log", 3022 - "phf 0.10.1", 3023 - "phf_codegen 0.10.0", 3014 + "phf 0.11.3", 3015 + "phf_codegen 0.11.3", 3024 3016 "string_cache", 3025 3017 "string_cache_codegen", 3026 3018 "tendril", 3027 3019 ] 3028 3020 3029 3021 [[package]] 3022 + name = "match_token" 3023 + version = "0.1.0" 3024 + source = "registry+https://github.com/rust-lang/crates.io-index" 3025 + checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" 3026 + dependencies = [ 3027 + "proc-macro2", 3028 + "quote", 3029 + "syn 2.0.104", 3030 + ] 3031 + 3032 + [[package]] 3030 3033 name = "matchbox_protocol" 3031 3034 version = "0.12.0" 3032 3035 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3173 3176 3174 3177 [[package]] 3175 3178 name = "muda" 3176 - version = "0.16.1" 3179 + version = "0.17.0" 3177 3180 source = "registry+https://github.com/rust-lang/crates.io-index" 3178 - checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" 3181 + checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988" 3179 3182 dependencies = [ 3180 3183 "crossbeam-channel", 3181 3184 "dpi", ··· 3758 3761 source = "registry+https://github.com/rust-lang/crates.io-index" 3759 3762 checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" 3760 3763 dependencies = [ 3761 - "phf_macros 0.8.0", 3762 3764 "phf_shared 0.8.0", 3763 - "proc-macro-hack", 3764 3765 ] 3765 3766 3766 3767 [[package]] ··· 3769 3770 source = "registry+https://github.com/rust-lang/crates.io-index" 3770 3771 checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" 3771 3772 dependencies = [ 3773 + "phf_macros 0.10.0", 3772 3774 "phf_shared 0.10.0", 3775 + "proc-macro-hack", 3773 3776 ] 3774 3777 3775 3778 [[package]] ··· 3794 3797 3795 3798 [[package]] 3796 3799 name = "phf_codegen" 3797 - version = "0.10.0" 3800 + version = "0.11.3" 3798 3801 source = "registry+https://github.com/rust-lang/crates.io-index" 3799 - checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" 3802 + checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 3800 3803 dependencies = [ 3801 - "phf_generator 0.10.0", 3802 - "phf_shared 0.10.0", 3804 + "phf_generator 0.11.3", 3805 + "phf_shared 0.11.3", 3803 3806 ] 3804 3807 3805 3808 [[package]] ··· 3834 3837 3835 3838 [[package]] 3836 3839 name = "phf_macros" 3837 - version = "0.8.0" 3840 + version = "0.10.0" 3838 3841 source = "registry+https://github.com/rust-lang/crates.io-index" 3839 - checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" 3842 + checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" 3840 3843 dependencies = [ 3841 - "phf_generator 0.8.0", 3842 - "phf_shared 0.8.0", 3844 + "phf_generator 0.10.0", 3845 + "phf_shared 0.10.0", 3843 3846 "proc-macro-hack", 3844 3847 "proc-macro2", 3845 3848 "quote", ··· 3932 3935 checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed" 3933 3936 dependencies = [ 3934 3937 "base64 0.22.1", 3935 - "indexmap 2.9.0", 3938 + "indexmap 2.10.0", 3936 3939 "quick-xml", 3937 3940 "serde", 3938 3941 "time", ··· 4817 4820 4818 4821 [[package]] 4819 4822 name = "selectors" 4820 - version = "0.22.0" 4823 + version = "0.24.0" 4821 4824 source = "registry+https://github.com/rust-lang/crates.io-index" 4822 - checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" 4825 + checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" 4823 4826 dependencies = [ 4824 4827 "bitflags 1.3.2", 4825 4828 "cssparser", 4826 4829 "derive_more 0.99.20", 4827 4830 "fxhash", 4828 4831 "log", 4829 - "matches", 4830 4832 "phf 0.8.0", 4831 4833 "phf_codegen 0.8.0", 4832 4834 "precomputed-hash", 4833 4835 "servo_arc", 4834 4836 "smallvec", 4835 - "thin-slice", 4836 4837 ] 4837 4838 4838 4839 [[package]] ··· 4915 4916 source = "registry+https://github.com/rust-lang/crates.io-index" 4916 4917 checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 4917 4918 dependencies = [ 4918 - "itoa 1.0.15", 4919 + "itoa", 4919 4920 "memchr", 4920 4921 "ryu", 4921 4922 "serde", ··· 4927 4928 source = "registry+https://github.com/rust-lang/crates.io-index" 4928 4929 checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 4929 4930 dependencies = [ 4930 - "itoa 1.0.15", 4931 + "itoa", 4931 4932 "serde", 4932 4933 ] 4933 4934 ··· 4958 4959 checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 4959 4960 dependencies = [ 4960 4961 "form_urlencoded", 4961 - "itoa 1.0.15", 4962 + "itoa", 4962 4963 "ryu", 4963 4964 "serde", 4964 4965 ] ··· 4973 4974 "chrono", 4974 4975 "hex", 4975 4976 "indexmap 1.9.3", 4976 - "indexmap 2.9.0", 4977 + "indexmap 2.10.0", 4977 4978 "schemars 0.9.0", 4978 4979 "serde", 4979 4980 "serde_derive", ··· 5018 5019 5019 5020 [[package]] 5020 5021 name = "servo_arc" 5021 - version = "0.1.1" 5022 + version = "0.2.0" 5022 5023 source = "registry+https://github.com/rust-lang/crates.io-index" 5023 - checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" 5024 + checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" 5024 5025 dependencies = [ 5025 5026 "nodrop", 5026 5027 "stable_deref_trait", ··· 5399 5400 5400 5401 [[package]] 5401 5402 name = "tao" 5402 - version = "0.33.0" 5403 + version = "0.34.0" 5403 5404 source = "registry+https://github.com/rust-lang/crates.io-index" 5404 - checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" 5405 + checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" 5405 5406 dependencies = [ 5406 5407 "bitflags 2.9.1", 5407 5408 "core-foundation 0.10.1", ··· 5461 5462 5462 5463 [[package]] 5463 5464 name = "tauri" 5464 - version = "2.5.1" 5465 + version = "2.6.1" 5465 5466 source = "registry+https://github.com/rust-lang/crates.io-index" 5466 - checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" 5467 + checksum = "773663ec28d911ac02f3a478c55f7f2fa581368d9e16ce9dff8d650b3666f91e" 5467 5468 dependencies = [ 5468 5469 "anyhow", 5469 5470 "bytes", 5470 5471 "dirs", 5471 5472 "dunce", 5472 5473 "embed_plist", 5473 - "futures-util", 5474 - "getrandom 0.2.16", 5474 + "getrandom 0.3.3", 5475 5475 "glob", 5476 5476 "gtk", 5477 5477 "heck 0.5.0", ··· 5513 5513 5514 5514 [[package]] 5515 5515 name = "tauri-build" 5516 - version = "2.2.0" 5516 + version = "2.3.0" 5517 5517 source = "registry+https://github.com/rust-lang/crates.io-index" 5518 - checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" 5518 + checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83" 5519 5519 dependencies = [ 5520 5520 "anyhow", 5521 5521 "cargo_toml", ··· 5535 5535 5536 5536 [[package]] 5537 5537 name = "tauri-codegen" 5538 - version = "2.2.0" 5538 + version = "2.3.0" 5539 5539 source = "registry+https://github.com/rust-lang/crates.io-index" 5540 - checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47" 5540 + checksum = "f5df493a1075a241065bc865ed5ef8d0fbc1e76c7afdc0bf0eccfaa7d4f0e406" 5541 5541 dependencies = [ 5542 5542 "base64 0.22.1", 5543 5543 "brotli", ··· 5562 5562 5563 5563 [[package]] 5564 5564 name = "tauri-macros" 5565 - version = "2.2.0" 5565 + version = "2.3.1" 5566 5566 source = "registry+https://github.com/rust-lang/crates.io-index" 5567 - checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112" 5567 + checksum = "f237fbea5866fa5f2a60a21bea807a2d6e0379db070d89c3a10ac0f2d4649bbc" 5568 5568 dependencies = [ 5569 5569 "heck 0.5.0", 5570 5570 "proc-macro2", ··· 5576 5576 5577 5577 [[package]] 5578 5578 name = "tauri-plugin" 5579 - version = "2.2.0" 5579 + version = "2.3.0" 5580 5580 source = "registry+https://github.com/rust-lang/crates.io-index" 5581 - checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601" 5581 + checksum = "1d9a0bd00bf1930ad1a604d08b0eb6b2a9c1822686d65d7f4731a7723b8901d3" 5582 5582 dependencies = [ 5583 5583 "anyhow", 5584 5584 "glob", ··· 5593 5593 5594 5594 [[package]] 5595 5595 name = "tauri-plugin-dialog" 5596 - version = "2.2.2" 5596 + version = "2.3.0" 5597 5597 source = "registry+https://github.com/rust-lang/crates.io-index" 5598 - checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8" 5598 + checksum = "1aefb14219b492afb30b12647b5b1247cadd2c0603467310c36e0f7ae1698c28" 5599 5599 dependencies = [ 5600 5600 "log", 5601 5601 "raw-window-handle", ··· 5611 5611 5612 5612 [[package]] 5613 5613 name = "tauri-plugin-fs" 5614 - version = "2.3.0" 5614 + version = "2.4.0" 5615 5615 source = "registry+https://github.com/rust-lang/crates.io-index" 5616 - checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da" 5616 + checksum = "c341290d31991dbca38b31d412c73dfbdb070bb11536784f19dd2211d13b778f" 5617 5617 dependencies = [ 5618 5618 "anyhow", 5619 5619 "dunce", ··· 5633 5633 5634 5634 [[package]] 5635 5635 name = "tauri-plugin-geolocation" 5636 - version = "2.2.5" 5636 + version = "2.3.0" 5637 5637 source = "registry+https://github.com/rust-lang/crates.io-index" 5638 - checksum = "3dc06e23cb12d17533aca6001252f7f0fdbd295868194748821d0f0e6fbfbb59" 5638 + checksum = "d3e5d020065f33a6590a5fbf7bdd508845399fbe644c2daeb51be319df99fdd7" 5639 5639 dependencies = [ 5640 5640 "log", 5641 5641 "serde", ··· 5647 5647 5648 5648 [[package]] 5649 5649 name = "tauri-plugin-log" 5650 - version = "2.5.0" 5650 + version = "2.6.0" 5651 5651 source = "registry+https://github.com/rust-lang/crates.io-index" 5652 - checksum = "1063890b541877c4367d0ee9b1758360d86398862fbaafdd99aac02a55e28bf8" 5652 + checksum = "a59139183e0907cec1499dddee4e085f5a801dc659efa0848ee224f461371426" 5653 5653 dependencies = [ 5654 5654 "android_logger", 5655 5655 "byte-unit", ··· 5669 5669 5670 5670 [[package]] 5671 5671 name = "tauri-plugin-notification" 5672 - version = "2.2.3" 5672 + version = "2.3.0" 5673 5673 source = "registry+https://github.com/rust-lang/crates.io-index" 5674 - checksum = "d1c87f171cdb35c3aa8f17e8dfd84c1b9f68eb4086ec16a5a1b9f13b5541c574" 5674 + checksum = "cfe06ed89cff6d0ec06ff4f544fb961e4718348a33309f56ccb2086e77bc9116" 5675 5675 dependencies = [ 5676 5676 "log", 5677 5677 "notify-rust", ··· 5688 5688 5689 5689 [[package]] 5690 5690 name = "tauri-plugin-opener" 5691 - version = "2.3.0" 5691 + version = "2.4.0" 5692 5692 source = "registry+https://github.com/rust-lang/crates.io-index" 5693 - checksum = "2c8983f50326d34437142a6d560b5c3426e91324297519b6eeb32ed0a1d1e0f2" 5693 + checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321" 5694 5694 dependencies = [ 5695 5695 "dunce", 5696 5696 "glob", ··· 5710 5710 5711 5711 [[package]] 5712 5712 name = "tauri-plugin-store" 5713 - version = "2.2.1" 5713 + version = "2.3.0" 5714 5714 source = "registry+https://github.com/rust-lang/crates.io-index" 5715 - checksum = "ada7e7aeea472dec9b8d09d25301e59fe3e8330dc11dbcf903d6388126cb3722" 5715 + checksum = "5916c609664a56c82aeaefffca9851fd072d4d41f73d63f22ee3ee451508194f" 5716 5716 dependencies = [ 5717 5717 "dunce", 5718 5718 "serde", ··· 5726 5726 5727 5727 [[package]] 5728 5728 name = "tauri-runtime" 5729 - version = "2.6.0" 5729 + version = "2.7.0" 5730 5730 source = "registry+https://github.com/rust-lang/crates.io-index" 5731 - checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39" 5731 + checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4" 5732 5732 dependencies = [ 5733 5733 "cookie", 5734 5734 "dpi", ··· 5748 5748 5749 5749 [[package]] 5750 5750 name = "tauri-runtime-wry" 5751 - version = "2.6.0" 5751 + version = "2.7.0" 5752 5752 source = "registry+https://github.com/rust-lang/crates.io-index" 5753 - checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2" 5753 + checksum = "fe52ed0ef40fd7ad51a620ecb3018e32eba3040bb95025216a962a37f6f050c5" 5754 5754 dependencies = [ 5755 5755 "gtk", 5756 5756 "http", ··· 5803 5803 5804 5804 [[package]] 5805 5805 name = "tauri-utils" 5806 - version = "2.4.0" 5806 + version = "2.5.0" 5807 5807 source = "registry+https://github.com/rust-lang/crates.io-index" 5808 - checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4" 5808 + checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e" 5809 5809 dependencies = [ 5810 5810 "anyhow", 5811 5811 "brotli", ··· 5846 5846 checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" 5847 5847 dependencies = [ 5848 5848 "embed-resource", 5849 - "indexmap 2.9.0", 5849 + "indexmap 2.10.0", 5850 5850 "toml", 5851 5851 ] 5852 5852 ··· 5885 5885 "mac", 5886 5886 "utf-8", 5887 5887 ] 5888 - 5889 - [[package]] 5890 - name = "thin-slice" 5891 - version = "0.1.1" 5892 - source = "registry+https://github.com/rust-lang/crates.io-index" 5893 - checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" 5894 5888 5895 5889 [[package]] 5896 5890 name = "thiserror" ··· 5939 5933 checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 5940 5934 dependencies = [ 5941 5935 "deranged", 5942 - "itoa 1.0.15", 5936 + "itoa", 5943 5937 "libc", 5944 5938 "num-conv", 5945 5939 "num_threads", ··· 6094 6088 source = "registry+https://github.com/rust-lang/crates.io-index" 6095 6089 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 6096 6090 dependencies = [ 6097 - "indexmap 2.9.0", 6091 + "indexmap 2.10.0", 6098 6092 "toml_datetime", 6099 6093 "winnow 0.5.40", 6100 6094 ] ··· 6105 6099 source = "registry+https://github.com/rust-lang/crates.io-index" 6106 6100 checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" 6107 6101 dependencies = [ 6108 - "indexmap 2.9.0", 6102 + "indexmap 2.10.0", 6109 6103 "toml_datetime", 6110 6104 "winnow 0.5.40", 6111 6105 ] ··· 6116 6110 source = "registry+https://github.com/rust-lang/crates.io-index" 6117 6111 checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 6118 6112 dependencies = [ 6119 - "indexmap 2.9.0", 6113 + "indexmap 2.10.0", 6120 6114 "serde", 6121 6115 "serde_spanned", 6122 6116 "toml_datetime", ··· 6211 6205 6212 6206 [[package]] 6213 6207 name = "tray-icon" 6214 - version = "0.20.1" 6208 + version = "0.21.0" 6215 6209 source = "registry+https://github.com/rust-lang/crates.io-index" 6216 - checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" 6210 + checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a" 6217 6211 dependencies = [ 6218 6212 "crossbeam-channel", 6219 6213 "dirs", ··· 6907 6901 6908 6902 [[package]] 6909 6903 name = "webview2-com" 6910 - version = "0.37.0" 6904 + version = "0.38.0" 6911 6905 source = "registry+https://github.com/rust-lang/crates.io-index" 6912 - checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601" 6906 + checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" 6913 6907 dependencies = [ 6914 6908 "webview2-com-macros", 6915 6909 "webview2-com-sys", ··· 6932 6926 6933 6927 [[package]] 6934 6928 name = "webview2-com-sys" 6935 - version = "0.37.0" 6929 + version = "0.38.0" 6936 6930 source = "registry+https://github.com/rust-lang/crates.io-index" 6937 - checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295" 6931 + checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" 6938 6932 dependencies = [ 6939 6933 "thiserror 2.0.12", 6940 6934 "windows", ··· 7073 7067 7074 7068 [[package]] 7075 7069 name = "windows-registry" 7076 - version = "0.5.2" 7070 + version = "0.5.3" 7077 7071 source = "registry+https://github.com/rust-lang/crates.io-index" 7078 - checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" 7072 + checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 7079 7073 dependencies = [ 7080 7074 "windows-link", 7081 7075 "windows-result", ··· 7384 7378 7385 7379 [[package]] 7386 7380 name = "wry" 7387 - version = "0.51.2" 7381 + version = "0.52.1" 7388 7382 source = "registry+https://github.com/rust-lang/crates.io-index" 7389 - checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" 7383 + checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" 7390 7384 dependencies = [ 7391 7385 "base64 0.22.1", 7392 7386 "block2 0.6.1",
+2 -2
TODO.md
··· 23 23 - [x] Transport : Handle transport cancellation better 24 24 - [x] Backend : Add checks for when the `powerup_locations` field is an empty array in settings 25 25 - [ ] Backend : More tests 26 - - [ ] Lobby tests 27 - - [ ] Game end test for actual return from loop 26 + - [x] Lobby tests 27 + - [x] Game end test for actual return from loop 28 28 - [ ] Testing crate for integration testing from a DSL 29 29 - [ ] NixOS VM tests wrapping the testing crate 30 30 - [ ] Nix : Cheat the dependency nightmare and use crane
+1 -1
backend/Cargo.toml
··· 3 3 version = "0.1.0" 4 4 description = "A mobile app for playing the game \"manhunt\"" 5 5 authors = ["Ben C <bwc9876@gmail.com>"] 6 - edition = "2021" 6 + edition = "2024" 7 7 default-run = "manhunt-app" 8 8 9 9 [lib]
+3 -1
backend/src/export_types.rs
··· 8 8 let path = args.get(1).expect("Usage: export-types path"); 9 9 let specta = mk_specta(); 10 10 let mut lang = Typescript::new(); 11 - lang.header = Cow::Borrowed("/* eslint @typescript-eslint/no-unused-vars: 0 */\n/* eslint @typescript-eslint/no-explicit-any: 0 */"); 11 + lang.header = Cow::Borrowed( 12 + "/* eslint @typescript-eslint/no-unused-vars: 0 */\n/* eslint @typescript-eslint/no-explicit-any: 0 */", 13 + ); 12 14 specta.export(lang, path).expect("Failed to export types"); 13 15 println!("Successfully exported types, events, and commands to {path}",); 14 16 }
+4 -4
backend/src/lib.rs
··· 6 6 7 7 use anyhow::Context; 8 8 use location::TauriLocation; 9 - use log::{error, info, warn, LevelFilter}; 9 + use log::{LevelFilter, error, info, warn}; 10 10 use manhunt_logic::{ 11 11 Game as BaseGame, GameSettings, GameUiState, Lobby as BaseLobby, LobbyState, PlayerProfile, 12 12 StartGameInfo, StateUpdateSender, 13 13 }; 14 - use manhunt_transport::{generate_join_code, room_exists, MatchboxTransport}; 14 + use manhunt_transport::{MatchboxTransport, generate_join_code, room_exists}; 15 15 use serde::{Deserialize, Serialize}; 16 16 use tauri::{AppHandle, Manager, State}; 17 17 use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; 18 - use tauri_specta::{collect_commands, collect_events, ErrorHandlingMode, Event}; 18 + use tauri_specta::{ErrorHandlingMode, Event, collect_commands, collect_events}; 19 19 use tokio::sync::RwLock; 20 20 use uuid::Uuid; 21 21 ··· 213 213 let mut state = state_handle.write().await; 214 214 match res { 215 215 Ok(Some(start)) => { 216 - info!("Starting game as"); 216 + info!("Starting Game"); 217 217 state.start_game(app_game, start).await; 218 218 } 219 219 Ok(None) => {
+6 -6
flake.lock
··· 36 36 }, 37 37 "nixpkgs_2": { 38 38 "locked": { 39 - "lastModified": 1750731501, 40 - "narHash": "sha256-Ah4qq+SbwMaGkuXCibyg+Fwn00el4KmI3XFX6htfDuk=", 39 + "lastModified": 1750811787, 40 + "narHash": "sha256-rD/978c35JXz6JLAzciTIOCMenPumF6zrQOj4rVZeHE=", 41 41 "owner": "NixOS", 42 42 "repo": "nixpkgs", 43 - "rev": "69dfebb3d175bde602f612915c5576a41b18486b", 43 + "rev": "992f916556fcfaa94451ebc7fc6e396134bbf5b1", 44 44 "type": "github" 45 45 }, 46 46 "original": { ··· 62 62 "nixpkgs": ["nixpkgs"] 63 63 }, 64 64 "locked": { 65 - "lastModified": 1750732748, 66 - "narHash": "sha256-HR2b3RHsPeJm+Fb+1ui8nXibgniVj7hBNvUbXEyz0DU=", 65 + "lastModified": 1750819193, 66 + "narHash": "sha256-XvkupGPZqD54HuKhN/2WhbKjAHeTl1UEnWspzUzRFfA=", 67 67 "owner": "oxalica", 68 68 "repo": "rust-overlay", 69 - "rev": "4b4494b2ba7e8a8041b2e28320b2ee02c115c75f", 69 + "rev": "1ba3b9c59b68a4b00156827ad46393127b51b808", 70 70 "type": "github" 71 71 }, 72 72 "original": {
+1 -1
flake.nix
··· 18 18 formatters = pkgs: let 19 19 prettier = "${pkgs.prettier}/bin/prettier --write ."; 20 20 alejandra = "${pkgs.alejandra}/bin/alejandra ."; 21 - rustfmt = "${pkgs.rustfmt}/bin/rustfmt"; 21 + rustfmt = "${pkgs.rustfmt}/bin/rustfmt fmt"; 22 22 just = "${pkgs.just}/bin/just --fmt --unstable"; 23 23 in { 24 24 "justfile" = just;
+2 -1
manhunt-logic/Cargo.toml
··· 10 10 rand_chacha = "0.9.0" 11 11 serde = { version = "1.0.219", features = ["derive"] } 12 12 specta = { version = "=2.0.0-rc.22", features = ["uuid", "chrono", "derive"] } 13 - tokio = { version = "1.45.1", features = ["macros", "rt", "sync", "time"] } 13 + tokio = { version = "1.45.1", features = ["macros", "rt", "sync", "time", "test-util"] } 14 + tokio-util = "0.7.15" 14 15 uuid = { version = "1.17.0", features = ["serde", "v4"] }
+151 -94
manhunt-logic/src/game.rs
··· 1 1 use anyhow::bail; 2 2 use chrono::{DateTime, Utc}; 3 3 use std::{sync::Arc, time::Duration}; 4 + use tokio_util::sync::CancellationToken; 4 5 use uuid::Uuid; 5 6 6 - use tokio::{sync::RwLock, time::MissedTickBehavior}; 7 + use tokio::sync::RwLock; 7 8 8 9 use crate::StartGameInfo; 9 10 use crate::{prelude::*, transport::TransportMessage}; ··· 35 36 location: L, 36 37 state_update_sender: S, 37 38 interval: Duration, 39 + cancel: CancellationToken, 38 40 } 39 41 40 42 impl<L: LocationService, T: Transport, S: StateUpdateSender> Game<L, T, S> { ··· 57 59 interval, 58 60 state: RwLock::new(state), 59 61 state_update_sender, 62 + cancel: CancellationToken::new(), 60 63 } 61 64 } 62 65 ··· 71 74 state.remove_ping(id); 72 75 // TODO: Maybe reroll for new powerups (specifically seeker ones) instead of just erasing it 73 76 state.use_powerup(); 74 - 75 - self.send_event(GameEvent::PlayerCaught(state.id)).await; 77 + drop(state); 78 + self.send_event(GameEvent::PlayerCaught(id)).await; 76 79 } 77 80 78 81 pub async fn clone_settings(&self) -> GameSettings { ··· 250 253 false 251 254 } 252 255 256 + pub async fn quit_game(&self) { 257 + self.cancel.cancel(); 258 + } 259 + 253 260 #[cfg(test)] 254 - pub async fn force_tick(&self, now: UtcDT) { 255 - let mut state = self.state.write().await; 256 - self.tick(&mut state, now).await; 261 + fn get_now() -> UtcDT { 262 + let fake = tokio::time::Instant::now(); 263 + let real = std::time::Instant::now(); 264 + Utc::now() + (fake.into_std().duration_since(real) + Duration::from_secs(1)) 257 265 } 258 266 259 - pub async fn quit_game(&self) { 260 - self.transport.disconnect().await; 267 + #[cfg(not(test))] 268 + fn get_now() -> UtcDT { 269 + Utc::now() 261 270 } 262 271 263 272 /// Main loop of the game, handles ticking and receiving messages from [Transport]. 264 273 pub async fn main_loop(&self) -> Result<Option<GameHistory>> { 265 274 let mut interval = tokio::time::interval(self.interval); 266 275 267 - interval.set_missed_tick_behavior(MissedTickBehavior::Delay); 276 + // interval.set_missed_tick_behavior(MissedTickBehavior::Delay); 268 277 269 278 let res = 'game: loop { 270 279 tokio::select! { 271 280 biased; 281 + 282 + _ = self.cancel.cancelled() => { 283 + break 'game Ok(None); 284 + } 272 285 273 286 messages = self.transport.receive_messages() => { 274 287 let mut state = self.state.write().await; ··· 286 299 287 300 _ = interval.tick() => { 288 301 let mut state = self.state.write().await; 289 - let should_break = self.tick(&mut state, Utc::now()).await; 302 + let should_break = self.tick(&mut state, Self::get_now()).await; 290 303 291 304 if should_break { 292 305 let history = state.as_game_history(); ··· 313 326 }; 314 327 315 328 use super::*; 316 - use tokio::{task::yield_now, test}; 329 + use tokio::{sync::oneshot, task::yield_now, test}; 317 330 318 331 type TestGame = Game<MockLocation, MockTransport, DummySender>; 319 332 333 + type EndRecv = oneshot::Receiver<Result<Option<GameHistory>>>; 334 + 320 335 struct MockMatch { 321 336 uuids: Vec<Uuid>, 322 - games: HashMap<u32, Arc<TestGame>>, 337 + games: Vec<Arc<TestGame>>, 323 338 settings: GameSettings, 324 - mock_now: UtcDT, 325 339 } 326 340 327 - const INTERVAL: Duration = Duration::from_secs(u64::MAX); 341 + const INTERVAL: Duration = Duration::from_secs(600000); 328 342 329 343 impl MockMatch { 330 344 pub fn new(settings: GameSettings, players: u32, seekers: u32) -> Self { 345 + tokio::time::pause(); 331 346 let (uuids, transports) = MockTransport::create_mesh(players); 332 347 333 348 let initial_caught_state = (0..players) ··· 336 351 337 352 let games = transports 338 353 .into_iter() 339 - .enumerate() 340 - .map(|(id, transport)| { 354 + .map(|transport| { 341 355 let location = MockLocation; 342 356 let start_info = StartGameInfo { 343 357 initial_caught_state: initial_caught_state.clone(), ··· 351 365 DummySender, 352 366 ); 353 367 354 - (id as u32, Arc::new(game)) 368 + Arc::new(game) 355 369 }) 356 - .collect::<HashMap<_, _>>(); 370 + .collect(); 357 371 358 372 Self { 359 373 settings, 360 374 games, 361 375 uuids, 362 - mock_now: Utc::now(), 363 376 } 364 377 } 365 378 366 - pub async fn start(&self) { 367 - for game in self.games.values() { 379 + pub async fn start(&self) -> Vec<EndRecv> { 380 + let mut recvs = Vec::with_capacity(self.games.len()); 381 + for game in self.games.iter() { 368 382 let game = game.clone(); 383 + let (send, recv) = oneshot::channel(); 384 + recvs.push(recv); 369 385 tokio::spawn(async move { 370 - game.main_loop().await.expect("Game Start Fail"); 386 + let res = game.main_loop().await; 387 + send.send(res).expect("Failed to send"); 371 388 }); 372 389 yield_now().await; 373 390 } 391 + recvs 374 392 } 375 393 376 - pub async fn pass_time(&mut self, d: Duration) { 377 - self.mock_now += d; 378 - } 379 - 380 - pub async fn assert_all_states(&self, f: impl Fn(&GameState)) { 381 - for game in self.games.values() { 394 + pub async fn assert_all_states(&self, f: impl Fn(usize, &GameState)) { 395 + for (i, game) in self.games.iter().enumerate() { 382 396 let state = game.state.read().await; 383 - f(&state); 397 + f(i, &state); 384 398 } 385 399 } 386 400 387 - pub fn game(&self, id: u32) -> &TestGame { 388 - self.games.get(&id).as_ref().unwrap() 401 + pub fn assert_all_transports_disconnected(&self) { 402 + for game in self.games.iter() { 403 + assert!( 404 + game.transport.is_disconnected(), 405 + "Game {} is still connected", 406 + game.transport.self_id() 407 + ); 408 + } 389 409 } 390 410 391 411 pub async fn wait_for_seekers(&mut self) { 392 412 let hiding_time = Duration::from_secs(self.settings.hiding_time_seconds as u64 + 1); 393 - self.mock_now += hiding_time; 413 + 414 + tokio::time::sleep(hiding_time).await; 394 415 395 416 self.tick().await; 396 417 397 - self.assert_all_states(|s| { 398 - assert!(s.seekers_released()); 418 + self.assert_all_states(|i, s| { 419 + assert!(s.seekers_released(), "Seekers not released on game {i}"); 399 420 }) 400 421 .await; 401 422 } 402 423 403 - async fn tick_all(&self, now: UtcDT) { 404 - for game in self.games.values() { 405 - game.force_tick(now).await; 424 + pub async fn wait_for_transports(&self) { 425 + for game in self.games.iter() { 426 + game.transport.wait_for_queue_empty().await; 406 427 } 407 428 } 408 429 409 430 pub async fn tick(&self) { 410 - self.tick_all(self.mock_now).await; 431 + tokio::time::sleep(INTERVAL + Duration::from_secs(1)).await; 432 + self.wait_for_transports().await; 411 433 yield_now().await; 412 434 } 413 435 } ··· 436 458 // 2 players, one is a seeker 437 459 let mut mat = MockMatch::new(settings, 2, 1); 438 460 439 - mat.start().await; 461 + let recvs = mat.start().await; 440 462 441 463 mat.wait_for_seekers().await; 442 464 443 - mat.game(1).mark_caught().await; 465 + mat.games[1].mark_caught().await; 444 466 445 - mat.tick().await; 467 + mat.wait_for_transports().await; 446 468 447 - mat.assert_all_states(|s| { 469 + mat.assert_all_states(|i, s| { 448 470 assert_eq!( 449 471 s.get_caught(mat.uuids[1]), 450 472 Some(true), 451 - "Game {} sees player 1 as not caught", 452 - s.id 473 + "Game {i} sees player 1 as not caught", 453 474 ); 454 475 }) 455 476 .await; 456 477 457 - // Extra tick for post-game syncing 478 + // Tick to process game end 479 + mat.tick().await; 480 + 481 + mat.assert_all_states(|i, s| { 482 + assert!(s.game_ended(), "Game {} has not ended", i); 483 + }) 484 + .await; 485 + 486 + // Tick for post-game sync 458 487 mat.tick().await; 459 488 460 - mat.assert_all_states(|s| assert!(s.game_ended(), "Game {} has not ended", s.id)) 461 - .await; 489 + mat.assert_all_transports_disconnected(); 490 + 491 + for (i, recv) in recvs.into_iter().enumerate() { 492 + let res = recv.await.expect("Failed to recv"); 493 + match res { 494 + Ok(Some(hist)) => { 495 + assert!(!hist.locations.is_empty(), "Game {i} has no locations"); 496 + assert!(!hist.events.is_empty(), "Game {i} has no event"); 497 + } 498 + Ok(None) => { 499 + panic!("Game {i} exited without a history (did not end via post game sync)"); 500 + } 501 + Err(why) => { 502 + panic!("Game {i} encountered error: {why:?}"); 503 + } 504 + } 505 + } 462 506 } 463 507 464 508 #[test] ··· 472 516 473 517 mat.wait_for_seekers().await; 474 518 475 - mat.assert_all_states(|s| { 519 + mat.assert_all_states(|i, s| { 476 520 for id in 0..4 { 477 521 let ping = s.get_ping(mat.uuids[id]); 478 522 if id == 0 { 479 523 assert!( 480 524 ping.is_none(), 481 - "Game 0 is a seeker and shouldn't be pinged (in {})", 482 - s.id 525 + "Game {i} has a ping for 0, despite them being a seeker", 483 526 ); 484 527 } else { 485 528 assert!( 486 529 ping.is_some(), 487 - "Game {} is a hider and should be pinged (in {})", 488 - id, 489 - s.id 530 + "Game {i} doesn't have a ping for {id}, despite them being a hider", 490 531 ); 491 532 } 492 533 } 493 534 }) 494 535 .await; 495 536 496 - mat.game(1).mark_caught().await; 537 + mat.games[1].mark_caught().await; 497 538 498 539 mat.tick().await; 499 540 500 - mat.assert_all_states(|s| { 541 + mat.assert_all_states(|i, s| { 501 542 for id in 0..4 { 502 543 let ping = s.get_ping(mat.uuids[id]); 503 544 if id <= 1 { 504 545 assert!( 505 546 ping.is_none(), 506 - "Game {} is a seeker and shouldn't be pinged (in {})", 507 - id, 508 - s.id 547 + "Game {i} has a ping for {id}, despite them being a seeker", 509 548 ); 510 549 } else { 511 550 assert!( 512 551 ping.is_some(), 513 - "Game {} is a hider and should be pinged (in {})", 514 - id, 515 - s.id 552 + "Game {i} doesn't have a ping for {id}, despite them being a hider", 516 553 ); 517 554 } 518 555 } ··· 539 576 mat.start().await; 540 577 mat.tick().await; 541 578 mat.wait_for_seekers().await; 542 - mat.pass_time(Duration::from_secs(60)).await; 579 + tokio::time::sleep(Duration::from_secs(60)).await; 543 580 mat.tick().await; 544 581 545 - let game = mat.game(0); 582 + let game = mat.games[0].clone(); 546 583 let state = game.state.read().await; 547 584 let location = state.powerup_location().expect("Powerup didn't spawn"); 548 585 549 586 drop(state); 550 587 551 - mat.assert_all_states(|s| { 588 + mat.assert_all_states(|i, s| { 552 589 assert_eq!( 553 590 s.powerup_location(), 554 591 Some(location), 555 - "Game {} has a different location than 0", 556 - s.id 592 + "Game {i} has a different location than 0", 557 593 ); 558 594 }) 559 595 .await; ··· 562 598 #[test] 563 599 async fn test_powerup_ping_seeker_as_you() { 564 600 let mut settings = mk_settings(); 565 - settings.ping_minutes_interval = 0; 601 + settings.ping_minutes_interval = 1; 566 602 let mut mat = MockMatch::new(settings, 2, 1); 567 603 568 604 mat.start().await; 569 605 mat.wait_for_seekers().await; 570 606 571 - let game = mat.game(1); 607 + mat.tick().await; 608 + 609 + tokio::time::sleep(Duration::from_secs(60)).await; 610 + 611 + let game = mat.games[1].clone(); 572 612 let mut state = game.state.write().await; 573 613 state.force_set_powerup(PowerUpType::PingSeeker); 574 614 drop(state); 575 615 576 616 mat.tick().await; 577 617 578 - mat.assert_all_states(|s| { 618 + mat.assert_all_states(|i, s| { 579 619 if let Some(ping) = s.get_ping(mat.uuids[1]) { 580 620 assert_eq!( 581 621 ping.real_player, mat.uuids[0], 582 - "Ping for 1 is not truly 0 (in {})", 583 - s.id 622 + "Game {i} has a ping for 1, but it wasn't from 0" 584 623 ); 585 624 } else { 586 - panic!("No ping for 1 (in {})", s.id); 625 + panic!("Game {i} has no ping for 1"); 587 626 } 588 627 }) 589 628 .await; ··· 591 630 592 631 #[test] 593 632 async fn test_powerup_ping_random_hider() { 594 - let settings = mk_settings(); 633 + let mut settings = mk_settings(); 634 + settings.ping_minutes_interval = u32::MAX; 595 635 596 636 let mut mat = MockMatch::new(settings, 3, 1); 597 637 598 638 mat.start().await; 599 639 mat.wait_for_seekers().await; 600 640 601 - let game = mat.game(1); 641 + let game = mat.games[1].clone(); 602 642 let mut state = game.state.write().await; 603 643 state.force_set_powerup(PowerUpType::ForcePingOther); 604 644 drop(state); ··· 606 646 game.use_powerup().await; 607 647 mat.tick().await; 608 648 609 - mat.assert_all_states(|s| { 610 - // Player 0 is a seeker, player 1 user the powerup, so 2 is the only one that should 611 - // could have pinged 612 - assert!(s.get_ping(mat.uuids[2]).is_some()); 613 - assert!(s.get_ping(mat.uuids[0]).is_none()); 614 - assert!(s.get_ping(mat.uuids[1]).is_none()); 649 + mat.assert_all_states(|i, s| { 650 + // Player 0 is a seeker, player 1 used the powerup, so 2 is the only one that should 651 + // have pinged 652 + assert!( 653 + s.get_ping(mat.uuids[2]).is_some(), 654 + "Ping 2 is not present in game {i}" 655 + ); 656 + assert!( 657 + s.get_ping(mat.uuids[0]).is_none(), 658 + "Ping 0 is present in game {i}" 659 + ); 660 + assert!( 661 + s.get_ping(mat.uuids[1]).is_none(), 662 + "Ping 1 is present in game {i}" 663 + ); 615 664 }) 616 665 .await; 617 666 } ··· 624 673 625 674 mat.start().await; 626 675 627 - let game = mat.game(3); 676 + mat.tick().await; 677 + 678 + let game = mat.games[3].clone(); 628 679 let mut state = game.state.write().await; 629 680 state.force_set_powerup(PowerUpType::PingAllSeekers); 630 681 drop(state); 631 682 632 683 game.use_powerup().await; 684 + // One tick to send out the ForcePing 685 + mat.tick().await; 686 + // One tick to for the seekers to reply 633 687 mat.tick().await; 634 688 635 - mat.assert_all_states(|s| { 689 + mat.assert_all_states(|i, s| { 636 690 for id in 0..3 { 637 691 assert!( 638 - s.get_caught(mat.uuids[id]).is_some(), 639 - "Player {} should be pinged due to the powerup (in {})", 640 - id, 641 - s.id 692 + &s.get_ping(mat.uuids[id]).is_some(), 693 + "Game {i} does not have a ping for {id}, despite the powerup being active", 642 694 ); 643 695 } 644 696 }) ··· 650 702 let settings = mk_settings(); 651 703 let mat = MockMatch::new(settings, 4, 1); 652 704 653 - mat.start().await; 705 + let mut recvs = mat.start().await; 654 706 655 - let game = mat.game(2); 656 - game.quit_game().await; 707 + let game = mat.games[2].clone(); 657 708 let id = game.state.read().await.id; 658 709 710 + game.quit_game().await; 711 + let res = recvs.swap_remove(2).await.expect("Failed to recv"); 712 + assert!(res.is_ok_and(|o| o.is_none()), "2 did not exit cleanly"); 713 + assert!( 714 + game.transport.is_disconnected(), 715 + "2's transport is not disconnected" 716 + ); 717 + 659 718 mat.tick().await; 660 719 661 - mat.assert_all_states(|s| { 720 + mat.assert_all_states(|i, s| { 662 721 if s.id != id { 663 722 assert!( 664 723 s.get_ping(id).is_none(), 665 - "Game {} has not removed 2 from pings", 666 - s.id 724 + "Game {i} has not removed 2 from pings", 667 725 ); 668 726 assert!( 669 727 s.get_caught(id).is_none(), 670 - "Game {} has not removed 2 from caught state", 671 - s.id 728 + "Game {i} has not removed 2 from caught state", 672 729 ); 673 730 } 674 731 })
+3 -3
manhunt-logic/src/game_state.rs
··· 2 2 3 3 use chrono::Utc; 4 4 use rand::{ 5 + Rng, SeedableRng, 5 6 distr::{Bernoulli, Distribution}, 6 7 seq::{IndexedRandom, IteratorRandom}, 7 - Rng, SeedableRng, 8 8 }; 9 9 use rand_chacha::ChaCha20Rng; 10 10 use serde::{Deserialize, Serialize}; ··· 416 416 my_id: Uuid, 417 417 pub game_started: UtcDT, 418 418 game_ended: UtcDT, 419 - events: Vec<(UtcDT, GameEvent)>, 420 - locations: Vec<(Uuid, Vec<(UtcDT, Location)>)>, 419 + pub events: Vec<(UtcDT, GameEvent)>, 420 + pub locations: Vec<(Uuid, Vec<(UtcDT, Location)>)>, 421 421 } 422 422 423 423 /// Subset of [GameState] that is meant to be sent to a UI frontend
+368 -18
manhunt-logic/src/lobby.rs
··· 3 3 use anyhow::anyhow; 4 4 use serde::{Deserialize, Serialize}; 5 5 use tokio::sync::Mutex; 6 + use tokio_util::sync::CancellationToken; 6 7 use uuid::Uuid; 7 8 8 9 use crate::{ ··· 31 32 PlayerSwitch(Uuid, bool), 32 33 } 33 34 34 - #[derive(Clone, Serialize, Deserialize, specta::Type)] 35 + #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] 35 36 pub struct LobbyState { 36 37 profiles: HashMap<Uuid, PlayerProfile>, 37 38 join_code: String, ··· 48 49 state: Mutex<LobbyState>, 49 50 transport: Arc<T>, 50 51 state_updates: U, 52 + cancel: CancellationToken, 51 53 } 52 54 53 55 impl<T: Transport, U: StateUpdateSender> Lobby<T, U> { 54 56 pub async fn new( 55 57 join_code: &str, 56 - host: bool, 58 + is_host: bool, 57 59 profile: PlayerProfile, 58 60 settings: GameSettings, 59 61 state_updates: U, 60 62 ) -> Result<Arc<Self>> { 61 - let transport = T::initialize(join_code, host) 63 + let transport = T::initialize(join_code, is_host) 62 64 .await 63 65 .context("Failed to connect to lobby")?; 64 66 67 + let lobby = Arc::new(Self::new_with_transport( 68 + join_code, 69 + is_host, 70 + profile, 71 + settings, 72 + state_updates, 73 + transport, 74 + )); 75 + 76 + Ok(lobby) 77 + } 78 + 79 + pub fn new_with_transport( 80 + join_code: &str, 81 + is_host: bool, 82 + profile: PlayerProfile, 83 + settings: GameSettings, 84 + state_updates: U, 85 + transport: Arc<T>, 86 + ) -> Self { 65 87 let self_id = transport.self_id(); 66 - 67 - let lobby = Arc::new(Self { 88 + Self { 68 89 transport, 69 90 state_updates, 70 - is_host: host, 91 + is_host, 92 + cancel: CancellationToken::new(), 71 93 join_code: join_code.to_string(), 72 94 state: Mutex::new(LobbyState { 73 95 teams: HashMap::from_iter([(self_id, false)]), 74 96 join_code: join_code.to_string(), 75 97 profiles: HashMap::from_iter([(self_id, profile)]), 76 98 self_id, 77 - is_host: host, 99 + is_host, 78 100 settings, 79 101 }), 80 - }); 81 - 82 - Ok(lobby) 102 + } 83 103 } 84 104 85 105 fn emit_state_update(&self) { ··· 134 154 .await 135 155 .map(|start_game| Ok(Some(start_game))), 136 156 TransportMessage::PeerConnect(peer) => { 137 - let mut state = self.state.lock().await; 138 - state.teams.insert(peer, false); 157 + let state = self.state.lock().await; 139 158 let id = state.self_id; 140 159 let msg = LobbyMessage::PlayerSync(id, state.profiles[&id].clone()); 160 + let msg2 = LobbyMessage::PlayerSwitch(id, state.teams[&id]); 141 161 drop(state); 142 162 self.send_transport_message(Some(peer), msg).await; 163 + self.send_transport_message(Some(peer), msg2).await; 143 164 if self.is_host { 144 165 let state = self.state.lock().await; 145 166 let msg = LobbyMessage::HostPush(state.settings.clone()); ··· 163 184 let res = 'lobby: loop { 164 185 self.emit_state_update(); 165 186 166 - let msgs = self.transport.receive_messages().await; 187 + tokio::select! { 188 + biased; 189 + 190 + msgs = self.transport.receive_messages() => { 191 + for (peer, msg) in msgs { 192 + if let Some(res) = self.handle_message(peer, msg).await { 193 + break 'lobby res; 194 + } 195 + } 196 + } 167 197 168 - for (peer, msg) in msgs { 169 - if let Some(res) = self.handle_message(peer, msg).await { 170 - break 'lobby res; 198 + _ = self.cancel.cancelled() => { 199 + break Ok(None); 171 200 } 172 201 } 173 202 }; 174 203 175 - if res.is_err() { 204 + if let Ok(None) | Err(_) = res { 176 205 self.transport.disconnect().await; 177 206 } 178 207 ··· 234 263 } 235 264 236 265 pub async fn quit_lobby(&self) { 237 - self.transport.disconnect().await; 266 + self.cancel.cancel(); 267 + } 268 + } 269 + 270 + #[cfg(test)] 271 + mod tests { 272 + use super::*; 273 + use std::sync::Arc; 274 + use tokio::{sync::oneshot, task::yield_now, test}; 275 + 276 + use crate::tests::{DummySender, MockTransport}; 277 + 278 + type MockLobby = Lobby<MockTransport, DummySender>; 279 + 280 + type CompleteRecv = oneshot::Receiver<Result<Option<StartGameInfo>>>; 281 + 282 + struct MockLobbyPool { 283 + uuids: Vec<Uuid>, 284 + lobbies: Vec<Arc<MockLobby>>, 285 + } 286 + 287 + impl MockLobbyPool { 288 + pub fn new(num_players: u32) -> Self { 289 + let settings = GameSettings::default(); 290 + let (uuids, transports) = MockTransport::create_mesh(num_players); 291 + 292 + let lobbies = transports 293 + .into_iter() 294 + .enumerate() 295 + .map(|(i, transport)| { 296 + let profile = PlayerProfile { 297 + display_name: format!("Lobby {i} ({})", uuids[i]), 298 + pfp_base64: None, 299 + }; 300 + 301 + Arc::new(MockLobby::new_with_transport( 302 + "aaa", 303 + i == 0, 304 + profile, 305 + settings.clone(), 306 + DummySender, 307 + Arc::new(transport), 308 + )) 309 + }) 310 + .collect(); 311 + 312 + Self { uuids, lobbies } 313 + } 314 + 315 + pub async fn wait(&self) { 316 + for lobby in self.lobbies.iter() { 317 + lobby.transport.wait_for_queue_empty().await; 318 + } 319 + yield_now().await; 320 + } 321 + 322 + pub async fn start_all_loops(&self) -> Vec<CompleteRecv> { 323 + let mut recv_set = Vec::with_capacity(self.lobbies.len()); 324 + for lobby in self.lobbies.iter() { 325 + let lobby = lobby.clone(); 326 + let (send, recv) = oneshot::channel(); 327 + recv_set.push(recv); 328 + tokio::spawn(async move { 329 + let res = lobby.main_loop().await; 330 + send.send(res).ok(); 331 + }); 332 + } 333 + recv_set 334 + } 335 + 336 + pub async fn player_join(&self, i: usize) { 337 + self.lobbies[i].transport.fake_join().await; 338 + } 339 + 340 + pub async fn assert_state(&self, i: usize, f: impl Fn(&LobbyState)) { 341 + let state = self.lobbies[i].state.lock().await; 342 + f(&state); 343 + } 344 + 345 + pub async fn assert_all_states(&self, f: impl Fn(usize, &LobbyState)) { 346 + for (i, lobby) in self.lobbies.iter().enumerate() { 347 + let state = lobby.state.lock().await; 348 + f(i, &state); 349 + } 350 + } 351 + } 352 + 353 + #[test] 354 + async fn test_joins() { 355 + let mat = MockLobbyPool::new(3); 356 + 357 + mat.start_all_loops().await; 358 + 359 + mat.player_join(0).await; 360 + mat.player_join(1).await; 361 + 362 + mat.wait().await; 363 + 364 + for i in 0..=1 { 365 + for j in 0..=1 { 366 + mat.assert_state(i, |s| { 367 + assert!( 368 + s.teams.contains_key(&mat.uuids[j]), 369 + "{i} doesn't have {j}'s uuid in teams" 370 + ); 371 + assert!( 372 + s.profiles.contains_key(&mat.uuids[j]), 373 + "{i} doesn't have {j}'s uuid in profiles" 374 + ); 375 + }) 376 + .await; 377 + } 378 + } 379 + 380 + mat.lobbies[0].switch_teams(true).await; 381 + 382 + mat.wait().await; 383 + 384 + mat.player_join(2).await; 385 + 386 + mat.wait().await; 387 + 388 + mat.assert_all_states(|i, s| { 389 + for j in 0..=2 { 390 + assert!( 391 + s.teams.contains_key(&mat.uuids[j]), 392 + "{i} doesn't have {j}'s uuid in teams" 393 + ); 394 + assert!( 395 + s.profiles.contains_key(&mat.uuids[j]), 396 + "{i} doesn't have {j}'s uuid in profiles" 397 + ); 398 + assert_eq!( 399 + s.teams.get(&mat.uuids[0]).copied(), 400 + Some(true), 401 + "{i} doesn't see 0 as a seeker" 402 + ) 403 + } 404 + }) 405 + .await; 406 + } 407 + 408 + #[test] 409 + async fn test_team_switch() { 410 + let mat = MockLobbyPool::new(3); 411 + 412 + mat.start_all_loops().await; 413 + 414 + mat.lobbies[2].switch_teams(true).await; 415 + 416 + mat.wait().await; 417 + 418 + mat.assert_all_states(|i, s| { 419 + assert_eq!( 420 + s.teams.get(&mat.uuids[2]).copied(), 421 + Some(true), 422 + "{i} ({}) does not see 2 as a seeker", 423 + mat.uuids[i] 424 + ); 425 + }) 426 + .await; 427 + } 428 + 429 + #[test] 430 + async fn test_update_settings() { 431 + let mat = MockLobbyPool::new(2); 432 + 433 + mat.start_all_loops().await; 434 + 435 + let mut settings = GameSettings::default(); 436 + const UPDATED_ID: u32 = 284829; 437 + settings.hiding_time_seconds = UPDATED_ID; 438 + 439 + mat.lobbies[0].update_settings(settings).await; 440 + 441 + mat.wait().await; 442 + 443 + mat.assert_all_states(|i, s| { 444 + assert_eq!( 445 + s.settings.hiding_time_seconds, UPDATED_ID, 446 + "{i} ({}) did not get updated settings", 447 + mat.uuids[i] 448 + ) 449 + }) 450 + .await; 451 + } 452 + 453 + #[test] 454 + async fn test_update_settings_not_host() { 455 + let mat = MockLobbyPool::new(2); 456 + 457 + mat.start_all_loops().await; 458 + 459 + let mut settings = GameSettings::default(); 460 + let target = settings.hiding_time_seconds; 461 + const UPDATED_ID: u32 = 284829; 462 + settings.hiding_time_seconds = UPDATED_ID; 463 + 464 + mat.lobbies[1].update_settings(settings).await; 465 + 466 + mat.wait().await; 467 + 468 + mat.assert_all_states(|i, s| { 469 + assert_eq!( 470 + s.settings.hiding_time_seconds, target, 471 + "{i} ({}) updated settings despite 1 not being host", 472 + mat.uuids[i] 473 + ) 474 + }) 475 + .await; 476 + } 477 + 478 + #[test] 479 + async fn test_game_start() { 480 + let mat = MockLobbyPool::new(4); 481 + 482 + let recvs = mat.start_all_loops().await; 483 + 484 + for i in 0..3 { 485 + mat.player_join(i).await; 486 + } 487 + 488 + mat.lobbies[2].switch_teams(true).await; 489 + 490 + let settings = GameSettings { 491 + hiding_time_seconds: 45, 492 + ..Default::default() 493 + }; 494 + 495 + mat.lobbies[0].update_settings(settings).await; 496 + 497 + mat.lobbies[3].quit_lobby().await; 498 + 499 + mat.wait().await; 500 + 501 + mat.lobbies[0].start_game().await; 502 + 503 + mat.wait().await; 504 + 505 + for (i, recv) in recvs.into_iter().enumerate() { 506 + let res = recv.await.expect("Failed to recv"); 507 + match res { 508 + Ok(Some(StartGameInfo { 509 + settings, 510 + initial_caught_state, 511 + })) => { 512 + assert_eq!( 513 + settings.hiding_time_seconds, 45, 514 + "Lobby {i} does not match pushed settings" 515 + ); 516 + assert_eq!( 517 + initial_caught_state.len(), 518 + 3, 519 + "Lobby {i} does not have 3 entries in caught state" 520 + ); 521 + assert_eq!( 522 + initial_caught_state.get(&mat.uuids[2]).copied(), 523 + Some(true), 524 + "Lobby {i} does not see 2 as a seeker" 525 + ); 526 + assert!( 527 + initial_caught_state.keys().all(|id| *id != mat.uuids[3]), 528 + "Lobby {i} still has a disconnected player saved in caught state" 529 + ); 530 + let profiles = mat.lobbies[i].clone_profiles().await; 531 + assert_eq!( 532 + profiles.len(), 533 + 3, 534 + "Lobby {i} does not have 3 entries in profiles" 535 + ); 536 + assert!( 537 + profiles.keys().all(|id| *id != mat.uuids[3]), 538 + "Lobby {i} still has a disconnected player saved in profiles" 539 + ); 540 + } 541 + Ok(None) => { 542 + if i != 3 { 543 + panic!("Lobby {i} did not exit with start info"); 544 + } 545 + } 546 + Err(why) => { 547 + panic!("Lobby {i} had an error: {why:?}"); 548 + } 549 + } 550 + } 551 + } 552 + 553 + #[test] 554 + async fn test_drop_player() { 555 + let mat = MockLobbyPool::new(3); 556 + 557 + let mut recvs = mat.start_all_loops().await; 558 + 559 + mat.lobbies[1].quit_lobby().await; 560 + 561 + let res = recvs.swap_remove(1).await.expect("Failed to recv"); 562 + assert!(res.is_ok_and(|o| o.is_none()), "1 did not quit gracefully"); 563 + 564 + mat.wait().await; 565 + 566 + assert!( 567 + mat.lobbies[1].transport.is_disconnected(), 568 + "1 is not disconnected" 569 + ); 570 + 571 + let id = mat.uuids[1]; 572 + 573 + mat.assert_all_states(|i, s| { 574 + if mat.uuids[i] != id { 575 + assert!( 576 + !s.teams.contains_key(&id), 577 + "{} has not been removed 1 from teams", 578 + i 579 + ); 580 + assert!( 581 + !s.profiles.contains_key(&id), 582 + "{} has not been removed 1 from profiles", 583 + i 584 + ); 585 + } 586 + }) 587 + .await; 238 588 } 239 589 }
+2 -2
manhunt-logic/src/profile.rs
··· 2 2 3 3 #[derive(Clone, Default, Debug, Serialize, Deserialize, specta::Type)] 4 4 pub struct PlayerProfile { 5 - display_name: String, 6 - pfp_base64: Option<String>, 5 + pub display_name: String, 6 + pub pfp_base64: Option<String>, 7 7 }
+35 -12
manhunt-logic/src/tests.rs
··· 1 1 use std::{collections::HashMap, sync::Arc}; 2 2 3 - use tokio::sync::{Mutex, mpsc}; 3 + use tokio::{ 4 + sync::{Mutex, mpsc}, 5 + task::yield_now, 6 + }; 4 7 use uuid::Uuid; 5 8 6 9 use crate::{ ··· 24 27 .map(|_| uuid::Uuid::new_v4()) 25 28 .collect::<Vec<_>>(); 26 29 let channels = (0..players) 27 - .map(|_| tokio::sync::mpsc::channel(10)) 30 + .map(|_| tokio::sync::mpsc::channel(20)) 28 31 .collect::<Vec<_>>(); 29 32 let txs = channels 30 33 .iter() ··· 41 44 (uuids, transports) 42 45 } 43 46 47 + pub async fn wait_for_queue_empty(&self) { 48 + // println!("Waiting for {} queue to empty", self.id); 49 + loop { 50 + let all_empty = self 51 + .txs 52 + .values() 53 + .all(|tx| tx.is_closed() || tx.capacity() == tx.max_capacity()); 54 + 55 + if all_empty { 56 + break; 57 + } else { 58 + yield_now().await; 59 + } 60 + } 61 + } 62 + 63 + pub async fn fake_join(&self) { 64 + self.send_message(TransportMessage::PeerConnect(self.id)) 65 + .await; 66 + } 67 + 68 + pub fn is_disconnected(&self) -> bool { 69 + self.txs[&self.id].is_closed() 70 + } 71 + 44 72 fn new(id: Uuid, rx: GameEventRx, txs: HashMap<Uuid, GameEventTx>) -> Self { 45 73 Self { 46 74 id, ··· 63 91 async fn disconnect(&self) { 64 92 self.send_message(TransportMessage::PeerDisconnect(self.id)) 65 93 .await; 94 + let mut rx = self.rx.lock().await; 95 + rx.close(); 66 96 } 67 97 68 98 async fn receive_messages(&self) -> impl Iterator<Item = MsgPair> { ··· 74 104 75 105 async fn send_message(&self, msg: TransportMessage) { 76 106 for (_id, tx) in self.txs.iter().filter(|(id, _)| **id != self.id) { 77 - tx.send((Some(self.id), msg.clone())) 78 - .await 79 - .expect("Failed to send msg"); 107 + tx.send((Some(self.id), msg.clone())).await.ok(); 80 108 } 81 109 } 82 110 83 111 async fn send_message_single(&self, peer: Uuid, msg: TransportMessage) { 84 112 if let Some(tx) = self.txs.get(&peer) { 85 - tx.send((Some(self.id), msg)) 86 - .await 87 - .expect("Failed to send msg"); 113 + tx.send((Some(self.id), msg)).await.ok(); 88 114 } 89 115 } 90 116 91 117 async fn send_self(&self, msg: TransportMessage) { 92 - self.txs[&self.id] 93 - .send((Some(self.id), msg)) 94 - .await 95 - .expect("Failed to send msg"); 118 + self.txs[&self.id].send((Some(self.id), msg)).await.ok(); 96 119 } 97 120 98 121 fn self_id(&self) -> Uuid {
+113 -110
manhunt-transport/src/matchbox.rs
··· 1 - use std::{pin::Pin, sync::Arc, time::Duration}; 1 + use std::{collections::HashSet, pin::Pin, sync::Arc}; 2 2 3 3 use anyhow::{Context, anyhow}; 4 - use futures::FutureExt; 5 - use log::error; 4 + use futures::{ 5 + SinkExt, StreamExt, 6 + channel::mpsc::{UnboundedReceiver, UnboundedSender}, 7 + }; 8 + use log::{error, info}; 6 9 use matchbox_socket::{Error as SocketError, PeerId, PeerState, WebRtcSocket}; 7 10 use tokio::{ 8 11 sync::{Mutex, mpsc}, ··· 22 25 pub struct MatchboxTransport { 23 26 my_id: Uuid, 24 27 incoming: Queue, 25 - outgoing: Queue, 28 + all_peers: Mutex<HashSet<Uuid>>, 29 + msg_sender: UnboundedSender<MatchboxMsgPair>, 26 30 cancel_token: CancellationToken, 27 31 } 28 32 29 33 type LoopFutRes = Result<(), SocketError>; 34 + 35 + type MatchboxMsgPair = (PeerId, Box<[u8]>); 30 36 31 37 fn map_socket_error(err: SocketError) -> anyhow::Error { 32 38 match err { ··· 38 44 impl MatchboxTransport { 39 45 pub async fn new(join_code: &str, is_host: bool) -> Result<Arc<Self>> { 40 46 let (itx, irx) = mpsc::channel(15); 41 - let (otx, orx) = mpsc::channel(15); 42 47 43 48 let ws_url = server::room_url(join_code, is_host); 44 49 45 50 let (mut socket, mut loop_fut) = WebRtcSocket::new_reliable(&ws_url); 46 51 52 + let (mtx, mrx) = socket 53 + .take_channel(0) 54 + .expect("Failed to get channel") 55 + .split(); 56 + 47 57 let res = loop { 48 58 tokio::select! { 49 59 id = Self::wait_for_id(&mut socket) => { 50 60 if let Some(id) = id { 51 - break Ok(id); 61 + break Ok(id); 52 62 } 53 63 }, 54 64 res = &mut loop_fut => { ··· 66 76 let transport = Arc::new(Self { 67 77 my_id, 68 78 incoming: (itx, Mutex::new(irx)), 69 - outgoing: (otx, Mutex::new(orx)), 79 + all_peers: Mutex::new(HashSet::with_capacity(5)), 80 + msg_sender: mtx.clone(), 70 81 cancel_token: CancellationToken::new(), 71 82 }); 72 83 73 84 tokio::spawn({ 74 85 let transport = transport.clone(); 75 86 async move { 76 - transport.main_loop(socket, loop_fut).await; 87 + transport.main_loop(socket, loop_fut, mrx).await; 77 88 } 78 89 }); 79 90 80 91 Ok(transport) 81 92 } 82 93 Err(why) => { 94 + drop(mrx); 95 + mtx.close_channel(); 83 96 drop(socket); 84 - loop_fut.await.context("While disconnecting")?; 85 97 Err(why) 86 98 } 87 99 } ··· 104 116 .expect("Failed to push to incoming queue"); 105 117 } 106 118 107 - async fn push_many_incoming(&self, msgs: Vec<MsgPair>) { 108 - let senders = self 109 - .incoming 110 - .0 111 - .reserve_many(msgs.len()) 112 - .await 113 - .expect("Failed to reserve in incoming queue"); 114 - 115 - for (sender, msg) in senders.into_iter().zip(msgs.into_iter()) { 116 - sender.send(msg); 117 - } 118 - } 119 - 120 119 async fn main_loop( 121 120 &self, 122 121 mut socket: WebRtcSocket, 123 122 loop_fut: Pin<Box<dyn Future<Output = LoopFutRes> + Send + 'static>>, 123 + mut mrx: UnboundedReceiver<MatchboxMsgPair>, 124 124 ) { 125 - let loop_fut = async { 126 - let msg = match loop_fut.await { 127 - Ok(_) => TransportMessage::Disconnected, 128 - Err(e) => { 129 - let msg = map_socket_error(e).to_string(); 130 - TransportMessage::Error(msg) 131 - } 132 - }; 133 - self.push_incoming(None, msg).await; 134 - } 135 - .fuse(); 136 - 137 125 tokio::pin!(loop_fut); 138 - 139 - let mut interval = tokio::time::interval(Duration::from_secs(1)); 140 - 141 - let mut outgoing_rx = self.outgoing.1.lock().await; 142 - const MAX_MSG_SEND: usize = 30; 143 - let mut message_buffer = Vec::with_capacity(MAX_MSG_SEND); 144 - 145 126 let mut packet_handler = PacketHandler::default(); 146 127 147 - loop { 148 - self.handle_peers(&mut socket).await; 128 + info!("Starting transport loop"); 149 129 150 - self.handle_recv(&mut socket, &mut packet_handler).await; 151 - 130 + let (should_await, msg) = loop { 152 131 tokio::select! { 153 132 biased; 154 133 155 - _ = self.cancel_token.cancelled() => { 156 - break; 134 + res = &mut loop_fut => { 135 + info!("Transport-initiated disconnect"); 136 + break (false, match res { 137 + Ok(_) => TransportMessage::Disconnected, 138 + Err(e) => { 139 + let msg = map_socket_error(e).to_string(); 140 + TransportMessage::Error(msg) 141 + } 142 + }); 157 143 } 158 144 159 - _ = &mut loop_fut => { 160 - break; 145 + _ = self.cancel_token.cancelled() => { 146 + info!("Logic-initiated disconnect"); 147 + break (true, TransportMessage::Disconnected); 161 148 } 162 149 163 - _ = outgoing_rx.recv_many(&mut message_buffer, MAX_MSG_SEND) => { 164 - let peers = socket.connected_peers().collect::<Vec<_>>(); 165 - self.handle_send(&mut socket, &peers, &mut message_buffer).await; 150 + Some((peer, state)) = socket.next() => { 151 + info!("Handling peer {peer}: {state:?}"); 152 + self.handle_peer(peer, state).await; 166 153 } 167 154 168 - _ = interval.tick() => { 169 - continue; 155 + Some(data) = mrx.next() => { 156 + info!("Handling new packet from {}", data.0); 157 + self.handle_recv(data, &mut packet_handler).await; 170 158 } 159 + 160 + 161 + } 162 + }; 163 + 164 + self.push_incoming(Some(self.my_id), msg).await; 165 + 166 + self.msg_sender.close_channel(); 167 + drop(mrx); 168 + socket.try_update_peers().ok(); 169 + drop(socket); 170 + if should_await { 171 + if let Err(why) = loop_fut.await { 172 + error!("Failed to await after disconnect: {why:?}"); 171 173 } 172 174 } 175 + info!("Transport disconnected"); 173 176 } 174 177 175 - async fn handle_peers(&self, socket: &mut WebRtcSocket) { 176 - for (peer, state) in socket.update_peers() { 177 - let msg = match state { 178 - PeerState::Connected => TransportMessage::PeerConnect(peer.0), 179 - PeerState::Disconnected => TransportMessage::PeerDisconnect(peer.0), 180 - }; 181 - self.push_incoming(Some(peer.0), msg).await; 182 - } 178 + async fn handle_peer(&self, peer: PeerId, state: PeerState) { 179 + let mut all_peers = self.all_peers.lock().await; 180 + let msg = match state { 181 + PeerState::Connected => { 182 + all_peers.insert(peer.0); 183 + TransportMessage::PeerConnect(peer.0) 184 + } 185 + PeerState::Disconnected => { 186 + all_peers.remove(&peer.0); 187 + TransportMessage::PeerDisconnect(peer.0) 188 + } 189 + }; 190 + drop(all_peers); 191 + self.push_incoming(Some(peer.0), msg).await; 183 192 } 184 193 185 - async fn handle_send( 194 + async fn handle_recv( 186 195 &self, 187 - socket: &mut WebRtcSocket, 188 - all_peers: &[PeerId], 189 - messages: &mut Vec<MsgPair>, 196 + (PeerId(peer), packet): MatchboxMsgPair, 197 + handler: &mut PacketHandler, 190 198 ) { 191 - let encoded_messages = messages.drain(..).filter_map(|(id, msg)| { 192 - match PacketHandler::message_to_packets(&msg) { 193 - Ok(packets) => Some((id, packets)), 194 - Err(why) => { 195 - error!("Error encoding message to packets: {why:?}"); 196 - None 197 - } 199 + match handler.consume_packet(peer, packet.into_vec()) { 200 + Ok(Some(msg)) => { 201 + self.push_incoming(Some(peer), msg).await; 198 202 } 199 - }); 203 + Ok(None) => { 204 + // Non complete message 205 + } 206 + Err(why) => { 207 + error!("Error receiving message: {why}"); 208 + } 209 + } 210 + } 200 211 201 - let channel = socket.channel_mut(0); 212 + pub async fn send_transport_message(&self, peer: Option<Uuid>, msg: TransportMessage) { 213 + let mut tx = self.msg_sender.clone(); 202 214 203 - for (peer, packets) in encoded_messages { 204 - if let Some(peer) = peer { 205 - for packet in packets { 206 - channel.send(packet.into_boxed_slice(), PeerId(peer)); 207 - } 208 - } else { 209 - for packet in packets { 210 - let boxed = packet.into_boxed_slice(); 211 - for peer in all_peers { 212 - channel.send(boxed.clone(), *peer); 215 + match PacketHandler::message_to_packets(&msg) { 216 + Ok(packets) => { 217 + if let Some(peer) = peer { 218 + let mut stream = futures::stream::iter( 219 + packets 220 + .into_iter() 221 + .map(|p| Ok((PeerId(peer), p.into_boxed_slice()))), 222 + ); 223 + if let Err(why) = tx.send_all(&mut stream).await { 224 + error!("Error sending packet: {why}"); 225 + } 226 + } else { 227 + let all_peers = self.all_peers.lock().await; 228 + for peer in all_peers.iter().copied() { 229 + let packets = packets.clone(); 230 + let mut stream = futures::stream::iter( 231 + packets 232 + .into_iter() 233 + .map(|p| Ok((PeerId(peer), p.into_boxed_slice()))), 234 + ); 235 + if let Err(why) = tx.send_all(&mut stream).await { 236 + error!("Error sending packet: {why}"); 237 + } 213 238 } 214 239 } 215 240 } 241 + Err(why) => { 242 + error!("Error encoding message: {why}"); 243 + } 216 244 } 217 - } 218 - 219 - async fn handle_recv(&self, socket: &mut WebRtcSocket, handler: &mut PacketHandler) { 220 - let data = socket.channel_mut(0).receive(); 221 - let messages = data 222 - .into_iter() 223 - .filter_map( 224 - |(peer, bytes)| match handler.consume_packet(peer.0, bytes.into_vec()) { 225 - Ok(msg) => msg.map(|msg| (Some(peer.0), msg)), 226 - Err(why) => { 227 - error!("Error receiving message: {why}"); 228 - None 229 - } 230 - }, 231 - ) 232 - .collect(); 233 - self.push_many_incoming(messages).await; 234 - } 235 - 236 - pub async fn send_transport_message(&self, peer: Option<Uuid>, msg: TransportMessage) { 237 - self.outgoing 238 - .0 239 - .send((peer, msg)) 240 - .await 241 - .expect("Failed to add to outgoing queue"); 242 245 } 243 246 244 247 pub async fn recv_transport_messages(&self) -> Vec<MsgPair> {
+1 -3
manhunt-transport/src/packets.rs
··· 1 1 use std::collections::HashMap; 2 2 3 3 use anyhow::{anyhow, bail}; 4 - use manhunt_logic::{prelude::*, TransportMessage}; 4 + use manhunt_logic::{TransportMessage, prelude::*}; 5 5 use serde::{Deserialize, Serialize}; 6 6 use uuid::Uuid; 7 7 ··· 174 174 assert_eq!(data.len(), 1); 175 175 176 176 let data = data.into_iter().next().unwrap(); 177 - 178 - println!("dat: {data:?}"); 179 177 180 178 let decoded = handler 181 179 .consume_packet(Uuid::default(), data)