Compare commits

..

2 commits

45 changed files with 2270 additions and 838 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target/
.direnv/
.devenv/
/run/

305
Cargo.lock generated
View file

@ -2,18 +2,72 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "darling"
version = "0.20.10"
@ -52,7 +106,7 @@ dependencies = [
[[package]]
name = "fastnbt"
version = "2.5.0"
source = "git+https://github.com/owengage/fastnbt.git#e2a5d8a7001d4f074ae99fd21bb485667934baeb"
source = "git+https://github.com/CheAle14/fastnbt.git#91ce891ca8aa3048b9e1d08ae0fcc51a7b714451"
dependencies = [
"byteorder",
"cesu8",
@ -66,6 +120,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "ident_case"
version = "1.0.1"
@ -78,33 +138,122 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "potato"
version = "0.1.0"
dependencies = [
"bytes",
"potato-data",
"potato-protocol",
"serde",
"serde_json",
"thiserror",
"tokio",
"uuid",
]
[[package]]
name = "potato-data"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "potato-protocol"
version = "0.1.0"
dependencies = [
"byteorder",
"bytes",
"fastnbt",
"potato-data",
"potato-protocol-derive",
"serde",
"serde_json",
"thiserror",
"tokio",
"uuid",
]
@ -136,12 +285,33 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
dependencies = [
"bitflags",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.218"
@ -183,6 +353,31 @@ dependencies = [
"serde",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "socket2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "strsim"
version = "0.11.1"
@ -220,6 +415,35 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -231,3 +455,82 @@ name = "uuid"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View file

@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["potato", "potato-protocol", "potato-protocol-derive"]
members = ["potato", "potato-data", "potato-protocol", "potato-protocol-derive"]
[workspace.package]
version = "0.1.0"
@ -11,3 +11,5 @@ serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.140"
thiserror = "2.0.11"
uuid = "1.15.1"
bytes = "1"
tokio = { version = "1.43.0", features = ["full"] }

9
potato-data/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "potato-data"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true

View file

@ -0,0 +1,11 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::identifier::Identifier;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BlockState {
pub name: Identifier,
pub properties: HashMap<String, String>,
}

35
potato-data/src/color.rs Normal file
View file

@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
// TODO: Serialize as array of [red, green, blue] in NBT
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RGBf32 {
red: f32,
green: f32,
blue: f32,
}
// TODO: Serialize as array of [red, green, blue, alpha] in NBT
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RGBAf32 {
red: f32,
green: f32,
blue: f32,
alpha: f32,
}
// TODO: Serialize as u32 of red<<16+green<<8+blue in NBT
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RGBu8 {
red: u8,
green: u8,
blue: u8,
}
// TODO: Serialize as u32 of alpha<<24+red<<16+green<<8+blue in NBT
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ARGBu8 {
alpha: u8,
red: u8,
green: u8,
blue: u8,
}

View file

@ -0,0 +1,86 @@
use std::{ffi::OsStr, fs::read_to_string, path::PathBuf};
use thiserror::Error;
use crate::{
identifier::Identifier,
registry::{Registries, Registry},
};
pub struct Datapack {
pub name: String,
path: PathBuf,
}
impl Datapack {
pub fn new(path: PathBuf) -> Self {
// TODO: Load name (and other info) from pack.mcmeta
let name = path.file_name().unwrap().to_str().unwrap().to_string();
Self { name, path }
}
pub fn load(&self, registries: &mut Registries) -> Result<(), DatapackError> {
println!("Loading datapack: {}", self.name);
self.load_registry(&mut registries.trim_materials, "trim_material")?;
self.load_registry(&mut registries.trim_patterns, "trim_pattern")?;
self.load_registry(&mut registries.banner_patterns, "banner_pattern")?;
self.load_registry(&mut registries.worldgen.biomes, "worldgen/biome")?;
self.load_registry(&mut registries.damage_types, "damage_type")?;
self.load_registry(&mut registries.dimension_types, "dimension_type")?;
self.load_registry(&mut registries.wolf_variants, "wolf_variant")?;
self.load_registry(&mut registries.painting_variants, "painting_variant")?;
self.load_registry(&mut registries.chat_types, "chat_type")?;
Ok(())
}
fn load_registry<T: serde::de::DeserializeOwned>(
&self,
registry: &mut Registry<T>,
path: &str,
) -> Result<(), DatapackError> {
let namespaces: Vec<_> = self
.path
.join("data")
.read_dir()
.unwrap()
.map(|f| f.unwrap().path())
.filter(|p| p.is_dir())
.collect();
for namespace in namespaces {
let namespace_str = namespace
.file_name()
.and_then(OsStr::to_str)
.ok_or(DatapackError::Utf8)?;
let path = namespace.join(path);
if let Ok(files) = std::fs::read_dir(path) {
for file in files {
let file = file.unwrap().path();
let name = file
.file_stem()
.and_then(OsStr::to_str)
.ok_or(DatapackError::Utf8)?;
let identfiier = Identifier::new_str(namespace_str, name);
let data: T = serde_json::from_str(&read_to_string(file)?)?;
registry.insert(identfiier, data);
}
}
}
Ok(())
}
}
#[derive(Debug, Error)]
pub enum DatapackError {
#[error("IO error while reading datapack: {0}")]
Io(#[from] std::io::Error),
#[error("Error while parsing JSON in datapack: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid UTF-8 in datapack")]
Utf8,
}

View file

@ -0,0 +1,71 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
// TODO: Custom serializer/deserializer for format namespace:path as string
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "&str", into = "String")]
pub struct Identifier {
pub namespace: String,
pub path: String,
}
impl Identifier {
pub fn new(namespace: String, path: String) -> Self {
// TODO: Validate namespace and path
Self { namespace, path }
}
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) -> Result<Self, IdentifierError> {
let mut parts = raw.split(":");
Ok(Self::new(
parts
.next()
.ok_or(IdentifierError::InvalidNamespace)?
.to_string(),
parts
.next()
.ok_or(IdentifierError::InvalidPath)?
.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)
}
}
impl TryFrom<&str> for Identifier {
type Error = IdentifierError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Identifier::from_raw_str(value)
}
}
#[derive(Debug, Error)]
pub enum IdentifierError {
#[error("Invalid namespace")]
InvalidNamespace,
#[error("Invalid path")]
InvalidPath,
}

View file

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use crate::identifier::Identifier;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ItemStack {
id: Identifier,
components: Vec<ItemComponent>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub enum ItemComponent {}

25
potato-data/src/lib.rs Normal file
View file

@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
pub mod block_state;
pub mod color;
pub mod datapack;
pub mod identifier;
pub mod item_stack;
pub mod particle;
pub mod registry;
pub mod tag;
pub mod text_component;
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum Either<A, B> {
Left(A),
Right(B),
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}

268
potato-data/src/particle.rs Normal file
View file

@ -0,0 +1,268 @@
use serde::{Deserialize, Serialize};
use crate::{
Either,
block_state::BlockState,
color::{ARGBu8, RGBAf32, RGBf32, RGBu8},
identifier::Identifier,
item_stack::ItemStack,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum Particle {
#[serde(rename = "minecraft:angry_villager")]
AngryVillager,
#[serde(rename = "minecraft:ash")]
Ash,
Block {
block_state: Either<Identifier, BlockState>,
},
#[serde(rename = "minecraft:block_crumble")]
BlockCrumble {
block_state: Either<Identifier, BlockState>,
},
BlockMarker {
block_state: Either<Identifier, BlockState>,
},
#[serde(rename = "minecraft:bubble")]
Bubble,
#[serde(rename = "minecraft:bubble_column_up")]
BubbleColumnUp,
#[serde(rename = "minecraft:bubble_pop")]
BuublePop,
#[serde(rename = "minecraft:campfire_cosy_smoke")]
CampfireCosySmoke,
#[serde(rename = "minecraft:campfire_signal_smoke")]
CampfireSignalSmoke,
#[serde(rename = "minecraft:cherry_leaves")]
CherryLeaves,
#[serde(rename = "minecraft:cloud")]
Cloud,
#[serde(rename = "minecraft:composter")]
Composter,
#[serde(rename = "minecraft:crimson_spore")]
CrimsonSpore,
#[serde(rename = "minecraft:crit")]
Crit,
#[serde(rename = "minecraft:current_down")]
CurrentDown,
#[serde(rename = "minecraft:damage_indicator")]
DamageIndicator,
#[serde(rename = "minecraft:dolphin")]
Dolphin,
#[serde(rename = "minecraft:dragon_breath")]
DragonBreath,
#[serde(rename = "minecraft:dripping_dripstone_lava")]
DrippingDripstoneLava,
#[serde(rename = "minecraft:dripping_dripstone_water")]
DrippingDripstoneWater,
#[serde(rename = "minecraft:dripping_honey")]
DrippingHoney,
#[serde(rename = "minecraft:dripping_lava")]
DrippingLava,
#[serde(rename = "minecraft:dripping_obsidian_tear")]
DrippingObsidianTear,
#[serde(rename = "minecraft:dripping_water")]
DrippingWater,
Dust {
color: RGBf32,
scale: f32,
},
DustColorTransition {
from_color: RGBf32,
to_color: RGBf32,
scale: f32,
},
DustPillar {
block_state: Either<Identifier, BlockState>,
},
#[serde(rename = "minecraft:dust_plume")]
DustPlume,
#[serde(rename = "minecraft:effect")]
Effect,
#[serde(rename = "minecraft:egg_crack")]
EggCrack,
#[serde(rename = "minecraft:elder_guardian")]
ElderGuardian,
#[serde(rename = "minecraft:electric_spark")]
ElectricSpark,
#[serde(rename = "minecraft:enchant")]
Enchant,
#[serde(rename = "minecraft:enchanted_hit")]
EnchantedHit,
#[serde(rename = "minecraft:end_rod")]
EndRod,
EntityEffect {
color: Either<RGBAf32, ARGBu8>,
},
#[serde(rename = "minecraft:explosion")]
Explosion,
#[serde(rename = "minecraft:explosion_emitter")]
ExplosionEmitter,
#[serde(rename = "minecraft:falling_dripstone_lava")]
FallingDripstoneLava,
#[serde(rename = "minecraft:falling_dripstone_water")]
FallingDrpistoneWater,
FallingDust {
block_state: Either<Identifier, BlockState>,
},
#[serde(rename = "minecraft:falling_honey")]
FallingHoney,
#[serde(rename = "minecraft:falling_lava")]
FallingLava,
#[serde(rename = "minecraft:falling_nectar")]
FallingNectar,
#[serde(rename = "minecraft:falling_obsidian_tear")]
FallingObsidianTear,
#[serde(rename = "minecraft:falling_spore_blossom")]
FallingSporeBlossom,
#[serde(rename = "minecraft:falling_water")]
FallingWater,
// Firefly, (Upcoming: 1.21.5)
#[serde(rename = "minecraft:firework")]
Firework,
#[serde(rename = "minecraft:fishing")]
Fishing,
#[serde(rename = "minecraft:flame")]
Flame,
#[serde(rename = "minecraft:flash")]
Flash,
#[serde(rename = "minecraft:glow")]
Glow,
#[serde(rename = "minecraft:glow_squid_ink")]
GlowSquidInk,
#[serde(rename = "minecraft:gust")]
Gust,
#[serde(rename = "minecraft:gust_emitter")]
GustEmitter,
#[serde(rename = "minecraft:happy_villager")]
HappyVillager,
#[serde(rename = "minecraft:heart")]
Heart,
#[serde(rename = "minecraft:infested")]
Infested,
#[serde(rename = "minecraft:instant_effect")]
InstantEffect,
Item {
item: Either<Identifier, ItemStack>,
},
#[serde(rename = "minecraft:item_cobweb")]
ItemCobweb,
#[serde(rename = "minecraft:item_slime")]
ItemSlime,
#[serde(rename = "minecraft:item_snowball")]
ItemSnowball,
#[serde(rename = "minecraft:landing_honey")]
LandingHoney,
#[serde(rename = "minecraft:landing_lava")]
LandingLava,
#[serde(rename = "minecraft:landing_obsidian_tear")]
LandingObsidianTear,
#[serde(rename = "minecraft:large_smoke")]
LargeSmoke,
#[serde(rename = "minecraft:lava")]
Lava,
#[serde(rename = "minecraft:mycelium")]
Mycelium,
#[serde(rename = "minecraft:nautilus")]
Nautlius,
#[serde(rename = "minecraft:note")]
Note,
#[serde(rename = "minecraft:ominous_spawning")]
OminousSpawning,
#[serde(rename = "minecraft:pale_oak_leaves")]
PaleOakLeaves,
#[serde(rename = "minecraft:poof")]
Poof,
#[serde(rename = "minecraft:portal")]
Portal,
#[serde(rename = "minecraft:raid_omen")]
RaidOmen,
#[serde(rename = "minecraft:rain")]
Rain,
#[serde(rename = "minecraft:reverse_portal")]
ReversePortal,
#[serde(rename = "minecraft:scrape")]
Scrape,
SculkCharge {
roll: f32,
},
#[serde(rename = "minecraft:sculk_charge_pop")]
SculkChargePop,
#[serde(rename = "minecraft:sculk_soul")]
SculkSoul,
Shriek {
delay: i32,
},
#[serde(rename = "minecraft:small_flame")]
SmallFlame,
#[serde(rename = "minecraft:small_gust")]
SmallGust,
#[serde(rename = "minecraft:smoke")]
Smoke,
#[serde(rename = "minecraft:sneeze")]
Sneeze,
#[serde(rename = "minecraft:snowflake")]
Snowflake,
#[serde(rename = "minecraft:sonic_boom")]
SonicBoom,
#[serde(rename = "minecraft:soul")]
Soul,
#[serde(rename = "minecraft:soul_fire_flame")]
SoulFireFlame,
#[serde(rename = "minecraft:spit")]
Spit,
#[serde(rename = "minecraft:splash")]
Splash,
#[serde(rename = "minecraft:spore_blossom_air")]
SporeBlossomAir,
#[serde(rename = "minecraft:squid_ink")]
SquidInk,
#[serde(rename = "minecraft:sweep_attack")]
SweepAttack,
#[serde(rename = "minecraft:totem_of_undying")]
TotemOfUndying,
Trail {
target: (f32, f32, f32),
colr: RGBu8,
},
#[serde(rename = "minecraft:trail_omen")]
TrailOmen,
#[serde(rename = "minecraft:trail_spawner_detection")]
TrailSpawnerDetection,
#[serde(rename = "minecraft:underwater")]
Underwater,
#[serde(rename = "minecraft:vault_connection")]
VaultConnection,
Vibration {
destination: PositionSource,
arrival_in_ticks: i32,
},
#[serde(rename = "minecraft:warped_spore")]
WarpedSpore,
#[serde(rename = "minecraft:wax_off")]
WaxOff,
#[serde(rename = "minecraft:wax_on")]
WaxOn,
#[serde(rename = "minecraft:white_ash")]
WhiteAsh,
#[serde(rename = "minecraft:white_smoke")]
WhiteSmoke,
#[serde(rename = "minecraft:witch")]
Witch,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PositionSource {
Block {
pos: (i32, i32, i32),
},
Entity {
// source_entity stores a UUID as 4xi32
source_entity: (i32, i32, i32, i32),
// y_offset is optional and defaults to 0.0
y_offset: f32,
},
}

View file

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
use crate::identifier::Identifier;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BannerPattern {
pub asset_id: Identifier,
pub translation_key: String,
}

View file

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use crate::text_component::TextComponentStyle;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct ChatType {
pub chat: ChatTypeInner,
pub narration: ChatTypeInner,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ChatTypeParameter {
Sender,
Target,
Content,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct ChatTypeInner {
pub translation_key: String,
pub parameters: Vec<ChatTypeParameter>,
pub style: Option<TextComponentStyle>,
}

View file

@ -0,0 +1,37 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DamageType {
message_id: String,
scaling: DamageTypeScaling,
exhaustion: f32,
effects: Option<DamageTypeEffect>,
death_message_type: Option<DamageTypeDeathMessageType>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DamageTypeScaling {
Never,
WhenCausedByLivingNonPlayer,
Always,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DamageTypeEffect {
Hurt,
Thorns,
Drowning,
Burning,
Poking,
Freezing,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DamageTypeDeathMessageType {
Default,
FallVariants,
IntentionalGameDesign,
}

View file

@ -0,0 +1,74 @@
use crate::Either;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DimensionType {
pub fixed_time: Option<i64>,
pub has_skylight: bool,
pub has_ceiling: bool,
pub ultrawarm: bool,
pub natural: bool,
pub coordinate_scale: f64,
pub bed_works: bool,
pub respawn_anchor_works: bool,
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: DimensionEffect,
pub ambient_light: f32,
pub piglin_safe: bool,
pub has_raids: bool,
pub monster_spawn_light_level: Either<i32, IntProvider>,
pub monster_spawn_block_light_limit: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum DimensionEffect {
#[serde(rename = "minecraft:overworld")]
Overworld,
#[serde(rename = "minecraft:the_nether")]
TheNether,
#[serde(rename = "minecraft:the_end")]
TheEnd,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum IntProvider {
#[serde(rename = "minecraft:constant")]
Constant { value: i32 },
#[serde(rename = "minecraft:uniform")]
Uniform {
min_inclusive: i32,
max_inclusive: i32,
},
#[serde(rename = "minecraft:biased_to_bottom")]
BiasedToBottom {
min_inclusive: i32,
max_inclusive: i32,
},
#[serde(rename = "minecraft:clamped")]
Clamped {
min_inclusive: i32,
max_inclusive: i32,
source: Box<IntProvider>,
},
#[serde(rename = "minecraft:clamped_normal")]
ClampedNormal {
mean: f32,
deviation: f32,
min_include: i32,
max_include: i32,
},
#[serde(rename = "minecraft:weighted_list")]
WeightedList {
distribution: Vec<WeightedListEntry>,
},
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WeightedListEntry {
pub data: Box<IntProvider>,
pub weight: i32,
}

View file

@ -0,0 +1,49 @@
use std::collections::HashMap;
use banner_pattern::BannerPattern;
use chat_type::ChatType;
use damage_type::DamageType;
use dimension_type::DimensionType;
use painting_variant::PaintingVariant;
use trim_material::TrimMaterial;
use trim_pattern::TrimPattern;
use wolf_variant::WolfVariant;
use worldgen::biome::Biome;
use crate::identifier::Identifier;
pub mod banner_pattern;
pub mod chat_type;
pub mod damage_type;
pub mod dimension_type;
pub mod painting_variant;
pub mod trim_material;
pub mod trim_pattern;
pub mod wolf_variant;
pub mod worldgen;
pub type Registry<T> = HashMap<Identifier, T>;
#[derive(Default)]
pub struct Registries {
pub trim_materials: Registry<TrimMaterial>,
pub trim_patterns: Registry<TrimPattern>,
pub banner_patterns: Registry<BannerPattern>,
pub worldgen: WorldgenRegistries,
pub dimension_types: Registry<DimensionType>,
pub damage_types: Registry<DamageType>,
pub painting_variants: Registry<PaintingVariant>,
pub wolf_variants: Registry<WolfVariant>,
pub chat_types: Registry<ChatType>,
}
impl Registries {
pub fn empty() -> Registries {
Self::default()
}
}
#[derive(Default)]
pub struct WorldgenRegistries {
pub biomes: Registry<Biome>,
}

View file

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PaintingVariant {
pub asset_id: String,
pub height: i32,
pub width: i32,
// TODO: Missing title and author fields
}

View file

@ -0,0 +1,14 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{identifier::Identifier, text_component::TextComponent};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TrimMaterial {
pub asset_name: String,
pub description: TextComponent,
pub ingredient: Identifier,
pub override_armor_assets: Option<HashMap<Identifier, String>>,
// NOTE: Wiki says item_model_index exists as option, but can't find it in the vanilla pack
}

View file

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use crate::{identifier::Identifier, text_component::TextComponent};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TrimPattern {
pub asset_id: Identifier,
pub description: TextComponent,
pub template_item: Identifier,
#[serde(default)]
pub decal: bool,
}

View file

@ -0,0 +1,11 @@
use serde::{Deserialize, Serialize};
use crate::{Either, OneOrMany, identifier::Identifier, tag::Tag};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WolfVariant {
pub wild_texture: String,
pub tame_texture: String,
pub angry_texture: String,
pub biomes: OneOrMany<Either<Identifier, Tag>>,
}

View file

@ -0,0 +1,124 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{Either, OneOrMany, identifier::Identifier, particle::Particle, tag::Tag};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Biome {
pub has_precipitation: bool,
pub temperature: f32,
#[serde(default)]
pub temperature_modifier: BiomeTemperatureModifier,
pub downfall: f32,
pub effects: BiomeEffects,
pub carvers: OneOrMany<Either<Identifier, Tag>>,
pub features: Vec<Vec<Either<Identifier, Tag>>>,
// TODO: Limit between 0.0 and 0.9999999
pub creature_spawn_probability: Option<f32>,
pub spawners: BiomeSpawners,
pub spawn_costs: HashMap<Identifier, BiomeSpawnCost>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "snake_case")]
pub enum BiomeTemperatureModifier {
#[default]
None,
Frozen,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeEffects {
pub fog_color: i32,
pub sky_color: i32,
pub water_color: i32,
pub water_fog_color: i32,
pub foliage_color: Option<i32>,
pub grass_color: Option<i32>,
#[serde(default)]
pub grass_color_modifier: BiomeGrassColorModifier,
pub particle: Option<BiomeParticle>,
pub ambient_sound: Option<Either<Identifier, SoundEvent>>,
pub mood_sound: Option<BiomeMoodSound>,
pub additions_sound: Option<BiomeAdditionsSound>,
pub music: Option<Vec<BiomeMusicEntry>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SoundEvent {
pub sound_id: Identifier,
pub range: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "snake_case")]
pub enum BiomeGrassColorModifier {
#[default]
None,
DarkForest,
Swamp,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeParticle {
pub probability: f64,
pub options: Particle,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeMoodSound {
pub sound: Either<Identifier, SoundEvent>,
pub tick_delay: i32,
pub block_search_extent: i32,
pub offset: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeAdditionsSound {
pub sound: Either<Identifier, SoundEvent>,
pub tick_chance: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeMusicEntry {
pub data: BiomeMusicData,
pub weight: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeMusicData {
pub sound: Either<Identifier, SoundEvent>,
pub min_delay: i32,
pub max_delay: i32,
pub replace_current_music: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeSpawners {
pub monster: Option<Vec<BiomeSpawner>>,
pub creature: Option<Vec<BiomeSpawner>>,
pub ambient: Option<Vec<BiomeSpawner>>,
pub water_creature: Option<Vec<BiomeSpawner>>,
pub underground_water_creature: Option<Vec<BiomeSpawner>>,
pub water_ambient: Option<Vec<BiomeSpawner>>,
pub misc: Option<Vec<BiomeSpawner>>,
pub axolotls: Option<Vec<BiomeSpawner>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeSpawner {
#[serde(rename = "type")]
pub ty: Identifier,
pub weight: i32,
#[serde(rename = "minCount")]
pub min_count: i32,
#[serde(rename = "maxCount")]
pub max_count: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BiomeSpawnCost {
pub energy_budget: f64,
pub charge: f64,
}

View file

@ -0,0 +1 @@
pub mod biome;

34
potato-data/src/tag.rs Normal file
View file

@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::identifier::{Identifier, IdentifierError};
// TODO: Needs to serialize/deserialize as the identifier with a hash in front.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "&str", into = "String")]
pub struct Tag(Identifier);
impl TryFrom<&str> for Tag {
type Error = TagError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.strip_prefix("#") {
Some(v) => Ok(Self(Identifier::try_from(v)?)),
None => Err(TagError::MissingHash),
}
}
}
impl From<Tag> for String {
fn from(value: Tag) -> Self {
let v: String = value.0.into();
format!("#{v}")
}
}
#[derive(Debug, Error)]
pub enum TagError {
#[error("Missing hash")]
MissingHash,
#[error("Invalid identifier: {0}")]
InvalidIdentifier(#[from] IdentifierError),
}

View file

@ -0,0 +1,166 @@
use serde::{Deserialize, Serialize};
use crate::{Either, identifier::Identifier, item_stack::ItemComponent};
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct TextComponent {
// Content
#[serde(flatten)]
pub content: TextComponentContent,
// Children
#[serde(default)]
pub extra: Vec<TextComponent>,
// Formatting
#[serde(flatten)]
pub style: TextComponentStyle,
// Interactivity
pub insertion: Option<String>,
#[serde(rename = "clickEvent")]
pub click_event: Option<ClickEvent>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct TextComponentStyle {
pub color: Option<String>,
pub font: Option<Identifier>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underlined: Option<bool>,
pub strikethrough: Option<bool>,
pub obfuscated: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "action", content = "value", rename_all = "snake_case")]
pub enum ClickEvent {
OpenUrl(String),
OpenFile(String),
RunCommand(String),
SuggestCommand(String),
ChangePage(String),
CopyToClipboard(String),
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "action", content = "contents", rename_all = "snake_case")]
pub enum HoverEvent {
ShowText(Box<TextComponent>),
ShowItem {
id: Identifier,
count: Option<i32>,
#[serde(default)]
components: Vec<ItemComponent>,
},
ShowEntity {
// NOTE: Not sure if this is a TextComponent or just a string.
name: String,
#[serde(rename = "type")]
ty: Identifier,
id: Either<String, (u32, u32, u32, u32)>,
},
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(untagged)]
pub enum TextComponentContent {
Text(TextComponentText),
Translatable(TextComponentTranslatable),
Score(TextComponentScore),
Selector(TextComponentSelector),
Keybind(TextComponentKeybind),
Nbt(TextComponentNbt),
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "type", rename = "text")]
pub struct TextComponentText {
pub text: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "type", rename = "translatable")]
pub struct TextComponentTranslatable {
pub translate: String,
pub fallback: Option<String>,
pub with: Option<Vec<TextComponent>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "type", rename = "score")]
pub struct TextComponentScore {
pub score: TextComponentScoreInner,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct TextComponentScoreInner {
pub name: String,
pub objective: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "type", rename = "selector")]
pub struct TextComponentSelector {
pub selector: String,
// TODO: Instead of an option, make this default to the default value.
pub separator: Option<Box<TextComponent>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "type", rename = "keybind")]
pub struct TextComponentKeybind {
pub keybind: String, // TODO: Maybe restrict this to the allowed keybinds, or maybe leave that
// for the client.
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(tag = "type", rename = "nbt")]
pub struct TextComponentNbt {
pub source: String,
pub nbt: String,
#[serde(default)]
pub interpret: bool,
// TODO: Instead of an option, make this default to the default value.
pub separator: Option<Box<TextComponent>>,
pub block: Option<String>,
pub entity: Option<String>,
pub storage: Option<String>,
}
#[cfg(test)]
mod test {
use crate::text_component::*;
#[test]
fn test_text_component() {
let text = r#"{"text":"Hello, world!"}"#;
let text_component: TextComponent = serde_json::from_str(text).unwrap();
assert_eq!(
text_component,
TextComponent {
content: TextComponentContent::Text(TextComponentText {
text: "Hello, world!".to_string(),
}),
}
);
}
#[test]
fn test_translatable_text_component() {
let text = r#"{"translate":"chat.type.text","with":[{"text":"Hello, world!"}]}"#;
let text_component: TextComponent = serde_json::from_str(text).unwrap();
assert_eq!(
text_component,
TextComponent {
content: TextComponentContent::Translatable(TextComponentTranslatable {
translate: "chat.type.text".to_string(),
fallback: None,
with: Some(vec![TextComponent {
content: TextComponentContent::Text(TextComponentText {
text: "Hello, world!".to_string(),
}),
}]),
}),
}
);
}
}

View file

@ -122,16 +122,16 @@ fn generate_packet_encodable_enum_impl(
.collect();
quote! {
impl crate::packet_encodable::PacketEncodable for #ident {
fn encode_packet(&self, cursor: &mut Vec<u8>) -> Result<(), crate::packet_encodable::PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl bytes::BufMut) -> Result<(), crate::packet_encodable::PacketEncodeError> {
let value: crate::datatypes::var_int::VarInt = match self {
#(#encode_arms)*
}.into();
value.encode_packet(cursor)
value.encode_packet(buf)
}
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();
fn decode_packet(buf: &mut impl bytes::Buf) -> Result<Self, crate::packet_encodable::PacketDecodeError> {
let value: i32 = crate::datatypes::var_int::VarInt::decode_packet(buf)?.into();
match value {
#(#decode_arms)*
_ => Err(crate::packet_encodable::PacketDecodeError::UnkownEnumVariant {
@ -160,12 +160,12 @@ fn generate_packet_encodable_struct_impl(
quote! {
impl crate::packet_encodable::PacketEncodable for #ident {
fn encode_packet(&self, cursor: &mut Vec<u8>) -> Result<(), crate::packet_encodable::PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl bytes::BufMut) -> 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> {
fn decode_packet(buf: &mut impl bytes::Buf) -> Result<Self, crate::packet_encodable::PacketDecodeError> {
Ok(Self {
#(#fields_decode_packet_calls)*
})
@ -179,7 +179,7 @@ fn generate_struct_field_decode_packet_calls(field: &syn::Field) -> proc_macro2:
let field_ty = &field.ty;
quote! {
#field_ident: <#field_ty>::decode_packet(cursor)?,
#field_ident: <#field_ty>::decode_packet(buf)?,
}
}
@ -187,7 +187,7 @@ fn generate_struct_field_encode_packet_calls(field: &syn::Field) -> proc_macro2:
let field_ident = &field.ident;
quote! {
self.#field_ident.encode_packet(cursor)?;
self.#field_ident.encode_packet(buf)?;
}
}

View file

@ -4,17 +4,22 @@ version.workspace = true
edition.workspace = true
[dependencies]
potato-data = { path = "../potato-data" }
potato-protocol-derive = { path = "../potato-protocol-derive" }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
uuid.workspace = true
bytes.workspace = true
tokio.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" }
# Original repo: https://github.com/owengage/fastnbt.git Using a fork
# to fix an issue with boolean serialization.
fastnbt = { git = "https://github.com/CheAle14/fastnbt.git" }
[build-dependencies]
serde.workspace = true

View file

@ -1,4 +1,6 @@
use crate::packet_encodable::{PacketDecodeError, PacketEncodable};
use bytes::{Buf, BufMut};
use crate::packet_encodable::PacketEncodable;
#[derive(Debug)]
pub struct ByteArray(Vec<u8>);
@ -6,25 +8,22 @@ pub struct ByteArray(Vec<u8>);
impl PacketEncodable for ByteArray {
fn encode_packet(
&self,
buffer: &mut Vec<u8>,
buf: &mut impl BufMut,
) -> Result<(), crate::packet_encodable::PacketEncodeError> {
// Push all data to the buffer, length is not included.
buffer.extend_from_slice(&self.0);
buf.put_slice(&self.0);
Ok(())
}
fn decode_packet(
cursor: &mut std::io::Cursor<&[u8]>,
buf: &mut impl Buf,
) -> 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();
let mut dst = Vec::with_capacity(buf.remaining());
buf.copy_to_slice(&mut dst);
Ok(ByteArray(data))
Ok(ByteArray(dst))
}
}

View file

@ -1,77 +0,0 @@
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

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

View file

@ -1,3 +1,5 @@
use bytes::{Buf, BufMut};
use crate::packet_encodable::PacketEncodable;
/// Struct holding an integer position in the world. Values are bound as follows:
@ -30,18 +32,18 @@ impl Position {
impl PacketEncodable for Position {
fn encode_packet(
&self,
buffer: &mut Vec<u8>,
buf: &mut impl BufMut,
) -> 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)
stuffed.encode_packet(buf)
}
fn decode_packet(
cursor: &mut std::io::Cursor<&[u8]>,
buf: &mut impl Buf,
) -> Result<Self, crate::packet_encodable::PacketDecodeError> {
let stuffed = i64::decode_packet(cursor)?;
let stuffed = i64::decode_packet(buf)?;
Ok(Position {
x: (stuffed >> 38) as i32,
y: (stuffed << 52 >> 52) as i16,

View file

@ -1,55 +1,86 @@
use std::io::Read;
use bytes::{Buf, BufMut};
use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf};
use crate::packet_encodable::{PacketDecodeError, PacketEncodable};
const SEGMENT_BITS: u8 = 0x7F;
const CONTINUE_BIT: u8 = 0x80;
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
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];
pub fn encoded_len(&self) -> usize {
match self.0 {
0 => 1,
n => (31 - n.leading_zeros() as usize) / 7 + 1,
}
}
pub async fn write_to_stream(&self, stream: &mut OwnedWriteHalf) -> Result<(), std::io::Error> {
let mut value = self.0;
loop {
readable.read_exact(&mut current_byte)?;
value |= ((current_byte[0] & SEGMENT_BITS) as i32) << pos;
if current_byte[0] & 0x80 == 0 {
if value & !(SEGMENT_BITS as i32) == 0 {
stream.write_u8(value as u8).await?;
break;
}
pos += 7;
stream
.write_u8(((value & (SEGMENT_BITS as i32)) | (CONTINUE_BIT as i32)) as u8)
.await?;
value >>= 7;
}
Ok(VarInt(value))
Ok(())
}
}
impl PacketEncodable for VarInt {
fn encode_packet(
&self,
buffer: &mut Vec<u8>,
buf: &mut impl BufMut,
) -> Result<(), crate::packet_encodable::PacketEncodeError> {
let mut value = self.0;
loop {
if value & !(SEGMENT_BITS as i32) == 0 {
buffer.push(value as u8);
buf.put_u8(value as u8);
break;
}
buffer.push(((value & (SEGMENT_BITS as i32)) | (CONTINUE_BIT as i32)) as u8);
buf.put_u8(((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)
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let mut value: i32 = 0;
let mut pos = 0;
loop {
if !buf.has_remaining() {
// The data is incomplete
return Err(PacketDecodeError::Incomplete);
}
let current_byte = buf.get_u8();
value |= ((current_byte & SEGMENT_BITS) as i32) << pos;
if current_byte & 0x80 == 0 {
break;
}
pos += 7;
if pos > 32 {
// The value is too large
return Err(PacketDecodeError::InvalidVarInt);
}
}
Ok(VarInt(value))
}
}

View file

@ -0,0 +1,10 @@
use potato_protocol_derive::Packet;
#[derive(Debug, Packet)]
#[packet(
configuration_id = crate::ids::configuration::clientbound::KEEP_ALIVE,
play_id = crate::ids::play::clientbound::KEEP_ALIVE,
)]
pub struct KeepAlivePacket {
pub id: i64,
}

View file

@ -1,6 +1,7 @@
use potato_data::identifier::Identifier;
use potato_protocol_derive::{Packet, PacketEncodable};
use crate::datatypes::{identifier::Identifier, position::Position, var_int::VarInt};
use crate::datatypes::{position::Position, var_int::VarInt};
#[derive(Debug, Packet)]
#[packet(play_id = crate::ids::play::clientbound::LOGIN)]

View file

@ -1,5 +1,6 @@
pub mod finish_configuration;
pub mod game_event;
pub mod keep_alive;
pub mod login;
pub mod login_disconnect;
pub mod login_finished;
@ -11,6 +12,7 @@ pub mod status_response;
pub use finish_configuration::FinishConfigurationPacket;
pub use game_event::GameEventPacket;
pub use keep_alive::KeepAlivePacket;
pub use login::LoginPacket;
pub use login_disconnect::LoginDisconnectPacket;
pub use login_finished::LoginFinishedPacket;

View file

@ -1,7 +1,16 @@
use potato_data::{
identifier::Identifier,
registry::{
banner_pattern::BannerPattern, chat_type::ChatType, damage_type::DamageType,
dimension_type::DimensionType, painting_variant::PaintingVariant,
trim_material::TrimMaterial, trim_pattern::TrimPattern, wolf_variant::WolfVariant,
worldgen::biome::Biome,
},
};
use potato_protocol_derive::{Packet, PacketEncodable};
use serde::{Deserialize, Serialize};
use crate::{datatypes::identifier::Identifier, packet_encodable::Nbt};
use crate::packet_encodable::Nbt;
#[derive(Debug, Packet)]
#[packet(configuration_id = crate::ids::configuration::clientbound::REGISTRY_DATA)]
@ -19,111 +28,37 @@ pub struct RegistryDataEntry {
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RegistryData {
DimensionType(DimensionType),
PaintingVariant(PaintingVariant),
WolfVariant(WolfVariant),
DamegeType(DamageType),
Biome(Biome),
TrimMaterial(Box<TrimMaterial>),
TrimPattern(Box<TrimPattern>),
BannerPattern(Box<BannerPattern>),
Biome(Box<Biome>),
ChatType(Box<ChatType>),
DamageType(Box<DamageType>),
DimensionType(Box<DimensionType>),
WolfVariant(Box<WolfVariant>),
PaintingVariant(Box<PaintingVariant>),
}
#[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,
macro_rules! impl_from_registry {
($($variant:ident),*) => {
$(
impl From<&$variant> for RegistryData {
fn from(value: &$variant) -> Self {
RegistryData::$variant(Box::new(value.clone()))
}
}
)*
};
}
#[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,
}
impl_from_registry!(
TrimMaterial,
TrimPattern,
BannerPattern,
Biome,
ChatType,
DamageType,
DimensionType,
WolfVariant,
PaintingVariant
);

View file

@ -1,7 +1,15 @@
pub mod clientbound;
pub mod serverbound;
use crate::packet_encodable::PacketEncodable;
use std::fmt;
use bytes::Bytes;
use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf};
use crate::{
datatypes::var_int::VarInt,
packet_encodable::{PacketDecodeError, PacketEncodable},
};
pub trait Packet: PacketEncodable {
const HANDSHAKE_ID: i32;
@ -9,4 +17,30 @@ pub trait Packet: PacketEncodable {
const LOGIN_ID: i32;
const CONFIGURATION_ID: i32;
const PLAY_ID: i32;
fn from_raw(mut raw: RawPacket) -> Result<Self, PacketDecodeError> {
Self::decode_packet(&mut raw.data)
}
}
pub struct RawPacket {
pub id: VarInt,
pub data: Bytes,
}
impl RawPacket {
pub async fn write_to_stream(&self, stream: &mut OwnedWriteHalf) -> Result<(), std::io::Error> {
let length: VarInt = (self.id.encoded_len() + self.data.len()).into();
length.write_to_stream(stream).await?;
self.id.write_to_stream(stream).await?;
stream.write_all(&self.data).await?;
Ok(())
}
}
impl fmt::Display for RawPacket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id: i32 = self.id.into();
write!(f, "{{ id: {}, data: {:X?} }}", id, self.data)
}
}

View file

@ -1,3 +1,4 @@
use bytes::{Buf, BufMut};
use potato_protocol_derive::{Packet, PacketEncodable};
use crate::packet_encodable::{PacketDecodeError, PacketEncodable, PacketEncodeError};
@ -56,7 +57,7 @@ pub struct DisplayedSkinParts {
}
impl PacketEncodable for DisplayedSkinParts {
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
let value = (self.cape as u8)
| (self.jacket as u8) << 1
| (self.left_sleeve as u8) << 2
@ -65,11 +66,11 @@ impl PacketEncodable for DisplayedSkinParts {
| (self.right_pants_leg as u8) << 5
| (self.hat as u8) << 6;
value.encode_packet(buffer)
value.encode_packet(buf)
}
fn decode_packet(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let value = u8::decode_packet(cursor)?;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let value = u8::decode_packet(buf)?;
Ok(Self {
cape: value & 0x01 != 0,

View file

@ -0,0 +1,10 @@
use potato_protocol_derive::Packet;
#[derive(Debug, Packet)]
#[packet(
configuration_id = crate::ids::configuration::serverbound::KEEP_ALIVE,
play_id = crate::ids::play::serverbound::KEEP_ALIVE,
)]
pub struct KeepAlivePacket {
pub id: i64,
}

View file

@ -4,6 +4,7 @@ pub mod custom_payload;
pub mod finish_configuration;
pub mod hello;
pub mod intention;
pub mod keep_alive;
pub mod login_acknowledged;
pub mod move_player_pos;
pub mod move_player_pos_rot;
@ -18,6 +19,7 @@ pub use custom_payload::CustomPayloadPacket;
pub use finish_configuration::FinishConfigurationPacket;
pub use hello::HelloPacket;
pub use intention::IntentionPacket;
pub use keep_alive::KeepAlivePacket;
pub use login_acknowledged::LoginAcknowledgedPacket;
pub use move_player_pos::MovePlayerPosPacket;
pub use move_player_pos_rot::MovePlayerPosRotPacket;

View file

@ -1,10 +1,7 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{
fmt,
io::{Cursor, Read},
marker::PhantomData,
};
use bytes::{Buf, BufMut};
use potato_data::identifier::Identifier;
use serde::{Serialize, de::DeserializeOwned};
use std::fmt;
use thiserror::Error;
use uuid::Uuid;
@ -12,8 +9,8 @@ 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>;
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError>;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError>;
}
#[derive(Error, Debug)]
@ -32,6 +29,10 @@ pub enum PacketDecodeError {
InvalidIdentifier(String),
#[error("Error while decoding NBT in packet {0}")]
NbtError(#[from] fastnbt::error::Error),
#[error("Not enough data to decode the packet yet")]
Incomplete,
#[error("Invalid VarInt")]
InvalidVarInt,
}
#[derive(Error, Debug)]
@ -47,13 +48,13 @@ pub enum PacketEncodeError {
}
impl PacketEncodable for Uuid {
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
buffer.write_u128::<BigEndian>(self.as_u128())?;
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
buf.put_u128(self.as_u128());
Ok(())
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let data: u128 = cursor.read_u128::<BigEndian>()?;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let data = buf.get_u128();
Ok(Uuid::from_u128(data))
}
}
@ -65,13 +66,13 @@ impl<T> PacketEncodable for Json<T>
where
T: Serialize + DeserializeOwned + fmt::Debug,
{
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
let json = serde_json::to_string(&self.0)?;
json.encode_packet(buffer)
json.encode_packet(buf)
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let json = String::decode_packet(cursor)?;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let json = String::decode_packet(buf)?;
Ok(Self(serde_json::from_str(&json)?))
}
}
@ -85,15 +86,16 @@ 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())?;
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
fastnbt::to_writer_with_opts(buf.writer(), &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))
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
todo!("Decode NBT in packets")
// let value = fastnbt::from_bytes(buf)?;
//
// Ok(Nbt(value))
}
}
@ -101,21 +103,21 @@ impl<T> PacketEncodable for Option<T>
where
T: PacketEncodable,
{
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
let present = self.is_some();
present.encode_packet(buffer)?;
present.encode_packet(buf)?;
if let Some(value) = self {
value.encode_packet(buffer)?;
value.encode_packet(buf)?;
}
Ok(())
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let present = bool::decode_packet(cursor)?;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let present = bool::decode_packet(buf)?;
if present {
Ok(Some(T::decode_packet(cursor)?))
Ok(Some(T::decode_packet(buf)?))
} else {
Ok(None)
}
@ -126,22 +128,22 @@ impl<T> PacketEncodable for Vec<T>
where
T: PacketEncodable,
{
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
let length: VarInt = self.len().into();
length.encode_packet(buffer)?;
length.encode_packet(buf)?;
for item in self {
item.encode_packet(buffer)?;
item.encode_packet(buf)?;
}
Ok(())
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let length: usize = VarInt::decode_packet(cursor)?.into();
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let length: usize = VarInt::decode_packet(buf)?.into();
let mut vec = Vec::with_capacity(length);
for _ in 0..length {
vec.push(T::decode_packet(cursor)?);
vec.push(T::decode_packet(buf)?);
}
Ok(vec)
@ -149,18 +151,18 @@ where
}
impl PacketEncodable for String {
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
let lenght: VarInt = self.len().into();
lenght.encode_packet(buffer)?;
lenght.encode_packet(buf)?;
buffer.extend_from_slice(self.as_bytes());
buf.put_slice(self.as_bytes());
Ok(())
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let lenght: usize = VarInt::decode_packet(cursor)?.into();
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let lenght: usize = VarInt::decode_packet(buf)?.into();
let mut buffer = vec![0; lenght];
cursor.read_exact(&mut buffer)?;
buf.copy_to_slice(&mut buffer);
let value = String::from_utf8(buffer)?;
Ok(value)
@ -168,38 +170,37 @@ impl PacketEncodable for String {
}
impl PacketEncodable for bool {
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
buffer.push(if *self { 1 } else { 0 });
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
buf.put_u8(if *self { 1 } else { 0 });
Ok(())
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let v = cursor.read_u8()?;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let v = buf.get_u8();
Ok(v != 0)
}
}
impl PacketEncodable for i8 {
fn encode_packet(&self, buffer: &mut Vec<u8>) -> Result<(), PacketEncodeError> {
buffer.write_i8(*self)?;
Ok(())
impl PacketEncodable for Identifier {
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
let val: String = self.into();
val.encode_packet(buf)
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let value = cursor.read_i8()?;
Ok(value)
}
}
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let str = String::decode_packet(buf)?;
let mut parts = str.split(":");
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)
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(),
))
}
}
@ -207,13 +208,13 @@ 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)?;
fn encode_packet(&self, buf: &mut impl BufMut) -> Result<(), PacketEncodeError> {
buf.$write_fn(*self);
Ok(())
}
fn decode_packet(cursor: &mut Cursor<&[u8]>) -> Result<Self, PacketDecodeError> {
let value = cursor.$read_fn::<BigEndian>()?;
fn decode_packet(buf: &mut impl Buf) -> Result<Self, PacketDecodeError> {
let value = buf.$read_fn();
Ok(value)
}
}
@ -222,14 +223,16 @@ macro_rules! number_impl {
}
number_impl! {
u16, read_u16, write_u16,
u32, read_u32, write_u32,
u64, read_u64, write_u64,
u8, get_u8, put_u8,
u16, get_u16, put_u16,
u32, get_u32, put_u32,
u64, get_u64, put_u64,
i16, read_i16, write_i16,
i32, read_i32, write_i32,
i64, read_i64, write_i64,
i8, get_i8, put_i8,
i16, get_i16, put_i16,
i32, get_i32, put_i32,
i64, get_i64, put_i64,
f32, read_f32, write_f32,
f64, read_f64, write_f64,
f32, get_f32, put_f32,
f64, get_f64, put_f64,
}

View file

@ -5,8 +5,11 @@ edition = "2024"
[dependencies]
potato-protocol = { path = "../potato-protocol" }
potato-data = { path = "../potato-data" }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
uuid.workspace = true
bytes.workspace = true

480
potato/src/connection.rs Normal file
View file

@ -0,0 +1,480 @@
use std::{collections::VecDeque, net::SocketAddr, sync::Arc};
use bytes::{Buf, BytesMut};
use potato_data::{
identifier::Identifier,
registry::{Registries, Registry},
};
use potato_protocol::{
datatypes::{pack::Pack, var_int::VarInt},
packet::{
Packet, RawPacket,
clientbound::{
self, GameEventPacket, SetChunkCacheCenterPacket,
registry_data::{RegistryData, RegistryDataEntry},
status_response,
},
serverbound,
},
packet_encodable::{Json, Nbt, PacketDecodeError, PacketEncodable, PacketEncodeError},
};
use thiserror::Error;
use tokio::{
io::AsyncReadExt,
net::tcp::OwnedReadHalf,
sync::{Mutex, RwLock, mpsc::Sender},
};
// Max packet size is 2MB in vanilla
const RECV_BUFFER_SIZE: usize = 1024 * 1024 * 2;
pub struct Client {
address: SocketAddr,
packet_sender: Sender<RawPacket>,
readable_stream: Mutex<OwnedReadHalf>,
state: RwLock<ConnectionState>,
recv_buffer: Mutex<BytesMut>,
packet_queue: Mutex<VecDeque<RawPacket>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum ConnectionState {
Handshaking,
Status,
Login,
Configuration,
Play,
}
#[allow(clippy::enum_variant_names)]
#[derive(Error, Debug)]
pub 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,
}
impl Client {
pub fn new(
packet_sender: Sender<RawPacket>,
readable_stream: OwnedReadHalf,
) -> Result<Client, std::io::Error> {
Ok(Client {
address: readable_stream.peer_addr()?,
packet_sender,
readable_stream: Mutex::new(readable_stream),
state: RwLock::new(ConnectionState::Handshaking),
recv_buffer: Mutex::new(BytesMut::new()),
packet_queue: Mutex::new(VecDeque::new()),
})
}
pub async fn needs_keep_alive(&self) -> bool {
let state = self.state.read().await;
*state == ConnectionState::Configuration || *state == ConnectionState::Play
}
pub async fn read_packets(&self) -> Result<(), ConnectionError> {
{
// NOTE: Deadlock waiting to happen, maybe there is a way to only lock once both are
// available.
let mut stream = self.readable_stream.lock().await;
let mut recv_buffer = self.recv_buffer.lock().await;
recv_buffer.reserve(RECV_BUFFER_SIZE);
// TODO: Deal with errors
let _ = stream.read_buf(&mut *recv_buffer).await;
}
// Try to read packets until we encounter an incomplete packet
loop {
match self.try_read_packet().await {
Ok(raw_packet) => {
let mut packet_queue = self.packet_queue.lock().await;
packet_queue.push_back(raw_packet);
}
Err(ConnectionError::DecodeError(PacketDecodeError::Incomplete)) => {
break;
}
Err(e) => return Err(e),
}
}
Ok(())
}
async fn try_read_packet(&self) -> Result<RawPacket, ConnectionError> {
let mut recv_buffer = self.recv_buffer.lock().await;
let length: usize = VarInt::decode_packet(&mut *recv_buffer)?.into();
if recv_buffer.remaining() < length {
return Err(PacketDecodeError::Incomplete.into());
}
let id = VarInt::decode_packet(&mut *recv_buffer)?;
let data = recv_buffer.split_to(length - id.encoded_len()).freeze();
Ok(RawPacket { id, data })
}
pub async fn send_packet<T: Packet>(&self, packet: &T) -> Result<(), ConnectionError> {
let state = { *self.state.read().await };
println!("[{} ({:?})] -> {:?}", self.address, state, packet);
let mut buffer = BytesMut::new();
// Encode ID
let packet_id: i32 = match 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,
};
assert!(packet_id != -1, "Packet ID can not be -1");
let packet_id: VarInt = packet_id.into();
packet.encode_packet(&mut buffer)?;
match self
.packet_sender
.send(RawPacket {
id: packet_id,
data: buffer.freeze(),
})
.await
{
Ok(_) => (),
Err(e) => {
println!("[{}] Error while sending packet: {}", self.address, e);
return Err(ConnectionError::Finished);
}
}
Ok(())
}
pub async fn handle_packets(
&self,
registries: &Arc<Registries>,
) -> Result<(), ConnectionError> {
while let Some(raw_packet) = {
let mut packet_queue = self.packet_queue.lock().await;
packet_queue.pop_front()
} {
// TODO: Need to check that this gets dropped immediately
let state = { *self.state.read().await };
match state {
ConnectionState::Handshaking => {
self.handle_handshaking(raw_packet).await?;
}
ConnectionState::Status => {
self.handle_status(raw_packet).await?;
}
ConnectionState::Login => {
self.handle_login(raw_packet).await?;
}
ConnectionState::Configuration => {
self.handle_configuration(raw_packet, registries).await?;
}
ConnectionState::Play => {
// TODO: Might not be able to handle this in the client, might need a full
// player for that.
// println!("Play packet on client. Ignoring for now")
// self.handle_play(raw_packet)?;
}
}
}
Ok(())
}
async fn handle_handshaking(&self, raw_packet: RawPacket) -> Result<(), ConnectionError> {
match raw_packet.id.into() {
serverbound::IntentionPacket::HANDSHAKE_ID => {
let packet = serverbound::IntentionPacket::from_raw(raw_packet)?;
println!("[{} (Handshaking)] <- {:?}", self.address, packet,);
self.change_state_num(packet.next_state.into()).await?
}
// TODO: Legacy server list ping
_ => {
println!(
"[{} (Handshaking)] <- Unknown packet: {}",
self.address, raw_packet
);
}
}
Ok(())
}
async fn handle_status(&self, raw_packet: RawPacket) -> Result<(), ConnectionError> {
match raw_packet.id.into() {
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,
}),
})
.await?;
}
serverbound::PingRequestPacket::STATUS_ID => {
let packet = serverbound::PingRequestPacket::from_raw(raw_packet)?;
println!("[{} (Status)] <- {:?}", self.address, packet);
self.send_packet(&clientbound::PongResponsePacket {
timestamp: packet.timestamp,
})
.await?;
// TODO: Signal to close connection
// self.stream.shutdown(Shutdown::Both)?;
return Err(ConnectionError::Finished);
}
_ => {
println!(
"[{} (Status)] <- Unknown packet: {}",
self.address, raw_packet
);
}
}
Ok(())
}
async fn handle_login(&self, raw_packet: RawPacket) -> Result<(), ConnectionError> {
match raw_packet.id.into() {
serverbound::HelloPacket::LOGIN_ID => {
let packet = serverbound::HelloPacket::from_raw(raw_packet)?;
println!("[{} (Login)] <- {:?}", self.address, packet);
self.send_packet(&clientbound::LoginFinishedPacket {
uuid: packet.uuid,
username: packet.name,
properties: vec![],
})
.await?;
}
serverbound::LoginAcknowledgedPacket::LOGIN_ID => {
let packet = serverbound::LoginAcknowledgedPacket::from_raw(raw_packet)?;
println!("[{} (Login)] <- {:?}", self.address, packet);
self.change_state(ConnectionState::Configuration).await?;
self.send_packet(&clientbound::SelectKnownPacksPacket {
packs: vec![Pack {
namespace: "minecraft".to_owned(),
id: "core".to_owned(),
version: "1.21.4".to_owned(),
}],
})
.await?;
}
_ => {
println!(
"[{} (Login)] <- Unknown packet: {}",
self.address, raw_packet
);
}
}
Ok(())
}
async fn handle_configuration(
&self,
raw_packet: RawPacket,
registries: &Arc<Registries>,
) -> Result<(), ConnectionError> {
match raw_packet.id.into() {
serverbound::ClientInformationPacket::CONFIGURATION_ID => {
let packet = serverbound::ClientInformationPacket::from_raw(raw_packet)?;
println!("[{} (Configuration)] <- {:?}", self.address, packet);
}
serverbound::CustomPayloadPacket::CONFIGURATION_ID => {
let packet = serverbound::CustomPayloadPacket::from_raw(raw_packet)?;
println!("[{} (Configuration)] <- {:?}", self.address, packet);
}
serverbound::SelectKnownPacksPacket::CONFIGURATION_ID => {
let packet = serverbound::SelectKnownPacksPacket::from_raw(raw_packet)?;
println!("[{} (Configuration)] <- {:?}", self.address, packet);
self.sync_registries(registries).await?;
self.send_packet(&clientbound::FinishConfigurationPacket)
.await?;
}
serverbound::FinishConfigurationPacket::CONFIGURATION_ID => {
let packet = serverbound::finish_configuration::FinishConfigurationPacket;
println!("[{} (Configuration)] <- {:?}", self.address, packet);
self.change_state(ConnectionState::Play).await?;
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,
})
.await?;
self.send_packet(&GameEventPacket {
event: 13,
data: 0.,
})
.await?;
self.send_packet(&SetChunkCacheCenterPacket {
x: 0.into(),
z: 0.into(),
})
.await?;
}
serverbound::KeepAlivePacket::CONFIGURATION_ID => {
let _ = serverbound::KeepAlivePacket::from_raw(raw_packet)?;
}
_ => {
println!(
"[{} (Configuration)] <- Unknown packet: {}",
self.address, raw_packet
);
}
}
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(())
// }
async fn sync_registries(&self, registries: &Arc<Registries>) -> Result<(), ConnectionError> {
self.send_registry(
Identifier::minecraft_str("dimension_type"),
&registries.dimension_types,
)
.await?;
self.send_registry(
Identifier::minecraft_str("painting_variant"),
&registries.painting_variants,
)
.await?;
self.send_registry(
Identifier::minecraft_str("wolf_variant"),
&registries.wolf_variants,
)
.await?;
self.send_registry(
Identifier::minecraft_str("damage_type"),
&registries.damage_types,
)
.await?;
self.send_registry(
Identifier::minecraft_str("worldgen/biome"),
&registries.worldgen.biomes,
)
.await?;
Ok(())
}
async fn send_registry<T>(
&self,
identifier: Identifier,
registry: &Registry<T>,
) -> Result<(), ConnectionError>
where
for<'a> &'a T: Into<RegistryData>,
{
self.send_packet(&clientbound::RegistryDataPacket {
registry_id: identifier,
entries: registry
.iter()
.map(|(key, value)| RegistryDataEntry {
id: key.clone(),
data: Some(Nbt(value.into())),
})
.collect(),
})
.await
}
async fn change_state_num(&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).await
}
async fn change_state(&self, new_state: ConnectionState) -> Result<(), ConnectionError> {
{
let state = self.state.read().await;
println!("[{}] {:?} -> {:?}", self.address, state, new_state);
}
*self.state.write().await = new_state;
Ok(())
}
}

View file

@ -1,544 +1,101 @@
use std::{
collections::HashMap,
fmt::Debug,
fs::read_to_string,
io::{Cursor, Read, Write},
net::{Shutdown, SocketAddr, TcpListener, TcpStream},
path::PathBuf,
sync::Arc,
thread,
};
pub mod connection;
pub mod player;
pub mod server;
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;
use std::{sync::Arc, time::Duration};
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");
use connection::Client;
use potato_protocol::packet::clientbound;
use server::Server;
use tokio::{net::TcpListener, time::interval};
damage_types.insert(Identifier::minecraft(name), data);
}
}
}
#[tokio::main]
async fn main() {
let server = PotatoServer::new();
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))),
}
}
server.listen_for_connections().await;
}
fn handle_client(stream: TcpStream, registries: Arc<Registries>) {
if let Ok(mut client) = ClientConnection::new(stream, registries) {
pub struct PotatoServer {
server: Server,
}
impl PotatoServer {
pub fn new() -> Self {
PotatoServer {
server: Server::new(),
}
}
pub async fn listen_for_connections(&self) {
let listener = TcpListener::bind("0.0.0.0:25565")
.await
.expect("Failed to start listening on port 25565");
println!("listening started, ready to accept");
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;
if let Ok((stream, _)) = listener.accept().await {
let (stream_read, mut stream_send) = stream.into_split();
let (tx, mut rx) = tokio::sync::mpsc::channel(64);
let Ok(client) = Client::new(tx, stream_read) else {
println!("Failed to create client");
continue;
};
let client = Arc::new(client);
tokio::spawn(async move {
while let Some(packet) = rx.recv().await {
if let Err(e) = packet.write_to_stream(&mut stream_send).await {
println!("Error while sending packet: {}", e);
// TODO: Close connection with client here
break;
}
}
});
// Keep alive task
{
let client = client.clone();
tokio::spawn(async move {
let mut interval = interval(Duration::from_secs(10));
// TODO: Terminate this when the client connection is closed.
loop {
interval.tick().await;
if client.needs_keep_alive().await {
// TODO: Deal with this error by closing the connection.
// TODO: Use a unique id for each keep alive packet (or at least a
// timestamp)
match client
.send_packet(&clientbound::KeepAlivePacket { id: 0 })
.await
{
Ok(_) => {}
Err(e) => {
println!("Error while sending keep alive packet: {}", e)
}
};
}
}
});
}
{
let registries = self.server.registries.clone();
tokio::spawn(async move {
// TODO: Deal with errors
loop {
client
.read_packets()
.await
.expect("Error while reading packets");
client
.handle_packets(&registries)
.await
.expect("Error while handling packets");
}
});
}
}
}
}
}
#[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(())
}
}

