~starkingdoms/starkingdoms

e01e242b99ea5a57d03e8551a2dc0dc176766420 — core 2 years ago 219a8aa + 4e12d1b
Merge branch 'feat/typescript' into 'master'

Rewrite the client in typescript

See merge request starkingdoms.tk/starkingdoms.tk!2
47 files changed, 3178 insertions(+), 1578 deletions(-)

M Cargo.lock
M Cargo.toml
A assets/dist/spritesheet-125.json
M assets/dist/spritesheet-125.png
D assets/dist/spritesheet-125.ron
A assets/dist/spritesheet-375.json
M assets/dist/spritesheet-375.png
D assets/dist/spritesheet-375.ron
A assets/dist/spritesheet-full.json
M assets/dist/spritesheet-full.png
D assets/dist/spritesheet-full.ron
A client/.gitignore
D client/Cargo.toml
A client/assets
A client/hearty.png
A client/index.html
A client/package.json
A client/play.html
D client/src/chat.rs
A client/src/gateway.ts
A client/src/index.ts
D client/src/lib.rs
A client/src/logger.ts
D client/src/macros.rs
A client/src/protocol/goodbye_reason.ts
A client/src/protocol/message_c2s.ts
A client/src/protocol/message_s2c.ts
A client/src/protocol/planet.ts
A client/src/protocol/player.ts
A client/src/protocol/starkingdoms-protocol.ts
A client/src/protocol/state.ts
D client/src/rendering/mod.rs
D client/src/rendering/renderer_canvascentric.rs
D client/src/rendering/renderer_playercentric.rs
D client/src/rendering/util.rs
A client/src/serde.ts
D client/src/textures/loader_fast.rs
D client/src/textures/loader_slow.rs
D client/src/textures/mod.rs
A client/src/vite-env.d.ts
A client/static/index.css
A client/static/play.css
A client/tsconfig.json
A client/yarn.lock
M server/src/handler.rs
M spacetime
M spacetime_py/spacetime.py
M Cargo.lock => Cargo.lock +1 -576
@@ 3,12 3,6 @@
version = 3

[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 39,39 33,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"

[[package]]
name = "async-recursion"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 2.0.14",
]

[[package]]
name = "async-trait"
version = "0.1.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 2.0.14",
]

[[package]]
name = "async_io_stream"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c"
dependencies = [
 "futures",
 "pharos",
 "rustc_version",
]

