Initial commit

This commit is contained in:
kalle 2025-03-05 12:21:53 +01:00
commit e06c4882a0
47 changed files with 3246 additions and 0 deletions

12
.envrc Normal file
View file

@ -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

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target/
.direnv/
.devenv/

233
Cargo.lock generated Normal file
View file

@ -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"

13
Cargo.toml Normal file
View file

@ -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"

316
flake.lock generated Normal file
View file

@ -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
}

59
flake.nix Normal file
View file

@ -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"
];
};
}
)
];
};
}
);
};
}

View file

@ -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

View file

@ -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,
}
}

View file

@ -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

65
potato-protocol/build.rs Normal file
View file

@ -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,
}

View file

@ -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
}
}
}
}

View file

@ -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))
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,5 @@
pub mod byte_array;
pub mod identifier;
pub mod pack;
pub mod position;
pub mod var_int;

View file

@ -0,0 +1,8 @@
use potato_protocol_derive::PacketEncodable;
#[derive(Debug, PacketEncodable)]
pub struct Pack {
pub namespace: String,
pub id: String,
pub version: String,
}

View file

@ -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: (),
})
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,7 @@
pub mod datatypes;
pub mod packet;
pub mod packet_encodable;
pub mod ids {
include!(concat!(env!("OUT_DIR"), "/ids.rs"));
}

View file

@ -0,0 +1,5 @@
use potato_protocol_derive::Packet;
#[derive(Debug, Packet)]
#[packet(configuration_id = crate::ids::configuration::clientbound::FINISH_CONFIGURATION)]
pub struct FinishConfigurationPacket;

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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>,
}

View file

@ -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;

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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>,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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;
}

View file

@ -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,
})
}
}

View file

@ -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;

View file

@ -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,
}

View file

@ -0,0 +1,5 @@
use potato_protocol_derive::Packet;
#[derive(Debug, Packet)]
#[packet(configuration_id = crate::ids::configuration::serverbound::FINISH_CONFIGURATION)]
pub struct FinishConfigurationPacket;

View file

@ -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,
}

View file

@ -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,
}

View file

@ -0,0 +1,5 @@
use potato_protocol_derive::Packet;
#[derive(Packet, Debug)]
#[packet(login_id = crate::ids::login::serverbound::LOGIN_ACKNOWLEDGED)]
pub struct LoginAcknowledgedPacket;

View file

@ -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;

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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>,
}

View file

@ -0,0 +1,5 @@
use potato_protocol_derive::Packet;
#[derive(Packet, Debug)]
#[packet(status_id = crate::ids::status::serverbound::STATUS_REQUEST)]
pub struct StatusRequestPacket;

View file

@ -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,
}

12
potato/Cargo.toml Normal file
View file

@ -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

544
potato/src/main.rs Normal file
View file

@ -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(())
}
}