mirror of
https://github.com/ThisTine/Showdown.git
synced 2026-08-18 23:18:48 +07:00
first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
target/
|
||||
Generated
+805
@@ -0,0 +1,805 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"base64",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "showdown-backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "showdown-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time", "sync"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tower-http = { version = "0.6", features = ["fs"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
@@ -0,0 +1,46 @@
|
||||
# Showdown backend
|
||||
|
||||
Rust (axum + tokio). Implements `../PROTOCOL.md`: WebSocket rooms that live
|
||||
entirely in memory and a static file server for the built frontend — the whole
|
||||
app is one binary plus a `dist/` folder.
|
||||
|
||||
## Run it
|
||||
|
||||
```sh
|
||||
# build the frontend first so there is something to serve
|
||||
cd ../frontend && npm run build && cd ../backend
|
||||
|
||||
cargo run # http://localhost:8080
|
||||
```
|
||||
|
||||
- `PORT` — listen port (default `8080`)
|
||||
- `STATIC_DIR` — built frontend location (default `../frontend/dist`)
|
||||
|
||||
During frontend development you can instead run `npm run dev` with
|
||||
`VITE_WS=1` and Vite proxies `/ws` here.
|
||||
|
||||
## How it stays small
|
||||
|
||||
- **No database, by design.** A `Room` is a deck, a `revealed` flag, a round
|
||||
counter and a `Vec` of players — a few hundred bytes. All rooms sit in one
|
||||
`Mutex<HashMap>`. When the last socket of a room closes, the room is
|
||||
removed; restart the server and everything is gone, which is the point.
|
||||
- **One task per socket, nothing else.** Each connection is a single
|
||||
`tokio::select!` loop (snapshots out, messages in, a 30s keepalive ping).
|
||||
No per-room goroutine-style actors, no background jobs, no timers per room.
|
||||
- **Snapshots are serialized once per change** (borrowed data, no clones) and
|
||||
fanned out through a per-room `broadcast` channel with a small buffer.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
src/main.rs routes + static serving with SPA fallback (~40 lines)
|
||||
src/room.rs room state and the game rules (vote/reveal/reset/deck)
|
||||
src/ws.rs socket lifecycle: hello → select loop → disconnect cleanup
|
||||
```
|
||||
|
||||
Rules enforced here (mirroring PROTOCOL.md): votes are hidden until reveal
|
||||
and rejected after it; reveal needs at least one vote; reset/deck-change
|
||||
start a fresh round; a refresh re-claims the same seat without losing the
|
||||
vote; watchers (`watch` handshake — the TV view) receive everything and may
|
||||
send nothing.
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Showdown backend: WebSocket rooms held entirely in memory, plus static
|
||||
//! file serving for the built frontend. No database — a room lives exactly
|
||||
//! as long as someone has it open. See ../PROTOCOL.md for the wire format.
|
||||
|
||||
mod room;
|
||||
mod ws;
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
use crate::room::Rooms;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let rooms = Rooms::default();
|
||||
|
||||
let static_dir =
|
||||
PathBuf::from(env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/dist".into()));
|
||||
// Unknown paths fall back to index.html so deep links like /room/abc work.
|
||||
let spa = ServeDir::new(&static_dir).fallback(ServeFile::new(static_dir.join("index.html")));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/ws/room/{room_id}", get(ws::ws_handler))
|
||||
.fallback_service(spa)
|
||||
.with_state(rooms);
|
||||
|
||||
let port: u16 = env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080);
|
||||
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
|
||||
.await
|
||||
.expect("failed to bind port");
|
||||
|
||||
println!("🤠 Showdown saloon open at http://localhost:{port}");
|
||||
println!(" serving frontend from {}", static_dir.display());
|
||||
axum::serve(listener, app).await.expect("server error");
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Room state and rules. Everything lives in one `Mutex<HashMap>` — a room
|
||||
//! is a few hundred bytes and is dropped the moment its last socket closes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub type Rooms = Arc<Mutex<HashMap<String, Room>>>;
|
||||
|
||||
pub const DEFAULT_DECK: &[&str] = &["0", "1", "2", "3", "5", "8", "13", "21", "34", "?", "☕"];
|
||||
const MAX_DECK_CARDS: usize = 15;
|
||||
const MAX_CARD_CHARS: usize = 4;
|
||||
|
||||
/// Everything a client may send. The first frame must be `join` or `watch`;
|
||||
/// the rest only make sense from a seated player.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum ClientMessage {
|
||||
Join {
|
||||
#[serde(rename = "playerId")]
|
||||
player_id: String,
|
||||
name: String,
|
||||
emoji: String,
|
||||
},
|
||||
Watch,
|
||||
Vote { value: Option<String> },
|
||||
Reveal,
|
||||
Reset,
|
||||
Deck { cards: Vec<String> },
|
||||
}
|
||||
|
||||
pub struct Player {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub emoji: String,
|
||||
pub vote: Option<String>,
|
||||
/// Newest socket claiming this seat. A refresh re-joins with the same
|
||||
/// player id and takes over the seat (keeping the vote); the old socket's
|
||||
/// disconnect then sees a newer `conn` and leaves the seat alone.
|
||||
pub conn: u64,
|
||||
}
|
||||
|
||||
pub struct Room {
|
||||
pub deck: Vec<String>,
|
||||
pub revealed: bool,
|
||||
pub round: u64,
|
||||
/// Join order is seat order, and scrum teams are small — a Vec beats a map.
|
||||
pub players: Vec<Player>,
|
||||
/// Open sockets, players and watchers alike. Zero means delete the room.
|
||||
pub conns: usize,
|
||||
/// Every state change is serialized once and fanned out through here.
|
||||
pub tx: broadcast::Sender<String>,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
deck: DEFAULT_DECK.iter().map(|c| c.to_string()).collect(),
|
||||
revealed: false,
|
||||
round: 1,
|
||||
players: Vec::new(),
|
||||
conns: 0,
|
||||
tx: broadcast::channel(32).0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(&mut self, id: &str, name: &str, emoji: &str, conn: u64) {
|
||||
match self.players.iter_mut().find(|p| p.id == id) {
|
||||
Some(p) => {
|
||||
p.name = name.to_string();
|
||||
p.emoji = emoji.to_string();
|
||||
p.conn = conn;
|
||||
}
|
||||
None => self.players.push(Player {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
emoji: emoji.to_string(),
|
||||
vote: None,
|
||||
conn,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Free the seat unless a newer socket (a refresh) already re-claimed it.
|
||||
/// Returns whether the player list actually changed.
|
||||
pub fn leave(&mut self, id: &str, conn: u64) -> bool {
|
||||
match self.players.iter().position(|p| p.id == id && p.conn == conn) {
|
||||
Some(pos) => {
|
||||
self.players.remove(pos);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&mut self, player_id: &str, msg: ClientMessage) {
|
||||
match msg {
|
||||
ClientMessage::Vote { value } => {
|
||||
if self.revealed {
|
||||
return; // no changing your story after the showdown
|
||||
}
|
||||
if let Some(v) = &value {
|
||||
if !self.deck.contains(v) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(p) = self.players.iter_mut().find(|p| p.id == player_id) {
|
||||
p.vote = value;
|
||||
}
|
||||
}
|
||||
ClientMessage::Reveal => {
|
||||
if self.players.iter().any(|p| p.vote.is_some()) {
|
||||
self.revealed = true;
|
||||
}
|
||||
}
|
||||
ClientMessage::Reset => self.new_round(),
|
||||
ClientMessage::Deck { cards } => {
|
||||
if let Some(deck) = clean_deck(cards) {
|
||||
self.deck = deck;
|
||||
self.new_round();
|
||||
}
|
||||
}
|
||||
// join/watch are handshake messages, meaningless mid-session.
|
||||
ClientMessage::Join { .. } | ClientMessage::Watch => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_round(&mut self) {
|
||||
self.revealed = false;
|
||||
self.round += 1;
|
||||
for p in &mut self.players {
|
||||
p.vote = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// One JSON snapshot of the room. Votes stay hidden until the reveal —
|
||||
/// only `voted` leaks beforehand. Serializes from borrows; no clones.
|
||||
pub fn snapshot_json(&self, room_id: &str) -> String {
|
||||
#[derive(Serialize)]
|
||||
struct PlayerOut<'a> {
|
||||
id: &'a str,
|
||||
name: &'a str,
|
||||
emoji: &'a str,
|
||||
voted: bool,
|
||||
vote: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Snapshot<'a> {
|
||||
room_id: &'a str,
|
||||
deck: &'a [String],
|
||||
revealed: bool,
|
||||
round: u64,
|
||||
players: Vec<PlayerOut<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StateMsg<'a> {
|
||||
r#type: &'static str,
|
||||
state: Snapshot<'a>,
|
||||
}
|
||||
|
||||
let players = self
|
||||
.players
|
||||
.iter()
|
||||
.map(|p| PlayerOut {
|
||||
id: &p.id,
|
||||
name: &p.name,
|
||||
emoji: &p.emoji,
|
||||
voted: p.vote.is_some(),
|
||||
vote: if self.revealed { p.vote.as_deref() } else { None },
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string(&StateMsg {
|
||||
r#type: "state",
|
||||
state: Snapshot {
|
||||
room_id,
|
||||
deck: &self.deck,
|
||||
revealed: self.revealed,
|
||||
round: self.round,
|
||||
players,
|
||||
},
|
||||
})
|
||||
.expect("room state always serializes")
|
||||
}
|
||||
|
||||
pub fn broadcast(&self, room_id: &str) {
|
||||
// Send fails only when nobody is listening, which is fine.
|
||||
let _ = self.tx.send(self.snapshot_json(room_id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim, dedupe and cap a proposed deck. None if fewer than two cards survive.
|
||||
fn clean_deck(cards: Vec<String>) -> Option<Vec<String>> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for raw in cards {
|
||||
let card = raw.trim();
|
||||
if card.is_empty() || card.chars().count() > MAX_CARD_CHARS {
|
||||
continue;
|
||||
}
|
||||
if out.iter().any(|c| c == card) {
|
||||
continue;
|
||||
}
|
||||
out.push(card.to_string());
|
||||
if out.len() == MAX_DECK_CARDS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(out.len() >= 2).then_some(out)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//! One async task per socket. The task introduces itself (join/watch), then
|
||||
//! sits in a single select loop: room snapshots out, client messages in.
|
||||
//! Disconnect cleanup is what makes presence work — closing the tab is
|
||||
//! leaving the room.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
|
||||
use crate::room::{ClientMessage, Room, Rooms};
|
||||
|
||||
/// Distinguishes the sockets of a player who refreshed; see `Player::conn`.
|
||||
static NEXT_CONN: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
const HELLO_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
enum Seat {
|
||||
Player { id: String },
|
||||
Watcher,
|
||||
}
|
||||
|
||||
pub async fn ws_handler(
|
||||
Path(room_id): Path<String>,
|
||||
State(rooms): State<Rooms>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> Response {
|
||||
if !valid_room_id(&room_id) {
|
||||
return (StatusCode::BAD_REQUEST, "bad room id").into_response();
|
||||
}
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, room_id, rooms))
|
||||
}
|
||||
|
||||
fn valid_room_id(id: &str) -> bool {
|
||||
!id.is_empty()
|
||||
&& id.len() <= 64
|
||||
&& id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket, room_id: String, rooms: Rooms) {
|
||||
// 1. The first frame must introduce the socket: join (player) or watch (TV).
|
||||
let hello = match tokio::time::timeout(HELLO_TIMEOUT, socket.recv()).await {
|
||||
Ok(Some(Ok(Message::Text(text)))) => text,
|
||||
_ => return,
|
||||
};
|
||||
let seat = match serde_json::from_str::<ClientMessage>(&hello) {
|
||||
Ok(ClientMessage::Join { player_id, name, emoji }) => {
|
||||
if player_id.is_empty() || player_id.len() > 64 {
|
||||
return;
|
||||
}
|
||||
let conn_id = NEXT_CONN.fetch_add(1, Ordering::Relaxed);
|
||||
register_player(
|
||||
&rooms,
|
||||
&room_id,
|
||||
&player_id,
|
||||
&clean_text(&name, 16, "Outlaw"),
|
||||
&clean_text(&emoji, 8, "🤠"),
|
||||
conn_id,
|
||||
);
|
||||
(Seat::Player { id: player_id }, conn_id)
|
||||
}
|
||||
Ok(ClientMessage::Watch) => {
|
||||
let conn_id = NEXT_CONN.fetch_add(1, Ordering::Relaxed);
|
||||
register_watcher(&rooms, &room_id, conn_id);
|
||||
(Seat::Watcher, conn_id)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let (seat, conn_id) = seat;
|
||||
|
||||
// 2. Subscribe to snapshots and send this socket the current one.
|
||||
let (mut rx, snapshot) = {
|
||||
let mut map = rooms.lock().unwrap();
|
||||
let room = map.get_mut(&room_id).expect("registered above");
|
||||
(room.tx.subscribe(), room.snapshot_json(&room_id))
|
||||
};
|
||||
|
||||
// 3. The whole session is one loop; whenever it ends, fall through to cleanup.
|
||||
serve(&mut socket, &room_id, &rooms, &seat, &mut rx, snapshot).await;
|
||||
|
||||
// 4. Disconnect: free the seat, tell the others, drop the room if empty.
|
||||
cleanup(&rooms, &room_id, &seat, conn_id);
|
||||
}
|
||||
|
||||
fn register_player(rooms: &Rooms, room_id: &str, id: &str, name: &str, emoji: &str, conn: u64) {
|
||||
let mut map = rooms.lock().unwrap();
|
||||
let room = map.entry(room_id.to_string()).or_insert_with(Room::new);
|
||||
room.conns += 1;
|
||||
room.join(id, name, emoji, conn);
|
||||
room.broadcast(room_id);
|
||||
}
|
||||
|
||||
fn register_watcher(rooms: &Rooms, room_id: &str, _conn: u64) {
|
||||
let mut map = rooms.lock().unwrap();
|
||||
let room = map.entry(room_id.to_string()).or_insert_with(Room::new);
|
||||
room.conns += 1;
|
||||
}
|
||||
|
||||
async fn serve(
|
||||
socket: &mut WebSocket,
|
||||
room_id: &str,
|
||||
rooms: &Rooms,
|
||||
seat: &Seat,
|
||||
rx: &mut broadcast::Receiver<String>,
|
||||
first_snapshot: String,
|
||||
) {
|
||||
if socket.send(Message::Text(first_snapshot.into())).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let mut ping = tokio::time::interval(PING_INTERVAL);
|
||||
ping.tick().await; // the first tick is immediate; skip it
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// A state change somewhere in the room — relay the snapshot.
|
||||
update = rx.recv() => {
|
||||
let json = match update {
|
||||
Ok(json) => json,
|
||||
// This socket fell behind; skip straight to the freshest state.
|
||||
Err(RecvError::Lagged(_)) => match current_snapshot(rooms, room_id) {
|
||||
Some(json) => json,
|
||||
None => return,
|
||||
},
|
||||
Err(RecvError::Closed) => return,
|
||||
};
|
||||
if socket.send(Message::Text(json.into())).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// A frame from this client.
|
||||
frame = socket.recv() => {
|
||||
let text = match frame {
|
||||
Some(Ok(Message::Text(text))) => text,
|
||||
Some(Ok(_)) => continue, // pongs etc. — ignore
|
||||
_ => return, // closed or errored
|
||||
};
|
||||
let Seat::Player { id } = seat else {
|
||||
continue; // watchers are read-only
|
||||
};
|
||||
let Ok(msg) = serde_json::from_str::<ClientMessage>(&text) else {
|
||||
continue; // malformed frame — ignore
|
||||
};
|
||||
let mut map = rooms.lock().unwrap();
|
||||
if let Some(room) = map.get_mut(room_id) {
|
||||
room.apply(id, msg);
|
||||
room.broadcast(room_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Keepalive so idle rooms survive proxies and NAT timeouts.
|
||||
_ = ping.tick() => {
|
||||
if socket.send(Message::Ping(Default::default())).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_snapshot(rooms: &Rooms, room_id: &str) -> Option<String> {
|
||||
rooms.lock().unwrap().get(room_id).map(|r| r.snapshot_json(room_id))
|
||||
}
|
||||
|
||||
fn cleanup(rooms: &Rooms, room_id: &str, seat: &Seat, conn_id: u64) {
|
||||
let mut map = rooms.lock().unwrap();
|
||||
let Some(room) = map.get_mut(room_id) else {
|
||||
return;
|
||||
};
|
||||
room.conns -= 1;
|
||||
if room.conns == 0 {
|
||||
// Last one out: the whole room vanishes. No database, no leftovers.
|
||||
map.remove(room_id);
|
||||
return;
|
||||
}
|
||||
if let Seat::Player { id } = seat {
|
||||
if room.leave(id, conn_id) {
|
||||
room.broadcast(room_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_text(raw: &str, max_chars: usize, fallback: &str) -> String {
|
||||
let trimmed: String = raw.trim().chars().take(max_chars).collect();
|
||||
if trimmed.is_empty() {
|
||||
fallback.to_string()
|
||||
} else {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user