commit e06c4882a075b018578d4d0feb362a23b59a0328
Author: Kalle Struik <kalle@kallestruik.nl>
Date:   Wed Mar 5 12:21:53 2025 +0100

    Initial commit

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<Path>,
+    status_id: Option<Path>,
+    login_id: Option<Path>,
+    configuration_id: Option<Path>,
+    play_id: Option<Path>,
+}
+
+#[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<syn::Variant, syn::token::Comma>,
+) -> 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<u8>) -> 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<Self, crate::packet_encodable::PacketDecodeError> {
+                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<u8>) -> Result<(), crate::packet_encodable::PacketEncodeError> {
+                #(#fields_encode_packet_calls)*
+                Ok(())
+            }
+
+            fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self, crate::packet_encodable::PacketDecodeError> {
+                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<String, Packet>, 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<HashMap<String, Packet>>,
+    serverbound: Option<HashMap<String, Packet>>,
+}
+
+#[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<u8>);
+
+impl PacketEncodable for ByteArray {
+    fn encode_packet(
+        &self,
+        buffer: &mut Vec<u8>,
+    ) -> 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<Self, crate::packet_encodable::PacketDecodeError> {
+        // 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<u8>) -> Result<(), PacketEncodeError> {
+        let val: String = self.into();
+        val.encode_packet(buffer)
+    }
+
+    fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        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<Identifier> 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<u8>,
+    ) -> 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<Self, crate::packet_encodable::PacketDecodeError> {
+        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<VarInt, std::io::Error> {
+        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<u8>,
+    ) -> 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<Self, PacketDecodeError> {
+        VarInt::read(cursor).map_err(PacketDecodeError::IOError)
+    }
+}
+
+impl From<VarInt> for i32 {
+    fn from(val: VarInt) -> Self {
+        val.0
+    }
+}
+
+impl From<VarInt> for usize {
+    fn from(val: VarInt) -> Self {
+        val.0 as usize
+    }
+}
+
+impl From<i32> for VarInt {
+    fn from(value: i32) -> Self {
+        VarInt(value)
+    }
+}
+
+impl From<usize> 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<Identifier>,
+    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<DeathInfo>,
+    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<Property>,
+}
+
+#[derive(PacketEncodable, Debug)]
+pub struct Property {
+    pub name: String,
+    pub value: String,
+    pub signature: Option<String>,
+}
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<RegistryDataEntry>,
+}
+
+#[derive(Debug, PacketEncodable)]
+pub struct RegistryDataEntry {
+    pub id: Identifier,
+    pub data: Option<Nbt<RegistryData>>,
+}
+
+#[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<i64>,
+    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<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct DamageType {
+    pub message_id: String,
+    pub scaling: String,
+    pub exhaustion: f32,
+    pub effects: Option<String>,
+    pub death_message_type: Option<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Biome {
+    pub has_precipitation: i8,
+    pub temperature: f32,
+    pub temperature_modifier: Option<String>,
+    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<i32>,
+    pub grass_color: Option<i32>,
+    pub grass_color_modifier: Option<String>,
+    pub particle: Option<BiomeParticle>,
+    pub ambient_sound: Option<String>, // TODO: Can also be a compound tag
+    pub mood_sound: Option<BiomeMoodSound>,
+    pub additions_sound: Option<BiomeAdditionsSound>,
+    pub music: Option<BiomeMusic>,
+}
+
+#[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<Pack>,
+}
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<StatusResponseData>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct StatusResponseData {
+    pub version: Version,
+    pub players: Players,
+    pub description: Description,
+    pub favicon: Option<String>,
+    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<Player>,
+}
+
+#[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<u8>) -> 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<Self, PacketDecodeError> {
+        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<Pack>,
+}
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<u8>) -> Result<(), PacketEncodeError>;
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError>;
+}
+
+#[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<u8>) -> Result<(), PacketEncodeError> {
+        buffer.write_u128::<BigEndian>(self.as_u128())?;
+        Ok(())
+    }
+
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        let data: u128 = cursor.read_u128::<BigEndian>()?;
+        Ok(Uuid::from_u128(data))
+    }
+}
+
+#[derive(Debug)]
+pub struct Json<T: Serialize + DeserializeOwned + fmt::Debug>(pub T);
+
+impl<T> PacketEncodable for Json<T>
+where
+    T: Serialize + DeserializeOwned + fmt::Debug,
+{
+    fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
+        let json = serde_json::to_string(&self.0)?;
+        json.encode_packet(buffer)
+    }
+
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        let json = String::decode_packet(cursor)?;
+        Ok(Self(serde_json::from_str(&json)?))
+    }
+}
+
+#[derive(Debug)]
+pub struct Nbt<T>(pub T)
+where
+    T: Serialize + DeserializeOwned + fmt::Debug;
+
+impl<T> PacketEncodable for Nbt<T>
+where
+    T: Serialize + DeserializeOwned + fmt::Debug,
+{
+    fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
+        fastnbt::to_writer_with_opts(buffer, &self.0, fastnbt::SerOpts::network_nbt())?;
+        Ok(())
+    }
+
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        let value = fastnbt::from_bytes(cursor.get_ref())?;
+
+        Ok(Nbt(value))
+    }
+}
+
+impl<T> PacketEncodable for Option<T>
+where
+    T: PacketEncodable,
+{
+    fn encode_packet(&self, buffer: &mut Vec<u8>) -> 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<Self, PacketDecodeError> {
+        let present = bool::decode_packet(cursor)?;
+
+        if present {
+            Ok(Some(T::decode_packet(cursor)?))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+impl<T> PacketEncodable for Vec<T>
+where
+    T: PacketEncodable,
+{
+    fn encode_packet(&self, buffer: &mut Vec<u8>) -> 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<Self, PacketDecodeError> {
+        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<u8>) -> 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<Self, PacketDecodeError> {
+        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<u8>) -> Result<(), PacketEncodeError> {
+        buffer.push(if *self { 1 } else { 0 });
+        Ok(())
+    }
+
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        let v = cursor.read_u8()?;
+        Ok(v != 0)
+    }
+}
+
+impl PacketEncodable for i8 {
+    fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
+        buffer.write_i8(*self)?;
+        Ok(())
+    }
+
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        let value = cursor.read_i8()?;
+        Ok(value)
+    }
+}
+
+impl PacketEncodable for u8 {
+    fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
+        buffer.write_u8(*self)?;
+        Ok(())
+    }
+
+    fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+        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<u8>) -> Result<(), PacketEncodeError> {
+                    buffer.$write_fn::<BigEndian>(*self)?;
+                    Ok(())
+                }
+
+                fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
+                    let value = cursor.$read_fn::<BigEndian>()?;
+                    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<PathBuf> = 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<Registries>) {
+    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<u8>,
+
+    registries: Arc<Registries>,
+}
+
+struct Registries {
+    damage_types: HashMap<Identifier, DamageType>,
+}
+
+// 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<Registries>,
+    ) -> Result<ClientConnection, std::io::Error> {
+        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<T: 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(())
+    }
+}