summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--Cargo.lock1501
-rw-r--r--Cargo.toml17
-rw-r--r--assets/VT323-Regular.ttfbin0 -> 149688 bytes
-rw-r--r--assets/client.js267
-rw-r--r--assets/index.html132
-rw-r--r--assets/room.html30
-rw-r--r--assets/style.css86
-rw-r--r--conf.json.sample4
-rw-r--r--src/conn.rs191
-rw-r--r--src/livepos.rs78
-rw-r--r--src/main.rs328
-rw-r--r--src/minesweeper.rs286
-rw-r--r--src/types.rs135
14 files changed, 3059 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0dc1483
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/target
+conf.json
+cert.pem
+cert.rsa
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..40fa6fb
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1501 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "ammonia"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74"
+dependencies = [
+ "html5ever",
+ "maplit",
+ "once_cell",
+ "tendril",
+ "url",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer 0.10.2",
+ "crypto-common",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
+
+[[package]]
+name = "futures-task"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+
+[[package]]
+name = "futures-util"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util 0.7.3",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
+
+[[package]]
+name = "headers"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
+dependencies = [
+ "base64",
+ "bitflags",
+ "bytes",
+ "headers-core",
+ "http",
+ "httpdate",
+ "mime",
+ "sha-1 0.10.0",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+dependencies = [
+ "http",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "js-sys"
+version = "0.3.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "markup5ever"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustls"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
+dependencies = [
+ "base64",
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "sct"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[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 = "sha-1"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.3",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "slab"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
+
+[[package]]
+name = "smallvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "string_cache"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
+dependencies = [
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
+dependencies = [
+ "rustls",
+ "tokio",
+ "webpki",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8"
+dependencies = [
+ "futures-util",
+ "log",
+ "pin-project",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "tungstenite"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
+dependencies = [
+ "base64",
+ "byteorder",
+ "bytes",
+ "http",
+ "httparse",
+ "log",
+ "rand",
+ "sha-1 0.9.8",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "warp"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "headers",
+ "http",
+ "hyper",
+ "log",
+ "mime",
+ "mime_guess",
+ "multipart",
+ "percent-encoding",
+ "pin-project",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tokio-tungstenite",
+ "tokio-util 0.6.10",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
+
+[[package]]
+name = "web-sys"
+version = "0.3.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "websweeper"
+version = "1.1.0"
+dependencies = [
+ "ammonia",
+ "flate2",
+ "futures",
+ "rand",
+ "serde",
+ "serde_json",
+ "tokio",
+ "warp",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3c68a1b
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "websweeper"
+version = "1.1.0"
+authors = ["stale <stale@masba.net>"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1.0"
+flate2 = "1.0"
+warp = { version = "0.3", features = ["tls", "websocket"] }
+rand = "0.8"
+futures = "0.3"
+ammonia = "3"
diff --git a/assets/VT323-Regular.ttf b/assets/VT323-Regular.ttf
new file mode 100644
index 0000000..6aec599
--- /dev/null
+++ b/assets/VT323-Regular.ttf
Binary files differ
diff --git a/assets/client.js b/assets/client.js
new file mode 100644
index 0000000..174628f
--- /dev/null
+++ b/assets/client.js
@@ -0,0 +1,267 @@
+window.player = { uid: NaN };
+window.info_elem = document.getElementById("miscinfo");
+window.identform = document.getElementById("identform");
+window.statusline = document.getElementsByClassName("statusline")[0];
+window.bcont_elem = document.getElementById("board-container");
+window.board_elem = document.getElementById("board");
+window.cursor_frame = document.getElementById("cursor-frame");
+window.queued_pos = undefined;
+
+window.room = {
+ name: undefined,
+ bconf: { w: NaN, h: NaN, tile_w: NaN, tile_h: NaN, mine_ratio: undefined },
+ board: {},
+ cbounds: {},
+ socket: undefined,
+ last_packet: undefined,
+ identity: JSON.parse(localStorage.getItem("identity")),
+ cursors: new Map(),
+};
+
+
+if (room.identity == null) {
+ statusline.style.display = "none";
+ identform.style.display = "initial";
+} else {
+ join();
+}
+
+function join() {
+ if (room.identity == null) {
+ room.identity = {};
+ room.identity.name = document.getElementById("name-in").value;
+ room.identity.clr = document.getElementById("clr-in").value;
+ localStorage.setItem("identity", JSON.stringify(room.identity));
+ }
+ identform.style.display = "none";
+ room.socket = connect();
+ statusline.style.display = "flex";
+}
+function clear_ident() {
+ localStorage.removeItem("identity");
+ document.location.reload();
+}
+
+function connect() {
+ let wsproto = (window.location.protocol == "https:")? "wss:": "ws:";
+ let s = new WebSocket(`${wsproto}//${location.hostname}:${location.port}${location.pathname}/ws`);
+ s.onopen = function() {
+ s.send(`register ${room.identity.name} ${room.identity.clr}`);
+ }
+ s.onmessage = function(e) {
+ room.last_packet = e;
+ let d = e.data;
+ if (typeof d == "object") {
+ d.arrayBuffer().then(acceptBoard);
+ info_elem.onclick = undefined;
+ info_elem.innerHTML = `${room.name} (${room.bconf.w}x${room.bconf.h}) >> Running, ${room.bconf.mine_ratio} tiles are mines`;
+ } else if (typeof e.data == "string") {
+ let fields = d.split(" ");
+ switch (fields[0]) {
+ case "pos": {
+ let posdata = JSON.parse(fields[1]);
+ posdata.forEach(pdat => {
+ let oid = Number(pdat[0]);
+ let x = pdat[1][0];
+ let y = pdat[1][1];
+ let curs = room.cursors.get(oid);
+ if (curs != undefined) {
+ movCursor(curs, x, y);
+ } else {
+ console.log("livepos sys incoherent");
+ }
+ });
+ } break;
+ case "players": {
+ let pdata = JSON.parse(fields[1]);
+ console.log(pdata);
+ pdata.forEach(p => {
+ let oid = Number(p[0]);
+ let name = p[1];
+ let clr = p[2];
+ console.log(oid, name, clr);
+ if (!room.cursors.has(oid)) {
+ createCursor(oid, name, clr);
+ }
+ });
+ } break;
+ case "regack": {
+ room.name = fields[1];
+ name = fields[2];
+ player.uid = Number(fields[3]);
+ let dims = fields[4].split("x");
+ room.bconf.w = Number(dims[0]);
+ room.bconf.h = Number(dims[1]);
+ room.bconf.mine_ratio = fields[5];
+ createCursor(player.uid, name, room.identity.clr);
+ } break;
+ case "win": {
+ info_elem.innerHTML = "You win! Click here to play again.";
+ info_elem.onclick = e => { s.send("reset") };
+ } break;
+ case "lose": {
+ let badone = fields[1];
+ info_elem.innerHTML = `You lost, ${badone} was blown up. Click here to retry.`;
+ info_elem.onclick = e => { s.send("reset") };
+ } break;
+ case "logoff": {
+ let oid = Number(fields[1]);
+ room.cursors.get(oid).elem.remove();
+ room.cursors.get(oid).selwin.remove();
+ room.cursors.delete(oid);
+ } break;
+ }
+ }
+ }
+ s.onerror = function(e) { info_elem.innerHTML += `<br>Connection error: ${e}`; }
+ s.onclose = function(e) { info_elem.innerHTML = "Connection closed"; }
+ return s;
+}
+
+function acceptBoard(data) {
+ let dataarr = new Uint8Array(data);
+ let vals = fflate.inflateSync(dataarr);
+ room.board = vals.reduce((s,c) => {
+ let v = String.fromCodePoint(c);
+ if (v == ' ') {
+ s = s + "&nbsp";
+ } else {
+ s = s + v;
+ }
+ return s;
+ }, "");
+ let last = room.board[0];
+ let last_idx = 0;
+ let split_board = [];
+ for (let i = 1; i < room.board.length+1; i++) {
+ let cur = room.board[i];
+ let gamechars = /^[CFO# 1-8]+$/;
+ if ((cur != last && gamechars.test(cur)) || cur == undefined) {
+ let txt = room.board.substr(last_idx, i-last_idx);
+ switch(txt[0]) {
+ case 'O':
+ txt = `<span style="color:red;">${txt}</span>`;
+ break;
+ case 'C':
+ txt = `<span style="color:green;">${txt}</span>`;
+ break;
+ case 'F':
+ txt = `<span style="color:yellow;">${txt}</span>`;
+ break;
+
+ case '1': txt = `<span style="color:#0100FB;">${txt}</span>`; break;
+ case '2': txt = `<span style="color:#027F01;">${txt}</span>`; break;
+ case '3': txt = `<span style="color:#FD0100;">${txt}</span>`; break;
+ case '4': txt = `<span style="color:#01017B;">${txt}</span>`; break;
+ case '5': txt = `<span style="color:#7D0302;">${txt}</span>`; break;
+ case '6': txt = `<span style="color:#00807F;">${txt}</span>`; break;
+
+ default: txt = `<span style="color:white;">${txt}</span>`; break;
+ }
+ split_board.push(txt);
+ last_idx = i;
+ }
+ last = room.board[i];
+ }
+ board_elem.innerHTML = split_board.join("");
+ room.cbounds = getBoardBounds();
+}
+
+function createCursor(id, name, clr) {
+ // shit doesn't line up
+ let cursor = document.createElement("div");
+ cursor.style.position = "absolute";
+ let nametag = document.createElement("p");
+ nametag.innerHTML = name;
+ nametag.classList.add('cursor-name');
+ let selection_window = document.createElement("div");
+ selection_window.style.backgroundColor = clr + "a0";
+ selection_window.style.position = "absolute";
+ selection_window.classList.add('cursor');
+ cursor.appendChild(nametag);
+ cursor.classList.add('cursor');
+ cursor.style.color = clr;
+ document.getElementById('cursor-frame').append(cursor);
+ document.getElementById('cursor-frame').append(selection_window);
+ let c = { name: name, elem: cursor, selwin: selection_window };
+ if (id == window.player.uid) {
+ document.addEventListener('mousemove', e => {
+ let bcoords = pageToBoardPx(e.pageX, e.pageY);
+ movCursor(c, bcoords[0], bcoords[1]);
+ window.queued_pos = bcoords;
+ },
+ false);
+ }
+ room.cursors.set(id, {name: name, elem: cursor, selwin: selection_window});
+ return cursor;
+}
+
+function pageToBoardPx(x,y) {
+ return [Math.floor(x - room.cbounds.ox), Math.floor(y - room.cbounds.oy)];
+}
+
+function movCursor(c, bx, by) {
+ c.elem.style.left = (room.cbounds.ox + bx) + 'px';
+ c.elem.style.top = (room.cbounds.oy + by) + 'px';
+ movSelWin(c.selwin, bx, by);
+}
+function movSelWin(win, bx, by) {
+ let tpos = tilepos(bx,by);
+ console.log(tpos);
+ if (tpos.x > (room.bconf.w - 1) || tpos.x < 0 || tpos.y > (room.bconf.h - 1) || tpos.y < 0) {
+ win.style.display = "none";
+ } else {
+ win.style.display = "";
+ }
+ win.style.left = (tpos.x * room.bconf.tile_w) + 'px';
+ win.style.top = (tpos.y * room.bconf.tile_h) + 'px';
+ win.style.width = room.bconf.tile_w + 'px';
+ win.style.height = room.bconf.tile_h + 'px';
+}
+function getBoardBounds() {
+ let a = bcont_elem.getBoundingClientRect();
+ let b = board_elem.getBoundingClientRect();
+ room.bconf.tile_w = b.width / room.bconf.w;
+ room.bconf.tile_h = 48;
+ return {
+ ox: b.x + window.scrollX,
+ oy: a.y + window.scrollY,
+ w: b.width,
+ h: a.height
+ };
+}
+window.onresize = () => {
+ room.cbounds = getBoardBounds();
+}
+
+bcont_elem.onclick = function(e) {
+ let bcoords = pageToBoardPx(e.pageX, e.pageY);
+ let tpos = tilepos(bcoords[0], bcoords[1]);
+ let cmd = `reveal ${tpos.x} ${tpos.y}`;
+ room.socket.send(cmd);
+}
+bcont_elem.oncontextmenu = function(e) {
+ let bcoords = pageToBoardPx(e.pageX, e.pageY);
+ let tpos = tilepos(bcoords[0], bcoords[1]);
+ let cmd = `flag ${tpos.x} ${tpos.y}`;
+ room.socket.send(cmd);
+ return false;
+}
+// these are board-px coords
+function tilepos(bx,by) {
+ let b = room.cbounds; // we can assume it is already computed by earlier aux calls
+ let tilex = Math.floor(room.bconf.w * bx/b.w);
+ let tiley = Math.floor(room.bconf.h * by/b.h);
+ return { x: tilex, y: tiley };
+}
+
+(function sendPos() {
+ let qp = window.queued_pos;
+ if (qp) {
+ room.socket.send(`pos ${qp[0]} ${qp[1]}`);
+ window.queued_pos = undefined;
+ }
+ setTimeout(function() {
+ sendPos();
+ }, 16);
+})();
diff --git a/assets/index.html b/assets/index.html
new file mode 100644
index 0000000..cce9248
--- /dev/null
+++ b/assets/index.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>websweeper</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <link rel="stylesheet" type="text/css" href="./s.css">
+ </head>
+ <body>
+ <div class="cent">
+ <div id="rlist"></div>
+ <span id="rspace"></span>
+ </div>
+ <form method="post" action="r" class="cent">
+ <fieldset>
+ <legend>-={ Create a new room }=-</legend>
+ <label>room name&nbsp;<input name="rname" type="text" autofocus></label><br>
+ <label>
+ board dimensions
+ <input name="rwidth" type="number" value="30" required>
+ x
+ <input name="rheight" type="number" value="20" required><br>
+ where <input name="rration" type="number" value="1" required>
+ in every <input name="rratiod" type="number" value="8" required>
+ tiles are mines
+ </label><br>
+ <label>public, ie. shown in the lobby <input name="raccess" type="checkbox" checked></label><br>
+ <label>safe first move (if possible) <input name="ralwayssafe1move" type="checkbox" checked></label><br>
+ <label>player limit <input name="rlimit" type="number" value="32"></label><br>
+ <button id="createbtn">create</button>
+ </fieldset>
+ </form>
+ <div class="statusline cent">
+ <p id="ident-name"></p>
+ <a id="ident-clr" href="javascript:clear_ident();">clear identity</a>
+ </div>
+ <script>
+ let rlist = {
+ elem: document.getElementById('rlist'),
+ map: new Map(),
+ };
+ let rspace = {
+ elem: document.getElementById('rspace'),
+ num: NaN,
+ txt: undefined,
+ };
+
+ function fetch_info(callback) {
+ fetch('rlist').then(r => r.json()).then(info => {
+ let rooms = info[0];
+ let pcounts = info[1];
+ Object.keys(rooms).forEach(id => {
+ let room = rlist.map.get(id);
+ if (!room) { room = { init: false }; }
+ let rinfo = JSON.parse(rooms[id]);
+ room.name = rinfo.name;
+ room.pcount = Number(pcounts[id][0]);
+ room.pcapacity = Number(pcounts[id][1]);
+ room.board_conf = rinfo.board_conf;
+ rlist.map.set(id, room);
+ });
+ callback();
+ });
+ fetch("rspace").then(resp => resp.text()).then(roomspace => {
+ rspace.num = Number(roomspace);
+ callback();
+ })
+ }
+
+ function render_info() {
+ rlist.map.forEach((room, id) => {
+ let full = room.pcount == room.pcapacity;
+ if (!room.init) {
+ let entry = (full)? document.createElement('span') : document.createElement('a');
+ room.h1 = document.createElement("h1");
+ room.h1_txt = document.createTextNode("");
+ room.h1.appendChild(room.h1_txt);
+ room.h4 = document.createElement("h4");
+ room.h4.appendChild(document.createTextNode(
+ `${room.board_conf.w} by ${room.board_conf.h} with
+ ${room.board_conf.mine_ratio[0]} in every ${room.board_conf.mine_ratio[1]} tiles mined`
+ ));
+ entry.append(room.h1);
+ entry.append(room.h4);
+ entry.href = 'room/' + id;
+ rlist.elem.append(entry);
+ rlist.elem.append(document.createElement('br'));
+ room.init = true;
+ }
+ let ptxt = `${room.pcount}/${room.pcapacity} players` + ((full)? " (full)" : "");
+ room.h1_txt.textContent = `> ${room.name} — ${ptxt}`;
+
+ });
+ if (!rspace.txt) {
+ rspace.txt = document.createTextNode("");
+ rspace.elem.appendChild(rspace.txt);
+ }
+ if (rspace.num == 0) {
+ rspace.txt.textContent = "all room slots filled, when a room empties it can be replaced by a new one";
+ document.getElementById("createbtn").disabled = "disabled";
+ } else {
+ document.getElementById("createbtn").disabled = "";
+ if (rspace.num == 1) {
+ rspace.txt.textContent = "there is 1 available room slot";
+ } else if (rspace.num > 1) {
+ rspace.txt.textContent = `there are ${rspace.num} available room slots`;
+ }
+ }
+ }
+
+ (function refresh_info() {
+ fetch_info(render_info);
+ setTimeout(function() {
+ refresh_info();
+ }, 2000);
+ })();
+
+ function clear_ident() {
+ localStorage.removeItem("identity");
+ document.location.reload();
+ }
+ let ident = JSON.parse(localStorage.getItem("identity"));
+ let ident_elem = document.getElementById("ident-name");
+ if (ident == null) {
+ ident_elem.innerHTML = "no identity yet";
+ document.getElementById("ident-clr").style.display = "none";
+ } else {
+ ident_elem.innerHTML = `you are <span style="color: ${ident.clr}">${ident.name}</span>`;
+ }
+ </script>
+ </body>
+</html>
diff --git a/assets/room.html b/assets/room.html
new file mode 100644
index 0000000..4dadc6b
--- /dev/null
+++ b/assets/room.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>websweeper</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <link rel="stylesheet" type="text/css" href="../s.css">
+ </head>
+ <body>
+ <div>
+ <div id="board-container">
+ <span id="board"></span>
+ <div id="cursor-frame"></div>
+ </div>
+ <form id="identform" style="display: none" action="javascript:;" onsubmit="join()">
+ <input id="name-in" type="text" value="anon">
+ <input id="clr-in" type="color" value="#33c033"></input>
+ <button>Join</button>
+ </form>
+ <div class="statusline">
+ <p id="miscinfo"></p>
+ <a href="javascript:navigator.clipboard.writeText(window.location.href);alert('copied link to clipboard');">🔗share</p>
+ <a href="javascript:clear_ident();">new identity</p>
+ <a href="..">back to lobby</a>
+ </div>
+ </div>
+ </body>
+ <script src="https://unpkg.com/fflate"></script>
+ <script src="../c.js"></script>
+</html>
diff --git a/assets/style.css b/assets/style.css
new file mode 100644
index 0000000..49e832e
--- /dev/null
+++ b/assets/style.css
@@ -0,0 +1,86 @@
+@font-face {
+ font-family: vt323;
+ src: url("./f.ttf");
+}
+#board-container {
+ font-size: 80px;
+ line-height: 48px;
+ margin: 2vw;
+ position: relative;
+ top: 0px;
+ left 0px;
+}
+#cursor-frame {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+}
+
+body {
+ font-family: vt323, monospace;
+ font-size: 20pt;
+ background-color: black;
+ color: white;
+ margin: 0;
+}
+.unsel {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.cursor {
+ font-size: 20pt;
+ padding: 0;
+ pointer-events: none;
+ z-index: 1;
+}
+.cursor * {
+ margin: 0 0;
+}
+.cursor-name {
+ background-color: #000000c0;
+ padding: 0 0.1em;
+ line-height: initial;
+}
+
+a {
+ text-decoration: none;
+ color: #8b8be8;
+}
+
+#miscinfo {
+ flex-grow: 1;
+}
+
+.statusline {
+ display: flex;
+ position: sticky;
+ background-color: black;
+ width: 96vw;
+ padding: 0 2vw;
+ bottom: 0px;
+ left: 0px;
+}
+.statusline * {
+ margin: 0.5em 2em 0 0;
+}
+
+/* i was today years old when i learned that css selector declaration order matters */
+/* cent should come after statusline, so it overwrites statusline's props */
+.cent {
+ width: 80vw;
+ margin: 0 auto;
+}
+
+span, h1, h4 {
+ margin: 0 auto;
+}
+
+h4 { margin-left: 1em; }
+
+a :visited {
+ color: inherit;
+}
diff --git a/conf.json.sample b/conf.json.sample
new file mode 100644
index 0000000..7ca5bec
--- /dev/null
+++ b/conf.json.sample
@@ -0,0 +1,4 @@
+{
+ "area_limit": 22500,
+ "room_limit": 16
+}
diff --git a/src/conn.rs b/src/conn.rs
new file mode 100644
index 0000000..addf3c5
--- /dev/null
+++ b/src/conn.rs
@@ -0,0 +1,191 @@
+use crate::types::*;
+use std::{
+ sync::Arc,
+ net::SocketAddr,
+};
+use tokio::sync::RwLock;
+use tokio::sync::mpsc as tokio_mpsc;
+use futures::{SinkExt, StreamExt, TryStreamExt, stream::SplitStream};
+use warp::ws::{ WebSocket, Message };
+use crate::livepos;
+
+const MAX_IN: usize = 2048;
+
+pub async fn lobby(socket: WebSocket, addr: SocketAddr, rinfo: (RoomId,Arc<RwLock<Room>>)) {
+ let (room_id, room) = rinfo;
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
+
+ // server <-> client comms
+ let (mut outgoing, incoming) = socket.split();
+
+ println!("{room_id} I: Incoming TCP connection from: {}", addr);
+
+ let full = {
+ let rl = room.read().await;
+ let pcap = rl.conf.player_cap;
+ let pl = rl.players.read().await;
+ pl.len() >= pcap
+ };
+ if full { return }
+ let drive_game = handle_room((incoming,tx), addr, (room_id.clone(),room.clone()));
+ let send_to_client = {
+ let room_id = room_id.clone();
+ async move {
+ while let Some(m) = rx.recv().await {
+ if let Err(e) = outgoing.send(m).await {
+ println!("{room_id} E: something went bad lol: {e}");
+ }
+ }
+ }
+ };
+
+ tokio::select! {
+ _ = drive_game => (),
+ _ = send_to_client => { println!("{room_id} E: anomalous close for {addr}"); }
+ };
+
+ let room_lock = room.read().await;
+ let mut players = room_lock.players.write().await;
+ if let Some(disconn_p) = players.remove(&addr) {
+ if let Err(e) = room_lock.pos_stream.send(livepos::Req { id: disconn_p.uid, data: livepos::ReqData::Quit }) {
+ println!("{room_id} E: couldn't send removal request for {disconn_p} from the live position system: {e}");
+ }
+ for p in players.values() {
+ if let Err(e) = p.conn.tx.send(Message::text(format!("logoff {}", disconn_p.uid))) {
+ println!("{room_id} E: couldn't deliver logoff info to {}: {}", p, e);
+ }
+ }
+ println!("{room_id} I: {disconn_p} disconnected");
+ } else {
+ println!("{room_id} I: {addr} disconnected");
+ }
+}
+
+type RoomStreams = (SplitStream<WebSocket>,tokio_mpsc::UnboundedSender<Message>);
+
+pub async fn handle_room(streams: RoomStreams, addr: SocketAddr, rinfo: (RoomId, Arc<RwLock<Room>>)) {
+ let (mut incoming, tx) = streams;
+ let (room_id, room) = rinfo;
+ let (players, cmd_tx, pos_tx, room_conf) = {
+ let room = room.read().await;
+ (room.players.clone(), room.cmd_stream.clone(), room.pos_stream.clone(), room.conf.clone())
+ };
+ while let Ok(cmd) = incoming.try_next().await {
+ if let Some(cmd) = cmd {
+ // if it ain't text we can't handle it
+ let cmd = match cmd.to_str() {
+ Ok(cmd) => { if cmd.len() > MAX_IN {
+ println!("{room_id} E: string too big: {cmd}");
+ return
+ } else { cmd.to_owned() } },
+ Err(_) => return
+ };
+
+ let mut fields = cmd.split(" ");
+ let parse_pos = |mut fields: std::str::Split<&str>| -> Option<(usize, usize)> {
+ let x = fields.next().and_then(|xstr| xstr.parse::<usize>().ok());
+ let y = fields.next().and_then(|ystr| ystr.parse::<usize>().ok());
+ x.zip(y)
+ };
+ if let Some(cmd_name) = fields.next() {
+ use crate::minesweeper::{Move,MoveType};
+ let mut players_lock = players.write().await;
+ match players_lock.get_mut(&addr) {
+ Some(me) => match cmd_name {
+ "pos" => {
+ if let Some(pos) = parse_pos(fields) {
+ if let Err(e) = pos_tx.send(livepos::Req { id: me.uid, data: livepos::ReqData::Pos(pos) }) {
+ println!("{room_id} E: couldn't process {me}'s position update: {e}");
+ };
+ }
+ },
+ "reveal" => {
+ match parse_pos(fields) {
+ Some(pos) => {
+ if let Err(e) = cmd_tx.send(MetaMove::Move(Move { t: MoveType::Reveal, pos }, addr)) {
+ println!("{room_id} E: couldn't process {me}'s reveal command: {e}");
+ };
+ },
+ None => {
+ println!("{room_id} E: bad reveal from {me}");
+ }
+ }
+ },
+ "flag" => {
+ match parse_pos(fields) {
+ Some(pos) => {
+ if let Err(e) = cmd_tx.send(MetaMove::Move(Move { t: MoveType::ToggleFlag, pos }, addr)) {
+ println!("{room_id} E: couldn't process {me}'s flag command: {e}");
+ };
+ },
+ None => {
+ println!("{room_id} E: bad flag from {me}");
+ }
+ }
+ },
+ "reset" => {
+ if let Err(e) = cmd_tx.send(MetaMove::Reset) {
+ println!("{room_id} E: couldn't request game dump in behalf of {me}: {e}");
+ }
+ },
+ e => println!("{room_id} E: unknown command {e:?} from {me}: \"{cmd}\""),
+ },
+ None => {
+ if cmd_name == "register" {
+ let mut all_fields = fields.collect::<Vec<&str>>();
+ let clr = all_fields.pop().expect("register without color").chars().filter(|c| c.is_digit(16) || *c == '#').collect::<String>();
+ let name = {
+ let def = "anon".to_string();
+ if all_fields.is_empty() { def }
+ else {
+ let n = ammonia::clean(&all_fields.join(" "));
+ if n.is_empty() { def } else { n }
+ }
+ };
+ println!("{room_id} I: registered \"{name}@{addr}\"");
+ drop(players_lock);
+ let uid = {
+ // new scope cuz paranoid bout deadlocks
+ let conn = Conn { addr, tx: tx.clone() };
+ room.write().await.players.insert_conn(conn, name.clone(), clr).await
+ };
+ let players_lock = players.read().await;
+ let me = players_lock.get(&addr).unwrap();
+ tx.send(Message::text(format!("regack {} {} {} {}",
+ room_conf.name.replace(' ', "&nbsp;"), name.replace(' ', "&nbsp;"), uid, room_conf.board_conf))
+ ).expect("couldn't send register ack");
+
+ {
+ let msg = Message::text(format!("players {}",
+ jsonenc_players(players_lock.values())
+ .expect("couldn't JSONify players")));
+ for p in players_lock.values() {
+ if let Err(e) = p.conn.tx.send(msg.clone()) {
+ println!("{room_id} E: couldn't dump players for {me}: {e}");
+ }
+ }
+ }
+ if let Err(e) = pos_tx.send(livepos::Req { id: uid, data: livepos::ReqData::StateDump }) {
+ println!("{room_id} E: couldn't request position dump for {me}: {e}");
+ }
+ if let Err(e) = cmd_tx.send(MetaMove::Dump) {
+ println!("{room_id} E: couldn't request game dump for {me}: {e}");
+ }
+ }
+ }
+ }
+ }
+ } else {
+ println!("{room_id} E: reached end of stream for {addr}");
+ break;
+ }
+ }
+}
+
+fn jsonenc_players<'a, I: IntoIterator<Item=&'a Player>>(players: I) -> Result<String, serde_json::Error> {
+ let mut pairs = Vec::new();
+ for player in players {
+ pairs.push((player.uid, player.name.replace(' ', "&nbsp"), player.clr.clone()));
+ }
+ serde_json::to_string(&pairs)
+}
diff --git a/src/livepos.rs b/src/livepos.rs
new file mode 100644
index 0000000..9112755
--- /dev/null
+++ b/src/livepos.rs
@@ -0,0 +1,78 @@
+use crate::types::*;
+use tokio::sync::mpsc as tokio_mpsc;
+use tokio::sync::Mutex;
+use std::collections::{HashMap,HashSet};
+use tokio::time::{self, Duration};
+use warp::ws::Message;
+
+pub enum ReqData {
+ Pos((usize,usize)),
+ StateDump,
+ Quit,
+}
+
+pub struct Req {
+ pub id: usize,
+ pub data: ReqData,
+}
+
+pub async fn livepos(players: PlayerMapData, mut recv: tokio_mpsc::UnboundedReceiver<Req>) {
+ let positions = Mutex::new(HashMap::new());
+ let dirty = Mutex::new(HashSet::new());
+ let process_upds = async {
+ while let Some(update) = recv.recv().await {
+ let mut dirty = dirty.lock().await;
+ let mut positions = positions.lock().await;
+ match update.data {
+ ReqData::Pos(p) => {
+ let old = positions.get(&update.id).unwrap_or(&(0,0));
+ if p != *old {
+ dirty.insert(update.id);
+ }
+ positions.insert(update.id, p);
+ },
+ ReqData::StateDump => {
+ dirty.clear();
+ dirty.extend(positions.keys().copied());
+ },
+ ReqData::Quit => {
+ positions.remove(&update.id);
+ dirty.retain(|x| *x != update.id);
+ }
+ }
+ }
+ };
+ let periodic_send = async {
+ let mut interv = tokio::time::interval(Duration::from_millis(16));
+ interv.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
+ loop {
+ interv.tick().await;
+ let mut dirty = dirty.lock().await;
+ if dirty.len() > 0 {
+ let mut positions = positions.lock().await;
+ let msg = jsonenc_ids(&mut positions, &*dirty).expect("couldn't JSONify player positions");
+ dirty.clear();
+ let plock = players.read().await;
+ for player in plock.values() {
+ if let Err(e) = player.conn.tx.send(Message::text(format!("pos {}", msg))) {
+ println!("E: couldn't send livepos update to {}: {}", player, e);
+ }
+ }
+ }
+ }
+ };
+
+ tokio::select!(
+ _ = process_upds => (),
+ _ = periodic_send => ()
+ );
+}
+
+fn jsonenc_ids<'a, I: IntoIterator<Item=&'a usize>>(positions: &mut HashMap<usize, (usize,usize)>, ids: I) -> Result<String, serde_json::Error> {
+ let mut pairs = Vec::new();
+ for id in ids {
+ pairs.push((id, positions[id]));
+ };
+ serde_json::to_string(&pairs)
+}
+
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..07ba00e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,328 @@
+use std::{
+ error::Error,
+ net::SocketAddr,
+ sync::Arc,
+ collections::HashMap,
+ num::NonZeroUsize,
+};
+use futures::stream::StreamExt;
+
+mod types;
+mod livepos;
+mod conn;
+mod minesweeper;
+use types::*;
+
+use tokio::sync::RwLock;
+
+const FONT_FILE: &[u8] = include_bytes!("../assets/VT323-Regular.ttf");
+const CONF_FILE: &str = "./conf.json";
+
+fn main() -> Result<(), Box<dyn Error>> {
+ let conf = Config {
+ cert: "./cert.pem".to_owned(),
+ pkey: "./cert.rsa".to_owned(),
+ index_pg: "./assets/index.html".to_owned(),
+ room_pg: "./assets/room.html".to_owned(),
+ client_code: "./assets/client.js".to_owned(),
+ stylesheet: "./assets/style.css".to_owned(),
+ socket_addr: ([0,0,0,0],31235).into(),
+ };
+
+ tokio_main(conf)
+}
+
+#[tokio::main]
+async fn tokio_main(conf: Config) -> Result<(), Box<dyn Error>> {
+ let conf_file: serde_json::Value = serde_json::from_str(&tokio::fs::read_to_string(CONF_FILE).await?)?;
+ let area_limit: usize = conf_file.get("area_limit")
+ .expect("no area_limit field in the conf.json file")
+ .as_u64().expect("area_limit not a number") as usize;
+ let room_limit: usize = conf_file.get("room_limit")
+ .expect("no room_limit field in the conf.json file")
+ .as_u64().expect("room_limit not a number") as usize;
+ let rooms: RoomMap = Arc::new(RwLock::new(HashMap::new()));
+ let public_rooms = Arc::new(RwLock::new(HashMap::new()));
+ use warp::*;
+
+ let index = path::end().and(fs::file(conf.index_pg.clone()));
+ let style = path!("s.css").and(fs::file(conf.stylesheet.clone()));
+ let code = path!("c.js").and(fs::file(conf.client_code.clone()));
+ let font = path!("f.ttf").map(|| FONT_FILE);
+ let listing = {
+ let rooms = rooms.clone();
+ let pubs = public_rooms.clone();
+ path!("rlist").and_then(move || {
+ let rooms = rooms.clone();
+ let pubs = pubs.clone();
+ async move {
+ let roomsl = rooms.read().await;
+ let pubsl = pubs.read().await;
+ let rooms_pcount = futures::stream::iter(pubsl.iter())
+ .then(|(id, _):(&RoomId,_)| {
+ let roomsl = roomsl.clone();
+ async move {
+ let room = roomsl.get(id).unwrap().read().await;
+ let pcount = room.players.read().await.len();
+ (id.clone(), (pcount, room.conf.player_cap))
+ }
+ })
+ .collect::<HashMap<RoomId,_>>().await;
+ let resp = (&*pubsl, rooms_pcount);
+ Ok::<_,std::convert::Infallible>(
+ reply::json(&resp)
+ )
+ }
+ })
+ };
+ let roomspace = {
+ let rooms = rooms.clone();
+
+ path!("rspace").and_then(move || {
+ let r = rooms.clone();
+ async move {
+ let empty_len = empty_rooms(r.clone()).await.len();
+ let space = room_limit - r.read().await.len() + empty_len;
+ Ok::<_,std::convert::Infallible>(
+ hyper::Response::builder()
+ .status(hyper::StatusCode::OK)
+ .body(hyper::Body::from(space.to_string()))
+ .unwrap()
+ )
+ }
+ })
+ };
+ let rform_recv = {
+ let rooms = rooms.clone();
+ let pubs = public_rooms.clone();
+ post().and(path("r")).and(body::content_length_limit(4096)).and(body::form())
+ .and_then(move |rinfo: HashMap<String, String>| {
+ println!("{:?}", rinfo);
+ let rooms = rooms.clone();
+ let pubs = pubs.clone();
+ async move {
+ let slots_available = room_limit - rooms.read().await.len();
+ let empty = empty_rooms(rooms.clone()).await;
+ if slots_available < 1 {
+ if slots_available + empty.len() > 0 {
+ remove_room(rooms.clone(), pubs.clone(), empty[0].clone()).await;
+ } else {
+ return Err(reject::custom(NoRoomSlots));
+ }
+ }
+
+ if let (Some(w),Some(h),Some(num),Some(denom),access,asfm,limit) = (
+ rinfo.get("rwidth").and_then(|wt| wt.parse::<NonZeroUsize>().ok()),
+ rinfo.get("rheight").and_then(|ht| ht.parse::<NonZeroUsize>().ok()),
+ rinfo.get("rration").and_then(|nt| nt.parse::<usize>().ok()),
+ rinfo.get("rratiod").and_then(|dt| dt.parse::<NonZeroUsize>().ok()),
+ rinfo.get("raccess"),
+ rinfo.get("ralwayssafe1move"),
+ rinfo.get("rlimit").and_then(|l| l.parse::<usize>().ok()),
+ ) {
+ if w.get()*h.get() > area_limit {
+ return Err(reject::custom(BoardTooBig))
+ }
+ let board_conf = minesweeper::BoardConf { w, h, mine_ratio: (num,denom), always_safe_first_move: asfm.is_some() };
+ let mut rooms = rooms.write().await;
+ let uid = types::RoomId::new_in(&rooms);
+ let name = {
+ let n = rinfo.get("rname").unwrap().to_owned();
+ if n.is_empty() { uid.to_string() } else { n }
+ };
+
+ let players = PlayerMap::default();
+
+ let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
+ let game_handle = tokio::spawn(gameloop(cmd_rx, players.clone(), board_conf));
+
+ let (pos_tx, pos_rx) = tokio::sync::mpsc::unbounded_channel();
+ let livepos_handle = tokio::spawn(livepos::livepos(players.clone(), pos_rx));
+
+ let room_conf = RoomConf {
+ name,
+ player_cap: match limit { Some(i) => i, None => usize::MAX },
+ public: access.is_some(),
+ board_conf,
+ };
+ let new_room = Room {
+ conf: room_conf,
+ players,
+ game_driver: game_handle,
+ cmd_stream: cmd_tx,
+ livepos_driver: livepos_handle,
+ pos_stream: pos_tx,
+ };
+ if access.is_some() {
+ pubs.write().await.insert(uid.clone(), serde_json::to_string(&new_room.conf).unwrap());
+ }
+ rooms.insert(uid.clone(), Arc::new(RwLock::new(new_room)));
+
+ Ok(
+ hyper::Response::builder()
+ .status(hyper::StatusCode::SEE_OTHER)
+ .header(hyper::header::LOCATION, format!("./room/{uid}"))
+ .body(hyper::Body::empty())
+ .unwrap()
+ )
+ } else { Err(reject::custom(BadFormData)) }
+ }
+ })
+ };
+ let room = {
+ let rooms_ws = rooms.clone();
+ let rooms_lobby = rooms.clone();
+ let prefix = get().and(path!("room" / String / ..));
+
+ // Fixme: better errors
+ prefix.and(path!("ws"))
+ .and(ws())
+ .and(addr::remote())
+ .and_then(move |id: String, websocket: warp::ws::Ws, saddr: Option<SocketAddr>| {
+ let rooms = rooms_ws.clone();
+ async move {
+ let id = RoomId(id);
+ match rooms.read().await.get(&id).cloned() {
+ Some(r) => {
+ println!("{id} I: conn from {saddr:?}");
+ Ok(websocket.on_upgrade(move |socket| {
+ conn::lobby(socket, saddr.expect("socket without address"), (id,r))
+ }))
+ },
+ None => {
+ println!("I: conn from {saddr:?} into inexistent room {id}");
+ Err(reject())
+ }
+ }
+ }
+ })
+ .or(prefix.and(path::end())
+ .and(fs::file(conf.room_pg.clone()))
+ .then(move |id: String, f: fs::File| {
+ let rooms = rooms_lobby.clone();
+ async move {
+ if rooms.read().await.contains_key(&RoomId(id)) {
+ f.into_response()
+ } else {
+ reply::with_status("No such room", http::StatusCode::BAD_REQUEST).into_response()
+ }
+ }
+ })
+ )
+ };
+
+
+ let route = get()
+ .and(index)
+ .or(style)
+ .or(code)
+ .or(font)
+ .or(listing)
+ .or(roomspace)
+ .or(rform_recv)
+ .or(room)
+ .recover(error_handler);
+
+ let server = warp::serve(route)
+ .tls()
+ .cert_path(conf.cert)
+ .key_path(conf.pkey)
+ .run(conf.socket_addr);
+ println!("Serving on {}", conf.socket_addr);
+ server.await;
+ Ok(())
+}
+
+// If a move is made, broadcast new board, else just send current board
+async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver<MetaMove>, players: PlayerMapData, bconf: minesweeper::BoardConf) {
+ use minesweeper::*;
+ use flate2::{ Compression, write::DeflateEncoder };
+ use std::io::Write;
+ let mut game = Game::new(bconf);
+ let mut latest_player_name = None;
+ while let Some(req) = move_rx.recv().await {
+ let done = game.phase == Phase::Die || game.phase == Phase::Win;
+ match req {
+ MetaMove::Move(m, o) => if !done {
+ game = game.act(m);
+ if game.phase == Phase::Win || game.phase == Phase::Die {
+ game.board = game.board.grade();
+ }
+ latest_player_name = players.read().await.get(&o).map(|p| p.name.clone());
+ },
+ MetaMove::Dump => (),
+ MetaMove::Reset => { game = Game::new(bconf); },
+ }
+ use warp::ws::Message;
+ let mut board_encoder = DeflateEncoder::new(Vec::new(), Compression::default());
+ board_encoder.write_all(&game.board.render()).unwrap();
+ let compressed_board = board_encoder.finish().unwrap();
+ let mut reply = vec![Message::binary(compressed_board)];
+ let lpname = latest_player_name.as_deref().unwrap_or("unknown player").replace(' ', "&nbsp");
+ match game.phase {
+ Phase::Win => { reply.push(Message::text(format!("win {lpname}"))); },
+ Phase::Die => { reply.push(Message::text(format!("lose {lpname}"))); },
+ _ => (),
+ }
+ {
+ let peers = players.read().await;
+ for (addr, p) in peers.iter() {
+ for r in reply.iter() {
+ if let Err(e) = p.conn.tx.send(r.clone()) {
+ println!("couldn't send game update {r:?} to {addr}: {e}");
+ }
+ }
+ }
+ }
+ }
+}
+
+use warp::{ reject::{ Reject, Rejection }, reply::{ self, Reply }, http::StatusCode };
+#[derive(Debug)]
+struct BadFormData;
+impl Reject for BadFormData {}
+
+#[derive(Debug)]
+struct BoardTooBig;
+impl Reject for BoardTooBig {}
+
+#[derive(Debug)]
+struct NoRoomSlots;
+impl Reject for NoRoomSlots {}
+
+async fn error_handler(err: Rejection) -> Result<impl Reply, std::convert::Infallible> {
+ if err.is_not_found() { Ok(reply::with_status("No such file", StatusCode::NOT_FOUND)) }
+ else if let Some(_e) = err.find::<BadFormData>() {
+ Ok(reply::with_status("Bad form data", StatusCode::BAD_REQUEST))
+ } else if let Some(_e) = err.find::<BoardTooBig>() {
+ Ok(reply::with_status("Board too big", StatusCode::BAD_REQUEST))
+ } else if let Some(_e) = err.find::<NoRoomSlots>() {
+ Ok(reply::with_status("No more rooms slots", StatusCode::BAD_REQUEST))
+ } else {
+ println!("unhandled rejection: {err:?}");
+ Ok(reply::with_status("Server error", StatusCode::INTERNAL_SERVER_ERROR))
+ }
+}
+
+async fn empty_rooms(rooms: RoomMap) -> Vec<RoomId> {
+ let rl = rooms.read().await;
+ futures::stream::iter(rl.iter())
+ .filter_map(|(id,roomarc)| async move {
+ let rrl = roomarc.read().await;
+ let rrrl = rrl.players.read().await;
+ if rrrl.len() == 0 { Some(id.clone()) } else { None }
+ })
+ .collect::<Vec<RoomId>>().await
+}
+
+async fn remove_room<T>(rooms: RoomMap, pubs: Arc<RwLock<HashMap<RoomId,T>>>, id: RoomId) {
+ {
+ let mut rwl = rooms.write().await;
+ rwl.remove(&id);
+ }
+ {
+ let mut pwl = pubs.write().await;
+ pwl.remove(&id);
+ }
+}
+
diff --git a/src/minesweeper.rs b/src/minesweeper.rs
new file mode 100644
index 0000000..9e362dc
--- /dev/null
+++ b/src/minesweeper.rs
@@ -0,0 +1,286 @@
+use std::{
+ convert::TryInto,
+ num::NonZeroUsize,
+};
+use rand::{ thread_rng, Rng, distributions::Uniform };
+use serde::Serialize;
+
+const HIDDEN_BIT: u8 = 1 << 7;
+pub const FLAGGED_BIT: u8 = 1 << 6;
+const CORRECT_BIT: u8 = 1 << 5; // grading for a rightly flagged mine
+// all the bits that aren't flags
+const TILE_NUMBITS: u8 = !(HIDDEN_BIT | FLAGGED_BIT | CORRECT_BIT);
+const MINED: u8 = HIDDEN_BIT | TILE_NUMBITS;
+const NEIGH_OFFS: &[(isize,isize)] = &[
+ (-1,-1),(0,-1),(1,-1),
+ (-1, 0), (1, 0),
+ (-1, 1),(0, 1),(1, 1),
+];
+#[derive(PartialEq)]
+pub enum Phase {
+ SafeFirstMove,
+ FirstMoveFail,
+ Run,
+ Die,
+ Win,
+// Leave,
+}
+pub struct Game {
+ pub phase: Phase,
+ pub board: Board,
+ pub board_conf: BoardConf,
+}
+
+#[derive(Debug, Clone, Copy, Serialize)]
+pub struct BoardConf {
+ pub w: NonZeroUsize,
+ pub h: NonZeroUsize,
+ /// mines/tiles, expressed as (numerator, denominator)
+ pub mine_ratio: (usize,NonZeroUsize),
+ pub always_safe_first_move: bool,
+}
+
+impl std::fmt::Display for BoardConf {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}x{} {}/{}", self.w, self.h, self.mine_ratio.0, self.mine_ratio.1)
+ }
+}
+
+pub struct Board {
+ pub data: Vec<u8>,
+ pub width: NonZeroUsize,
+ pub height: NonZeroUsize,
+ pub hidden_tiles: usize,
+ pub mine_count: usize,
+}
+#[derive(Debug)]
+pub enum MoveType {
+ Reveal,
+ ToggleFlag,
+}
+#[derive(Debug)]
+pub struct Move {
+ pub t: MoveType,
+ pub pos: (usize,usize),
+}
+
+pub struct MoveResult(pub Board, pub bool);
+impl Game {
+ pub fn new(conf: BoardConf) -> Self {
+ let board = Board::new(conf);
+ Game {
+ phase: if conf.always_safe_first_move { Phase::SafeFirstMove } else { Phase::Run },
+ board,
+ board_conf: conf
+ }
+ }
+ pub fn act(mut self, m: Move) -> Self {
+ let lost_phase = | phase | {
+ match phase {
+ Phase::SafeFirstMove => Phase::FirstMoveFail,
+ Phase::Run => Phase::Die,
+ _ => unreachable!(),
+ }
+ };
+
+ match m.t {
+ MoveType::Reveal => {
+ let kaboom: bool;
+ self.board = {
+ let mr = self.board.reveal(m.pos);
+ kaboom = mr.1;
+ mr.0
+ };
+ if kaboom { self.phase = lost_phase(self.phase) }
+ if self.phase == Phase::SafeFirstMove { self.phase = Phase::Run }
+ },
+ MoveType::ToggleFlag => self.board = self.board.flag(m.pos).0,
+ };
+
+ if self.phase == Phase::FirstMoveFail {
+ let winnable = self.board.mine_count < (self.board.width.get() * self.board.height.get());
+ if winnable {
+ self.board.move_mine_elsewhere(m.pos);
+ self.phase = Phase::Run;
+ self = self.act(m);
+ } else {
+ self.phase = Phase::Die;
+ }
+ } else if self.phase != Phase::Die && self.board.hidden_tiles == self.board.mine_count {
+ self.phase = Phase::Win;
+ }
+ self
+ }
+}
+impl Board {
+ pub fn new(conf: BoardConf) -> Self {
+ let (w,h) = (conf.w,conf.h);
+ let area = w.get()*h.get();
+ let mine_count = ((conf.mine_ratio.0 * area) / conf.mine_ratio.1.get()).clamp(0, area);
+ let b = Board {
+ data: [HIDDEN_BIT].repeat(area),
+ width: w,
+ height: h,
+ hidden_tiles: area,
+ mine_count,
+ };
+ b.spread_mines(mine_count)
+ }
+ pub fn spread_mines(mut self, mut count: usize) -> Self {
+ let mut rng = thread_rng();
+ let w = self.width.get();
+ let h = self.height.get();
+ while count > 0 {
+ let randpos: (usize, usize) = (rng.sample(Uniform::new(0,w)), rng.sample(Uniform::new(0,h)));
+ let o = self.pos_to_off_unchecked(randpos);
+ if self.data[o] == MINED { continue }
+ else {
+ self.data[o] = MINED;
+ count -= 1;
+ let minepos = pos_u2i(randpos).unwrap();
+ self.map_neighs(minepos, |neigh| {
+ if neigh != MINED {
+ neigh + 1
+ } else { neigh }
+ });
+ }
+ }
+ self
+ }
+
+ fn neighs<T>(&self, pos: (T,T)) -> Option<Vec<(usize,usize)>>
+ where T: TryInto<isize>
+ {
+ if let (Ok(ox),Ok(oy)) = (pos.0.try_into(),pos.1.try_into()) {
+ Some(NEIGH_OFFS
+ .iter()
+ .map(|(x,y)| (*x + ox, *y + oy)).filter_map(|p| self.bounded(p))
+ .collect())
+ } else {
+ None
+ }
+ }
+ fn map_neighs<T>(&mut self, pos: (T,T), mut f: impl FnMut(u8) -> u8) where T: TryInto<isize> {
+ if let Some(neighs) = self.neighs(pos) {
+ let npos = neighs.iter().filter_map(|pos| self.pos_to_off(*pos)).collect::<Vec<usize>>();
+ npos.iter().for_each(|o| {
+ self.data[*o] = f(self.data[*o]);
+ });
+ }
+ }
+
+ pub fn pos_to_off(&self, pos: (usize,usize)) -> Option<usize>
+ {
+ self.bounded(pos).map(|x| self.pos_to_off_unchecked(x))
+ }
+ pub fn pos_to_off_unchecked(&self, pos: (usize, usize)) -> usize {
+ pos.0 + pos.1 * self.width.get()
+ }
+ pub fn bounded<T>(&self, pos: (T,T)) -> Option<(usize, usize)>
+ where T: TryInto<usize>
+ {
+ if let (Ok(x),Ok(y)) = (
+ pos.0.try_into(),
+ pos.1.try_into(),
+ ) {
+ (x < self.width.get() && y < self.height.get()).then(|| (x,y))
+ } else { None }
+ }
+ pub fn flood_reveal(&mut self, pos: (usize,usize)) {
+ let mut queue = vec![pos];
+ while let Some(pos) = queue.pop() {
+ if let Some(off) = self.pos_to_off(pos) {
+ let c = &mut self.data[off];
+ if *c & HIDDEN_BIT > 0 {
+ *c &= !(HIDDEN_BIT | FLAGGED_BIT);
+ self.hidden_tiles -= 1;
+ if *c > 0 { continue; }
+ if let Some(mut adj) = self.neighs(pos) {
+ queue.append(&mut adj);
+ }
+ }
+ }
+ }
+ }
+ pub fn reveal(mut self, pos: (usize,usize)) -> MoveResult {
+ if let Some(off) = self.pos_to_off(pos) {
+ self.flood_reveal(pos);
+ let c = self.data[off];
+ MoveResult(self, (c & !(FLAGGED_BIT | CORRECT_BIT)) == TILE_NUMBITS)
+ } else {
+ MoveResult(self, false)
+ }
+ }
+ pub fn grade(mut self) -> Board {
+ for i in &mut self.data {
+ if *i == TILE_NUMBITS | FLAGGED_BIT | HIDDEN_BIT {
+ *i |= CORRECT_BIT;
+ }
+ }
+ self
+ }
+ pub fn flag(mut self, pos: (usize,usize)) -> MoveResult {
+ if let Some(off) = self.pos_to_off(pos) {
+ self.data[off] ^= FLAGGED_BIT;
+ }
+ MoveResult(self, false)
+ }
+
+ pub fn render(&self) -> Vec<u8> {
+ let mut ret = vec![];
+ for y in 0..self.height.get() {
+ for x in 0..self.width.get() {
+ let c = &self.data[self.pos_to_off_unchecked((x,y))];
+ match *c {
+ 0 => ret.push(b' '),
+ _ if *c <= 8 => ret.push(b'0' + c),
+ _ if (*c & CORRECT_BIT) > 0 => ret.push(b'C'),
+ _ if (*c & FLAGGED_BIT) > 0 => ret.push(b'F'),
+ _ if (*c & HIDDEN_BIT) > 0 => ret.push(b'#'),
+ _ if *c == TILE_NUMBITS => ret.push(b'O'),
+ _ => ret.push(b'?'),
+ }
+ }
+ ret.extend_from_slice(b"<br>");
+ }
+ ret
+ }
+
+ pub fn move_mine_elsewhere(&mut self, pos: (usize, usize)) {
+ let mut surround_count = 0;
+ self.map_neighs(pos, |val| {
+ if (val & !FLAGGED_BIT) == MINED {
+ surround_count += 1;
+ val
+ } else {
+ val - 1
+ }});
+ let off = self.pos_to_off(pos).unwrap();
+ let vacant_pos = {
+ let v = self.data.iter()
+ .enumerate()
+ .filter(|(_,val)| (*val & TILE_NUMBITS) != TILE_NUMBITS)
+ .map(|(p,_)| p)
+ .next()
+ .unwrap(); // there must be at least one
+ (v%self.width.get(), v/self.width.get())
+ };
+ let voff = self.pos_to_off_unchecked(vacant_pos);
+ debug_assert!(voff != off, "swapped mine to the same position in a FirstMoveFail/grace'd first move (???)");
+
+ { // swap 'em (keep these together, pls kthnx (bugs were had))
+ self.data[voff] |= MINED;
+ self.data[off] = surround_count;
+ }
+
+ self.map_neighs(vacant_pos, |val| {
+ if (val & !FLAGGED_BIT) == MINED { val } else { val + 1 }
+ });
+ }
+}
+
+fn pos_u2i(pos: (usize, usize)) -> Option<(isize, isize)> {
+ if let (Ok(x),Ok(y)) = (pos.0.try_into(), pos.1.try_into())
+ { Some((x,y)) } else { None }
+}
+
diff --git a/src/types.rs b/src/types.rs
new file mode 100644
index 0000000..f9f166a
--- /dev/null
+++ b/src/types.rs
@@ -0,0 +1,135 @@
+use std::{
+ collections::HashMap,
+ net::SocketAddr,
+ sync::{
+ Arc,
+ atomic::{ AtomicUsize, Ordering },
+ },
+ fmt::Display,
+ ops::{ Deref, DerefMut },
+};
+use warp::ws::Message;
+use tokio::sync::RwLock;
+use serde::Serialize;
+use crate::minesweeper;
+use crate::livepos;
+
+#[derive(Debug, Clone)]
+pub struct Config {
+ pub cert: String,
+ pub pkey: String,
+ pub index_pg: String,
+ pub room_pg: String,
+ pub client_code: String,
+ pub stylesheet: String,
+ pub socket_addr: SocketAddr,
+}
+
+#[derive(Debug, Serialize, Clone)]
+pub struct RoomConf {
+ pub name: String,
+ pub player_cap: usize,
+ pub public: bool,
+ pub board_conf: minesweeper::BoardConf,
+}
+
+pub struct Room {
+ pub conf: RoomConf,
+ pub players: PlayerMap,
+ pub game_driver: tokio::task::JoinHandle<()>,
+ pub cmd_stream: CmdTx,
+ pub livepos_driver: tokio::task::JoinHandle<()>,
+ pub pos_stream: tokio::sync::mpsc::UnboundedSender<livepos::Req>,
+}
+
+#[derive(Debug)]
+pub enum MetaMove {
+ Move(minesweeper::Move,SocketAddr),
+ Dump,
+ Reset,
+}
+
+#[derive(Debug)]
+pub struct Conn {
+ pub tx: tokio::sync::mpsc::UnboundedSender<Message>,
+ pub addr: SocketAddr,
+}
+
+#[derive(Debug)]
+pub struct Player {
+ pub conn: Conn,
+ pub uid: usize,
+ pub name: String,
+ pub clr: String,
+}
+
+impl Display for Player {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "\"{}\"@{}", self.name, self.conn.addr)
+ }
+}
+
+#[derive(Eq, PartialEq, Hash, Debug, Clone, serde::Serialize)]
+pub struct RoomId(pub String);
+impl Display for RoomId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+impl std::borrow::Borrow<str> for RoomId {
+ fn borrow(&self) -> &str {
+ self.0.borrow()
+ }
+}
+
+impl RoomId {
+ pub fn new_in<T>(map: &HashMap<RoomId, T>) -> Self {
+ use rand::{ thread_rng, Rng, distributions::Alphanumeric };
+ let id = RoomId(thread_rng()
+ .sample_iter(&Alphanumeric)
+ .take(16)
+ .map(char::from)
+ .collect::<String>());
+ if map.contains_key(&id) { RoomId::new_in(map) }
+ else { id }
+ }
+}
+
+pub type CmdTx = tokio::sync::mpsc::UnboundedSender<MetaMove>;
+pub type RoomMap = Arc<RwLock<HashMap<RoomId, Arc<RwLock<Room>>>>>;
+pub type PlayerMapData = Arc<RwLock<HashMap<SocketAddr, Player>>>;
+#[derive(Debug)]
+pub struct PlayerMap {
+ inner: PlayerMapData,
+ uid_counter: AtomicUsize,
+}
+
+impl Deref for PlayerMap {
+ type Target = Arc<RwLock<HashMap<SocketAddr, Player>>>;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+impl DerefMut for PlayerMap {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
+impl Default for PlayerMap {
+ fn default() -> Self {
+ Self { inner: Arc::new(RwLock::new(HashMap::new())), uid_counter: 0.into() }
+ }
+}
+
+impl PlayerMap {
+ pub async fn insert_conn(&mut self, conn: Conn, name: String, clr: String) -> usize {
+ let mut map = self.write().await;
+ let uid = self.uid_counter.fetch_add(1, Ordering::Relaxed);
+ map.insert(
+ conn.addr,
+ Player { conn, uid, name, clr },
+ );
+ uid
+ }
+}
+