Live location tracking and playback for the game "manhunt"

Split up logic across multiple crates

bwc9876.dev dfd5c71a 2b2a6622

verified
+1515 -1123
+128 -98
Cargo.lock
··· 235 235 dependencies = [ 236 236 "proc-macro2", 237 237 "quote", 238 - "syn 2.0.103", 238 + "syn 2.0.104", 239 239 "synstructure", 240 240 ] 241 241 ··· 247 247 dependencies = [ 248 248 "proc-macro2", 249 249 "quote", 250 - "syn 2.0.103", 250 + "syn 2.0.104", 251 251 ] 252 252 253 253 [[package]] ··· 384 384 dependencies = [ 385 385 "proc-macro2", 386 386 "quote", 387 - "syn 2.0.103", 387 + "syn 2.0.104", 388 388 ] 389 389 390 390 [[package]] ··· 458 458 dependencies = [ 459 459 "proc-macro2", 460 460 "quote", 461 - "syn 2.0.103", 461 + "syn 2.0.104", 462 462 ] 463 463 464 464 [[package]] ··· 520 520 521 521 [[package]] 522 522 name = "autocfg" 523 - version = "1.4.0" 523 + version = "1.5.0" 524 524 source = "registry+https://github.com/rust-lang/crates.io-index" 525 - checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 525 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 526 526 527 527 [[package]] 528 528 name = "axum" ··· 725 725 "proc-macro-crate 3.3.0", 726 726 "proc-macro2", 727 727 "quote", 728 - "syn 2.0.103", 728 + "syn 2.0.104", 729 729 ] 730 730 731 731 [[package]] ··· 1189 1189 checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" 1190 1190 dependencies = [ 1191 1191 "quote", 1192 - "syn 2.0.103", 1192 + "syn 2.0.104", 1193 1193 ] 1194 1194 1195 1195 [[package]] ··· 1199 1199 checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" 1200 1200 dependencies = [ 1201 1201 "quote", 1202 - "syn 2.0.103", 1202 + "syn 2.0.104", 1203 1203 ] 1204 1204 1205 1205 [[package]] ··· 1234 1234 dependencies = [ 1235 1235 "proc-macro2", 1236 1236 "quote", 1237 - "syn 2.0.103", 1237 + "syn 2.0.104", 1238 1238 ] 1239 1239 1240 1240 [[package]] ··· 1258 1258 "proc-macro2", 1259 1259 "quote", 1260 1260 "strsim", 1261 - "syn 2.0.103", 1261 + "syn 2.0.104", 1262 1262 ] 1263 1263 1264 1264 [[package]] ··· 1269 1269 dependencies = [ 1270 1270 "darling_core", 1271 1271 "quote", 1272 - "syn 2.0.103", 1272 + "syn 2.0.104", 1273 1273 ] 1274 1274 1275 1275 [[package]] ··· 1323 1323 "proc-macro2", 1324 1324 "quote", 1325 1325 "rustc_version", 1326 - "syn 2.0.103", 1326 + "syn 2.0.104", 1327 1327 ] 1328 1328 1329 1329 [[package]] ··· 1343 1343 dependencies = [ 1344 1344 "proc-macro2", 1345 1345 "quote", 1346 - "syn 2.0.103", 1346 + "syn 2.0.104", 1347 1347 "unicode-xid", 1348 1348 ] 1349 1349 ··· 1416 1416 dependencies = [ 1417 1417 "proc-macro2", 1418 1418 "quote", 1419 - "syn 2.0.103", 1419 + "syn 2.0.104", 1420 1420 ] 1421 1421 1422 1422 [[package]] ··· 1439 1439 dependencies = [ 1440 1440 "proc-macro2", 1441 1441 "quote", 1442 - "syn 2.0.103", 1442 + "syn 2.0.104", 1443 1443 ] 1444 1444 1445 1445 [[package]] ··· 1515 1515 1516 1516 [[package]] 1517 1517 name = "embed-resource" 1518 - version = "3.0.3" 1518 + version = "3.0.4" 1519 1519 source = "registry+https://github.com/rust-lang/crates.io-index" 1520 - checksum = "e8fe7d068ca6b3a5782ca5ec9afc244acd99dd441e4686a83b1c3973aba1d489" 1520 + checksum = "0963f530273dc3022ab2bdc3fcd6d488e850256f2284a82b7413cb9481ee85dd" 1521 1521 dependencies = [ 1522 1522 "cc", 1523 1523 "memchr", ··· 1566 1566 dependencies = [ 1567 1567 "proc-macro2", 1568 1568 "quote", 1569 - "syn 2.0.103", 1569 + "syn 2.0.104", 1570 1570 ] 1571 1571 1572 1572 [[package]] ··· 1610 1610 1611 1611 [[package]] 1612 1612 name = "errno" 1613 - version = "0.3.12" 1613 + version = "0.3.13" 1614 1614 source = "registry+https://github.com/rust-lang/crates.io-index" 1615 - checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 1615 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 1616 1616 dependencies = [ 1617 1617 "libc", 1618 - "windows-sys 0.59.0", 1618 + "windows-sys 0.60.2", 1619 1619 ] 1620 1620 1621 1621 [[package]] ··· 1729 1729 dependencies = [ 1730 1730 "proc-macro2", 1731 1731 "quote", 1732 - "syn 2.0.103", 1732 + "syn 2.0.104", 1733 1733 ] 1734 1734 1735 1735 [[package]] ··· 1832 1832 dependencies = [ 1833 1833 "proc-macro2", 1834 1834 "quote", 1835 - "syn 2.0.103", 1835 + "syn 2.0.104", 1836 1836 ] 1837 1837 1838 1838 [[package]] ··· 2114 2114 "proc-macro-error", 2115 2115 "proc-macro2", 2116 2116 "quote", 2117 - "syn 2.0.103", 2117 + "syn 2.0.104", 2118 2118 ] 2119 2119 2120 2120 [[package]] ··· 2228 2228 "proc-macro-error", 2229 2229 "proc-macro2", 2230 2230 "quote", 2231 - "syn 2.0.103", 2231 + "syn 2.0.104", 2232 2232 ] 2233 2233 2234 2234 [[package]] ··· 2402 2402 "tokio", 2403 2403 "tokio-rustls", 2404 2404 "tower-service", 2405 - "webpki-roots 1.0.0", 2405 + "webpki-roots 1.0.1", 2406 2406 ] 2407 2407 2408 2408 [[package]] ··· 2736 2736 dependencies = [ 2737 2737 "proc-macro2", 2738 2738 "quote", 2739 - "syn 2.0.103", 2739 + "syn 2.0.104", 2740 2740 ] 2741 2741 2742 2742 [[package]] ··· 2858 2858 2859 2859 [[package]] 2860 2860 name = "libc" 2861 - version = "0.2.173" 2861 + version = "0.2.174" 2862 2862 source = "registry+https://github.com/rust-lang/crates.io-index" 2863 - checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" 2863 + checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 2864 2864 2865 2865 [[package]] 2866 2866 name = "libloading" ··· 2943 2943 dependencies = [ 2944 2944 "anyhow", 2945 2945 "chrono", 2946 - "const-str", 2947 - "futures", 2948 2946 "log", 2949 - "matchbox_socket", 2950 - "rand 0.9.1", 2951 - "rand_chacha 0.9.0", 2952 - "reqwest", 2953 - "rmp-serde", 2947 + "manhunt-logic", 2948 + "manhunt-transport", 2954 2949 "serde", 2955 2950 "serde_json", 2956 2951 "specta", ··· 2965 2960 "tauri-plugin-store", 2966 2961 "tauri-specta", 2967 2962 "tokio", 2968 - "tokio-util", 2963 + "uuid", 2964 + ] 2965 + 2966 + [[package]] 2967 + name = "manhunt-logic" 2968 + version = "0.1.0" 2969 + dependencies = [ 2970 + "anyhow", 2971 + "chrono", 2972 + "rand 0.9.1", 2973 + "rand_chacha 0.9.0", 2974 + "serde", 2975 + "specta", 2976 + "tokio", 2969 2977 "uuid", 2970 2978 ] 2971 2979 ··· 2982 2990 "matchbox_protocol", 2983 2991 "matchbox_signaling", 2984 2992 "tokio", 2993 + ] 2994 + 2995 + [[package]] 2996 + name = "manhunt-transport" 2997 + version = "0.1.0" 2998 + dependencies = [ 2999 + "anyhow", 3000 + "const-str", 3001 + "futures", 3002 + "log", 3003 + "manhunt-logic", 3004 + "matchbox_protocol", 3005 + "matchbox_socket", 3006 + "rand 0.9.1", 3007 + "reqwest", 3008 + "rmp-serde", 3009 + "serde", 3010 + "tokio", 3011 + "tokio-util", 3012 + "uuid", 2985 3013 ] 2986 3014 2987 3015 [[package]] ··· 3292 3320 3293 3321 [[package]] 3294 3322 name = "num_enum" 3295 - version = "0.7.3" 3323 + version = "0.7.4" 3296 3324 source = "registry+https://github.com/rust-lang/crates.io-index" 3297 - checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" 3325 + checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" 3298 3326 dependencies = [ 3299 3327 "num_enum_derive", 3328 + "rustversion", 3300 3329 ] 3301 3330 3302 3331 [[package]] 3303 3332 name = "num_enum_derive" 3304 - version = "0.7.3" 3333 + version = "0.7.4" 3305 3334 source = "registry+https://github.com/rust-lang/crates.io-index" 3306 - checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" 3335 + checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" 3307 3336 dependencies = [ 3308 3337 "proc-macro-crate 3.3.0", 3309 3338 "proc-macro2", 3310 3339 "quote", 3311 - "syn 2.0.103", 3340 + "syn 2.0.104", 3312 3341 ] 3313 3342 3314 3343 [[package]] ··· 3827 3856 "phf_shared 0.11.3", 3828 3857 "proc-macro2", 3829 3858 "quote", 3830 - "syn 2.0.103", 3859 + "syn 2.0.104", 3831 3860 ] 3832 3861 3833 3862 [[package]] ··· 4142 4171 4143 4172 [[package]] 4144 4173 name = "quinn-udp" 4145 - version = "0.5.12" 4174 + version = "0.5.13" 4146 4175 source = "registry+https://github.com/rust-lang/crates.io-index" 4147 - checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" 4176 + checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" 4148 4177 dependencies = [ 4149 4178 "cfg_aliases", 4150 4179 "libc", ··· 4165 4194 4166 4195 [[package]] 4167 4196 name = "r-efi" 4168 - version = "5.2.0" 4197 + version = "5.3.0" 4169 4198 source = "registry+https://github.com/rust-lang/crates.io-index" 4170 - checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 4199 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 4171 4200 4172 4201 [[package]] 4173 4202 name = "radium" ··· 4342 4371 dependencies = [ 4343 4372 "proc-macro2", 4344 4373 "quote", 4345 - "syn 2.0.103", 4374 + "syn 2.0.104", 4346 4375 ] 4347 4376 4348 4377 [[package]] ··· 4424 4453 "wasm-bindgen-futures", 4425 4454 "wasm-streams", 4426 4455 "web-sys", 4427 - "webpki-roots 1.0.0", 4456 + "webpki-roots 1.0.1", 4428 4457 ] 4429 4458 4430 4459 [[package]] ··· 4735 4764 "proc-macro2", 4736 4765 "quote", 4737 4766 "serde_derive_internals", 4738 - "syn 2.0.103", 4767 + "syn 2.0.104", 4739 4768 ] 4740 4769 4741 4770 [[package]] ··· 4866 4895 dependencies = [ 4867 4896 "proc-macro2", 4868 4897 "quote", 4869 - "syn 2.0.103", 4898 + "syn 2.0.104", 4870 4899 ] 4871 4900 4872 4901 [[package]] ··· 4877 4906 dependencies = [ 4878 4907 "proc-macro2", 4879 4908 "quote", 4880 - "syn 2.0.103", 4909 + "syn 2.0.104", 4881 4910 ] 4882 4911 4883 4912 [[package]] ··· 4910 4939 dependencies = [ 4911 4940 "proc-macro2", 4912 4941 "quote", 4913 - "syn 2.0.103", 4942 + "syn 2.0.104", 4914 4943 ] 4915 4944 4916 4945 [[package]] ··· 4962 4991 "darling", 4963 4992 "proc-macro2", 4964 4993 "quote", 4965 - "syn 2.0.103", 4994 + "syn 2.0.104", 4966 4995 ] 4967 4996 4968 4997 [[package]] ··· 5154 5183 checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" 5155 5184 dependencies = [ 5156 5185 "chrono", 5186 + "ctor", 5157 5187 "paste", 5158 5188 "specta-macros", 5159 5189 "thiserror 1.0.69", ··· 5169 5199 "Inflector", 5170 5200 "proc-macro2", 5171 5201 "quote", 5172 - "syn 2.0.103", 5202 + "syn 2.0.104", 5173 5203 ] 5174 5204 5175 5205 [[package]] ··· 5304 5334 5305 5335 [[package]] 5306 5336 name = "syn" 5307 - version = "2.0.103" 5337 + version = "2.0.104" 5308 5338 source = "registry+https://github.com/rust-lang/crates.io-index" 5309 - checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" 5339 + checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 5310 5340 dependencies = [ 5311 5341 "proc-macro2", 5312 5342 "quote", ··· 5330 5360 dependencies = [ 5331 5361 "proc-macro2", 5332 5362 "quote", 5333 - "syn 2.0.103", 5363 + "syn 2.0.104", 5334 5364 ] 5335 5365 5336 5366 [[package]] ··· 5414 5444 dependencies = [ 5415 5445 "proc-macro2", 5416 5446 "quote", 5417 - "syn 2.0.103", 5447 + "syn 2.0.104", 5418 5448 ] 5419 5449 5420 5450 [[package]] ··· 5521 5551 "serde", 5522 5552 "serde_json", 5523 5553 "sha2", 5524 - "syn 2.0.103", 5554 + "syn 2.0.104", 5525 5555 "tauri-utils", 5526 5556 "thiserror 2.0.12", 5527 5557 "time", ··· 5539 5569 "heck 0.5.0", 5540 5570 "proc-macro2", 5541 5571 "quote", 5542 - "syn 2.0.103", 5572 + "syn 2.0.104", 5543 5573 "tauri-codegen", 5544 5574 "tauri-utils", 5545 5575 ] ··· 5603 5633 5604 5634 [[package]] 5605 5635 name = "tauri-plugin-geolocation" 5606 - version = "2.2.4" 5636 + version = "2.2.5" 5607 5637 source = "registry+https://github.com/rust-lang/crates.io-index" 5608 - checksum = "f70978d3dbca4d900f8708dad1e0cff32c52de578101998d0f30dc7e12104e85" 5638 + checksum = "3dc06e23cb12d17533aca6001252f7f0fdbd295868194748821d0f0e6fbfbb59" 5609 5639 dependencies = [ 5610 5640 "log", 5611 5641 "serde", ··· 5617 5647 5618 5648 [[package]] 5619 5649 name = "tauri-plugin-log" 5620 - version = "2.4.0" 5650 + version = "2.5.0" 5621 5651 source = "registry+https://github.com/rust-lang/crates.io-index" 5622 - checksum = "8d2b582d860eb214f28323f4ce4f2797ae3b78f197e27b11677f976f9f52aedb" 5652 + checksum = "1063890b541877c4367d0ee9b1758360d86398862fbaafdd99aac02a55e28bf8" 5623 5653 dependencies = [ 5624 5654 "android_logger", 5625 5655 "byte-unit", ··· 5639 5669 5640 5670 [[package]] 5641 5671 name = "tauri-plugin-notification" 5642 - version = "2.2.2" 5672 + version = "2.2.3" 5643 5673 source = "registry+https://github.com/rust-lang/crates.io-index" 5644 - checksum = "c474c7cc524385e682ccc1e149e13913a66fd8586ac4c2319cf01b78f070d309" 5674 + checksum = "d1c87f171cdb35c3aa8f17e8dfd84c1b9f68eb4086ec16a5a1b9f13b5541c574" 5645 5675 dependencies = [ 5646 5676 "log", 5647 5677 "notify-rust", ··· 5658 5688 5659 5689 [[package]] 5660 5690 name = "tauri-plugin-opener" 5661 - version = "2.2.7" 5691 + version = "2.3.0" 5662 5692 source = "registry+https://github.com/rust-lang/crates.io-index" 5663 - checksum = "66644b71a31ec1a8a52c4a16575edd28cf763c87cf4a7da24c884122b5c77097" 5693 + checksum = "2c8983f50326d34437142a6d560b5c3426e91324297519b6eeb32ed0a1d1e0f2" 5664 5694 dependencies = [ 5665 5695 "dunce", 5666 5696 "glob", ··· 5680 5710 5681 5711 [[package]] 5682 5712 name = "tauri-plugin-store" 5683 - version = "2.2.0" 5713 + version = "2.2.1" 5684 5714 source = "registry+https://github.com/rust-lang/crates.io-index" 5685 - checksum = "1c0c08fae6995909f5e9a0da6038273b750221319f2c0f3b526d6de1cde21505" 5715 + checksum = "ada7e7aeea472dec9b8d09d25301e59fe3e8330dc11dbcf903d6388126cb3722" 5686 5716 dependencies = [ 5687 5717 "dunce", 5688 5718 "serde", ··· 5768 5798 "heck 0.5.0", 5769 5799 "proc-macro2", 5770 5800 "quote", 5771 - "syn 2.0.103", 5801 + "syn 2.0.104", 5772 5802 ] 5773 5803 5774 5804 [[package]] ··· 5888 5918 dependencies = [ 5889 5919 "proc-macro2", 5890 5920 "quote", 5891 - "syn 2.0.103", 5921 + "syn 2.0.104", 5892 5922 ] 5893 5923 5894 5924 [[package]] ··· 5899 5929 dependencies = [ 5900 5930 "proc-macro2", 5901 5931 "quote", 5902 - "syn 2.0.103", 5932 + "syn 2.0.104", 5903 5933 ] 5904 5934 5905 5935 [[package]] ··· 5987 6017 dependencies = [ 5988 6018 "proc-macro2", 5989 6019 "quote", 5990 - "syn 2.0.103", 6020 + "syn 2.0.104", 5991 6021 ] 5992 6022 5993 6023 [[package]] ··· 6161 6191 6162 6192 [[package]] 6163 6193 name = "tracing-attributes" 6164 - version = "0.1.29" 6194 + version = "0.1.30" 6165 6195 source = "registry+https://github.com/rust-lang/crates.io-index" 6166 - checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" 6196 + checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 6167 6197 dependencies = [ 6168 6198 "proc-macro2", 6169 6199 "quote", 6170 - "syn 2.0.103", 6200 + "syn 2.0.104", 6171 6201 ] 6172 6202 6173 6203 [[package]] ··· 6512 6542 "log", 6513 6543 "proc-macro2", 6514 6544 "quote", 6515 - "syn 2.0.103", 6545 + "syn 2.0.104", 6516 6546 "wasm-bindgen-shared", 6517 6547 ] 6518 6548 ··· 6547 6577 dependencies = [ 6548 6578 "proc-macro2", 6549 6579 "quote", 6550 - "syn 2.0.103", 6580 + "syn 2.0.104", 6551 6581 "wasm-bindgen-backend", 6552 6582 "wasm-bindgen-shared", 6553 6583 ] ··· 6659 6689 6660 6690 [[package]] 6661 6691 name = "webpki-roots" 6662 - version = "1.0.0" 6692 + version = "1.0.1" 6663 6693 source = "registry+https://github.com/rust-lang/crates.io-index" 6664 - checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 6694 + checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" 6665 6695 dependencies = [ 6666 6696 "rustls-pki-types", 6667 6697 ] ··· 6897 6927 dependencies = [ 6898 6928 "proc-macro2", 6899 6929 "quote", 6900 - "syn 2.0.103", 6930 + "syn 2.0.104", 6901 6931 ] 6902 6932 6903 6933 [[package]] ··· 7011 7041 dependencies = [ 7012 7042 "proc-macro2", 7013 7043 "quote", 7014 - "syn 2.0.103", 7044 + "syn 2.0.104", 7015 7045 ] 7016 7046 7017 7047 [[package]] ··· 7022 7052 dependencies = [ 7023 7053 "proc-macro2", 7024 7054 "quote", 7025 - "syn 2.0.103", 7055 + "syn 2.0.104", 7026 7056 ] 7027 7057 7028 7058 [[package]] ··· 7504 7534 dependencies = [ 7505 7535 "proc-macro2", 7506 7536 "quote", 7507 - "syn 2.0.103", 7537 + "syn 2.0.104", 7508 7538 "synstructure", 7509 7539 ] 7510 7540 ··· 7551 7581 "proc-macro-crate 3.3.0", 7552 7582 "proc-macro2", 7553 7583 "quote", 7554 - "syn 2.0.103", 7584 + "syn 2.0.104", 7555 7585 "zbus_names", 7556 7586 "zvariant", 7557 7587 "zvariant_utils", ··· 7571 7601 7572 7602 [[package]] 7573 7603 name = "zerocopy" 7574 - version = "0.8.25" 7604 + version = "0.8.26" 7575 7605 source = "registry+https://github.com/rust-lang/crates.io-index" 7576 - checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 7606 + checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 7577 7607 dependencies = [ 7578 7608 "zerocopy-derive", 7579 7609 ] 7580 7610 7581 7611 [[package]] 7582 7612 name = "zerocopy-derive" 7583 - version = "0.8.25" 7613 + version = "0.8.26" 7584 7614 source = "registry+https://github.com/rust-lang/crates.io-index" 7585 - checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 7615 + checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 7586 7616 dependencies = [ 7587 7617 "proc-macro2", 7588 7618 "quote", 7589 - "syn 2.0.103", 7619 + "syn 2.0.104", 7590 7620 ] 7591 7621 7592 7622 [[package]] ··· 7606 7636 dependencies = [ 7607 7637 "proc-macro2", 7608 7638 "quote", 7609 - "syn 2.0.103", 7639 + "syn 2.0.104", 7610 7640 "synstructure", 7611 7641 ] 7612 7642 ··· 7627 7657 dependencies = [ 7628 7658 "proc-macro2", 7629 7659 "quote", 7630 - "syn 2.0.103", 7660 + "syn 2.0.104", 7631 7661 ] 7632 7662 7633 7663 [[package]] ··· 7660 7690 dependencies = [ 7661 7691 "proc-macro2", 7662 7692 "quote", 7663 - "syn 2.0.103", 7693 + "syn 2.0.104", 7664 7694 ] 7665 7695 7666 7696 [[package]] ··· 7687 7717 "proc-macro-crate 3.3.0", 7688 7718 "proc-macro2", 7689 7719 "quote", 7690 - "syn 2.0.103", 7720 + "syn 2.0.104", 7691 7721 "zvariant_utils", 7692 7722 ] 7693 7723 ··· 7701 7731 "quote", 7702 7732 "serde", 7703 7733 "static_assertions", 7704 - "syn 2.0.103", 7734 + "syn 2.0.104", 7705 7735 "winnow 0.7.11", 7706 7736 ]
+1 -1
Cargo.toml
··· 1 1 [workspace] 2 - members = ["backend", "manhunt-signaling"] 2 + members = ["backend", "manhunt-logic", "manhunt-signaling", "manhunt-transport"] 3 3 resolver = "3" 4 4 5 5 [profile.release]
+1 -1
TODO.md
··· 19 19 - [x] Meta : Recipes for type binding generation 20 20 - [x] Signaling: All of it 21 21 - [x] Backend : Better transport error handling 22 - - [ ] Backend : Abstract lobby? Separate crate? 22 + - [x] Backend : Abstract lobby? Separate crate? 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
+5 -11
backend/Cargo.toml
··· 22 22 tauri-plugin-opener = "2" 23 23 serde = { version = "1", features = ["derive"] } 24 24 serde_json = "1" 25 - chrono = { version = "0.4", features = ["serde", "now"] } 26 25 tokio = { version = "1.45", features = ["sync", "macros", "time", "fs"] } 27 - rand = { version = "0.9", features = ["thread_rng"] } 28 26 tauri-plugin-geolocation = "2" 29 - rand_chacha = "0.9.0" 30 - futures = "0.3.31" 31 - matchbox_socket = "0.12.0" 32 - uuid = { version = "1.17.0", features = ["serde", "v4"] } 33 - rmp-serde = "1.3.0" 34 27 tauri-plugin-store = "2.2.0" 35 - specta = { version = "=2.0.0-rc.22", features = ["chrono", "uuid"] } 28 + specta = { version = "=2.0.0-rc.22", features = ["chrono", "uuid", "export"] } 36 29 tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } 37 30 specta-typescript = "0.0.9" 38 31 tauri-plugin-log = "2" 39 32 tauri-plugin-notification = "2" 40 33 log = "0.4.27" 41 - tokio-util = "0.7.15" 42 34 anyhow = "1.0.98" 43 - reqwest = { version = "0.12.20", default-features = false, features = ["charset", "http2", "rustls-tls", "system-proxy"] } 44 - const-str = "0.6.2" 45 35 tauri-plugin-dialog = "2" 36 + manhunt-logic = { version = "0.1.0", path = "../manhunt-logic" } 37 + manhunt-transport = { version = "0.1.0", path = "../manhunt-transport" } 38 + uuid = { version = "1.17.0", features = ["serde"] } 39 + chrono = { version = "0.4.41", features = ["serde"] }
+5 -8
backend/src/game/events.rs manhunt-logic/src/game_events.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 - use super::{location::Location, state::PlayerPing, Id, UtcDT}; 3 + use crate::{ 4 + game::{Id, UtcDT}, 5 + game_state::PlayerPing, 6 + location::Location, 7 + }; 4 8 5 9 /// An event used between players to update state 6 10 #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] ··· 17 21 /// Contains location history of the given player, used after the game to sync location 18 22 /// histories 19 23 PostGameSync(Id, Vec<(UtcDT, Location)>), 20 - /// A player has been disconnected and removed from the game (because of error or otherwise). 21 - /// The player should be removed from all state 22 - DroppedPlayer(Id), 23 - /// The underlying transport has disconnected 24 - TransportDisconnect, 25 - /// The underlying transport encountered an error 26 - TransportError(String), 27 24 }
backend/src/game/location.rs manhunt-logic/src/location.rs
+114 -121
backend/src/game/mod.rs manhunt-logic/src/game.rs
··· 1 + use anyhow::bail; 1 2 use chrono::{DateTime, Utc}; 2 - pub use events::GameEvent; 3 - use powerups::PowerUpType; 4 - pub use settings::GameSettings; 5 - use std::{collections::HashMap, sync::Arc, time::Duration}; 3 + use std::{sync::Arc, time::Duration}; 6 4 use uuid::Uuid; 7 5 8 6 use tokio::{sync::RwLock, time::MissedTickBehavior}; 9 7 10 - mod events; 11 - mod location; 12 - mod powerups; 13 - mod settings; 14 - mod state; 15 - mod transport; 16 - 17 - use crate::prelude::*; 8 + use crate::StartGameInfo; 9 + use crate::{prelude::*, transport::TransportMessage}; 18 10 19 - pub use location::{Location, LocationService}; 20 - pub use state::{GameHistory, GameState, GameUiState}; 21 - pub use transport::Transport; 11 + use crate::{ 12 + game_events::GameEvent, 13 + game_state::{GameHistory, GameState, GameUiState}, 14 + location::LocationService, 15 + powerups::PowerUpType, 16 + settings::GameSettings, 17 + transport::Transport, 18 + }; 22 19 23 20 pub type Id = Uuid; 24 21 25 - /// Convenence alias for UTC DT 22 + /// Convenience alias for UTC DT 26 23 pub type UtcDT = DateTime<Utc>; 27 24 28 25 pub trait StateUpdateSender { ··· 42 39 43 40 impl<L: LocationService, T: Transport, S: StateUpdateSender> Game<L, T, S> { 44 41 pub fn new( 45 - my_id: Id, 46 42 interval: Duration, 47 - initial_caught_state: HashMap<Id, bool>, 48 - settings: GameSettings, 43 + start_info: StartGameInfo, 49 44 transport: Arc<T>, 50 45 location: L, 51 46 state_update_sender: S, 52 47 ) -> Self { 53 - let state = GameState::new(settings, my_id, initial_caught_state); 48 + let state = GameState::new( 49 + start_info.settings, 50 + transport.self_id(), 51 + start_info.initial_caught_state, 52 + ); 54 53 55 54 Self { 56 55 transport, ··· 61 60 } 62 61 } 63 62 63 + async fn send_event(&self, event: GameEvent) { 64 + self.transport.send_message(event.into()).await; 65 + } 66 + 64 67 pub async fn mark_caught(&self) { 65 68 let mut state = self.state.write().await; 66 69 let id = state.id; ··· 69 72 // TODO: Maybe reroll for new powerups (specifically seeker ones) instead of just erasing it 70 73 state.use_powerup(); 71 74 72 - self.transport 73 - .send_message(GameEvent::PlayerCaught(state.id)) 74 - .await; 75 + self.send_event(GameEvent::PlayerCaught(state.id)).await; 75 76 } 76 77 77 78 pub async fn clone_settings(&self) -> GameSettings { ··· 85 86 pub async fn get_powerup(&self) { 86 87 let mut state = self.state.write().await; 87 88 state.get_powerup(); 88 - self.transport 89 - .send_message(GameEvent::PowerupDespawn(state.id)) 90 - .await; 89 + self.send_event(GameEvent::PowerupDespawn(state.id)).await; 91 90 } 92 91 93 92 pub async fn use_powerup(&self) { ··· 98 97 PowerUpType::PingSeeker => {} 99 98 PowerUpType::PingAllSeekers => { 100 99 for seeker in state.iter_seekers() { 101 - self.transport 102 - .send_message(GameEvent::ForcePing(seeker, None)) 103 - .await; 100 + self.send_event(GameEvent::ForcePing(seeker, None)).await; 104 101 } 105 102 } 106 103 PowerUpType::ForcePingOther => { ··· 108 105 let target = state.random_other_hider().or_else(|| state.random_seeker()); 109 106 110 107 if let Some(target) = target { 111 - self.transport 112 - .send_message(GameEvent::ForcePing(target, None)) 113 - .await; 108 + self.send_event(GameEvent::ForcePing(target, None)).await; 114 109 } 115 110 } 116 111 } 117 112 } 118 113 } 119 114 120 - async fn consume_event(&self, state: &mut GameState, event: GameEvent) -> Result<bool> { 115 + async fn consume_event(&self, state: &mut GameState, event: GameEvent) { 121 116 if !state.game_ended() { 122 117 state.event_history.push((Utc::now(), event.clone())); 123 118 } ··· 126 121 GameEvent::Ping(player_ping) => state.add_ping(player_ping), 127 122 GameEvent::ForcePing(target, display) => { 128 123 if target != state.id { 129 - return Ok(false); 124 + return; 130 125 } 131 126 132 127 let ping = if let Some(display) = display { ··· 137 132 138 133 if let Some(ping) = ping { 139 134 state.add_ping(ping.clone()); 140 - self.transport.send_message(GameEvent::Ping(ping)).await; 135 + self.send_event(GameEvent::Ping(ping)).await; 141 136 } 142 137 } 143 138 GameEvent::PowerupDespawn(_) => state.despawn_powerup(), 144 139 GameEvent::PlayerCaught(player) => { 145 140 state.mark_caught(player); 146 141 state.remove_ping(player); 147 - } 148 - GameEvent::DroppedPlayer(id) => { 149 - state.remove_player(id); 150 - } 151 - GameEvent::TransportDisconnect => { 152 - return Ok(true); 153 - } 154 - GameEvent::TransportError(err) => { 155 - bail!("Transport error: {err}"); 156 142 } 157 143 GameEvent::PostGameSync(id, history) => { 158 144 state.insert_player_location_history(id, history); ··· 160 146 } 161 147 162 148 self.state_update_sender.send_update(); 149 + } 163 150 164 - Ok(false) 151 + async fn consume_message( 152 + &self, 153 + state: &mut GameState, 154 + _id: Option<Uuid>, 155 + msg: TransportMessage, 156 + ) -> Result<bool> { 157 + match msg { 158 + TransportMessage::Game(event) => { 159 + self.consume_event(state, *event).await; 160 + Ok(false) 161 + } 162 + TransportMessage::PeerDisconnect(id) => { 163 + state.remove_player(id); 164 + Ok(false) 165 + } 166 + TransportMessage::Disconnected => { 167 + // Expected disconnect, exit 168 + Ok(true) 169 + } 170 + TransportMessage::Error(err) => bail!("Transport error: {err}"), 171 + _ => Ok(false), 172 + } 165 173 } 166 174 167 175 /// Perform a tick for a specific moment in time ··· 172 180 if state.check_end_game() { 173 181 // If we're at the point where the game is over, send out our location history 174 182 let msg = GameEvent::PostGameSync(state.id, state.location_history.clone()); 175 - self.transport.send_message(msg).await; 183 + self.send_event(msg).await; 176 184 send_update = true; 177 185 } 178 186 ··· 208 216 // We have a powerup that lets us ping a seeker as us, use it. 209 217 if let Some(seeker) = state.random_seeker() { 210 218 state.use_powerup(); 211 - self.transport 212 - .send_message(GameEvent::ForcePing(seeker, Some(state.id))) 219 + self.send_event(GameEvent::ForcePing(seeker, Some(state.id))) 213 220 .await; 214 221 state.start_pings(now); 215 222 } 216 223 } else { 217 224 // No powerup, normal ping 218 225 if let Some(ping) = state.create_self_ping() { 219 - self.transport.send_message(GameEvent::Ping(ping)).await; 226 + self.send_event(GameEvent::Ping(ping.clone())).await; 227 + state.add_ping(ping); 220 228 state.start_pings(now); 221 229 } 222 230 } ··· 248 256 self.tick(&mut state, now).await; 249 257 } 250 258 251 - pub fn quit_game(&self) { 252 - self.transport.disconnect(); 259 + pub async fn quit_game(&self) { 260 + self.transport.disconnect().await; 253 261 } 254 262 255 263 /// Main loop of the game, handles ticking and receiving messages from [Transport]. ··· 262 270 tokio::select! { 263 271 biased; 264 272 265 - events = self.transport.receive_messages() => { 273 + messages = self.transport.receive_messages() => { 266 274 let mut state = self.state.write().await; 267 - for event in events { 268 - match self.consume_event(&mut state, event).await { 275 + for (id, msg) in messages { 276 + match self.consume_message(&mut state, id, msg).await { 269 277 Ok(should_break) => { 270 278 if should_break { 271 - break 'game Ok(None); 279 + break 'game Ok(None); 272 280 } 273 281 } 274 282 Err(why) => { break 'game Err(why); } ··· 288 296 } 289 297 }; 290 298 291 - self.transport.disconnect(); 299 + self.transport.disconnect().await; 292 300 293 301 res 294 302 } ··· 296 304 297 305 #[cfg(test)] 298 306 mod tests { 299 - use std::sync::Arc; 307 + use std::{collections::HashMap, sync::Arc}; 300 308 301 - use crate::game::{location::Location, settings::PingStartCondition}; 309 + use crate::{ 310 + location::Location, 311 + settings::PingStartCondition, 312 + tests::{DummySender, MockLocation, MockTransport}, 313 + }; 302 314 303 315 use super::*; 304 - use tokio::{sync::Mutex, task::yield_now, test}; 305 - 306 - type GameEventRx = tokio::sync::mpsc::Receiver<GameEvent>; 307 - type GameEventTx = tokio::sync::mpsc::Sender<GameEvent>; 308 - 309 - struct MockTransport { 310 - rx: Mutex<GameEventRx>, 311 - txs: Vec<GameEventTx>, 312 - } 313 - 314 - impl Transport for MockTransport { 315 - async fn receive_messages(&self) -> impl Iterator<Item = GameEvent> { 316 - let mut rx = self.rx.lock().await; 317 - let mut buf = Vec::with_capacity(20); 318 - rx.recv_many(&mut buf, 20).await; 319 - buf.into_iter() 320 - } 321 - 322 - async fn send_message(&self, msg: GameEvent) { 323 - for tx in self.txs.iter() { 324 - tx.send(msg.clone()).await.expect("Failed to send msg"); 325 - } 326 - } 327 - } 328 - 329 - struct MockLocation; 330 - 331 - impl LocationService for MockLocation { 332 - fn get_loc(&self) -> Option<Location> { 333 - Some(location::Location { 334 - lat: 0.0, 335 - long: 0.0, 336 - heading: None, 337 - }) 338 - } 339 - } 340 - 341 - struct DummySender; 342 - 343 - impl StateUpdateSender for DummySender { 344 - fn send_update(&self) {} 345 - } 316 + use tokio::{task::yield_now, test}; 346 317 347 318 type TestGame = Game<MockLocation, MockTransport, DummySender>; 348 319 ··· 357 328 358 329 impl MockMatch { 359 330 pub fn new(settings: GameSettings, players: u32, seekers: u32) -> Self { 360 - let uuids = (0..players) 361 - .map(|_| uuid::Uuid::new_v4()) 362 - .collect::<Vec<_>>(); 363 - 364 - let channels = (0..players) 365 - .map(|_| tokio::sync::mpsc::channel(10)) 366 - .collect::<Vec<_>>(); 331 + let (uuids, transports) = MockTransport::create_mesh(players); 367 332 368 333 let initial_caught_state = (0..players) 369 334 .map(|id| (uuids[id as usize], id < seekers)) 370 335 .collect::<HashMap<_, _>>(); 371 - let txs = channels 372 - .iter() 373 - .map(|(tx, _)| tx.clone()) 374 - .collect::<Vec<_>>(); 375 336 376 - let games = channels 337 + let games = transports 377 338 .into_iter() 378 339 .enumerate() 379 - .map(|(id, (_, rx))| { 380 - let transport = MockTransport { 381 - rx: Mutex::new(rx), 382 - txs: txs.clone(), 340 + .map(|(id, transport)| { 341 + let location = MockLocation; 342 + let start_info = StartGameInfo { 343 + initial_caught_state: initial_caught_state.clone(), 344 + settings: settings.clone(), 383 345 }; 384 - let location = MockLocation; 385 346 let game = TestGame::new( 386 - uuids[id], 387 347 INTERVAL, 388 - initial_caught_state.clone(), 389 - settings.clone(), 348 + start_info, 390 349 Arc::new(transport), 391 350 location, 392 351 DummySender, ··· 495 454 }) 496 455 .await; 497 456 498 - // Game over, See TODO in main_loop for more assertions 457 + // Extra tick for post-game syncing 458 + mat.tick().await; 459 + 460 + mat.assert_all_states(|s| assert!(s.game_ended(), "Game {} has not ended", s.id)) 461 + .await; 499 462 } 500 463 501 464 #[test] ··· 675 638 s.get_caught(mat.uuids[id]).is_some(), 676 639 "Player {} should be pinged due to the powerup (in {})", 677 640 id, 641 + s.id 642 + ); 643 + } 644 + }) 645 + .await; 646 + } 647 + 648 + #[test] 649 + async fn test_player_dropped() { 650 + let settings = mk_settings(); 651 + let mat = MockMatch::new(settings, 4, 1); 652 + 653 + mat.start().await; 654 + 655 + let game = mat.game(2); 656 + game.quit_game().await; 657 + let id = game.state.read().await.id; 658 + 659 + mat.tick().await; 660 + 661 + mat.assert_all_states(|s| { 662 + if s.id != id { 663 + assert!( 664 + s.get_ping(id).is_none(), 665 + "Game {} has not removed 2 from pings", 666 + s.id 667 + ); 668 + assert!( 669 + s.get_caught(id).is_none(), 670 + "Game {} has not removed 2 from caught state", 678 671 s.id 679 672 ); 680 673 }
backend/src/game/powerups.rs manhunt-logic/src/powerups.rs
backend/src/game/settings.rs manhunt-logic/src/settings.rs
+3 -4
backend/src/game/state.rs manhunt-logic/src/game_state.rs
··· 10 10 use serde::{Deserialize, Serialize}; 11 11 use uuid::Uuid; 12 12 13 - use crate::game::GameEvent; 14 - 15 - use super::{ 13 + use crate::{ 14 + game::{Id, UtcDT}, 15 + game_events::GameEvent, 16 16 location::Location, 17 17 powerups::PowerUpType, 18 18 settings::{GameSettings, PingStartCondition}, 19 - Id, UtcDT, 20 19 }; 21 20 22 21 #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
-10
backend/src/game/transport.rs
··· 1 - use super::events::GameEvent; 2 - 3 - pub trait Transport { 4 - /// Receive an event 5 - async fn receive_messages(&self) -> impl Iterator<Item = GameEvent>; 6 - /// Send an event 7 - async fn send_message(&self, msg: GameEvent); 8 - /// Disconnect from the transport 9 - fn disconnect(&self) {} 10 - }
+7 -6
backend/src/history.rs
··· 1 + use anyhow::Context; 1 2 use serde::{Deserialize, Serialize}; 2 - use std::{collections::HashMap, sync::Arc}; 3 + use std::{collections::HashMap, result::Result as StdResult, sync::Arc}; 3 4 use tauri::{AppHandle, Runtime}; 4 5 use tauri_plugin_store::{Store, StoreExt}; 5 6 use uuid::Uuid; 6 7 7 - use crate::{ 8 - game::{GameHistory, UtcDT}, 9 - prelude::*, 10 - profile::PlayerProfile, 11 - }; 8 + use manhunt_logic::{GameHistory, PlayerProfile}; 9 + 10 + use crate::UtcDT; 11 + 12 + type Result<T = (), E = anyhow::Error> = StdResult<T, E>; 12 13 13 14 #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] 14 15 pub struct AppGameHistory {
+125 -121
backend/src/lib.rs
··· 1 - mod game; 2 1 mod history; 3 - mod lobby; 4 2 mod location; 5 - mod profile; 6 - mod server; 7 - mod transport; 3 + mod profiles; 8 4 9 - use std::{collections::HashMap, sync::Arc, time::Duration}; 5 + use std::{collections::HashMap, marker::PhantomData, sync::Arc, time::Duration}; 10 6 11 - use game::{Game as BaseGame, GameSettings}; 12 - use history::AppGameHistory; 13 - use lobby::{Lobby, LobbyState, StartGameInfo}; 7 + use anyhow::Context; 14 8 use location::TauriLocation; 15 9 use log::{error, info, warn, LevelFilter}; 16 - use profile::PlayerProfile; 10 + use manhunt_logic::{ 11 + Game as BaseGame, GameSettings, GameUiState, Lobby as BaseLobby, LobbyState, PlayerProfile, 12 + StartGameInfo, StateUpdateSender, 13 + }; 14 + use manhunt_transport::{generate_join_code, room_exists, MatchboxTransport}; 17 15 use serde::{Deserialize, Serialize}; 18 16 use tauri::{AppHandle, Manager, State}; 19 17 use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; 20 18 use tauri_specta::{collect_commands, collect_events, ErrorHandlingMode, Event}; 21 19 use tokio::sync::RwLock; 22 - use transport::MatchboxTransport; 23 20 use uuid::Uuid; 24 21 25 - mod prelude { 26 - pub use anyhow::{anyhow, bail, Context, Error as AnyhowError}; 27 - pub use std::result::Result as StdResult; 22 + type UtcDT = chrono::DateTime<chrono::Utc>; 23 + 24 + /// The state of the game has changed 25 + #[derive(Serialize, Deserialize, Clone, Default, Debug, specta::Type, tauri_specta::Event)] 26 + struct GameStateUpdate; 28 27 29 - pub type Result<T = (), E = AnyhowError> = StdResult<T, E>; 28 + /// The state of the lobby has changed 29 + #[derive(Serialize, Deserialize, Clone, Default, Debug, specta::Type, tauri_specta::Event)] 30 + struct LobbyStateUpdate; 31 + 32 + struct TauriStateUpdateSender<E: Clone + Default + Event + Serialize>(AppHandle, PhantomData<E>); 33 + 34 + impl<E: Serialize + Clone + Default + Event> TauriStateUpdateSender<E> { 35 + fn new(app: &AppHandle) -> Self { 36 + Self(app.clone(), PhantomData) 37 + } 30 38 } 31 39 32 - use prelude::*; 40 + impl<E: Serialize + Clone + Default + Event> StateUpdateSender for TauriStateUpdateSender<E> { 41 + fn send_update(&self) { 42 + if let Err(why) = E::default().emit(&self.0) { 43 + error!("Error sending Game state update to UI: {why:?}"); 44 + } 45 + } 46 + } 33 47 34 - type Game = BaseGame<TauriLocation, MatchboxTransport, TauriStateUpdateSender>; 48 + type Game = BaseGame<TauriLocation, MatchboxTransport, TauriStateUpdateSender<GameStateUpdate>>; 49 + type Lobby = BaseLobby<MatchboxTransport, TauriStateUpdateSender<LobbyStateUpdate>>; 35 50 36 51 enum AppState { 37 52 Setup, ··· 52 67 53 68 type AppStateHandle = RwLock<AppState>; 54 69 55 - fn generate_join_code() -> String { 56 - // 5 character sequence of A-Z 57 - (0..5) 58 - .map(|_| (b'A' + rand::random_range(0..26)) as char) 59 - .collect::<String>() 60 - } 61 - 62 70 const GAME_TICK_RATE: Duration = Duration::from_secs(1); 63 71 64 72 /// The app is changing screens, contains the screen it's switching to 65 73 #[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] 66 74 struct ChangeScreen(AppScreen); 67 75 68 - /// The state of the game has updated in some way, you're expected to call [get_game_state] when 69 - /// receiving this 70 - #[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] 71 - struct GameStateUpdate; 72 - 73 - struct TauriStateUpdateSender(AppHandle); 74 - 75 - impl StateUpdateSender for TauriStateUpdateSender { 76 - fn send_update(&self) { 77 - if let Err(why) = GameStateUpdate.emit(&self.0) { 78 - error!("Error sending Game state update to UI: {why:?}"); 79 - } 80 - } 76 + fn error_dialog(app: &AppHandle, msg: &str) { 77 + app.dialog() 78 + .message(msg) 79 + .kind(MessageDialogKind::Error) 80 + .show(|_| {}); 81 81 } 82 82 83 83 impl AppState { 84 - pub async fn start_game(&mut self, app: AppHandle, my_id: Uuid, start: StartGameInfo) { 84 + pub async fn start_game(&mut self, app: AppHandle, start: StartGameInfo) { 85 85 if let AppState::Lobby(lobby) = self { 86 86 let transport = lobby.clone_transport(); 87 87 let profiles = lobby.clone_profiles().await; 88 88 let location = TauriLocation::new(app.clone()); 89 - let state_updates = TauriStateUpdateSender(app.clone()); 89 + let state_updates = TauriStateUpdateSender::new(&app); 90 90 let game = Arc::new(Game::new( 91 - my_id, 92 91 GAME_TICK_RATE, 93 - start.initial_caught_state, 94 - start.settings, 92 + start, 95 93 transport, 96 94 location, 97 95 state_updates, 98 96 )); 99 97 *self = AppState::Game(game.clone(), profiles.clone()); 98 + Self::game_loop(app.clone(), game, profiles); 100 99 Self::emit_screen_change(&app, AppScreen::Game); 101 - tokio::spawn(async move { 102 - let res = game.main_loop().await; 103 - let app2 = app.clone(); 104 - let state_handle = app.state::<AppStateHandle>(); 105 - let mut state = state_handle.write().await; 106 - match res { 107 - Ok(Some(history)) => { 108 - let history = AppGameHistory::new(history, profiles); 109 - if let Err(why) = history.save_history(&app2) { 110 - error!("Failed to save game history: {why:?}"); 111 - app2.dialog() 112 - .message("Failed to save the history of this game") 113 - .kind(MessageDialogKind::Error) 114 - .show(|_| {}); 115 - } 116 - state.quit_to_menu(app2); 117 - } 118 - Ok(None) => { 119 - info!("User quit game"); 120 - } 121 - Err(why) => { 122 - error!("Game Error: {why:?}"); 123 - app2.dialog() 124 - .message(format!("Connection Error: {why}")) 125 - .kind(MessageDialogKind::Error) 126 - .show(|_| {}); 127 - state.quit_to_menu(app2); 100 + } 101 + } 102 + 103 + fn game_loop(app: AppHandle, game: Arc<Game>, profiles: HashMap<Uuid, PlayerProfile>) { 104 + tokio::spawn(async move { 105 + let res = game.main_loop().await; 106 + let state_handle = app.state::<AppStateHandle>(); 107 + let mut state = state_handle.write().await; 108 + match res { 109 + Ok(Some(history)) => { 110 + let history = AppGameHistory::new(history, profiles); 111 + if let Err(why) = history.save_history(&app) { 112 + error!("Failed to save game history: {why:?}"); 113 + error_dialog(&app, "Failed to save the history of this game"); 128 114 } 115 + state.quit_to_menu(app.clone()).await; 116 + } 117 + Ok(None) => { 118 + info!("User quit game"); 129 119 } 130 - }); 131 - } 120 + Err(why) => { 121 + error!("Game Error: {why:?}"); 122 + app.dialog() 123 + .message(format!("Connection Error: {why}")) 124 + .kind(MessageDialogKind::Error) 125 + .show(|_| {}); 126 + state.quit_to_menu(app.clone()).await; 127 + } 128 + } 129 + }); 132 130 } 133 131 134 132 pub fn get_menu(&self) -> Result<&PlayerProfile> { ··· 185 183 186 184 pub fn complete_setup(&mut self, app: &AppHandle, profile: PlayerProfile) -> Result { 187 185 if let AppState::Setup = self { 188 - profile.write_to_store(app); 186 + write_profile_to_store(app, profile.clone()); 189 187 *self = AppState::Menu(profile); 190 188 Self::emit_screen_change(app, AppScreen::Menu); 191 189 Ok(()) ··· 207 205 } 208 206 } 209 207 210 - pub fn start_lobby( 208 + fn lobby_loop(app: AppHandle, lobby: Arc<Lobby>) { 209 + tokio::spawn(async move { 210 + let res = lobby.main_loop().await; 211 + let app_game = app.clone(); 212 + let state_handle = app.state::<AppStateHandle>(); 213 + let mut state = state_handle.write().await; 214 + match res { 215 + Ok(Some(start)) => { 216 + info!("Starting game as"); 217 + state.start_game(app_game, start).await; 218 + } 219 + Ok(None) => { 220 + info!("User quit lobby"); 221 + } 222 + Err(why) => { 223 + error!("Lobby Error: {why}"); 224 + error_dialog(&app_game, &format!("Error joining the lobby: {why}")); 225 + state.quit_to_menu(app_game).await; 226 + } 227 + } 228 + }); 229 + } 230 + 231 + pub async fn start_lobby( 211 232 &mut self, 212 233 join_code: Option<String>, 213 234 app: AppHandle, ··· 216 237 if let AppState::Menu(profile) = self { 217 238 let host = join_code.is_none(); 218 239 let room_code = join_code.unwrap_or_else(generate_join_code); 219 - let lobby = Arc::new(Lobby::new( 220 - &room_code, 221 - host, 222 - profile.clone(), 223 - settings, 224 - app.clone(), 225 - )); 226 - *self = AppState::Lobby(lobby.clone()); 227 - let app2 = app.clone(); 228 - tokio::spawn(async move { 229 - let res = lobby.open().await; 230 - let app_game = app2.clone(); 231 - let state_handle = app2.state::<AppStateHandle>(); 232 - let mut state = state_handle.write().await; 233 - match res { 234 - Ok(Some((my_id, start))) => { 235 - info!("Starting game as {my_id}"); 236 - state.start_game(app_game, my_id, start).await; 237 - } 238 - Ok(None) => { 239 - info!("User quit lobby"); 240 - } 241 - Err(why) => { 242 - error!("Lobby Error: {why}"); 243 - app_game 244 - .dialog() 245 - .message(format!("Error joining the lobby: {why}")) 246 - .kind(MessageDialogKind::Error) 247 - .show(|_| {}); 248 - state.quit_to_menu(app_game); 249 - } 240 + let state_updates = TauriStateUpdateSender::<LobbyStateUpdate>::new(&app); 241 + let lobby = 242 + Lobby::new(&room_code, host, profile.clone(), settings, state_updates).await; 243 + match lobby { 244 + Ok(lobby) => { 245 + *self = AppState::Lobby(lobby.clone()); 246 + Self::lobby_loop(app.clone(), lobby); 247 + Self::emit_screen_change(&app, AppScreen::Lobby); 248 + } 249 + Err(why) => { 250 + error_dialog( 251 + &app, 252 + &format!("Couldn't connect you to the lobby\n\n{why:?}"), 253 + ); 250 254 } 251 - }); 252 - Self::emit_screen_change(&app, AppScreen::Lobby); 255 + } 253 256 } 254 257 } 255 258 256 - pub fn quit_to_menu(&mut self, app: AppHandle) { 259 + pub async fn quit_to_menu(&mut self, app: AppHandle) { 257 260 let profile = match self { 258 261 AppState::Setup => None, 259 262 AppState::Menu(_) => { ··· 261 264 return; 262 265 } 263 266 AppState::Lobby(lobby) => { 264 - lobby.quit_lobby(); 265 - Some(lobby.self_profile.clone()) 267 + lobby.quit_lobby().await; 268 + read_profile_from_store(&app) 266 269 } 267 270 AppState::Game(game, _) => { 268 - game.quit_game(); 269 - PlayerProfile::load_from_store(&app) 271 + game.quit_game().await; 272 + read_profile_from_store(&app) 270 273 } 271 - AppState::Replay(_) => PlayerProfile::load_from_store(&app), 274 + AppState::Replay(_) => read_profile_from_store(&app), 272 275 }; 273 276 let screen = if let Some(profile) = profile { 274 277 *self = AppState::Menu(profile); ··· 284 287 285 288 use std::result::Result as StdResult; 286 289 287 - use crate::game::{GameUiState, StateUpdateSender, UtcDT}; 290 + use crate::{ 291 + history::AppGameHistory, 292 + profiles::{read_profile_from_store, write_profile_to_store}, 293 + }; 288 294 289 295 type Result<T = (), E = String> = StdResult<T, E>; 290 296 ··· 309 315 /// Quit a running game or leave a lobby 310 316 async fn quit_to_menu(app: AppHandle, state: State<'_, AppStateHandle>) -> Result { 311 317 let mut state = state.write().await; 312 - state.quit_to_menu(app); 318 + state.quit_to_menu(app).await; 313 319 Ok(()) 314 320 } 315 321 ··· 358 364 /// (Screen: Menu) Check if a room code is valid to join, use this before starting a game 359 365 /// for faster error checking. 360 366 async fn check_room_code(code: &str) -> Result<bool> { 361 - server::room_exists(code) 362 - .await 363 - .map_err(|err| err.to_string()) 367 + room_exists(code).await.map_err(|err| err.to_string()) 364 368 } 365 369 366 370 #[tauri::command] ··· 371 375 app: AppHandle, 372 376 state: State<'_, AppStateHandle>, 373 377 ) -> Result { 374 - new_profile.write_to_store(&app); 378 + write_profile_to_store(&app, new_profile.clone()); 375 379 let mut state = state.write().await; 376 380 let profile = state.get_menu_mut()?; 377 381 *profile = new_profile; ··· 389 393 state: State<'_, AppStateHandle>, 390 394 ) -> Result { 391 395 let mut state = state.write().await; 392 - state.start_lobby(join_code, app, settings); 396 + state.start_lobby(join_code, app, settings).await; 393 397 Ok(()) 394 398 } 395 399 ··· 523 527 .events(collect_events![ 524 528 ChangeScreen, 525 529 GameStateUpdate, 526 - lobby::LobbyStateUpdate 530 + LobbyStateUpdate 527 531 ]) 528 532 } 529 533 ··· 551 555 552 556 let handle = app.handle().clone(); 553 557 tauri::async_runtime::spawn(async move { 554 - if let Some(profile) = PlayerProfile::load_from_store(&handle) { 558 + if let Some(profile) = read_profile_from_store(&handle) { 555 559 let state_handle = handle.state::<AppStateHandle>(); 556 560 let mut state = state_handle.write().await; 557 561 *state = AppState::Menu(profile);
-244
backend/src/lobby.rs
··· 1 - use std::{collections::HashMap, sync::Arc}; 2 - 3 - use log::{error, warn}; 4 - use serde::{Deserialize, Serialize}; 5 - use tauri::AppHandle; 6 - use tauri_specta::Event; 7 - use tokio::sync::Mutex; 8 - use uuid::Uuid; 9 - 10 - use crate::{ 11 - game::GameSettings, 12 - prelude::*, 13 - profile::PlayerProfile, 14 - server, 15 - transport::{MatchboxTransport, TransportMessage}, 16 - }; 17 - 18 - #[derive(Debug, Clone, Serialize, Deserialize)] 19 - pub struct StartGameInfo { 20 - pub settings: GameSettings, 21 - pub initial_caught_state: HashMap<Uuid, bool>, 22 - } 23 - 24 - #[derive(Debug, Clone, Serialize, Deserialize)] 25 - pub enum LobbyMessage { 26 - /// Message sent on a new peer, to sync profiles 27 - PlayerSync(PlayerProfile), 28 - /// Message sent on a new peer from the host, to sync game settings 29 - HostPush(GameSettings), 30 - /// Host signals starting the game 31 - StartGame(StartGameInfo), 32 - /// A player has switched teams 33 - PlayerSwitch(bool), 34 - } 35 - 36 - #[derive(Clone, Serialize, Deserialize, specta::Type)] 37 - pub struct LobbyState { 38 - profiles: HashMap<Uuid, PlayerProfile>, 39 - join_code: String, 40 - /// True represents seeker, false hider 41 - teams: HashMap<Uuid, bool>, 42 - self_id: Option<Uuid>, 43 - self_seeker: bool, 44 - is_host: bool, 45 - settings: GameSettings, 46 - } 47 - 48 - pub struct Lobby { 49 - is_host: bool, 50 - join_code: String, 51 - pub self_profile: PlayerProfile, 52 - state: Mutex<LobbyState>, 53 - transport: Arc<MatchboxTransport>, 54 - app: AppHandle, 55 - } 56 - 57 - /// The lobby state has updated in some way, you're expected to call [get_lobby_state] after 58 - /// receiving this 59 - #[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] 60 - pub struct LobbyStateUpdate; 61 - 62 - impl Lobby { 63 - pub fn new( 64 - join_code: &str, 65 - host: bool, 66 - profile: PlayerProfile, 67 - settings: GameSettings, 68 - app: AppHandle, 69 - ) -> Self { 70 - Self { 71 - app, 72 - transport: Arc::new(MatchboxTransport::new(join_code, host)), 73 - is_host: host, 74 - self_profile: profile, 75 - join_code: join_code.to_string(), 76 - state: Mutex::new(LobbyState { 77 - teams: HashMap::with_capacity(5), 78 - join_code: join_code.to_string(), 79 - profiles: HashMap::with_capacity(5), 80 - self_seeker: false, 81 - self_id: None, 82 - is_host: host, 83 - settings, 84 - }), 85 - } 86 - } 87 - 88 - fn emit_state_update(&self) { 89 - if let Err(why) = LobbyStateUpdate.emit(&self.app) { 90 - error!("Error emitting Lobby state update: {why:?}"); 91 - } 92 - } 93 - 94 - pub fn clone_transport(&self) -> Arc<MatchboxTransport> { 95 - self.transport.clone() 96 - } 97 - 98 - pub async fn clone_state(&self) -> LobbyState { 99 - self.state.lock().await.clone() 100 - } 101 - 102 - pub async fn clone_profiles(&self) -> HashMap<Uuid, PlayerProfile> { 103 - let state = self.state.lock().await; 104 - state.profiles.clone() 105 - } 106 - 107 - /// Set self as seeker or hider 108 - pub async fn switch_teams(&self, seeker: bool) { 109 - let mut state = self.state.lock().await; 110 - state.self_seeker = seeker; 111 - if let Some(id) = state.self_id { 112 - if let Some(state_seeker) = state.teams.get_mut(&id) { 113 - *state_seeker = seeker; 114 - } 115 - } 116 - drop(state); 117 - self.transport 118 - .send_transport_message(None, LobbyMessage::PlayerSwitch(seeker).into()) 119 - .await; 120 - self.emit_state_update(); 121 - } 122 - 123 - /// (Host) Update game settings 124 - pub async fn update_settings(&self, new_settings: GameSettings) { 125 - if self.is_host { 126 - let mut state = self.state.lock().await; 127 - state.settings = new_settings.clone(); 128 - drop(state); 129 - let msg = LobbyMessage::HostPush(new_settings); 130 - self.send_transport_message(None, msg).await; 131 - self.emit_state_update(); 132 - } 133 - } 134 - 135 - async fn send_transport_message(&self, id: Option<Uuid>, msg: LobbyMessage) { 136 - self.transport.send_transport_message(id, msg.into()).await 137 - } 138 - 139 - async fn signaling_mark_started(&self) -> Result { 140 - server::mark_room_started(&self.join_code).await 141 - } 142 - 143 - /// (Host) Start the game 144 - pub async fn start_game(&self) { 145 - if self.is_host { 146 - let state = self.state.lock().await; 147 - let start_game_info = StartGameInfo { 148 - settings: state.settings.clone(), 149 - initial_caught_state: state.teams.clone(), 150 - }; 151 - drop(state); 152 - let msg = LobbyMessage::StartGame(start_game_info); 153 - self.send_transport_message(None, msg).await; 154 - if let Err(why) = self.signaling_mark_started().await { 155 - warn!("Failed to tell signalling server that the match started: {why:?}"); 156 - } 157 - self.emit_state_update(); 158 - } 159 - } 160 - 161 - pub fn quit_lobby(&self) { 162 - self.transport.cancel(); 163 - } 164 - 165 - pub async fn open(&self) -> Result<Option<(Uuid, StartGameInfo)>> { 166 - let transport_inner = self.transport.clone(); 167 - tokio::spawn(async move { transport_inner.transport_loop().await }); 168 - 169 - let res = 'lobby: loop { 170 - self.emit_state_update(); 171 - 172 - let msgs = self.transport.recv_transport_messages().await; 173 - 174 - for (peer, msg) in msgs { 175 - match msg { 176 - TransportMessage::IdAssigned(id) => { 177 - let mut state = self.state.lock().await; 178 - state.self_id = Some(id); 179 - let seeker = state.self_seeker; 180 - state.teams.insert(id, seeker); 181 - state.profiles.insert(id, self.self_profile.clone()); 182 - } 183 - TransportMessage::Disconnected => { 184 - break 'lobby Ok(None); 185 - } 186 - TransportMessage::Error(why) => { 187 - break 'lobby Err(anyhow!("Transport error: {why}")); 188 - } 189 - TransportMessage::Game(game_event) => { 190 - eprintln!("Peer {peer:?} sent a GameEvent: {game_event:?}"); 191 - } 192 - TransportMessage::Lobby(lobby_message) => match *lobby_message { 193 - LobbyMessage::PlayerSync(player_profile) => { 194 - let mut state = self.state.lock().await; 195 - state.profiles.insert(peer, player_profile); 196 - } 197 - LobbyMessage::HostPush(game_settings) => { 198 - let mut state = self.state.lock().await; 199 - state.settings = game_settings; 200 - } 201 - LobbyMessage::StartGame(start_game_info) => { 202 - let id = self 203 - .state 204 - .lock() 205 - .await 206 - .self_id 207 - .expect("Error getting self ID"); 208 - break 'lobby Ok(Some((id, start_game_info))); 209 - } 210 - LobbyMessage::PlayerSwitch(seeker) => { 211 - let mut state = self.state.lock().await; 212 - state.teams.insert(peer, seeker); 213 - } 214 - }, 215 - TransportMessage::PeerConnect => { 216 - let msg = LobbyMessage::PlayerSync(self.self_profile.clone()); 217 - let mut state = self.state.lock().await; 218 - state.teams.insert(peer, false); 219 - drop(state); 220 - self.send_transport_message(Some(peer), msg).await; 221 - if self.is_host { 222 - let state = self.state.lock().await; 223 - let msg = LobbyMessage::HostPush(state.settings.clone()); 224 - drop(state); 225 - self.send_transport_message(Some(peer), msg).await; 226 - } 227 - } 228 - TransportMessage::PeerDisconnect => { 229 - let mut state = self.state.lock().await; 230 - state.profiles.remove(&peer); 231 - state.teams.remove(&peer); 232 - } 233 - TransportMessage::Seq(_) => {} 234 - } 235 - } 236 - }; 237 - 238 - if res.is_err() { 239 - self.transport.cancel(); 240 - } 241 - 242 - res 243 - } 244 - }
+1 -1
backend/src/location.rs
··· 1 1 use tauri::AppHandle; 2 2 use tauri_plugin_geolocation::{GeolocationExt, PositionOptions}; 3 3 4 - use crate::game::{Location, LocationService}; 4 + use manhunt_logic::{Location, LocationService}; 5 5 6 6 pub struct TauriLocation(AppHandle); 7 7
-36
backend/src/profile.rs
··· 1 - use serde::{Deserialize, Serialize}; 2 - use tauri::AppHandle; 3 - use tauri_plugin_store::StoreExt; 4 - 5 - #[derive(Clone, Default, Debug, Serialize, Deserialize, specta::Type)] 6 - pub struct PlayerProfile { 7 - display_name: String, 8 - pfp_base64: Option<String>, 9 - } 10 - 11 - const STORE_NAME: &str = "profile.json"; 12 - 13 - impl PlayerProfile { 14 - // pub fn has_pfp(&self) -> bool { 15 - // self.pfp_base64.is_some() 16 - // } 17 - 18 - pub fn load_from_store(app: &AppHandle) -> Option<Self> { 19 - let store = app.store(STORE_NAME).expect("Couldn't Create Store"); 20 - 21 - let profile = store 22 - .get("profile") 23 - .and_then(|v| serde_json::from_value::<Self>(v).ok()); 24 - 25 - store.close_resource(); 26 - 27 - profile 28 - } 29 - 30 - pub fn write_to_store(&self, app: &AppHandle) { 31 - let store = app.store(STORE_NAME).expect("Couldn't create store"); 32 - 33 - let value = serde_json::to_value(self.clone()).expect("Failed to serialize"); 34 - store.set("profile", value); 35 - } 36 - }
+24
backend/src/profiles.rs
··· 1 + use manhunt_logic::PlayerProfile; 2 + use tauri::AppHandle; 3 + use tauri_plugin_store::StoreExt; 4 + 5 + const STORE_NAME: &str = "profile"; 6 + 7 + pub fn read_profile_from_store(app: &AppHandle) -> Option<PlayerProfile> { 8 + let store = app.store(STORE_NAME).expect("Couldn't Create Store"); 9 + 10 + let profile = store 11 + .get("profile") 12 + .and_then(|v| serde_json::from_value::<PlayerProfile>(v).ok()); 13 + 14 + store.close_resource(); 15 + 16 + profile 17 + } 18 + 19 + pub fn write_profile_to_store(app: &AppHandle, profile: PlayerProfile) { 20 + let store = app.store(STORE_NAME).expect("Couldn't create store"); 21 + 22 + let value = serde_json::to_value(profile).expect("Failed to serialize"); 23 + store.set("profile", value); 24 + }
+10 -11
backend/src/server.rs manhunt-transport/src/server.rs
··· 1 1 use reqwest::StatusCode; 2 2 3 - use crate::prelude::*; 3 + use manhunt_logic::prelude::*; 4 4 5 5 const fn server_host() -> &'static str { 6 6 if let Some(host) = option_env!("SIGNAL_SERVER_HOST") { ··· 27 27 } 28 28 29 29 const fn server_ws_proto() -> &'static str { 30 - if server_secure() { 31 - "wss" 32 - } else { 33 - "ws" 34 - } 30 + if server_secure() { "wss" } else { "ws" } 35 31 } 36 32 37 33 const fn server_http_proto() -> &'static str { 38 - if server_secure() { 39 - "https" 40 - } else { 41 - "http" 42 - } 34 + if server_secure() { "https" } else { "http" } 43 35 } 44 36 45 37 const SERVER_HOST: &str = server_host(); ··· 77 69 .context("Server returned error")?; 78 70 Ok(()) 79 71 } 72 + 73 + pub fn generate_join_code() -> String { 74 + // 5 character sequence of A-Z 75 + (0..5) 76 + .map(|_| (b'A' + rand::random_range(0..26)) as char) 77 + .collect::<String>() 78 + }
-378
backend/src/transport.rs
··· 1 - use std::{ 2 - collections::{HashMap, HashSet}, 3 - time::Duration, 4 - }; 5 - 6 - use anyhow::Context; 7 - use futures::FutureExt; 8 - use log::error; 9 - use matchbox_socket::{Error as SocketError, PeerId, PeerState, WebRtcSocket}; 10 - use serde::{Deserialize, Serialize}; 11 - use tokio::sync::{Mutex, RwLock}; 12 - use tokio_util::sync::CancellationToken; 13 - use uuid::Uuid; 14 - 15 - use crate::{ 16 - game::{GameEvent, Transport}, 17 - lobby::LobbyMessage, 18 - prelude::*, 19 - server, 20 - }; 21 - 22 - #[derive(Serialize, Deserialize, Debug, Clone)] 23 - pub struct TransportChunk { 24 - id: u64, 25 - current: usize, 26 - total: usize, 27 - data: Vec<u8>, 28 - } 29 - 30 - #[derive(Debug, Serialize, Deserialize, Clone)] 31 - pub enum TransportMessage { 32 - /// The transport has received a peer id 33 - IdAssigned(Uuid), 34 - /// Message related to the actual game 35 - /// Boxed for space reasons 36 - Game(Box<GameEvent>), 37 - /// Message related to the pre-game lobby 38 - Lobby(Box<LobbyMessage>), 39 - /// Internal message when peer connects 40 - PeerConnect, 41 - /// Internal message when peer disconnects 42 - PeerDisconnect, 43 - /// Event sent when the transport gets disconnected, used to help consumers know when to stop 44 - /// consuming messages 45 - Disconnected, 46 - /// Event when the transport encounters a critical error and needs to disconnect 47 - Error(String), 48 - /// Internal message for packet chunking 49 - Seq(TransportChunk), 50 - } 51 - 52 - // Max packet size according to: https://github.com/johanhelsing/matchbox/issues/272 53 - const MAX_PACKET_SIZE: usize = 65535; 54 - 55 - // Align packets with a bit of extra space for [TransportMessage::Seq] header 56 - const PACKET_ALIGNMENT: usize = MAX_PACKET_SIZE - 128; 57 - 58 - impl TransportMessage { 59 - pub fn serialize(&self) -> Vec<u8> { 60 - rmp_serde::to_vec(self).expect("Failed to encode") 61 - } 62 - 63 - pub fn deserialize(data: &[u8]) -> Result<Self> { 64 - rmp_serde::from_slice(data).context("While deserializing message") 65 - } 66 - 67 - pub fn from_packets(packets: impl Iterator<Item = Box<[u8]>>) -> Result<Self> { 68 - let full_data = packets.flatten().collect::<Box<[u8]>>(); 69 - Self::deserialize(&full_data).context("While decoding a multi-part message") 70 - } 71 - 72 - pub fn to_packets(&self) -> Vec<Vec<u8>> { 73 - let bytes = self.serialize(); 74 - if bytes.len() > MAX_PACKET_SIZE { 75 - let id = rand::random_range(0..u64::MAX); 76 - let packets_needed = bytes.len().div_ceil(PACKET_ALIGNMENT); 77 - let rem = bytes.len() % PACKET_ALIGNMENT; 78 - let bytes = bytes.into_boxed_slice(); 79 - (0..packets_needed) 80 - .map(|idx| { 81 - let start = PACKET_ALIGNMENT * idx; 82 - let end = if idx == packets_needed - 1 { 83 - start + rem 84 - } else { 85 - PACKET_ALIGNMENT * (idx + 1) 86 - }; 87 - let data = bytes[start..end].to_vec(); 88 - let chunk = TransportChunk { 89 - id, 90 - current: idx, 91 - total: packets_needed, 92 - data, 93 - }; 94 - TransportMessage::Seq(chunk).serialize() 95 - }) 96 - .collect() 97 - } else { 98 - vec![bytes] 99 - } 100 - } 101 - } 102 - 103 - impl From<GameEvent> for TransportMessage { 104 - fn from(v: GameEvent) -> Self { 105 - Self::Game(Box::new(v)) 106 - } 107 - } 108 - 109 - impl From<LobbyMessage> for TransportMessage { 110 - fn from(v: LobbyMessage) -> Self { 111 - Self::Lobby(Box::new(v)) 112 - } 113 - } 114 - 115 - type OutgoingMsgPair = (Option<Uuid>, TransportMessage); 116 - type OutgoingQueueSender = tokio::sync::mpsc::Sender<OutgoingMsgPair>; 117 - type OutgoingQueueReceiver = tokio::sync::mpsc::Receiver<OutgoingMsgPair>; 118 - 119 - type IncomingMsgPair = (Uuid, TransportMessage); 120 - type IncomingQueueSender = tokio::sync::mpsc::Sender<IncomingMsgPair>; 121 - type IncomingQueueReceiver = tokio::sync::mpsc::Receiver<IncomingMsgPair>; 122 - 123 - pub struct MatchboxTransport { 124 - ws_url: String, 125 - incoming: (IncomingQueueSender, Mutex<IncomingQueueReceiver>), 126 - outgoing: (OutgoingQueueSender, Mutex<OutgoingQueueReceiver>), 127 - my_id: RwLock<Option<Uuid>>, 128 - cancel_token: CancellationToken, 129 - } 130 - 131 - impl MatchboxTransport { 132 - pub fn new(join_code: &str, is_host: bool) -> Self { 133 - let (itx, irx) = tokio::sync::mpsc::channel(15); 134 - let (otx, orx) = tokio::sync::mpsc::channel(15); 135 - 136 - Self { 137 - ws_url: server::room_url(join_code, is_host), 138 - incoming: (itx, Mutex::new(irx)), 139 - outgoing: (otx, Mutex::new(orx)), 140 - my_id: RwLock::new(None), 141 - cancel_token: CancellationToken::new(), 142 - } 143 - } 144 - 145 - pub async fn send_transport_message(&self, peer: Option<Uuid>, msg: TransportMessage) { 146 - self.outgoing 147 - .0 148 - .send((peer, msg)) 149 - .await 150 - .expect("Failed to add to outgoing queue"); 151 - } 152 - 153 - pub async fn recv_transport_messages(&self) -> Vec<IncomingMsgPair> { 154 - let mut incoming_rx = self.incoming.1.lock().await; 155 - let mut buffer = Vec::with_capacity(60); 156 - incoming_rx.recv_many(&mut buffer, 60).await; 157 - buffer 158 - } 159 - 160 - async fn push_incoming(&self, id: Uuid, msg: TransportMessage) { 161 - self.incoming 162 - .0 163 - .send((id, msg)) 164 - .await 165 - .expect("Failed to push to incoming queue"); 166 - } 167 - 168 - async fn handle_send( 169 - &self, 170 - socket: &mut WebRtcSocket, 171 - all_peers: &HashSet<PeerId>, 172 - messages: &mut Vec<OutgoingMsgPair>, 173 - ) { 174 - if let Some(my_id) = *self.my_id.read().await { 175 - for (_, msg) in messages.iter().filter(|(id, _)| id.is_none()) { 176 - self.push_incoming(my_id, msg.clone()).await; 177 - } 178 - } 179 - 180 - let packets = messages.drain(..).flat_map(|(id, msg)| { 181 - msg.to_packets() 182 - .into_iter() 183 - .map(move |packet| (id, packet.into_boxed_slice())) 184 - }); 185 - 186 - for (peer, packet) in packets { 187 - if let Some(peer) = peer { 188 - let channel = socket.channel_mut(0); 189 - channel.send(packet, PeerId(peer)); 190 - } else { 191 - let channel = socket.channel_mut(0); 192 - 193 - for peer in all_peers.iter() { 194 - // TODO: Any way around having to clone here? 195 - let data = packet.clone(); 196 - channel.send(data, *peer); 197 - } 198 - } 199 - } 200 - } 201 - 202 - pub fn cancel(&self) { 203 - self.cancel_token.cancel(); 204 - } 205 - 206 - pub async fn transport_loop(&self) { 207 - let (mut socket, loop_fut) = WebRtcSocket::new_reliable(&self.ws_url); 208 - 209 - let loop_fut = async { 210 - let msg = match loop_fut.await { 211 - Ok(_) => TransportMessage::Disconnected, 212 - Err(e) => { 213 - let msg = match e { 214 - SocketError::ConnectionFailed(e) => { 215 - format!("Failed to connect to server: {e}") 216 - } 217 - SocketError::Disconnected(e) => { 218 - format!("Disconnected from server, network error or kick: {e}") 219 - } 220 - }; 221 - TransportMessage::Error(msg) 222 - } 223 - }; 224 - self.push_incoming(self.my_id.read().await.unwrap_or_default(), msg) 225 - .await; 226 - } 227 - .fuse(); 228 - 229 - tokio::pin!(loop_fut); 230 - 231 - let mut all_peers = HashSet::<PeerId>::with_capacity(20); 232 - let mut my_id = None; 233 - 234 - let mut timer = tokio::time::interval(Duration::from_millis(100)); 235 - 236 - let mut partial_packets = 237 - HashMap::<u64, (Uuid, HashMap<usize, Option<Vec<u8>>>)>::with_capacity(3); 238 - 239 - loop { 240 - for (peer, state) in socket.update_peers() { 241 - let msg = match state { 242 - PeerState::Connected => { 243 - all_peers.insert(peer); 244 - TransportMessage::PeerConnect 245 - } 246 - PeerState::Disconnected => { 247 - all_peers.remove(&peer); 248 - TransportMessage::PeerDisconnect 249 - } 250 - }; 251 - self.push_incoming(peer.0, msg).await; 252 - } 253 - 254 - let messages = socket.channel_mut(0).receive(); 255 - 256 - let mut messages = messages 257 - .into_iter() 258 - .filter_map(|(id, data)| { 259 - let msg = TransportMessage::deserialize(&data).ok(); 260 - 261 - if let Some(TransportMessage::Seq(TransportChunk { 262 - id: multipart_id, 263 - current, 264 - total, 265 - data, 266 - })) = msg 267 - { 268 - if let Some((_, map)) = partial_packets.get_mut(&multipart_id) { 269 - map.insert(current, Some(data)); 270 - } else { 271 - let mut map = HashMap::from_iter((0..total).map(|idx| (idx, None))); 272 - map.insert(current, Some(data)); 273 - partial_packets.insert(multipart_id, (id.0, map)); 274 - } 275 - None 276 - } else { 277 - msg.map(|msg| (id.0, msg)) 278 - } 279 - }) 280 - .collect::<Vec<_>>(); 281 - 282 - let complete_messages = partial_packets 283 - .keys() 284 - .copied() 285 - .filter(|id| { 286 - partial_packets 287 - .get(id) 288 - .is_some_and(|(_, v)| v.values().all(Option::is_some)) 289 - }) 290 - .collect::<Vec<_>>(); 291 - 292 - for id in complete_messages { 293 - let (peer, packet_map) = partial_packets.remove(&id).unwrap(); 294 - 295 - let res = TransportMessage::from_packets( 296 - packet_map 297 - .into_values() 298 - .map(|v| v.unwrap().into_boxed_slice()), 299 - ); 300 - 301 - match res { 302 - Ok(msg) => messages.push((peer, msg)), 303 - Err(why) => error!("Error receiving message: {why:?}"), 304 - } 305 - } 306 - 307 - let push_iter = self 308 - .incoming 309 - .0 310 - .reserve_many(messages.len()) 311 - .await 312 - .expect("Couldn't reserve space"); 313 - 314 - for (sender, msg) in push_iter.zip(messages.into_iter()) { 315 - sender.send(msg); 316 - } 317 - 318 - if my_id.is_none() { 319 - if let Some(new_id) = socket.id() { 320 - my_id = Some(new_id.0); 321 - *self.my_id.write().await = Some(new_id.0); 322 - self.push_incoming(new_id.0, TransportMessage::IdAssigned(new_id.0)) 323 - .await; 324 - } 325 - } 326 - 327 - let mut outgoing_rx = self.outgoing.1.lock().await; 328 - 329 - let mut buffer = Vec::with_capacity(30); 330 - 331 - tokio::select! { 332 - _ = self.cancel_token.cancelled() => { 333 - // Break if cancelled externally 334 - break; 335 - } 336 - 337 - _ = &mut loop_fut => { 338 - // Break if disconnected 339 - break; 340 - } 341 - 342 - // Rerun every tick 343 - _ = timer.tick() => {} 344 - 345 - _ = outgoing_rx.recv_many(&mut buffer, 30) => { 346 - // Handle sending new messages 347 - self.handle_send(&mut socket, &all_peers, &mut buffer).await; 348 - } 349 - } 350 - } 351 - 352 - drop(socket); 353 - loop_fut.await 354 - } 355 - } 356 - 357 - impl Transport for MatchboxTransport { 358 - async fn receive_messages(&self) -> impl Iterator<Item = GameEvent> { 359 - self.recv_transport_messages() 360 - .await 361 - .into_iter() 362 - .filter_map(|(id, msg)| match msg { 363 - TransportMessage::Game(game_event) => Some(*game_event), 364 - TransportMessage::PeerDisconnect => Some(GameEvent::DroppedPlayer(id)), 365 - TransportMessage::Disconnected => Some(GameEvent::TransportDisconnect), 366 - TransportMessage::Error(err) => Some(GameEvent::TransportError(err)), 367 - _ => None, 368 - }) 369 - } 370 - 371 - async fn send_message(&self, msg: GameEvent) { 372 - self.send_transport_message(None, msg.into()).await; 373 - } 374 - 375 - fn disconnect(&self) { 376 - self.cancel(); 377 - } 378 - }
+72 -72
frontend/package-lock.json
··· 1080 1080 } 1081 1081 }, 1082 1082 "node_modules/@rolldown/pluginutils": { 1083 - "version": "1.0.0-beta.11", 1084 - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", 1085 - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", 1083 + "version": "1.0.0-beta.19", 1084 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", 1085 + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", 1086 1086 "dev": true, 1087 1087 "license": "MIT" 1088 1088 }, ··· 1511 1511 } 1512 1512 }, 1513 1513 "node_modules/@typescript-eslint/eslint-plugin": { 1514 - "version": "8.34.1", 1515 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", 1516 - "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", 1514 + "version": "8.35.0", 1515 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", 1516 + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", 1517 1517 "dev": true, 1518 1518 "license": "MIT", 1519 1519 "dependencies": { 1520 1520 "@eslint-community/regexpp": "^4.10.0", 1521 - "@typescript-eslint/scope-manager": "8.34.1", 1522 - "@typescript-eslint/type-utils": "8.34.1", 1523 - "@typescript-eslint/utils": "8.34.1", 1524 - "@typescript-eslint/visitor-keys": "8.34.1", 1521 + "@typescript-eslint/scope-manager": "8.35.0", 1522 + "@typescript-eslint/type-utils": "8.35.0", 1523 + "@typescript-eslint/utils": "8.35.0", 1524 + "@typescript-eslint/visitor-keys": "8.35.0", 1525 1525 "graphemer": "^1.4.0", 1526 1526 "ignore": "^7.0.0", 1527 1527 "natural-compare": "^1.4.0", ··· 1535 1535 "url": "https://opencollective.com/typescript-eslint" 1536 1536 }, 1537 1537 "peerDependencies": { 1538 - "@typescript-eslint/parser": "^8.34.1", 1538 + "@typescript-eslint/parser": "^8.35.0", 1539 1539 "eslint": "^8.57.0 || ^9.0.0", 1540 1540 "typescript": ">=4.8.4 <5.9.0" 1541 1541 } ··· 1551 1551 } 1552 1552 }, 1553 1553 "node_modules/@typescript-eslint/parser": { 1554 - "version": "8.34.1", 1555 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", 1556 - "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", 1554 + "version": "8.35.0", 1555 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", 1556 + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", 1557 1557 "dev": true, 1558 1558 "license": "MIT", 1559 1559 "dependencies": { 1560 - "@typescript-eslint/scope-manager": "8.34.1", 1561 - "@typescript-eslint/types": "8.34.1", 1562 - "@typescript-eslint/typescript-estree": "8.34.1", 1563 - "@typescript-eslint/visitor-keys": "8.34.1", 1560 + "@typescript-eslint/scope-manager": "8.35.0", 1561 + "@typescript-eslint/types": "8.35.0", 1562 + "@typescript-eslint/typescript-estree": "8.35.0", 1563 + "@typescript-eslint/visitor-keys": "8.35.0", 1564 1564 "debug": "^4.3.4" 1565 1565 }, 1566 1566 "engines": { ··· 1576 1576 } 1577 1577 }, 1578 1578 "node_modules/@typescript-eslint/project-service": { 1579 - "version": "8.34.1", 1580 - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", 1581 - "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", 1579 + "version": "8.35.0", 1580 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", 1581 + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", 1582 1582 "dev": true, 1583 1583 "license": "MIT", 1584 1584 "dependencies": { 1585 - "@typescript-eslint/tsconfig-utils": "^8.34.1", 1586 - "@typescript-eslint/types": "^8.34.1", 1585 + "@typescript-eslint/tsconfig-utils": "^8.35.0", 1586 + "@typescript-eslint/types": "^8.35.0", 1587 1587 "debug": "^4.3.4" 1588 1588 }, 1589 1589 "engines": { ··· 1598 1598 } 1599 1599 }, 1600 1600 "node_modules/@typescript-eslint/scope-manager": { 1601 - "version": "8.34.1", 1602 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", 1603 - "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", 1601 + "version": "8.35.0", 1602 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", 1603 + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", 1604 1604 "dev": true, 1605 1605 "license": "MIT", 1606 1606 "dependencies": { 1607 - "@typescript-eslint/types": "8.34.1", 1608 - "@typescript-eslint/visitor-keys": "8.34.1" 1607 + "@typescript-eslint/types": "8.35.0", 1608 + "@typescript-eslint/visitor-keys": "8.35.0" 1609 1609 }, 1610 1610 "engines": { 1611 1611 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 1616 1616 } 1617 1617 }, 1618 1618 "node_modules/@typescript-eslint/tsconfig-utils": { 1619 - "version": "8.34.1", 1620 - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", 1621 - "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", 1619 + "version": "8.35.0", 1620 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", 1621 + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", 1622 1622 "dev": true, 1623 1623 "license": "MIT", 1624 1624 "engines": { ··· 1633 1633 } 1634 1634 }, 1635 1635 "node_modules/@typescript-eslint/type-utils": { 1636 - "version": "8.34.1", 1637 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", 1638 - "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", 1636 + "version": "8.35.0", 1637 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", 1638 + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", 1639 1639 "dev": true, 1640 1640 "license": "MIT", 1641 1641 "dependencies": { 1642 - "@typescript-eslint/typescript-estree": "8.34.1", 1643 - "@typescript-eslint/utils": "8.34.1", 1642 + "@typescript-eslint/typescript-estree": "8.35.0", 1643 + "@typescript-eslint/utils": "8.35.0", 1644 1644 "debug": "^4.3.4", 1645 1645 "ts-api-utils": "^2.1.0" 1646 1646 }, ··· 1657 1657 } 1658 1658 }, 1659 1659 "node_modules/@typescript-eslint/types": { 1660 - "version": "8.34.1", 1661 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", 1662 - "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", 1660 + "version": "8.35.0", 1661 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", 1662 + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", 1663 1663 "dev": true, 1664 1664 "license": "MIT", 1665 1665 "engines": { ··· 1671 1671 } 1672 1672 }, 1673 1673 "node_modules/@typescript-eslint/typescript-estree": { 1674 - "version": "8.34.1", 1675 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", 1676 - "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", 1674 + "version": "8.35.0", 1675 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", 1676 + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", 1677 1677 "dev": true, 1678 1678 "license": "MIT", 1679 1679 "dependencies": { 1680 - "@typescript-eslint/project-service": "8.34.1", 1681 - "@typescript-eslint/tsconfig-utils": "8.34.1", 1682 - "@typescript-eslint/types": "8.34.1", 1683 - "@typescript-eslint/visitor-keys": "8.34.1", 1680 + "@typescript-eslint/project-service": "8.35.0", 1681 + "@typescript-eslint/tsconfig-utils": "8.35.0", 1682 + "@typescript-eslint/types": "8.35.0", 1683 + "@typescript-eslint/visitor-keys": "8.35.0", 1684 1684 "debug": "^4.3.4", 1685 1685 "fast-glob": "^3.3.2", 1686 1686 "is-glob": "^4.0.3", ··· 1739 1739 } 1740 1740 }, 1741 1741 "node_modules/@typescript-eslint/utils": { 1742 - "version": "8.34.1", 1743 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", 1744 - "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", 1742 + "version": "8.35.0", 1743 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", 1744 + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", 1745 1745 "dev": true, 1746 1746 "license": "MIT", 1747 1747 "dependencies": { 1748 1748 "@eslint-community/eslint-utils": "^4.7.0", 1749 - "@typescript-eslint/scope-manager": "8.34.1", 1750 - "@typescript-eslint/types": "8.34.1", 1751 - "@typescript-eslint/typescript-estree": "8.34.1" 1749 + "@typescript-eslint/scope-manager": "8.35.0", 1750 + "@typescript-eslint/types": "8.35.0", 1751 + "@typescript-eslint/typescript-estree": "8.35.0" 1752 1752 }, 1753 1753 "engines": { 1754 1754 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 1763 1763 } 1764 1764 }, 1765 1765 "node_modules/@typescript-eslint/visitor-keys": { 1766 - "version": "8.34.1", 1767 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", 1768 - "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", 1766 + "version": "8.35.0", 1767 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", 1768 + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", 1769 1769 "dev": true, 1770 1770 "license": "MIT", 1771 1771 "dependencies": { 1772 - "@typescript-eslint/types": "8.34.1", 1772 + "@typescript-eslint/types": "8.35.0", 1773 1773 "eslint-visitor-keys": "^4.2.1" 1774 1774 }, 1775 1775 "engines": { ··· 1781 1781 } 1782 1782 }, 1783 1783 "node_modules/@vitejs/plugin-react": { 1784 - "version": "4.5.2", 1785 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz", 1786 - "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==", 1784 + "version": "4.6.0", 1785 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", 1786 + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", 1787 1787 "dev": true, 1788 1788 "license": "MIT", 1789 1789 "dependencies": { 1790 1790 "@babel/core": "^7.27.4", 1791 1791 "@babel/plugin-transform-react-jsx-self": "^7.27.1", 1792 1792 "@babel/plugin-transform-react-jsx-source": "^7.27.1", 1793 - "@rolldown/pluginutils": "1.0.0-beta.11", 1793 + "@rolldown/pluginutils": "1.0.0-beta.19", 1794 1794 "@types/babel__core": "^7.20.5", 1795 1795 "react-refresh": "^0.17.0" 1796 1796 }, ··· 2399 2399 } 2400 2400 }, 2401 2401 "node_modules/electron-to-chromium": { 2402 - "version": "1.5.171", 2403 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", 2404 - "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", 2402 + "version": "1.5.172", 2403 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.172.tgz", 2404 + "integrity": "sha512-fnKW9dGgmBfsebbYognQSv0CGGLFH1a5iV9EDYTBwmAQn+whbzHbLFlC+3XbHc8xaNtpO0etm8LOcRXs1qMRkQ==", 2405 2405 "dev": true, 2406 2406 "license": "ISC" 2407 2407 }, ··· 4269 4269 } 4270 4270 }, 4271 4271 "node_modules/prettier": { 4272 - "version": "3.5.3", 4273 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 4274 - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", 4272 + "version": "3.6.0", 4273 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz", 4274 + "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==", 4275 4275 "dev": true, 4276 4276 "license": "MIT", 4277 4277 "bin": { ··· 5082 5082 } 5083 5083 }, 5084 5084 "node_modules/typescript-eslint": { 5085 - "version": "8.34.1", 5086 - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", 5087 - "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", 5085 + "version": "8.35.0", 5086 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", 5087 + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", 5088 5088 "dev": true, 5089 5089 "license": "MIT", 5090 5090 "dependencies": { 5091 - "@typescript-eslint/eslint-plugin": "8.34.1", 5092 - "@typescript-eslint/parser": "8.34.1", 5093 - "@typescript-eslint/utils": "8.34.1" 5091 + "@typescript-eslint/eslint-plugin": "8.35.0", 5092 + "@typescript-eslint/parser": "8.35.0", 5093 + "@typescript-eslint/utils": "8.35.0" 5094 5094 }, 5095 5095 "engines": { 5096 5096 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+14
manhunt-logic/Cargo.toml
··· 1 + [package] 2 + name = "manhunt-logic" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + anyhow = "1.0.98" 8 + chrono = { version = "0.4.41", features = ["serde", "now"] } 9 + rand = { version = "0.9.1", features = ["thread_rng"] } 10 + rand_chacha = "0.9.0" 11 + serde = { version = "1.0.219", features = ["derive"] } 12 + specta = { version = "=2.0.0-rc.22", features = ["uuid", "chrono", "derive"] } 13 + tokio = { version = "1.45.1", features = ["macros", "rt", "sync", "time"] } 14 + uuid = { version = "1.17.0", features = ["serde", "v4"] }
+27
manhunt-logic/src/lib.rs
··· 1 + mod game; 2 + mod game_events; 3 + mod game_state; 4 + mod lobby; 5 + mod location; 6 + mod powerups; 7 + mod profile; 8 + mod settings; 9 + #[cfg(test)] 10 + mod tests; 11 + mod transport; 12 + 13 + pub use game::{Game, StateUpdateSender}; 14 + pub use game_events::GameEvent; 15 + pub use game_state::{GameHistory, GameUiState}; 16 + pub use lobby::{Lobby, LobbyMessage, LobbyState, StartGameInfo}; 17 + pub use location::{Location, LocationService}; 18 + pub use profile::PlayerProfile; 19 + pub use settings::GameSettings; 20 + pub use transport::{MsgPair, Transport, TransportMessage}; 21 + 22 + pub mod prelude { 23 + use anyhow::Error as AnyhowError; 24 + use std::result::Result as StdResult; 25 + pub type Result<T = (), E = AnyhowError> = StdResult<T, E>; 26 + pub use anyhow::Context; 27 + }
+239
manhunt-logic/src/lobby.rs
··· 1 + use std::{collections::HashMap, sync::Arc}; 2 + 3 + use anyhow::anyhow; 4 + use serde::{Deserialize, Serialize}; 5 + use tokio::sync::Mutex; 6 + use uuid::Uuid; 7 + 8 + use crate::{ 9 + game::StateUpdateSender, 10 + prelude::*, 11 + profile::PlayerProfile, 12 + settings::GameSettings, 13 + transport::{Transport, TransportMessage}, 14 + }; 15 + 16 + #[derive(Debug, Clone, Serialize, Deserialize)] 17 + pub struct StartGameInfo { 18 + pub settings: GameSettings, 19 + pub initial_caught_state: HashMap<Uuid, bool>, 20 + } 21 + 22 + #[derive(Debug, Clone, Serialize, Deserialize)] 23 + pub enum LobbyMessage { 24 + /// Message sent on a new peer, to sync profiles 25 + PlayerSync(Uuid, PlayerProfile), 26 + /// Message sent on a new peer from the host, to sync game settings 27 + HostPush(GameSettings), 28 + /// Host signals starting the game 29 + StartGame(StartGameInfo), 30 + /// A player has switched teams 31 + PlayerSwitch(Uuid, bool), 32 + } 33 + 34 + #[derive(Clone, Serialize, Deserialize, specta::Type)] 35 + pub struct LobbyState { 36 + profiles: HashMap<Uuid, PlayerProfile>, 37 + join_code: String, 38 + /// True represents seeker, false hider 39 + teams: HashMap<Uuid, bool>, 40 + self_id: Uuid, 41 + is_host: bool, 42 + settings: GameSettings, 43 + } 44 + 45 + pub struct Lobby<T: Transport, U: StateUpdateSender> { 46 + is_host: bool, 47 + join_code: String, 48 + state: Mutex<LobbyState>, 49 + transport: Arc<T>, 50 + state_updates: U, 51 + } 52 + 53 + impl<T: Transport, U: StateUpdateSender> Lobby<T, U> { 54 + pub async fn new( 55 + join_code: &str, 56 + host: bool, 57 + profile: PlayerProfile, 58 + settings: GameSettings, 59 + state_updates: U, 60 + ) -> Result<Arc<Self>> { 61 + let transport = T::initialize(join_code, host) 62 + .await 63 + .context("Failed to connect to lobby")?; 64 + 65 + let self_id = transport.self_id(); 66 + 67 + let lobby = Arc::new(Self { 68 + transport, 69 + state_updates, 70 + is_host: host, 71 + join_code: join_code.to_string(), 72 + state: Mutex::new(LobbyState { 73 + teams: HashMap::from_iter([(self_id, false)]), 74 + join_code: join_code.to_string(), 75 + profiles: HashMap::from_iter([(self_id, profile)]), 76 + self_id, 77 + is_host: host, 78 + settings, 79 + }), 80 + }); 81 + 82 + Ok(lobby) 83 + } 84 + 85 + fn emit_state_update(&self) { 86 + self.state_updates.send_update(); 87 + } 88 + 89 + async fn send_transport_message(&self, id: Option<Uuid>, msg: LobbyMessage) { 90 + if let Some(id) = id { 91 + self.transport.send_message_single(id, msg.into()).await 92 + } else { 93 + self.transport.send_message(msg.into()).await 94 + } 95 + } 96 + 97 + async fn signaling_mark_started(&self) { 98 + self.transport.mark_room_started(&self.join_code).await 99 + } 100 + 101 + async fn handle_lobby(&self, msg: LobbyMessage) -> Option<StartGameInfo> { 102 + let mut state = self.state.lock().await; 103 + match msg { 104 + LobbyMessage::PlayerSync(peer, player_profile) => { 105 + state.profiles.insert(peer, player_profile); 106 + } 107 + LobbyMessage::HostPush(game_settings) => { 108 + state.settings = game_settings; 109 + } 110 + LobbyMessage::StartGame(start_game_info) => { 111 + return Some(start_game_info); 112 + } 113 + LobbyMessage::PlayerSwitch(peer, seeker) => { 114 + state.teams.insert(peer, seeker); 115 + } 116 + } 117 + None 118 + } 119 + 120 + async fn handle_message( 121 + &self, 122 + peer: Option<Uuid>, 123 + msg: TransportMessage, 124 + ) -> Option<Result<Option<StartGameInfo>>> { 125 + match msg { 126 + TransportMessage::Disconnected => Some(Ok(None)), 127 + TransportMessage::Error(why) => Some(Err(anyhow!("Transport error: {why}"))), 128 + TransportMessage::Game(game_event) => { 129 + eprintln!("Peer {peer:?} sent a GameEvent???: {game_event:?}"); 130 + None 131 + } 132 + TransportMessage::Lobby(lobby_message) => self 133 + .handle_lobby(*lobby_message) 134 + .await 135 + .map(|start_game| Ok(Some(start_game))), 136 + TransportMessage::PeerConnect(peer) => { 137 + let mut state = self.state.lock().await; 138 + state.teams.insert(peer, false); 139 + let id = state.self_id; 140 + let msg = LobbyMessage::PlayerSync(id, state.profiles[&id].clone()); 141 + drop(state); 142 + self.send_transport_message(Some(peer), msg).await; 143 + if self.is_host { 144 + let state = self.state.lock().await; 145 + let msg = LobbyMessage::HostPush(state.settings.clone()); 146 + drop(state); 147 + self.send_transport_message(Some(peer), msg).await; 148 + } 149 + None 150 + } 151 + TransportMessage::PeerDisconnect(peer) => { 152 + let mut state = self.state.lock().await; 153 + if peer != state.self_id { 154 + state.profiles.remove(&peer); 155 + state.teams.remove(&peer); 156 + } 157 + None 158 + } 159 + } 160 + } 161 + 162 + pub async fn main_loop(&self) -> Result<Option<StartGameInfo>> { 163 + let res = 'lobby: loop { 164 + self.emit_state_update(); 165 + 166 + let msgs = self.transport.receive_messages().await; 167 + 168 + for (peer, msg) in msgs { 169 + if let Some(res) = self.handle_message(peer, msg).await { 170 + break 'lobby res; 171 + } 172 + } 173 + }; 174 + 175 + if res.is_err() { 176 + self.transport.disconnect().await; 177 + } 178 + 179 + res 180 + } 181 + 182 + pub fn clone_transport(&self) -> Arc<T> { 183 + self.transport.clone() 184 + } 185 + 186 + pub async fn clone_state(&self) -> LobbyState { 187 + self.state.lock().await.clone() 188 + } 189 + 190 + pub async fn clone_profiles(&self) -> HashMap<Uuid, PlayerProfile> { 191 + let state = self.state.lock().await; 192 + state.profiles.clone() 193 + } 194 + 195 + /// Set self as seeker or hider 196 + pub async fn switch_teams(&self, seeker: bool) { 197 + let mut state = self.state.lock().await; 198 + let id = state.self_id; 199 + if let Some(state_seeker) = state.teams.get_mut(&id) { 200 + *state_seeker = seeker; 201 + } 202 + drop(state); 203 + let msg = LobbyMessage::PlayerSwitch(id, seeker); 204 + self.send_transport_message(None, msg).await; 205 + self.emit_state_update(); 206 + } 207 + 208 + /// (Host) Update game settings 209 + pub async fn update_settings(&self, new_settings: GameSettings) { 210 + if self.is_host { 211 + let mut state = self.state.lock().await; 212 + state.settings = new_settings.clone(); 213 + drop(state); 214 + let msg = LobbyMessage::HostPush(new_settings); 215 + self.send_transport_message(None, msg).await; 216 + self.emit_state_update(); 217 + } 218 + } 219 + 220 + /// (Host) Start the game 221 + pub async fn start_game(&self) { 222 + if self.is_host { 223 + let state = self.state.lock().await; 224 + let start_game_info = StartGameInfo { 225 + settings: state.settings.clone(), 226 + initial_caught_state: state.teams.clone(), 227 + }; 228 + drop(state); 229 + let msg = LobbyMessage::StartGame(start_game_info); 230 + self.signaling_mark_started().await; 231 + self.transport.send_self(msg.clone().into()).await; 232 + self.send_transport_message(None, msg).await; 233 + } 234 + } 235 + 236 + pub async fn quit_lobby(&self) { 237 + self.transport.disconnect().await; 238 + } 239 + }
+7
manhunt-logic/src/profile.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Clone, Default, Debug, Serialize, Deserialize, specta::Type)] 4 + pub struct PlayerProfile { 5 + display_name: String, 6 + pfp_base64: Option<String>, 7 + }
+119
manhunt-logic/src/tests.rs
··· 1 + use std::{collections::HashMap, sync::Arc}; 2 + 3 + use tokio::sync::{Mutex, mpsc}; 4 + use uuid::Uuid; 5 + 6 + use crate::{ 7 + MsgPair, StateUpdateSender, Transport, TransportMessage, 8 + location::{Location, LocationService}, 9 + prelude::*, 10 + }; 11 + 12 + type GameEventRx = mpsc::Receiver<MsgPair>; 13 + type GameEventTx = mpsc::Sender<MsgPair>; 14 + 15 + pub struct MockTransport { 16 + id: Uuid, 17 + rx: Mutex<GameEventRx>, 18 + txs: HashMap<Uuid, GameEventTx>, 19 + } 20 + 21 + impl MockTransport { 22 + pub fn create_mesh(players: u32) -> (Vec<Uuid>, Vec<Self>) { 23 + let uuids = (0..players) 24 + .map(|_| uuid::Uuid::new_v4()) 25 + .collect::<Vec<_>>(); 26 + let channels = (0..players) 27 + .map(|_| tokio::sync::mpsc::channel(10)) 28 + .collect::<Vec<_>>(); 29 + let txs = channels 30 + .iter() 31 + .enumerate() 32 + .map(|(i, (tx, _))| (uuids[i], tx.clone())) 33 + .collect::<HashMap<_, _>>(); 34 + 35 + let transports = channels 36 + .into_iter() 37 + .enumerate() 38 + .map(|(i, (_tx, rx))| Self::new(uuids[i], rx, txs.clone())) 39 + .collect::<Vec<_>>(); 40 + 41 + (uuids, transports) 42 + } 43 + 44 + fn new(id: Uuid, rx: GameEventRx, txs: HashMap<Uuid, GameEventTx>) -> Self { 45 + Self { 46 + id, 47 + rx: Mutex::new(rx), 48 + txs, 49 + } 50 + } 51 + } 52 + 53 + impl Transport for MockTransport { 54 + async fn initialize(_code: &str, _host: bool) -> Result<Arc<Self>> { 55 + let (_, rx) = mpsc::channel(5); 56 + Ok(Arc::new(Self { 57 + id: Uuid::default(), 58 + rx: Mutex::new(rx), 59 + txs: HashMap::default(), 60 + })) 61 + } 62 + 63 + async fn disconnect(&self) { 64 + self.send_message(TransportMessage::PeerDisconnect(self.id)) 65 + .await; 66 + } 67 + 68 + async fn receive_messages(&self) -> impl Iterator<Item = MsgPair> { 69 + let mut rx = self.rx.lock().await; 70 + let mut buf = Vec::with_capacity(20); 71 + rx.recv_many(&mut buf, 20).await; 72 + buf.into_iter() 73 + } 74 + 75 + async fn send_message(&self, msg: TransportMessage) { 76 + 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"); 80 + } 81 + } 82 + 83 + async fn send_message_single(&self, peer: Uuid, msg: TransportMessage) { 84 + if let Some(tx) = self.txs.get(&peer) { 85 + tx.send((Some(self.id), msg)) 86 + .await 87 + .expect("Failed to send msg"); 88 + } 89 + } 90 + 91 + 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"); 96 + } 97 + 98 + fn self_id(&self) -> Uuid { 99 + self.id 100 + } 101 + } 102 + 103 + pub struct MockLocation; 104 + 105 + impl LocationService for MockLocation { 106 + fn get_loc(&self) -> Option<Location> { 107 + Some(crate::location::Location { 108 + lat: 0.0, 109 + long: 0.0, 110 + heading: None, 111 + }) 112 + } 113 + } 114 + 115 + pub struct DummySender; 116 + 117 + impl StateUpdateSender for DummySender { 118 + fn send_update(&self) {} 119 + }
+69
manhunt-logic/src/transport.rs
··· 1 + use std::sync::Arc; 2 + 3 + use super::{game_events::GameEvent, lobby::LobbyMessage}; 4 + use serde::{Deserialize, Serialize}; 5 + use uuid::Uuid; 6 + 7 + #[derive(Debug, Serialize, Deserialize, Clone)] 8 + pub enum TransportMessage { 9 + /// Message related to the actual game 10 + /// Boxed for space reasons 11 + Game(Box<GameEvent>), 12 + /// Message related to the pre-game lobby 13 + Lobby(Box<LobbyMessage>), 14 + /// Internal message when peer connects 15 + PeerConnect(Uuid), 16 + /// Internal message when peer disconnects 17 + PeerDisconnect(Uuid), 18 + /// Event sent when the transport gets disconnected, used to help consumers know when to stop 19 + /// consuming messages. Note this should represent a success state, the disconnect was 20 + /// triggered by user action. 21 + Disconnected, 22 + /// Event when the transport encounters a critical error and needs to disconnect. 23 + Error(String), 24 + } 25 + 26 + impl From<GameEvent> for TransportMessage { 27 + fn from(v: GameEvent) -> Self { 28 + Self::Game(Box::new(v)) 29 + } 30 + } 31 + 32 + impl From<LobbyMessage> for TransportMessage { 33 + fn from(v: LobbyMessage) -> Self { 34 + Self::Lobby(Box::new(v)) 35 + } 36 + } 37 + 38 + pub type MsgPair = (Option<Uuid>, TransportMessage); 39 + 40 + pub trait Transport: Send + Sync { 41 + /// Start the transport loop, This is expected to spawn a new job that will loop until 42 + /// cancelled or an error occurs. 43 + fn initialize( 44 + code: &str, 45 + host: bool, 46 + ) -> impl std::future::Future<Output = Result<Arc<Self>, anyhow::Error>> + Send; 47 + /// Get the local user's ID 48 + fn self_id(&self) -> Uuid; 49 + /// Check if a room is open to join, non-host players will call this with a code 50 + fn room_joinable(&self, _code: &str) -> impl std::future::Future<Output = bool> + Send { 51 + async { true } 52 + } 53 + /// Request a room be marked unjoinable (due to a game starting), the host user will call this. 54 + fn mark_room_started(&self, _code: &str) -> impl Future<Output = ()> { 55 + async {} 56 + } 57 + /// Receive an event 58 + fn receive_messages(&self) -> impl Future<Output = impl Iterator<Item = MsgPair>>; 59 + /// Send a message to a specific peer 60 + fn send_message_single(&self, peer: Uuid, msg: TransportMessage) -> impl Future<Output = ()>; 61 + /// Send a message to all other peers 62 + fn send_message(&self, msg: TransportMessage) -> impl Future<Output = ()>; 63 + /// Send a message to the local user 64 + fn send_self(&self, msg: TransportMessage) -> impl Future<Output = ()>; 65 + /// Disconnect from the transport 66 + fn disconnect(&self) -> impl Future<Output = ()> { 67 + async {} 68 + } 69 + }
+20
manhunt-transport/Cargo.toml
··· 1 + [package] 2 + name = "manhunt-transport" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + anyhow = "1.0.98" 8 + futures = "0.3.31" 9 + log = "0.4.27" 10 + matchbox_protocol = "0.12.0" 11 + matchbox_socket = "0.12.0" 12 + rmp-serde = "1.3.0" 13 + serde = { version = "1.0.219", features = ["derive"] } 14 + tokio = { version = "1.45.1", features = ["macros", "sync", "time", "rt"] } 15 + tokio-util = "0.7.15" 16 + uuid = { version = "1.17.0", features = ["serde"] } 17 + manhunt-logic = { version = "0.1.0", path = "../manhunt-logic" } 18 + rand = { version = "0.9.1", features = ["thread_rng"] } 19 + reqwest = { version = "0.12.20", default-features = false, features = ["charset", "http2", "rustls-tls", "system-proxy"] } 20 + const-str = "0.6.2"
+6
manhunt-transport/src/lib.rs
··· 1 + mod matchbox; 2 + mod packets; 3 + mod server; 4 + 5 + pub use matchbox::MatchboxTransport; 6 + pub use server::{generate_join_code, room_exists};
+294
manhunt-transport/src/matchbox.rs
··· 1 + use std::{pin::Pin, sync::Arc, time::Duration}; 2 + 3 + use anyhow::{Context, anyhow}; 4 + use futures::FutureExt; 5 + use log::error; 6 + use matchbox_socket::{Error as SocketError, PeerId, PeerState, WebRtcSocket}; 7 + use tokio::{ 8 + sync::{Mutex, mpsc}, 9 + task::yield_now, 10 + }; 11 + use tokio_util::sync::CancellationToken; 12 + use uuid::Uuid; 13 + 14 + use manhunt_logic::{Transport, TransportMessage, prelude::*}; 15 + 16 + use crate::{packets::PacketHandler, server}; 17 + 18 + type QueuePair<T> = (mpsc::Sender<T>, Mutex<mpsc::Receiver<T>>); 19 + type MsgPair = (Option<Uuid>, TransportMessage); 20 + type Queue = QueuePair<MsgPair>; 21 + 22 + pub struct MatchboxTransport { 23 + my_id: Uuid, 24 + incoming: Queue, 25 + outgoing: Queue, 26 + cancel_token: CancellationToken, 27 + } 28 + 29 + type LoopFutRes = Result<(), SocketError>; 30 + 31 + fn map_socket_error(err: SocketError) -> anyhow::Error { 32 + match err { 33 + SocketError::ConnectionFailed(e) => anyhow!("Connection to server failed: {e:?}"), 34 + SocketError::Disconnected(e) => anyhow!("Connection to server lost: {e:?}"), 35 + } 36 + } 37 + 38 + impl MatchboxTransport { 39 + pub async fn new(join_code: &str, is_host: bool) -> Result<Arc<Self>> { 40 + let (itx, irx) = mpsc::channel(15); 41 + let (otx, orx) = mpsc::channel(15); 42 + 43 + let ws_url = server::room_url(join_code, is_host); 44 + 45 + let (mut socket, mut loop_fut) = WebRtcSocket::new_reliable(&ws_url); 46 + 47 + let res = loop { 48 + tokio::select! { 49 + id = Self::wait_for_id(&mut socket) => { 50 + if let Some(id) = id { 51 + break Ok(id); 52 + } 53 + }, 54 + res = &mut loop_fut => { 55 + break Err(match res { 56 + Ok(_) => anyhow!("Transport disconnected unexpectedly"), 57 + Err(err) => map_socket_error(err) 58 + }); 59 + } 60 + } 61 + } 62 + .context("While trying to join the lobby"); 63 + 64 + match res { 65 + Ok(my_id) => { 66 + let transport = Arc::new(Self { 67 + my_id, 68 + incoming: (itx, Mutex::new(irx)), 69 + outgoing: (otx, Mutex::new(orx)), 70 + cancel_token: CancellationToken::new(), 71 + }); 72 + 73 + tokio::spawn({ 74 + let transport = transport.clone(); 75 + async move { 76 + transport.main_loop(socket, loop_fut).await; 77 + } 78 + }); 79 + 80 + Ok(transport) 81 + } 82 + Err(why) => { 83 + drop(socket); 84 + loop_fut.await.context("While disconnecting")?; 85 + Err(why) 86 + } 87 + } 88 + } 89 + 90 + async fn wait_for_id(socket: &mut WebRtcSocket) -> Option<Uuid> { 91 + if let Some(id) = socket.id() { 92 + Some(id.0) 93 + } else { 94 + yield_now().await; 95 + None 96 + } 97 + } 98 + 99 + async fn push_incoming(&self, id: Option<Uuid>, msg: TransportMessage) { 100 + self.incoming 101 + .0 102 + .send((id, msg)) 103 + .await 104 + .expect("Failed to push to incoming queue"); 105 + } 106 + 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 + async fn main_loop( 121 + &self, 122 + mut socket: WebRtcSocket, 123 + loop_fut: Pin<Box<dyn Future<Output = LoopFutRes> + Send + 'static>>, 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 + 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 + let mut packet_handler = PacketHandler::default(); 146 + 147 + loop { 148 + self.handle_peers(&mut socket).await; 149 + 150 + self.handle_recv(&mut socket, &mut packet_handler).await; 151 + 152 + tokio::select! { 153 + biased; 154 + 155 + _ = self.cancel_token.cancelled() => { 156 + break; 157 + } 158 + 159 + _ = &mut loop_fut => { 160 + break; 161 + } 162 + 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; 166 + } 167 + 168 + _ = interval.tick() => { 169 + continue; 170 + } 171 + } 172 + } 173 + } 174 + 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 + } 183 + } 184 + 185 + async fn handle_send( 186 + &self, 187 + socket: &mut WebRtcSocket, 188 + all_peers: &[PeerId], 189 + messages: &mut Vec<MsgPair>, 190 + ) { 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 + } 198 + } 199 + }); 200 + 201 + let channel = socket.channel_mut(0); 202 + 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); 213 + } 214 + } 215 + } 216 + } 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 + } 243 + 244 + pub async fn recv_transport_messages(&self) -> Vec<MsgPair> { 245 + let mut incoming_rx = self.incoming.1.lock().await; 246 + let mut buffer = Vec::with_capacity(60); 247 + incoming_rx.recv_many(&mut buffer, 60).await; 248 + buffer 249 + } 250 + 251 + pub fn cancel(&self) { 252 + self.cancel_token.cancel(); 253 + } 254 + } 255 + 256 + impl Transport for MatchboxTransport { 257 + fn self_id(&self) -> Uuid { 258 + self.my_id 259 + } 260 + 261 + async fn receive_messages(&self) -> impl Iterator<Item = manhunt_logic::MsgPair> { 262 + self.recv_transport_messages().await.into_iter() 263 + } 264 + 265 + async fn send_message_single(&self, peer: Uuid, msg: TransportMessage) { 266 + self.send_transport_message(Some(peer), msg).await; 267 + } 268 + 269 + async fn send_message(&self, msg: TransportMessage) { 270 + self.send_transport_message(None, msg).await; 271 + } 272 + 273 + async fn send_self(&self, msg: TransportMessage) { 274 + self.push_incoming(Some(self.my_id), msg).await; 275 + } 276 + 277 + async fn room_joinable(&self, code: &str) -> bool { 278 + server::room_exists(code).await.unwrap_or(false) 279 + } 280 + 281 + async fn mark_room_started(&self, code: &str) { 282 + if let Err(why) = server::mark_room_started(code).await { 283 + error!("Failed to mark room {code} as started: {why:?}"); 284 + } 285 + } 286 + 287 + async fn disconnect(&self) { 288 + self.cancel(); 289 + } 290 + 291 + async fn initialize(code: &str, host: bool) -> Result<Arc<Self>> { 292 + Self::new(code, host).await 293 + } 294 + }
+224
manhunt-transport/src/packets.rs
··· 1 + use std::collections::HashMap; 2 + 3 + use anyhow::{anyhow, bail}; 4 + use manhunt_logic::{prelude::*, TransportMessage}; 5 + use serde::{Deserialize, Serialize}; 6 + use uuid::Uuid; 7 + 8 + type PacketEncoded = Vec<u8>; 9 + type PacketSet = Vec<Vec<u8>>; 10 + 11 + #[derive(Debug, Clone, Serialize, Deserialize)] 12 + pub struct Packet { 13 + remaining_packets: SeqHeader, 14 + data: Vec<u8>, 15 + } 16 + 17 + type SeqHeader = u64; 18 + const SEQ_HEADER_SIZE: usize = size_of::<SeqHeader>(); 19 + 20 + const MATCHBOX_MAX_SIZE: usize = 65535; 21 + const PACKET_SIZE: usize = MATCHBOX_MAX_SIZE - SEQ_HEADER_SIZE; 22 + const MAX_NUM_PACKETS: u64 = u64::MAX - 1; 23 + 24 + impl Packet { 25 + pub fn from_raw_bytes(mut bytes: PacketEncoded) -> Result<Self> { 26 + // First [SEQ_HEADER_SIZE] bytes are our sequence header, in little endian. 27 + if bytes.len() > SEQ_HEADER_SIZE { 28 + let rest = bytes.split_off(SEQ_HEADER_SIZE); 29 + let header = bytes; 30 + let header = header 31 + .try_into() 32 + .map_err(|_| anyhow!("Couldn't parse sequence header"))?; 33 + let remaining_packets = SeqHeader::from_le_bytes(header); 34 + // Remaining bytes are the data 35 + Ok(Self { 36 + remaining_packets, 37 + data: rest, 38 + }) 39 + } else { 40 + bail!("Incoming packet is not long enough"); 41 + } 42 + } 43 + 44 + pub fn into_bytes(self) -> PacketEncoded { 45 + let header_encoded = self.remaining_packets.to_le_bytes(); 46 + header_encoded 47 + .into_iter() 48 + .chain(self.data) 49 + .collect::<Vec<_>>() 50 + } 51 + 52 + fn packets_needed(len: u64) -> Result<SeqHeader> { 53 + if len >= MAX_NUM_PACKETS.saturating_mul(PACKET_SIZE as u64) { 54 + bail!("Message is too long, refusing to send"); 55 + } else if len == 0 { 56 + bail!("Message is empty"); 57 + } else { 58 + Ok(len.div_ceil(PACKET_SIZE as u64)) 59 + } 60 + } 61 + } 62 + 63 + #[derive(Debug, Clone, Default)] 64 + pub struct PacketHandler { 65 + partials: HashMap<Uuid, PacketSet>, 66 + } 67 + 68 + impl PacketHandler { 69 + fn message_to_bytes(msg: &TransportMessage) -> Result<Vec<u8>> { 70 + rmp_serde::to_vec(&msg).context("Failed to serialize message") 71 + } 72 + 73 + fn message_from_bytes(msg: &[u8]) -> Result<TransportMessage> { 74 + rmp_serde::from_slice(msg).context("Failed to deserialize message") 75 + } 76 + 77 + pub fn message_to_packets(msg: &TransportMessage) -> Result<PacketSet> { 78 + let mut bytes = Self::message_to_bytes(msg)?; 79 + let needed_packets = Packet::packets_needed(bytes.len() as u64)?; 80 + let mut packets = Vec::with_capacity(needed_packets as usize); 81 + for i in 1..=needed_packets { 82 + let remaining_packets = needed_packets - i; 83 + let mut data = bytes.split_off(bytes.len().min(PACKET_SIZE)); 84 + std::mem::swap(&mut data, &mut bytes); 85 + packets.push( 86 + Packet { 87 + remaining_packets, 88 + data, 89 + } 90 + .into_bytes(), 91 + ); 92 + } 93 + 94 + if !bytes.is_empty() { 95 + bail!("Bytes not emptied?"); 96 + } 97 + 98 + Ok(packets) 99 + } 100 + 101 + fn decode_packet_set(set: PacketSet) -> Result<TransportMessage> { 102 + let combined_bytes = set.into_iter().flatten().collect::<Vec<_>>(); 103 + Self::message_from_bytes(&combined_bytes) 104 + } 105 + 106 + pub fn consume_packet( 107 + &mut self, 108 + peer: Uuid, 109 + bytes: PacketEncoded, 110 + ) -> Result<Option<TransportMessage>> { 111 + match Packet::from_raw_bytes(bytes).context("Failed to decode packet") { 112 + Ok(Packet { 113 + remaining_packets, 114 + data, 115 + }) => { 116 + if remaining_packets == 0 { 117 + let res = if let Some(mut partial) = self.partials.remove(&peer) { 118 + partial.push(data); 119 + Self::decode_packet_set(partial) 120 + } else { 121 + Self::message_from_bytes(&data) 122 + }; 123 + 124 + Some(res).transpose() 125 + } else { 126 + let partial = self 127 + .partials 128 + .entry(peer) 129 + .or_insert_with(|| Vec::with_capacity(remaining_packets as usize + 1)); 130 + partial.push(data); 131 + Ok(None) 132 + } 133 + } 134 + Err(why) => { 135 + // Remove current partial message if we received an invalid packet as the entire 136 + // sequence will now be wrong. 137 + self.partials.remove(&peer); 138 + Err(why) 139 + } 140 + } 141 + } 142 + } 143 + 144 + #[cfg(test)] 145 + mod tests { 146 + use super::*; 147 + 148 + #[test] 149 + fn test_packets_needed() { 150 + assert_eq!(Packet::packets_needed(5).unwrap(), 1, "5 bytes, one packet"); 151 + assert_eq!( 152 + Packet::packets_needed((MATCHBOX_MAX_SIZE + 12) as u64).unwrap(), 153 + 2, 154 + "MAX + 12 bytes, two packets" 155 + ); 156 + assert!( 157 + Packet::packets_needed(0).is_err(), 158 + "Empty packets disallowed" 159 + ); 160 + assert!( 161 + Packet::packets_needed(u64::MAX).is_err(), 162 + "Too many packets disallowed" 163 + ); 164 + } 165 + 166 + #[test] 167 + fn test_basic_packet_handling() { 168 + let mut handler = PacketHandler::default(); 169 + 170 + let msg = TransportMessage::Disconnected; 171 + let data = 172 + PacketHandler::message_to_packets(&msg).expect("Failed to make message into bytes"); 173 + 174 + assert_eq!(data.len(), 1); 175 + 176 + let data = data.into_iter().next().unwrap(); 177 + 178 + println!("dat: {data:?}"); 179 + 180 + let decoded = handler 181 + .consume_packet(Uuid::default(), data) 182 + .expect("Failed to load message from bytes") 183 + .expect("Message not complete despite being less than PACKET_SIZE"); 184 + 185 + assert!( 186 + matches!(decoded, TransportMessage::Disconnected), 187 + "Transport message does not match input" 188 + ); 189 + } 190 + 191 + #[test] 192 + fn test_multipart() { 193 + // Adding random amount to make sure we account for remainders, etc. 194 + let really_big_string = "a".repeat(MATCHBOX_MAX_SIZE * 5 + 35); 195 + let really_big_message = TransportMessage::Error(really_big_string.clone()); 196 + 197 + let packets = 198 + PacketHandler::message_to_packets(&really_big_message).expect("Failed to encode"); 199 + 200 + assert!(packets.len() > 1, "Saving in one packet"); 201 + 202 + let mut handler = PacketHandler::default(); 203 + let mut res = None; 204 + 205 + for pack in packets { 206 + assert!( 207 + pack.len() <= MATCHBOX_MAX_SIZE, 208 + "Packets aren't small enough, {} > {}", 209 + pack.len(), 210 + MATCHBOX_MAX_SIZE 211 + ); 212 + 213 + res = handler 214 + .consume_packet(Uuid::default(), pack) 215 + .expect("Failed to decode"); 216 + } 217 + 218 + if let Some(TransportMessage::Error(s)) = res { 219 + assert_eq!(s, really_big_string, "internal strings aren't equal"); 220 + } else { 221 + panic!("Decoded is the wrong type or wasn't completed"); 222 + } 223 + } 224 + }