From e06c4882a075b018578d4d0feb362a23b59a0328 Mon Sep 17 00:00:00 2001 From: Kalle Struik Date: Wed, 5 Mar 2025 12:21:53 +0100 Subject: [PATCH] Initial commit --- .envrc | 12 + .gitignore | 3 + Cargo.lock | 233 ++++++ Cargo.toml | 13 + flake.lock | 316 ++++++++ flake.nix | 59 ++ potato-protocol-derive/Cargo.toml | 13 + potato-protocol-derive/src/lib.rs | 216 ++++++ potato-protocol/Cargo.toml | 21 + potato-protocol/build.rs | 65 ++ potato-protocol/packets.json | 732 ++++++++++++++++++ potato-protocol/src/datatypes/byte_array.rs | 30 + potato-protocol/src/datatypes/identifier.rs | 77 ++ potato-protocol/src/datatypes/mod.rs | 5 + potato-protocol/src/datatypes/pack.rs | 8 + potato-protocol/src/datatypes/position.rs | 52 ++ potato-protocol/src/datatypes/var_int.rs | 78 ++ potato-protocol/src/lib.rs | 7 + .../clientbound/finish_configuration.rs | 5 + .../src/packet/clientbound/game_event.rs | 8 + .../src/packet/clientbound/login.rs | 34 + .../packet/clientbound/login_disconnect.rs | 8 + .../src/packet/clientbound/login_finished.rs | 17 + potato-protocol/src/packet/clientbound/mod.rs | 21 + .../src/packet/clientbound/pong_response.rs | 7 + .../src/packet/clientbound/registry_data.rs | 129 +++ .../packet/clientbound/select_known_packs.rs | 9 + .../clientbound/set_chunk_cache_center.rs | 10 + .../src/packet/clientbound/status_response.rs | 43 + potato-protocol/src/packet/mod.rs | 12 + .../packet/serverbound/client_information.rs | 84 ++ .../src/packet/serverbound/client_tick_end.rs | 5 + .../src/packet/serverbound/custom_payload.rs | 13 + .../serverbound/finish_configuration.rs | 5 + .../src/packet/serverbound/hello.rs | 9 + .../src/packet/serverbound/intention.rs | 12 + .../packet/serverbound/login_acknowledged.rs | 5 + potato-protocol/src/packet/serverbound/mod.rs | 27 + .../src/packet/serverbound/move_player_pos.rs | 10 + .../packet/serverbound/move_player_pos_rot.rs | 12 + .../src/packet/serverbound/move_player_rot.rs | 9 + .../src/packet/serverbound/ping_request.rs | 7 + .../packet/serverbound/select_known_packs.rs | 9 + .../src/packet/serverbound/status_request.rs | 5 + potato-protocol/src/packet_encodable/mod.rs | 235 ++++++ potato/Cargo.toml | 12 + potato/src/main.rs | 544 +++++++++++++ 47 files changed, 3246 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 potato-protocol-derive/Cargo.toml create mode 100644 potato-protocol-derive/src/lib.rs create mode 100644 potato-protocol/Cargo.toml create mode 100644 potato-protocol/build.rs create mode 100644 potato-protocol/packets.json create mode 100644 potato-protocol/src/datatypes/byte_array.rs create mode 100644 potato-protocol/src/datatypes/identifier.rs create mode 100644 potato-protocol/src/datatypes/mod.rs create mode 100644 potato-protocol/src/datatypes/pack.rs create mode 100644 potato-protocol/src/datatypes/position.rs create mode 100644 potato-protocol/src/datatypes/var_int.rs create mode 100644 potato-protocol/src/lib.rs create mode 100644 potato-protocol/src/packet/clientbound/finish_configuration.rs create mode 100644 potato-protocol/src/packet/clientbound/game_event.rs create mode 100644 potato-protocol/src/packet/clientbound/login.rs create mode 100644 potato-protocol/src/packet/clientbound/login_disconnect.rs create mode 100644 potato-protocol/src/packet/clientbound/login_finished.rs create mode 100644 potato-protocol/src/packet/clientbound/mod.rs create mode 100644 potato-protocol/src/packet/clientbound/pong_response.rs create mode 100644 potato-protocol/src/packet/clientbound/registry_data.rs create mode 100644 potato-protocol/src/packet/clientbound/select_known_packs.rs create mode 100644 potato-protocol/src/packet/clientbound/set_chunk_cache_center.rs create mode 100644 potato-protocol/src/packet/clientbound/status_response.rs create mode 100644 potato-protocol/src/packet/mod.rs create mode 100644 potato-protocol/src/packet/serverbound/client_information.rs create mode 100644 potato-protocol/src/packet/serverbound/client_tick_end.rs create mode 100644 potato-protocol/src/packet/serverbound/custom_payload.rs create mode 100644 potato-protocol/src/packet/serverbound/finish_configuration.rs create mode 100644 potato-protocol/src/packet/serverbound/hello.rs create mode 100644 potato-protocol/src/packet/serverbound/intention.rs create mode 100644 potato-protocol/src/packet/serverbound/login_acknowledged.rs create mode 100644 potato-protocol/src/packet/serverbound/mod.rs create mode 100644 potato-protocol/src/packet/serverbound/move_player_pos.rs create mode 100644 potato-protocol/src/packet/serverbound/move_player_pos_rot.rs create mode 100644 potato-protocol/src/packet/serverbound/move_player_rot.rs create mode 100644 potato-protocol/src/packet/serverbound/ping_request.rs create mode 100644 potato-protocol/src/packet/serverbound/select_known_packs.rs create mode 100644 potato-protocol/src/packet/serverbound/status_request.rs create mode 100644 potato-protocol/src/packet_encodable/mod.rs create mode 100644 potato/Cargo.toml create mode 100644 potato/src/main.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1587a60 --- /dev/null +++ b/.envrc @@ -0,0 +1,12 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" +fi + +watch_file devenv.nix +watch_file devenv.lock +watch_file devenv.yaml +if ! use flake . --impure +then + echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 +fi + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..710d3d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ +.direnv/ +.devenv/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bbc2d8b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,233 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "fastnbt" +version = "2.5.0" +source = "git+https://github.com/owengage/fastnbt.git#e2a5d8a7001d4f074ae99fd21bb485667934baeb" +dependencies = [ + "byteorder", + "cesu8", + "serde", + "serde_bytes", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "potato" +version = "0.1.0" +dependencies = [ + "potato-protocol", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "potato-protocol" +version = "0.1.0" +dependencies = [ + "byteorder", + "fastnbt", + "potato-protocol-derive", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "potato-protocol-derive" +version = "0.1.0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364fec0df39c49a083c9a8a18a23a6bcfd9af130fe9fe321d18520a0d113e09e" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..52d7033 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +resolver = "2" +members = ["potato", "potato-protocol", "potato-protocol-derive"] + +[workspace.package] +version = "0.1.0" +edition = "2024" + +[workspace.dependencies] +serde = { version = "1.0.218", features = ["derive"] } +serde_json = "1.0.140" +thiserror = "2.0.11" +uuid = "1.15.1" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bd273aa --- /dev/null +++ b/flake.lock @@ -0,0 +1,316 @@ +{ + "nodes": { + "cachix": { + "inputs": { + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv" + ], + "git-hooks": [ + "devenv" + ], + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1737621947, + "narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=", + "owner": "cachix", + "repo": "cachix", + "rev": "f65a3cd5e339c223471e64c051434616e18cc4f5", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", + "nix": "nix", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1741068816, + "narHash": "sha256-JvaktGlQ/j+7+sbcl1OHcQmht7w+7AGDVmHldCezUkc=", + "owner": "cachix", + "repo": "devenv", + "rev": "9f6da63c162ad86b6fb84edcbd8c447fdc411c3d", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "main", + "repo": "devenv", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1741156584, + "narHash": "sha256-Xju6PhR09gR8cSS1s4FOHw4AhUUmrFDUs9Wj/9KFoGY=", + "owner": "nix-community", + "repo": "fenix", + "rev": "1271797d7c0537b4e5bdd4061a2954b846f2c29c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "devenv" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1740849354, + "narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "libgit2": { + "flake": false, + "locked": { + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", + "type": "github" + }, + "original": { + "owner": "libgit2", + "repo": "libgit2", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": [ + "devenv" + ], + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ], + "pre-commit-hooks": [ + "devenv" + ] + }, + "locked": { + "lastModified": 1734114420, + "narHash": "sha256-n52PUzub5jZWc8nI/sR7UICOheU8rNA+YZ73YaHeCBg=", + "owner": "domenkozar", + "repo": "nix", + "rev": "bde6a1a0d1f2af86caa4d20d23eca019f3d57eee", + "type": "github" + }, + "original": { + "owner": "domenkozar", + "ref": "devenv-2.24", + "repo": "nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1733477122, + "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1741010256, + "narHash": "sha256-WZNlK/KX7Sni0RyqLSqLPbK8k08Kq7H7RijPJbq9KHM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ba487dbc9d04e0634c64e3b1f0d25839a0a68246", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "fenix": "fenix", + "nixpkgs": "nixpkgs_4", + "systems": "systems" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1741011961, + "narHash": "sha256-bssSxw3Z9CUNB9+f3EHAX/2urT15e12Jy6YU8tHyWkk=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "02862f5d52c30b476a5dca909a17aa4386d1fdc5", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7b0fe7d --- /dev/null +++ b/flake.nix @@ -0,0 +1,59 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default"; + devenv.url = "github:cachix/devenv/main"; + + fenix.url = "github:nix-community/fenix"; + fenix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + devenv, + systems, + ... + }@inputs: + let + forEachSystem = nixpkgs.lib.genAttrs (import systems); + in + { + packages = forEachSystem (system: { + devenv-up = self.devShells.${system}.default.config.procfileScript; + }); + + devShells = forEachSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = devenv.lib.mkShell { + inherit inputs pkgs; + modules = [ + ( + { ... }: + { + dotenv.disableHint = true; + + languages.rust = { + enable = true; + channel = "nightly"; + components = [ + "rustc" + "cargo" + "clippy" + "rustfmt" + "rust-analyzer" + ]; + }; + } + ) + ]; + }; + } + ); + }; +} diff --git a/potato-protocol-derive/Cargo.toml b/potato-protocol-derive/Cargo.toml new file mode 100644 index 0000000..2669da1 --- /dev/null +++ b/potato-protocol-derive/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "potato-protocol-derive" +version.workspace = true +edition.workspace = true + +[dependencies] +darling = "0.20.10" +proc-macro2 = "1.0.94" +quote = "1.0.39" +syn = "2.0.99" + +[lib] +proc-macro = true diff --git a/potato-protocol-derive/src/lib.rs b/potato-protocol-derive/src/lib.rs new file mode 100644 index 0000000..4c34726 --- /dev/null +++ b/potato-protocol-derive/src/lib.rs @@ -0,0 +1,216 @@ +#![recursion_limit = "128"] + +use darling::{FromDeriveInput, FromVariant}; +use quote::ToTokens; +use syn::Path; + +extern crate proc_macro; +extern crate syn; +#[macro_use] +extern crate quote; + +#[derive(FromDeriveInput)] +#[darling(attributes(packet))] +struct PacketOpts { + handshake_id: Option, + status_id: Option, + login_id: Option, + configuration_id: Option, + play_id: Option, +} + +#[proc_macro_derive(Packet, attributes(packet))] +pub fn packet_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse(input).unwrap(); + let opts = PacketOpts::from_derive_input(&ast).expect("Wrong options"); + let packet_encodable_impl = generate_packet_encodable_impl(&ast); + let packet_impl = generate_packet_impl(&ast, opts); + + quote! { + #packet_encodable_impl + #packet_impl + } + .into() +} + +#[proc_macro_derive(PacketEncodable, attributes(packet))] +pub fn packet_encodable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse(input).unwrap(); + let generated = generate_packet_encodable_impl(&ast); + + generated.into() +} + +#[proc_macro_derive(JsonString)] +pub fn json_string_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + let ident = &ast.ident; + + quote! { + impl crate::packet_encodable::JsonString for #ident {} + } + .into() +} + +fn generate_packet_impl( + ast: &syn::DeriveInput, + packet_opts: PacketOpts, +) -> proc_macro2::TokenStream { + let ident = &ast.ident; + + let handshake_id = packet_opts + .handshake_id + .map(ToTokens::into_token_stream) + .unwrap_or(quote! {-1}); + + let status_id = packet_opts + .status_id + .map(ToTokens::into_token_stream) + .unwrap_or(quote! {-1}); + + let login_id = packet_opts + .login_id + .map(ToTokens::into_token_stream) + .unwrap_or(quote! {-1}); + + let configuration_id = packet_opts + .configuration_id + .map(ToTokens::into_token_stream) + .unwrap_or(quote! {-1}); + + let play_id = packet_opts + .play_id + .map(ToTokens::into_token_stream) + .unwrap_or(quote! {-1}); + + quote! { + impl crate::packet::Packet for #ident { + const HANDSHAKE_ID: i32 = #handshake_id; + const STATUS_ID: i32 = #status_id; + const LOGIN_ID: i32 = #login_id; + const CONFIGURATION_ID: i32 = #configuration_id; + const PLAY_ID: i32 = #play_id; + } + } +} + +fn generate_packet_encodable_impl(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { + match ast.data { + syn::Data::Enum(syn::DataEnum { ref variants, .. }) => { + generate_packet_encodable_enum_impl(ast, variants) + } + syn::Data::Struct(syn::DataStruct { ref fields, .. }) => { + generate_packet_encodable_struct_impl(ast, fields) + } + + syn::Data::Union(_) => panic!("PacketEncodable can not be implemented for unions"), + } +} + +fn generate_packet_encodable_enum_impl( + ast: &syn::DeriveInput, + variants: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let ident = &ast.ident; + let encode_arms: Vec<_> = variants + .iter() + .map(generate_enum_variant_encode_packet_arms) + .collect(); + let decode_arms: Vec<_> = variants + .iter() + .map(generate_enum_variant_decode_packet_arms) + .collect(); + quote! { + impl crate::packet_encodable::PacketEncodable for #ident { + fn encode_packet(&self, cursor: &mut Vec) -> Result<(), crate::packet_encodable::PacketEncodeError> { + let value: crate::datatypes::var_int::VarInt = match self { + #(#encode_arms)* + }.into(); + + value.encode_packet(cursor) + } + + fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let value: i32 = crate::datatypes::var_int::VarInt::decode_packet(cursor)?.into(); + match value { + #(#decode_arms)* + _ => Err(crate::packet_encodable::PacketDecodeError::UnkownEnumVariant { + name: stringify!(#ident), + value, + }), + } + } + } + } +} + +fn generate_packet_encodable_struct_impl( + ast: &syn::DeriveInput, + fields: &syn::Fields, +) -> proc_macro2::TokenStream { + let ident = &ast.ident; + let fields_encode_packet_calls: Vec<_> = fields + .iter() + .map(generate_struct_field_encode_packet_calls) + .collect(); + let fields_decode_packet_calls: Vec<_> = fields + .iter() + .map(generate_struct_field_decode_packet_calls) + .collect(); + + quote! { + impl crate::packet_encodable::PacketEncodable for #ident { + fn encode_packet(&self, cursor: &mut Vec) -> Result<(), crate::packet_encodable::PacketEncodeError> { + #(#fields_encode_packet_calls)* + Ok(()) + } + + fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + Ok(Self { + #(#fields_decode_packet_calls)* + }) + } + } + } +} + +fn generate_struct_field_decode_packet_calls(field: &syn::Field) -> proc_macro2::TokenStream { + let field_ident = &field.ident; + let field_ty = &field.ty; + + quote! { + #field_ident: <#field_ty>::decode_packet(cursor)?, + } +} + +fn generate_struct_field_encode_packet_calls(field: &syn::Field) -> proc_macro2::TokenStream { + let field_ident = &field.ident; + + quote! { + self.#field_ident.encode_packet(cursor)?; + } +} + +#[derive(FromVariant)] +#[darling(attributes(packet))] +struct EnumVariant { + id: i32, +} + +fn generate_enum_variant_decode_packet_arms(variant: &syn::Variant) -> proc_macro2::TokenStream { + let ident = &variant.ident; + let enum_id = EnumVariant::from_variant(variant).unwrap().id; + + quote! { + #enum_id => Ok(Self::#ident), + } +} + +fn generate_enum_variant_encode_packet_arms(variant: &syn::Variant) -> proc_macro2::TokenStream { + let ident = &variant.ident; + let enum_id = EnumVariant::from_variant(variant).unwrap().id; + + quote! { + Self::#ident => #enum_id, + } +} diff --git a/potato-protocol/Cargo.toml b/potato-protocol/Cargo.toml new file mode 100644 index 0000000..72793e4 --- /dev/null +++ b/potato-protocol/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "potato-protocol" +version.workspace = true +edition.workspace = true + +[dependencies] +potato-protocol-derive = { path = "../potato-protocol-derive" } + +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +uuid.workspace = true + +byteorder = "1.5.0" + +# Build from git, since there has not been a release in over a year +fastnbt = { git = "https://github.com/owengage/fastnbt.git" } + +[build-dependencies] +serde.workspace = true +serde_json.workspace = true diff --git a/potato-protocol/build.rs b/potato-protocol/build.rs new file mode 100644 index 0000000..c6402b4 --- /dev/null +++ b/potato-protocol/build.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +const PACKETS_STR: &'static str = include_str!("packets.json"); + +fn main() { + println!("cargo::rerun-if-changed=packets.json"); + + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let out_path = std::path::Path::new(&out_dir).join("ids.rs"); + + let packets: Packets = serde_json::from_str(PACKETS_STR).unwrap(); + + let out_string = &mut String::new(); + write_phase("handshake", &packets.handshake, out_string); + write_phase("status", &packets.status, out_string); + write_phase("login", &packets.login, out_string); + write_phase("configuration", &packets.configuration, out_string); + write_phase("play", &packets.play, out_string); + + std::fs::write(&out_path, out_string).unwrap(); +} + +fn write_phase(name: &str, phase: &Phase, out: &mut String) { + out.push_str(&format!("pub mod {} {{\n", name)); + if let Some(packets) = &phase.clientbound { + write_side("clientbound", packets, out); + } + + if let Some(packets) = &phase.serverbound { + write_side("serverbound", packets, out); + } + out.push_str("}\n"); +} + +fn write_side(side: &str, packets: &HashMap, out: &mut String) { + out.push_str(&format!("pub mod {} {{\n", side.to_lowercase())); + for (name, packet) in packets { + out.push_str(&format!( + "pub const {}: i32 = 0x{:02X};\n", + name.replace("minecraft:", "").to_uppercase(), + packet.protocol_id + )); + } + out.push_str("}\n"); +} + +#[derive(serde::Deserialize)] +struct Packets { + handshake: Phase, + status: Phase, + login: Phase, + configuration: Phase, + play: Phase, +} + +#[derive(serde::Deserialize)] +struct Phase { + clientbound: Option>, + serverbound: Option>, +} + +#[derive(serde::Deserialize)] +struct Packet { + protocol_id: i32, +} diff --git a/potato-protocol/packets.json b/potato-protocol/packets.json new file mode 100644 index 0000000..6dc0e25 --- /dev/null +++ b/potato-protocol/packets.json @@ -0,0 +1,732 @@ +{ + "configuration": { + "clientbound": { + "minecraft:cookie_request": { + "protocol_id": 0 + }, + "minecraft:custom_payload": { + "protocol_id": 1 + }, + "minecraft:custom_report_details": { + "protocol_id": 15 + }, + "minecraft:disconnect": { + "protocol_id": 2 + }, + "minecraft:finish_configuration": { + "protocol_id": 3 + }, + "minecraft:keep_alive": { + "protocol_id": 4 + }, + "minecraft:ping": { + "protocol_id": 5 + }, + "minecraft:registry_data": { + "protocol_id": 7 + }, + "minecraft:reset_chat": { + "protocol_id": 6 + }, + "minecraft:resource_pack_pop": { + "protocol_id": 8 + }, + "minecraft:resource_pack_push": { + "protocol_id": 9 + }, + "minecraft:select_known_packs": { + "protocol_id": 14 + }, + "minecraft:server_links": { + "protocol_id": 16 + }, + "minecraft:store_cookie": { + "protocol_id": 10 + }, + "minecraft:transfer": { + "protocol_id": 11 + }, + "minecraft:update_enabled_features": { + "protocol_id": 12 + }, + "minecraft:update_tags": { + "protocol_id": 13 + } + }, + "serverbound": { + "minecraft:client_information": { + "protocol_id": 0 + }, + "minecraft:cookie_response": { + "protocol_id": 1 + }, + "minecraft:custom_payload": { + "protocol_id": 2 + }, + "minecraft:finish_configuration": { + "protocol_id": 3 + }, + "minecraft:keep_alive": { + "protocol_id": 4 + }, + "minecraft:pong": { + "protocol_id": 5 + }, + "minecraft:resource_pack": { + "protocol_id": 6 + }, + "minecraft:select_known_packs": { + "protocol_id": 7 + } + } + }, + "handshake": { + "serverbound": { + "minecraft:intention": { + "protocol_id": 0 + } + } + }, + "login": { + "clientbound": { + "minecraft:cookie_request": { + "protocol_id": 5 + }, + "minecraft:custom_query": { + "protocol_id": 4 + }, + "minecraft:hello": { + "protocol_id": 1 + }, + "minecraft:login_compression": { + "protocol_id": 3 + }, + "minecraft:login_disconnect": { + "protocol_id": 0 + }, + "minecraft:login_finished": { + "protocol_id": 2 + } + }, + "serverbound": { + "minecraft:cookie_response": { + "protocol_id": 4 + }, + "minecraft:custom_query_answer": { + "protocol_id": 2 + }, + "minecraft:hello": { + "protocol_id": 0 + }, + "minecraft:key": { + "protocol_id": 1 + }, + "minecraft:login_acknowledged": { + "protocol_id": 3 + } + } + }, + "play": { + "clientbound": { + "minecraft:add_entity": { + "protocol_id": 1 + }, + "minecraft:add_experience_orb": { + "protocol_id": 2 + }, + "minecraft:animate": { + "protocol_id": 3 + }, + "minecraft:award_stats": { + "protocol_id": 4 + }, + "minecraft:block_changed_ack": { + "protocol_id": 5 + }, + "minecraft:block_destruction": { + "protocol_id": 6 + }, + "minecraft:block_entity_data": { + "protocol_id": 7 + }, + "minecraft:block_event": { + "protocol_id": 8 + }, + "minecraft:block_update": { + "protocol_id": 9 + }, + "minecraft:boss_event": { + "protocol_id": 10 + }, + "minecraft:bundle_delimiter": { + "protocol_id": 0 + }, + "minecraft:change_difficulty": { + "protocol_id": 11 + }, + "minecraft:chunk_batch_finished": { + "protocol_id": 12 + }, + "minecraft:chunk_batch_start": { + "protocol_id": 13 + }, + "minecraft:chunks_biomes": { + "protocol_id": 14 + }, + "minecraft:clear_titles": { + "protocol_id": 15 + }, + "minecraft:command_suggestions": { + "protocol_id": 16 + }, + "minecraft:commands": { + "protocol_id": 17 + }, + "minecraft:container_close": { + "protocol_id": 18 + }, + "minecraft:container_set_content": { + "protocol_id": 19 + }, + "minecraft:container_set_data": { + "protocol_id": 20 + }, + "minecraft:container_set_slot": { + "protocol_id": 21 + }, + "minecraft:cookie_request": { + "protocol_id": 22 + }, + "minecraft:cooldown": { + "protocol_id": 23 + }, + "minecraft:custom_chat_completions": { + "protocol_id": 24 + }, + "minecraft:custom_payload": { + "protocol_id": 25 + }, + "minecraft:custom_report_details": { + "protocol_id": 129 + }, + "minecraft:damage_event": { + "protocol_id": 26 + }, + "minecraft:debug_sample": { + "protocol_id": 27 + }, + "minecraft:delete_chat": { + "protocol_id": 28 + }, + "minecraft:disconnect": { + "protocol_id": 29 + }, + "minecraft:disguised_chat": { + "protocol_id": 30 + }, + "minecraft:entity_event": { + "protocol_id": 31 + }, + "minecraft:entity_position_sync": { + "protocol_id": 32 + }, + "minecraft:explode": { + "protocol_id": 33 + }, + "minecraft:forget_level_chunk": { + "protocol_id": 34 + }, + "minecraft:game_event": { + "protocol_id": 35 + }, + "minecraft:horse_screen_open": { + "protocol_id": 36 + }, + "minecraft:hurt_animation": { + "protocol_id": 37 + }, + "minecraft:initialize_border": { + "protocol_id": 38 + }, + "minecraft:keep_alive": { + "protocol_id": 39 + }, + "minecraft:level_chunk_with_light": { + "protocol_id": 40 + }, + "minecraft:level_event": { + "protocol_id": 41 + }, + "minecraft:level_particles": { + "protocol_id": 42 + }, + "minecraft:light_update": { + "protocol_id": 43 + }, + "minecraft:login": { + "protocol_id": 44 + }, + "minecraft:map_item_data": { + "protocol_id": 45 + }, + "minecraft:merchant_offers": { + "protocol_id": 46 + }, + "minecraft:move_entity_pos": { + "protocol_id": 47 + }, + "minecraft:move_entity_pos_rot": { + "protocol_id": 48 + }, + "minecraft:move_entity_rot": { + "protocol_id": 50 + }, + "minecraft:move_minecart_along_track": { + "protocol_id": 49 + }, + "minecraft:move_vehicle": { + "protocol_id": 51 + }, + "minecraft:open_book": { + "protocol_id": 52 + }, + "minecraft:open_screen": { + "protocol_id": 53 + }, + "minecraft:open_sign_editor": { + "protocol_id": 54 + }, + "minecraft:ping": { + "protocol_id": 55 + }, + "minecraft:place_ghost_recipe": { + "protocol_id": 57 + }, + "minecraft:player_abilities": { + "protocol_id": 58 + }, + "minecraft:player_chat": { + "protocol_id": 59 + }, + "minecraft:player_combat_end": { + "protocol_id": 60 + }, + "minecraft:player_combat_enter": { + "protocol_id": 61 + }, + "minecraft:player_combat_kill": { + "protocol_id": 62 + }, + "minecraft:player_info_remove": { + "protocol_id": 63 + }, + "minecraft:player_info_update": { + "protocol_id": 64 + }, + "minecraft:player_look_at": { + "protocol_id": 65 + }, + "minecraft:player_position": { + "protocol_id": 66 + }, + "minecraft:player_rotation": { + "protocol_id": 67 + }, + "minecraft:pong_response": { + "protocol_id": 56 + }, + "minecraft:projectile_power": { + "protocol_id": 128 + }, + "minecraft:recipe_book_add": { + "protocol_id": 68 + }, + "minecraft:recipe_book_remove": { + "protocol_id": 69 + }, + "minecraft:recipe_book_settings": { + "protocol_id": 70 + }, + "minecraft:remove_entities": { + "protocol_id": 71 + }, + "minecraft:remove_mob_effect": { + "protocol_id": 72 + }, + "minecraft:reset_score": { + "protocol_id": 73 + }, + "minecraft:resource_pack_pop": { + "protocol_id": 74 + }, + "minecraft:resource_pack_push": { + "protocol_id": 75 + }, + "minecraft:respawn": { + "protocol_id": 76 + }, + "minecraft:rotate_head": { + "protocol_id": 77 + }, + "minecraft:section_blocks_update": { + "protocol_id": 78 + }, + "minecraft:select_advancements_tab": { + "protocol_id": 79 + }, + "minecraft:server_data": { + "protocol_id": 80 + }, + "minecraft:server_links": { + "protocol_id": 130 + }, + "minecraft:set_action_bar_text": { + "protocol_id": 81 + }, + "minecraft:set_border_center": { + "protocol_id": 82 + }, + "minecraft:set_border_lerp_size": { + "protocol_id": 83 + }, + "minecraft:set_border_size": { + "protocol_id": 84 + }, + "minecraft:set_border_warning_delay": { + "protocol_id": 85 + }, + "minecraft:set_border_warning_distance": { + "protocol_id": 86 + }, + "minecraft:set_camera": { + "protocol_id": 87 + }, + "minecraft:set_chunk_cache_center": { + "protocol_id": 88 + }, + "minecraft:set_chunk_cache_radius": { + "protocol_id": 89 + }, + "minecraft:set_cursor_item": { + "protocol_id": 90 + }, + "minecraft:set_default_spawn_position": { + "protocol_id": 91 + }, + "minecraft:set_display_objective": { + "protocol_id": 92 + }, + "minecraft:set_entity_data": { + "protocol_id": 93 + }, + "minecraft:set_entity_link": { + "protocol_id": 94 + }, + "minecraft:set_entity_motion": { + "protocol_id": 95 + }, + "minecraft:set_equipment": { + "protocol_id": 96 + }, + "minecraft:set_experience": { + "protocol_id": 97 + }, + "minecraft:set_health": { + "protocol_id": 98 + }, + "minecraft:set_held_slot": { + "protocol_id": 99 + }, + "minecraft:set_objective": { + "protocol_id": 100 + }, + "minecraft:set_passengers": { + "protocol_id": 101 + }, + "minecraft:set_player_inventory": { + "protocol_id": 102 + }, + "minecraft:set_player_team": { + "protocol_id": 103 + }, + "minecraft:set_score": { + "protocol_id": 104 + }, + "minecraft:set_simulation_distance": { + "protocol_id": 105 + }, + "minecraft:set_subtitle_text": { + "protocol_id": 106 + }, + "minecraft:set_time": { + "protocol_id": 107 + }, + "minecraft:set_title_text": { + "protocol_id": 108 + }, + "minecraft:set_titles_animation": { + "protocol_id": 109 + }, + "minecraft:sound": { + "protocol_id": 111 + }, + "minecraft:sound_entity": { + "protocol_id": 110 + }, + "minecraft:start_configuration": { + "protocol_id": 112 + }, + "minecraft:stop_sound": { + "protocol_id": 113 + }, + "minecraft:store_cookie": { + "protocol_id": 114 + }, + "minecraft:system_chat": { + "protocol_id": 115 + }, + "minecraft:tab_list": { + "protocol_id": 116 + }, + "minecraft:tag_query": { + "protocol_id": 117 + }, + "minecraft:take_item_entity": { + "protocol_id": 118 + }, + "minecraft:teleport_entity": { + "protocol_id": 119 + }, + "minecraft:ticking_state": { + "protocol_id": 120 + }, + "minecraft:ticking_step": { + "protocol_id": 121 + }, + "minecraft:transfer": { + "protocol_id": 122 + }, + "minecraft:update_advancements": { + "protocol_id": 123 + }, + "minecraft:update_attributes": { + "protocol_id": 124 + }, + "minecraft:update_mob_effect": { + "protocol_id": 125 + }, + "minecraft:update_recipes": { + "protocol_id": 126 + }, + "minecraft:update_tags": { + "protocol_id": 127 + } + }, + "serverbound": { + "minecraft:accept_teleportation": { + "protocol_id": 0 + }, + "minecraft:block_entity_tag_query": { + "protocol_id": 1 + }, + "minecraft:bundle_item_selected": { + "protocol_id": 2 + }, + "minecraft:change_difficulty": { + "protocol_id": 3 + }, + "minecraft:chat": { + "protocol_id": 7 + }, + "minecraft:chat_ack": { + "protocol_id": 4 + }, + "minecraft:chat_command": { + "protocol_id": 5 + }, + "minecraft:chat_command_signed": { + "protocol_id": 6 + }, + "minecraft:chat_session_update": { + "protocol_id": 8 + }, + "minecraft:chunk_batch_received": { + "protocol_id": 9 + }, + "minecraft:client_command": { + "protocol_id": 10 + }, + "minecraft:client_information": { + "protocol_id": 12 + }, + "minecraft:client_tick_end": { + "protocol_id": 11 + }, + "minecraft:command_suggestion": { + "protocol_id": 13 + }, + "minecraft:configuration_acknowledged": { + "protocol_id": 14 + }, + "minecraft:container_button_click": { + "protocol_id": 15 + }, + "minecraft:container_click": { + "protocol_id": 16 + }, + "minecraft:container_close": { + "protocol_id": 17 + }, + "minecraft:container_slot_state_changed": { + "protocol_id": 18 + }, + "minecraft:cookie_response": { + "protocol_id": 19 + }, + "minecraft:custom_payload": { + "protocol_id": 20 + }, + "minecraft:debug_sample_subscription": { + "protocol_id": 21 + }, + "minecraft:edit_book": { + "protocol_id": 22 + }, + "minecraft:entity_tag_query": { + "protocol_id": 23 + }, + "minecraft:interact": { + "protocol_id": 24 + }, + "minecraft:jigsaw_generate": { + "protocol_id": 25 + }, + "minecraft:keep_alive": { + "protocol_id": 26 + }, + "minecraft:lock_difficulty": { + "protocol_id": 27 + }, + "minecraft:move_player_pos": { + "protocol_id": 28 + }, + "minecraft:move_player_pos_rot": { + "protocol_id": 29 + }, + "minecraft:move_player_rot": { + "protocol_id": 30 + }, + "minecraft:move_player_status_only": { + "protocol_id": 31 + }, + "minecraft:move_vehicle": { + "protocol_id": 32 + }, + "minecraft:paddle_boat": { + "protocol_id": 33 + }, + "minecraft:pick_item_from_block": { + "protocol_id": 34 + }, + "minecraft:pick_item_from_entity": { + "protocol_id": 35 + }, + "minecraft:ping_request": { + "protocol_id": 36 + }, + "minecraft:place_recipe": { + "protocol_id": 37 + }, + "minecraft:player_abilities": { + "protocol_id": 38 + }, + "minecraft:player_action": { + "protocol_id": 39 + }, + "minecraft:player_command": { + "protocol_id": 40 + }, + "minecraft:player_input": { + "protocol_id": 41 + }, + "minecraft:player_loaded": { + "protocol_id": 42 + }, + "minecraft:pong": { + "protocol_id": 43 + }, + "minecraft:recipe_book_change_settings": { + "protocol_id": 44 + }, + "minecraft:recipe_book_seen_recipe": { + "protocol_id": 45 + }, + "minecraft:rename_item": { + "protocol_id": 46 + }, + "minecraft:resource_pack": { + "protocol_id": 47 + }, + "minecraft:seen_advancements": { + "protocol_id": 48 + }, + "minecraft:select_trade": { + "protocol_id": 49 + }, + "minecraft:set_beacon": { + "protocol_id": 50 + }, + "minecraft:set_carried_item": { + "protocol_id": 51 + }, + "minecraft:set_command_block": { + "protocol_id": 52 + }, + "minecraft:set_command_minecart": { + "protocol_id": 53 + }, + "minecraft:set_creative_mode_slot": { + "protocol_id": 54 + }, + "minecraft:set_jigsaw_block": { + "protocol_id": 55 + }, + "minecraft:set_structure_block": { + "protocol_id": 56 + }, + "minecraft:sign_update": { + "protocol_id": 57 + }, + "minecraft:swing": { + "protocol_id": 58 + }, + "minecraft:teleport_to_entity": { + "protocol_id": 59 + }, + "minecraft:use_item": { + "protocol_id": 61 + }, + "minecraft:use_item_on": { + "protocol_id": 60 + } + } + }, + "status": { + "clientbound": { + "minecraft:pong_response": { + "protocol_id": 1 + }, + "minecraft:status_response": { + "protocol_id": 0 + } + }, + "serverbound": { + "minecraft:ping_request": { + "protocol_id": 1 + }, + "minecraft:status_request": { + "protocol_id": 0 + } + } + } +} diff --git a/potato-protocol/src/datatypes/byte_array.rs b/potato-protocol/src/datatypes/byte_array.rs new file mode 100644 index 0000000..2b2fbaf --- /dev/null +++ b/potato-protocol/src/datatypes/byte_array.rs @@ -0,0 +1,30 @@ +use crate::packet_encodable::{PacketDecodeError, PacketEncodable}; + +#[derive(Debug)] +pub struct ByteArray(Vec); + +impl PacketEncodable for ByteArray { + fn encode_packet( + &self, + buffer: &mut Vec, + ) -> Result<(), crate::packet_encodable::PacketEncodeError> { + // Push all data to the buffer, length is not included. + buffer.extend_from_slice(&self.0); + + Ok(()) + } + + fn decode_packet( + cursor: &mut std::io::Cursor<&[u8]>, + ) -> Result { + // Read all remaining data from the cursor. Length should be assumed to be the rest of the + // message. + let data = cursor + .get_ref() + .get(cursor.position() as usize..) + .ok_or(PacketDecodeError::UnexpectedEndOfPacket)? + .to_vec(); + + Ok(ByteArray(data)) + } +} diff --git a/potato-protocol/src/datatypes/identifier.rs b/potato-protocol/src/datatypes/identifier.rs new file mode 100644 index 0000000..b17af97 --- /dev/null +++ b/potato-protocol/src/datatypes/identifier.rs @@ -0,0 +1,77 @@ +use crate::packet_encodable::{PacketDecodeError, PacketEncodable, PacketEncodeError}; + +// The _phantom is used to stop construction of the struct outside of the new() function. Not for +// future api compatibility, but to avoid accidental instantiation of invalid identifiers. +#[allow(clippy::manual_non_exhaustive)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Identifier { + pub namespace: String, + pub path: String, + _phantom: (), +} + +impl Identifier { + pub fn new(namespace: String, path: String) -> Self { + // TODO: Validate namespace and path + Self { + namespace, + path, + _phantom: (), + } + } + + pub fn minecraft(path: String) -> Self { + Self::new("minecraft".to_string(), path) + } + + pub fn new_str(namespace: &str, path: &str) -> Self { + Self::new(namespace.to_string(), path.to_string()) + } + + pub fn minecraft_str(path: &str) -> Self { + Self::minecraft(path.to_string()) + } + + pub fn from_raw_str(raw: &str) -> Self { + let mut parts = raw.split(":"); + Self::new( + parts.next().expect("Invalid identifier").to_string(), + parts.next().expect("Invalid identifier").to_string(), + ) + } +} + +impl PacketEncodable for Identifier { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + let val: String = self.into(); + val.encode_packet(buffer) + } + + fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let str = String::decode_packet(cursor)?; + let mut parts = str.split(":"); + + Ok(Self::new( + parts + .next() + .ok_or_else(|| PacketDecodeError::InvalidIdentifier(str.clone()))? + .to_string(), + parts + .next() + .ok_or_else(|| PacketDecodeError::InvalidIdentifier(str.clone()))? + .to_string(), + )) + } +} + +impl From for String { + fn from(value: Identifier) -> Self { + (&value).into() + } +} + +impl From<&Identifier> for String { + fn from(value: &Identifier) -> Self { + format!("{}:{}", value.namespace, value.path) + } +} diff --git a/potato-protocol/src/datatypes/mod.rs b/potato-protocol/src/datatypes/mod.rs new file mode 100644 index 0000000..3ff2ac7 --- /dev/null +++ b/potato-protocol/src/datatypes/mod.rs @@ -0,0 +1,5 @@ +pub mod byte_array; +pub mod identifier; +pub mod pack; +pub mod position; +pub mod var_int; diff --git a/potato-protocol/src/datatypes/pack.rs b/potato-protocol/src/datatypes/pack.rs new file mode 100644 index 0000000..dde9a5d --- /dev/null +++ b/potato-protocol/src/datatypes/pack.rs @@ -0,0 +1,8 @@ +use potato_protocol_derive::PacketEncodable; + +#[derive(Debug, PacketEncodable)] +pub struct Pack { + pub namespace: String, + pub id: String, + pub version: String, +} diff --git a/potato-protocol/src/datatypes/position.rs b/potato-protocol/src/datatypes/position.rs new file mode 100644 index 0000000..52ef149 --- /dev/null +++ b/potato-protocol/src/datatypes/position.rs @@ -0,0 +1,52 @@ +use crate::packet_encodable::PacketEncodable; + +/// Struct holding an integer position in the world. Values are bound as follows: +/// -33554432 <= x <= 33554431 +/// -2048 <= y <= 2047 +/// -33554432 <= z <= 33554431 +// The _phantom is used to stop construction of the struct outside of the new() function. Not for +// future api compatibility, but to avoid accidental instantiation of invalid positions. +#[allow(clippy::manual_non_exhaustive)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Position { + pub x: i32, + pub y: i16, + pub z: i32, + _phantom: (), +} + +impl Position { + pub fn new(x: i32, y: i16, z: i32) -> Self { + // TODO: Validate x, y, z are in range + Self { + x, + y, + z, + _phantom: (), + } + } +} + +impl PacketEncodable for Position { + fn encode_packet( + &self, + buffer: &mut Vec, + ) -> Result<(), crate::packet_encodable::PacketEncodeError> { + let stuffed: i64 = ((self.x as i64 & 0x3FFFFFF) << 38) + | ((self.z as i64 & 0x3FFFFFF) << 12) + | (self.y as i64 & 0xFFF); + stuffed.encode_packet(buffer) + } + + fn decode_packet( + cursor: &mut std::io::Cursor<&[u8]>, + ) -> Result { + let stuffed = i64::decode_packet(cursor)?; + Ok(Position { + x: (stuffed >> 38) as i32, + y: (stuffed << 52 >> 52) as i16, + z: (stuffed << 26 >> 38) as i32, + _phantom: (), + }) + } +} diff --git a/potato-protocol/src/datatypes/var_int.rs b/potato-protocol/src/datatypes/var_int.rs new file mode 100644 index 0000000..7c5699f --- /dev/null +++ b/potato-protocol/src/datatypes/var_int.rs @@ -0,0 +1,78 @@ +use std::io::Read; + +use crate::packet_encodable::{PacketDecodeError, PacketEncodable}; + +const SEGMENT_BITS: u8 = 0x7F; +const CONTINUE_BIT: u8 = 0x80; + +#[derive(Debug)] +pub struct VarInt(i32); + +impl VarInt { + pub fn read(readable: &mut dyn Read) -> Result { + let mut value: i32 = 0; + let mut pos = 0; + let mut current_byte = vec![0u8; 1]; + + loop { + readable.read_exact(&mut current_byte)?; + + value |= ((current_byte[0] & SEGMENT_BITS) as i32) << pos; + if current_byte[0] & 0x80 == 0 { + break; + } + pos += 7; + } + + Ok(VarInt(value)) + } +} + +impl PacketEncodable for VarInt { + fn encode_packet( + &self, + buffer: &mut Vec, + ) -> Result<(), crate::packet_encodable::PacketEncodeError> { + let mut value = self.0; + + loop { + if value & !(SEGMENT_BITS as i32) == 0 { + buffer.push(value as u8); + break; + } + + buffer.push(((value & (SEGMENT_BITS as i32)) | (CONTINUE_BIT as i32)) as u8); + value >>= 7; + } + + Ok(()) + } + + fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + VarInt::read(cursor).map_err(PacketDecodeError::IOError) + } +} + +impl From for i32 { + fn from(val: VarInt) -> Self { + val.0 + } +} + +impl From for usize { + fn from(val: VarInt) -> Self { + val.0 as usize + } +} + +impl From for VarInt { + fn from(value: i32) -> Self { + VarInt(value) + } +} + +impl From for VarInt { + fn from(value: usize) -> Self { + VarInt(value as i32) + } +} diff --git a/potato-protocol/src/lib.rs b/potato-protocol/src/lib.rs new file mode 100644 index 0000000..221c15b --- /dev/null +++ b/potato-protocol/src/lib.rs @@ -0,0 +1,7 @@ +pub mod datatypes; +pub mod packet; +pub mod packet_encodable; + +pub mod ids { + include!(concat!(env!("OUT_DIR"), "/ids.rs")); +} diff --git a/potato-protocol/src/packet/clientbound/finish_configuration.rs b/potato-protocol/src/packet/clientbound/finish_configuration.rs new file mode 100644 index 0000000..66031a5 --- /dev/null +++ b/potato-protocol/src/packet/clientbound/finish_configuration.rs @@ -0,0 +1,5 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(configuration_id = crate::ids::configuration::clientbound::FINISH_CONFIGURATION)] +pub struct FinishConfigurationPacket; diff --git a/potato-protocol/src/packet/clientbound/game_event.rs b/potato-protocol/src/packet/clientbound/game_event.rs new file mode 100644 index 0000000..b986b9f --- /dev/null +++ b/potato-protocol/src/packet/clientbound/game_event.rs @@ -0,0 +1,8 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::clientbound::GAME_EVENT)] +pub struct GameEventPacket { + pub event: u8, + pub data: f32, +} diff --git a/potato-protocol/src/packet/clientbound/login.rs b/potato-protocol/src/packet/clientbound/login.rs new file mode 100644 index 0000000..ab0b14f --- /dev/null +++ b/potato-protocol/src/packet/clientbound/login.rs @@ -0,0 +1,34 @@ +use potato_protocol_derive::{Packet, PacketEncodable}; + +use crate::datatypes::{identifier::Identifier, position::Position, var_int::VarInt}; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::clientbound::LOGIN)] +pub struct LoginPacket { + pub entity_id: i32, + pub is_hardcore: bool, + pub dimension_names: Vec, + pub max_players: VarInt, + pub view_distance: VarInt, + pub simulation_distance: VarInt, + pub reduced_debug_info: bool, + pub enable_respawn_screen: bool, + pub do_limited_crafting: bool, + pub dimension_type: VarInt, + pub dimension_name: Identifier, + pub hashed_seed: i64, + pub game_mode: u8, + pub previous_game_mode: i8, + pub is_debug: bool, + pub is_flat: bool, + pub death_info: Option, + pub portal_cooldown: VarInt, + pub sea_level: VarInt, + pub enforces_secure_chat: bool, +} + +#[derive(Debug, PacketEncodable)] +pub struct DeathInfo { + pub death_dimension: Identifier, + pub death_location: Position, +} diff --git a/potato-protocol/src/packet/clientbound/login_disconnect.rs b/potato-protocol/src/packet/clientbound/login_disconnect.rs new file mode 100644 index 0000000..b9007a3 --- /dev/null +++ b/potato-protocol/src/packet/clientbound/login_disconnect.rs @@ -0,0 +1,8 @@ +use potato_protocol_derive::Packet; + +#[derive(Packet, Debug)] +#[packet(login_id = crate::ids::login::clientbound::LOGIN_DISCONNECT)] +pub struct LoginDisconnectPacket { + // TODO: This is a text component, maybe the type needs to reflect that + pub reason: String, +} diff --git a/potato-protocol/src/packet/clientbound/login_finished.rs b/potato-protocol/src/packet/clientbound/login_finished.rs new file mode 100644 index 0000000..3b3dc18 --- /dev/null +++ b/potato-protocol/src/packet/clientbound/login_finished.rs @@ -0,0 +1,17 @@ +use potato_protocol_derive::{Packet, PacketEncodable}; +use uuid::Uuid; + +#[derive(Packet, Debug)] +#[packet(login_id = crate::ids::login::clientbound::LOGIN_FINISHED)] +pub struct LoginFinishedPacket { + pub uuid: Uuid, + pub username: String, + pub properties: Vec, +} + +#[derive(PacketEncodable, Debug)] +pub struct Property { + pub name: String, + pub value: String, + pub signature: Option, +} diff --git a/potato-protocol/src/packet/clientbound/mod.rs b/potato-protocol/src/packet/clientbound/mod.rs new file mode 100644 index 0000000..dcc712e --- /dev/null +++ b/potato-protocol/src/packet/clientbound/mod.rs @@ -0,0 +1,21 @@ +pub mod finish_configuration; +pub mod game_event; +pub mod login; +pub mod login_disconnect; +pub mod login_finished; +pub mod pong_response; +pub mod registry_data; +pub mod select_known_packs; +pub mod set_chunk_cache_center; +pub mod status_response; + +pub use finish_configuration::FinishConfigurationPacket; +pub use game_event::GameEventPacket; +pub use login::LoginPacket; +pub use login_disconnect::LoginDisconnectPacket; +pub use login_finished::LoginFinishedPacket; +pub use pong_response::PongResponsePacket; +pub use registry_data::RegistryDataPacket; +pub use select_known_packs::SelectKnownPacksPacket; +pub use set_chunk_cache_center::SetChunkCacheCenterPacket; +pub use status_response::StatusResponsePacket; diff --git a/potato-protocol/src/packet/clientbound/pong_response.rs b/potato-protocol/src/packet/clientbound/pong_response.rs new file mode 100644 index 0000000..f446ee7 --- /dev/null +++ b/potato-protocol/src/packet/clientbound/pong_response.rs @@ -0,0 +1,7 @@ +use potato_protocol_derive::Packet; + +#[derive(Packet, Debug)] +#[packet(status_id = crate::ids::status::clientbound::PONG_RESPONSE)] +pub struct PongResponsePacket { + pub timestamp: i64, +} diff --git a/potato-protocol/src/packet/clientbound/registry_data.rs b/potato-protocol/src/packet/clientbound/registry_data.rs new file mode 100644 index 0000000..25b1ae9 --- /dev/null +++ b/potato-protocol/src/packet/clientbound/registry_data.rs @@ -0,0 +1,129 @@ +use potato_protocol_derive::{Packet, PacketEncodable}; +use serde::{Deserialize, Serialize}; + +use crate::{datatypes::identifier::Identifier, packet_encodable::Nbt}; + +#[derive(Debug, Packet)] +#[packet(configuration_id = crate::ids::configuration::clientbound::REGISTRY_DATA)] +pub struct RegistryDataPacket { + pub registry_id: Identifier, + pub entries: Vec, +} + +#[derive(Debug, PacketEncodable)] +pub struct RegistryDataEntry { + pub id: Identifier, + pub data: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RegistryData { + DimensionType(DimensionType), + PaintingVariant(PaintingVariant), + WolfVariant(WolfVariant), + DamegeType(DamageType), + Biome(Biome), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DimensionType { + pub fixed_time: Option, + pub has_skylight: i8, + pub has_ceiling: i8, + pub ultrawarm: i8, + pub natural: i8, + pub coordinate_scale: f64, + pub bed_works: i8, + pub respawn_anchor_works: i8, + pub min_y: i32, + pub height: i32, + pub logical_height: i32, + pub infiniburn: String, // TODO: This is a tag, create a type for these + pub effects: String, // TODO: This is an enum of 3 string variants: + // - mincecraft:overworld + // - minecraft:the_nether + // - minecraft:the_end + pub ambient_light: f32, + pub piglin_safe: i8, + pub has_raids: i8, + pub monster_spawn_light_level: i32, // TODO: This can also be a tag compound of some sort + pub monster_spawn_block_light_limit: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaintingVariant { + pub asset_id: String, + pub height: i32, + pub width: i32, + // TODO: Missing title and author fields +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WolfVariant { + pub wild_texture: String, + pub tame_texture: String, + pub angry_texture: String, + pub biomes: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DamageType { + pub message_id: String, + pub scaling: String, + pub exhaustion: f32, + pub effects: Option, + pub death_message_type: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Biome { + pub has_precipitation: i8, + pub temperature: f32, + pub temperature_modifier: Option, + pub downfall: f32, + pub effects: BiomeEffects, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BiomeEffects { + pub fog_color: i32, + pub water_color: i32, + pub water_fog_color: i32, + pub sky_color: i32, + pub foliage_color: Option, + pub grass_color: Option, + pub grass_color_modifier: Option, + pub particle: Option, + pub ambient_sound: Option, // TODO: Can also be a compound tag + pub mood_sound: Option, + pub additions_sound: Option, + pub music: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BiomeParticle { + // TODO: Add fields +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BiomeMoodSound { + pub sound: String, + pub tick_delay: i32, + pub block_search_extent: i32, + pub offset: f64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BiomeAdditionsSound { + sound: String, + tick_chance: f64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BiomeMusic { + sound: String, + min_delay: i32, + max_delay: i32, + replace_current_music: i8, +} diff --git a/potato-protocol/src/packet/clientbound/select_known_packs.rs b/potato-protocol/src/packet/clientbound/select_known_packs.rs new file mode 100644 index 0000000..ff850ec --- /dev/null +++ b/potato-protocol/src/packet/clientbound/select_known_packs.rs @@ -0,0 +1,9 @@ +use potato_protocol_derive::Packet; + +use crate::datatypes::pack::Pack; + +#[derive(Debug, Packet)] +#[packet(configuration_id = crate::ids::configuration::clientbound::SELECT_KNOWN_PACKS)] +pub struct SelectKnownPacksPacket { + pub packs: Vec, +} diff --git a/potato-protocol/src/packet/clientbound/set_chunk_cache_center.rs b/potato-protocol/src/packet/clientbound/set_chunk_cache_center.rs new file mode 100644 index 0000000..7311efd --- /dev/null +++ b/potato-protocol/src/packet/clientbound/set_chunk_cache_center.rs @@ -0,0 +1,10 @@ +use potato_protocol_derive::Packet; + +use crate::datatypes::var_int::VarInt; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::clientbound::SET_CHUNK_CACHE_CENTER)] +pub struct SetChunkCacheCenterPacket { + pub x: VarInt, + pub z: VarInt, +} diff --git a/potato-protocol/src/packet/clientbound/status_response.rs b/potato-protocol/src/packet/clientbound/status_response.rs new file mode 100644 index 0000000..551a7f2 --- /dev/null +++ b/potato-protocol/src/packet/clientbound/status_response.rs @@ -0,0 +1,43 @@ +use potato_protocol_derive::Packet; +use serde::{Deserialize, Serialize}; + +use crate::packet_encodable::Json; + +#[derive(Packet, Debug)] +#[packet(status_id = crate::ids::status::clientbound::STATUS_RESPONSE)] +pub struct StatusResponsePacket { + pub status: Json, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StatusResponseData { + pub version: Version, + pub players: Players, + pub description: Description, + pub favicon: Option, + pub enforce_secure_chat: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Version { + pub name: String, + pub protocol: i32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Players { + pub max: i32, + pub online: i32, + pub sample: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Player { + pub name: String, + pub id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Description { + pub text: String, +} diff --git a/potato-protocol/src/packet/mod.rs b/potato-protocol/src/packet/mod.rs new file mode 100644 index 0000000..6307416 --- /dev/null +++ b/potato-protocol/src/packet/mod.rs @@ -0,0 +1,12 @@ +pub mod clientbound; +pub mod serverbound; + +use crate::packet_encodable::PacketEncodable; + +pub trait Packet: PacketEncodable { + const HANDSHAKE_ID: i32; + const STATUS_ID: i32; + const LOGIN_ID: i32; + const CONFIGURATION_ID: i32; + const PLAY_ID: i32; +} diff --git a/potato-protocol/src/packet/serverbound/client_information.rs b/potato-protocol/src/packet/serverbound/client_information.rs new file mode 100644 index 0000000..ecabec5 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/client_information.rs @@ -0,0 +1,84 @@ +use potato_protocol_derive::{Packet, PacketEncodable}; + +use crate::packet_encodable::{PacketDecodeError, PacketEncodable, PacketEncodeError}; + +#[derive(Debug, Packet)] +#[packet(configuration_id = crate::ids::configuration::serverbound::CLIENT_INFORMATION)] +pub struct ClientInformationPacket { + locale: String, + view_distance: i8, + chat_mode: ChatMode, + chat_colors: bool, + displayed_skin_parts: DisplayedSkinParts, + main_hand: MainHand, + enable_text_filtering: bool, + allow_server_listings: bool, + particle_status: ParticleStatus, +} + +#[derive(Debug, PacketEncodable)] +pub enum ChatMode { + #[packet(id = 0)] + Enabled, + #[packet(id = 1)] + CommandsOnly, + #[packet(id = 2)] + Hidden, +} + +#[derive(Debug, PacketEncodable)] +pub enum MainHand { + #[packet(id = 0)] + Left, + #[packet(id = 1)] + Right, +} + +#[derive(Debug, PacketEncodable)] +pub enum ParticleStatus { + #[packet(id = 0)] + All, + #[packet(id = 1)] + Decreased, + #[packet(id = 2)] + Minimal, +} + +#[derive(Debug)] +pub struct DisplayedSkinParts { + pub cape: bool, + pub jacket: bool, + pub left_sleeve: bool, + pub right_sleeve: bool, + pub left_pants_leg: bool, + pub right_pants_leg: bool, + pub hat: bool, +} + +impl PacketEncodable for DisplayedSkinParts { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + let value = (self.cape as u8) + | (self.jacket as u8) << 1 + | (self.left_sleeve as u8) << 2 + | (self.right_sleeve as u8) << 3 + | (self.left_pants_leg as u8) << 4 + | (self.right_pants_leg as u8) << 5 + | (self.hat as u8) << 6; + + value.encode_packet(buffer) + } + + fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let value = u8::decode_packet(cursor)?; + + Ok(Self { + cape: value & 0x01 != 0, + jacket: value & 0x02 != 0, + left_sleeve: value & 0x04 != 0, + right_sleeve: value & 0x08 != 0, + left_pants_leg: value & 0x10 != 0, + right_pants_leg: value & 0x20 != 0, + hat: value & 0x40 != 0, + }) + } +} diff --git a/potato-protocol/src/packet/serverbound/client_tick_end.rs b/potato-protocol/src/packet/serverbound/client_tick_end.rs new file mode 100644 index 0000000..962ab6a --- /dev/null +++ b/potato-protocol/src/packet/serverbound/client_tick_end.rs @@ -0,0 +1,5 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::serverbound::CLIENT_TICK_END)] +pub struct ClientTickEndPacket; diff --git a/potato-protocol/src/packet/serverbound/custom_payload.rs b/potato-protocol/src/packet/serverbound/custom_payload.rs new file mode 100644 index 0000000..a5aac5e --- /dev/null +++ b/potato-protocol/src/packet/serverbound/custom_payload.rs @@ -0,0 +1,13 @@ +use potato_protocol_derive::Packet; + +use crate::datatypes::byte_array::ByteArray; + +#[derive(Debug, Packet)] +#[packet( + configuration_id = crate::ids::configuration::serverbound::CUSTOM_PAYLOAD, + play_id = crate::ids::play::serverbound::CUSTOM_PAYLOAD +)] +pub struct CustomPayloadPacket { + pub channel: String, + pub data: ByteArray, +} diff --git a/potato-protocol/src/packet/serverbound/finish_configuration.rs b/potato-protocol/src/packet/serverbound/finish_configuration.rs new file mode 100644 index 0000000..6c7aa79 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/finish_configuration.rs @@ -0,0 +1,5 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(configuration_id = crate::ids::configuration::serverbound::FINISH_CONFIGURATION)] +pub struct FinishConfigurationPacket; diff --git a/potato-protocol/src/packet/serverbound/hello.rs b/potato-protocol/src/packet/serverbound/hello.rs new file mode 100644 index 0000000..3025c8a --- /dev/null +++ b/potato-protocol/src/packet/serverbound/hello.rs @@ -0,0 +1,9 @@ +use potato_protocol_derive::Packet; +use uuid::Uuid; + +#[derive(Debug, Packet)] +#[packet(login_id = crate::ids::login::serverbound::HELLO)] +pub struct HelloPacket { + pub name: String, + pub uuid: Uuid, +} diff --git a/potato-protocol/src/packet/serverbound/intention.rs b/potato-protocol/src/packet/serverbound/intention.rs new file mode 100644 index 0000000..e42c913 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/intention.rs @@ -0,0 +1,12 @@ +use potato_protocol_derive::Packet; + +use crate::datatypes::var_int::VarInt; + +#[derive(Packet, Debug)] +#[packet(handshake_id = crate::ids::handshake::serverbound::INTENTION)] +pub struct IntentionPacket { + pub protocol_version: VarInt, + pub server_address: String, + pub server_port: u16, + pub next_state: VarInt, +} diff --git a/potato-protocol/src/packet/serverbound/login_acknowledged.rs b/potato-protocol/src/packet/serverbound/login_acknowledged.rs new file mode 100644 index 0000000..d99a0b5 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/login_acknowledged.rs @@ -0,0 +1,5 @@ +use potato_protocol_derive::Packet; + +#[derive(Packet, Debug)] +#[packet(login_id = crate::ids::login::serverbound::LOGIN_ACKNOWLEDGED)] +pub struct LoginAcknowledgedPacket; diff --git a/potato-protocol/src/packet/serverbound/mod.rs b/potato-protocol/src/packet/serverbound/mod.rs new file mode 100644 index 0000000..9719ab2 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/mod.rs @@ -0,0 +1,27 @@ +pub mod client_information; +pub mod client_tick_end; +pub mod custom_payload; +pub mod finish_configuration; +pub mod hello; +pub mod intention; +pub mod login_acknowledged; +pub mod move_player_pos; +pub mod move_player_pos_rot; +pub mod move_player_rot; +pub mod ping_request; +pub mod select_known_packs; +pub mod status_request; + +pub use client_information::ClientInformationPacket; +pub use client_tick_end::ClientTickEndPacket; +pub use custom_payload::CustomPayloadPacket; +pub use finish_configuration::FinishConfigurationPacket; +pub use hello::HelloPacket; +pub use intention::IntentionPacket; +pub use login_acknowledged::LoginAcknowledgedPacket; +pub use move_player_pos::MovePlayerPosPacket; +pub use move_player_pos_rot::MovePlayerPosRotPacket; +pub use move_player_rot::MovePlayerRotPacket; +pub use ping_request::PingRequestPacket; +pub use select_known_packs::SelectKnownPacksPacket; +pub use status_request::StatusRequestPacket; diff --git a/potato-protocol/src/packet/serverbound/move_player_pos.rs b/potato-protocol/src/packet/serverbound/move_player_pos.rs new file mode 100644 index 0000000..3593efa --- /dev/null +++ b/potato-protocol/src/packet/serverbound/move_player_pos.rs @@ -0,0 +1,10 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::serverbound::MOVE_PLAYER_POS)] +pub struct MovePlayerPosPacket { + pub x: f64, + pub feet_y: f64, + pub z: f64, + pub flags: u8, +} diff --git a/potato-protocol/src/packet/serverbound/move_player_pos_rot.rs b/potato-protocol/src/packet/serverbound/move_player_pos_rot.rs new file mode 100644 index 0000000..e925007 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/move_player_pos_rot.rs @@ -0,0 +1,12 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::serverbound::MOVE_PLAYER_POS_ROT)] +pub struct MovePlayerPosRotPacket { + pub x: f64, + pub feet_y: f64, + pub z: f64, + pub yaw: f32, + pub pitch: f32, + pub flags: u8, +} diff --git a/potato-protocol/src/packet/serverbound/move_player_rot.rs b/potato-protocol/src/packet/serverbound/move_player_rot.rs new file mode 100644 index 0000000..20fa6a3 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/move_player_rot.rs @@ -0,0 +1,9 @@ +use potato_protocol_derive::Packet; + +#[derive(Debug, Packet)] +#[packet(play_id = crate::ids::play::serverbound::MOVE_PLAYER_ROT)] +pub struct MovePlayerRotPacket { + pub yaw: f32, + pub pitch: f32, + pub flags: u8, +} diff --git a/potato-protocol/src/packet/serverbound/ping_request.rs b/potato-protocol/src/packet/serverbound/ping_request.rs new file mode 100644 index 0000000..10a7458 --- /dev/null +++ b/potato-protocol/src/packet/serverbound/ping_request.rs @@ -0,0 +1,7 @@ +use potato_protocol_derive::Packet; + +#[derive(Packet, Debug)] +#[packet(status_id = crate::ids::status::serverbound::PING_REQUEST)] +pub struct PingRequestPacket { + pub timestamp: i64, +} diff --git a/potato-protocol/src/packet/serverbound/select_known_packs.rs b/potato-protocol/src/packet/serverbound/select_known_packs.rs new file mode 100644 index 0000000..d3ed1de --- /dev/null +++ b/potato-protocol/src/packet/serverbound/select_known_packs.rs @@ -0,0 +1,9 @@ +use potato_protocol_derive::Packet; + +use crate::datatypes::pack::Pack; + +#[derive(Debug, Packet)] +#[packet(configuration_id = crate::ids::configuration::serverbound::SELECT_KNOWN_PACKS)] +pub struct SelectKnownPacksPacket { + pub packs: Vec, +} diff --git a/potato-protocol/src/packet/serverbound/status_request.rs b/potato-protocol/src/packet/serverbound/status_request.rs new file mode 100644 index 0000000..9e4441d --- /dev/null +++ b/potato-protocol/src/packet/serverbound/status_request.rs @@ -0,0 +1,5 @@ +use potato_protocol_derive::Packet; + +#[derive(Packet, Debug)] +#[packet(status_id = crate::ids::status::serverbound::STATUS_REQUEST)] +pub struct StatusRequestPacket; diff --git a/potato-protocol/src/packet_encodable/mod.rs b/potato-protocol/src/packet_encodable/mod.rs new file mode 100644 index 0000000..03cf4d9 --- /dev/null +++ b/potato-protocol/src/packet_encodable/mod.rs @@ -0,0 +1,235 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use std::{ + fmt, + io::{Cursor, Read}, + marker::PhantomData, +}; + +use thiserror::Error; +use uuid::Uuid; + +use crate::datatypes::var_int::VarInt; + +pub trait PacketEncodable: Sized + fmt::Debug { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError>; + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result; +} + +#[derive(Error, Debug)] +pub enum PacketDecodeError { + #[error("IO error while reading packet {0}")] + IOError(#[from] std::io::Error), + #[error("Invalid UTF-8 sequence in string")] + FromUtf8Error(#[from] std::string::FromUtf8Error), + #[error("Error while decoding JSON in packet {0}")] + JsonError(#[from] serde_json::Error), + #[error("Unexpected end of packet")] + UnexpectedEndOfPacket, + #[error("Unknown enum variant in enum {name} with value {value}")] + UnkownEnumVariant { name: &'static str, value: i32 }, + #[error("Invalid identifier: {0}")] + InvalidIdentifier(String), + #[error("Error while decoding NBT in packet {0}")] + NbtError(#[from] fastnbt::error::Error), +} + +#[derive(Error, Debug)] +pub enum PacketEncodeError { + #[error("IO error while writing packet")] + IOError(#[from] std::io::Error), + #[error("Error while encoding JSON in packet {0}")] + JsonError(#[from] serde_json::Error), + #[error("Invalid packet id: {0}")] + InvalidPacketId(i32), + #[error("Error while encoding NBT in packet {0}")] + NbtError(#[from] fastnbt::error::Error), +} + +impl PacketEncodable for Uuid { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + buffer.write_u128::(self.as_u128())?; + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let data: u128 = cursor.read_u128::()?; + Ok(Uuid::from_u128(data)) + } +} + +#[derive(Debug)] +pub struct Json(pub T); + +impl PacketEncodable for Json +where + T: Serialize + DeserializeOwned + fmt::Debug, +{ + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + let json = serde_json::to_string(&self.0)?; + json.encode_packet(buffer) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let json = String::decode_packet(cursor)?; + Ok(Self(serde_json::from_str(&json)?)) + } +} + +#[derive(Debug)] +pub struct Nbt(pub T) +where + T: Serialize + DeserializeOwned + fmt::Debug; + +impl PacketEncodable for Nbt +where + T: Serialize + DeserializeOwned + fmt::Debug, +{ + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + fastnbt::to_writer_with_opts(buffer, &self.0, fastnbt::SerOpts::network_nbt())?; + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let value = fastnbt::from_bytes(cursor.get_ref())?; + + Ok(Nbt(value)) + } +} + +impl PacketEncodable for Option +where + T: PacketEncodable, +{ + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + let present = self.is_some(); + present.encode_packet(buffer)?; + if let Some(value) = self { + value.encode_packet(buffer)?; + } + + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let present = bool::decode_packet(cursor)?; + + if present { + Ok(Some(T::decode_packet(cursor)?)) + } else { + Ok(None) + } + } +} + +impl PacketEncodable for Vec +where + T: PacketEncodable, +{ + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + let length: VarInt = self.len().into(); + length.encode_packet(buffer)?; + + for item in self { + item.encode_packet(buffer)?; + } + + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let length: usize = VarInt::decode_packet(cursor)?.into(); + let mut vec = Vec::with_capacity(length); + for _ in 0..length { + vec.push(T::decode_packet(cursor)?); + } + + Ok(vec) + } +} + +impl PacketEncodable for String { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + let lenght: VarInt = self.len().into(); + lenght.encode_packet(buffer)?; + + buffer.extend_from_slice(self.as_bytes()); + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let lenght: usize = VarInt::decode_packet(cursor)?.into(); + let mut buffer = vec![0; lenght]; + cursor.read_exact(&mut buffer)?; + let value = String::from_utf8(buffer)?; + + Ok(value) + } +} + +impl PacketEncodable for bool { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + buffer.push(if *self { 1 } else { 0 }); + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let v = cursor.read_u8()?; + Ok(v != 0) + } +} + +impl PacketEncodable for i8 { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + buffer.write_i8(*self)?; + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let value = cursor.read_i8()?; + Ok(value) + } +} + +impl PacketEncodable for u8 { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + buffer.write_u8(*self)?; + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let value = cursor.read_u8()?; + Ok(value) + } +} + +macro_rules! number_impl { + ($($type:ty, $read_fn:tt, $write_fn:tt),* $(,)?) => { + $( + impl PacketEncodable for $type { + fn encode_packet(&self, buffer: &mut Vec) -> Result<(), PacketEncodeError> { + buffer.$write_fn::(*self)?; + Ok(()) + } + + fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result { + let value = cursor.$read_fn::()?; + Ok(value) + } + } + )* + } +} + +number_impl! { + u16, read_u16, write_u16, + u32, read_u32, write_u32, + u64, read_u64, write_u64, + + i16, read_i16, write_i16, + i32, read_i32, write_i32, + i64, read_i64, write_i64, + + f32, read_f32, write_f32, + f64, read_f64, write_f64, +} diff --git a/potato/Cargo.toml b/potato/Cargo.toml new file mode 100644 index 0000000..68d0ca4 --- /dev/null +++ b/potato/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "potato" +version = "0.1.0" +edition = "2024" + +[dependencies] +potato-protocol = { path = "../potato-protocol" } + +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +uuid.workspace = true diff --git a/potato/src/main.rs b/potato/src/main.rs new file mode 100644 index 0000000..839472f --- /dev/null +++ b/potato/src/main.rs @@ -0,0 +1,544 @@ +use std::{ + collections::HashMap, + fmt::Debug, + fs::read_to_string, + io::{Cursor, Read, Write}, + net::{Shutdown, SocketAddr, TcpListener, TcpStream}, + path::PathBuf, + sync::Arc, + thread, +}; + +use potato_protocol::{ + datatypes::{identifier::Identifier, pack::Pack, var_int::VarInt}, + packet::{ + Packet, + clientbound::{ + self, GameEventPacket, SetChunkCacheCenterPacket, + registry_data::{ + Biome, BiomeEffects, DamageType, DimensionType, PaintingVariant, RegistryData, + RegistryDataEntry, WolfVariant, + }, + status_response, + }, + serverbound, + }, + packet_encodable::{Json, Nbt, PacketDecodeError, PacketEncodable, PacketEncodeError}, +}; +use thiserror::Error; + +fn main() { + println!("Looking for datapacks..."); + let datapacks: Vec = std::fs::read_dir("run/datapacks") + .expect("Failed to read datapacks directory") + .map(|f| f.unwrap().path()) + .filter(|p| p.is_dir()) + .collect(); + println!("Found {} datapacks", datapacks.len()); + println!("Loading datapacks..."); + let mut damage_types = HashMap::new(); + for datapack in datapacks { + let datapack_name = datapack.file_name().unwrap().to_str().unwrap(); + println!("Loading datapack: {}", datapack_name); + let path = datapack.join("data/minecraft/damage_type"); + if let Ok(files) = std::fs::read_dir(path) { + for file in files { + let file = file.unwrap().path(); + let name = file.file_stem().unwrap().to_str().unwrap().to_owned(); + let data: DamageType = serde_json::from_str(&read_to_string(file).unwrap()) + .expect("Failed to parse damage type"); + + damage_types.insert(Identifier::minecraft(name), data); + } + } + } + + println!("Finished loading datapacks"); + println!("Loaded {} damage types", damage_types.len()); + + let registries = Arc::new(Registries { damage_types }); + + let listener = TcpListener::bind("127.0.0.1:25565").unwrap(); + println!("listening started, ready to accept"); + for stream in listener.incoming() { + let registries = registries.clone(); + match stream { + Err(e) => println!("Accept ERROR: {}", e), + // Explicit drop because of the diverging branches + Ok(stream) => drop(thread::spawn(|| handle_client(stream, registries))), + } + } +} + +fn handle_client(stream: TcpStream, registries: Arc) { + if let Ok(mut client) = ClientConnection::new(stream, registries) { + loop { + match client.read_packet() { + Ok(_) => (), + Err(e) => { + println!("[{}] Error while handling packets: {}", client.address, e); + // Make sure the connection is closed. + let _ = client.stream.shutdown(Shutdown::Both); + break; + } + } + } + } +} + +#[derive(Debug)] +enum ConnectionState { + Handshaking, + Status, + Login, + Configuration, + Play, +} + +struct ClientConnection { + stream: TcpStream, + address: SocketAddr, + state: ConnectionState, + recv_buffer: Vec, + + registries: Arc, +} + +struct Registries { + damage_types: HashMap, +} + +// Max packet size is 2MB in vanilla +const RECV_BUFFER_SIZE: usize = 1024 * 1024 * 2; + +#[allow(clippy::enum_variant_names)] +#[derive(Error, Debug)] +enum ConnectionError { + #[error("IO error while reading packet: {0}")] + IoError(#[from] std::io::Error), + #[error("Error while decoding packet: {0}")] + DecodeError(#[from] PacketDecodeError), + #[error("Error while encoding packet: {0}")] + EncodeError(#[from] PacketEncodeError), + #[error("Client provided invalid next state")] + InvalidNextState, + #[error("Finished")] + Finished, +} + +// TODO: Need to start sending keep alive packets once we enter configuration phase +impl ClientConnection { + fn new( + stream: TcpStream, + registries: Arc, + ) -> Result { + let address = stream.peer_addr()?; + + Ok(ClientConnection { + state: ConnectionState::Handshaking, + address, + stream, + recv_buffer: vec![0; RECV_BUFFER_SIZE], + registries, + }) + } + + fn read_packet(&mut self) -> Result<(), ConnectionError> { + let packet_size: usize = VarInt::read(&mut self.stream)?.into(); + + self.stream + .read_exact(&mut self.recv_buffer[..packet_size])?; + + self.handle_packet(packet_size) + } + + fn send_packet(&mut self, packet: &T) -> Result<(), ConnectionError> { + println!("[{} ({:?})] -> {:?}", self.address, self.state, packet); + let buffer = &mut Vec::new(); + // Encode ID + let packet_id: i32 = match self.state { + ConnectionState::Handshaking => T::HANDSHAKE_ID, + ConnectionState::Status => T::STATUS_ID, + ConnectionState::Login => T::LOGIN_ID, + ConnectionState::Configuration => T::CONFIGURATION_ID, + ConnectionState::Play => T::PLAY_ID, + }; + if packet_id == -1 { + return Err(ConnectionError::EncodeError( + PacketEncodeError::InvalidPacketId(packet_id), + )); + } + let packet_id: VarInt = packet_id.into(); + + packet_id.encode_packet(buffer)?; + // Encode packet + packet.encode_packet(buffer)?; + // Encode length + let length_buffer = &mut Vec::new(); + let length: VarInt = buffer.len().into(); + length.encode_packet(length_buffer)?; + + // Print raw packet bytes + // println!( + // "{:X?} ({}) {:X?} ({})", + // length_buffer, + // length_buffer.len(), + // buffer, + // buffer.len() + // ); + + // Send packet + self.stream.write_all(length_buffer)?; + self.stream.write_all(buffer)?; + + Ok(()) + } + + fn handle_packet(&mut self, length: usize) -> Result<(), ConnectionError> { + match self.state { + ConnectionState::Handshaking => { + self.handle_handshaking(length)?; + } + ConnectionState::Status => { + self.handle_status(length)?; + } + ConnectionState::Login => { + self.handle_login(length)?; + } + ConnectionState::Configuration => { + self.handle_configuration(length)?; + } + ConnectionState::Play => { + self.handle_play(length)?; + } + } + + Ok(()) + } + + fn handle_handshaking(&mut self, length: usize) -> Result<(), ConnectionError> { + let cursor = &mut Cursor::new(&self.recv_buffer[..length]); + let id = VarInt::decode_packet(cursor)?.into(); + + match id { + serverbound::IntentionPacket::HANDSHAKE_ID => { + let packet = serverbound::IntentionPacket::decode_packet(cursor)?; + println!("[{} (Handshaking)] <- {:?}", self.address, packet,); + self.change_state_num(packet.next_state.into())? + } + // TODO: Legacy server list ping + _ => { + println!( + "[{} (Handshaking)] <- Unknown packet id: {} with length {} data: {:X?}", + self.address, + id, + length, + &self.recv_buffer[..length] + ); + } + } + Ok(()) + } + + fn handle_status(&mut self, length: usize) -> Result<(), ConnectionError> { + let cursor = &mut Cursor::new(&self.recv_buffer[..length]); + let id: i32 = VarInt::decode_packet(cursor)?.into(); + + match id { + serverbound::StatusRequestPacket::STATUS_ID => { + let packet = serverbound::StatusRequestPacket; + println!("[{} (Status)] <- {:?}", self.address, packet); + self.send_packet(&clientbound::StatusResponsePacket { + status: Json(status_response::StatusResponseData { + version: status_response::Version { + name: "Rust 1.21.4".to_owned(), + protocol: 769, + }, + players: status_response::Players { + max: 500, + online: 0, + sample: vec![], + }, + description: status_response::Description { + text: "A rust server!".to_owned(), + }, + favicon: None, + enforce_secure_chat: false, + }), + })?; + } + serverbound::PingRequestPacket::STATUS_ID => { + let packet = serverbound::PingRequestPacket::decode_packet(cursor)?; + println!("[{} (Status)] <- {:?}", self.address, packet); + self.send_packet(&clientbound::PongResponsePacket { + timestamp: packet.timestamp, + })?; + self.stream.shutdown(Shutdown::Both)?; + return Err(ConnectionError::Finished); + } + + _ => { + println!( + "[{} (Status)] <- Unknown packet id: {} with length {} data: {:X?}", + self.address, + id, + length, + &self.recv_buffer[..length] + ); + } + } + Ok(()) + } + + fn handle_login(&mut self, length: usize) -> Result<(), ConnectionError> { + let cursor = &mut Cursor::new(&self.recv_buffer[..length]); + let id: i32 = VarInt::decode_packet(cursor)?.into(); + + match id { + serverbound::HelloPacket::LOGIN_ID => { + let packet = serverbound::HelloPacket::decode_packet(cursor)?; + println!("[{} (Login)] <- {:?}", self.address, packet); + self.send_packet(&clientbound::LoginFinishedPacket { + uuid: packet.uuid, + username: packet.name, + properties: vec![], + })?; + } + serverbound::LoginAcknowledgedPacket::LOGIN_ID => { + let packet = serverbound::LoginAcknowledgedPacket::decode_packet(cursor)?; + println!("[{} (Login)] <- {:?}", self.address, packet); + self.change_state(ConnectionState::Configuration)?; + self.send_packet(&clientbound::SelectKnownPacksPacket { + packs: vec![Pack { + namespace: "minecraft".to_owned(), + id: "core".to_owned(), + version: "1.21.4".to_owned(), + }], + })?; + } + _ => { + println!( + "[{} (Login)] <- Unknown packet id: {} with length {} data: {:X?}", + self.address, + id, + length, + &self.recv_buffer[..length] + ); + } + } + Ok(()) + } + + fn handle_configuration(&mut self, length: usize) -> Result<(), ConnectionError> { + let cursor = &mut Cursor::new(&self.recv_buffer[..length]); + let id: i32 = VarInt::decode_packet(cursor)?.into(); + + match id { + serverbound::ClientInformationPacket::CONFIGURATION_ID => { + let packet = serverbound::ClientInformationPacket::decode_packet(cursor)?; + println!("[{} (Configuration)] <- {:?}", self.address, packet); + } + serverbound::CustomPayloadPacket::CONFIGURATION_ID => { + let packet = serverbound::CustomPayloadPacket::decode_packet(cursor)?; + println!("[{} (Configuration)] <- {:?}", self.address, packet); + } + serverbound::SelectKnownPacksPacket::CONFIGURATION_ID => { + let packet = serverbound::SelectKnownPacksPacket::decode_packet(cursor)?; + println!("[{} (Configuration)] <- {:?}", self.address, packet); + + self.send_packet(&clientbound::RegistryDataPacket { + registry_id: Identifier::minecraft_str("dimension_type"), + entries: vec![RegistryDataEntry { + id: Identifier::minecraft_str("overworld"), + data: Some(Nbt(RegistryData::DimensionType(DimensionType { + fixed_time: None, + has_skylight: 1, + has_ceiling: 0, + ultrawarm: 0, + natural: 1, + coordinate_scale: 1., + bed_works: 1, + respawn_anchor_works: 0, + min_y: 0, + height: 256, + logical_height: 256, + infiniburn: "#minecraft:infiniburn_overworld".to_owned(), + effects: "minecraft:overworld".to_owned(), + ambient_light: 0., + piglin_safe: 1, + has_raids: 1, + monster_spawn_light_level: 0, + monster_spawn_block_light_limit: 0, + }))), + }], + })?; + + self.send_packet(&clientbound::RegistryDataPacket { + registry_id: Identifier::minecraft_str("painting_variant"), + entries: vec![RegistryDataEntry { + id: Identifier::minecraft_str("backyard"), + data: Some(Nbt(RegistryData::PaintingVariant(PaintingVariant { + asset_id: "minecraft:backyard".to_owned(), + height: 2, + width: 2, + }))), + }], + })?; + + self.send_packet(&clientbound::RegistryDataPacket { + registry_id: Identifier::minecraft_str("wolf_variant"), + entries: vec![RegistryDataEntry { + id: Identifier::minecraft_str("ashen"), + data: Some(Nbt(RegistryData::WolfVariant(WolfVariant { + wild_texture: "minecraft:entity/wolf/wolf_ashen".to_owned(), + tame_texture: "minecraft:entity/wolf/wolf_ashen_tame".to_owned(), + angry_texture: "minecraft:entity/wolf/wolf_ashen_angry".to_owned(), + biomes: vec![], + }))), + }], + })?; + + let damage_type_registry_entries = self + .registries + .damage_types + .iter() + .map(|(key, value)| RegistryDataEntry { + id: key.clone(), + data: Some(Nbt(RegistryData::DamegeType(value.clone()))), + }) + .collect(); + + self.send_packet(&clientbound::RegistryDataPacket { + registry_id: Identifier::minecraft_str("damage_type"), + entries: damage_type_registry_entries, + })?; + + self.send_packet(&clientbound::RegistryDataPacket { + registry_id: Identifier::minecraft_str("worldgen/biome"), + entries: vec![RegistryDataEntry { + id: Identifier::minecraft_str("plains"), + data: Some(Nbt(RegistryData::Biome(Biome { + has_precipitation: 0, + temperature: 0., + temperature_modifier: None, + downfall: 0., + effects: BiomeEffects { + fog_color: 0, + water_color: 0, + water_fog_color: 0, + sky_color: 0, + foliage_color: None, + grass_color: None, + grass_color_modifier: None, + particle: None, + ambient_sound: None, + mood_sound: None, + additions_sound: None, + music: None, + }, + }))), + }], + })?; + + self.send_packet(&clientbound::FinishConfigurationPacket)?; + } + serverbound::FinishConfigurationPacket::CONFIGURATION_ID => { + let packet = serverbound::finish_configuration::FinishConfigurationPacket; + println!("[{} (Configuration)] <- {:?}", self.address, packet); + self.change_state(ConnectionState::Play)?; + self.send_packet(&clientbound::LoginPacket { + entity_id: 0, + is_hardcore: false, + dimension_names: vec![], + max_players: 500.into(), + view_distance: 10.into(), + simulation_distance: 10.into(), + reduced_debug_info: false, + enable_respawn_screen: true, + do_limited_crafting: false, + dimension_type: 0.into(), + dimension_name: Identifier::minecraft("overworld".to_owned()), + hashed_seed: 0, + game_mode: 1, + previous_game_mode: -1, + is_debug: false, + is_flat: false, + death_info: None, + portal_cooldown: 0.into(), + sea_level: 63.into(), + enforces_secure_chat: false, + })?; + + self.send_packet(&GameEventPacket { + event: 13, + data: 0., + })?; + + self.send_packet(&SetChunkCacheCenterPacket { + x: 0.into(), + z: 0.into(), + })?; + } + _ => { + println!( + "[{} (Configuration)] <- Unknown packet id: {} with length {} data: {:X?}", + self.address, + id, + length, + &self.recv_buffer[..length] + ); + } + } + Ok(()) + } + + fn handle_play(&mut self, length: usize) -> Result<(), ConnectionError> { + let cursor = &mut Cursor::new(&self.recv_buffer[..length]); + let id: i32 = VarInt::decode_packet(cursor)?.into(); + + match id { + serverbound::ClientTickEndPacket::PLAY_ID => { + serverbound::ClientTickEndPacket::decode_packet(cursor)?; + } + serverbound::MovePlayerPosPacket::PLAY_ID => { + let _packet = serverbound::MovePlayerPosPacket::decode_packet(cursor)?; + // println!("[{} (Play)] <- {:?}", self.address, packet); + } + serverbound::MovePlayerPosRotPacket::PLAY_ID => { + let _packet = serverbound::MovePlayerPosRotPacket::decode_packet(cursor)?; + // println!("[{} (Play)] <- {:?}", self.address, packet); + } + serverbound::MovePlayerRotPacket::PLAY_ID => { + let _packet = serverbound::MovePlayerRotPacket::decode_packet(cursor)?; + // println!("[{} (Play)] <- {:?}", self.address, packet); + } + _ => { + println!( + "[{} (Play)] <- Unknown packet id: {} with length {} data: {:X?}", + self.address, + id, + length, + &self.recv_buffer[..length] + ); + } + } + Ok(()) + } + + fn change_state_num(&mut self, state: i32) -> Result<(), ConnectionError> { + let new_state = match state { + 1 => ConnectionState::Status, + 2 => ConnectionState::Login, + _ => return Err(ConnectionError::InvalidNextState), + }; + + self.change_state(new_state) + } + + fn change_state(&mut self, new_state: ConnectionState) -> Result<(), ConnectionError> { + println!("[{}] {:?} -> {:?}", self.address, self.state, new_state); + self.state = new_state; + + Ok(()) + } +}