commit f7b8519646a7ea44b3d9d375bc2f7ce225694070
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Fri, 14 Aug 2026 22:47:43 -0400
Add basic multiplayer lobby
Diffstat:
20 files changed, 1640 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
@@ -0,0 +1,483 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "client"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "common",
+ "macroquad",
+ "mio",
+ "postcard",
+ "serde",
+]
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "common"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "glam",
+ "postcard",
+ "serde",
+]
+
+[[package]]
+name = "core_maths"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "fontdue"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7894823fa221401399e2598f8b63f81ac77ff5c63248b7656779bff1632d7d3d"
+dependencies = [
+ "hashbrown",
+ "ttf-parser",
+]
+
+[[package]]
+name = "glam"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "image"
+version = "0.24.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "num-traits",
+ "png",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "macroquad"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cbd54d99d4d3ac59deed7ee4927bbbf4eb33a55635530af3be9395a55746090"
+dependencies = [
+ "fontdue",
+ "glam",
+ "image",
+ "macroquad_macro",
+ "miniquad",
+ "quad-rand",
+]
+
+[[package]]
+name = "macroquad_macro"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd"
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "miniquad"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbaea978d741f3b60ced666b06a2a1e4a2bc81cb8b9dd29f4a40965e1f59e65"
+dependencies = [
+ "libc",
+ "ndk-sys",
+ "objc-rs",
+ "winapi",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "objc-rs"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quad-rand"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "server"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "common",
+ "glam",
+ "mio",
+ "postcard",
+ "serde",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+dependencies = [
+ "core_maths",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[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-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",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,3 @@
+[workspace]
+resolver = "3"
+members = ["client", "common", "server"]
diff --git a/client/Cargo.lock b/client/Cargo.lock
@@ -0,0 +1,432 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "core_maths"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "either"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "fontdue"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7894823fa221401399e2598f8b63f81ac77ff5c63248b7656779bff1632d7d3d"
+dependencies = [
+ "hashbrown 0.15.5",
+ "ttf-parser",
+]
+
+[[package]]
+name = "glam"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "foldhash 0.2.0",
+ "rayon",
+]
+
+[[package]]
+name = "image"
+version = "0.24.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "num-traits",
+ "png",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "library"
+version = "0.1.0"
+dependencies = [
+ "macroquad",
+ "shipyard",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "macroquad"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cbd54d99d4d3ac59deed7ee4927bbbf4eb33a55635530af3be9395a55746090"
+dependencies = [
+ "fontdue",
+ "glam",
+ "image",
+ "macroquad_macro",
+ "miniquad",
+ "quad-rand",
+]
+
+[[package]]
+name = "macroquad_macro"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd"
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "miniquad"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbaea978d741f3b60ced666b06a2a1e4a2bc81cb8b9dd29f4a40965e1f59e65"
+dependencies = [
+ "libc",
+ "ndk-sys",
+ "objc-rs",
+ "winapi",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "objc-rs"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quad-rand"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "shipyard"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ced12597d87bb2a67f3c712a9cea816bd9f822a5a83883f59ec8805b7ceba6a"
+dependencies = [
+ "hashbrown 0.16.1",
+ "lock_api",
+ "rayon",
+ "shipyard_proc",
+]
+
+[[package]]
+name = "shipyard_proc"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e09f75ea0c5902cedcbb5889628232eb49d6d09728d7cc1a3f1736fa4e7ce7c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+dependencies = [
+ "core_maths",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[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"
diff --git a/client/Cargo.toml b/client/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "client"
+version = "0.1.0"
+edition = "2024"
+default-run = "client"
+
+[dependencies]
+anyhow = "1.0.104"
+common = { path = "../common/" }
+macroquad = { version = "0.4.16", features = ["glam-serde"] }
+mio = { version = "1.2.2", features = ["net", "os-poll"] }
+postcard = { version = "1.1.3", default-features = false, features = ["use-std"] }
+serde = { version = "1.0.229", features = ["derive"] }
diff --git a/client/assets/books.png b/client/assets/books.png
Binary files differ.
diff --git a/client/assets/meeple.png b/client/assets/meeple.png
Binary files differ.
diff --git a/client/src/game.rs b/client/src/game.rs
@@ -0,0 +1,27 @@
+use common::game::{PlayerAction, PlayerActionKind};
+use macroquad::prelude::*;
+
+pub(crate) fn handle_input(player_name: &str) -> Option<PlayerAction> {
+ let velocity_x = if is_key_down(KeyCode::A) {
+ -1.
+ } else if is_key_down(KeyCode::D) {
+ 1.
+ } else {
+ 0.
+ };
+ let velocity_y = if is_key_down(KeyCode::W) {
+ -1.
+ } else if is_key_down(KeyCode::S) {
+ 1.
+ } else {
+ 0.
+ };
+ if velocity_x != 0. || velocity_y != 0. {
+ Some(PlayerAction::new(
+ player_name.to_string(),
+ PlayerActionKind::Move(Vec2::new(velocity_x, velocity_y)),
+ ))
+ } else {
+ None
+ }
+}
diff --git a/client/src/main.rs b/client/src/main.rs
@@ -0,0 +1,80 @@
+use std::{
+ sync::mpsc::channel,
+ thread,
+ time::{SystemTime, UNIX_EPOCH},
+};
+
+use common::{
+ codec::Codec,
+ game::{GameState, PlayerAction, PlayerActionKind},
+};
+use macroquad::{
+ prelude::*,
+ rand::{gen_range, srand},
+};
+
+use crate::{
+ game::handle_input,
+ net::{OnShutdown, handle_connection},
+ render::{Camera, follow_local_player, load_sprites, render},
+};
+
+pub mod game;
+pub mod net;
+pub mod render;
+
+#[macroquad::main("Library")]
+async fn main() {
+ request_new_screen_size(1280., 720.);
+ let spritesheet = load_sprites();
+
+ let (client_to_server_tx, client_to_server_rx) = channel();
+ let (server_to_client_tx, server_to_client_rx) = channel();
+ thread::spawn(|| {
+ handle_connection(
+ "127.0.0.1:6767".parse().unwrap(),
+ server_to_client_tx,
+ client_to_server_rx,
+ )
+ });
+
+ // seed the rng with current nanosecond timestamp to get (probably) different output on each run
+ srand(
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .subsec_nanos() as u64,
+ );
+
+ // choose a random three-letter lowercase string as our player name (for now)
+ let local_player_name = (0..3)
+ .map(|_| gen_range('a' as u8, 'z' as u8))
+ .fold(String::new(), |acc, el| acc + &(el as char).to_string());
+
+ let _ = client_to_server_tx.send(PlayerAction::new(
+ local_player_name.to_string(),
+ PlayerActionKind::Join,
+ ));
+
+ let mut camera = Camera::default();
+ let mut codec = Codec::default();
+ let mut game = GameState::default();
+ let _on_shutdown = OnShutdown::new(client_to_server_tx.clone(), local_player_name.to_string());
+
+ loop {
+ let bytes: Vec<u8> = server_to_client_rx.try_iter().flatten().collect();
+ if let Some(decoded) = codec.decode(&bytes).last() {
+ if let Ok(g) = postcard::from_bytes::<GameState>(decoded) {
+ game = g;
+ }
+ }
+ if let Some(action) = handle_input(&local_player_name) {
+ if let Err(_) = client_to_server_tx.send(action) {
+ return;
+ }
+ }
+ follow_local_player(&mut camera, &game, &local_player_name);
+ render(&camera, &game, &spritesheet);
+ next_frame().await;
+ }
+}
diff --git a/client/src/net.rs b/client/src/net.rs
@@ -0,0 +1,113 @@
+use std::{
+ io::{self, Read, Write},
+ net::SocketAddr,
+ sync::{
+ Arc, Mutex,
+ mpsc::{Receiver, Sender},
+ },
+ thread,
+ time::Duration,
+};
+
+use common::{
+ codec::Codec,
+ game::{PlayerAction, PlayerActionKind},
+};
+use mio::{Events, Interest, Poll, Token, net::TcpStream};
+
+const TOKEN: Token = Token(0);
+
+pub(crate) fn handle_connection(addr: SocketAddr, tx: Sender<Vec<u8>>, rx: Receiver<PlayerAction>) {
+ let mut stream = TcpStream::connect(addr).unwrap();
+ let poll = Poll::new().unwrap();
+ poll.registry()
+ .register(&mut stream, TOKEN, Interest::READABLE)
+ .unwrap();
+ let shared_stream = Arc::new(Mutex::new(stream));
+ let shared_stream_read = shared_stream.clone();
+ let shared_stream_write = shared_stream.clone();
+ let read_thread = thread::spawn(|| handle_server_read(poll, shared_stream_read, tx));
+ let write_thread = thread::spawn(|| handle_server_write(shared_stream_write, rx));
+ while !read_thread.is_finished() && !write_thread.is_finished() {
+ thread::sleep(Duration::from_millis(500));
+ }
+}
+
+pub(crate) fn handle_server_read(
+ mut poll: Poll,
+ stream: Arc<Mutex<TcpStream>>,
+ tx: Sender<Vec<u8>>,
+) {
+ let mut events = Events::with_capacity(128);
+ let mut buffer = [0x00; 1024];
+ let mut buffer_index;
+ loop {
+ // need an event loop here because otherwise this thread would hold a permanent lock on the
+ // stream while blocking on reading it
+ poll.poll(&mut events, None).unwrap();
+ for event in events.iter() {
+ if event.is_readable() {
+ buffer_index = stream.lock().unwrap().read(&mut buffer).unwrap();
+ if let Err(_) = tx.send(buffer[..buffer_index].to_vec()) {
+ break;
+ }
+ }
+ }
+ }
+}
+
+fn handle_server_write(stream: Arc<Mutex<TcpStream>>, rx: Receiver<PlayerAction>) {
+ let mut all_sent;
+ loop {
+ match rx.recv() {
+ Ok(m) => match postcard::to_stdvec(&m) {
+ Ok(to_write) => {
+ all_sent = false;
+ while !all_sent {
+ match stream
+ .lock()
+ .unwrap()
+ .write_all(Codec::encode(&to_write).as_slice())
+ {
+ Ok(_) => all_sent = true,
+ Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
+ all_sent = false;
+ }
+ Err(e) if e.kind() == io::ErrorKind::NotConnected => {
+ all_sent = false;
+ }
+ Err(_) => {
+ return;
+ }
+ }
+ }
+ }
+ Err(_) => (),
+ },
+ Err(_) => return,
+ }
+ }
+}
+
+pub(crate) struct OnShutdown {
+ tx: Sender<PlayerAction>,
+ local_player_name: String,
+}
+
+impl OnShutdown {
+ pub(crate) fn new(tx: Sender<PlayerAction>, local_player_name: String) -> Self {
+ OnShutdown {
+ tx,
+ local_player_name,
+ }
+ }
+}
+
+impl Drop for OnShutdown {
+ fn drop(&mut self) {
+ let _ = self.tx.send(PlayerAction::new(
+ self.local_player_name.clone(),
+ PlayerActionKind::Leave,
+ ));
+ }
+}
diff --git a/client/src/render.rs b/client/src/render.rs
@@ -0,0 +1,85 @@
+use common::game::{GameState, SpriteIndex};
+use macroquad::prelude::*;
+
+#[derive(Debug, Default)]
+pub(crate) struct Camera {
+ position: Vec2,
+}
+
+#[derive(Debug)]
+pub(crate) struct Spritesheet {
+ meeple: Sprite,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Sprite {
+ texture: Texture2D,
+ size: Vec2,
+}
+
+impl Sprite {
+ fn render(&self, position: Vec2) {
+ draw_texture_ex(
+ &self.texture,
+ position.x,
+ position.y,
+ WHITE,
+ DrawTextureParams {
+ dest_size: Some(self.size),
+ ..Default::default()
+ },
+ );
+ }
+}
+
+impl From<Image> for Sprite {
+ fn from(value: Image) -> Self {
+ const TEXTURE_SCALE: f32 = 10.;
+ let texture = Texture2D::from_image(&value);
+ let size = Vec2::new(
+ texture.width() / TEXTURE_SCALE,
+ texture.height() / TEXTURE_SCALE,
+ );
+ Sprite { texture, size }
+ }
+}
+
+pub(crate) fn load_sprites() -> Spritesheet {
+ Spritesheet {
+ meeple: Image::from_file_with_format(
+ include_bytes!("../assets/meeple.png"),
+ Some(ImageFormat::Png),
+ )
+ .unwrap()
+ .into(),
+ }
+}
+
+pub(crate) fn render(camera: &Camera, game: &GameState, spritesheet: &Spritesheet) {
+ clear_background(MAGENTA);
+ for (name, player) in game.players.iter() {
+ let sprite = match player.sprite_index {
+ SpriteIndex::Meeple => spritesheet.meeple.clone(),
+ };
+ let position_camera = player.position - camera.position;
+ sprite.render(position_camera);
+ draw_text(
+ name,
+ position_camera.x + 12.,
+ position_camera.y + sprite.size.y + 16.,
+ 16.,
+ BLACK,
+ );
+ }
+}
+
+pub(crate) fn follow_local_player(camera: &mut Camera, game: &GameState, local_player_name: &str) {
+ let screen_center = Vec2::new(screen_width() / 2., screen_height() / 2.);
+ if let Some((_, player)) = game
+ .players
+ .iter()
+ .find(|(name, _)| *name == local_player_name)
+ {
+ camera.position = player.position - screen_center;
+ }
+}
diff --git a/common/Cargo.toml b/common/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "common"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.104"
+glam = { version = "0.27.0", features = ["scalar-math", "serde"] }
+postcard = { version = "1.1.3", default-features = false, features = ["use-std"] }
+serde = { version = "1.0.229", features = ["derive"] }
diff --git a/common/src/codec.rs b/common/src/codec.rs
@@ -0,0 +1,65 @@
+#[derive(Default, Debug)]
+enum State {
+ #[default]
+ Start,
+ Data(u8),
+ Escape,
+ End,
+}
+
+impl State {
+ const DELIMITER: u8 = 0xff;
+ const ESCAPE: u8 = 0xfe;
+ fn next(&mut self, input: u8) {
+ *self = match (input, &self) {
+ (Self::DELIMITER, State::Data(_) | State::Start | State::End) => State::End,
+ (Self::ESCAPE, State::Data(_) | State::Start | State::End) => State::Escape,
+ (d, _) => State::Data(d),
+ };
+ }
+}
+
+#[derive(Default, Debug)]
+pub struct Codec {
+ state: State,
+ message: Vec<u8>,
+}
+
+impl Codec {
+ fn next(&mut self, input: u8) -> Option<Vec<u8>> {
+ self.state.next(input);
+ match self.state {
+ State::Data(d) => {
+ self.message.push(d);
+ None
+ }
+ State::End => {
+ let complete = self.message.clone();
+ self.message.clear();
+ Some(complete)
+ }
+ State::Start | State::Escape => None,
+ }
+ }
+ pub fn decode(&mut self, input: &[u8]) -> Vec<Vec<u8>> {
+ let mut output = vec![];
+ for byte in input {
+ if let Some(message) = self.next(*byte) {
+ output.push(message);
+ }
+ }
+ output
+ }
+ pub fn encode(input: &[u8]) -> Vec<u8> {
+ let mut output = vec![];
+ for byte in input {
+ match *byte {
+ State::DELIMITER => output.extend_from_slice(&[State::ESCAPE, State::DELIMITER]),
+ State::ESCAPE => output.extend_from_slice(&[State::ESCAPE, State::ESCAPE]),
+ b => output.push(b),
+ }
+ }
+ output.push(State::DELIMITER);
+ output
+ }
+}
diff --git a/common/src/game.rs b/common/src/game.rs
@@ -0,0 +1,50 @@
+use std::{collections::HashMap, time::SystemTime};
+
+use glam::Vec2;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct GameState {
+ pub timestamp: SystemTime,
+ pub players: HashMap<String, Player>,
+}
+
+impl Default for GameState {
+ fn default() -> Self {
+ Self {
+ timestamp: SystemTime::now(),
+ players: Default::default(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Player {
+ pub name: String,
+ pub position: Vec2,
+ pub sprite_index: SpriteIndex,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct PlayerAction {
+ pub player_name: String,
+ pub kind: PlayerActionKind,
+}
+
+impl PlayerAction {
+ pub fn new(player_name: String, kind: PlayerActionKind) -> Self {
+ PlayerAction { player_name, kind }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum PlayerActionKind {
+ Join,
+ Move(Vec2),
+ Leave,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum SpriteIndex {
+ Meeple,
+}
diff --git a/common/src/lib.rs b/common/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod codec;
+pub mod game;
diff --git a/flake.lock b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "root": {
+ "inputs": {
+ "stable-pkgs": "stable-pkgs"
+ }
+ },
+ "stable-pkgs": {
+ "locked": {
+ "lastModified": 1785989512,
+ "narHash": "sha256-HFQhkQcl5D1hUNoen3SGHCSFCt2Bg6uP+HgbrnA3InQ=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "445d861c6d31b4af0c79d8d4be2331f762a361d7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-26.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
@@ -0,0 +1,41 @@
+{
+ inputs = {
+ stable-pkgs.url = "github:nixos/nixpkgs?ref=nixos-26.05";
+ };
+
+ outputs =
+ { self, stable-pkgs }:
+ let
+ system = "x86_64-linux";
+ stable = import stable-pkgs {
+ inherit system;
+ config = { };
+ overlays = [ ];
+ };
+ libInputs = with stable; [
+ libX11
+ libxkbcommon
+ libXi
+ libXcursor
+ libXrandr
+ libXxf86vm
+ libGL
+ ];
+ lib-path = with stable; lib.makeLibraryPath libInputs;
+ in
+ {
+ devShells.${system}.default = stable.mkShellNoCC {
+ packages = with stable; [
+ cargo
+ clippy
+ rust-analyzer
+ rustc
+ rustfmt
+ ];
+
+ shellHook = ''
+ export "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${lib-path}"
+ '';
+ };
+ };
+}
diff --git a/server/Cargo.toml b/server/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "server"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.104"
+common = { path = "../common/" }
+glam = { version = "0.27.0", features = ["scalar-math", "serde"] }
+mio = { version = "1.2.2", features = ["net", "os-poll"] }
+postcard = { version = "1.1.3", default-features = false, features = ["use-std"] }
+serde = { version = "1.0.229", features = ["derive"] }
diff --git a/server/src/main.rs b/server/src/main.rs
@@ -0,0 +1,79 @@
+use std::{
+ collections::LinkedList,
+ sync::{Arc, Mutex, mpsc::channel},
+ thread,
+ time::{Duration, Instant},
+};
+
+use common::{
+ codec::Codec,
+ game::{GameState, Player, PlayerActionKind, SpriteIndex},
+};
+use glam::Vec2;
+use postcard::to_stdvec;
+
+use crate::net::listen;
+
+pub mod net;
+
+const TICK_DURATION: Duration = Duration::from_millis(10);
+
+fn main() {
+ let handlers = Arc::new(Mutex::new(LinkedList::new()));
+ let (handler_to_main_tx, handler_to_main_rx) = channel();
+ let handlers_clone = handlers.clone();
+ let _listener = thread::spawn(|| listen(handlers_clone, handler_to_main_tx));
+ let mut next_tick = Instant::now() + TICK_DURATION;
+ let mut game = GameState::default();
+ loop {
+ let mut send_game_status = false;
+
+ // Process all pending player actions
+ for player_action in handler_to_main_rx.try_iter() {
+ send_game_status = true;
+ match player_action.kind {
+ PlayerActionKind::Join => {
+ game.players.insert(
+ player_action.player_name.clone(),
+ Player {
+ name: player_action.player_name.clone(),
+ position: Vec2::ZERO,
+ sprite_index: SpriteIndex::Meeple,
+ },
+ );
+ }
+ PlayerActionKind::Move(vec2) => {
+ if let Some(player) = game.players.get_mut(&player_action.player_name) {
+ player.position += vec2;
+ }
+ }
+ PlayerActionKind::Leave => {
+ game.players.remove(&player_action.player_name);
+ }
+ }
+ }
+
+ // Only bother to send the latest game status to clients if we really need to, instead of
+ // every tick
+ if send_game_status {
+ match handlers.lock() {
+ Ok(mut h) => {
+ let game_bytes = Codec::encode(&to_stdvec(&game).unwrap());
+ for handler in h.iter() {
+ let _ = handler.tx.send(game_bytes.clone());
+ }
+ h.extract_if(|handler| handler.thread.is_finished())
+ .for_each(drop);
+ }
+ Err(_) => break,
+ }
+ }
+
+ // Throttling
+ let now = Instant::now();
+ if now < next_tick {
+ thread::sleep(next_tick - now);
+ }
+ next_tick = Instant::now() + TICK_DURATION;
+ }
+}
diff --git a/server/src/net.rs b/server/src/net.rs
@@ -0,0 +1,117 @@
+use std::{
+ collections::LinkedList,
+ io::{self, Read, Write},
+ net::SocketAddr,
+ sync::{
+ Arc, Mutex,
+ mpsc::{Receiver, Sender, channel},
+ },
+ thread::{self, JoinHandle},
+ time::Duration,
+};
+
+use common::{codec::Codec, game::PlayerAction};
+use mio::{
+ Events, Interest, Poll, Token,
+ net::{TcpListener, TcpStream},
+};
+
+const TOKEN: Token = Token(0);
+
+#[derive(Debug)]
+pub(crate) struct Handler {
+ _addr: SocketAddr,
+ pub thread: JoinHandle<()>,
+ pub tx: Sender<Vec<u8>>,
+}
+
+pub(crate) fn listen(handlers: Arc<Mutex<LinkedList<Handler>>>, tx: Sender<PlayerAction>) {
+ let mut poll = Poll::new().unwrap();
+ let mut events = Events::with_capacity(128);
+ let mut listener = TcpListener::bind("0.0.0.0:6767".parse().unwrap()).unwrap();
+ poll.registry()
+ .register(&mut listener, TOKEN, Interest::READABLE)
+ .unwrap();
+ loop {
+ poll.poll(&mut events, None).unwrap();
+ for _ in events.iter() {
+ let (stream, _addr) = listener.accept().unwrap();
+ let (main_to_handler_tx, main_to_handler_rx) = channel();
+ if let Ok(addr) = stream.peer_addr() {
+ let tx_clone = tx.clone();
+ let thread =
+ thread::spawn(|| handle_connection(stream, main_to_handler_rx, tx_clone));
+ let handler = Handler {
+ _addr: addr,
+ thread,
+ tx: main_to_handler_tx.clone(),
+ };
+ handlers.lock().unwrap().push_back(handler);
+ }
+ }
+ }
+}
+
+fn handle_connection(mut stream: TcpStream, rx: Receiver<Vec<u8>>, tx: Sender<PlayerAction>) {
+ let poll = Poll::new().unwrap();
+ poll.registry()
+ .register(&mut stream, TOKEN, Interest::READABLE)
+ .unwrap();
+ let shared_stream = Arc::new(Mutex::new(stream));
+ let shared_stream_read = shared_stream.clone();
+ let shared_stream_write = shared_stream.clone();
+ let read_thread = thread::spawn(|| handle_client_read(poll, shared_stream_read, tx));
+ let write_thread = thread::spawn(|| handle_client_write(shared_stream_write, rx));
+ while !read_thread.is_finished() && !write_thread.is_finished() {
+ thread::sleep(Duration::from_millis(500));
+ }
+}
+
+fn handle_client_read(mut poll: Poll, stream: Arc<Mutex<TcpStream>>, tx: Sender<PlayerAction>) {
+ let mut events = Events::with_capacity(128);
+ let mut codec = Codec::default();
+ let mut buffer = [0x00; 1024];
+ let mut buffer_index;
+ loop {
+ poll.poll(&mut events, None).unwrap();
+ for event in events.iter() {
+ if event.is_readable() {
+ let mut stream = stream.lock().unwrap();
+ match stream.read(&mut buffer) {
+ Ok(0) => return,
+ Ok(i) => {
+ buffer_index = i;
+ let actions = codec.decode(&buffer[..buffer_index]);
+ for action in actions {
+ match postcard::from_bytes(action.as_slice()) {
+ Ok(a) => {
+ if let Err(_) = tx.send(a) {
+ break;
+ }
+ }
+ Err(_) => (),
+ }
+ }
+ }
+ Err(e) => match e.kind() {
+ io::ErrorKind::WouldBlock => (),
+ _ => return,
+ },
+ }
+ }
+ }
+ }
+}
+
+fn handle_client_write(stream: Arc<Mutex<TcpStream>>, rx: Receiver<Vec<u8>>) {
+ loop {
+ match rx.recv() {
+ Ok(bytes) => {
+ if let Err(_) = stream.lock().unwrap().write_all(&bytes) {
+ return;
+ }
+ }
+ Err(_) => return,
+ }
+ }
+}