[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 95,24 56,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"

[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"

[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"

[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 128,12 77,6 @@ dependencies = [
]

[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"

[[package]]
name = "bytemuck"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 196,12 139,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"

[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 213,27 150,6 @@ dependencies = [
]

[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
 "cfg-if",
 "wasm-bindgen",
]

[[package]]
name = "console_log"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f"
dependencies = [
 "log",
 "wasm-bindgen",
 "web-sys",
]

[[package]]
name = "cpufeatures"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 243,15 159,6 @@ dependencies = [
]

[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
 "cfg-if",
]

[[package]]
name = "crossbeam"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 319,12 226,6 @@ dependencies = [
]

[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"

[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 378,22 279,6 @@ dependencies = [
]

[[package]]
name = "exr"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4"
dependencies = [
 "bit_field",
 "flume",
 "half",
 "lebe",
 "miniz_oxide 0.6.2",
 "rayon-core",
 "smallvec",
 "zune-inflate",
]

[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 403,38 288,6 @@ dependencies = [
]

[[package]]
name = "fdeflate"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
dependencies = [
 "simd-adler32",
]

[[package]]
name = "flate2"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
 "crc32fast",
 "miniz_oxide 0.6.2",
]

[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [
 "futures-core",
 "futures-sink",
 "nanorand",
 "pin-project",
 "spin",
]

[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 457,7 310,6 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-executor",
 "futures-io",
 "futures-sink",
 "futures-task",


@@ 481,34 333,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"

[[package]]
name = "futures-executor"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
 "futures-core",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-io"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"

[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 2.0.14",
]

[[package]]
name = "futures-sink"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 526,13 356,9 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-macro",
 "futures-sink",
 "futures-task",
 "memchr",
 "pin-project-lite",
 "pin-utils",
 "slab",


@@ 555,20 381,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
 "cfg-if",
 "js-sys",
 "libc",
 "wasi",
 "wasm-bindgen",
]

[[package]]
name = "gif"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
dependencies = [
 "color_quant",
 "weezl",
]

[[package]]


@@ 591,15 405,6 @@ dependencies = [
]

[[package]]
name = "half"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
dependencies = [
 "crunchy",
]

[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 698,25 503,6 @@ dependencies = [
]

[[package]]
name = "image"
version = "0.24.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
dependencies = [
 "bytemuck",
 "byteorder",
 "color_quant",
 "exr",
 "gif",
 "jpeg-decoder",
 "num-rational",
 "num-traits",
 "png",
 "qoi",
 "tiff",
]

[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 753,36 539,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"

[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
dependencies = [
 "rayon",
]

[[package]]
name = "js-sys"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
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 = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"

[[package]]
name = "libc"
version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 801,16 563,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"

[[package]]
name = "lock_api"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
 "autocfg",
 "scopeguard",
]

[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 820,15 572,6 @@ dependencies = [
]

[[package]]
name = "markdown"
version = "1.0.0-alpha.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de49c677e95e00eaa74c42a0b07ea55e1e0b1ebca5b2cbc7657f288cd714eb"
dependencies = [
 "unicode-id",
]

[[package]]
name = "matrixmultiply"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 853,25 596,6 @@ dependencies = [
]

[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
 "adler",
]

[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
 "adler",
 "simd-adler32",
]

[[package]]
name = "mio"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 911,15 635,6 @@ dependencies = [
]

[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
 "getrandom",
]

[[package]]
name = "num-complex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1035,36 750,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"

[[package]]
name = "pharos"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414"
dependencies = [
 "futures",
 "rustc_version",
]

[[package]]
name = "pin-project"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
dependencies = [
 "pin-project-internal",
]

[[package]]
name = "pin-project-internal"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 1.0.109",
]

[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1077,19 762,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"

[[package]]
name = "png"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa"
dependencies = [
 "bitflags",
 "crc32fast",
 "fdeflate",
 "flate2",
 "miniz_oxide 0.7.1",
]

[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1193,15 865,6 @@ dependencies = [
]

[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
 "bytemuck",
]

[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1268,28 931,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"

[[package]]
name = "rayon"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
dependencies = [
 "either",
 "rayon-core",
]

[[package]]
name = "rayon-core"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
dependencies = [
 "crossbeam-channel",
 "crossbeam-deque",
 "crossbeam-utils",
 "num_cpus",
]

[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1316,60 957,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"

[[package]]
name = "rmp"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f"
dependencies = [
 "byteorder",
 "num-traits",
 "paste",
]

[[package]]
name = "rmp-serde"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e"
dependencies = [
 "byteorder",
 "rmp",
 "serde",
]

[[package]]
name = "robust"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5864e7ef1a6b7bcf1d6ca3f655e65e724ed3b52546a0d0a663c991522f552ea"

[[package]]
name = "ron"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
dependencies = [
 "base64 0.13.1",
 "bitflags",
 "serde",
]

[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"

[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
 "semver",
]

[[package]]
name = "rustix"
version = "0.37.11"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1414,12 1013,6 @@ dependencies = [
]

[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"

[[package]]
name = "serde"
version = "1.0.160"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1475,12 1068,6 @@ dependencies = [
]

[[package]]
name = "simd-adler32"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"

[[package]]
name = "simple_logger"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1531,41 1118,6 @@ dependencies = [
]

[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
 "lock_api",
]

[[package]]
name = "starkingdoms-client"
version = "0.1.0"
dependencies = [
 "async-recursion",
 "async-trait",
 "base64 0.21.0",
 "console_error_panic_hook",
 "console_log",
 "futures",
 "image",
 "js-sys",
 "lazy_static",
 "log",
 "markdown",
 "rmp-serde",
 "ron",
 "serde",
 "starkingdoms-protocol",
 "url",
 "wasm-bindgen",
 "wasm-bindgen-futures",
 "web-sys",
 "ws_stream_wasm",
]

[[package]]
name = "starkingdoms-protocol"
version = "0.1.0"
dependencies = [


@@ 1650,17 1202,6 @@ dependencies = [
]

[[package]]
name = "tiff"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
dependencies = [
 "flate2",
 "jpeg-decoder",
 "weezl",
]

[[package]]
name = "time"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1796,7 1337,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
dependencies = [
 "base64 0.13.1",
 "base64",
 "byteorder",
 "bytes",
 "http",


@@ 1822,12 1363,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"

[[package]]
name = "unicode-id"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a"

[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1888,88 1423,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

[[package]]
name = "wasm-bindgen"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
dependencies = [
 "cfg-if",
 "wasm-bindgen-macro",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
dependencies = [
 "bumpalo",
 "log",
 "once_cell",
 "proc-macro2",
 "quote",
 "syn 1.0.109",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-futures"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
dependencies = [
 "cfg-if",
 "js-sys",
 "wasm-bindgen",
 "web-sys",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",
]

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 1.0.109",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"

[[package]]
name = "web-sys"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
dependencies = [
 "js-sys",
 "wasm-bindgen",
]

[[package]]
name = "weezl"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"

[[package]]
name = "which"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 2158,31 1611,3 @@ name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"

[[package]]
name = "ws_stream_wasm"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5"
dependencies = [
 "async_io_stream",
 "futures",
 "js-sys",
 "log",
 "pharos",
 "rustc_version",
 "send_wrapper",
 "thiserror",
 "wasm-bindgen",
 "wasm-bindgen-futures",
 "web-sys",
]

[[package]]
name = "zune-inflate"
version = "0.2.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "440a08fd59c6442e4b846ea9b10386c38307eae728b216e1ab2c305d1c9daaf8"
dependencies = [
 "simd-adler32",
]

M Cargo.toml => Cargo.toml +0 -1
@@ 1,6 1,5 @@
[workspace]
members = [
    "server",
    "client",
    "protocol"
]
\ No newline at end of file

A assets/dist/spritesheet-125.json => assets/dist/spritesheet-125.json +192 -0
@@ 0,0 1,192 @@
{
  "frames": {
    "earth.png": {
      "frame": { "x": 0, "y": 0, "w": 256, "h": 256 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 256, "h": 256 },
      "sourceSize": { "w": 256, "h": 256 },
      "pivot": { "x": 128, "y": 128 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 256, "h": 256 }
    },
    "starfield.png": {
      "frame": { "x": 0, "y": 256, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hearty.png": {
      "frame": { "x": 0, "y": 320, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 384, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 448, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "superthruster_on.png": {
      "frame": { "x": 0, "y": 512, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 0, "y": 576, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 0, "y": 640, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "thruster_on.png": {
      "frame": { "x": 0, "y": 704, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingleg.png": {
      "frame": { "x": 0, "y": 768, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "powerhub_on.png": {
      "frame": { "x": 0, "y": 832, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hub_on.png": {
      "frame": { "x": 0, "y": 896, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "autoplr_on.png": {
      "frame": { "x": 0, "y": 960, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "superthruster_off.png": {
      "frame": { "x": 0, "y": 1024, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 0, "y": 1088, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "thruster_off.png": {
      "frame": { "x": 0, "y": 1152, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "cargo_on.png": {
      "frame": { "x": 0, "y": 1216, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "cargo_off.png": {
      "frame": { "x": 0, "y": 1280, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "powerhub_off.png": {
      "frame": { "x": 0, "y": 1344, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    },
    "hub_off.png": {
      "frame": { "x": 0, "y": 1408, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 32, "y": 32 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 64, "h": 64 }
    }
  },
  "meta": {
    "app": "atlasify https://github.com/soimy/atlasify#readme",
    "version": "0.4.1",
    "image": "spritesheet-125.png",
    "format": "RGBA8888",
    "size": { "w": 4096, "h": 4096 },
    "scale": 1
  }
}
\ No newline at end of file

M assets/dist/spritesheet-125.png => assets/dist/spritesheet-125.png +0 -0
D assets/dist/spritesheet-125.ron => assets/dist/spritesheet-125.ron +0 -1
@@ 1,1 0,0 @@
(texture_width:256,texture_height:1472,sprites:[(name:"earth",x:0,y:0,width:256,height:256,offsets:None,),(name:"ecothruster_on",x:0,y:256,width:64,height:64,offsets:None,),(name:"landingleg",x:0,y:320,width:64,height:64,offsets:None,),(name:"superthruster_on",x:0,y:384,width:64,height:64,offsets:None,),(name:"cargo_on",x:0,y:448,width:64,height:64,offsets:None,),(name:"autoplr_cfg",x:0,y:512,width:64,height:64,offsets:None,),(name:"landingthruster_off",x:0,y:576,width:64,height:64,offsets:None,),(name:"superthruster_off",x:0,y:640,width:64,height:64,offsets:None,),(name:"autoplr_on",x:0,y:704,width:64,height:64,offsets:None,),(name:"thruster_off",x:0,y:768,width:64,height:64,offsets:None,),(name:"cargo_off",x:0,y:832,width:64,height:64,offsets:None,),(name:"thruster_on",x:0,y:896,width:64,height:64,offsets:None,),(name:"autoplr_error",x:0,y:960,width:64,height:64,offsets:None,),(name:"hub_off",x:0,y:1024,width:64,height:64,offsets:None,),(name:"powerhub_off",x:0,y:1088,width:64,height:64,offsets:None,),(name:"hearty",x:0,y:1152,width:64,height:64,offsets:None,),(name:"hub_on",x:0,y:1216,width:64,height:64,offsets:None,),(name:"starfield",x:0,y:1280,width:64,height:64,offsets:None,),(name:"landingthruster_on",x:0,y:1344,width:64,height:64,offsets:None,),(name:"powerhub_on",x:0,y:1408,width:64,height:64,offsets:None,),],)
\ No newline at end of file

A assets/dist/spritesheet-375.json => assets/dist/spritesheet-375.json +192 -0
@@ 0,0 1,192 @@
{
  "frames": {
    "earth.png": {
      "frame": { "x": 0, "y": 0, "w": 768, "h": 768 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 768, "h": 768 },
      "sourceSize": { "w": 768, "h": 768 },
      "pivot": { "x": 384, "y": 384 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 768, "h": 768 }
    },
    "starfield.png": {
      "frame": { "x": 0, "y": 768, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hearty.png": {
      "frame": { "x": 0, "y": 960, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 1152, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 1344, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "superthruster_on.png": {
      "frame": { "x": 0, "y": 1536, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 0, "y": 1728, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 0, "y": 1920, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "thruster_on.png": {
      "frame": { "x": 0, "y": 2112, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingleg.png": {
      "frame": { "x": 0, "y": 2304, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "powerhub_on.png": {
      "frame": { "x": 0, "y": 2496, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hub_on.png": {
      "frame": { "x": 0, "y": 2688, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "autoplr_on.png": {
      "frame": { "x": 0, "y": 2880, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 0, "y": 3072, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "superthruster_off.png": {
      "frame": { "x": 0, "y": 3264, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "thruster_off.png": {
      "frame": { "x": 0, "y": 3456, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "cargo_on.png": {
      "frame": { "x": 0, "y": 3648, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "cargo_off.png": {
      "frame": { "x": 0, "y": 3840, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "powerhub_off.png": {
      "frame": { "x": 768, "y": 0, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    },
    "hub_off.png": {
      "frame": { "x": 960, "y": 0, "w": 192, "h": 192 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 192, "h": 192 },
      "sourceSize": { "w": 192, "h": 192 },
      "pivot": { "x": 96, "y": 96 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 192, "h": 192 }
    }
  },
  "meta": {
    "app": "atlasify https://github.com/soimy/atlasify#readme",
    "version": "0.4.1",
    "image": "spritesheet-375.png",
    "format": "RGBA8888",
    "size": { "w": 4096, "h": 4096 },
    "scale": 1
  }
}
\ No newline at end of file

M assets/dist/spritesheet-375.png => assets/dist/spritesheet-375.png +0 -0
D assets/dist/spritesheet-375.ron => assets/dist/spritesheet-375.ron +0 -1
@@ 1,1 0,0 @@
(texture_width:768,texture_height:4224,sprites:[(name:"earth",x:0,y:0,width:768,height:768,offsets:None,),(name:"ecothruster_on",x:0,y:768,width:192,height:192,offsets:None,),(name:"landingleg",x:0,y:960,width:192,height:192,offsets:None,),(name:"superthruster_on",x:0,y:1152,width:192,height:192,offsets:None,),(name:"cargo_on",x:0,y:1344,width:192,height:192,offsets:None,),(name:"autoplr_cfg",x:0,y:1536,width:192,height:192,offsets:None,),(name:"landingthruster_off",x:0,y:1728,width:192,height:192,offsets:None,),(name:"superthruster_off",x:0,y:1920,width:192,height:192,offsets:None,),(name:"autoplr_on",x:0,y:2112,width:192,height:192,offsets:None,),(name:"thruster_off",x:0,y:2304,width:192,height:192,offsets:None,),(name:"cargo_off",x:0,y:2496,width:192,height:192,offsets:None,),(name:"thruster_on",x:0,y:2688,width:192,height:192,offsets:None,),(name:"autoplr_error",x:0,y:2880,width:192,height:192,offsets:None,),(name:"hub_off",x:0,y:3072,width:192,height:192,offsets:None,),(name:"powerhub_off",x:0,y:3264,width:192,height:192,offsets:None,),(name:"hearty",x:0,y:3456,width:192,height:192,offsets:None,),(name:"hub_on",x:0,y:3648,width:192,height:192,offsets:None,),(name:"starfield",x:0,y:3840,width:192,height:192,offsets:None,),(name:"landingthruster_on",x:0,y:4032,width:192,height:192,offsets:None,),(name:"powerhub_on",x:192,y:768,width:192,height:192,offsets:None,),],)
\ No newline at end of file

A assets/dist/spritesheet-full.json => assets/dist/spritesheet-full.json +192 -0
@@ 0,0 1,192 @@
{
  "frames": {
    "earth.png": {
      "frame": { "x": 0, "y": 0, "w": 2048, "h": 2048 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 2048, "h": 2048 },
      "sourceSize": { "w": 2048, "h": 2048 },
      "pivot": { "x": 1024, "y": 1024 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 2048, "h": 2048 }
    },
    "starfield.png": {
      "frame": { "x": 0, "y": 2048, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hearty.png": {
      "frame": { "x": 0, "y": 2560, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "autoplr_error.png": {
      "frame": { "x": 0, "y": 3072, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "autoplr_cfg.png": {
      "frame": { "x": 0, "y": 3584, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "superthruster_on.png": {
      "frame": { "x": 2048, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "ecothruster_on.png": {
      "frame": { "x": 2560, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingthruster_on.png": {
      "frame": { "x": 3072, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "thruster_on.png": {
      "frame": { "x": 3584, "y": 0, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingleg.png": {
      "frame": { "x": 512, "y": 2048, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "powerhub_on.png": {
      "frame": { "x": 512, "y": 2560, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hub_on.png": {
      "frame": { "x": 512, "y": 3072, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "autoplr_on.png": {
      "frame": { "x": 512, "y": 3584, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "superthruster_off.png": {
      "frame": { "x": 2048, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "landingthruster_off.png": {
      "frame": { "x": 2560, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "thruster_off.png": {
      "frame": { "x": 3072, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "cargo_on.png": {
      "frame": { "x": 3584, "y": 512, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "cargo_off.png": {
      "frame": { "x": 1024, "y": 2048, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "powerhub_off.png": {
      "frame": { "x": 1024, "y": 2560, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    },
    "hub_off.png": {
      "frame": { "x": 1024, "y": 3072, "w": 512, "h": 512 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 512, "h": 512 },
      "sourceSize": { "w": 512, "h": 512 },
      "pivot": { "x": 256, "y": 256 },
      "9slicedFrame": { "x": 0, "y": 0, "w": 512, "h": 512 }
    }
  },
  "meta": {
    "app": "atlasify https://github.com/soimy/atlasify#readme",
    "version": "0.4.1",
    "image": "spritesheet-full.png",
    "format": "RGBA8888",
    "size": { "w": 4096, "h": 4096 },
    "scale": 1
  }
}
\ No newline at end of file

M assets/dist/spritesheet-full.png => assets/dist/spritesheet-full.png +0 -0
D assets/dist/spritesheet-full.ron => assets/dist/spritesheet-full.ron +0 -1
@@ 1,1 0,0 @@
(texture_width:3584,texture_height:4096,sprites:[(name:"earth",x:0,y:0,width:2048,height:2048,offsets:None,),(name:"ecothruster_on",x:0,y:2048,width:512,height:512,offsets:None,),(name:"landingleg",x:0,y:2560,width:512,height:512,offsets:None,),(name:"superthruster_on",x:0,y:3072,width:512,height:512,offsets:None,),(name:"cargo_on",x:0,y:3584,width:512,height:512,offsets:None,),(name:"autoplr_cfg",x:512,y:2048,width:512,height:512,offsets:None,),(name:"landingthruster_off",x:512,y:2560,width:512,height:512,offsets:None,),(name:"superthruster_off",x:512,y:3072,width:512,height:512,offsets:None,),(name:"autoplr_on",x:512,y:3584,width:512,height:512,offsets:None,),(name:"thruster_off",x:1024,y:2048,width:512,height:512,offsets:None,),(name:"cargo_off",x:1024,y:2560,width:512,height:512,offsets:None,),(name:"thruster_on",x:1024,y:3072,width:512,height:512,offsets:None,),(name:"autoplr_error",x:1024,y:3584,width:512,height:512,offsets:None,),(name:"hub_off",x:1536,y:2048,width:512,height:512,offsets:None,),(name:"powerhub_off",x:1536,y:2560,width:512,height:512,offsets:None,),(name:"hearty",x:1536,y:3072,width:512,height:512,offsets:None,),(name:"hub_on",x:1536,y:3584,width:512,height:512,offsets:None,),(name:"starfield",x:2048,y:0,width:512,height:512,offsets:None,),(name:"landingthruster_on",x:2560,y:0,width:512,height:512,offsets:None,),(name:"powerhub_on",x:3072,y:0,width:512,height:512,offsets:None,),],)
\ No newline at end of file

A client/.gitignore => client/.gitignore +24 -0
@@ 0,0 1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

D client/Cargo.toml => client/Cargo.toml +0 -62
@@ 1,62 0,0 @@
[package]
name = "starkingdoms-client"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
console_log = { version = "1", features = ["color"] }
log = "0.4"
futures = { version = "0.3", default-features = false }
wasm-bindgen-futures = "0.4"
url = "2.3"
starkingdoms-protocol = { version = "0.1.0", path = "../protocol" }
rmp-serde = "1.1"
ws_stream_wasm = "0.7"
serde = { version = "1", features = ["derive"] }
lazy_static = "1.4"
markdown = "1.0.0-alpha.7" # DO NOT DOWNGRADE
async-recursion = "1"
console_error_panic_hook = "0.1"
async-trait = "0.1.68"
image = { version = "0.24.6", optional = true }
ron = { version = "0.8", optional = true }
base64 = { version = "0.21.0", optional = true }

[dependencies.web-sys]
version = "0.3.4"
features = [
    'Document',
    'Element',
    'HtmlCanvasElement',
    'Window',
    'CanvasRenderingContext2d',
    'HtmlImageElement',
    'SvgImageElement',
    'HtmlVideoElement',
    'ImageBitmap',
    'OffscreenCanvas',
    'VideoFrame',
    'CanvasWindingRule',
    'Path2d',
    'CanvasPattern',
    'CanvasGradient',
    'HitRegionOptions',
    'ImageData',
    'TextMetrics',
    'DomMatrix',
    'CssStyleDeclaration'
]

[features]
textures-slow = []
textures-fast = ['image', 'ron', 'base64']

renderer-canvascentric = []
renderer-playercentric = []
\ No newline at end of file

A client/assets => client/assets +1 -0
@@ 0,0 1,1 @@
../assets
\ No newline at end of file

A client/hearty.png => client/hearty.png +0 -0
A client/index.html => client/index.html +58 -0
@@ 0,0 1,58 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
    <meta charset="utf-8" />
    <title>StarKingdoms.TK</title>
    <link rel="stylesheet" href="./static/index.css"/>
</head>

<body>
<h1>StarKingdoms</h1>
<fieldset class="joingamebox">
    <legend>Join Game</legend>
    <form action="/play.html" method="GET">
        <label for="server">Choose server</label>
        <select class="joingamecontent" name="server" id="server">
            <!-- Dynamically filled by the JS later in this file -->
        </select>
        <br>
        <label for="textures">Texture quality</label>
        <select class="joingamecontent" name="textures" id="textures">
            <option value="full">Full quality (May impact loading times)</option>
            <option selected value="375">Medium quality (Recommended)</option>
            <option value="125">Low quality</option>
        </select>
        <br>
        <label for="username">Username</label>
        <br>
        <input class="m-5px" type="text" name="username" id="username" required />
        <br>
        <button class="m-5px w-full">Launch!</button>
    </form>
</fieldset>

<script>

    let servers = ["localhost:3000"];

    function server_url_to_ping_url(server) { return "http://" + server + "/ping" }
    function server_url_to_gateway_url(server) { return "ws://" + server + "/ws" }

    function load_server(server) {
        // ping the server to get server information
        fetch(server_url_to_ping_url(server)).then(response => {
            response.json().then(response => {
                let elem = document.createElement("option");
                elem.value = server_url_to_gateway_url(server);
                elem.text = `${response.description} - ${response.version.name} - ${response.players} online`;
                document.getElementById("server").appendChild(elem);
            })
        })
    }

    for (let i = 0; i < servers.length; i++) {
        load_server(servers[i]);
    }
</script>
</body>
</html>

A client/package.json => client/package.json +18 -0
@@ 0,0 1,18 @@
{
  "name": "starkingdoms-client",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "protobuf": "mkdir -p ./src/protocol && protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/protocol -I ../protocol/src/pbuf ../protocol/src/pbuf/starkingdoms-protocol.proto"
  },
  "devDependencies": {
    "ts-proto": "^1.146.0",
    "typescript": "^4.9.3",
    "vite": "^4.2.0"
  },
  "dependencies": {}
}

A client/play.html => client/play.html +14 -0
@@ 0,0 1,14 @@
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>StarKingdoms</title>
    <link rel="stylesheet" href="./static/play.css" />
  </head>
  <body>
    <script type="module" src="/src/index.ts"></script>

    <canvas id="canvas" class="renderbox"></canvas>
  </body>
</html>

D client/src/chat.rs => client/src/chat.rs +0 -25
@@ 1,25 0,0 @@
use std::error::Error;
use wasm_bindgen::prelude::*;
use starkingdoms_protocol::MessageC2S;
use crate::CLIENT;
use futures::SinkExt;
use starkingdoms_protocol::message_c2s::MessageC2SChat;

#[wasm_bindgen]
// TODO: Switch to async-aware mutexes
#[allow(clippy::await_holding_lock)]
pub async fn send_chat(message: &str) -> Result<(), JsError> {
    let client_data = &mut CLIENT.write()?.client_data;

    if let Some(data) = client_data {
        let msg = MessageC2S::Chat(MessageC2SChat {
            message: message.to_string(),
            special_fields: Default::default(),
        }).try_into().map_err(|e: Box<dyn Error> | JsError::new(&e.to_string()))?;
        send!(data.tx, msg).await?;
    } else {
        return Err(JsError::new("Client not yet connected to server"));
    }

    Ok(())
}
\ No newline at end of file

A client/src/gateway.ts => client/src/gateway.ts +168 -0
@@ 0,0 1,168 @@
import {Logger} from "./logger";
import {
    MessageC2SGoodbye, MessageC2SGoodbye_packetInfo,
    MessageC2SHello,
    MessageC2SHello_packetInfo, MessageC2SPing,
    MessageC2SPing_packetInfo
} from "./protocol/message_c2s";
import {State} from "./protocol/state";
import {decode, encode} from "./serde";
import {
    MessageS2CChat,
    MessageS2CChat_packetInfo,
    MessageS2CGoodbye,
    MessageS2CGoodbye_packetInfo,
    MessageS2CHello,
    MessageS2CHello_packetInfo,
    MessageS2CPlanetData, MessageS2CPlanetData_packetInfo,
    MessageS2CPlayersUpdate,
    MessageS2CPlayersUpdate_packetInfo,
    MessageS2CPong_packetInfo
} from "./protocol/message_s2c";
import {GoodbyeReason} from "./protocol/goodbye_reason";
import {global} from "./index";

const logger = new Logger("Gateway");

export interface GatewayClient {
    state: State;
    socket: WebSocket;
    username: string | null;
    version: number | null;
    ping_timeout: Timeout | null; // i am aware that these types dont exist
    ping_timeout_left: Timeout; // its fine
}

export async function gateway_connect(gateway_url: string, username: string): GatewayClient {
    logger.info("FAST CONNECT - Connecting to gateway socket at " + gateway_url);

    let ws = await _websocket_connect(gateway_url);

    logger.debug("[fastconnect] connected to gateway, performing handshake with server");

    let client: GatewayClient = {
        state: State.Handshake,
        socket: ws,
        username: null,
        version: null,
        ping_timeout: null,
        ping_timeout_left: null
    };

    let ping_timeout_fn = () => {
        logger.error("Server didn't send back pong in time.");
        let cya = MessageC2SGoodbye.encode({reason: GoodbyeReason.PingPongTimeout}).finish();
        client.socket.send(encode(MessageC2SGoodbye_packetInfo.type, cya));
        client.state = State.Handshake;
        client.username = null;
        client.version = null;
        client.socket.close();
    }
    client.ping_timeout_left = setTimeout(ping_timeout_fn, 10 * 1000);

    let ping_fn = () => {
        let ping = MessageC2SPing.encode({}).finish();
        client.socket.send(encode(MessageC2SPing_packetInfo.type, ping));
        setTimeout(ping_fn, 5 * 1000);
    }
    client.ping_timeout = setTimeout(ping_fn, 5 * 1000);

    let handshake_start_msg = MessageC2SHello.encode({
        version: 1,
        requestedUsername: username,
        nextState: State.Play
    }).finish();
    client.socket.send(encode(MessageC2SHello_packetInfo.type, handshake_start_msg));

    client.socket.addEventListener('message', async (msg) => {
        let pkt_info = decode(new Uint8Array(await msg.data.arrayBuffer()));
        let pkt_id = pkt_info[0];
        let pkt_data = pkt_info[1];

        if (pkt_id == 0) {
            // not a real message, skip
            return;
        }

        if (client.state == State.Handshake) {
            if (pkt_id == MessageS2CHello_packetInfo.type) {
                let pkt = MessageS2CHello.decode(pkt_data);

                logger.info(`FAST CONNECT - Handshake finished with server with protocol v${pkt.version}, assigned username ${pkt.givenUsername}, switching to state ${pkt.nextState}`);

                client.state = pkt.nextState;
                client.username = pkt.givenUsername;
                client.version = pkt.version;
            } else if (pkt_id == MessageS2CGoodbye_packetInfo.type) {
                let pkt = MessageS2CGoodbye.decode(pkt_data);

                logger.error(`Disconnected by server. Reason: ${pkt.reason}`);

                client.state = State.Handshake;
                client.username = null;
                client.version = null;

                throw "Disconnected by server";
            } else {
                logger.warn(`server sent unexpected packet ${pkt_id} for state Handshake`);
            }
        } else if (client.state == State.Play) {
            if (pkt_id == MessageS2CGoodbye_packetInfo.type) {
                let pkt = MessageS2CGoodbye.decode(pkt_data);

                logger.error(`Disconnected by server. Reason: ${pkt.reason}`);

                client.state = State.Handshake;
                client.username = null;
                client.version = null;

                throw "Disconnected by server";
            } else if (pkt_id == MessageS2CChat_packetInfo.type) {
                let pkt = MessageS2CChat.decode(pkt_data);

                logger.info(`CHAT: [${pkt.from}] ${pkt.message}`);
            } else if (pkt_id == MessageS2CPong_packetInfo.type) {
                clearTimeout(client.ping_timeout_left);
                client.ping_timeout_left = setTimeout(ping_timeout_fn, 10 * 1000);
            } else if (pkt_id == MessageS2CPlayersUpdate_packetInfo.type) {
                let pkt = MessageS2CPlayersUpdate.decode(pkt_data);
                global.players = pkt.players;

                for (let i = 0; i < pkt.players.length; i++) {
                    if (pkt.players[i].username == client.username) {
                        global.me = pkt.players[i];
                    }
                }
            } else if (pkt_id == MessageS2CPlanetData_packetInfo.type) {
                let pkt = MessageS2CPlanetData.decode(pkt_data);
                global.planets = pkt.planets;
            } else {
                logger.warn(`server sent unexpected packet ${pkt_id} for state Play`);
            }
        }
    });



    return client;
}

let socket: WebSocket | undefined = undefined;

function _websocket_connect(url: string): Promise<WebSocket> {
    if (socket && socket.readyState < 2) {
        // reuse socket connection
        return Promise.resolve(socket);
    }

    return new Promise((resolve, reject) => {
        socket = new WebSocket(url);
        socket.onopen = () => {
            // @ts-ignore if here, guaranteed that `socket` != undefined
            resolve(socket);
        }
        socket.onerror = (err) => {
            reject(err);
        }
    });
}
\ No newline at end of file

A client/src/index.ts => client/src/index.ts +170 -0
@@ 0,0 1,170 @@
import * as PIXI from "pixi.js";
import {Sprite} from "pixi.js";

import {Logger, logSetup} from "./logger";
import {gateway_connect, GatewayClient} from "./gateway";
import {Player} from "./protocol/player";
import {Planet, PlanetType} from "./protocol/planet";

logSetup();
const logger = new Logger("client");

export interface GlobalData {
    client: GatewayClient | null,
    players: Player[],
    planets: Planet[],
    me: Player | null,
    canvas: HTMLCanvasElement,
    spritesheet_img: HTMLImageElement | null,
    spritesheet: object | null,
    context: CanvasRenderingContext2D
}
export const global: GlobalData = {
    client: null,
    players: [],
    planets: [],
    me: null,
    canvas: <HTMLCanvasElement>document.getElementById("canvas"),
    spritesheet_img: null,
    spritesheet: null,
    context: <CanvasRenderingContext2D>(<HTMLCanvasElement>document.getElementById("canvas")).getContext("2d")
}

async function client_main(server: string, username: string, texture_quality: string) {
    logger.info("StarKingdoms client - starting");

    logger.info("Loading textures");
    let spritesheet_url = `/assets/dist/spritesheet-${texture_quality}.png`;
    let spritesheet_data_url = `/assets/dist/spritesheet-${texture_quality}.json`;

    let load_textures = new Promise<void>(async (resolve) => {
        const image_promise: Promise<HTMLImageElement> = new Promise((resolve, reject) => {
            const image = document.createElement("img");
            image.src = spritesheet_url;
            image.onload = () => { resolve(image); }
            image.onerror = err => reject(err);
        });
        const dat_promise: Promise<Object> = fetch(spritesheet_data_url).then(res => res.json());
        let image = await image_promise;
        let data = await dat_promise;

        global.spritesheet_img = image;
        global.spritesheet = data;

        resolve();
    });
    await load_textures;

    logger.info("Starting the renderer");


    //let sprite = PIXI.Sprite.from(global.spritesheet?.textures["hearty.png"]);

    global.client = await gateway_connect(server, username);

    global.canvas.width = window.innerWidth;
    global.canvas.height = window.innerHeight;

    window.onresize = () => {
        global.canvas.width = window.innerWidth;
        global.canvas.height = window.innerHeight;
    }

    global.canvas.style.setProperty("background-image", `url("/assets/final/${texture_quality}/starfield.png")`);

    let last_time = performance.now();
    let render = (now_time: DOMHighResTimeStamp) => {
        const delta_ms = now_time - last_time;
        last_time = now_time;

        let viewer_size_x = global.canvas.width;
        let viewer_size_y = global.canvas.height;

        global.canvas.style.setProperty("background-position", `${-global.me?.x!}px ${-global.me?.y!}px`);

        global.context.setTransform(1, 0, 0, 1, 0, 0);
        global.context.clearRect(0, 0, viewer_size_x, viewer_size_y);

        // *dont* translate the camera. we're movign everything else around us. cameracentrism.
        // only translation will be to center our core module.
        global.context.translate(viewer_size_x / 2, viewer_size_y / 2);

        for (let i = 0; i < global.planets.length; i++) {
            let planet = global.planets[i];
            // @ts-ignore
            let tex = global.spritesheet!["frames"][planet_type_to_tex_id(planet.planetType)];
            global.context.drawImage(global.spritesheet_img!,
                tex.frame.x, // sx
                tex.frame.y, // sy
                tex.frame.w, // sw
                tex.frame.h, // sh
                (planet.x - planet.radius - global.me?.x!), // dx
                (planet.y - planet.radius - global.me?.y!), // dy
                planet.radius * 2, // dw
                planet.radius * 2); // dh
        }

        for (let i = 0; i < global.players.length; i++) {
            let player = global.players[i];
            // @ts-ignore
            let tex = global.spritesheet!["frames"]["hearty.png"];

            global.context.save();

            global.context.translate(player.x - global.me!.x, player.y - global.me!.y);

            global.context.textAlign = "center";
            global.context.font = "30px Segoe UI";
            global.context.fillStyle = "white";
            global.context.fillText(player.username, 0, -35);

            global.context.rotate(player.rotation);

            global.context.drawImage(global.spritesheet_img!,
                tex.frame.x, // sx
                tex.frame.y, // sy
                tex.frame.w, // sw
                tex.frame.h, // sh
                -25, -25, 50, 50); // dh

            global.context.restore();
        }

        requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
}

let query = new URLSearchParams(window.location.search);

if (!(query.has("server") || query.has("username") || query.has("textures"))) {
    window.location.href = "/index.html";
}


client_main(query.get("server")!, query.get("username")!, query.get("textures")!).then(() => {});

/*

let app = new PIXI.Application({width: window.innerWidth, height: window.innerHeight, resizeTo: window });

// @ts-ignore
document.body.appendChild(app.view);

let sprite = PIXI.Sprite.from("./hearty.png");
app.stage.addChild(sprite);

let elapsed = 0.0;
app.ticker.add((delta) => {
    elapsed += delta;
    sprite.x = 100.0 + Math.cos(elapsed/50.0) * 100.0;
});

 */

function planet_type_to_tex_id(ty: PlanetType): string {
    if (ty == PlanetType.Earth) {
        return "earth.png"
    }
    return "unknown.png"
}
\ No newline at end of file

D client/src/lib.rs => client/src/lib.rs +0 -327
@@ 1,327 0,0 @@
use std::error::Error;
use std::panic;
use std::str::FromStr;
use futures::stream::{SplitSink, SplitStream};
use futures::{StreamExt};
use log::{error, info, Level, trace, warn};
use wasm_bindgen::prelude::*;
use ws_stream_wasm::{WsErr, WsMessage, WsMeta, WsStream};
use futures::SinkExt;
use lazy_static::lazy_static;
use std::sync::Arc;
use std::sync::RwLock;
use async_recursion::async_recursion;
use futures::FutureExt;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Window};
use starkingdoms_protocol::message_c2s::{MessageC2SGoodbye, MessageC2SHello, MessageC2SPing};
use starkingdoms_protocol::{MessageC2S, MessageS2C, PROTOCOL_VERSION};
use starkingdoms_protocol::goodbye_reason::GoodbyeReason;
use starkingdoms_protocol::planet::Planet;
use starkingdoms_protocol::player::Player;
use starkingdoms_protocol::state::State;

use crate::rendering::Renderer;
use crate::rendering::renderer::WebRenderer;
use crate::textures::loader::TextureLoader;
use crate::textures::{TextureManager, TextureSize};


#[macro_use]
pub mod macros;
pub mod chat;
pub mod rendering;
pub mod textures;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[derive(Debug)]
pub struct Client {
    pub client_data: Option<ClientData>,
    pub planets: Vec<Planet>,
    pub x: f64,
    pub y: f64,
    pub players: Vec<Player>
}

#[derive(Debug)]
pub struct ClientData {
    pub state: State,
    pub tx: SplitSink<WsStream, WsMessage>,
    pub rx: SplitStream<WsStream>,
    pub pong_timeout: u64,
    pub textures: TextureLoader,
    pub renderer: WebRenderer,
    pub username: String
}

pub const PONG_MAX_TIMEOUT: u64 = 5;

lazy_static! {
    pub static ref CLIENT: Arc<RwLock<Client>> = Arc::new(RwLock::new(Client {
        client_data: None,
        planets: vec![],
        x: 0f64,
        y: 0f64,
        players: vec![]
    }));
    //pub static ref BUTTONS: Arc<Buttons> = Arc::new(Buttons { up: false, left: false, right: false, down: false });
}

pub const MAX_CONNECTION_TRIES: i32 = 10;

#[wasm_bindgen]
pub async fn rust_init(gateway: &str, username: &str, texture_size: &str) -> Result<(), JsValue> {
    panic::set_hook(Box::new(console_error_panic_hook::hook));

    set_status("Starting logger...");

    console_log::init_with_level(Level::Debug).unwrap();

    info!("Logger setup successfully");

    info!("Loading sprites...");
    set_status("Loading sprites..");

    let textures = TextureLoader::load(TextureSize::from_str(texture_size).unwrap()).map_err(|e| e.to_string())?;

    match main(gateway, username, 1, textures, WebRenderer::get("canvas").await.unwrap()).await {
        Ok(c) => c,
        Err(e) => {
            error!("Error initializing gateway client: {}", e);
            return Err(JsValue::from_str(&e.to_string()));
        }
    };

    info!("StarKingdoms client set up successfully");

    Ok(())
}

#[async_recursion(?Send)]
pub async fn main(gateway: &str, username: &str, backoff: i32, textures: TextureLoader, renderer: WebRenderer) -> Result<(), Box<dyn Error>> {
    if backoff != 1 {
        info!("Backing off connection: waiting {} seconds", backoff * backoff);
        wait_for(sleep(backoff * backoff * 1000)).await;
    }

    if backoff > MAX_CONNECTION_TRIES {
        set_status("Connection to server failed");
        return Err("Hit backoff limit during reconnection attempt".into());
    }
    if backoff == 1 {
        set_status("Connecting to server...");
    } else {
        set_status(&format!("Connection failed, retrying... (try {}/10)", backoff));
    }

    info!("FAST CONNECT: {}", gateway);
    let gateway_url = url::Url::parse(gateway)?;
    trace!("Gateway URL parsed");
    let (_ws, ws_stream) = match WsMeta::connect(gateway_url, None).await {
        Ok(r) => r,
        Err(e) => {
            return match e {
                WsErr::ConnectionFailed { .. } => {
                    main(gateway, username, backoff + 1, textures, renderer).await
                },
                _ => {
                    Err(e.into())
                }
            }
        }
    };
    trace!("Connected to gateway socket");
    let (tx, rx) = ws_stream.split();

    let mut client_data = ClientData {
        state: State::Handshake,
        tx,
        rx,
        pong_timeout: (js_sys::Date::now() as u64 / 1000) + 5,
        textures,
        renderer,
        username: username.to_string()
    };

    trace!("Split stream, handshaking with server");

    set_status("Handshaking with server...");

    let msg = MessageC2S::Hello(MessageC2SHello {
        version: PROTOCOL_VERSION,
        requested_username: username.to_string(),
        next_state: State::Play.into(),
        special_fields: Default::default(),
    }).try_into()?;
    send!(client_data.tx, msg).await?;

    trace!("Sent handshake start packet");

    if let Some(msg) = recv_now!(client_data.rx)? {
        let typed_msg: MessageS2C = msg;

        match typed_msg {
            MessageS2C::Hello(pkt) => {
                info!("FAST CONNECT - connected to server protocol {} given username {}, switching to state {:?}", pkt.version, pkt.given_username, pkt.next_state);
                client_data.state = pkt.next_state.unwrap();
            },
            MessageS2C::Goodbye(pkt) => {
                error!("server disconnected before finishing handshake: {:?}", pkt.reason);
                return Err(format!("disconnected by server: {:?}", pkt.reason).into());
            },
            _ => {
                warn!("received unexpected packet from server: {:?}", typed_msg);
            }
        }
    } else {
        error!("Server closed the connection")
    }

    CLIENT.write()?.client_data = Some(client_data);

    set_status(&format!("Connected! Username: {}", username));

    Ok(())
}

#[wasm_bindgen]
pub async fn send_ping_pong() -> Result<(), JsError> {
    let mut client = CLIENT.write()?;

    if client.client_data.is_none() {
        return Err(JsError::new("Client not yet initialized"));
    }

    let client_data = client.client_data.as_mut().unwrap();


    send!(client_data.tx, MessageC2S::Ping(MessageC2SPing {special_fields: Default::default()}).try_into().map_err(|_| JsError::new("I/O Error"))?).await?;

    Ok(())
}

#[wasm_bindgen]
pub async fn update_socket() -> Result<(), JsError> {

    let mut client = CLIENT.write()?;

    if client.client_data.is_none() {
        return Err(JsError::new("Client not yet initialized"));
    }

    let client_data = client.client_data.as_mut().unwrap();

    if client_data.pong_timeout < (js_sys::Date::now() as u64 / 1000) {
        error!("Connection timed out");
        let msg = MessageC2S::Goodbye(MessageC2SGoodbye {
            reason: GoodbyeReason::PingPongTimeout.into(),
            special_fields: Default::default(),
        }).try_into().map_err(|e: Box<dyn Error> | JsError::new(&e.to_string()))?;
        send!(client_data.tx, msg).await?;
        client.client_data = None;
        set_status("Connection timed out. Reload to reconnect");
        return Err(JsError::new("Connection timed out"));
    }

    let maybe_msg: Option<MessageS2C> = match recv!(client_data.rx) {
        Ok(r) => r,
        Err(e) => {
            return Err(JsError::new(e))
        }
    };

    if let Some(msg) = maybe_msg {
        match msg {
            MessageS2C::Goodbye(pkt) => {
                info!("server sent disconnect: {:?}", pkt.reason);
                client.client_data = None;
                return Err(JsError::new("disconnected by server"));
            }
            MessageS2C::Chat(pkt) => {
                info!("[CHAT] {}: {}", pkt.from, pkt.message);

                let window: Window = web_sys::window().expect("no global `window` exists");
                let document = window.document().expect("should have a document on window");
                let chatbox = document.get_element_by_id("chats").expect("chatbox does not exist");

                let new_elem = document.create_element("div").expect("could not create element");

                let msg_formatted = markdown::to_html(&format!("**[{}]** {}", pkt.from, pkt.message));

                new_elem.set_inner_html(&msg_formatted);

                chatbox.append_child(&new_elem).unwrap();
            },
            MessageS2C::Pong(_) => {
                client_data.pong_timeout = (js_sys::Date::now() as u64 / 1000) + PONG_MAX_TIMEOUT
            },
            MessageS2C::PlanetData(pkt) => {
                client.planets = pkt.planets;
            },
            MessageS2C::PlayersUpdate(pkt) => {
                let me = pkt.players.iter().find(|i| i.username == client_data.username);
                if let Some(me) = me {
                    client.x = me.x as f64;
                    client.y = me.y as f64;
                }
                client.players = pkt.players;
            }
            _ => {
                warn!("server sent unexpected packet {:?}, ignoring", msg);
            }
        }
    }

    Ok(())
}

#[wasm_bindgen]
pub fn set_status(new_status: &str) {
    let window: Window = web_sys::window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
    let status = document.get_element_by_id("status").expect("statusbox does not exist");
    status.set_inner_html(new_status);
}

#[wasm_bindgen]
pub fn sleep(ms: i32) -> js_sys::Promise {
    js_sys::Promise::new(&mut |resolve, _| {
        web_sys::window()
            .unwrap()
            .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
            .unwrap();
    })
}

pub async fn wait_for(promise: js_sys::Promise) -> JsFuture {
    wasm_bindgen_futures::JsFuture::from(promise)
}

#[wasm_bindgen]
pub fn version() -> u32 {
    PROTOCOL_VERSION
}

#[wasm_bindgen]
pub fn get_texture(texture_id: &str) -> Option<String> {
    let client = CLIENT.read().unwrap();
    if let Some(client_data) = &client.client_data {
        client_data.textures.get_texture(texture_id)
    } else {
        None
    }
}

#[wasm_bindgen]
pub async fn render_frame(delta: f64) -> Result<(), JsError> {
    let client = CLIENT.read().unwrap();
    if let Some(client_data) = &client.client_data {
        Ok(client_data.renderer.render_frame(delta).await.map_err(|e| JsError::new(&format!("{}", e)))?)
    } else {
        Err(JsError::new("Client not yet initialized"))
    }
}

A client/src/logger.ts => client/src/logger.ts +118 -0
@@ 0,0 1,118 @@
// LOG LEVELS:
// 0: Fatal
// 1: Error
// 2: Warn
// 3: Info
// 4: Debug
// Logs are written in-memory and logged to console if logLevel > showLogLevel
let loglevel = 0;

export const LEVEL_FATAL = 0;
export const LEVEL_ERROR = 1;
export const LEVEL_WARN = 2;
export const LEVEL_INFO = 3;
export const LEVEL_DEBUG = 4;

interface LogLine {
    level: number,
    module: string,
    message: string,
    timestamp: number,
    deltaStart: number,
    deltaLast: number
}

const debugLog: Array<LogLine> = [];
let timestampStart: number;
let deltaTimestamp: number;
let logger: Logger;
let loggerSetup = false;

export function logSetup() {
    if (loggerSetup) return;
    loggerSetup = true;
    timestampStart = Date.now();
    deltaTimestamp = Date.now();
    logger = new Logger("Logger.ts");
    consoleLogLevel(LEVEL_INFO);
    logger.info("Logger setup complete");
}

function log(level: number, module: string, message: string) {
    const log = {
        level: level,
        module: module,
        message: message,
        timestamp: Date.now(),
        deltaStart: Date.now() - timestampStart,
        deltaLast: Date.now() - deltaTimestamp
    };
    deltaTimestamp = Date.now();

    debugLog.push(log);

    consoleLogHandler(log);
}

function consoleLogHandler(log: LogLine) {
    if (import.meta.env.SSR) return; // no logging in ssr
    if (log.level <= loglevel) {
        let textstyle = "";
        switch (log.level) {
            case 0:
                textstyle = "color:red;font-weight:bold;";
                break;
            case 1:
                textstyle = "color:red;";
                break;
            case 2:
                textstyle = "color:yellow;";
                break;
            case 3:
                textstyle = "";
                break;
            case 4:
                textstyle = "color:gray;";
                break;
        }
        console.log(`%c${new Date(log.timestamp).toISOString()} ${(log.deltaStart / 1000).toFixed(3)}Σ ${(log.deltaLast / 1000).toFixed(3)}Δ\n%c[${log.module}]%c ${log.message}`, "color:gray", "color:#8c188c", textstyle);
    }
}

export function consoleLogLevel(level: number) {
    loglevel = level;
}

export function consoleLogDump() {
    for (let i = 0; i < debugLog.length; i++) {
        consoleLogHandler(debugLog[i]);
    }
}

export class Logger {
    private readonly name: string;

    constructor(name: string) {
        this.name = name;
    }

    fatal(message: any) {
        log(LEVEL_FATAL, this.name, message)
    }

    error(message: any) {
        log(LEVEL_ERROR, this.name, message)
    }

    warn(message: any) {
        log(LEVEL_WARN, this.name, message)
    }

    info(message: any) {
        log(LEVEL_INFO, this.name, message)
    }

    debug(message: any) {
        log(LEVEL_DEBUG, this.name, message)
    }
}
\ No newline at end of file

D client/src/macros.rs => client/src/macros.rs +0 -64
@@ 1,64 0,0 @@
use ws_stream_wasm::WsMessage;

#[macro_export]
macro_rules! send {
    ($writer:expr,$pkt:expr) => {
        $writer.send($crate::macros::__generic_packet_to_message($pkt))
    };
}

#[macro_export]
macro_rules! recv {
    ($reader:expr) => {
        {
            if let Some(future_result) = $reader.next().now_or_never() {
                if let Some(msg) = future_result {
                    if let WsMessage::Binary(msg) = msg {
                        match MessageS2C::try_from(msg.as_slice()) {
                            Ok(d) => Ok(Some(d)),
                            Err(e) => {
                                log::error!("error deserializing message: {}", e);
                                Ok(None)
                            }
                        }
                    } else {
                        Ok(None)
                    }
                } else {
                    log::error!("pipe closed");
                    Err("Pipe closed")
                }
            } else {
                Ok(None)
            }
        }
    }
}

#[macro_export]
macro_rules! recv_now {
    ($reader:expr) => {
        {
            if let Some(msg) = $reader.next().await {
                if let WsMessage::Binary(msg) = msg {
                    match MessageS2C::try_from(msg.as_slice()) {
                        Ok(d) => Ok(Some(d)),
                        Err(e) => {
                            log::error!("error deserializing message: {}", e);
                            Ok(None)
                        }
                    }
                } else {
                    Ok(None)
                }
            } else {
                log::error!("pipe closed");
                Err("Pipe closed")
            }
        }
    };
}

pub fn __generic_packet_to_message(pkt: Vec<u8>) -> WsMessage {
    WsMessage::from(pkt)
}
\ No newline at end of file

A client/src/protocol/goodbye_reason.ts => client/src/protocol/goodbye_reason.ts +60 -0
@@ 0,0 1,60 @@
/* eslint-disable */

export const protobufPackage = "protocol.goodbye_reason";

export enum GoodbyeReason {
  UnsupportedProtocol = 0,
  UnexpectedPacket = 1,
  UnexpectedNextState = 2,
  UsernameTaken = 3,
  PingPongTimeout = 4,
  Done = 5,
  UNRECOGNIZED = -1,
}

export function goodbyeReasonFromJSON(object: any): GoodbyeReason {
  switch (object) {
    case 0:
    case "UnsupportedProtocol":
      return GoodbyeReason.UnsupportedProtocol;
    case 1:
    case "UnexpectedPacket":
      return GoodbyeReason.UnexpectedPacket;
    case 2:
    case "UnexpectedNextState":
      return GoodbyeReason.UnexpectedNextState;
    case 3:
    case "UsernameTaken":
      return GoodbyeReason.UsernameTaken;
    case 4:
    case "PingPongTimeout":
      return GoodbyeReason.PingPongTimeout;
    case 5:
    case "Done":
      return GoodbyeReason.Done;
    case -1:
    case "UNRECOGNIZED":
    default:
      return GoodbyeReason.UNRECOGNIZED;
  }
}

export function goodbyeReasonToJSON(object: GoodbyeReason): string {
  switch (object) {
    case GoodbyeReason.UnsupportedProtocol:
      return "UnsupportedProtocol";
    case GoodbyeReason.UnexpectedPacket:
      return "UnexpectedPacket";
    case GoodbyeReason.UnexpectedNextState:
      return "UnexpectedNextState";
    case GoodbyeReason.UsernameTaken:
      return "UsernameTaken";
    case GoodbyeReason.PingPongTimeout:
      return "PingPongTimeout";
    case GoodbyeReason.Done:
      return "Done";
    case GoodbyeReason.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

A client/src/protocol/message_c2s.ts => client/src/protocol/message_c2s.ts +415 -0
@@ 0,0 1,415 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal";
import { GoodbyeReason, goodbyeReasonFromJSON, goodbyeReasonToJSON } from "./goodbye_reason";
import { State, stateFromJSON, stateToJSON } from "./state";

export const protobufPackage = "protocol.message_c2s";

export interface MessageC2SHello {
  /** Version of the protocol. Currently always 1 */
  version: number;
  /** The username that the client is requesting. */
  requestedUsername: string;
  /** The state the connection will go into after the handshake. */
  nextState: State;
}

export enum MessageC2SHello_packetInfo {
  unknown = 0,
  type = 1,
  UNRECOGNIZED = -1,
}

export function messageC2SHello_packetInfoFromJSON(object: any): MessageC2SHello_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageC2SHello_packetInfo.unknown;
    case 1:
    case "type":
      return MessageC2SHello_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageC2SHello_packetInfo.UNRECOGNIZED;
  }
}

export function messageC2SHello_packetInfoToJSON(object: MessageC2SHello_packetInfo): string {
  switch (object) {
    case MessageC2SHello_packetInfo.unknown:
      return "unknown";
    case MessageC2SHello_packetInfo.type:
      return "type";
    case MessageC2SHello_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageC2SGoodbye {
  /** The reason the client is disconnecting the server */
  reason: GoodbyeReason;
}

export enum MessageC2SGoodbye_packetInfo {
  unknown = 0,
  type = 2,
  UNRECOGNIZED = -1,
}

export function messageC2SGoodbye_packetInfoFromJSON(object: any): MessageC2SGoodbye_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageC2SGoodbye_packetInfo.unknown;
    case 2:
    case "type":
      return MessageC2SGoodbye_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageC2SGoodbye_packetInfo.UNRECOGNIZED;
  }
}

export function messageC2SGoodbye_packetInfoToJSON(object: MessageC2SGoodbye_packetInfo): string {
  switch (object) {
    case MessageC2SGoodbye_packetInfo.unknown:
      return "unknown";
    case MessageC2SGoodbye_packetInfo.type:
      return "type";
    case MessageC2SGoodbye_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageC2SChat {
  /** The chat message to sent */
  message: string;
}

export enum MessageC2SChat_packetInfo {
  unknown = 0,
  type = 3,
  UNRECOGNIZED = -1,
}

export function messageC2SChat_packetInfoFromJSON(object: any): MessageC2SChat_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageC2SChat_packetInfo.unknown;
    case 3:
    case "type":
      return MessageC2SChat_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageC2SChat_packetInfo.UNRECOGNIZED;
  }
}

export function messageC2SChat_packetInfoToJSON(object: MessageC2SChat_packetInfo): string {
  switch (object) {
    case MessageC2SChat_packetInfo.unknown:
      return "unknown";
    case MessageC2SChat_packetInfo.type:
      return "type";
    case MessageC2SChat_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageC2SPing {
}

export enum MessageC2SPing_packetInfo {
  unknown = 0,
  type = 4,
  UNRECOGNIZED = -1,
}

export function messageC2SPing_packetInfoFromJSON(object: any): MessageC2SPing_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageC2SPing_packetInfo.unknown;
    case 4:
    case "type":
      return MessageC2SPing_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageC2SPing_packetInfo.UNRECOGNIZED;
  }
}

export function messageC2SPing_packetInfoToJSON(object: MessageC2SPing_packetInfo): string {
  switch (object) {
    case MessageC2SPing_packetInfo.unknown:
      return "unknown";
    case MessageC2SPing_packetInfo.type:
      return "type";
    case MessageC2SPing_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

function createBaseMessageC2SHello(): MessageC2SHello {
  return { version: 0, requestedUsername: "", nextState: 0 };
}

export const MessageC2SHello = {
  encode(message: MessageC2SHello, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.version !== 0) {
      writer.uint32(8).uint32(message.version);
    }
    if (message.requestedUsername !== "") {
      writer.uint32(18).string(message.requestedUsername);
    }
    if (message.nextState !== 0) {
      writer.uint32(24).int32(message.nextState);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageC2SHello {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageC2SHello();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 8) {
            break;
          }

          message.version = reader.uint32();
          continue;
        case 2:
          if (tag != 18) {
            break;
          }

          message.requestedUsername = reader.string();
          continue;
        case 3:
          if (tag != 24) {
            break;
          }

          message.nextState = reader.int32() as any;
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageC2SHello {
    return {
      version: isSet(object.version) ? Number(object.version) : 0,
      requestedUsername: isSet(object.requestedUsername) ? String(object.requestedUsername) : "",
      nextState: isSet(object.nextState) ? stateFromJSON(object.nextState) : 0,
    };
  },

  toJSON(message: MessageC2SHello): unknown {
    const obj: any = {};
    message.version !== undefined && (obj.version = Math.round(message.version));
    message.requestedUsername !== undefined && (obj.requestedUsername = message.requestedUsername);
    message.nextState !== undefined && (obj.nextState = stateToJSON(message.nextState));
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageC2SHello>, I>>(base?: I): MessageC2SHello {
    return MessageC2SHello.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageC2SHello>, I>>(object: I): MessageC2SHello {
    const message = createBaseMessageC2SHello();
    message.version = object.version ?? 0;
    message.requestedUsername = object.requestedUsername ?? "";
    message.nextState = object.nextState ?? 0;
    return message;
  },
};

function createBaseMessageC2SGoodbye(): MessageC2SGoodbye {
  return { reason: 0 };
}

export const MessageC2SGoodbye = {
  encode(message: MessageC2SGoodbye, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.reason !== 0) {
      writer.uint32(8).int32(message.reason);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageC2SGoodbye {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageC2SGoodbye();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 8) {
            break;
          }

          message.reason = reader.int32() as any;
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageC2SGoodbye {
    return { reason: isSet(object.reason) ? goodbyeReasonFromJSON(object.reason) : 0 };
  },

  toJSON(message: MessageC2SGoodbye): unknown {
    const obj: any = {};
    message.reason !== undefined && (obj.reason = goodbyeReasonToJSON(message.reason));
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageC2SGoodbye>, I>>(base?: I): MessageC2SGoodbye {
    return MessageC2SGoodbye.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageC2SGoodbye>, I>>(object: I): MessageC2SGoodbye {
    const message = createBaseMessageC2SGoodbye();
    message.reason = object.reason ?? 0;
    return message;
  },
};

function createBaseMessageC2SChat(): MessageC2SChat {
  return { message: "" };
}

export const MessageC2SChat = {
  encode(message: MessageC2SChat, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.message !== "") {
      writer.uint32(10).string(message.message);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageC2SChat {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageC2SChat();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 10) {
            break;
          }

          message.message = reader.string();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageC2SChat {
    return { message: isSet(object.message) ? String(object.message) : "" };
  },

  toJSON(message: MessageC2SChat): unknown {
    const obj: any = {};
    message.message !== undefined && (obj.message = message.message);
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageC2SChat>, I>>(base?: I): MessageC2SChat {
    return MessageC2SChat.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageC2SChat>, I>>(object: I): MessageC2SChat {
    const message = createBaseMessageC2SChat();
    message.message = object.message ?? "";
    return message;
  },
};

function createBaseMessageC2SPing(): MessageC2SPing {
  return {};
}

export const MessageC2SPing = {
  encode(_: MessageC2SPing, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageC2SPing {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageC2SPing();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(_: any): MessageC2SPing {
    return {};
  },

  toJSON(_: MessageC2SPing): unknown {
    const obj: any = {};
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageC2SPing>, I>>(base?: I): MessageC2SPing {
    return MessageC2SPing.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageC2SPing>, I>>(_: I): MessageC2SPing {
    const message = createBaseMessageC2SPing();
    return message;
  },
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
  : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
  return value !== null && value !== undefined;
}

A client/src/protocol/message_s2c.ts => client/src/protocol/message_s2c.ts +630 -0
@@ 0,0 1,630 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal";
import { GoodbyeReason, goodbyeReasonFromJSON, goodbyeReasonToJSON } from "./goodbye_reason";
import { Planet } from "./planet";
import { Player } from "./player";
import { State, stateFromJSON, stateToJSON } from "./state";

export const protobufPackage = "protocol.message_s2c";

export interface MessageS2CHello {
  /** The version of the protocol in use. Currently always 1. */
  version: number;
  /** The username actually assigned to the player */
  givenUsername: string;
  /** The state to switch the game into */
  nextState: State;
}

export enum MessageS2CHello_packetInfo {
  unknown = 0,
  type = 5,
  UNRECOGNIZED = -1,
}

export function messageS2CHello_packetInfoFromJSON(object: any): MessageS2CHello_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageS2CHello_packetInfo.unknown;
    case 5:
    case "type":
      return MessageS2CHello_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageS2CHello_packetInfo.UNRECOGNIZED;
  }
}

export function messageS2CHello_packetInfoToJSON(object: MessageS2CHello_packetInfo): string {
  switch (object) {
    case MessageS2CHello_packetInfo.unknown:
      return "unknown";
    case MessageS2CHello_packetInfo.type:
      return "type";
    case MessageS2CHello_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageS2CGoodbye {
  /** The reason for the disconnect */
  reason: GoodbyeReason;
}

export enum MessageS2CGoodbye_packetInfo {
  unknown = 0,
  type = 6,
  UNRECOGNIZED = -1,
}

export function messageS2CGoodbye_packetInfoFromJSON(object: any): MessageS2CGoodbye_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageS2CGoodbye_packetInfo.unknown;
    case 6:
    case "type":
      return MessageS2CGoodbye_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageS2CGoodbye_packetInfo.UNRECOGNIZED;
  }
}

export function messageS2CGoodbye_packetInfoToJSON(object: MessageS2CGoodbye_packetInfo): string {
  switch (object) {
    case MessageS2CGoodbye_packetInfo.unknown:
      return "unknown";
    case MessageS2CGoodbye_packetInfo.type:
      return "type";
    case MessageS2CGoodbye_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageS2CChat {
  /** The username of the player who sent the message */
  from: string;
  /** The contents of the chat message */
  message: string;
}

export enum MessageS2CChat_packetInfo {
  unknown = 0,
  type = 7,
  UNRECOGNIZED = -1,
}

export function messageS2CChat_packetInfoFromJSON(object: any): MessageS2CChat_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageS2CChat_packetInfo.unknown;
    case 7:
    case "type":
      return MessageS2CChat_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageS2CChat_packetInfo.UNRECOGNIZED;
  }
}

export function messageS2CChat_packetInfoToJSON(object: MessageS2CChat_packetInfo): string {
  switch (object) {
    case MessageS2CChat_packetInfo.unknown:
      return "unknown";
    case MessageS2CChat_packetInfo.type:
      return "type";
    case MessageS2CChat_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageS2CPong {
}

export enum MessageS2CPong_packetInfo {
  unknown = 0,
  type = 8,
  UNRECOGNIZED = -1,
}

export function messageS2CPong_packetInfoFromJSON(object: any): MessageS2CPong_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageS2CPong_packetInfo.unknown;
    case 8:
    case "type":
      return MessageS2CPong_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageS2CPong_packetInfo.UNRECOGNIZED;
  }
}

export function messageS2CPong_packetInfoToJSON(object: MessageS2CPong_packetInfo): string {
  switch (object) {
    case MessageS2CPong_packetInfo.unknown:
      return "unknown";
    case MessageS2CPong_packetInfo.type:
      return "type";
    case MessageS2CPong_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageS2CPlayersUpdate {
  /** List of all players in the server */
  players: Player[];
}

export enum MessageS2CPlayersUpdate_packetInfo {
  unknown = 0,
  type = 9,
  UNRECOGNIZED = -1,
}

export function messageS2CPlayersUpdate_packetInfoFromJSON(object: any): MessageS2CPlayersUpdate_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageS2CPlayersUpdate_packetInfo.unknown;
    case 9:
    case "type":
      return MessageS2CPlayersUpdate_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageS2CPlayersUpdate_packetInfo.UNRECOGNIZED;
  }
}

export function messageS2CPlayersUpdate_packetInfoToJSON(object: MessageS2CPlayersUpdate_packetInfo): string {
  switch (object) {
    case MessageS2CPlayersUpdate_packetInfo.unknown:
      return "unknown";
    case MessageS2CPlayersUpdate_packetInfo.type:
      return "type";
    case MessageS2CPlayersUpdate_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface MessageS2CPlanetData {
  /** List of all planets on the server */
  planets: Planet[];
}

export enum MessageS2CPlanetData_packetInfo {
  unknown = 0,
  type = 10,
  UNRECOGNIZED = -1,
}

export function messageS2CPlanetData_packetInfoFromJSON(object: any): MessageS2CPlanetData_packetInfo {
  switch (object) {
    case 0:
    case "unknown":
      return MessageS2CPlanetData_packetInfo.unknown;
    case 10:
    case "type":
      return MessageS2CPlanetData_packetInfo.type;
    case -1:
    case "UNRECOGNIZED":
    default:
      return MessageS2CPlanetData_packetInfo.UNRECOGNIZED;
  }
}

export function messageS2CPlanetData_packetInfoToJSON(object: MessageS2CPlanetData_packetInfo): string {
  switch (object) {
    case MessageS2CPlanetData_packetInfo.unknown:
      return "unknown";
    case MessageS2CPlanetData_packetInfo.type:
      return "type";
    case MessageS2CPlanetData_packetInfo.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

function createBaseMessageS2CHello(): MessageS2CHello {
  return { version: 0, givenUsername: "", nextState: 0 };
}

export const MessageS2CHello = {
  encode(message: MessageS2CHello, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.version !== 0) {
      writer.uint32(8).uint32(message.version);
    }
    if (message.givenUsername !== "") {
      writer.uint32(18).string(message.givenUsername);
    }
    if (message.nextState !== 0) {
      writer.uint32(24).int32(message.nextState);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageS2CHello {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageS2CHello();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 8) {
            break;
          }

          message.version = reader.uint32();
          continue;
        case 2:
          if (tag != 18) {
            break;
          }

          message.givenUsername = reader.string();
          continue;
        case 3:
          if (tag != 24) {
            break;
          }

          message.nextState = reader.int32() as any;
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageS2CHello {
    return {
      version: isSet(object.version) ? Number(object.version) : 0,
      givenUsername: isSet(object.givenUsername) ? String(object.givenUsername) : "",
      nextState: isSet(object.nextState) ? stateFromJSON(object.nextState) : 0,
    };
  },

  toJSON(message: MessageS2CHello): unknown {
    const obj: any = {};
    message.version !== undefined && (obj.version = Math.round(message.version));
    message.givenUsername !== undefined && (obj.givenUsername = message.givenUsername);
    message.nextState !== undefined && (obj.nextState = stateToJSON(message.nextState));
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageS2CHello>, I>>(base?: I): MessageS2CHello {
    return MessageS2CHello.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageS2CHello>, I>>(object: I): MessageS2CHello {
    const message = createBaseMessageS2CHello();
    message.version = object.version ?? 0;
    message.givenUsername = object.givenUsername ?? "";
    message.nextState = object.nextState ?? 0;
    return message;
  },
};

function createBaseMessageS2CGoodbye(): MessageS2CGoodbye {
  return { reason: 0 };
}

export const MessageS2CGoodbye = {
  encode(message: MessageS2CGoodbye, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.reason !== 0) {
      writer.uint32(8).int32(message.reason);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageS2CGoodbye {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageS2CGoodbye();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 8) {
            break;
          }

          message.reason = reader.int32() as any;
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageS2CGoodbye {
    return { reason: isSet(object.reason) ? goodbyeReasonFromJSON(object.reason) : 0 };
  },

  toJSON(message: MessageS2CGoodbye): unknown {
    const obj: any = {};
    message.reason !== undefined && (obj.reason = goodbyeReasonToJSON(message.reason));
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageS2CGoodbye>, I>>(base?: I): MessageS2CGoodbye {
    return MessageS2CGoodbye.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageS2CGoodbye>, I>>(object: I): MessageS2CGoodbye {
    const message = createBaseMessageS2CGoodbye();
    message.reason = object.reason ?? 0;
    return message;
  },
};

function createBaseMessageS2CChat(): MessageS2CChat {
  return { from: "", message: "" };
}

export const MessageS2CChat = {
  encode(message: MessageS2CChat, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.from !== "") {
      writer.uint32(10).string(message.from);
    }
    if (message.message !== "") {
      writer.uint32(18).string(message.message);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageS2CChat {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageS2CChat();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 10) {
            break;
          }

          message.from = reader.string();
          continue;
        case 2:
          if (tag != 18) {
            break;
          }

          message.message = reader.string();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageS2CChat {
    return {
      from: isSet(object.from) ? String(object.from) : "",
      message: isSet(object.message) ? String(object.message) : "",
    };
  },

  toJSON(message: MessageS2CChat): unknown {
    const obj: any = {};
    message.from !== undefined && (obj.from = message.from);
    message.message !== undefined && (obj.message = message.message);
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageS2CChat>, I>>(base?: I): MessageS2CChat {
    return MessageS2CChat.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageS2CChat>, I>>(object: I): MessageS2CChat {
    const message = createBaseMessageS2CChat();
    message.from = object.from ?? "";
    message.message = object.message ?? "";
    return message;
  },
};

function createBaseMessageS2CPong(): MessageS2CPong {
  return {};
}

export const MessageS2CPong = {
  encode(_: MessageS2CPong, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageS2CPong {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageS2CPong();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(_: any): MessageS2CPong {
    return {};
  },

  toJSON(_: MessageS2CPong): unknown {
    const obj: any = {};
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageS2CPong>, I>>(base?: I): MessageS2CPong {
    return MessageS2CPong.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageS2CPong>, I>>(_: I): MessageS2CPong {
    const message = createBaseMessageS2CPong();
    return message;
  },
};

function createBaseMessageS2CPlayersUpdate(): MessageS2CPlayersUpdate {
  return { players: [] };
}

export const MessageS2CPlayersUpdate = {
  encode(message: MessageS2CPlayersUpdate, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    for (const v of message.players) {
      Player.encode(v!, writer.uint32(10).fork()).ldelim();
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageS2CPlayersUpdate {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageS2CPlayersUpdate();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 10) {
            break;
          }

          message.players.push(Player.decode(reader, reader.uint32()));
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageS2CPlayersUpdate {
    return { players: Array.isArray(object?.players) ? object.players.map((e: any) => Player.fromJSON(e)) : [] };
  },

  toJSON(message: MessageS2CPlayersUpdate): unknown {
    const obj: any = {};
    if (message.players) {
      obj.players = message.players.map((e) => e ? Player.toJSON(e) : undefined);
    } else {
      obj.players = [];
    }
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageS2CPlayersUpdate>, I>>(base?: I): MessageS2CPlayersUpdate {
    return MessageS2CPlayersUpdate.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageS2CPlayersUpdate>, I>>(object: I): MessageS2CPlayersUpdate {
    const message = createBaseMessageS2CPlayersUpdate();
    message.players = object.players?.map((e) => Player.fromPartial(e)) || [];
    return message;
  },
};

function createBaseMessageS2CPlanetData(): MessageS2CPlanetData {
  return { planets: [] };
}

export const MessageS2CPlanetData = {
  encode(message: MessageS2CPlanetData, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    for (const v of message.planets) {
      Planet.encode(v!, writer.uint32(10).fork()).ldelim();
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): MessageS2CPlanetData {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBaseMessageS2CPlanetData();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 10) {
            break;
          }

          message.planets.push(Planet.decode(reader, reader.uint32()));
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): MessageS2CPlanetData {
    return { planets: Array.isArray(object?.planets) ? object.planets.map((e: any) => Planet.fromJSON(e)) : [] };
  },

  toJSON(message: MessageS2CPlanetData): unknown {
    const obj: any = {};
    if (message.planets) {
      obj.planets = message.planets.map((e) => e ? Planet.toJSON(e) : undefined);
    } else {
      obj.planets = [];
    }
    return obj;
  },

  create<I extends Exact<DeepPartial<MessageS2CPlanetData>, I>>(base?: I): MessageS2CPlanetData {
    return MessageS2CPlanetData.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<MessageS2CPlanetData>, I>>(object: I): MessageS2CPlanetData {
    const message = createBaseMessageS2CPlanetData();
    message.planets = object.planets?.map((e) => Planet.fromPartial(e)) || [];
    return message;
  },
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
  : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
  return value !== null && value !== undefined;
}

A client/src/protocol/planet.ts => client/src/protocol/planet.ts +154 -0
@@ 0,0 1,154 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "protocol.planet";

export enum PlanetType {
  Earth = 0,
  UNRECOGNIZED = -1,
}

export function planetTypeFromJSON(object: any): PlanetType {
  switch (object) {
    case 0:
    case "Earth":
      return PlanetType.Earth;
    case -1:
    case "UNRECOGNIZED":
    default:
      return PlanetType.UNRECOGNIZED;
  }
}

export function planetTypeToJSON(object: PlanetType): string {
  switch (object) {
    case PlanetType.Earth:
      return "Earth";
    case PlanetType.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

export interface Planet {
  /** Type of the planet */
  planetType: PlanetType;
  /** Translation on the X axis, in game units */
  x: number;
  /** Translation on the Y axis, in game units */
  y: number;
  /** The radius of the planet extending out from (x, y) */
  radius: number;
}

function createBasePlanet(): Planet {
  return { planetType: 0, x: 0, y: 0, radius: 0 };
}

export const Planet = {
  encode(message: Planet, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.planetType !== 0) {
      writer.uint32(8).int32(message.planetType);
    }
    if (message.x !== 0) {
      writer.uint32(21).float(message.x);
    }
    if (message.y !== 0) {
      writer.uint32(29).float(message.y);
    }
    if (message.radius !== 0) {
      writer.uint32(37).float(message.radius);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): Planet {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBasePlanet();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 8) {
            break;
          }

          message.planetType = reader.int32() as any;
          continue;
        case 2:
          if (tag != 21) {
            break;
          }

          message.x = reader.float();
          continue;
        case 3:
          if (tag != 29) {
            break;
          }

          message.y = reader.float();
          continue;
        case 4:
          if (tag != 37) {
            break;
          }

          message.radius = reader.float();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): Planet {
    return {
      planetType: isSet(object.planetType) ? planetTypeFromJSON(object.planetType) : 0,
      x: isSet(object.x) ? Number(object.x) : 0,
      y: isSet(object.y) ? Number(object.y) : 0,
      radius: isSet(object.radius) ? Number(object.radius) : 0,
    };
  },

  toJSON(message: Planet): unknown {
    const obj: any = {};
    message.planetType !== undefined && (obj.planetType = planetTypeToJSON(message.planetType));
    message.x !== undefined && (obj.x = message.x);
    message.y !== undefined && (obj.y = message.y);
    message.radius !== undefined && (obj.radius = message.radius);
    return obj;
  },

  create<I extends Exact<DeepPartial<Planet>, I>>(base?: I): Planet {
    return Planet.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<Planet>, I>>(object: I): Planet {
    const message = createBasePlanet();
    message.planetType = object.planetType ?? 0;
    message.x = object.x ?? 0;
    message.y = object.y ?? 0;
    message.radius = object.radius ?? 0;
    return message;
  },
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
  : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
  return value !== null && value !== undefined;
}

A client/src/protocol/player.ts => client/src/protocol/player.ts +127 -0
@@ 0,0 1,127 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "protocol.player";

export interface Player {
  /** The rotation, clockwise, in degrees, of the player */
  rotation: number;
  /** The translation on the X axis, in game units, of the player */
  x: number;
  /** The translation on the Y axis, in game units, of the player */
  y: number;
  /** The username of the player */
  username: string;
}

function createBasePlayer(): Player {
  return { rotation: 0, x: 0, y: 0, username: "" };
}

export const Player = {
  encode(message: Player, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.rotation !== 0) {
      writer.uint32(13).float(message.rotation);
    }
    if (message.x !== 0) {
      writer.uint32(21).float(message.x);
    }
    if (message.y !== 0) {
      writer.uint32(29).float(message.y);
    }
    if (message.username !== "") {
      writer.uint32(34).string(message.username);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): Player {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBasePlayer();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 13) {
            break;
          }

          message.rotation = reader.float();
          continue;
        case 2:
          if (tag != 21) {
            break;
          }

          message.x = reader.float();
          continue;
        case 3:
          if (tag != 29) {
            break;
          }

          message.y = reader.float();
          continue;
        case 4:
          if (tag != 34) {
            break;
          }

          message.username = reader.string();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): Player {
    return {
      rotation: isSet(object.rotation) ? Number(object.rotation) : 0,
      x: isSet(object.x) ? Number(object.x) : 0,
      y: isSet(object.y) ? Number(object.y) : 0,
      username: isSet(object.username) ? String(object.username) : "",
    };
  },

  toJSON(message: Player): unknown {
    const obj: any = {};
    message.rotation !== undefined && (obj.rotation = message.rotation);
    message.x !== undefined && (obj.x = message.x);
    message.y !== undefined && (obj.y = message.y);
    message.username !== undefined && (obj.username = message.username);
    return obj;
  },

  create<I extends Exact<DeepPartial<Player>, I>>(base?: I): Player {
    return Player.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<Player>, I>>(object: I): Player {
    const message = createBasePlayer();
    message.rotation = object.rotation ?? 0;
    message.x = object.x ?? 0;
    message.y = object.y ?? 0;
    message.username = object.username ?? "";
    return message;
  },
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
  : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
  return value !== null && value !== undefined;
}

A client/src/protocol/starkingdoms-protocol.ts => client/src/protocol/starkingdoms-protocol.ts +157 -0
@@ 0,0 1,157 @@
/* eslint-disable */
import * as Long from "long";
import * as _m0 from "protobufjs/minimal";

export const protobufPackage = "protocol";

export interface PacketWrapper {
  /** What is the Packet ID of this packet? */
  packetId: number;
  /** Protobuf-encoded bytearray containing the actual packet */
  packetData: Uint8Array;
}

function createBasePacketWrapper(): PacketWrapper {
  return { packetId: 0, packetData: new Uint8Array() };
}

export const PacketWrapper = {
  encode(message: PacketWrapper, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
    if (message.packetId !== 0) {
      writer.uint32(8).int64(message.packetId);
    }
    if (message.packetData.length !== 0) {
      writer.uint32(18).bytes(message.packetData);
    }
    return writer;
  },

  decode(input: _m0.Reader | Uint8Array, length?: number): PacketWrapper {
    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
    let end = length === undefined ? reader.len : reader.pos + length;
    const message = createBasePacketWrapper();
    while (reader.pos < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          if (tag != 8) {
            break;
          }

          message.packetId = longToNumber(reader.int64() as Long);
          continue;
        case 2:
          if (tag != 18) {
            break;
          }

          message.packetData = reader.bytes();
          continue;
      }
      if ((tag & 7) == 4 || tag == 0) {
        break;
      }
      reader.skipType(tag & 7);
    }
    return message;
  },

  fromJSON(object: any): PacketWrapper {
    return {
      packetId: isSet(object.packetId) ? Number(object.packetId) : 0,
      packetData: isSet(object.packetData) ? bytesFromBase64(object.packetData) : new Uint8Array(),
    };
  },

  toJSON(message: PacketWrapper): unknown {
    const obj: any = {};
    message.packetId !== undefined && (obj.packetId = Math.round(message.packetId));
    message.packetData !== undefined &&
      (obj.packetData = base64FromBytes(message.packetData !== undefined ? message.packetData : new Uint8Array()));
    return obj;
  },

  create<I extends Exact<DeepPartial<PacketWrapper>, I>>(base?: I): PacketWrapper {
    return PacketWrapper.fromPartial(base ?? {});
  },

  fromPartial<I extends Exact<DeepPartial<PacketWrapper>, I>>(object: I): PacketWrapper {
    const message = createBasePacketWrapper();
    message.packetId = object.packetId ?? 0;
    message.packetData = object.packetData ?? new Uint8Array();
    return message;
  },
};

declare var self: any | undefined;
declare var window: any | undefined;
declare var global: any | undefined;
var tsProtoGlobalThis: any = (() => {
  if (typeof globalThis !== "undefined") {
    return globalThis;
  }
  if (typeof self !== "undefined") {
    return self;
  }
  if (typeof window !== "undefined") {
    return window;
  }
  if (typeof global !== "undefined") {
    return global;
  }
  throw "Unable to locate global object";
})();

function bytesFromBase64(b64: string): Uint8Array {
  if (tsProtoGlobalThis.Buffer) {
    return Uint8Array.from(tsProtoGlobalThis.Buffer.from(b64, "base64"));
  } else {
    const bin = tsProtoGlobalThis.atob(b64);
    const arr = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; ++i) {
      arr[i] = bin.charCodeAt(i);
    }
    return arr;
  }
}

function base64FromBytes(arr: Uint8Array): string {
  if (tsProtoGlobalThis.Buffer) {
    return tsProtoGlobalThis.Buffer.from(arr).toString("base64");
  } else {
    const bin: string[] = [];
    arr.forEach((byte) => {
      bin.push(String.fromCharCode(byte));
    });
    return tsProtoGlobalThis.btoa(bin.join(""));
  }
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
  : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
  : Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
  : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function longToNumber(long: Long): number {
  if (long.gt(Number.MAX_SAFE_INTEGER)) {
    throw new tsProtoGlobalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
  }
  return long.toNumber();
}

// If you get a compile-error about 'Constructor<Long> and ... have no overlap',
// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'.
if (_m0.util.Long !== Long) {
  _m0.util.Long = Long as any;
  _m0.configure();
}

function isSet(value: any): boolean {
  return value !== null && value !== undefined;
}

A client/src/protocol/state.ts => client/src/protocol/state.ts +36 -0
@@ 0,0 1,36 @@
/* eslint-disable */

export const protobufPackage = "protocol.state";

export enum State {
  Handshake = 0,
  Play = 1,
  UNRECOGNIZED = -1,
}

export function stateFromJSON(object: any): State {
  switch (object) {
    case 0:
    case "Handshake":
      return State.Handshake;
    case 1:
    case "Play":
      return State.Play;
    case -1:
    case "UNRECOGNIZED":
    default:
      return State.UNRECOGNIZED;
  }
}

export function stateToJSON(object: State): string {
  switch (object) {
    case State.Handshake:
      return "Handshake";
    case State.Play:
      return "Play";
    case State.UNRECOGNIZED:
    default:
      return "UNRECOGNIZED";
  }
}

D client/src/rendering/mod.rs => client/src/rendering/mod.rs +0 -23
@@ 1,23 0,0 @@
use std::error::Error;
use async_trait::async_trait;

#[cfg(all(feature = "renderer-playercentric", feature = "renderer-canvascentric"))]
compile_error!("Mutually exclusive features renderer-playercentric and renderer-canvascentric selected");
#[cfg(not(any(feature = "renderer-playercentric", feature = "renderer-canvascentric")))]
compile_error!("Required feature renderer not selected");

#[cfg(feature = "renderer-canvascentric")]
#[path = "renderer_canvascentric.rs"]
pub mod renderer;

#[cfg(feature = "renderer-playercentric")]
#[path = "renderer_playercentric.rs"]
pub mod renderer;

pub mod util;

#[async_trait]
pub trait Renderer {
    async fn get(canvas_element_id: &str) -> Result<Self, Box<dyn Error>> where Self: Sized;
    async fn render_frame(&self, time_delta_ms: f64) -> Result<(), Box<dyn Error>>;
}
\ No newline at end of file

D client/src/rendering/renderer_canvascentric.rs => client/src/rendering/renderer_canvascentric.rs +0 -110
@@ 1,110 0,0 @@
use std::error::Error;
use async_trait::async_trait;
use log::debug;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlImageElement};
use crate::rendering::Renderer;
use wasm_bindgen::{JsCast, JsValue};
use crate::CLIENT;
use crate::rendering::util::texid_to_html_image_unchecked;
use crate::textures::TextureManager;

#[derive(Debug)]
pub struct WebRenderer {
    canvas_element_id: String
}

pub const USERNAME_TEXT_ALIGN: &str = "center";
pub const USERNAME_FONT: &str = "30px Segoe UI";
pub const USERNAME_COLOR: &str = "white";
pub const USERNAME_OFFSET_X: f64 = 0f64;
pub const USERNAME_OFFSET_Y: f64 = -35f64;

pub const HEARTY_OFFSET_X: f64 = -25f64;
pub const HEARTY_OFFSET_Y: f64 = -25f64;
pub const HEARTY_WIDTH: f64 = 50f64;
pub const HEARTY_HEIGHT: f64 = 50f64;

#[async_trait]
impl Renderer for WebRenderer {
    async fn get(canvas_element_id: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self {
            canvas_element_id: canvas_element_id.to_string()
        })
    }

    async fn render_frame(&self, _time_delta_ms: f64) -> Result<(), Box<dyn Error>> {
        // TODO - core is working on this, please no touchy without telling him
        // TODO - until this notice is removed
        // time_delta_ms is the delta, in ms, from when the last render_frame was called by the browser
        let window = web_sys::window().ok_or("window needs to exist")?;
        let document = window.document().ok_or("window.document needs to exist")?;
        let canvas_element = document.get_element_by_id(&self.canvas_element_id).ok_or("canvas element does not exist")?;
        let typed_canvas_element: HtmlCanvasElement = canvas_element.dyn_into::<web_sys::HtmlCanvasElement>().map_err(|_| ()).unwrap();
        let context = typed_canvas_element.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
        let client = CLIENT.read()?;
        if client.client_data.is_none() {
            return Err("client not yet initialized".into());
        }
        let client_data = client.client_data.as_ref().unwrap();

        context.set_transform(1f64, 0f64, 0f64, 1f64, 0f64, 0f64).map_err(|e: JsValue| e.as_string().unwrap())?;
        context.clear_rect(0f64, 0f64, typed_canvas_element.width() as f64, typed_canvas_element.height() as f64);

        let hearty = texid_to_html_image_unchecked("hearty");

        // draw players
        for player in &client.players {
            context.save(); // save current position

            // teleport to the player's location
            context.translate(-player.x, -player.y).map_err(|e| e.as_string().unwrap())?;

            debug!("[render] PL: ({}, {}) {}", player.x, player.y, player.username);

            // draw username
            context.set_text_align(USERNAME_TEXT_ALIGN);
            context.set_font(USERNAME_FONT);
            context.set_fill_style(&JsValue::from_str(USERNAME_COLOR)); // CssStyleColor
            context.fill_text(&player.username, USERNAME_OFFSET_X, USERNAME_OFFSET_Y).map_err(|e: JsValue| e.as_string().unwrap())?;

            // rotate the canvas so we can draw hearty
            context.rotate(player.rotation).map_err(|e| e.as_string().unwrap())?;

            // draw hearty
            context.draw_image_with_html_image_element_and_dw_and_dh(&hearty, HEARTY_OFFSET_X, HEARTY_OFFSET_Y, HEARTY_WIDTH, HEARTY_HEIGHT).map_err(|e| e.as_string().unwrap())?;

            context.restore(); // return to canvas base
        }

        // finally, translate to hearty
        context.translate(-client.x + ((typed_canvas_element.width() / 2) as f64), -client.y + ((typed_canvas_element.height() / 2) as f64)).map_err(|e| e.as_string().unwrap())?;

/*
        context.begin_path();

        // Draw the outer circle.
        context
            .arc(75.0, 75.0, 50.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the mouth.
        context.move_to(110.0, 75.0);
        context.arc(75.0, 75.0, 35.0, 0.0, std::f64::consts::PI).unwrap();

        // Draw the left eye.
        context.move_to(65.0, 65.0);
        context
            .arc(60.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the right eye.
        context.move_to(95.0, 65.0);
        context
            .arc(90.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        context.stroke();
*/
        Ok(())
    }
}

D client/src/rendering/renderer_playercentric.rs => client/src/rendering/renderer_playercentric.rs +0 -120
@@ 1,120 0,0 @@
use std::error::Error;
use async_trait::async_trait;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlImageElement};
use crate::rendering::Renderer;
use wasm_bindgen::{JsCast, JsValue};
use crate::CLIENT;

// TODO: Remove all the f32s

pub const STARFIELD_RENDER_SCALE: f64 = 1.0;

#[derive(Debug)]
pub struct WebRenderer {
    canvas_element_id: String
}

#[async_trait]
impl Renderer for WebRenderer {
    async fn get(canvas_element_id: &str) -> Result<Self, Box<dyn Error>> {
        Ok(Self {
            canvas_element_id: canvas_element_id.to_string()
        })
    }

    async fn render_frame(&self, _time_delta_ms: f64) -> Result<(), Box<dyn Error>> {
        // TODO - terra is working on this, please no touchy without telling him
        // TODO - until this notice is removed
        // time_delta_ms is the delta, in ms, from when the last render_frame was called by the browser
        let window = web_sys::window().ok_or("window needs to exist")?;
        let document = window.document().ok_or("window.document needs to exist")?;
        let canvas_element = document.get_element_by_id(&self.canvas_element_id).ok_or("canvas element does not exist")?;
        let typed_canvas_element: HtmlCanvasElement = canvas_element.dyn_into::<web_sys::HtmlCanvasElement>().map_err(|_| ()).unwrap();
        let context = typed_canvas_element.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
        let client = CLIENT.read()?;
        if client.client_data.is_none() {
            return Err("client not yet initialized".into());
        }
        let _client_data = client.client_data.as_ref().unwrap();

        //let camera_translate_x = -client.x + (typed_canvas_element.width() / 2) as f64;
        let viewer_size_x = typed_canvas_element.width() as f64;
        //let camera_translate_y = -client.y + (typed_canvas_element.height() / 2) as f64;
        let viewer_size_y = typed_canvas_element.height() as f64;

        typed_canvas_element.style().set_property("background-position", &format!("{}px {}px", -client.x / STARFIELD_RENDER_SCALE, -client.y / STARFIELD_RENDER_SCALE)).map_err(|e| e.as_string().unwrap())?;

        context.set_transform(1f64, 0f64, 0f64, 1f64, 0f64, 0f64).map_err(|e: JsValue| e.as_string().unwrap())?;
        context.clear_rect(0f64, 0f64, viewer_size_x, viewer_size_y);

        // *dont* translate the camera. we're movign everything else around us. cameracentrism.
        // only translation will be to center our core module.
        //context.translate(camera_translate_x, camera_translate_y).map_err(|e: JsValue| e.as_string().unwrap())?;
        context.translate(viewer_size_x / 2.0, viewer_size_y / 2.0).map_err(|e: JsValue| e.as_string().unwrap())?;

        for planet in &client.planets {
            //context.save();

            //context.set_transform(1f64, 0f64, 0f64, 1f64, 0f64, 0f64).map_err(|e: JsValue| e.as_string().unwrap())?;
            //context.translate(-planet.x, -planet.y).map_err(|e: JsValue| e.as_string().unwrap())?;

            let texture_image = document.get_element_by_id(&format!("tex-{}", planet.planet_type.unwrap().as_texture_id())).unwrap().dyn_into::<HtmlImageElement>().unwrap();
            // pos:
            //debug!("P {} {}", planet.x - planet.radius - client.x, planet.y - planet.radius - client.y);
            context.draw_image_with_html_image_element_and_dw_and_dh(&texture_image, (planet.x - planet.radius - client.x as f32) as f64, (planet.y - planet.radius - client.y as f32) as f64, planet.radius as f64 * 2f64, planet.radius as f64 * 2f64).map_err(|e: JsValue| e.as_string().unwrap())?;

            //context.restore();
        }

        for player in &client.players {
            context.save();

            //context.translate(player.x, player.y).map_err(|e: JsValue| e.as_string().unwrap())?;
            //gaah fuck why noo i didnt want this. godforsaken canvas rotation
            context.translate(player.x as f64 - client.x, player.y as f64 - client.y).map_err(|e: JsValue| e.as_string().unwrap())?; // fwip

            context.set_text_align("center");
            context.set_font("30px Segoe UI");
            context.set_fill_style(&JsValue::from_str("white")); // CssStyleColor
            context.fill_text(&player.username, 0f64, -35f64).map_err(|e: JsValue| e.as_string().unwrap())?;

            context.rotate(player.rotation as f64).map_err(|e: JsValue| e.as_string().unwrap())?; // fwip

            let texture_image = document.get_element_by_id("tex-hearty").unwrap().dyn_into::<HtmlImageElement>().unwrap();
            //context.draw_image_with_html_image_element_and_dw_and_dh(&texture_image, player.x - 25f64 - client.x, player.y - 25f64 - client.y, 50f64, 50f64).map_err(|e: JsValue| e.as_string().unwrap())?;
            context.draw_image_with_html_image_element_and_dw_and_dh(&texture_image, -25f64, -25f64, 50f64, 50f64).map_err(|e: JsValue| e.as_string().unwrap())?; // sktch

            //context.rotate(-player.rotation).map_err(|e: JsValue| e.as_string().unwrap())?; // fwoop

            context.restore();
        }

/*
        context.begin_path();

        // Draw the outer circle.
        context
            .arc(75.0, 75.0, 50.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the mouth.
        context.move_to(110.0, 75.0);
        context.arc(75.0, 75.0, 35.0, 0.0, std::f64::consts::PI).unwrap();

        // Draw the left eye.
        context.move_to(65.0, 65.0);
        context
            .arc(60.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        // Draw the right eye.
        context.move_to(95.0, 65.0);
        context
            .arc(90.0, 65.0, 5.0, 0.0, std::f64::consts::PI * 2.0)
            .unwrap();

        context.stroke();
*/
        Ok(())
    }
}

D client/src/rendering/util.rs => client/src/rendering/util.rs +0 -8
@@ 1,8 0,0 @@
use web_sys::HtmlImageElement;
use wasm_bindgen::JsCast;

pub fn texid_to_html_image_unchecked(tex: &str) -> HtmlImageElement {
    let window = web_sys::window().expect("window needs to exist");
    let document = window.document().expect("window.document needs to exist");
    document.get_element_by_id(&format!("tex-{}", tex)).unwrap().dyn_into::<HtmlImageElement>().unwrap()
}
\ No newline at end of file

A client/src/serde.ts => client/src/serde.ts +14 -0
@@ 0,0 1,14 @@
import {PacketWrapper} from "./protocol/starkingdoms-protocol";

export function encode(id: number, pktd: Uint8Array): Uint8Array {
    let pkt = PacketWrapper.encode({
        packetId: id,
        packetData: pktd
    }).finish();
    return pkt;
}

export function decode(pktd: Uint8Array): [number, Uint8Array] {
    let pkt = PacketWrapper.decode(pktd);
    return [pkt.packetId, pkt.packetData];
}
\ No newline at end of file

D client/src/textures/loader_fast.rs => client/src/textures/loader_fast.rs +0 -96
@@ 1,96 0,0 @@
use std::collections::HashMap;
use std::error::Error;
use std::io::Cursor;
use base64::Engine;
use image::ImageOutputFormat;
use log::debug;
use serde::{Deserialize, Serialize};
use crate::textures::{TextureManager, TextureSize};

pub const SPRITESHEET_IMAGE_FILE_FULL: &[u8] = include_bytes!("../../../assets/dist/spritesheet-full.png");
pub const SPRITESHEET_DATA_FILE_FULL: &str = include_str!("../../../assets/dist/spritesheet-full.ron");

pub const SPRITESHEET_IMAGE_FILE_375: &[u8] = include_bytes!("../../../assets/dist/spritesheet-375.png");
pub const SPRITESHEET_DATA_FILE_375: &str = include_str!("../../../assets/dist/spritesheet-375.ron");

pub const SPRITESHEET_IMAGE_FILE_125: &[u8] = include_bytes!("../../../assets/dist/spritesheet-125.png");
pub const SPRITESHEET_DATA_FILE_125: &str = include_str!("../../../assets/dist/spritesheet-125.ron");

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SpritePosition {
    pub name: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub offsets: Option<[f32; 2]>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SerializedSpriteSheet {
    pub texture_width: f32,
    pub texture_height: f32,
    pub sprites: Vec<SpritePosition>,
}

#[derive(Debug)]
pub struct TextureLoader {
    pub sprites: HashMap<String, String>
}
impl TextureManager for TextureLoader {
    fn load(size: TextureSize) -> Result<Self, Box<dyn Error>> where Self: Sized {
        debug!("Loading textures - starting fast texture loader (size: {})", size.to_string());
        let start = js_sys::Date::now() as u64;
        // load the generated spritesheet data
        let spritesheet_data: SerializedSpriteSheet = ron::from_str(pick_data_file(size))?;

        // load the generated spritesheet image
        let spritesheet_image = image::load_from_memory(pick_image_file(size))?;

        if spritesheet_image.width() as f32 != spritesheet_data.texture_width {
            return Err("Image width mismatch between spritesheet and data file".into());
        }
        if spritesheet_image.height() as f32 != spritesheet_data.texture_height {
            return Err("Image height mismatch between spritesheet and data file".into());
        }

        let mut sprites = HashMap::new();

        for sprite in spritesheet_data.sprites {
            debug!("Loading texture {} ({}x{}, start at {}, {})", sprite.name, sprite.width, sprite.height, sprite.x, sprite.y);
            let sprite_img = spritesheet_image.crop_imm(sprite.x as u32, sprite.y as u32, sprite.width as u32, sprite.height as u32);
            let mut image_data: Vec<u8> = Vec::new();
            sprite_img.write_to(&mut Cursor::new(&mut image_data), ImageOutputFormat::Png)
                .unwrap();
            let res_base64 = base64::engine::general_purpose::STANDARD.encode(image_data);
            sprites.insert(sprite.name, format!("data:image/png;base64,{}", res_base64));
        }

        let end = js_sys::Date::now() as u64;
        debug!("Loaded {} sprites from spritesheet in {} ms", sprites.len(), end - start);

        Ok(Self {
            sprites,
        })
    }

    fn get_texture(&self, texture_id: &str) -> Option<String> {
        self.sprites.get(texture_id).map(|u| u.clone())
    }
}

fn pick_data_file(for_size: TextureSize) -> &'static str {
    match for_size {
        TextureSize::Full => SPRITESHEET_DATA_FILE_FULL,
        TextureSize::Scaled375 => SPRITESHEET_DATA_FILE_375,
        TextureSize::Scaled125 => SPRITESHEET_DATA_FILE_125
    }
}

fn pick_image_file(for_size: TextureSize) -> &'static [u8] {
    match for_size {
        TextureSize::Full => SPRITESHEET_IMAGE_FILE_FULL,
        TextureSize::Scaled375 => SPRITESHEET_IMAGE_FILE_375,
        TextureSize::Scaled125 => SPRITESHEET_IMAGE_FILE_125
    }
}
\ No newline at end of file

D client/src/textures/loader_slow.rs => client/src/textures/loader_slow.rs +0 -20
@@ 1,20 0,0 @@
use crate::textures::{TextureManager, TextureSize};
use std::error::Error;
use log::debug;

#[derive(Debug)]
pub struct TextureLoader {
    size: TextureSize
}
impl TextureManager for TextureLoader {
    fn load(size: TextureSize) -> Result<Self, Box<dyn Error>> where Self: Sized {
        debug!("Using slow texture loader, textures will be loaded on-the-fly");
        Ok(TextureLoader {
            size
        })
    }

    fn get_texture(&self, texture_id: &str) -> Option<String> {
        Some(format!("/assets/final/{}/{}.png", self.size.to_string(), texture_id))
    }
}
\ No newline at end of file

D client/src/textures/mod.rs => client/src/textures/mod.rs +0 -48
@@ 1,48 0,0 @@
use std::error::Error;
use std::str::FromStr;

#[cfg(all(feature = "textures-fast", feature = "textures-slow"))]
compile_error!("Mutually exclusive modules textures-fast and textures-slow selected.");
#[cfg(not(any(feature = "textures-fast", feature = "textures-slow")))]
compile_error!("Required feature textures not specified. Please specify one of textures-fast, textures-slow");

#[cfg(feature = "textures-fast")]
#[path = "loader_fast.rs"]
pub mod loader;

#[cfg(feature = "textures-slow")]
#[path = "loader_slow.rs"]
pub mod loader;

pub trait TextureManager {
    fn load(size: TextureSize) -> Result<Self, Box<dyn Error>> where Self: Sized;
    fn get_texture(&self, texture_id: &str) -> Option<String>;
}

#[derive(Debug, Copy, Clone)]
pub enum TextureSize {
    Full,
    Scaled375,
    Scaled125
}
impl ToString for TextureSize {
    fn to_string(&self) -> String {
        match self {
            TextureSize::Full => "full".to_string(),
            TextureSize::Scaled375 => "375".to_string(),
            TextureSize::Scaled125 => "125".to_string()
        }
    }
}
impl FromStr for TextureSize {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "full" => Ok(TextureSize::Full),
            "375" => Ok(TextureSize::Scaled375),
            "125" => Ok(TextureSize::Scaled125),
            _ => Err(())
        }
    }
}
\ No newline at end of file

A client/src/vite-env.d.ts => client/src/vite-env.d.ts +1 -0
@@ 0,0 1,1 @@
/// <reference types="vite/client" />

A client/static/index.css => client/static/index.css +16 -0
@@ 0,0 1,16 @@
.joingamebox {
    width: min-content;
    padding: 10px;
}

.m-5px {
    margin: 5px;
}

.w-full {
    width: 100%;
}

.w-90 {
    width: 90%;
}

A client/static/play.css => client/static/play.css +10 -0
@@ 0,0 1,10 @@
.renderbox {
    position: absolute;
    top: 0;
    left: 0;
}

body {
    margin: 0;
    padding: 0;
}
\ No newline at end of file

A client/tsconfig.json => client/tsconfig.json +19 -0
@@ 0,0 1,19 @@
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "moduleResolution": "Node",
    "strict": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

A client/yarn.lock => client/yarn.lock +379 -0
@@ 0,0 1,379 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@esbuild/android-arm64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz#7b18cab5f4d93e878306196eed26b6d960c12576"
  integrity sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==

"@esbuild/android-arm@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.16.tgz#5c47f6a7c2cada6ed4b4d4e72d8c66e76d812812"
  integrity sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==

"@esbuild/android-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.16.tgz#8686a6e98359071ffd5312046551943e7244c51a"
  integrity sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==

"@esbuild/darwin-arm64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz#aa79fbf447630ca0696a596beba962a775bbf394"
  integrity sha512-/Ofw8UXZxuzTLsNFmz1+lmarQI6ztMZ9XktvXedTbt3SNWDn0+ODTwxExLYQ/Hod91EZB4vZPQJLoqLF0jvEzA==

"@esbuild/darwin-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz#d5d68ee510507104da7e7503224c647c957e163e"
  integrity sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==

"@esbuild/freebsd-arm64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz#b00b4cc8c2e424907cfe3a607384ab24794edd52"
  integrity sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==

"@esbuild/freebsd-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz#84af4430a07730b50bbc945a90cf7036c1853b76"
  integrity sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==

"@esbuild/linux-arm64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz#35571d15de6272c862d9ce6341372fb3cef0f266"
  integrity sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==

"@esbuild/linux-arm@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz#b65c7cd5b0eadd08f91aab66b9dda81b6a4b2a70"
  integrity sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==

"@esbuild/linux-ia32@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz#673a68cb251ce44a00a6422ada29064c5a1cd2c0"
  integrity sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==

"@esbuild/linux-loong64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz#477e2da34ab46ffdbf4740fa6441e80045249385"
  integrity sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==

"@esbuild/linux-mips64el@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz#e1e9687bbdaa831d7c34edc9278200982c1a4bf4"
  integrity sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==

"@esbuild/linux-ppc64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz#2f19075d63622987e86e83a4b7866cd57b796c60"
  integrity sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==

"@esbuild/linux-riscv64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz#bbf40a38f03ba2434fe69b5ceeec5d13c742b329"
  integrity sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==

"@esbuild/linux-s390x@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz#d2b8c0779ccd2b7917cdf0fab8831a468e0f9c01"
  integrity sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==

"@esbuild/linux-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz#da48b39cfdc1b12a74976625f583f031eac43590"
  integrity sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==

"@esbuild/netbsd-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz#ddef985aed37cc81908d2573b66c0299dbc49037"
  integrity sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==

"@esbuild/openbsd-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz#85035bf89efd66e9068bc72aa6bb85a2c317d090"
  integrity sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==

"@esbuild/sunos-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz#16338ecab854cb2d831cc9ee9cc21ef69566e1f3"
  integrity sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==

"@esbuild/win32-arm64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz#423f46bb744aff897a5f74435469e1ef4952e343"
  integrity sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==

"@esbuild/win32-ia32@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz#1978be5b192c7063bd2c8d5960eb213e1964740e"
  integrity sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==

"@esbuild/win32-x64@0.17.16":
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz#260f19b0a3300d22c3a3f52722c671dc561edaa3"
  integrity sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==

"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
  version "1.1.2"
  resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
  integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==

"@protobufjs/base64@^1.1.2":
  version "1.1.2"
  resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
  integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==

"@protobufjs/codegen@^2.0.4":
  version "2.0.4"
  resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
  integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==

"@protobufjs/eventemitter@^1.1.0":
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
  integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==

"@protobufjs/fetch@^1.1.0":
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
  integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
  dependencies:
    "@protobufjs/aspromise" "^1.1.1"
    "@protobufjs/inquire" "^1.1.0"

"@protobufjs/float@^1.0.2":
  version "1.0.2"
  resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
  integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==

"@protobufjs/inquire@^1.1.0":
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
  integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==

"@protobufjs/path@^1.1.2":
  version "1.1.2"
  resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
  integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==

"@protobufjs/pool@^1.1.0":
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
  integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==

"@protobufjs/utf8@^1.1.0":
  version "1.1.0"
  resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
  integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==

"@types/long@^4.0.1":
  version "4.0.2"
  resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
  integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==

"@types/node@>=13.7.0":
  version "18.15.11"
  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
  integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==

"@types/object-hash@^1.3.0":
  version "1.3.4"
  resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b"
  integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA==

case-anything@^2.1.10:
  version "2.1.10"
  resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.10.tgz#d18a6ca968d54ec3421df71e3e190f3bced23410"
  integrity sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==

dataloader@^1.4.0:
  version "1.4.0"
  resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8"
  integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==

detect-libc@^1.0.3:
  version "1.0.3"
  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
  integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==

dprint-node@^1.0.7:
  version "1.0.7"
  resolved "https://registry.yarnpkg.com/dprint-node/-/dprint-node-1.0.7.tgz#f571eaf61affb3a696cff1bdde78a021875ba540"
  integrity sha512-NTZOW9A7ipb0n7z7nC3wftvsbceircwVHSgzobJsEQa+7RnOMbhrfX5IflA6CtC4GA63DSAiHYXa4JKEy9F7cA==
  dependencies:
    detect-libc "^1.0.3"

esbuild@^0.17.5:
  version "0.17.16"
  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.16.tgz#5efec24a8ff29e0c157359f27e1b5532a728b720"
  integrity sha512-aeSuUKr9aFVY9Dc8ETVELGgkj4urg5isYx8pLf4wlGgB0vTFjxJQdHnNH6Shmx4vYYrOTLCHtRI5i1XZ9l2Zcg==
  optionalDependencies:
    "@esbuild/android-arm" "0.17.16"
    "@esbuild/android-arm64" "0.17.16"
    "@esbuild/android-x64" "0.17.16"
    "@esbuild/darwin-arm64" "0.17.16"
    "@esbuild/darwin-x64" "0.17.16"
    "@esbuild/freebsd-arm64" "0.17.16"
    "@esbuild/freebsd-x64" "0.17.16"
    "@esbuild/linux-arm" "0.17.16"
    "@esbuild/linux-arm64" "0.17.16"
    "@esbuild/linux-ia32" "0.17.16"
    "@esbuild/linux-loong64" "0.17.16"
    "@esbuild/linux-mips64el" "0.17.16"
    "@esbuild/linux-ppc64" "0.17.16"
    "@esbuild/linux-riscv64" "0.17.16"
    "@esbuild/linux-s390x" "0.17.16"
    "@esbuild/linux-x64" "0.17.16"
    "@esbuild/netbsd-x64" "0.17.16"
    "@esbuild/openbsd-x64" "0.17.16"
    "@esbuild/sunos-x64" "0.17.16"
    "@esbuild/win32-arm64" "0.17.16"
    "@esbuild/win32-ia32" "0.17.16"
    "@esbuild/win32-x64" "0.17.16"

fsevents@~2.3.2:
  version "2.3.2"
  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

function-bind@^1.1.1:
  version "1.1.1"
  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==

has@^1.0.3:
  version "1.0.3"
  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
  dependencies:
    function-bind "^1.1.1"

is-core-module@^2.11.0:
  version "2.12.0"
  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4"
  integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==
  dependencies:
    has "^1.0.3"

long@^4.0.0:
  version "4.0.0"
  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
  integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==

nanoid@^3.3.4:
  version "3.3.6"
  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
  integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==

object-hash@^1.3.1:
  version "1.3.1"
  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
  integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==

path-parse@^1.0.7:
  version "1.0.7"
  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==

picocolors@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==

postcss@^8.4.21:
  version "8.4.21"
  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
  integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
  dependencies:
    nanoid "^3.3.4"
    picocolors "^1.0.0"
    source-map-js "^1.0.2"

protobufjs@^6.11.3, protobufjs@^6.8.8:
  version "6.11.3"
  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74"
  integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==
  dependencies:
    "@protobufjs/aspromise" "^1.1.2"
    "@protobufjs/base64" "^1.1.2"
    "@protobufjs/codegen" "^2.0.4"
    "@protobufjs/eventemitter" "^1.1.0"
    "@protobufjs/fetch" "^1.1.0"
    "@protobufjs/float" "^1.0.2"
    "@protobufjs/inquire" "^1.1.0"
    "@protobufjs/path" "^1.1.2"
    "@protobufjs/pool" "^1.1.0"
    "@protobufjs/utf8" "^1.1.0"
    "@types/long" "^4.0.1"
    "@types/node" ">=13.7.0"
    long "^4.0.0"

resolve@^1.22.1:
  version "1.22.2"
  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
  integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
  dependencies:
    is-core-module "^2.11.0"
    path-parse "^1.0.7"
    supports-preserve-symlinks-flag "^1.0.0"

rollup@^3.18.0:
  version "3.20.2"
  resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.20.2.tgz#f798c600317f216de2e4ad9f4d9ab30a89b690ff"
  integrity sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==
  optionalDependencies:
    fsevents "~2.3.2"

source-map-js@^1.0.2:
  version "1.0.2"
  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==

supports-preserve-symlinks-flag@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==

ts-poet@^6.4.1:
  version "6.4.1"
  resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-6.4.1.tgz#e68d314a07cf9c0d568a3bfd87023ec91ff77964"
  integrity sha512-AjZEs4h2w4sDfwpHMxQKHrTlNh2wRbM5NRXmLz0RiH+yPGtSQFbe9hBpNocU8vqVNgfh0BIOiXR80xDz3kKxUQ==
  dependencies:
    dprint-node "^1.0.7"

ts-proto-descriptors@1.8.0:
  version "1.8.0"
  resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.8.0.tgz#ba4a26c156a77f76c68d7dac0d049bd2aa3f0920"
  integrity sha512-iV20plcI8+GRkeZIAygxOOH0p2xpOsKfw9kI1W20NCwawi1/4bG/YRd9rQY9XSJP+lD9j7XbSy3tFFuikfsljw==
  dependencies:
    long "^4.0.0"
    protobufjs "^6.8.8"

ts-proto@^1.146.0:
  version "1.146.0"
  resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.146.0.tgz#4017c700976d3657009c4e7e741377a4272ffc27"
  integrity sha512-OyBZRjmqqw+aatLEUbRooWO6VKTtOLJQyaQFMciigEZPNgTsWtApqHpQDtqDMQFWEXhIARqEV+B7ZJx8cljhZA==
  dependencies:
    "@types/object-hash" "^1.3.0"
    case-anything "^2.1.10"
    dataloader "^1.4.0"
    object-hash "^1.3.1"
    protobufjs "^6.11.3"
    ts-poet "^6.4.1"
    ts-proto-descriptors "1.8.0"

typescript@^4.9.3:
  version "4.9.5"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
  integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==

vite@^4.2.0:
  version "4.2.1"
  resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.1.tgz#6c2eb337b0dfd80a9ded5922163b94949d7fc254"
  integrity sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==
  dependencies:
    esbuild "^0.17.5"
    postcss "^8.4.21"
    resolve "^1.22.1"
    rollup "^3.18.0"
  optionalDependencies:
    fsevents "~2.3.2"

M server/src/handler.rs => server/src/handler.rs +1 -0
@@ 78,6 78,7 @@ pub async fn handle_client(mgr: ClientManager, data: Arc<RwLock<PhysicsData>>, r
                State::Handshake => {
                    match pkt {
                        MessageC2S::Hello(pkt) => {
                            info!("client sent hello");
                            if !matches!(pkt.next_state.unwrap(), State::Play) {
                                error!("client sent unexpected state {:?} (expected: Play)", pkt.next_state);
                                let msg = MessageS2C::Goodbye(MessageS2CGoodbye {

M spacetime => spacetime +6 -29
@@ 4,7 4,6 @@ set -e

SCRIPT_PATH=$(readlink -f "${BASH_SOURCE:-$0}")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
CLIENT_MODS=${STK_CLIENT_MODULES:-"textures-slow renderer-playercentric"}
SERVER_MODS=${STK_SERVER_MODULES:=""}

exec_spacetime() {


@@ 25,13 24,10 @@ sub_help() {
  echo "Available targets:"
  echo "    help - Show this help screen" # done
  echo "    run_http - Compile the client and run a development http server for testing it" # done
  echo "    run_http_prod - Compile the client in production mode and run a development http server for testing it" # done
  echo "    run_server (default) - Compile and run the game server" # done
  echo "    build_server - Compile the game server" # done
  echo "    run_server_prod - Compile and run the game server with optimizations enabled" # done
  echo "    build_server_prod - Compile the game server with optimizations enabled" # done
  echo "    build_client_bundle - Compile an optimized WebAssembly client bundle" # done
  echo "    build_client_bundle_prod - Compile an optimized WebAssembly client bundle using textures-fast" # done
  echo "    install_tooling - Install the compilation utilities required for compiling StarKingdoms" # done
  echo "    build_assets - Compile spritesheets in all three texture sizes for textures-fast" # done
  echo "    build_assets_full - Compile spritesheets in full size for textures-fast" # done


@@ 59,9 55,9 @@ check() {
}

check_all() {
  check wasm-pack
  check sheep
  check inkscape
  check protoc
  check atlasify
}

sub_clean() {


@@ 73,36 69,17 @@ sub_clean() {
}

sub_install_tooling() {
  check_install_cargo wasm-pack wasm-pack --no-default-features
  check_install_cargo sheep sheep_cli
  check inkscape
  check protoc
  check atlasify
  echo "[*] All required tools are installed"
}

sub_build_client_bundle() {
  check_all
  exec_spacetime client dev "$SCRIPT_DIR" "$CLIENT_MODS"
  exec_ninja client
}

sub_run_http() {
  check_all
  exec_spacetime client dev "$SCRIPT_DIR" "$CLIENT_MODS"
  exec_ninja client
  cd web && python3 -m http.server
}

sub_build_client_bundle_prod() {
  check_all
  exec_spacetime client prod "$SCRIPT_DIR" "$CLIENT_MODS"
  exec_ninja client
}

sub_run_http_prod() {
  check_all
  exec_spacetime client prod "$SCRIPT_DIR" "$CLIENT_MODS"
  exec_ninja client
  cd web && python3 -m http.server
  exec_ninja asset
  cd client && yarn && yarn run dev
}

sub_build_server() {

M spacetime_py/spacetime.py => spacetime_py/spacetime.py +5 -66
@@ 57,64 57,6 @@ def gen_inkscape_rules_for_asset(root, asset, writer, files_375, files_full, fil
    writer.build([out_375], rule_375, [in_file])
    writer.build([out_125], rule_125, [in_file])


def gen_rules_for_client(root, env, writer, modules):
    if env == 'dev':
        writer.rule('cargo-client', f'wasm-pack build --target web client --features "{modules}"',
                    depfile=f'{root}/target/wasm32-unknown-unknown/release/starkingdoms_client.d', pool='console')
    elif env == 'prod':
        writer.rule('cargo-client',
                    f'wasm-pack build --target web client --features "{modules}"',
                    depfile=f'{root}/target/wasm32-unknown-unknown/release/starkingdoms_client.d', pool='console')
    writer.build(
        [
            f'{root}/client/pkg/.gitignore',
            f'{root}/client/pkg/package.json',
            f'{root}/client/pkg/starkingdoms_client.d.ts',
            f'{root}/client/pkg/starkingdoms_client.js',
            f'{root}/client/pkg/starkingdoms_client_bg.wasm',
            f'{root}/client/pkg/starkingdoms_client_bg.wasm.d.ts',
            f'{root}/target/wasm32-unknown-unknown/release/starkingdoms_client.wasm'
        ],
        'cargo-client',
        [
            f'{root}/assets/dist/spritesheet-125',
            f'{root}/assets/dist/spritesheet-125.png',
            f'{root}/assets/dist/spritesheet-125.ron',
            f'{root}/assets/dist/spritesheet-375',
            f'{root}/assets/dist/spritesheet-375.png',
            f'{root}/assets/dist/spritesheet-375.ron',
            f'{root}/assets/dist/spritesheet-full',
            f'{root}/assets/dist/spritesheet-full.png',
            f'{root}/assets/dist/spritesheet-full.ron',
        ]
    )

    writer.rule('client-copy-file', f'rm -rf {root}/web/dist && cp -r {root}/client/pkg {root}/web/dist')

    writer.build(
        [
            f'{root}/web/dist/.gitignore',
            f'{root}/web/dist/package.json',
            f'{root}/web/dist/starkingdoms_client.d.ts',
            f'{root}/web/dist/starkingdoms_client.js',
            f'{root}/web/dist/starkingdoms_client_bg.wasm',
            f'{root}/web/dist/starkingdoms_client_bg.wasm.d.ts'
        ],
        'client-copy-file',
        [
            f'{root}/client/pkg/.gitignore',
            f'{root}/client/pkg/package.json',
            f'{root}/client/pkg/starkingdoms_client.d.ts',
            f'{root}/client/pkg/starkingdoms_client.js',
            f'{root}/client/pkg/starkingdoms_client_bg.wasm',
            f'{root}/client/pkg/starkingdoms_client_bg.wasm.d.ts',
        ],
    )

    writer.build(['client'], 'phony', [f'{root}/web/dist/starkingdoms_client.js'])


def gen_rules_for_server(root, env, writer, modules):
    if env == 'dev':
        out_dir = 'debug'


@@ 138,21 80,21 @@ def gen_inkscape(root, assets, writer, files_375, files_full, files_125):

def gen_packers(root, writer, files_375, files_full, files_125):
    # sheep pack assets/final/full/*.png -f amethyst_named -o assets/dist/spritesheet-full
    writer.rule(f'pack', 'sheep pack -f amethyst_named -o $out $in && touch $out')
    writer.rule(f'pack', 'cd assets/dist && atlasify -m 4096,4096 -o $out $in && touch $out')

    writer.build(f'{root}/assets/dist/spritesheet-full', 'pack', inputs=files_full,
                 implicit_outputs=[f'{root}/assets/dist/spritesheet-full.png',
                                   f'{root}/assets/dist/spritesheet-full.ron'])
                                   f'{root}/assets/dist/spritesheet-full.json'])
    writer.build(f'asset-full', 'phony', inputs=[f'{root}/assets/dist/spritesheet-full'])

    writer.build(f'{root}/assets/dist/spritesheet-375', 'pack', inputs=files_375,
                 implicit_outputs=[f'{root}/assets/dist/spritesheet-375.png',
                                   f'{root}/assets/dist/spritesheet-375.ron'])
                                   f'{root}/assets/dist/spritesheet-375.json'])
    writer.build(f'asset-375', 'phony', inputs=[f'{root}/assets/dist/spritesheet-375'])

    writer.build(f'{root}/assets/dist/spritesheet-125', 'pack', inputs=files_125,
                 implicit_outputs=[f'{root}/assets/dist/spritesheet-125.png',
                                   f'{root}/assets/dist/spritesheet-125.ron'])
                                   f'{root}/assets/dist/spritesheet-125.json'])
    writer.build(f'asset-125', 'phony', inputs=[f'{root}/assets/dist/spritesheet-125'])

    writer.build(f'asset', 'phony',


@@ 190,14 132,11 @@ def main():
        writer.comment('Generated by spacetime.py')
        writer.comment('Do not manually edit this file')

        if env == 'prod' or target == 'asset':
        if target == 'client' or target == 'asset':
            assets = scan_assets(root)
            if verbose:
                print(f'[spacetime -v] discovered assets: {assets}')
            generate_assets_build_command(root, assets, writer)

        if target == 'client':
            gen_rules_for_client(root, env, writer, modules)
        elif target == 'server':
            gen_rules_for_server(root, env, writer, modules)