2
potato/src/player.rs Normal file
View file

@ -0,0 +1,2 @@

46
potato/src/server.rs Normal file
View file

@ -0,0 +1,46 @@
use std::sync::Arc;
use potato_data::{datapack::Datapack, registry::Registries};
pub struct Server {
pub registries: Arc<Registries>,
}
impl Server {
#[allow(clippy::new_without_default)]
pub fn new() -> Server {
println!("Looking for datapacks...");
let datapacks: Vec<_> = std::fs::read_dir("datapacks")
.expect("Failed to read datapacks directory")
.map(|f| f.unwrap().path())
.filter(|p| p.is_dir())
.map(Datapack::new)
.collect();
println!("Found {} datapacks", datapacks.len());
println!("Loading datapacks...");
let mut registries = Registries::empty();
datapacks
.iter()
.for_each(|pack| pack.load(&mut registries).expect("Failed to load datapack"));
println!("Finished loading datapacks");
println!("Loaded {} trim materials", registries.trim_materials.len());
println!("Loaded {} trim patterns", registries.trim_patterns.len());
println!("Loaded {} chat types", registries.chat_types.len());
println!(
"Loaded {} dimension types",
registries.dimension_types.len()
);
println!(
"Loaded {} painting variants",
registries.painting_variants.len()
);
println!("Loaded {} wolf variants", registries.wolf_variants.len());
println!("Loaded {} damage types", registries.damage_types.len());
println!("Loaded {} biomes", registries.worldgen.biomes.len());
Server {
registries: Arc::new(registries),
}
}
}