diff --git a/Cargo.lock b/Cargo.lock index 44188414f..876ea3d42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3145,6 +3145,19 @@ dependencies = [ [[package]] name = "hyperion-minecraft-proto" version = "0.1.0" +dependencies = [ + "hyperion-minecraft-proto-derive", + "serde_json", +] + +[[package]] +name = "hyperion-minecraft-proto-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] [[package]] name = "hyperion-nerd-font" diff --git a/Cargo.toml b/Cargo.toml index c8e3b79de..2ffd2750f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ 'crates/hyperion-inventory', 'crates/hyperion-item', 'crates/hyperion-minecraft-proto', + 'crates/hyperion-minecraft-proto-derive', 'crates/hyperion-nerd-font', 'crates/hyperion-packet-macros', 'crates/hyperion-palette', @@ -209,6 +210,12 @@ path = 'crates/hyperion-inventory' [workspace.dependencies.hyperion-item] path = 'crates/hyperion-item' +[workspace.dependencies.hyperion-minecraft-proto] +path = 'crates/hyperion-minecraft-proto' + +[workspace.dependencies.hyperion-minecraft-proto-derive] +path = 'crates/hyperion-minecraft-proto-derive' + [workspace.dependencies.hyperion-nerd-font] path = 'crates/hyperion-nerd-font' diff --git a/crates/hyperion-minecraft-proto-derive/.gitignore b/crates/hyperion-minecraft-proto-derive/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/crates/hyperion-minecraft-proto-derive/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/hyperion-minecraft-proto-derive/Cargo.toml b/crates/hyperion-minecraft-proto-derive/Cargo.toml new file mode 100644 index 000000000..424895e7c --- /dev/null +++ b/crates/hyperion-minecraft-proto-derive/Cargo.toml @@ -0,0 +1,18 @@ +[dependencies] +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true + +[lib] +proc-macro = true + +[lints] +workspace = true + +[package] +authors = ["Andrew Gazelka "] +edition.workspace = true +name = "hyperion-minecraft-proto-derive" +publish = false +readme = "README.md" +version.workspace = true diff --git a/crates/hyperion-minecraft-proto-derive/README.md b/crates/hyperion-minecraft-proto-derive/README.md new file mode 100644 index 000000000..f848122a4 --- /dev/null +++ b/crates/hyperion-minecraft-proto-derive/README.md @@ -0,0 +1,53 @@ +# hyperion-minecraft-proto-derive + +`#[derive(Encode, Decode)]` for the Minecraft wire format. + +A packet body is a fixed sequence of fields with nothing between them, so the +derive is a loop over the fields in declaration order. Anything that is not +that — a length that is not a `VarInt`, a discriminant that selects a layout — +is deliberately out of scope: a derive that guessed would produce a codec that +looks right and desynchronises the stream. + +```rust,ignore +use hyperion_minecraft_proto::{Decode, Encode, Uuid}; + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct Hello<'a> { + #[proto(max_len = 16)] + pub name: &'a str, + pub profile_id: Uuid, +} +``` + +## What it supports + +- Named, tuple and unit structs. A unit struct occupies no bytes, matching the + server's `StreamCodec.unit` packets. +- Fieldless enums, written as a `VarInt` discriminant. An unrecognised value + decodes to `Error::InvalidEnum` rather than to a wrong variant. +- Borrowed fields: a type with one lifetime gets `impl<'a> Decode<'a>`, one + with none gets a fresh lifetime. +- Type parameters, bounded with `Encode` and `Decode<'de>` respectively. + +## Field attributes + +| attribute | effect | +| --- | --- | +| `#[proto(max_len = N)]` | limit on the innermost string, in UTF-16 code units | +| `#[proto(max_count = N)]` | limit on the innermost collection's element count | +| `#[proto(with = path)]` | use `path::encode` and `path::decode` for this field | + +`max_len` and `max_count` thread through `Option<_>` and `Vec<_>` to the type +they constrain, so `#[proto(max_len = 1024)] pages: Vec<&'a str>` bounds each +page rather than the list. + +## What it refuses + +Every one of these is a compile error rather than a silent approximation: + +- an enum variant with fields, since the discriminant alone does not say how to + read what follows +- more than one lifetime, since the derive would have to guess which one the + reader lends to +- a `with` beside a limit, since the limit would never be applied +- a union diff --git a/crates/hyperion-minecraft-proto-derive/src/attr.rs b/crates/hyperion-minecraft-proto-derive/src/attr.rs new file mode 100644 index 000000000..4103f7889 --- /dev/null +++ b/crates/hyperion-minecraft-proto-derive/src/attr.rs @@ -0,0 +1,54 @@ +//! Parsing of the `#[proto(...)]` field attribute. + +use syn::{Attribute, Expr, LitInt, Path, Result, spanned::Spanned}; + +/// What a `#[proto(...)]` attribute says about one field. +#[derive(Default)] +pub struct Field { + /// `max_len = N`: the innermost string's limit in UTF-16 code units. + pub max_len: Option, + /// `max_count = N`: the innermost collection's element limit. + pub max_count: Option, + /// `with = path`: a module supplying `encode` and `decode` for this field. + pub with: Option, +} + +impl Field { + /// Read the single `#[proto(...)]` attribute a field may carry. + pub fn parse(attrs: &[Attribute]) -> Result { + let mut out = Self::default(); + for attr in attrs.iter().filter(|a| a.path().is_ident("proto")) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("max_len") { + out.max_len = Some(usize_value(&meta.value()?.parse()?)?); + } else if meta.path.is_ident("max_count") { + out.max_count = Some(usize_value(&meta.value()?.parse()?)?); + } else if meta.path.is_ident("with") { + out.with = Some(meta.value()?.parse()?); + } else { + return Err(meta.error("expected `max_len`, `max_count` or `with`")); + } + Ok(()) + })?; + } + if out.with.is_some() && (out.max_len.is_some() || out.max_count.is_some()) { + // A `with` module owns the whole field, so a limit beside it would + // be silently ignored rather than enforced somewhere unexpected. + return Err(syn::Error::new( + attrs[0].span(), + "`with` supplies the whole codec, so it cannot be combined with a limit", + )); + } + Ok(out) + } +} + +fn usize_value(expr: &Expr) -> Result { + let Expr::Lit(lit) = expr else { + return Err(syn::Error::new(expr.span(), "expected an integer literal")); + }; + let syn::Lit::Int(int) = &lit.lit else { + return Err(syn::Error::new(expr.span(), "expected an integer literal")); + }; + LitInt::base10_parse::(int) +} diff --git a/crates/hyperion-minecraft-proto-derive/src/expand.rs b/crates/hyperion-minecraft-proto-derive/src/expand.rs new file mode 100644 index 000000000..a8957eeb7 --- /dev/null +++ b/crates/hyperion-minecraft-proto-derive/src/expand.rs @@ -0,0 +1,410 @@ +//! Turning a type definition into `Encode` and `Decode` impls. + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{ + Data, DataEnum, DataStruct, DeriveInput, Fields, GenericParam, Generics, Index, Lifetime, + LifetimeParam, Result, Type, TypeParamBound, parse_quote, spanned::Spanned, +}; + +use crate::attr; + +/// Path to the runtime crate. `extern crate self as hyperion_minecraft_proto` +/// in the crate root is what makes this resolve from inside it as well. +fn krate() -> TokenStream { + quote!(::hyperion_minecraft_proto) +} + +pub fn encode(input: &DeriveInput) -> Result { + let name = &input.ident; + let krate = krate(); + + let body = match &input.data { + Data::Struct(data) => encode_struct(data)?, + Data::Enum(data) => encode_enum(data)?, + Data::Union(_) => return Err(unsupported(input, "a union has no wire form")), + }; + + let mut generics = input.generics.clone(); + bound_type_params(&mut generics, parse_quote!(#krate::Encode)); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics #krate::Encode for #name #ty_generics #where_clause { + fn encode(&self, writer: &mut #krate::Writer) -> #krate::Result<()> { + #body + ::core::result::Result::Ok(()) + } + } + }) +} + +pub fn decode(input: &DeriveInput) -> Result { + let name = &input.ident; + let krate = krate(); + let life = decode_lifetime(&input.generics)?; + + let body = match &input.data { + Data::Struct(data) => decode_struct(data)?, + Data::Enum(data) => decode_enum(name, data)?, + Data::Union(_) => return Err(unsupported(input, "a union has no wire form")), + }; + + let mut generics = input.generics.clone(); + bound_type_params(&mut generics, parse_quote!(#krate::Decode<#life>)); + let (_, ty_generics, where_clause) = generics.split_for_impl(); + let impl_generics = decode_impl_generics(&generics, &life); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics #krate::Decode<#life> for #name #ty_generics #where_clause { + fn decode(reader: &mut #krate::Reader<#life>) -> #krate::Result { + ::core::result::Result::Ok(#body) + } + } + }) +} + +// --------------------------------------------------------------------------- +// Structs +// --------------------------------------------------------------------------- + +fn encode_struct(data: &DataStruct) -> Result { + let mut out = TokenStream::new(); + for (index, field) in data.fields.iter().enumerate() { + let options = attr::Field::parse(&field.attrs)?; + let place = field.ident.as_ref().map_or_else( + || { + let index = Index::from(index); + quote!(self.#index) + }, + |ident| quote!(self.#ident), + ); + out.extend(encode_value("e!(&#place), &field.ty, &options)?); + } + Ok(out) +} + +fn decode_struct(data: &DataStruct) -> Result { + match &data.fields { + Fields::Named(fields) => { + let values = fields + .named + .iter() + .map(|field| { + let ident = field.ident.as_ref().expect("named field"); + let value = decode_value(&field.ty, &attr::Field::parse(&field.attrs)?)?; + Ok(quote!(#ident: #value)) + }) + .collect::>>()?; + Ok(quote!(Self { #(#values,)* })) + } + Fields::Unnamed(fields) => { + let values = fields + .unnamed + .iter() + .map(|field| decode_value(&field.ty, &attr::Field::parse(&field.attrs)?)) + .collect::>>()?; + Ok(quote!(Self(#(#values,)*))) + } + Fields::Unit => Ok(quote!(Self)), + } +} + +// --------------------------------------------------------------------------- +// Enums +// --------------------------------------------------------------------------- + +/// Discriminants in declaration order, filling in the implicit ones the way +/// Rust does: each unannotated variant is one past the previous. +fn discriminants(data: &DataEnum) -> Result> { + let mut out = Vec::with_capacity(data.variants.len()); + let mut next = 0i32; + for variant in &data.variants { + if !matches!(variant.fields, Fields::Unit) { + return Err(syn::Error::new( + variant.span(), + "a variant with fields needs a hand-written codec: the wire form of the \ + discriminant does not say how to read what follows", + )); + } + if let Some((_, expr)) = &variant.discriminant { + let syn::Expr::Lit(lit) = expr else { + return Err(syn::Error::new(expr.span(), "expected an integer literal")); + }; + let syn::Lit::Int(int) = &lit.lit else { + return Err(syn::Error::new(expr.span(), "expected an integer literal")); + }; + next = int.base10_parse()?; + } + out.push((&variant.ident, next)); + next = next.checked_add(1).ok_or_else(|| { + syn::Error::new(variant.span(), "discriminant overflows the wire's `i32`") + })?; + } + Ok(out) +} + +fn encode_enum(data: &DataEnum) -> Result { + let arms = discriminants(data)? + .into_iter() + .map(|(variant, value)| quote!(Self::#variant => #value)); + // A match rather than `*self as i32`, so the derive works on an enum that + // is neither `Copy` nor `#[repr(i32)]`. + Ok(quote! { + writer.var_int(match self { #(#arms,)* }); + }) +} + +fn decode_enum(name: &syn::Ident, data: &DataEnum) -> Result { + let krate = krate(); + let label = name.to_string(); + let arms = discriminants(data)? + .into_iter() + .map(|(variant, value)| quote!(#value => Self::#variant)); + Ok(quote! { + match reader.var_int()? { + #(#arms,)* + value => { + return ::core::result::Result::Err(#krate::Error::InvalidEnum { + name: #label, + value, + }); + } + } + }) +} + +// --------------------------------------------------------------------------- +// One field +// --------------------------------------------------------------------------- + +/// A field type as far as the derive needs to understand it. +enum Shape<'a> { + /// `Option`: a boolean, then the value when present. + Option(&'a Type), + /// `Vec`: a `VarInt` count, then that many elements. + Vec(&'a Type), + /// `&str`, whose length limit the derive can apply directly. + Str, + /// `&[u8]`, whose limit is a byte count rather than a character count. + Bytes, + /// Anything else, which carries its own `Encode`/`Decode`. + Opaque, +} + +fn shape(ty: &Type) -> Shape<'_> { + if let Type::Reference(reference) = ty { + return match reference.elem.as_ref() { + Type::Path(path) if path.path.is_ident("str") => Shape::Str, + Type::Slice(slice) => match slice.elem.as_ref() { + Type::Path(path) if path.path.is_ident("u8") => Shape::Bytes, + _ => Shape::Opaque, + }, + _ => Shape::Opaque, + }; + } + let Type::Path(path) = ty else { + return Shape::Opaque; + }; + let Some(segment) = path.path.segments.last() else { + return Shape::Opaque; + }; + let syn::PathArguments::AngleBracketed(args) = &segment.arguments else { + return Shape::Opaque; + }; + let [syn::GenericArgument::Type(inner)] = args.args.iter().collect::>()[..] else { + return Shape::Opaque; + }; + if segment.ident == "Option" { + Shape::Option(inner) + } else if segment.ident == "Vec" { + Shape::Vec(inner) + } else { + Shape::Opaque + } +} + +/// Whether the derive has to walk into the type rather than delegate to its +/// `Encode`/`Decode` impl. It does exactly when a limit has to reach a type +/// nested inside the field's own. +const fn limited(options: &attr::Field) -> bool { + options.max_len.is_some() || options.max_count.is_some() +} + +fn encode_value(place: &TokenStream, ty: &Type, options: &attr::Field) -> Result { + let krate = krate(); + if let Some(path) = &options.with { + return Ok(quote!(#path::encode(#place, writer)?;)); + } + if !limited(options) { + return Ok(quote!(#krate::Encode::encode(#place, writer)?;)); + } + // Bound to a name first: the place is an expression like `&self.pages`, + // and `&self.pages.iter()` would borrow the iterator rather than the field. + let body = encode_limited("e!(value), ty, options, 0)?; + Ok(quote! { + { + let value = #place; + #body + } + }) +} + +fn encode_limited( + place: &TokenStream, + ty: &Type, + options: &attr::Field, + depth: usize, +) -> Result { + let krate = krate(); + match shape(ty) { + Shape::Option(inner) => { + let binding = format_ident!("value_{}", depth); + let body = encode_limited("e!(#binding), inner, options, depth + 1)?; + Ok(quote! { + writer.bool(#place.is_some()); + if let ::core::option::Option::Some(#binding) = #place.as_ref() { + #body + } + }) + } + Shape::Vec(inner) => { + let binding = format_ident!("item_{}", depth); + let body = encode_limited("e!(#binding), inner, options, depth + 1)?; + let max = count_limit(options.max_count); + Ok(quote! { + #krate::codec::write_count(writer, #place.len(), #max)?; + for #binding in #place.iter() { + #body + } + }) + } + Shape::Str => { + let max = require_len(options, ty)?; + Ok(quote!(writer.string_with_limit(#place, #max)?;)) + } + Shape::Bytes => { + let max = require_len(options, ty)?; + Ok(quote!(writer.byte_array_with_limit(#place, #max)?;)) + } + Shape::Opaque => Ok(quote!(#krate::Encode::encode(#place, writer)?;)), + } +} + +fn decode_value(ty: &Type, options: &attr::Field) -> Result { + let krate = krate(); + if let Some(path) = &options.with { + return Ok(quote!(#path::decode(reader)?)); + } + if !limited(options) { + return Ok(quote!(#krate::Decode::decode(reader)?)); + } + decode_limited(ty, options) +} + +fn decode_limited(ty: &Type, options: &attr::Field) -> Result { + let krate = krate(); + match shape(ty) { + Shape::Option(inner) => { + let body = decode_limited(inner, options)?; + Ok(quote! { + if reader.bool()? { + ::core::option::Option::Some(#body) + } else { + ::core::option::Option::None + } + }) + } + Shape::Vec(inner) => { + let body = decode_limited(inner, options)?; + let max = count_limit(options.max_count); + // The capacity is clamped to the bytes actually available, because + // the count is attacker-controlled and every element costs at + // least one byte. + Ok(quote! { + { + let count = #krate::codec::read_count(reader, #max)?; + let mut items = ::std::vec::Vec::with_capacity( + ::core::cmp::min(count, reader.remaining_len()), + ); + for _ in 0..count { + items.push(#body); + } + items + } + }) + } + Shape::Str => { + let max = require_len(options, ty)?; + Ok(quote!(reader.string_with_limit(#max)?)) + } + Shape::Bytes => { + let max = require_len(options, ty)?; + Ok(quote!(reader.byte_array_with_limit(#max)?)) + } + Shape::Opaque => Ok(quote!(#krate::Decode::decode(reader)?)), + } +} + +fn require_len(options: &attr::Field, ty: &Type) -> Result { + options.max_len.ok_or_else(|| { + syn::Error::new( + ty.span(), + "`max_count` reached a string or byte slice; use `max_len`", + ) + }) +} + +fn count_limit(max: Option) -> TokenStream { + max.map_or_else( + || quote!(::core::option::Option::None), + |max| quote!(::core::option::Option::Some(#max)), + ) +} + +// --------------------------------------------------------------------------- +// Generics +// --------------------------------------------------------------------------- + +/// The lifetime the decoded value borrows from. +/// +/// One lifetime is the borrowed case and none is the owned case. Two would +/// mean the derive had to decide which of them the reader lends to, and +/// guessing wrong is a borrow error a long way from its cause. +fn decode_lifetime(generics: &Generics) -> Result { + let mut lifetimes = generics.lifetimes(); + let first = lifetimes.next(); + if let Some(extra) = lifetimes.next() { + return Err(syn::Error::new( + extra.span(), + "`Decode` needs one lifetime to borrow from, and this type has several", + )); + } + Ok(first.map_or_else( + || Lifetime::new("'de", Span::call_site()), + |param| param.lifetime.clone(), + )) +} + +fn decode_impl_generics(generics: &Generics, life: &Lifetime) -> Generics { + let mut out = generics.clone(); + if !generics.lifetimes().any(|param| param.lifetime == *life) { + out.params + .insert(0, GenericParam::Lifetime(LifetimeParam::new(life.clone()))); + } + out +} + +fn bound_type_params(generics: &mut Generics, bound: TypeParamBound) { + for param in &mut generics.params { + if let GenericParam::Type(param) = param { + param.bounds.push(bound.clone()); + } + } +} + +fn unsupported(input: &DeriveInput, why: &str) -> syn::Error { + syn::Error::new(input.span(), why) +} diff --git a/crates/hyperion-minecraft-proto-derive/src/lib.rs b/crates/hyperion-minecraft-proto-derive/src/lib.rs new file mode 100644 index 000000000..fba5effa0 --- /dev/null +++ b/crates/hyperion-minecraft-proto-derive/src/lib.rs @@ -0,0 +1,78 @@ +//! `Encode` and `Decode` derives for the Minecraft wire format. +//! +//! A packet body is a fixed sequence of fields with nothing between them, so +//! the derive is a loop over the fields in declaration order. Everything that +//! is not that -- a length that is not a `VarInt`, a discriminant that selects +//! a layout -- is out of scope on purpose: a derive that guessed would produce +//! a codec that looks right and desynchronises the stream. +//! +//! # Structs +//! +//! ```ignore +//! #[derive(Encode, Decode)] +//! pub struct Hello<'a> { +//! #[proto(max_len = 16)] +//! pub name: &'a str, +//! pub profile_id: Uuid, +//! } +//! ``` +//! +//! Fields are written in order and read in the same order. Named, tuple and +//! unit structs all work; a unit struct occupies no bytes, which is how the +//! server's own `StreamCodec.unit` packets are shaped. +//! +//! # Enums +//! +//! ```ignore +//! #[derive(Encode, Decode)] +//! #[repr(i32)] +//! pub enum ClientIntent { +//! Status = 1, +//! Login = 2, +//! Transfer = 3, +//! } +//! ``` +//! +//! Every variant must be fieldless. The discriminant is written as a `VarInt`, +//! and a value naming no variant decodes to [`Error::InvalidEnum`] rather than +//! to a silently wrong variant. +//! +//! # Field attributes +//! +//! | attribute | effect | +//! | --- | --- | +//! | `#[proto(max_len = N)]` | limit on the innermost string, in UTF-16 code units | +//! | `#[proto(max_count = N)]` | limit on the innermost collection's element count | +//! | `#[proto(with = path)]` | use `path::encode` and `path::decode` for this field | +//! +//! `max_len` and `max_count` thread through `Option<_>` and `Vec<_>` to the +//! type they constrain, so `#[proto(max_len = 1024)] pages: Vec<&'a str>` +//! bounds each page rather than the list. Both limits are enforced on write as +//! well as on read: the server checks on read only, but a value that cannot be +//! read is one that should never have been sent. +//! +//! [`Error::InvalidEnum`]: ../hyperion_minecraft_proto/enum.Error.html + +mod attr; +mod expand; + +use proc_macro::TokenStream; +use syn::{DeriveInput, parse_macro_input}; + +/// Derive [`Encode`](../hyperion_minecraft_proto/trait.Encode.html). +#[proc_macro_derive(Encode, attributes(proto))] +pub fn derive_encode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand::encode(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Derive [`Decode`](../hyperion_minecraft_proto/trait.Decode.html). +#[proc_macro_derive(Decode, attributes(proto))] +pub fn derive_decode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand::decode(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/crates/hyperion-minecraft-proto/Cargo.toml b/crates/hyperion-minecraft-proto/Cargo.toml index 3dcd9ed73..812377a1d 100644 --- a/crates/hyperion-minecraft-proto/Cargo.toml +++ b/crates/hyperion-minecraft-proto/Cargo.toml @@ -1,4 +1,8 @@ +[build-dependencies] +serde_json.workspace = true + [dependencies] +hyperion-minecraft-proto-derive.workspace = true [lints] workspace = true diff --git a/crates/hyperion-minecraft-proto/README.md b/crates/hyperion-minecraft-proto/README.md index 27993adbc..d43558187 100644 --- a/crates/hyperion-minecraft-proto/README.md +++ b/crates/hyperion-minecraft-proto/README.md @@ -5,39 +5,68 @@ third-party protocol crate. Currently targets **Minecraft 26.2, protocol 776**. -## What is generated and what is not +## Where each thing comes from -`src/generated/` comes out of the nix pipeline in `nix/minecraft-data.nix` and -is committed so `cargo build` works without nix. `nix flake check` fails if the -committed copy drifts from what the pipeline produces. +`nix/minecraft-data.nix` runs Mojang's data generator and cfr over the server +jar, and `nix/extract-protocol.py` follows the server's own stream codecs down +to the bytes. Its output is `protocol.json`, committed here. Everything else is +derived from that one file. -| module | contents | source | +| artifact | where it lives | how it gets there | | --- | --- | --- | -| `generated::version` | protocol 776, game version, world version | the jar's own `version.json` | -| `generated::packet_id` | 256 packet ids across five states | Mojang's data generator, cross-checked against the server's registration order | -| `generated::registry` | 95 registries, 6,979 entries | Mojang's data generator | +| `protocol.json` | committed | `nix run .#sync-minecraft-proto` | +| `packets::` | `OUT_DIR`, at build time | `build.rs` | +| `generated::{packet_id, registry, version, wire, data_component}` | committed | `nix run .#sync-minecraft-proto` | +| `types::` primitives, `nbt`, `text`, `item` | hand-written | against the decompiled sources | -`packets::` is **not** generated. Packet layouts live in `StreamCodec` -compositions in the server's code, and about half of them bottom out in domain -codecs — `ItemStack`, chat components, entity metadata — that no shallow -analysis recovers. Codecs are written by hand against the decompiled sources, -with the extractor's `protocol.json` acting as a checklist. See -`docs/minecraft-26.2-migration.md` for the measurements behind that decision. +`nix flake check` fails if either committed artifact drifts from what the +pipeline produces, which is what makes the committed copies trustworthy rather +than merely present. + +The split is deliberate and `docs/minecraft-26.2-migration.md` argues it: the +tables are a *total* projection of `protocol.json` and are what a reader greps, +so they are committed; the packet structs are a *partial* one — the generator +declines what it cannot express — so they are produced at build time, where no +committed copy can disagree with the filter. + +## What a generated packet looks like + +```rust +use hyperion_minecraft_proto::{PROTOCOL_VERSION, VarInt, packets::handshake, types::ClientIntent}; + +let hello = handshake::serverbound::Intention { + protocol_version: VarInt(PROTOCOL_VERSION), + host_name: "localhost", + port: 25565, + intention: ClientIntent::Login, +}; +``` + +177 of the 180 packet classes whose layout the extractor recovered in full are +generated. **A layout it could not follow all the way has no struct at all**, so +a generated codec is complete by construction; the three it declines are named, +with the reason, at the top of the file they would have been written into. Names are Mojang's own. Since 26.1 the server ships unobfuscated, so every symbol here greps directly against the server jar. ## State -Implemented: handshake, status, login, configuration, and the play-state join -sequence (`packets::play_login`). The rest of play is not started. Compression -and encryption are not implemented. +Every state has generated packets. Alongside them, hand-written codecs for the +54 packets whose encoder branches on a runtime value -- `ClientInformation`, +`CustomPayload` and `UpdateTags` among them -- plus NBT, text components and +item stacks. Not started: chunk data, entity metadata, compression, encryption. + +Eleven packets are currently defined twice, once hand-written and once +generated. `src/packets/mod.rs` says which, and why the reconciliation is a +separate change. ## Regenerating + ```sh nix run .#update-minecraft-data # re-resolve Mojang's manifest, rewrite the pin -nix run .#sync-minecraft-proto # regenerate src/generated/ +nix run .#sync-minecraft-proto # rewrite protocol.json and src/generated/ ``` ## Testing against a real server diff --git a/crates/hyperion-minecraft-proto/build.rs b/crates/hyperion-minecraft-proto/build.rs new file mode 100644 index 000000000..2441d438c --- /dev/null +++ b/crates/hyperion-minecraft-proto/build.rs @@ -0,0 +1,1279 @@ +//! Packet structs, generated from the committed `protocol.json`. +//! +//! `protocol.json` is what `nix/extract-protocol.py` recovered by following +//! the server's own stream codecs down to the bytes. This turns the layouts it +//! recovered *in full* into Rust; a layout with an unresolved leaf anywhere +//! under it produces no struct at all, because a partial codec is worse than a +//! missing one -- it looks right and desynchronises the stream. +//! +//! Everything here is deterministic and ordered, so a rebuild against an +//! unchanged `protocol.json` produces byte-identical output. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Write as _, + fs, + path::{Path, PathBuf}, + process::Command, +}; + +use serde_json::Value; + +/// Connection states, in the order a connection passes through them. +const STATES: [&str; 5] = ["handshake", "status", "login", "configuration", "play"]; + +/// Where a packet class used by more than one state is defined. +/// +/// `ClientboundKeepAlivePacket` is one class serving configuration and play. +/// Emitting it once and re-exporting keeps it one Rust type too, so a value +/// built for one state is accepted by the other. +const COMMON: &str = "common"; + +const DIRECTIONS: [&str; 2] = ["clientbound", "serverbound"]; + +fn main() { + let manifest = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir")); + let source = manifest.join("protocol.json"); + println!("cargo::rerun-if-changed={}", source.display()); + println!("cargo::rerun-if-changed=build.rs"); + + let text = fs::read_to_string(&source) + .unwrap_or_else(|error| panic!("reading {}: {error}", source.display())); + let doc: Value = serde_json::from_str(&text) + .unwrap_or_else(|error| panic!("parsing {}: {error}", source.display())); + + let out = PathBuf::from(std::env::var_os("OUT_DIR").expect("out dir")); + let generated = Generator::new(&doc).run(); + + write(&out.join("types.rs"), &generated.types); + for (name, body) in &generated.packets { + write(&out.join(format!("packets/{name}.rs")), body); + } + + // A build script cannot return a value, so the count is written where a + // test can read it back and hold the generator to it. + fs::write( + out.join("coverage.txt"), + format!("{} {}\n", generated.written, generated.skipped), + ) + .expect("write coverage"); +} + +/// Write a file, formatting it when rustfmt is available. +/// +/// The output is spliced into the crate with `include!`, so it shows up in +/// compiler diagnostics and in rustdoc. Formatting it is the difference +/// between those being readable and not; it is best-effort because a build +/// must not fail on a machine without rustfmt. +fn write(path: &Path, body: &str) { + fs::create_dir_all(path.parent().expect("parent")).expect("create out dir"); + fs::write(path, body).unwrap_or_else(|error| panic!("writing {}: {error}", path.display())); + let _unused = Command::new("rustfmt") + .args(["--edition", "2024", "--emit", "files"]) + .arg(path) + .status(); +} + +// --------------------------------------------------------------------------- +// Rust identifiers +// --------------------------------------------------------------------------- + +/// Words that cannot be an identifier, and so are written as `r#word`. +const KEYWORDS: [&str; 51] = [ + "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate", + "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in", + "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref", + "return", "static", "struct", "super", "trait", "true", "try", "type", "typeof", "unsafe", + "unsized", "use", "virtual", "where", "while", "yield", "gen", "self", +]; + +/// Split a Java identifier into lower-case words. +/// +/// `newCenterX` is three words and `blockPosXYZ` is two, because a run of +/// capitals is one word up to the capital that starts the next. +fn words(name: &str) -> Vec { + let chars: Vec = name.chars().collect(); + let mut out: Vec = Vec::new(); + let mut current = String::new(); + for (index, &ch) in chars.iter().enumerate() { + if ch == '_' || ch == '/' || ch == '.' { + if !current.is_empty() { + out.push(std::mem::take(&mut current)); + } + continue; + } + let starts_word = ch.is_ascii_uppercase() + && !current.is_empty() + && (!chars[index - 1].is_ascii_uppercase() + || chars.get(index + 1).is_some_and(char::is_ascii_lowercase)); + if starts_word { + out.push(std::mem::take(&mut current)); + } + current.push(ch.to_ascii_lowercase()); + } + if !current.is_empty() { + out.push(current); + } + out +} + +/// `minecraft:add_entity` and `ClientboundAddEntityPacket` both give `AddEntity`. +fn pascal(name: &str) -> String { + words(name.rsplit(':').next().unwrap_or(name)) + .into_iter() + .map(|word| { + let mut chars = word.chars(); + chars.next().map_or_else(String::new, |first| { + first.to_ascii_uppercase().to_string() + chars.as_str() + }) + }) + .collect() +} + +/// `protocolVersion` gives `protocol_version`, escaped if it is a keyword. +fn snake(name: &str) -> String { + let joined = words(name.rsplit(':').next().unwrap_or(name)).join("_"); + if KEYWORDS.contains(&joined.as_str()) { + format!("r#{joined}") + } else if joined.starts_with(|c: char| c.is_ascii_digit()) { + // A leading digit cannot start an identifier and no escape helps. + format!("field_{joined}") + } else { + joined + } +} + +/// `properties` gives `property`, so that a list's element type is named for +/// one element rather than for the list. +fn singular(name: &str) -> String { + match name.strip_suffix("ies") { + Some(stem) => format!("{stem}y"), + None if name.ends_with("ss") || !name.ends_with('s') => name.to_owned(), + None => name[..name.len() - 1].to_owned(), + } +} + +/// The simple name of a Java class, keeping only the innermost nested part. +fn simple_name(owner: &str) -> &str { + owner + .rsplit('$') + .next() + .and_then(|inner| inner.rsplit('.').next()) + .unwrap_or(owner) +} + +/// The class a nested class is declared in, if it is nested at all. +fn outer_name(owner: &str) -> Option<&str> { + owner.rsplit_once('$').map(|(outer, _)| outer) +} +// --------------------------------------------------------------------------- +// Output tree +// --------------------------------------------------------------------------- + +/// One module's worth of generated items. +/// +/// Imports are one path per line and left for rustfmt to merge, so the output +/// is correct without rustfmt and tidy with it. +#[derive(Default)] +struct Module { + imports: BTreeSet, + reexports: BTreeSet, + /// Items keyed by type name, which is also the order they are written in. + items: BTreeMap, + children: BTreeMap, +} + +impl Module { + fn child(&mut self, name: &str, doc: &str) -> &mut Self { + let entry = self + .children + .entry(name.to_owned()) + .or_insert_with(|| (doc.to_owned(), Self::default())); + if entry.0.is_empty() { + doc.clone_into(&mut entry.0); + } + &mut entry.1 + } + + fn render(&self, indent: usize, out: &mut String) { + let pad = " ".repeat(indent); + // Imports are recorded while lowering, before it is known whether the + // packet that wanted one survives; the ones nothing ended up naming + // are dropped here rather than being guessed at up front. + let mut body = String::new(); + self.render_items(indent, &mut body); + let used: Vec<&String> = self + .imports + .iter() + .filter(|path| mentions(&body, path.rsplit("::").next().unwrap_or(path))) + .collect(); + for path in used { + let _unused = writeln!(out, "{pad}use {path};"); + } + for path in &self.reexports { + let _unused = writeln!(out, "{pad}pub use {path};"); + } + if !out.is_empty() { + out.push('\n'); + } + out.push_str(&body); + while out.ends_with("\n\n") { + out.pop(); + } + } + + fn render_items(&self, indent: usize, out: &mut String) { + let pad = " ".repeat(indent); + for body in self.items.values() { + for line in body.lines() { + if line.is_empty() { + out.push('\n'); + } else { + let _unused = writeln!(out, "{pad}{line}"); + } + } + out.push('\n'); + } + for (name, (doc, child)) in &self.children { + let _unused = writeln!(out, "{pad}/// {doc}"); + let _unused = writeln!(out, "{pad}pub mod {name} {{"); + let mut inner = String::new(); + child.render(indent + 1, &mut inner); + out.push_str(&inner); + let _unused = writeln!(out, "{pad}}}"); + out.push('\n'); + } + while out.ends_with("\n\n") { + out.pop(); + } + } +} + +/// The derives a type's fields permit, always in the same order. +fn derive_list(traits: Traits) -> Vec<&'static str> { + let mut derives = vec!["Debug", "Clone"]; + if traits.copy { + derives.push("Copy"); + } + derives.push("PartialEq"); + if traits.eq { + derives.push("Eq"); + derives.push("Hash"); + } + derives.push("Encode"); + derives.push("Decode"); + derives +} + +/// Whether a name appears in the text as a whole word. +fn mentions(text: &str, name: &str) -> bool { + let boundary = |ch: Option| ch.is_none_or(|ch| !ch.is_alphanumeric() && ch != '_'); + text.match_indices(name).any(|(index, _)| { + boundary(text[..index].chars().next_back()) + && boundary(text[index + name.len()..].chars().next()) + }) +} + +/// Where a generated type lives: which file, and which submodule of it. +#[derive(Clone, PartialEq, Eq, Debug)] +struct Place { + file: String, + module: Option, +} + +impl Place { + const fn root(file: String) -> Self { + Self { file, module: None } + } + + /// How code sitting in `from` names an item defined here, and the import + /// that makes that spelling work. + fn path_from(&self, from: &Self, name: &str) -> (String, Option) { + if self.file != from.file { + // Only the shared type file is referenced across files. + return self.module.as_ref().map_or_else( + || (name.to_owned(), Some(format!("crate::types::{name}"))), + |module| { + ( + format!("{module}::{name}"), + Some(format!("crate::types::{module}")), + ) + }, + ); + } + match (&from.module, &self.module) { + (a, b) if a == b => (name.to_owned(), None), + (_, None) => (format!("super::{name}"), None), + (None, Some(module)) => (format!("{module}::{name}"), None), + (Some(_), Some(module)) => (format!("super::{module}::{name}"), None), + } + } +} + +/// Which derives a type's fields permit. +#[derive(Clone, Copy)] +struct Traits { + /// Whether the type borrows from the buffer, and so needs a lifetime. + borrows: bool, + copy: bool, + /// `Eq`, and with it `Hash`: false as soon as a float is anywhere inside. + eq: bool, +} + +/// A lowered field or element type. +#[derive(Clone)] +struct Lowered { + ty: String, + traits: Traits, + limits: Limits, + doc: Option, + /// A module supplying the whole field's codec, where the derive's + /// structural rules do not describe the layout. + with: Option<&'static str>, +} + +impl Lowered { + fn scalar(ty: &str, traits: Traits) -> Self { + Self { + ty: ty.to_owned(), + traits, + limits: Limits::default(), + doc: None, + with: None, + } + } + + /// Put this type inside a combinator, keeping what still applies. + fn wrap(self, ty: String, copy: bool) -> Result { + if self.with.is_some() { + // The derive applies a custom codec to a whole field, so it cannot + // reach inside a list or an `Either`. + return Err( + "a custom codec inside a combinator needs a hand-written packet".to_owned(), + ); + } + Ok(Self { + ty, + traits: Traits { + copy: self.traits.copy && copy, + ..self.traits + }, + doc: None, + ..self + }) + } +} + +/// The `#[proto(...)]` limits a field's subtree asks for. +/// +/// The derive threads one string limit and one collection limit through +/// `Option` and `Vec`, so two of either kind in one field cannot be expressed. +/// Rather than drop one, the field is refused and the packet goes unwritten. +#[derive(Clone, Copy, Default, PartialEq, Eq)] +struct Limits { + len: Option, + count: Option, +} + +impl Limits { + fn merge(self, other: Self) -> Result { + Ok(Self { + len: pick(self.len, other.len, "max_len")?, + count: pick(self.count, other.count, "max_count")?, + }) + } + + fn render(self) -> Option { + let mut parts = Vec::new(); + if let Some(len) = self.len { + parts.push(format!("max_len = {len}")); + } + if let Some(count) = self.count { + parts.push(format!("max_count = {count}")); + } + (!parts.is_empty()).then(|| format!("#[proto({})]", parts.join(", "))) + } +} + +fn pick(a: Option, b: Option, what: &str) -> Result, String> { + match (a, b) { + (Some(a), Some(b)) if a != b => Err(format!("two different `{what}` limits in one field")), + (Some(value), _) | (None, Some(value)) => Ok(Some(value)), + (None, None) => Ok(None), + } +} + +// --------------------------------------------------------------------------- +// Generator +// --------------------------------------------------------------------------- + +/// A packet as the generator needs it: one Java class, and every state that +/// sends it. +#[derive(Clone)] +struct PacketDef { + java_class: String, + codec: String, + resource: String, + name: String, + module: String, + direction: String, + wire: Value, + /// `(state, protocol id)` for every channel this class serves, in the + /// order a connection passes through the states. + channels: Vec<(String, i64)>, +} + +impl PacketDef { + /// What the struct is called on the wire, where it is sent, and the class + /// to read if the layout ever needs checking. + fn doc(&self) -> String { + let channels: Vec = self + .channels + .iter() + .map(|(state, id)| format!("{state} id {id}")) + .collect(); + format!( + "`{}`, sent {} as {}.\n\nLayout from `{}`.", + self.resource, + self.direction, + channels.join(" and "), + self.codec, + ) + } + + fn file(&self) -> String { + format!("{}_{}", self.module, self.direction) + } + + /// The module holding the types this packet nests. + fn scope(&self) -> String { + snake(&self.resource) + } +} + +/// A generated type that has been placed, so a second reference to it is a +/// path rather than a second definition. +#[derive(Clone)] +struct Placed { + place: Place, + name: String, + traits: Traits, +} + +struct Generated { + types: String, + packets: BTreeMap, + written: usize, + skipped: usize, +} + +struct Generator<'a> { + types: &'a serde_json::Map, + packets: Vec, + /// Java classes that are packets, which is what decides whether a nested + /// class belongs beside a packet or in `crate::types`. + packet_classes: BTreeSet, + files: BTreeMap, + placed: BTreeMap, + /// Named references currently being lowered, which is how a cycle is + /// caught before it becomes an infinitely sized Rust type. + active: BTreeSet, + skipped: BTreeMap>, +} + +impl<'a> Generator<'a> { + fn new(doc: &'a Value) -> Self { + let packets = collect_packets(doc); + Self { + types: doc["types"].as_object().expect("types table"), + packet_classes: packets + .iter() + .map(|packet| packet.java_class.clone()) + .collect(), + packets, + files: BTreeMap::new(), + placed: BTreeMap::new(), + active: BTreeSet::new(), + skipped: BTreeMap::new(), + } + } + + fn run(mut self) -> Generated { + // Every module is written even when it is empty, so that the file each + // shim includes always exists and a state losing its last generated + // packet is a diff rather than a build failure. + self.files.entry("types".to_owned()).or_default(); + for home in STATES.into_iter().chain([COMMON]) { + for direction in DIRECTIONS { + self.files.entry(format!("{home}_{direction}")).or_default(); + } + } + for index in 0..self.packets.len() { + self.emit_packet(index); + } + self.reexport_common(); + self.render() + } + + fn emit_packet(&mut self, index: usize) { + let packet = self.packets[index].clone(); + if self.placed.contains_key(&packet.codec) { + return; + } + let place = Place::root(packet.file()); + // Reserve the name before lowering, so a layout that turns out to + // reference this packet finds a path rather than defining it twice. + if !self.active.insert(packet.codec.clone()) { + return; + } + let result = self.struct_body( + &packet.name, + Some(&packet.doc()), + &packet.wire, + &place, + &packet.scope(), + ); + self.active.remove(&packet.codec); + + match result { + Ok((body, traits)) => { + let module = self.file(&place); + module.imports.insert("crate::Decode".to_owned()); + module.imports.insert("crate::Encode".to_owned()); + module.items.insert(packet.name.clone(), body); + self.placed.insert(packet.codec.clone(), Placed { + place, + name: packet.name.clone(), + traits, + }); + } + Err(reason) => self + .skipped + .entry(packet.file()) + .or_default() + .push(format!("{} -- {reason}", packet.resource)), + } + } + + /// Bring the packets that several states share into each of those states. + /// + /// `ClientboundKeepAlivePacket` is one Java class serving configuration + /// and play. Defining it once and re-exporting keeps it one Rust type, so + /// a value built for one state is the same value in the other. + fn reexport_common(&mut self) { + let shared: Vec = self + .packets + .iter() + .filter(|packet| packet.module == COMMON && self.placed.contains_key(&packet.codec)) + .cloned() + .collect(); + for packet in shared { + let has_scope = self + .files + .get(&packet.file()) + .is_some_and(|file| file.children.contains_key(&packet.scope())); + for (state, _) in &packet.channels { + let target = Place::root(format!("{state}_{}", packet.direction)); + let base = format!("crate::packets::common::{}", packet.direction); + let module = self.file(&target); + module.reexports.insert(format!("{base}::{}", packet.name)); + if has_scope { + module + .reexports + .insert(format!("{base}::{}", packet.scope())); + } + } + } + } + + fn render(&self) -> Generated { + let mut out = Generated { + types: String::new(), + packets: BTreeMap::new(), + written: self + .packets + .iter() + .filter(|packet| self.placed.contains_key(&packet.codec)) + .count(), + skipped: self.skipped.values().map(Vec::len).sum(), + }; + for (file, module) in &self.files { + let mut body = String::from(HEADER); + if let Some(reasons) = self.skipped.get(file) { + body.push_str(REFUSED); + for reason in reasons { + let _unused = writeln!(body, "// {reason}"); + } + } + body.push('\n'); + module.render(0, &mut body); + if file == "types" { + out.types = body; + } else { + out.packets.insert(file.clone(), body); + } + } + out + } +} + +/// Complete packets, one entry per Java class. +fn collect_packets(doc: &Value) -> Vec { + let mut by_class: BTreeMap = BTreeMap::new(); + for entry in doc["packets"].as_array().expect("packets") { + if !entry["complete"].as_bool().unwrap_or(false) { + continue; + } + let state = entry["state"].as_str().expect("state").to_owned(); + let id = entry["protocolId"].as_i64().expect("protocol id"); + let java_class = entry["javaClass"].as_str().unwrap_or_default().to_owned(); + by_class + .entry(java_class.clone()) + .or_insert_with(|| PacketDef { + java_class, + codec: entry["layoutSource"] + .as_str() + .unwrap_or_default() + .to_owned(), + resource: entry["resource"].as_str().expect("resource").to_owned(), + name: pascal(entry["resource"].as_str().expect("resource")), + module: state.clone(), + direction: entry["direction"].as_str().expect("direction").to_owned(), + wire: entry["wire"].clone(), + channels: Vec::new(), + }) + .channels + .push((state, id)); + } + + let mut packets: Vec = by_class.into_values().collect(); + for packet in &mut packets { + packet + .channels + .sort_by_key(|(state, _)| STATES.iter().position(|name| name == state)); + if packet.channels.len() > 1 { + COMMON.clone_into(&mut packet.module); + } + } + packets +} + +const HEADER: &str = "// @generated by build.rs from protocol.json. Do not edit.\n"; + +const REFUSED: &str = "//\n// Layouts the extractor recovered in full but this generator \ + declined, and\n// why. Each needs a hand-written codec; none of them is \ + approximated here.\n"; + +// --------------------------------------------------------------------------- +// Lowering a wire tree to Rust +// --------------------------------------------------------------------------- + +/// A scalar wire kind and its Rust spelling, with the derives it permits. +/// +/// Anything not listed has no Rust spelling yet, and a packet that reaches one +/// goes unwritten rather than approximated. +const SCALARS: [(&str, &str, bool, bool, bool); 21] = [ + // kind, rust type, borrows, copy, eq + ("unit", "()", false, true, true), + ("bool", "bool", false, true, true), + ("i8", "i8", false, true, true), + ("u8", "u8", false, true, true), + ("i16", "i16", false, true, true), + ("u16", "u16", false, true, true), + ("i32", "i32", false, true, true), + ("i64", "i64", false, true, true), + ("f32", "f32", false, true, false), + ("f64", "f64", false, true, false), + ("varint", "VarInt", false, true, true), + ("varlong", "VarLong", false, true, true), + ("uuid", "Uuid", false, true, true), + ("block_pos", "BlockPos", false, true, true), + ("chunk_pos", "ChunkPos", false, true, true), + ("block_hit_result", "BlockHitResult", false, true, false), + ("identifier", "Identifier<'a>", true, true, true), + ("string", "&'a str", true, true, true), + ("byte_array", "&'a [u8]", true, true, true), + ("long_array", "Vec", false, false, true), + ("varint_array", "Vec", false, false, true), +]; + +/// Types the generated code may name, all re-exported at the crate root so +/// that one import works however deeply a module nests. +const ROOT_TYPES: [&str; 12] = [ + "BlockHitResult", + "BlockPos", + "ChunkPos", + "Either", + "Holder", + "Identifier", + "LengthPrefixed", + "RegistryId", + "Uuid", + "VarInt", + "VarLong", + "nbt", +]; + +impl Generator<'_> { + /// The module at a place, creating it if this is the first item in it. + fn file(&mut self, at: &Place) -> &mut Module { + let file = self.files.entry(at.file.clone()).or_default(); + match &at.module { + Some(module) => file.child(module, ""), + None => file, + } + } + + /// Follow `named` references to the node that actually describes bytes. + fn resolve<'v>(&'v self, wire: &'v Value) -> &'v Value { + let mut wire = wire; + let mut seen = BTreeSet::new(); + while wire["kind"] == "named" { + let Some(reference) = wire["ref"].as_str() else { + break; + }; + if !seen.insert(reference.to_owned()) { + break; + } + let Some(next) = self.types.get(reference) else { + break; + }; + wire = next; + } + wire + } + + /// The Rust type for one wire node, defining any struct it needs on the way. + /// + /// `at` is the module the type will be named from and `scope` the + /// submodule holding the types this packet or class nests. `label` is the + /// field the node hangs off, which is what an anonymous struct is named + /// after. + fn lower( + &mut self, + wire: &Value, + at: &Place, + scope: &str, + label: &str, + ) -> Result { + if wire["kind"] == "named" + && let Some(reference) = wire["ref"].as_str() + { + return self.lower_named(reference, at, scope, label); + } + + let kind = wire["kind"].as_str().unwrap_or("unresolved"); + if let Some(&(_, ty, borrows, copy, eq)) = SCALARS.iter().find(|entry| entry.0 == kind) { + let mut lowered = Lowered::scalar(ty, Traits { borrows, copy, eq }); + // A string's limit is UTF-16 code units and a byte array's is + // bytes; `max_len` means "the length this type is measured in". + if matches!(kind, "string" | "byte_array") { + lowered.limits.len = wire["max"].as_u64(); + } + self.note_import(at, ty); + return Ok(lowered); + } + + match kind { + "registry_id" => { + self.note_import(at, "RegistryId"); + Ok(Lowered { + doc: wire["registry"] + .as_str() + .map(|registry| format!("Index into `{registry}`.")), + ..Lowered::scalar("RegistryId", Traits { + borrows: false, + copy: true, + eq: true, + }) + }) + } + "nbt" => { + self.note_import(at, "nbt"); + Ok(Lowered::scalar("nbt::Tag<'a>", Traits { + borrows: true, + copy: false, + eq: false, + })) + } + // A root `TAG_End` means absent here, which is not the + // boolean-prefixed `Option` the derive writes by default. + "optional_nbt" => { + self.note_import(at, "nbt"); + Ok(Lowered { + with: Some("crate::nbt::optional"), + ..Lowered::scalar("Option>", Traits { + borrows: true, + copy: false, + eq: false, + }) + }) + } + "option" => { + let inner = self.lower(&wire["of"], at, scope, label)?; + let ty = format!("Option<{}>", inner.ty); + inner.wrap(ty, true) + } + "list" => { + let inner = self.lower(&wire["of"], at, scope, &singular(label))?; + let ty = format!("Vec<{}>", inner.ty); + let count = wire["max"].as_u64(); + let mut out = inner.wrap(ty, false)?; + out.limits = out.limits.merge(Limits { len: None, count })?; + Ok(out) + } + "length_prefixed" => { + let inner = self.lower(&wire["of"], at, scope, label)?; + self.note_import(at, "LengthPrefixed"); + let ty = format!("LengthPrefixed<{}>", inner.ty); + inner.wrap(ty, true) + } + "holder" => { + let inner = self.lower(&wire["of"], at, scope, label)?; + self.note_import(at, "Holder"); + let ty = format!("Holder<{}>", inner.ty); + let doc = wire["registry"] + .as_str() + .map(|registry| format!("A `{registry}` entry, by id or written out in full.")); + Ok(Lowered { + doc, + ..inner.wrap(ty, true)? + }) + } + "either" => { + let left = self.lower(&wire["left"], at, scope, label)?; + let right = self.lower(&wire["right"], at, scope, label)?; + self.note_import(at, "Either"); + let ty = format!("Either<{}, {}>", left.ty, right.ty); + let limits = left.limits.merge(right.limits)?; + let traits = Traits { + borrows: left.traits.borrows || right.traits.borrows, + copy: left.traits.copy && right.traits.copy, + eq: left.traits.eq && right.traits.eq, + }; + let merged = Lowered { + limits, + ..Lowered::scalar(&ty, traits) + }; + if left.with.is_some() || right.with.is_some() { + return Err( + "a custom codec inside `Either` needs a hand-written packet".to_owned() + ); + } + Ok(merged) + } + "enum" => self.lower_enum(wire, at, scope), + "map" => self.lower_map(wire, at, scope, label), + // An anonymous struct: no Java class named it, so it takes the + // name of the field it hangs off and sits in the packet's scope. + "struct" => { + let name = pascal(label); + let place = Place { + file: at.file.clone(), + module: Some(scope.to_owned()), + }; + self.define(&name, None, wire, at, &place, scope, None) + } + "unresolved" => Err(wire["why"] + .as_str() + .unwrap_or("unresolved layout") + .to_owned()), + other => Err(format!("no Rust type for wire kind `{other}`")), + } + } + + /// A Java enum, which the server sends as a `VarInt` and which is worth + /// keeping as an enum: a field typed this way cannot hold a number the + /// server would reject. + fn lower_enum(&mut self, wire: &Value, at: &Place, scope: &str) -> Result { + let owner = wire["type"].as_str().ok_or("enum without a class")?; + if let Some(placed) = self.placed.get(owner).cloned() { + let ty = self.reference(&placed, at); + return Ok(Lowered::scalar(&ty, placed.traits)); + } + + let name = pascal(simple_name(owner)); + let (place, _) = self.place_for(owner, at, scope, &name); + let sent = wire["note"].as_str().unwrap_or("varint"); + let mut body = format!("/// `{owner}`, sent as a {sent}.\n"); + body.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]\n"); + let _unused = writeln!(body, "pub enum {name} {{"); + for constant in wire["constants"] + .as_array() + .ok_or("enum without constants")? + { + let java = constant["name"].as_str().ok_or("constant without a name")?; + let value = constant["value"] + .as_i64() + .ok_or("constant without a value")?; + let _unused = writeln!(body, " /// `{java}`"); + let _unused = writeln!(body, " {} = {value},", pascal(java)); + } + body.push_str("}\n"); + + self.describe_scope(&place); + let module = self.file(&place); + module.imports.insert("crate::Decode".to_owned()); + module.imports.insert("crate::Encode".to_owned()); + module.items.insert(name.clone(), body); + + let placed = Placed { + place, + name, + traits: Traits { + borrows: false, + copy: true, + eq: true, + }, + }; + self.placed.insert(owner.to_owned(), placed.clone()); + Ok(Lowered::scalar(&self.reference(&placed, at), placed.traits)) + } + + /// A map is a count and then pairs. As a list of named entries it reads + /// better than a list of tuples, and each half can carry its own limit. + fn lower_map( + &mut self, + wire: &Value, + at: &Place, + scope: &str, + label: &str, + ) -> Result { + let name = format!("{}Entry", pascal(label)); + let shape = serde_json::json!({ + "kind": "struct", + "fields": [ + { "name": "key", "wire": wire["key"] }, + { "name": "value", "wire": wire["value"] }, + ], + }); + let place = Place { + file: at.file.clone(), + module: Some(scope.to_owned()), + }; + let entry = self.define(&name, None, &shape, at, &place, scope, None)?; + let ty = format!("Vec<{}>", entry.ty); + let count = wire["max"].as_u64(); + let mut out = entry.wrap(ty, false)?; + out.limits = out.limits.merge(Limits { len: None, count })?; + Ok(out) + } + + /// A reference to a named codec: a struct becomes a Rust type, anything + /// else is simply the layout it stands for. + fn lower_named( + &mut self, + reference: &str, + at: &Place, + scope: &str, + label: &str, + ) -> Result { + let owner = reference.split('#').next().unwrap_or(reference); + // A packet's own codec: emit that packet where it belongs rather than + // a second copy of it here. + if self.packet_classes.contains(owner) + && !self.placed.contains_key(reference) + && let Some(index) = self + .packets + .iter() + .position(|packet| packet.codec == reference) + { + self.emit_packet(index); + } + + if let Some(placed) = self.placed.get(reference).cloned() { + let ty = self.reference(&placed, at); + return Ok(Lowered::scalar(&ty, placed.traits)); + } + + let Some(target) = self.types.get(reference).cloned() else { + return Err(format!("no layout for `{reference}`")); + }; + if target["kind"] != "struct" { + // A named scalar such as `ByteBufCodecs#VAR_INT` names a layout, + // not a type, so it lowers to that layout under the field's own + // name: the codec holder's class name says nothing useful. + return self.lower(&target, at, scope, label); + } + if !self.active.insert(reference.to_owned()) { + return Err(format!("`{reference}` contains itself")); + } + let result = self.define_named(reference, &target, at, scope); + self.active.remove(reference); + result + } + + /// Record that a file names one of the crate's own types. + fn note_import(&mut self, at: &Place, ty: &str) { + let head = ty + .trim_start_matches("&'a ") + .split(['<', ':']) + .next() + .unwrap_or(ty); + if ROOT_TYPES.contains(&head) { + self.file(at).imports.insert(format!("crate::{head}")); + } + } +} + +// --------------------------------------------------------------------------- +// Defining and rendering a struct +// --------------------------------------------------------------------------- + +impl Generator<'_> { + /// Define a named codec's struct and return how `at` refers to it. + fn define_named( + &mut self, + reference: &str, + target: &Value, + at: &Place, + scope: &str, + ) -> Result { + let owner = reference.split('#').next().unwrap_or(reference); + let member = reference.rsplit('#').next().unwrap_or("STREAM_CODEC"); + // A member ending in `_CODEC` describes a variant of the class it sits + // in -- `AttributeSnapshot#MODIFIER_STREAM_CODEC` is a modifier, not a + // second snapshot -- while one that does not is the type's own name, + // as with the codecs collected in `ByteBufCodecs`. + let qualifier = member + .trim_end_matches("_STREAM_CODEC") + .trim_end_matches("_CODEC"); + let name = if member == "STREAM_CODEC" { + pascal(simple_name(owner)) + } else if qualifier == member { + pascal(member) + } else { + format!("{}{}", pascal(simple_name(owner)), pascal(qualifier)) + }; + + let (place, child_scope) = self.place_for(owner, at, scope, &name); + let doc = format!("Layout from `{reference}`."); + self.define( + &name, + Some(&doc), + target, + at, + &place, + &child_scope, + Some(reference), + ) + } + + /// Where a named class's Rust type goes. + /// + /// A class nested in a packet belongs beside that packet; anything else is + /// a domain type shared across packets and belongs in `crate::types`. In + /// both cases a nested class keeps Java's own scoping, as a module named + /// after the class it is declared in. + fn place_for(&self, owner: &str, at: &Place, scope: &str, name: &str) -> (Place, String) { + match outer_name(owner) { + Some(outer) if self.packet_classes.contains(outer) => ( + Place { + file: at.file.clone(), + module: Some(scope.to_owned()), + }, + scope.to_owned(), + ), + Some(outer) => { + let module = snake(simple_name(outer)); + ( + Place { + file: "types".to_owned(), + module: Some(module.clone()), + }, + module, + ) + } + None => (Place::root("types".to_owned()), snake(name)), + } + } + + /// Render a struct into `place` and return how `at` refers to it. + #[expect( + clippy::too_many_arguments, + reason = "where a type goes, where it is named from, and where its own nested types go \ + are three independent facts" + )] + fn define( + &mut self, + name: &str, + doc: Option<&str>, + wire: &Value, + at: &Place, + place: &Place, + scope: &str, + reference: Option<&str>, + ) -> Result { + let (body, traits) = self.struct_body(name, doc, wire, place, scope)?; + self.describe_scope(place); + let module = self.file(place); + module.imports.insert("crate::Decode".to_owned()); + module.imports.insert("crate::Encode".to_owned()); + module.items.insert(name.to_owned(), body); + + let placed = Placed { + place: place.clone(), + name: name.to_owned(), + traits, + }; + if let Some(reference) = reference { + self.placed.insert(reference.to_owned(), placed.clone()); + } + Ok(Lowered::scalar(&self.reference(&placed, at), traits)) + } + + /// How code in `at` spells a type that has already been placed. + /// + /// A short name is imported unless the referring module already defines + /// that name -- `minecraft:recipe_book_settings` is a packet and + /// `net.minecraft.stats.RecipeBookSettings` is a domain type, and one of + /// them has to be spelled out. + fn reference(&mut self, placed: &Placed, at: &Place) -> String { + let (short, import) = placed.place.path_from(at, &placed.name); + let ty = match import { + // Only the shared type file is referenced across files, so the + // spelled-out form is always rooted there. + Some(_) if self.occupied(at, &placed.name) => format!("crate::types::{short}"), + Some(import) => { + self.file(at).imports.insert(import); + short + } + None => short, + }; + if placed.traits.borrows { + format!("{ty}<'a>") + } else { + ty + } + } + + /// Whether a file's root module already defines this type name. + fn occupied(&self, at: &Place, name: &str) -> bool { + self.packets + .iter() + .any(|packet| packet.file() == at.file && packet.name == name) + } + + /// Give a submodule its doc comment, once it has an occupant. + /// + /// A scope is always named after the type whose nested types it holds, so + /// the module name is enough to say whose they are. + fn describe_scope(&mut self, place: &Place) { + let Some(module) = place.module.clone() else { + return; + }; + let doc = format!("Types declared inside `{}`.", pascal(&module)); + self.files + .entry(place.file.clone()) + .or_default() + .child(&module, &doc); + } + + /// The text of one struct, and the derives its fields permit. + fn struct_body( + &mut self, + name: &str, + doc: Option<&str>, + wire: &Value, + place: &Place, + scope: &str, + ) -> Result<(String, Traits), String> { + let resolved = self.resolve(wire).clone(); + let kind = resolved["kind"].as_str().unwrap_or("unresolved"); + + let mut out = String::new(); + for line in doc.unwrap_or_default().lines() { + let _unused = if line.is_empty() { + writeln!(out, "///") + } else { + writeln!(out, "/// {line}") + }; + } + + if kind == "unit" { + out.push_str( + "#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Encode, Decode)]\n", + ); + let _unused = writeln!(out, "pub struct {name};"); + return Ok((out, Traits { + borrows: false, + copy: true, + eq: true, + })); + } + if kind != "struct" { + // One value with nothing around it: the server's codec for + // `keep_alive` is `ByteBufCodecs.LONG`, not a composite, and a + // newtype says that more honestly than a one-field struct would. + let lowered = self.lower(&resolved, place, scope, &snake(name))?; + let lifetime = if lowered.traits.borrows { "<'a>" } else { "" }; + if let Some(doc) = &lowered.doc { + let _unused = writeln!(out, "///\n/// {doc}"); + } + let _unused = writeln!(out, "#[derive({})]", derive_list(lowered.traits).join(", ")); + let attr = lowered + .with + .map(|with| format!("#[proto(with = {with})] ")) + .or_else(|| lowered.limits.render().map(|attr| format!("{attr} "))) + .unwrap_or_default(); + let _unused = writeln!( + out, + "pub struct {name}{lifetime}({attr}pub {});", + lowered.ty + ); + return Ok((out, lowered.traits)); + } + + let empty = Vec::new(); + let mut fields = Vec::new(); + let mut traits = Traits { + borrows: false, + copy: true, + eq: true, + }; + let mut seen: BTreeSet = BTreeSet::new(); + for field in resolved["fields"].as_array().unwrap_or(&empty) { + let label = field["name"].as_str().unwrap_or("value"); + let rust = snake(label); + if !seen.insert(rust.clone()) { + return Err(format!("two fields would both be named `{rust}`")); + } + let lowered = self.lower(&field["wire"], place, scope, label)?; + traits.borrows |= lowered.traits.borrows; + traits.copy &= lowered.traits.copy; + traits.eq &= lowered.traits.eq; + fields.push((rust, lowered)); + } + + let _unused = writeln!(out, "#[derive({})]", derive_list(traits).join(", ")); + // clippy suggests a state machine past three flags. Here the flags are + // the bytes, and grouping them would not change what is written. + if fields + .iter() + .filter(|(_, field)| field.ty == "bool") + .count() + > 3 + { + out.push_str( + "#[expect(\n clippy::struct_excessive_bools,\n reason = \"the server writes \ + this many independent flags\"\n)]\n", + ); + } + + let lifetime = if traits.borrows { "<'a>" } else { "" }; + let _unused = writeln!(out, "pub struct {name}{lifetime} {{"); + for (rust, lowered) in fields { + if let Some(doc) = &lowered.doc { + let _unused = writeln!(out, " /// {doc}"); + } + if let Some(with) = lowered.with { + let _unused = writeln!(out, " #[proto(with = {with})]"); + } else if let Some(attr) = lowered.limits.render() { + let _unused = writeln!(out, " {attr}"); + } + let _unused = writeln!(out, " pub {rust}: {},", lowered.ty); + } + out.push_str("}\n"); + Ok((out, traits)) + } +} diff --git a/crates/hyperion-minecraft-proto/protocol.json b/crates/hyperion-minecraft-proto/protocol.json new file mode 100644 index 000000000..a644a7798 --- /dev/null +++ b/crates/hyperion-minecraft-proto/protocol.json @@ -0,0 +1,20287 @@ +{ + "version": { + "id": "26.2", + "protocolVersion": 776, + "worldVersion": 4903, + "releaseTime": "2026-06-16T12:01:27+00:00" + }, + "coverage": { + "packets": 256, + "fullyMechanical": 202, + "partial": 54, + "unrecovered": 0, + "dataComponents": 111, + "dataComponentsMechanical": 100, + "namedTypes": 474, + "assertedTypes": [ + "net.minecraft.network.codec.ByteBufCodecs#GAME_PROFILE_PROPERTIES" + ], + "idCrossCheckMismatches": [], + "dataComponentProblems": [] + }, + "types": { + "net.minecraft.advancements.Advancement#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled statement: this.requirements.write(output)" + }, + "net.minecraft.advancements.AdvancementHolder#LIST_STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.advancements.AdvancementHolder#STREAM_CODEC" + } + }, + "net.minecraft.advancements.AdvancementHolder#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + } + }, + { + "name": "value", + "wire": { + "kind": "named", + "ref": "net.minecraft.advancements.Advancement#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.advancements.DisplayInfo#STREAM_CODEC": { + "kind": "unresolved", + "why": "branching encode body: if (this.background.isPresent()) {\n flags |= 1;\n " + }, + "net.minecraft.advancements.predicates.BlockPredicate#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "blocks", + "wire": { + "kind": "option", + "of": { + "kind": "holder_set", + "registry": "minecraft:block", + "note": "varint 0 then a tag identifier, otherwise count + 1 then that many holder ids" + } + } + }, + { + "name": "properties", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.StatePropertiesPredicate#STREAM_CODEC" + } + } + }, + { + "name": "nbt", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.NbtPredicate#STREAM_CODEC" + } + } + }, + { + "name": "components", + "wire": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.DataComponentMatchers#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.advancements.predicates.DataComponentMatchers#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "exact", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.component.DataComponentExactPredicate#STREAM_CODEC" + } + }, + { + "name": "partial", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.component.predicates.DataComponentPredicate#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.advancements.predicates.NbtPredicate#STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#COMPOUND_TAG" + }, + "net.minecraft.advancements.predicates.StatePropertiesPredicate#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.StatePropertiesPredicate$PropertyMatcher#STREAM_CODEC" + } + }, + "net.minecraft.advancements.predicates.StatePropertiesPredicate$ExactMatcher#STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + }, + "net.minecraft.advancements.predicates.StatePropertiesPredicate$PropertyMatcher#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "name", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + }, + { + "name": "valueMatcher", + "wire": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.StatePropertiesPredicate$ValueMatcher#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.advancements.predicates.StatePropertiesPredicate$RangedMatcher#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "minValue", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + } + }, + { + "name": "maxValue", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + } + } + ] + }, + "net.minecraft.advancements.predicates.StatePropertiesPredicate$ValueMatcher#STREAM_CODEC": { + "kind": "either", + "left": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.StatePropertiesPredicate$ExactMatcher#STREAM_CODEC" + }, + "right": { + "kind": "named", + "ref": "net.minecraft.advancements.predicates.StatePropertiesPredicate$RangedMatcher#STREAM_CODEC" + }, + "note": "bool discriminant, true selects left" + }, + "net.minecraft.core.BlockPos#STREAM_CODEC": { + "kind": "block_pos", + "note": "one i64, x/z/y packed 26/26/12" + }, + "net.minecraft.core.ClientAsset$ResourceTexture#STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + }, + "net.minecraft.core.GlobalPos#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "dimension", + "wire": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + } + }, + { + "name": "pos", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.BlockPos#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.core.RegistrySynchronization$PackedRegistryEntry#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + } + }, + { + "name": "data", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#TAG" + } + } + } + ] + }, + "net.minecraft.core.SectionPos#STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#LONG" + }, + "net.minecraft.core.UUIDUtil#STREAM_CODEC": { + "kind": "uuid", + "note": "two big-endian longs, most significant first" + }, + "net.minecraft.core.Vec3i#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "x", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "y", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "z", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + } + ] + }, + "net.minecraft.core.component.DataComponentExactPredicate#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.core.component.TypedDataComponent#STREAM_CODEC" + } + }, + "net.minecraft.core.component.DataComponentPatch#STREAM_CODEC": { + "kind": "unresolved", + "why": "branching encode body: if (patch.isEmpty()) {\n output.writeVarIn" + }, + "net.minecraft.core.component.DataComponentType#STREAM_CODEC": { + "kind": "registry_id", + "registry": "minecraft:data_component_type" + }, + "net.minecraft.core.component.TypedDataComponent#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled statement: 1.encodeCap(output, value)" + }, + "net.minecraft.core.component.predicates.DataComponentPredicate#SINGLE_STREAM_CODEC": { + "kind": "unresolved", + "why": "dispatched codec: the layout depends on a runtime type" + }, + "net.minecraft.core.component.predicates.DataComponentPredicate#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.core.component.predicates.DataComponentPredicate#SINGLE_STREAM_CODEC" + }, + "max": 64 + }, + "net.minecraft.core.component.predicates.DataComponentPredicate$Type#STREAM_CODEC": { + "kind": "either", + "left": { + "kind": "registry_id", + "registry": "minecraft:data_component_predicate_type" + }, + "right": { + "kind": "registry_id", + "registry": "minecraft:data_component_type" + }, + "note": "bool discriminant, true selects left" + }, + "net.minecraft.core.particles.ExplosionParticleInfo#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "particle", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.particles.ParticleTypes#STREAM_CODEC" + } + }, + { + "name": "scaling", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + }, + { + "name": "speed", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + } + ] + }, + "net.minecraft.core.particles.ParticleTypes#STREAM_CODEC": { + "kind": "unresolved", + "why": "dispatched codec: the layout depends on a runtime type" + }, + "net.minecraft.network.HashedPatchMap#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "addedComponents", + "wire": { + "kind": "map", + "key": { + "kind": "registry_id", + "registry": "minecraft:data_component_type" + }, + "value": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#INT" + }, + "max": 256 + } + }, + { + "name": "removedComponents", + "wire": { + "kind": "list", + "of": { + "kind": "registry_id", + "registry": "minecraft:data_component_type" + }, + "max": 256 + } + } + ] + }, + "net.minecraft.network.HashedStack#STREAM_CODEC": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.HashedStack$ActualItem#STREAM_CODEC" + } + }, + "net.minecraft.network.HashedStack$ActualItem#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "item", + "wire": { + "kind": "registry_id", + "registry": "minecraft:item" + } + }, + { + "name": "count", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "components", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.HashedPatchMap#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.chat.ChatType$Bound#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "chatType", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ChatType$Bound#STREAM_CODEC" + } + }, + { + "name": "name", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC" + } + }, + { + "name": "targetName", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_OPTIONAL_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.chat.ComponentSerialization#OPTIONAL_STREAM_CODEC": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#STREAM_CODEC" + } + }, + "net.minecraft.network.chat.ComponentSerialization#STREAM_CODEC": { + "kind": "nbt", + "note": "DFU codec serialised to NBT" + }, + "net.minecraft.network.chat.ComponentSerialization#TRUSTED_CONTEXT_FREE_STREAM_CODEC": { + "kind": "nbt", + "note": "DFU codec serialised to NBT" + }, + "net.minecraft.network.chat.ComponentSerialization#TRUSTED_OPTIONAL_STREAM_CODEC": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC" + } + }, + "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC": { + "kind": "nbt", + "note": "DFU codec serialised to NBT" + }, + "net.minecraft.network.chat.numbers.NumberFormatTypes#OPTIONAL_STREAM_CODEC": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.chat.numbers.NumberFormatTypes#STREAM_CODEC" + } + }, + "net.minecraft.network.chat.numbers.NumberFormatTypes#STREAM_CODEC": { + "kind": "unresolved", + "why": "dispatched codec: the layout depends on a runtime type" + }, + "net.minecraft.network.codec.ByteBufCodecs#BOOL": { + "kind": "bool" + }, + "net.minecraft.network.codec.ByteBufCodecs#BYTE": { + "kind": "i8" + }, + "net.minecraft.network.codec.ByteBufCodecs#BYTE_ARRAY": { + "kind": "byte_array", + "note": "varint length, then that many bytes" + }, + "net.minecraft.network.codec.ByteBufCodecs#COMPOUND_TAG": { + "kind": "nbt" + }, + "net.minecraft.network.codec.ByteBufCodecs#CONTAINER_ID": { + "kind": "varint" + }, + "net.minecraft.network.codec.ByteBufCodecs#DOUBLE": { + "kind": "f64" + }, + "net.minecraft.network.codec.ByteBufCodecs#FLOAT": { + "kind": "f32" + }, + "net.minecraft.network.codec.ByteBufCodecs#GAME_PROFILE": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.UUIDUtil#STREAM_CODEC" + } + }, + { + "name": "name", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#PLAYER_NAME" + } + }, + { + "name": "properties", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#GAME_PROFILE_PROPERTIES" + } + } + ] + }, + "net.minecraft.network.codec.ByteBufCodecs#GAME_PROFILE_PROPERTIES": { + "kind": "list", + "max": 16, + "of": { + "kind": "struct", + "name": "ProfileProperty", + "fields": [ + { + "name": "name", + "wire": { + "kind": "string", + "max": 64 + } + }, + { + "name": "value", + "wire": { + "kind": "string", + "max": 32767 + } + }, + { + "name": "signature", + "wire": { + "kind": "option", + "of": { + "kind": "string", + "max": 1024 + } + } + } + ] + } + }, + "net.minecraft.network.codec.ByteBufCodecs#INT": { + "kind": "i32" + }, + "net.minecraft.network.codec.ByteBufCodecs#LONG": { + "kind": "i64" + }, + "net.minecraft.network.codec.ByteBufCodecs#OPTIONAL_VAR_INT": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + }, + "net.minecraft.network.codec.ByteBufCodecs#PLAYER_NAME": { + "kind": "string", + "max": 16 + }, + "net.minecraft.network.codec.ByteBufCodecs#ROTATION_BYTE": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BYTE" + }, + "net.minecraft.network.codec.ByteBufCodecs#SHORT": { + "kind": "i16" + }, + "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8": { + "kind": "string", + "max": 32767 + }, + "net.minecraft.network.codec.ByteBufCodecs#TAG": { + "kind": "nbt" + }, + "net.minecraft.network.codec.ByteBufCodecs#TRUSTED_COMPOUND_TAG": { + "kind": "nbt" + }, + "net.minecraft.network.codec.ByteBufCodecs#VAR_INT": { + "kind": "varint" + }, + "net.minecraft.network.codec.ByteBufCodecs#VAR_LONG": { + "kind": "varlong" + }, + "net.minecraft.network.protocol.common.ClientboundClearDialogPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket#GAMEPLAY_STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled factory CustomPacketPayload.codec(..)" + }, + "net.minecraft.network.protocol.common.ClientboundCustomReportDetailsPacket#DETAILS_STREAM_CODEC": { + "kind": "map", + "key": { + "kind": "string", + "max": 128 + }, + "value": { + "kind": "string", + "max": 4096 + }, + "max": 32 + }, + "net.minecraft.network.protocol.common.ClientboundCustomReportDetailsPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "details", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.protocol.common.ClientboundCustomReportDetailsPacket#DETAILS_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.common.ClientboundDisconnectPacket#STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_CONTEXT_FREE_STREAM_CODEC" + }, + "net.minecraft.network.protocol.common.ClientboundKeepAlivePacket#STREAM_CODEC": { + "kind": "i64" + }, + "net.minecraft.network.protocol.common.ClientboundPingPacket#STREAM_CODEC": { + "kind": "i32" + }, + "net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket#STREAM_CODEC": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.core.UUIDUtil#STREAM_CODEC" + } + }, + "net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.UUIDUtil#STREAM_CODEC" + } + }, + { + "name": "url", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + }, + { + "name": "hash", + "wire": { + "kind": "string", + "max": 40 + } + }, + { + "name": "required", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + }, + { + "name": "prompt", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_CONTEXT_FREE_STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.common.ClientboundServerLinksPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "links", + "wire": { + "kind": "named", + "ref": "net.minecraft.server.ServerLinks#UNTRUSTED_LINKS_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.common.ClientboundShowDialogPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "dialog", + "wire": { + "kind": "named", + "ref": "net.minecraft.server.dialog.Dialog#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.common.ClientboundStoreCookiePacket#PAYLOAD_STREAM_CODEC": { + "kind": "byte_array", + "max": 5120 + }, + "net.minecraft.network.protocol.common.ClientboundStoreCookiePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "key", + "wire": { + "kind": "identifier", + "note": "namespaced name written as a utf string" + } + }, + { + "name": "payload", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.protocol.common.ClientboundStoreCookiePacket#PAYLOAD_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.common.ClientboundTransferPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "host", + "wire": { + "kind": "string", + "max": 32767 + } + }, + { + "name": "port", + "wire": { + "kind": "varint" + } + } + ] + }, + "net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket#STREAM_CODEC": { + "kind": "map", + "key": { + "kind": "identifier" + }, + "value": { + "kind": "unresolved", + "why": "unmodelled statement: value.write((FriendlyByteBuf)((Object)buffer))" + } + }, + "net.minecraft.network.protocol.common.ServerboundClientInformationPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "language", + "wire": { + "kind": "string", + "max": 32767 + } + }, + { + "name": "viewDistance", + "wire": { + "kind": "i8" + } + }, + { + "name": "chatVisibility", + "wire": { + "kind": "varint", + "note": "enum ordinal" + } + }, + { + "name": "chatColors", + "wire": { + "kind": "bool" + } + }, + { + "name": "modelCustomisation", + "wire": { + "kind": "i8" + } + }, + { + "name": "mainHand", + "wire": { + "kind": "enum", + "type": "net.minecraft.world.entity.HumanoidArm", + "constants": [ + { + "name": "LEFT", + "value": 0 + }, + { + "name": "RIGHT", + "value": 1 + } + ], + "note": "varint ordinal" + } + }, + { + "name": "textFilteringEnabled", + "wire": { + "kind": "bool" + } + }, + { + "name": "allowsListing", + "wire": { + "kind": "bool" + } + }, + { + "name": "particleStatus", + "wire": { + "kind": "varint", + "note": "enum ordinal" + } + } + ] + }, + "net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + } + }, + { + "name": "payload", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket#UNTRUSTED_TAG_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket#UNTRUSTED_TAG_CODEC": { + "kind": "length_prefixed", + "of": { + "kind": "optional_nbt", + "note": "TAG_End means absent" + }, + "max": 65536, + "note": "varint byte length, then the value" + }, + "net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled factory CustomPacketPayload.codec(..)" + }, + "net.minecraft.network.protocol.common.ServerboundKeepAlivePacket#STREAM_CODEC": { + "kind": "i64" + }, + "net.minecraft.network.protocol.common.ServerboundPongPacket#STREAM_CODEC": { + "kind": "i32" + }, + "net.minecraft.network.protocol.common.ServerboundResourcePackPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "uuid", + "note": "two big-endian longs, most significant first" + } + }, + { + "name": "action", + "wire": { + "kind": "enum", + "type": "net.minecraft.network.protocol.common.ServerboundResourcePackPacket$Action", + "constants": [ + { + "name": "SUCCESSFULLY_LOADED", + "value": 0 + }, + { + "name": "DECLINED", + "value": 1 + }, + { + "name": "FAILED_DOWNLOAD", + "value": 2 + }, + { + "name": "ACCEPTED", + "value": 3 + }, + { + "name": "DOWNLOADED", + "value": 4 + }, + { + "name": "INVALID_URL", + "value": 5 + }, + { + "name": "FAILED_RELOAD", + "value": 6 + }, + { + "name": "DISCARDED", + "value": 7 + } + ], + "note": "varint ordinal" + } + } + ] + }, + "net.minecraft.network.protocol.configuration.ClientboundCodeOfConductPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "codeOfConduct", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + } + ] + }, + "net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket#REGISTRY_KEY_STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + }, + "net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "registry", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket#REGISTRY_KEY_STREAM_CODEC" + } + }, + { + "name": "entries", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.core.RegistrySynchronization$PackedRegistryEntry#STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.configuration.ClientboundResetChatPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.configuration.ClientboundSelectKnownPacks#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "knownPacks", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.server.packs.repository.KnownPack#STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "identifier", + "note": "namespaced name written as a utf string" + } + }, + "net.minecraft.network.protocol.configuration.ServerboundAcceptCodeOfConductPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.configuration.ServerboundFinishConfigurationPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.configuration.ServerboundSelectKnownPacks#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "knownPacks", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.server.packs.repository.KnownPack#STREAM_CODEC" + }, + "max": 64 + } + } + ] + }, + "net.minecraft.network.protocol.cookie.ClientboundCookieRequestPacket#STREAM_CODEC": { + "kind": "identifier", + "note": "namespaced name written as a utf string" + }, + "net.minecraft.network.protocol.cookie.ServerboundCookieResponsePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "key", + "wire": { + "kind": "identifier", + "note": "namespaced name written as a utf string" + } + }, + { + "name": "payload", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.protocol.common.ClientboundStoreCookiePacket#PAYLOAD_STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundAddEntityPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "varint" + } + }, + { + "name": "uuid", + "wire": { + "kind": "uuid", + "note": "two big-endian longs, most significant first" + } + }, + { + "name": "type", + "wire": { + "kind": "registry_id", + "registry": "minecraft:entity_type" + } + }, + { + "name": "x", + "wire": { + "kind": "f64" + } + }, + { + "name": "y", + "wire": { + "kind": "f64" + } + }, + { + "name": "z", + "wire": { + "kind": "f64" + } + }, + { + "name": "movement", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.phys.Vec3#LP_STREAM_CODEC" + } + }, + { + "name": "xRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "yRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "yHeadRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "data", + "wire": { + "kind": "varint" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundAnimatePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "varint" + } + }, + { + "name": "action", + "wire": { + "kind": "i8" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundAwardStatsPacket#STAT_VALUES_STREAM_CODEC": { + "kind": "map", + "key": { + "kind": "named", + "ref": "net.minecraft.stats.Stat#STREAM_CODEC" + }, + "value": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + "net.minecraft.network.protocol.game.ClientboundAwardStatsPacket#STREAM_CODEC": { + "kind": "named", + "ref": "net.minecraft.network.protocol.game.ClientboundAwardStatsPacket#STAT_VALUES_STREAM_CODEC" + }, + "net.minecraft.network.protocol.game.ClientboundBlockChangedAckPacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundBlockDestructionPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "varint" + } + }, + { + "name": "pos", + "wire": { + "kind": "block_pos", + "note": "one i64, x/z/y packed 26/26/12" + } + }, + { + "name": "progress", + "wire": { + "kind": "i8" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "pos", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.BlockPos#STREAM_CODEC" + } + }, + { + "name": "type", + "wire": { + "kind": "registry_id", + "registry": "minecraft:block_entity_type" + } + }, + { + "name": "tag", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#TRUSTED_COMPOUND_TAG" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundBlockEventPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "pos", + "wire": { + "kind": "block_pos", + "note": "one i64, x/z/y packed 26/26/12" + } + }, + { + "name": "b0", + "wire": { + "kind": "i8" + } + }, + { + "name": "b1", + "wire": { + "kind": "i8" + } + }, + { + "name": "block", + "wire": { + "kind": "registry_id", + "registry": "minecraft:block" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "pos", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.BlockPos#STREAM_CODEC" + } + }, + { + "name": "blockState", + "wire": { + "kind": "varint", + "note": "index into an id map" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundBossEventPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "difficulty", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.Difficulty#STREAM_CODEC" + } + }, + { + "name": "locked", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundChunkBatchFinishedPacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundChunkBatchStartPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "unresolved", + "why": "unmodelled statement: c.write((FriendlyByteBuf)((Object)o))" + } + }, + "net.minecraft.network.protocol.game.ClientboundClearTitlesPacket#STREAM_CODEC": { + "kind": "bool" + }, + "net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "start", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "length", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "suggestions", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket$Entry#STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket$Entry#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "text", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + }, + { + "name": "tooltip", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_OPTIONAL_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundCommandsPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entries", + "wire": { + "kind": "list", + "of": { + "kind": "unresolved", + "why": "unmodelled statement: entry.write((FriendlyByteBuf)((Object)buffer))" + } + } + }, + { + "name": "rootIndex", + "wire": { + "kind": "varint" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundContainerClosePacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#CONTAINER_ID" + } + }, + { + "name": "stateId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "items", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.ItemStack#OPTIONAL_LIST_STREAM_CODEC" + } + }, + { + "name": "carriedItem", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.ItemStack#OPTIONAL_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundContainerSetDataPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "varint" + } + }, + { + "name": "id", + "wire": { + "kind": "i16" + } + }, + { + "name": "value", + "wire": { + "kind": "i16" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "varint" + } + }, + { + "name": "stateId", + "wire": { + "kind": "varint" + } + }, + { + "name": "slot", + "wire": { + "kind": "i16" + } + }, + { + "name": "itemStack", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.ItemStack#OPTIONAL_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundCooldownPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "cooldownGroup", + "wire": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + } + }, + { + "name": "duration", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "action", + "wire": { + "kind": "enum", + "type": "net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket$Action", + "constants": [ + { + "name": "ADD", + "value": 0 + }, + { + "name": "REMOVE", + "value": 1 + }, + { + "name": "SET", + "value": 2 + } + ], + "note": "varint ordinal" + } + }, + { + "name": "entries", + "wire": { + "kind": "list", + "of": { + "kind": "string", + "max": 32767 + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDamageEventPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "varint" + } + }, + { + "name": "sourceType", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.damagesource.DamageType#STREAM_CODEC" + } + }, + { + "name": "output", + "wire": { + "kind": "varint" + } + }, + { + "name": "output", + "wire": { + "kind": "varint" + } + }, + { + "name": "sourcePosition", + "wire": { + "kind": "option", + "of": { + "kind": "struct", + "fields": [ + { + "name": "x", + "wire": { + "kind": "f64" + } + }, + { + "name": "y", + "wire": { + "kind": "f64" + } + }, + { + "name": "z", + "wire": { + "kind": "f64" + } + } + ] + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDebugBlockValuePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "blockPos", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.BlockPos#STREAM_CODEC" + } + }, + { + "name": "update", + "wire": { + "kind": "named", + "ref": "net.minecraft.util.debug.DebugSubscription$Update#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDebugChunkValuePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "chunkPos", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.level.ChunkPos#STREAM_CODEC" + } + }, + { + "name": "update", + "wire": { + "kind": "named", + "ref": "net.minecraft.util.debug.DebugSubscription$Update#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDebugEntityValuePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "update", + "wire": { + "kind": "named", + "ref": "net.minecraft.util.debug.DebugSubscription$Update#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDebugEventPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "event", + "wire": { + "kind": "named", + "ref": "net.minecraft.util.debug.DebugSubscription$Event#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDebugSamplePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "sample", + "wire": { + "kind": "long_array", + "note": "varint length, then that many i64" + } + }, + { + "name": "debugSampleType", + "wire": { + "kind": "enum", + "type": "net.minecraft.util.debugchart.RemoteDebugSampleType", + "constants": [ + { + "name": "TICK_TIME", + "value": 0 + } + ], + "note": "varint ordinal" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundDeleteChatPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled statement: MessageSignature.Packed.write(output, this.messageSignature)" + }, + "net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "message", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC" + } + }, + { + "name": "chatType", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ChatType$Bound#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundEntityEventPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "i32" + } + }, + { + "name": "eventId", + "wire": { + "kind": "i8" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "values", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.entity.PositionMoveRotation#STREAM_CODEC" + } + }, + { + "name": "onGround", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundExplodePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "center", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.phys.Vec3#STREAM_CODEC" + } + }, + { + "name": "radius", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + }, + { + "name": "blockCount", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#INT" + } + }, + { + "name": "playerKnockback", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.world.phys.Vec3#STREAM_CODEC" + } + } + }, + { + "name": "explosionParticle", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.particles.ParticleTypes#STREAM_CODEC" + } + }, + { + "name": "explosionSound", + "wire": { + "kind": "named", + "ref": "net.minecraft.sounds.SoundEvent#STREAM_CODEC" + } + }, + { + "name": "blockParticles", + "wire": { + "kind": "list", + "of": { + "kind": "struct", + "fields": [ + { + "name": "value", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.particles.ExplosionParticleInfo#STREAM_CODEC" + } + }, + { + "name": "weight", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + } + ] + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket#STREAM_CODEC": { + "kind": "chunk_pos", + "note": "one i64, x and z as two i32" + }, + "net.minecraft.network.protocol.game.ClientboundGameEventPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "event", + "wire": { + "kind": "i8" + } + }, + { + "name": "param", + "wire": { + "kind": "f32" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundGameRuleValuesPacket#STREAM_CODEC": { + "kind": "map", + "key": { + "kind": "named", + "ref": "net.minecraft.resources.Identifier#STREAM_CODEC" + }, + "value": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8" + } + }, + "net.minecraft.network.protocol.game.ClientboundGameTestHighlightPosPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "absolutePos", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.BlockPos#STREAM_CODEC" + } + }, + { + "name": "relativePos", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.BlockPos#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "varint" + } + }, + { + "name": "yaw", + "wire": { + "kind": "f32" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundInitializeBorderPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "newCenterX", + "wire": { + "kind": "f64" + } + }, + { + "name": "newCenterZ", + "wire": { + "kind": "f64" + } + }, + { + "name": "oldSize", + "wire": { + "kind": "f64" + } + }, + { + "name": "newSize", + "wire": { + "kind": "f64" + } + }, + { + "name": "lerpTime", + "wire": { + "kind": "varlong" + } + }, + { + "name": "newAbsoluteMaxSize", + "wire": { + "kind": "varint" + } + }, + { + "name": "warningBlocks", + "wire": { + "kind": "varint" + } + }, + { + "name": "warningTime", + "wire": { + "kind": "varint" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled statement: this.chunkData.write(output)" + }, + "net.minecraft.network.protocol.game.ClientboundLevelEventPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "type", + "wire": { + "kind": "i32" + } + }, + { + "name": "pos", + "wire": { + "kind": "block_pos", + "note": "one i64, x/z/y packed 26/26/12" + } + }, + { + "name": "data", + "wire": { + "kind": "i32" + } + }, + { + "name": "globalEvent", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "overrideLimiter", + "wire": { + "kind": "bool" + } + }, + { + "name": "alwaysShow", + "wire": { + "kind": "bool" + } + }, + { + "name": "x", + "wire": { + "kind": "f64" + } + }, + { + "name": "y", + "wire": { + "kind": "f64" + } + }, + { + "name": "z", + "wire": { + "kind": "f64" + } + }, + { + "name": "xDist", + "wire": { + "kind": "f32" + } + }, + { + "name": "yDist", + "wire": { + "kind": "f32" + } + }, + { + "name": "zDist", + "wire": { + "kind": "f32" + } + }, + { + "name": "maxSpeed", + "wire": { + "kind": "f32" + } + }, + { + "name": "count", + "wire": { + "kind": "i32" + } + }, + { + "name": "particle", + "wire": { + "kind": "named", + "ref": "net.minecraft.core.particles.ParticleTypes#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundLightUpdatePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "x", + "wire": { + "kind": "varint" + } + }, + { + "name": "z", + "wire": { + "kind": "varint" + } + }, + { + "name": "lightData", + "wire": { + "kind": "struct", + "fields": [ + { + "name": "skyYMask", + "wire": { + "kind": "long_array" + } + }, + { + "name": "blockYMask", + "wire": { + "kind": "long_array" + } + }, + { + "name": "emptySkyYMask", + "wire": { + "kind": "long_array" + } + }, + { + "name": "emptyBlockYMask", + "wire": { + "kind": "long_array" + } + }, + { + "name": "skyUpdates", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.network.protocol.game.ClientboundLightUpdatePacketData#DATA_LAYER_STREAM_CODEC" + } + } + }, + { + "name": "blockUpdates", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.network.protocol.game.ClientboundLightUpdatePacketData#DATA_LAYER_STREAM_CODEC" + } + } + } + ] + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundLightUpdatePacketData#DATA_LAYER_STREAM_CODEC": { + "kind": "byte_array", + "max": 2048 + }, + "net.minecraft.network.protocol.game.ClientboundLoginPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "playerId", + "wire": { + "kind": "i32" + } + }, + { + "name": "hardcore", + "wire": { + "kind": "bool" + } + }, + { + "name": "levels", + "wire": { + "kind": "list", + "of": { + "kind": "identifier" + } + } + }, + { + "name": "maxPlayers", + "wire": { + "kind": "varint" + } + }, + { + "name": "chunkRadius", + "wire": { + "kind": "varint" + } + }, + { + "name": "simulationDistance", + "wire": { + "kind": "varint" + } + }, + { + "name": "reducedDebugInfo", + "wire": { + "kind": "bool" + } + }, + { + "name": "showDeathScreen", + "wire": { + "kind": "bool" + } + }, + { + "name": "doLimitedCrafting", + "wire": { + "kind": "bool" + } + }, + { + "name": "commonPlayerSpawnInfo", + "wire": { + "kind": "struct", + "fields": [ + { + "name": "dimensionType", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.level.dimension.DimensionType#STREAM_CODEC" + } + }, + { + "name": "dimension", + "wire": { + "kind": "identifier" + } + }, + { + "name": "seed", + "wire": { + "kind": "i64" + } + }, + { + "name": "gameType", + "wire": { + "kind": "i8" + } + }, + { + "name": "previousGameType", + "wire": { + "kind": "i8" + } + }, + { + "name": "isDebug", + "wire": { + "kind": "bool" + } + }, + { + "name": "isFlat", + "wire": { + "kind": "bool" + } + }, + { + "name": "lastDeathLocation", + "wire": { + "kind": "option", + "of": { + "kind": "struct", + "name": "GlobalPos", + "fields": [ + { + "name": "dimension", + "wire": { + "kind": "identifier" + } + }, + { + "name": "pos", + "wire": { + "kind": "block_pos" + } + } + ] + } + } + }, + { + "name": "portalCooldown", + "wire": { + "kind": "varint" + } + }, + { + "name": "seaLevel", + "wire": { + "kind": "varint" + } + } + ] + } + }, + { + "name": "onlineMode", + "wire": { + "kind": "bool" + } + }, + { + "name": "enforcesSecureChat", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundLowDiskSpaceWarningPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.game.ClientboundMapItemDataPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "mapId", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.level.saveddata.maps.MapId#STREAM_CODEC" + } + }, + { + "name": "scale", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BYTE" + } + }, + { + "name": "locked", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + }, + { + "name": "decorations", + "wire": { + "kind": "option", + "of": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.world.level.saveddata.maps.MapDecoration#STREAM_CODEC" + } + } + } + }, + { + "name": "colorPatch", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.level.saveddata.maps.MapItemSavedData$MapPatch#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMerchantOffersPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "varint" + } + }, + { + "name": "offers", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.trading.MerchantOffers#STREAM_CODEC" + } + }, + { + "name": "villagerLevel", + "wire": { + "kind": "varint" + } + }, + { + "name": "villagerXp", + "wire": { + "kind": "varint" + } + }, + { + "name": "showProgress", + "wire": { + "kind": "bool" + } + }, + { + "name": "canRestock", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMountScreenOpenPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "varint" + } + }, + { + "name": "inventoryColumns", + "wire": { + "kind": "varint" + } + }, + { + "name": "entityId", + "wire": { + "kind": "i32" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Pos#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "varint" + } + }, + { + "name": "xa", + "wire": { + "kind": "i16" + } + }, + { + "name": "ya", + "wire": { + "kind": "i16" + } + }, + { + "name": "za", + "wire": { + "kind": "i16" + } + }, + { + "name": "onGround", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$PosRot#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "varint" + } + }, + { + "name": "xa", + "wire": { + "kind": "i16" + } + }, + { + "name": "ya", + "wire": { + "kind": "i16" + } + }, + { + "name": "za", + "wire": { + "kind": "i16" + } + }, + { + "name": "yRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "xRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "onGround", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Rot#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "varint" + } + }, + { + "name": "yRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "xRot", + "wire": { + "kind": "i8" + } + }, + { + "name": "onGround", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMoveMinecartPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "lerpSteps", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.world.entity.vehicle.minecart.NewMinecartBehavior$MinecartStep#STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundMoveVehiclePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "position", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.phys.Vec3#STREAM_CODEC" + } + }, + { + "name": "yRot", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + }, + { + "name": "xRot", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundOpenBookPacket#STREAM_CODEC": { + "kind": "enum", + "type": "net.minecraft.world.InteractionHand", + "constants": [ + { + "name": "MAIN_HAND", + "value": 0 + }, + { + "name": "OFF_HAND", + "value": 1 + } + ], + "note": "varint ordinal" + }, + "net.minecraft.network.protocol.game.ClientboundOpenScreenPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#CONTAINER_ID" + } + }, + { + "name": "type", + "wire": { + "kind": "registry_id", + "registry": "minecraft:menu" + } + }, + { + "name": "title", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "pos", + "wire": { + "kind": "block_pos", + "note": "one i64, x/z/y packed 26/26/12" + } + }, + { + "name": "isFrontText", + "wire": { + "kind": "bool" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundPlaceGhostRecipePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "containerId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#CONTAINER_ID" + } + }, + { + "name": "recipeDisplay", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.crafting.display.RecipeDisplay#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "branching encode body: if (this.invulnerable) {\n bitfield = (byte)(bitfi" + }, + "net.minecraft.network.protocol.game.ClientboundPlayerChatPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled statement: this.body.write(output)" + }, + "net.minecraft.network.protocol.game.ClientboundPlayerCombatEndPacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundPlayerCombatEnterPacket#STREAM_CODEC": { + "kind": "unit", + "note": "carries no bytes" + }, + "net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "playerId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "message", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.core.UUIDUtil#STREAM_CODEC" + } + }, + "net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "actions", + "wire": { + "kind": "fixed_bitset", + "bits": 1, + "note": "one bit per enum constant, rounded up to whole bytes" + } + }, + { + "name": "entries", + "wire": { + "kind": "list", + "of": { + "kind": "unresolved", + "why": "branching encode body: for (Action action : this.actions) {\n action." + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundPlayerLookAtPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "branching encode body: if (this.atEntity) {\n output.writeVarInt(this.ent" + }, + "net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "change", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.entity.PositionMoveRotation#STREAM_CODEC" + } + }, + { + "name": "relatives", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.entity.Relative#SET_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundPlayerRotationPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "yRot", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + }, + { + "name": "relativeY", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + }, + { + "name": "xRot", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#FLOAT" + } + }, + { + "name": "relativeX", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundProjectilePowerPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "varint" + } + }, + { + "name": "accelerationPower", + "wire": { + "kind": "f64" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRecipeBookAddPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entries", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.network.protocol.game.ClientboundRecipeBookAddPacket$Entry#STREAM_CODEC" + } + } + }, + { + "name": "replace", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BOOL" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRecipeBookAddPacket$Entry#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "contents", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.crafting.display.RecipeDisplayEntry#STREAM_CODEC" + } + }, + { + "name": "flags", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BYTE" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRecipeBookRemovePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "recipes", + "wire": { + "kind": "list", + "of": { + "kind": "named", + "ref": "net.minecraft.world.item.crafting.display.RecipeDisplayId#STREAM_CODEC" + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRecipeBookSettingsPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "bookSettings", + "wire": { + "kind": "named", + "ref": "net.minecraft.stats.RecipeBookSettings#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket#STREAM_CODEC": { + "kind": "list", + "of": { + "kind": "varint" + } + }, + "net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "effect", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.effect.MobEffect#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundResetScorePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "wire": { + "kind": "string", + "max": 32767 + } + }, + { + "name": "objectiveName", + "wire": { + "kind": "option", + "of": { + "kind": "string", + "max": 32767 + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRespawnPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "commonPlayerSpawnInfo", + "wire": { + "kind": "struct", + "fields": [ + { + "name": "dimensionType", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.level.dimension.DimensionType#STREAM_CODEC" + } + }, + { + "name": "dimension", + "wire": { + "kind": "identifier" + } + }, + { + "name": "seed", + "wire": { + "kind": "i64" + } + }, + { + "name": "gameType", + "wire": { + "kind": "i8" + } + }, + { + "name": "previousGameType", + "wire": { + "kind": "i8" + } + }, + { + "name": "isDebug", + "wire": { + "kind": "bool" + } + }, + { + "name": "isFlat", + "wire": { + "kind": "bool" + } + }, + { + "name": "lastDeathLocation", + "wire": { + "kind": "option", + "of": { + "kind": "struct", + "name": "GlobalPos", + "fields": [ + { + "name": "dimension", + "wire": { + "kind": "identifier" + } + }, + { + "name": "pos", + "wire": { + "kind": "block_pos" + } + } + ] + } + } + }, + { + "name": "portalCooldown", + "wire": { + "kind": "varint" + } + }, + { + "name": "seaLevel", + "wire": { + "kind": "varint" + } + } + ] + } + }, + { + "name": "dataToKeep", + "wire": { + "kind": "i8" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundRotateHeadPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "entityId", + "wire": { + "kind": "varint" + } + }, + { + "name": "yHeadRot", + "wire": { + "kind": "i8" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "branching encode body: for (int i = 0; i < this.positions.length; ++i) {\n " + }, + "net.minecraft.network.protocol.game.ClientboundSelectAdvancementsTabPacket#STREAM_CODEC": { + "kind": "option", + "of": { + "kind": "identifier", + "note": "namespaced name written as a utf string" + } + }, + "net.minecraft.network.protocol.game.ClientboundServerDataPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "motd", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_CONTEXT_FREE_STREAM_CODEC" + } + }, + { + "name": "iconBytes", + "wire": { + "kind": "option", + "of": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#BYTE_ARRAY" + } + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "text", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.chat.ComponentSerialization#TRUSTED_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetBorderCenterPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "newCenterX", + "wire": { + "kind": "f64" + } + }, + { + "name": "newCenterZ", + "wire": { + "kind": "f64" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetBorderLerpSizePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "oldSize", + "wire": { + "kind": "f64" + } + }, + { + "name": "newSize", + "wire": { + "kind": "f64" + } + }, + { + "name": "lerpTime", + "wire": { + "kind": "varlong" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetBorderSizePacket#STREAM_CODEC": { + "kind": "f64" + }, + "net.minecraft.network.protocol.game.ClientboundSetBorderWarningDelayPacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundSetCameraPacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "x", + "wire": { + "kind": "varint" + } + }, + { + "name": "z", + "wire": { + "kind": "varint" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket#STREAM_CODEC": { + "kind": "varint" + }, + "net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "contents", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.item.ItemStack#OPTIONAL_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "respawnData", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.level.storage.LevelData$RespawnData#STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "varint", + "note": "index produced by a ToIntFunction" + } + }, + { + "name": "objectiveName", + "wire": { + "kind": "string", + "max": 32767 + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "unmodelled statement: ClientboundSetEntityDataPacket.pack(this.packedItems, output)" + }, + "net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "sourceId", + "wire": { + "kind": "i32" + } + }, + { + "name": "destId", + "wire": { + "kind": "i32" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket#STREAM_CODEC": { + "kind": "struct", + "fields": [ + { + "name": "id", + "wire": { + "kind": "named", + "ref": "net.minecraft.network.codec.ByteBufCodecs#VAR_INT" + } + }, + { + "name": "movement", + "wire": { + "kind": "named", + "ref": "net.minecraft.world.phys.Vec3#LP_STREAM_CODEC" + } + } + ] + }, + "net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket#STREAM_CODEC": { + "kind": "unresolved", + "why": "branching encode body: for (int i = 0; i < size; ++i) {\n Pair Reader<'a> { self.take(length) } + /// Read a length-prefixed byte array no longer than `max` bytes. + /// + /// # Errors + /// Returns [`Error::ListTooLong`] past `max`, and otherwise as + /// [`Reader::byte_array`]. + pub fn byte_array_with_limit(&mut self, max: usize) -> Result<&'a [u8]> { + let bytes = self.byte_array()?; + if bytes.len() > max { + return Err(Error::ListTooLong { + length: bytes.len(), + max, + }); + } + Ok(bytes) + } + /// Read a UTF-8 string limited to [`MAX_STRING_LENGTH`] characters. /// /// # Errors @@ -387,6 +403,20 @@ impl Writer { Ok(()) } + /// Append a length-prefixed byte array no longer than `max` bytes. + /// + /// # Errors + /// Returns [`Error::ListTooLong`] past `max`. + pub fn byte_array_with_limit(&mut self, bytes: &[u8], max: usize) -> Result<()> { + if bytes.len() > max { + return Err(Error::ListTooLong { + length: bytes.len(), + max, + }); + } + self.byte_array(bytes) + } + /// Append a string limited to [`MAX_STRING_LENGTH`] characters. /// /// # Errors @@ -416,3 +446,47 @@ impl Writer { self.raw(&value.to_be_bytes()); } } + +// --------------------------------------------------------------------------- +// Counts +// --------------------------------------------------------------------------- + +/// Write a collection's element count, refusing one the reader would reject. +/// +/// The server checks the count only on read (`ByteBufCodecs.writeCount` +/// asserts, `readCount` throws). Checking on write as well turns a value that +/// could never be delivered into an error here rather than a disconnect at the +/// far end. +/// +/// # Errors +/// Returns [`Error::ListTooLong`] past `max`, or when the count does not fit +/// in a `VarInt`. +pub fn write_count(writer: &mut Writer, count: usize, max: Option) -> Result<()> { + if let Some(max) = max + && count > max + { + return Err(Error::ListTooLong { length: count, max }); + } + let count = i32::try_from(count).map_err(|_| Error::ListTooLong { + length: count, + max: i32::MAX as usize, + })?; + writer.var_int(count); + Ok(()) +} + +/// Read a collection's element count. +/// +/// # Errors +/// Returns [`Error::NegativeLength`] for a negative count and +/// [`Error::ListTooLong`] for one past `max`. +pub fn read_count(reader: &mut Reader<'_>, max: Option) -> Result { + let count = reader.var_int()?; + let count = usize::try_from(count).map_err(|_| Error::NegativeLength(count))?; + if let Some(max) = max + && count > max + { + return Err(Error::ListTooLong { length: count, max }); + } + Ok(count) +} diff --git a/crates/hyperion-minecraft-proto/src/error.rs b/crates/hyperion-minecraft-proto/src/error.rs index 381137538..af4c71821 100644 --- a/crates/hyperion-minecraft-proto/src/error.rs +++ b/crates/hyperion-minecraft-proto/src/error.rs @@ -20,6 +20,15 @@ pub enum Error { VarIntTooLong, /// A length prefix was negative, which the protocol never emits. NegativeLength(i32), + /// A collection field held more elements than the field permits. + ListTooLong { + /// Element count seen. + length: usize, + /// Maximum the field permits. + max: usize, + }, + /// A namespaced name used a character outside the permitted set. + InvalidIdentifier(String), /// A string field exceeded the limit for that field. StringTooLong { /// Length seen, in UTF-16 code units to match the server's own check. @@ -89,6 +98,12 @@ impl fmt::Display for Error { Self::StringTooLong { length, max } => { write!(f, "string too long: {length} characters, maximum {max}") } + Self::ListTooLong { length, max } => { + write!(f, "list too long: {length} elements, maximum {max}") + } + Self::InvalidIdentifier(value) => { + write!(f, "not a valid namespaced identifier: {value}") + } Self::InvalidUtf8 => f.write_str("string field was not valid UTF-8"), Self::InvalidEnum { name, value } => write!(f, "invalid {name} discriminant: {value}"), Self::TrailingBytes(n) => write!(f, "{n} trailing bytes after packet body"), diff --git a/crates/hyperion-minecraft-proto/src/generated/mod.rs b/crates/hyperion-minecraft-proto/src/generated/mod.rs index 507747a5e..6bd4feea6 100644 --- a/crates/hyperion-minecraft-proto/src/generated/mod.rs +++ b/crates/hyperion-minecraft-proto/src/generated/mod.rs @@ -16,4 +16,4 @@ pub mod wire; pub use data_component::DataComponent; pub use version::{MINECRAFT_VERSION, PROTOCOL_VERSION, WORLD_VERSION}; -pub use wire::{Field, Wire}; +pub use wire::{Constant, Field, Wire}; diff --git a/crates/hyperion-minecraft-proto/src/generated/packet_id.rs b/crates/hyperion-minecraft-proto/src/generated/packet_id.rs index b2b8467fc..b667c86ab 100644 --- a/crates/hyperion-minecraft-proto/src/generated/packet_id.rs +++ b/crates/hyperion-minecraft-proto/src/generated/packet_id.rs @@ -591,6 +591,7 @@ pub mod configuration { #[must_use] pub const fn layout(self) -> Option<&'static crate::generated::wire::Wire> { match self { + Self::ClientInformation => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDCLIENTINFORMATIONPACKET_STREAM_CODEC), Self::CookieResponse => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_COOKIE_SERVERBOUNDCOOKIERESPONSEPACKET_STREAM_CODEC), Self::FinishConfiguration => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_CONFIGURATION_SERVERBOUNDFINISHCONFIGURATIONPACKET_STREAM_CODEC), Self::KeepAlive => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDKEEPALIVEPACKET_STREAM_CODEC), @@ -1470,6 +1471,7 @@ pub mod play { Self::ChunkBatchReceived => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCHUNKBATCHRECEIVEDPACKET_STREAM_CODEC), Self::ClientCommand => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCLIENTCOMMANDPACKET_STREAM_CODEC), Self::ClientTickEnd => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCLIENTTICKENDPACKET_STREAM_CODEC), + Self::ClientInformation => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDCLIENTINFORMATIONPACKET_STREAM_CODEC), Self::CommandSuggestion => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCOMMANDSUGGESTIONPACKET_STREAM_CODEC), Self::ConfigurationAcknowledged => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCONFIGURATIONACKNOWLEDGEDPACKET_STREAM_CODEC), Self::ContainerButtonClick => Some(&crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCONTAINERBUTTONCLICKPACKET_STREAM_CODEC), diff --git a/crates/hyperion-minecraft-proto/src/generated/wire.rs b/crates/hyperion-minecraft-proto/src/generated/wire.rs index fb55eb6a7..0a363cb21 100644 --- a/crates/hyperion-minecraft-proto/src/generated/wire.rs +++ b/crates/hyperion-minecraft-proto/src/generated/wire.rs @@ -17,6 +17,16 @@ pub struct Field { pub wire: &'static Wire, } +/// One constant of a [`Wire::Enum`], and the number it is sent as. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Constant { + /// Mojang's own name for the constant. + pub name: &'static str, + /// The `VarInt` the server writes for it, which is the declaration index + /// unless the class supplies ids of its own. + pub value: i32, +} + /// The layout of a value on the wire. /// /// Variants name what the server writes rather than how a Rust value is @@ -124,6 +134,14 @@ pub enum Wire { /// Layout written when the discriminant is false. right: &'static Self, }, + /// A `VarInt` naming one of a fixed set of constants. + Enum { + /// Class the constants come from, e.g. + /// `net.minecraft.world.InteractionHand`. + java_class: &'static str, + /// Every constant, in declaration order. + constants: &'static [Constant], + }, /// Fields written back to back with nothing between them. Struct(&'static [Field]), } @@ -164,8 +182,24 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_HANDSHAKE_CLIENTINTENTIONPACKET wire: &crate::generated::wire::Wire::I16, }, crate::generated::wire::Field { - name: "id", - wire: &crate::generated::wire::Wire::VarInt, + name: "intention", + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.network.protocol.handshake.ClientIntent", + constants: &[ + crate::generated::wire::Constant { + name: "STATUS", + value: 1, + }, + crate::generated::wire::Constant { + name: "LOGIN", + value: 2, + }, + crate::generated::wire::Constant { + name: "TRANSFER", + value: 3, + }, + ], + }, }, ]); @@ -284,6 +318,9 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_LOGIN_CLIENTBOUNDLOGINCOMPRESSI /// `net.minecraft.network.protocol.cookie.ClientboundCookieRequestPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_COOKIE_CLIENTBOUNDCOOKIEREQUESTPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Identifier; +/// `net.minecraft.network.protocol.common.ServerboundClientInformationPacket#STREAM_CODEC` +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDCLIENTINFORMATIONPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "language", wire: &crate::generated::wire::Wire::Str { max: Some(32_767) } }, crate::generated::wire::Field { name: "viewDistance", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "chatVisibility", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "chatColors", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "modelCustomisation", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "mainHand", wire: &crate::generated::wire::Wire::Enum { java_class: "net.minecraft.world.entity.HumanoidArm", constants: &[crate::generated::wire::Constant { name: "LEFT", value: 0 }, crate::generated::wire::Constant { name: "RIGHT", value: 1 }] } }, crate::generated::wire::Field { name: "textFilteringEnabled", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "allowsListing", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "particleStatus", wire: &crate::generated::wire::Wire::VarInt }]); + /// `net.minecraft.network.protocol.configuration.ServerboundFinishConfigurationPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_CONFIGURATION_SERVERBOUNDFINISHCONFIGURATIONPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Unit; @@ -296,7 +333,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDPONGPACKET_ST crate::generated::wire::Wire::I32; /// `net.minecraft.network.protocol.common.ServerboundResourcePackPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDRESOURCEPACKPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "id", wire: &crate::generated::wire::Wire::Uuid }, crate::generated::wire::Field { name: "action", wire: &crate::generated::wire::Wire::VarInt }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_COMMON_SERVERBOUNDRESOURCEPACKPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "id", wire: &crate::generated::wire::Wire::Uuid }, crate::generated::wire::Field { name: "action", wire: &crate::generated::wire::Wire::Enum { java_class: "net.minecraft.network.protocol.common.ServerboundResourcePackPacket$Action", constants: &[crate::generated::wire::Constant { name: "SUCCESSFULLY_LOADED", value: 0 }, crate::generated::wire::Constant { name: "DECLINED", value: 1 }, crate::generated::wire::Constant { name: "FAILED_DOWNLOAD", value: 2 }, crate::generated::wire::Constant { name: "ACCEPTED", value: 3 }, crate::generated::wire::Constant { name: "DOWNLOADED", value: 4 }, crate::generated::wire::Constant { name: "INVALID_URL", value: 5 }, crate::generated::wire::Constant { name: "FAILED_RELOAD", value: 6 }, crate::generated::wire::Constant { name: "DISCARDED", value: 7 }] } }]); /// `net.minecraft.network.codec.ByteBufCodecs#STRING_UTF8` pub(crate) static NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_STRING_UTF8: Wire = @@ -505,7 +542,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCHATCOMMANDPACK pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCHUNKBATCHRECEIVEDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::F32; /// `net.minecraft.network.protocol.game.ServerboundClientCommandPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCLIENTCOMMANDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::VarInt; +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCLIENTCOMMANDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Enum { java_class: "net.minecraft.network.protocol.game.ServerboundClientCommandPacket$Action", constants: &[crate::generated::wire::Constant { name: "PERFORM_RESPAWN", value: 0 }, crate::generated::wire::Constant { name: "REQUEST_STATS", value: 1 }, crate::generated::wire::Constant { name: "REQUEST_GAMERULE_VALUES", value: 2 }] }; /// `net.minecraft.network.protocol.game.ServerboundClientTickEndPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDCLIENTTICKENDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Unit; @@ -634,13 +671,13 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDJIGSAWGENERATEP pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDLOCKDIFFICULTYPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Bool; /// `net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$Pos#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_POS_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "x", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "y", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "z", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "horizontalCollision", wire: &crate::generated::wire::Wire::I8 }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_POS_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "x", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "y", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "z", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "onGround", wire: &crate::generated::wire::Wire::I8 }]); /// `net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$PosRot#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_POSROT_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "x", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "y", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "z", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "yRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "xRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "horizontalCollision", wire: &crate::generated::wire::Wire::I8 }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_POSROT_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "x", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "y", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "z", wire: &crate::generated::wire::Wire::F64 }, crate::generated::wire::Field { name: "yRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "xRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "onGround", wire: &crate::generated::wire::Wire::I8 }]); /// `net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$Rot#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_ROT_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "yRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "xRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "horizontalCollision", wire: &crate::generated::wire::Wire::I8 }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_ROT_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "yRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "xRot", wire: &crate::generated::wire::Wire::F32 }, crate::generated::wire::Field { name: "onGround", wire: &crate::generated::wire::Wire::I8 }]); /// `net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$StatusOnly#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDMOVEPLAYERPACKET_STATUSONLY_STREAM_CODEC: Wire = crate::generated::wire::Wire::I8; @@ -725,14 +762,50 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDPLAYERACTIONPAC Wire = crate::generated::wire::Wire::Struct(&[ crate::generated::wire::Field { name: "action", - wire: &crate::generated::wire::Wire::VarInt, + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.network.protocol.game.ServerboundPlayerActionPacket$Action", + constants: &[ + crate::generated::wire::Constant { + name: "START_DESTROY_BLOCK", + value: 0, + }, + crate::generated::wire::Constant { + name: "ABORT_DESTROY_BLOCK", + value: 1, + }, + crate::generated::wire::Constant { + name: "STOP_DESTROY_BLOCK", + value: 2, + }, + crate::generated::wire::Constant { + name: "DROP_ALL_ITEMS", + value: 3, + }, + crate::generated::wire::Constant { + name: "DROP_ITEM", + value: 4, + }, + crate::generated::wire::Constant { + name: "RELEASE_USE_ITEM", + value: 5, + }, + crate::generated::wire::Constant { + name: "SWAP_ITEM_WITH_OFFHAND", + value: 6, + }, + crate::generated::wire::Constant { + name: "STAB", + value: 7, + }, + ], + }, }, crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos, }, crate::generated::wire::Field { - name: "get3DDataValue", + name: "direction", wire: &crate::generated::wire::Wire::I8, }, crate::generated::wire::Field { @@ -742,14 +815,14 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDPLAYERACTIONPAC ]); /// `net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDPLAYERCOMMANDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "id", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "action", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "data", wire: &crate::generated::wire::Wire::VarInt }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDPLAYERCOMMANDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "id", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "action", wire: &crate::generated::wire::Wire::Enum { java_class: "net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket$Action", constants: &[crate::generated::wire::Constant { name: "STOP_SLEEPING", value: 0 }, crate::generated::wire::Constant { name: "START_SPRINTING", value: 1 }, crate::generated::wire::Constant { name: "STOP_SPRINTING", value: 2 }, crate::generated::wire::Constant { name: "START_RIDING_JUMP", value: 3 }, crate::generated::wire::Constant { name: "STOP_RIDING_JUMP", value: 4 }, crate::generated::wire::Constant { name: "OPEN_INVENTORY", value: 5 }, crate::generated::wire::Constant { name: "START_FALL_FLYING", value: 6 }] } }, crate::generated::wire::Field { name: "data", wire: &crate::generated::wire::Wire::VarInt }]); /// `net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDPLAYERLOADEDPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Unit; /// `net.minecraft.network.protocol.game.ServerboundRecipeBookChangeSettingsPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDRECIPEBOOKCHANGESETTINGSPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "bookType", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "isOpen", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "isFiltering", wire: &crate::generated::wire::Wire::Bool }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDRECIPEBOOKCHANGESETTINGSPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "bookType", wire: &crate::generated::wire::Wire::Enum { java_class: "net.minecraft.world.inventory.RecipeBookType", constants: &[crate::generated::wire::Constant { name: "CRAFTING", value: 0 }, crate::generated::wire::Constant { name: "FURNACE", value: 1 }, crate::generated::wire::Constant { name: "BLAST_FURNACE", value: 2 }, crate::generated::wire::Constant { name: "SMOKER", value: 3 }] } }, crate::generated::wire::Field { name: "isOpen", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "isFiltering", wire: &crate::generated::wire::Wire::Bool }]); /// `net.minecraft.network.protocol.game.ServerboundRecipeBookSeenRecipePacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDRECIPEBOOKSEENRECIPEPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "recipe", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_ITEM_CRAFTING_DISPLAY_RECIPEDISPLAYID_STREAM_CODEC }]); @@ -798,7 +871,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSETGAMERULEPACK pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSETGAMERULEPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "entries", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSETGAMERULEPACKET_ENTRY_STREAM_CODEC, max: None } }]); /// `net.minecraft.network.protocol.game.ServerboundSetJigsawBlockPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSETJIGSAWBLOCKPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }, crate::generated::wire::Field { name: "name", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "target", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "pool", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "finalState", wire: &crate::generated::wire::Wire::Str { max: Some(32_767) } }, crate::generated::wire::Field { name: "getSerializedName", wire: &crate::generated::wire::Wire::Str { max: Some(32_767) } }, crate::generated::wire::Field { name: "selectionPriority", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "placementPriority", wire: &crate::generated::wire::Wire::VarInt }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSETJIGSAWBLOCKPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }, crate::generated::wire::Field { name: "name", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "target", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "pool", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "finalState", wire: &crate::generated::wire::Wire::Str { max: Some(32_767) } }, crate::generated::wire::Field { name: "joint", wire: &crate::generated::wire::Wire::Str { max: Some(32_767) } }, crate::generated::wire::Field { name: "selectionPriority", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "placementPriority", wire: &crate::generated::wire::Wire::VarInt }]); /// `net.minecraft.world.level.block.state.properties.TestBlockMode#STREAM_CODEC` pub(crate) static NET_MINECRAFT_WORLD_LEVEL_BLOCK_STATE_PROPERTIES_TESTBLOCKMODE_STREAM_CODEC: @@ -812,7 +885,19 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSPECTATORACTION /// `net.minecraft.network.protocol.game.ServerboundSwingPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDSWINGPACKET_STREAM_CODEC: Wire = - crate::generated::wire::Wire::VarInt; + crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.world.InteractionHand", + constants: &[ + crate::generated::wire::Constant { + name: "MAIN_HAND", + value: 0, + }, + crate::generated::wire::Constant { + name: "OFF_HAND", + value: 1, + }, + ], + }; /// `net.minecraft.network.protocol.game.ServerboundTeleportToEntityPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDTELEPORTTOENTITYPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Uuid; @@ -824,15 +909,15 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDTESTINSTANCEBLO pub(crate) static NET_MINECRAFT_CORE_VEC3I_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[ crate::generated::wire::Field { - name: "getX", + name: "x", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT, }, crate::generated::wire::Field { - name: "getY", + name: "y", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT, }, crate::generated::wire::Field { - name: "getZ", + name: "z", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT, }, ]); @@ -859,7 +944,19 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDUSEITEMONPACKET Wire = crate::generated::wire::Wire::Struct(&[ crate::generated::wire::Field { name: "hand", - wire: &crate::generated::wire::Wire::VarInt, + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.world.InteractionHand", + constants: &[ + crate::generated::wire::Constant { + name: "MAIN_HAND", + value: 0, + }, + crate::generated::wire::Constant { + name: "OFF_HAND", + value: 1, + }, + ], + }, }, crate::generated::wire::Field { name: "blockHit", @@ -876,7 +973,19 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_SERVERBOUNDUSEITEMPACKET_S crate::generated::wire::Wire::Struct(&[ crate::generated::wire::Field { name: "hand", - wire: &crate::generated::wire::Wire::VarInt, + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.world.InteractionHand", + constants: &[ + crate::generated::wire::Constant { + name: "MAIN_HAND", + value: 0, + }, + crate::generated::wire::Constant { + name: "OFF_HAND", + value: 1, + }, + ], + }, }, crate::generated::wire::Field { name: "sequence", @@ -920,7 +1029,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_TRUSTED_COMPOUND_TAG crate::generated::wire::Wire::Nbt; /// `net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDBLOCKENTITYDATAPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "getPos", wire: &crate::generated::wire::NET_MINECRAFT_CORE_BLOCKPOS_STREAM_CODEC }, crate::generated::wire::Field { name: "getType", wire: &crate::generated::wire::Wire::RegistryId { registry: "minecraft:block_entity_type" } }, crate::generated::wire::Field { name: "getTag", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_TRUSTED_COMPOUND_TAG }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDBLOCKENTITYDATAPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::NET_MINECRAFT_CORE_BLOCKPOS_STREAM_CODEC }, crate::generated::wire::Field { name: "type", wire: &crate::generated::wire::Wire::RegistryId { registry: "minecraft:block_entity_type" } }, crate::generated::wire::Field { name: "tag", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_TRUSTED_COMPOUND_TAG }]); /// `net.minecraft.network.protocol.game.ClientboundBlockEventPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDBLOCKEVENTPACKET_STREAM_CODEC: @@ -949,11 +1058,11 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDBLOCKEVENTPACKE pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDBLOCKUPDATEPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[ crate::generated::wire::Field { - name: "getPos", + name: "pos", wire: &crate::generated::wire::NET_MINECRAFT_CORE_BLOCKPOS_STREAM_CODEC, }, crate::generated::wire::Field { - name: "getBlockState", + name: "blockState", wire: &crate::generated::wire::Wire::VarInt, }, ]); @@ -1008,7 +1117,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDCOOLDOWNPACKET_ ]); /// `net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDCUSTOMCHATCOMPLETIONSPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "action", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "entries", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::Wire::Str { max: Some(32_767) }, max: None } }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDCUSTOMCHATCOMPLETIONSPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "action", wire: &crate::generated::wire::Wire::Enum { java_class: "net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket$Action", constants: &[crate::generated::wire::Constant { name: "ADD", value: 0 }, crate::generated::wire::Constant { name: "REMOVE", value: 1 }, crate::generated::wire::Constant { name: "SET", value: 2 }] } }, crate::generated::wire::Field { name: "entries", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::Wire::Str { max: Some(32_767) }, max: None } }]); /// `net.minecraft.world.damagesource.DamageType#STREAM_CODEC` pub(crate) static NET_MINECRAFT_WORLD_DAMAGESOURCE_DAMAGETYPE_STREAM_CODEC: Wire = @@ -1063,7 +1172,13 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDDEBUGSAMPLEPACK }, crate::generated::wire::Field { name: "debugSampleType", - wire: &crate::generated::wire::Wire::VarInt, + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.util.debugchart.RemoteDebugSampleType", + constants: &[crate::generated::wire::Constant { + name: "TICK_TIME", + value: 0, + }], + }, }, ]); @@ -1117,7 +1232,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDFORGETLEVELCHUN pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDGAMEEVENTPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[ crate::generated::wire::Field { - name: "id", + name: "event", wire: &crate::generated::wire::Wire::I8, }, crate::generated::wire::Field { @@ -1175,7 +1290,7 @@ pub(crate) static NET_MINECRAFT_WORLD_LEVEL_DIMENSION_DIMENSIONTYPE_STREAM_CODEC }; /// `net.minecraft.network.protocol.game.ClientboundLoginPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDLOGINPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "playerId", wire: &crate::generated::wire::Wire::I32 }, crate::generated::wire::Field { name: "hardcore", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "levels", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::Wire::Identifier, max: None } }, crate::generated::wire::Field { name: "maxPlayers", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "chunkRadius", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "simulationDistance", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "reducedDebugInfo", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "showDeathScreen", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "doLimitedCrafting", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "commonPlayerSpawnInfo", wire: &crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimensionType", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_LEVEL_DIMENSION_DIMENSIONTYPE_STREAM_CODEC }, crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "seed", wire: &crate::generated::wire::Wire::I64 }, crate::generated::wire::Field { name: "getId", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "previousGameType", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "isDebug", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "isFlat", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "lastDeathLocation", wire: &crate::generated::wire::Wire::Optional(&crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }])) }, crate::generated::wire::Field { name: "portalCooldown", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "seaLevel", wire: &crate::generated::wire::Wire::VarInt }]) }, crate::generated::wire::Field { name: "onlineMode", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "enforcesSecureChat", wire: &crate::generated::wire::Wire::Bool }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDLOGINPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "playerId", wire: &crate::generated::wire::Wire::I32 }, crate::generated::wire::Field { name: "hardcore", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "levels", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::Wire::Identifier, max: None } }, crate::generated::wire::Field { name: "maxPlayers", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "chunkRadius", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "simulationDistance", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "reducedDebugInfo", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "showDeathScreen", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "doLimitedCrafting", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "commonPlayerSpawnInfo", wire: &crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimensionType", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_LEVEL_DIMENSION_DIMENSIONTYPE_STREAM_CODEC }, crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "seed", wire: &crate::generated::wire::Wire::I64 }, crate::generated::wire::Field { name: "gameType", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "previousGameType", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "isDebug", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "isFlat", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "lastDeathLocation", wire: &crate::generated::wire::Wire::Optional(&crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }])) }, crate::generated::wire::Field { name: "portalCooldown", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "seaLevel", wire: &crate::generated::wire::Wire::VarInt }]) }, crate::generated::wire::Field { name: "onlineMode", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "enforcesSecureChat", wire: &crate::generated::wire::Wire::Bool }]); /// `net.minecraft.network.protocol.game.ClientboundLowDiskSpaceWarningPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDLOWDISKSPACEWARNINGPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Unit; @@ -1214,10 +1329,22 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDMOVEVEHICLEPACK /// `net.minecraft.network.protocol.game.ClientboundOpenBookPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDOPENBOOKPACKET_STREAM_CODEC: Wire = - crate::generated::wire::Wire::VarInt; + crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.world.InteractionHand", + constants: &[ + crate::generated::wire::Constant { + name: "MAIN_HAND", + value: 0, + }, + crate::generated::wire::Constant { + name: "OFF_HAND", + value: 1, + }, + ], + }; /// `net.minecraft.network.protocol.game.ClientboundOpenScreenPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDOPENSCREENPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "getContainerId", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_CONTAINER_ID }, crate::generated::wire::Field { name: "getType", wire: &crate::generated::wire::Wire::RegistryId { registry: "minecraft:menu" } }, crate::generated::wire::Field { name: "getTitle", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CHAT_COMPONENTSERIALIZATION_TRUSTED_STREAM_CODEC }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDOPENSCREENPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "containerId", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_CONTAINER_ID }, crate::generated::wire::Field { name: "type", wire: &crate::generated::wire::Wire::RegistryId { registry: "minecraft:menu" } }, crate::generated::wire::Field { name: "title", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CHAT_COMPONENTSERIALIZATION_TRUSTED_STREAM_CODEC }]); /// `net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDOPENSIGNEDITORPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }, crate::generated::wire::Field { name: "isFrontText", wire: &crate::generated::wire::Wire::Bool }]); @@ -1257,7 +1384,7 @@ pub(crate) static NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_COD ]); /// `net.minecraft.stats.RecipeBookSettings#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "o -> o.crafting", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }, crate::generated::wire::Field { name: "o -> o.furnace", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }, crate::generated::wire::Field { name: "o -> o.blastFurnace", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }, crate::generated::wire::Field { name: "o -> o.smoker", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }]); +pub(crate) static NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "crafting", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }, crate::generated::wire::Field { name: "furnace", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }, crate::generated::wire::Field { name: "blastFurnace", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }, crate::generated::wire::Field { name: "smoker", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_TYPESETTINGS_STREAM_CODEC }]); /// `net.minecraft.network.protocol.game.ClientboundRecipeBookSettingsPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDRECIPEBOOKSETTINGSPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "bookSettings", wire: &crate::generated::wire::NET_MINECRAFT_STATS_RECIPEBOOKSETTINGS_STREAM_CODEC }]); @@ -1284,7 +1411,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDRESETSCOREPACKE ]); /// `net.minecraft.network.protocol.game.ClientboundRespawnPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDRESPAWNPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "commonPlayerSpawnInfo", wire: &crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimensionType", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_LEVEL_DIMENSION_DIMENSIONTYPE_STREAM_CODEC }, crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "seed", wire: &crate::generated::wire::Wire::I64 }, crate::generated::wire::Field { name: "getId", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "previousGameType", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "isDebug", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "isFlat", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "lastDeathLocation", wire: &crate::generated::wire::Wire::Optional(&crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }])) }, crate::generated::wire::Field { name: "portalCooldown", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "seaLevel", wire: &crate::generated::wire::Wire::VarInt }]) }, crate::generated::wire::Field { name: "dataToKeep", wire: &crate::generated::wire::Wire::I8 }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDRESPAWNPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "commonPlayerSpawnInfo", wire: &crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimensionType", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_LEVEL_DIMENSION_DIMENSIONTYPE_STREAM_CODEC }, crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "seed", wire: &crate::generated::wire::Wire::I64 }, crate::generated::wire::Field { name: "gameType", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "previousGameType", wire: &crate::generated::wire::Wire::I8 }, crate::generated::wire::Field { name: "isDebug", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "isFlat", wire: &crate::generated::wire::Wire::Bool }, crate::generated::wire::Field { name: "lastDeathLocation", wire: &crate::generated::wire::Wire::Optional(&crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "dimension", wire: &crate::generated::wire::Wire::Identifier }, crate::generated::wire::Field { name: "pos", wire: &crate::generated::wire::Wire::BlockPos }])) }, crate::generated::wire::Field { name: "portalCooldown", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "seaLevel", wire: &crate::generated::wire::Wire::VarInt }]) }, crate::generated::wire::Field { name: "dataToKeep", wire: &crate::generated::wire::Wire::I8 }]); /// `net.minecraft.network.protocol.game.ClientboundRotateHeadPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDROTATEHEADPACKET_STREAM_CODEC: @@ -1483,7 +1610,55 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDSOUNDENTITYPACK }, crate::generated::wire::Field { name: "source", - wire: &crate::generated::wire::Wire::VarInt, + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.sounds.SoundSource", + constants: &[ + crate::generated::wire::Constant { + name: "MASTER", + value: 0, + }, + crate::generated::wire::Constant { + name: "MUSIC", + value: 1, + }, + crate::generated::wire::Constant { + name: "RECORDS", + value: 2, + }, + crate::generated::wire::Constant { + name: "WEATHER", + value: 3, + }, + crate::generated::wire::Constant { + name: "BLOCKS", + value: 4, + }, + crate::generated::wire::Constant { + name: "HOSTILE", + value: 5, + }, + crate::generated::wire::Constant { + name: "NEUTRAL", + value: 6, + }, + crate::generated::wire::Constant { + name: "PLAYERS", + value: 7, + }, + crate::generated::wire::Constant { + name: "AMBIENT", + value: 8, + }, + crate::generated::wire::Constant { + name: "VOICE", + value: 9, + }, + crate::generated::wire::Constant { + name: "UI", + value: 10, + }, + ], + }, }, crate::generated::wire::Field { name: "id", @@ -1512,7 +1687,55 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDSOUNDPACKET_STR }, crate::generated::wire::Field { name: "source", - wire: &crate::generated::wire::Wire::VarInt, + wire: &crate::generated::wire::Wire::Enum { + java_class: "net.minecraft.sounds.SoundSource", + constants: &[ + crate::generated::wire::Constant { + name: "MASTER", + value: 0, + }, + crate::generated::wire::Constant { + name: "MUSIC", + value: 1, + }, + crate::generated::wire::Constant { + name: "RECORDS", + value: 2, + }, + crate::generated::wire::Constant { + name: "WEATHER", + value: 3, + }, + crate::generated::wire::Constant { + name: "BLOCKS", + value: 4, + }, + crate::generated::wire::Constant { + name: "HOSTILE", + value: 5, + }, + crate::generated::wire::Constant { + name: "NEUTRAL", + value: 6, + }, + crate::generated::wire::Constant { + name: "PLAYERS", + value: 7, + }, + crate::generated::wire::Constant { + name: "AMBIENT", + value: 8, + }, + crate::generated::wire::Constant { + name: "VOICE", + value: 9, + }, + crate::generated::wire::Constant { + name: "UI", + value: 10, + }, + ], + }, }, crate::generated::wire::Field { name: "x", @@ -1608,7 +1831,7 @@ pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTE pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTESPACKET_ATTRIBUTESNAPSHOT_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "attribute", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_ENTITY_AI_ATTRIBUTES_ATTRIBUTE_STREAM_CODEC }, crate::generated::wire::Field { name: "base", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_DOUBLE }, crate::generated::wire::Field { name: "modifiers", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTESPACKET_ATTRIBUTESNAPSHOT_MODIFIER_STREAM_CODEC, max: None } }]); /// `net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTESPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "getEntityId", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT }, crate::generated::wire::Field { name: "getValues", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTESPACKET_ATTRIBUTESNAPSHOT_STREAM_CODEC, max: Some(128) } }]); +pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTESPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "entityId", wire: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT }, crate::generated::wire::Field { name: "values", wire: &crate::generated::wire::Wire::List { element: &crate::generated::wire::NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEATTRIBUTESPACKET_ATTRIBUTESNAPSHOT_STREAM_CODEC, max: Some(128) } }]); /// `net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket#STREAM_CODEC` pub(crate) static NET_MINECRAFT_NETWORK_PROTOCOL_GAME_CLIENTBOUNDUPDATEMOBEFFECTPACKET_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "entityId", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "effect", wire: &crate::generated::wire::NET_MINECRAFT_WORLD_EFFECT_MOBEFFECT_STREAM_CODEC }, crate::generated::wire::Field { name: "effectAmplifier", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "effectDurationTicks", wire: &crate::generated::wire::Wire::VarInt }, crate::generated::wire::Field { name: "flags", wire: &crate::generated::wire::Wire::I8 }]); @@ -1658,7 +1881,7 @@ pub(crate) static NET_MINECRAFT_WORLD_ITEM_ENCHANTMENT_ENCHANTMENT_STREAM_CODEC: }; /// `net.minecraft.world.item.enchantment.ItemEnchantments#STREAM_CODEC` -pub(crate) static NET_MINECRAFT_WORLD_ITEM_ENCHANTMENT_ITEMENCHANTMENTS_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "c -> c.enchantments", wire: &crate::generated::wire::Wire::Map { key: &crate::generated::wire::NET_MINECRAFT_WORLD_ITEM_ENCHANTMENT_ENCHANTMENT_STREAM_CODEC, value: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT } }]); +pub(crate) static NET_MINECRAFT_WORLD_ITEM_ENCHANTMENT_ITEMENCHANTMENTS_STREAM_CODEC: Wire = crate::generated::wire::Wire::Struct(&[crate::generated::wire::Field { name: "enchantments", wire: &crate::generated::wire::Wire::Map { key: &crate::generated::wire::NET_MINECRAFT_WORLD_ITEM_ENCHANTMENT_ENCHANTMENT_STREAM_CODEC, value: &crate::generated::wire::NET_MINECRAFT_NETWORK_CODEC_BYTEBUFCODECS_VAR_INT } }]); /// `net.minecraft.world.item.component.CustomModelData#STREAM_CODEC` pub(crate) static NET_MINECRAFT_WORLD_ITEM_COMPONENT_CUSTOMMODELDATA_STREAM_CODEC: Wire = diff --git a/crates/hyperion-minecraft-proto/src/lib.rs b/crates/hyperion-minecraft-proto/src/lib.rs index 459fe226d..e8b7f3823 100644 --- a/crates/hyperion-minecraft-proto/src/lib.rs +++ b/crates/hyperion-minecraft-proto/src/lib.rs @@ -1,12 +1,28 @@ //! Minecraft Java Edition wire protocol, generated from Mojang's own data. //! -//! The version tables under [`generated`] come out of the vanilla data -//! generator and the decompiled server sources; the codecs under [`packets`] -//! are hand-written against those same sources. Nothing here depends on a -//! third-party protocol crate. -//! //! Since Minecraft 26.1 the server ships unobfuscated, so every name in this //! crate is Mojang's own name and greps directly against the server jar. +//! +//! Three things come out of the jar and none of them is transcribed by hand: +//! +//! * `nix/extract-protocol.py` follows the server's own stream codecs down to +//! the bytes and writes [`protocol.json`], which is committed beside this +//! crate and regenerated by `nix run .#sync-minecraft-proto`. +//! * `build.rs` turns the layouts it recovered in full into the packet structs +//! under [`packets`]. A layout the extractor could not follow all the way +//! has no struct at all, so a generated codec is complete by construction. +//! * The version numbers, packet id tables and registries under [`generated`] +//! are committed Rust, because they restate data rather than a wire contract +//! and a reader wants to grep them. +//! +//! Packets the generator cannot produce are hand-written in the same modules, +//! against the same decompiled sources. +//! +//! [`protocol.json`]: https://github.com/andrewgazelka/hyperion/blob/main/crates/hyperion-minecraft-proto/protocol.json + +// The derives emit `::hyperion_minecraft_proto` paths so that they work from +// any crate; this is what makes them resolve inside this one too. +extern crate self as hyperion_minecraft_proto; pub mod codec; pub mod generated; @@ -14,12 +30,18 @@ pub mod item; pub mod nbt; pub mod packets; pub mod text; +pub mod types; mod error; pub use codec::{Reader, Writer}; pub use error::{Error, Result}; pub use generated::{MINECRAFT_VERSION, PROTOCOL_VERSION, WORLD_VERSION}; +pub use hyperion_minecraft_proto_derive::{Decode, Encode}; +pub use types::{ + BlockHitResult, BlockPos, ChunkPos, Direction, Either, Holder, Identifier, LengthPrefixed, + RegistryId, Uuid, VarInt, VarLong, +}; /// A value that can be written to the wire. pub trait Encode { diff --git a/crates/hyperion-minecraft-proto/src/nbt/mod.rs b/crates/hyperion-minecraft-proto/src/nbt/mod.rs index eded0bda2..1db2b8240 100644 --- a/crates/hyperion-minecraft-proto/src/nbt/mod.rs +++ b/crates/hyperion-minecraft-proto/src/nbt/mod.rs @@ -701,3 +701,28 @@ const fn ensure_available(reader: &Reader<'_>, count: usize, stride: usize) -> R } Ok(()) } + +/// The `TAG_End`-means-absent form, as a `#[proto(with = ...)]` module. +/// +/// `FriendlyByteBuf.writeNbt` maps a null tag to `TAG_End`, so a nullable NBT +/// field is not the boolean-prefixed [`Option`] the derive writes by default. +pub mod optional { + use super::{Tag, decode_optional, encode_optional}; + use crate::{Reader, Result, Writer}; + + /// Write `TAG_End` for an absent tag, the tag itself otherwise. + /// + /// # Errors + /// Returns an error when the tag itself cannot be written. + pub fn encode(value: &Option>, writer: &mut Writer) -> Result<()> { + encode_optional(value.as_ref(), writer) + } + + /// Read a tag, mapping a root `TAG_End` to absence. + /// + /// # Errors + /// Returns an error on a malformed tag or truncated input. + pub fn decode<'a>(reader: &mut Reader<'a>) -> Result>> { + decode_optional(reader) + } +} diff --git a/crates/hyperion-minecraft-proto/src/packets/common.rs b/crates/hyperion-minecraft-proto/src/packets/common.rs new file mode 100644 index 000000000..c7306d4f3 --- /dev/null +++ b/crates/hyperion-minecraft-proto/src/packets/common.rs @@ -0,0 +1,16 @@ +//! Packets more than one state sends. +//! +//! `net.minecraft.network.protocol.common` and its neighbours hold packet +//! classes that several protocol states register. One Java class is one Rust +//! type here too, re-exported into each state that sends it, so a value built +//! for one state is the same value in another. + +/// Packets the server sends. +pub mod clientbound { + include!(concat!(env!("OUT_DIR"), "/packets/common_clientbound.rs")); +} + +/// Packets the client sends. +pub mod serverbound { + include!(concat!(env!("OUT_DIR"), "/packets/common_serverbound.rs")); +} diff --git a/crates/hyperion-minecraft-proto/src/packets/configuration.rs b/crates/hyperion-minecraft-proto/src/packets/configuration.rs index fe60a6422..8c8f012de 100644 --- a/crates/hyperion-minecraft-proto/src/packets/configuration.rs +++ b/crates/hyperion-minecraft-proto/src/packets/configuration.rs @@ -781,3 +781,22 @@ empty_packet! { // `play_login` has unit packets of its own and shares the definition. pub(super) use empty_packet; + +/// Packets the server sends, generated from `protocol.json`. +/// +/// See this module's own note: several of these are also defined above by +/// hand, and these are the ones to keep. +pub mod clientbound { + include!(concat!( + env!("OUT_DIR"), + "/packets/configuration_clientbound.rs" + )); +} + +/// Packets the client sends, generated from `protocol.json`. +pub mod serverbound { + include!(concat!( + env!("OUT_DIR"), + "/packets/configuration_serverbound.rs" + )); +} diff --git a/crates/hyperion-minecraft-proto/src/packets/handshake.rs b/crates/hyperion-minecraft-proto/src/packets/handshake.rs index de068cb64..6a151acfd 100644 --- a/crates/hyperion-minecraft-proto/src/packets/handshake.rs +++ b/crates/hyperion-minecraft-proto/src/packets/handshake.rs @@ -1,86 +1,20 @@ -//! Handshake state. One packet, sent by the client before anything else. - -use crate::{Decode, Encode, Error, Reader, Result, Writer}; - -/// What the client wants to do after the handshake. -/// -/// Discriminants are from `net.minecraft.network.protocol.handshake.ClientIntent`. -/// Note they start at one, not zero. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(i32)] -pub enum ClientIntent { - /// Query the server list ping. - Status = 1, - /// Log in and play. - Login = 2, - /// Arrive via a transfer from another server. - Transfer = 3, -} - -impl ClientIntent { - /// Resolve a wire discriminant. - /// - /// # Errors - /// Returns [`Error::InvalidEnum`] for anything outside 1..=3, matching - /// `ClientIntent.byId` throwing on an unknown id. - pub const fn from_raw(value: i32) -> Result { - match value { - 1 => Ok(Self::Status), - 2 => Ok(Self::Login), - 3 => Ok(Self::Transfer), - _ => Err(Error::InvalidEnum { - name: "ClientIntent", - value, - }), - } - } - - /// The wire discriminant. - #[must_use] - pub const fn to_raw(self) -> i32 { - self as i32 - } -} - -/// `minecraft:intention`, id 0 (`ClientIntentionPacket`). -/// -/// The one packet whose layout is frozen across protocol versions, because a -/// server has to read `protocol_version` before it knows which version it is -/// speaking. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Intention<'a> { - /// Protocol number the client speaks. - pub protocol_version: i32, - /// Host name the client connected to, as typed. - pub host_name: &'a str, - /// Port the client connected to. - pub port: u16, - /// What the client intends to do next. - pub intention: ClientIntent, -} - -/// `ClientIntentionPacket.MAX_HOST_LENGTH`. -const MAX_HOST_LENGTH: usize = 255; - -impl Encode for Intention<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.var_int(self.protocol_version); - // The server writes without a limit here and enforces it on read; the - // limit is applied on both sides so a malformed value cannot leave. - writer.string_with_limit(self.host_name, MAX_HOST_LENGTH)?; - writer.u16(self.port); - writer.var_int(self.intention.to_raw()); - Ok(()) - } +//! Handshake state: one packet, sent before anything else. +//! +//! Its layout is frozen across protocol versions, because a server has to +//! read `protocol_version` before it knows which version it is speaking. + +/// Nothing is sent clientbound in this state. +pub mod clientbound { + include!(concat!( + env!("OUT_DIR"), + "/packets/handshake_clientbound.rs" + )); } -impl<'a> Decode<'a> for Intention<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - protocol_version: reader.var_int()?, - host_name: reader.string_with_limit(MAX_HOST_LENGTH)?, - port: reader.u16()?, - intention: ClientIntent::from_raw(reader.var_int()?)?, - }) - } +/// Packets the client sends. +pub mod serverbound { + include!(concat!( + env!("OUT_DIR"), + "/packets/handshake_serverbound.rs" + )); } diff --git a/crates/hyperion-minecraft-proto/src/packets/login.rs b/crates/hyperion-minecraft-proto/src/packets/login.rs index 59b2f5328..820c51486 100644 --- a/crates/hyperion-minecraft-proto/src/packets/login.rs +++ b/crates/hyperion-minecraft-proto/src/packets/login.rs @@ -1,186 +1,11 @@ //! Login state: authentication and the handover into configuration. -use crate::{Decode, Encode, Reader, Result, Writer}; - -/// `minecraft:hello`, serverbound id 0 (`ServerboundHelloPacket`). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Hello<'a> { - /// Player name, capped at 16 characters by the server. - pub name: &'a str, - /// Offline-mode or cached profile id. - pub profile_id: u128, -} - -/// `ServerboundHelloPacket` reads the name with a 16-character limit. -const MAX_NAME_LENGTH: usize = 16; - -impl Encode for Hello<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.string_with_limit(self.name, MAX_NAME_LENGTH)?; - writer.uuid(self.profile_id); - Ok(()) - } -} - -impl<'a> Decode<'a> for Hello<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - name: reader.string_with_limit(MAX_NAME_LENGTH)?, - profile_id: reader.uuid()?, - }) - } -} - -/// `minecraft:hello`, clientbound id 1 (`ClientboundHelloPacket`). -/// -/// Carries the encryption request. `should_authenticate` was added when Mojang -/// split online-mode enforcement from the presence of encryption. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HelloRequest<'a> { - /// Server id string, capped at 20 characters. - pub server_id: &'a str, - /// DER-encoded public key. - pub public_key: &'a [u8], - /// Verify token the client must return encrypted. - pub challenge: &'a [u8], - /// Whether the client should complete the session-server handshake. - pub should_authenticate: bool, -} - -/// `ClientboundHelloPacket` reads the server id with a 20-character limit. -const MAX_SERVER_ID_LENGTH: usize = 20; - -impl Encode for HelloRequest<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.string_with_limit(self.server_id, MAX_SERVER_ID_LENGTH)?; - writer.byte_array(self.public_key)?; - writer.byte_array(self.challenge)?; - writer.bool(self.should_authenticate); - Ok(()) - } -} - -impl<'a> Decode<'a> for HelloRequest<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - server_id: reader.string_with_limit(MAX_SERVER_ID_LENGTH)?, - public_key: reader.byte_array()?, - challenge: reader.byte_array()?, - should_authenticate: reader.bool()?, - }) - } -} - -/// `minecraft:key`, serverbound id 1 (`ServerboundKeyPacket`). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Key<'a> { - /// Shared secret, encrypted with the server's public key. - pub key_bytes: &'a [u8], - /// The challenge from the hello, encrypted the same way. - pub encrypted_challenge: &'a [u8], -} - -impl Encode for Key<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.byte_array(self.key_bytes)?; - writer.byte_array(self.encrypted_challenge)?; - Ok(()) - } -} - -impl<'a> Decode<'a> for Key<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - key_bytes: reader.byte_array()?, - encrypted_challenge: reader.byte_array()?, - }) - } -} - -/// `minecraft:login_compression`, clientbound id 3 (`ClientboundLoginCompressionPacket`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LoginCompression { - /// Packets at or above this size are compressed. Negative disables it. - pub compression_threshold: i32, -} - -impl Encode for LoginCompression { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.var_int(self.compression_threshold); - Ok(()) - } -} - -impl Decode<'_> for LoginCompression { - fn decode(reader: &mut Reader<'_>) -> Result { - Ok(Self { - compression_threshold: reader.var_int()?, - }) - } -} - -/// `minecraft:login_disconnect`, clientbound id 0 (`ClientboundLoginDisconnectPacket`). -/// -/// The reason is JSON rather than the NBT-encoded component used after login, -/// because at this point no registries have been sent. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LoginDisconnect<'a> { - /// Disconnect reason, as a JSON chat component. - pub reason: &'a str, -} - -/// `ByteBufCodecs.lenientJson(262144)` in `ClientboundLoginDisconnectPacket`. -const MAX_REASON_LENGTH: usize = 262_144; - -impl Encode for LoginDisconnect<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.string_with_limit(self.reason, MAX_REASON_LENGTH) - } -} - -impl<'a> Decode<'a> for LoginDisconnect<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - reason: reader.string_with_limit(MAX_REASON_LENGTH)?, - }) - } -} - -/// `minecraft:login_acknowledged`, serverbound id 3 (`ServerboundLoginAcknowledgedPacket`). -/// -/// Empty on the wire; moves the connection into the configuration state. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct LoginAcknowledged; - -impl Encode for LoginAcknowledged { - fn encode(&self, _writer: &mut Writer) -> Result<()> { - Ok(()) - } -} - -impl Decode<'_> for LoginAcknowledged { - fn decode(_reader: &mut Reader<'_>) -> Result { - Ok(Self) - } -} - -/// `minecraft:cookie_request`, clientbound id 5 (`ClientboundCookieRequestPacket`). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CookieRequest<'a> { - /// Identifier of the cookie being requested. - pub key: &'a str, -} - -impl Encode for CookieRequest<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.string(self.key) - } +/// Packets the server sends. +pub mod clientbound { + include!(concat!(env!("OUT_DIR"), "/packets/login_clientbound.rs")); } -impl<'a> Decode<'a> for CookieRequest<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - key: reader.string()?, - }) - } +/// Packets the client sends. +pub mod serverbound { + include!(concat!(env!("OUT_DIR"), "/packets/login_serverbound.rs")); } diff --git a/crates/hyperion-minecraft-proto/src/packets/mod.rs b/crates/hyperion-minecraft-proto/src/packets/mod.rs index dc73dc616..884dbadba 100644 --- a/crates/hyperion-minecraft-proto/src/packets/mod.rs +++ b/crates/hyperion-minecraft-proto/src/packets/mod.rs @@ -1,15 +1,32 @@ -//! Packet bodies. +//! Packet bodies, one module per connection state. //! -//! Layouts are transcribed from the decompiled server sources for this -//! protocol version. Each packet carries the name of the class it came from so -//! a reader can check it against the jar. +//! Layouts the extractor recovered in full are generated from `protocol.json` +//! by `build.rs` and spliced in with `include!`; the rest are hand-written in +//! the same modules, against the same decompiled sources. Which is which is +//! visible in the source: a generated file names the codec it came from in +//! every doc comment. //! -//! Handshake, status, login and configuration are complete. [`play_login`] -//! covers only the join sequence; the rest of play is not implemented. See -//! `docs/minecraft-26.2-migration.md` for what remains. +//! Ids restart at zero in every state and direction, so a struct here is only +//! meaningful alongside the matching [`crate::generated::packet_id`] entry. +//! +//! # Two definitions of eleven packets, for now +//! +//! [`configuration`] and [`play_login`] were hand-written against the same +//! decompiled sources, in parallel with this generator, and landed first. +//! Eleven of their packets are now generated as well, so the crate defines +//! each of those twice: once flat in the hand-written module and once under +//! the state's `clientbound` or `serverbound` module. +//! +//! The generated one is the one to keep, and the reconciliation is the next +//! change rather than this one -- it means rewriting 725 lines of tests that +//! name the hand-written types, which is a review of its own. What is *not* +//! duplicated is the part the generator cannot do: `ClientInformation`, +//! `CustomPayload` and `UpdateTags` all branch on a runtime value. +pub mod common; pub mod configuration; pub mod handshake; pub mod login; +pub mod play; pub mod play_login; pub mod status; diff --git a/crates/hyperion-minecraft-proto/src/packets/play.rs b/crates/hyperion-minecraft-proto/src/packets/play.rs new file mode 100644 index 000000000..824b9933c --- /dev/null +++ b/crates/hyperion-minecraft-proto/src/packets/play.rs @@ -0,0 +1,11 @@ +//! Play state: everything after the world is loaded. + +/// Packets the server sends. +pub mod clientbound { + include!(concat!(env!("OUT_DIR"), "/packets/play_clientbound.rs")); +} + +/// Packets the client sends. +pub mod serverbound { + include!(concat!(env!("OUT_DIR"), "/packets/play_serverbound.rs")); +} diff --git a/crates/hyperion-minecraft-proto/src/packets/status.rs b/crates/hyperion-minecraft-proto/src/packets/status.rs index 578c1fa47..165d14e46 100644 --- a/crates/hyperion-minecraft-proto/src/packets/status.rs +++ b/crates/hyperion-minecraft-proto/src/packets/status.rs @@ -1,90 +1,11 @@ //! Status state: the server list ping exchange. -use crate::{Decode, Encode, Reader, Result, Writer}; - -/// `minecraft:status_request`, serverbound id 0 (`ServerboundStatusRequestPacket`). -/// -/// Empty on the wire; the server's codec is `StreamCodec.unit`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct StatusRequest; - -impl Encode for StatusRequest { - fn encode(&self, _writer: &mut Writer) -> Result<()> { - Ok(()) - } -} - -impl Decode<'_> for StatusRequest { - fn decode(_reader: &mut Reader<'_>) -> Result { - Ok(Self) - } -} - -/// `minecraft:status_response`, clientbound id 0 (`ClientboundStatusResponsePacket`). -/// -/// The body is a JSON document capped at `Short.MAX_VALUE` characters. The -/// server parses it into `ServerStatus` with a DFU codec, but that shapes only -/// the in-memory value: on the wire it is a length-prefixed string. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StatusResponse<'a> { - /// Server status as a JSON document. - pub status: &'a str, -} - -impl Encode for StatusResponse<'_> { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.string(self.status) - } -} - -impl<'a> Decode<'a> for StatusResponse<'a> { - fn decode(reader: &mut Reader<'a>) -> Result { - Ok(Self { - status: reader.string()?, - }) - } -} - -/// `minecraft:ping_request`, serverbound id 1 (`ServerboundPingRequestPacket`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PingRequest { - /// Opaque value echoed back in the pong. - pub time: i64, -} - -impl Encode for PingRequest { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.i64(self.time); - Ok(()) - } -} - -impl Decode<'_> for PingRequest { - fn decode(reader: &mut Reader<'_>) -> Result { - Ok(Self { - time: reader.i64()?, - }) - } -} - -/// `minecraft:pong_response`, clientbound id 1 (`ClientboundPongResponsePacket`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PongResponse { - /// The value from the matching ping. - pub time: i64, -} - -impl Encode for PongResponse { - fn encode(&self, writer: &mut Writer) -> Result<()> { - writer.i64(self.time); - Ok(()) - } +/// Packets the server sends. +pub mod clientbound { + include!(concat!(env!("OUT_DIR"), "/packets/status_clientbound.rs")); } -impl Decode<'_> for PongResponse { - fn decode(reader: &mut Reader<'_>) -> Result { - Ok(Self { - time: reader.i64()?, - }) - } +/// Packets the client sends. +pub mod serverbound { + include!(concat!(env!("OUT_DIR"), "/packets/status_serverbound.rs")); } diff --git a/crates/hyperion-minecraft-proto/src/types.rs b/crates/hyperion-minecraft-proto/src/types.rs new file mode 100644 index 000000000..39c4c2b2e --- /dev/null +++ b/crates/hyperion-minecraft-proto/src/types.rs @@ -0,0 +1,601 @@ +//! The value types a packet field can have. +//! +//! Each one exists because the wire distinguishes something Rust's primitives +//! do not. A `VarInt` and an `i32` are both 32-bit signed integers and are +//! different numbers of bytes; a [`BlockPos`] and an `i64` are the same eight +//! bytes and mean different things. Giving each its own type is what lets +//! `#[derive(Encode, Decode)]` read a field's wire form off its Rust type +//! instead of being told. + +use std::fmt; + +use crate::{Decode, Encode, Error, Reader, Result, Writer, codec}; + +/// A 32-bit signed integer written seven bits per byte. +/// +/// Most integers in the protocol are this rather than a fixed four bytes, +/// which is why it is the type that appears and `i32` is the exception. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct VarInt(pub i32); + +/// A 64-bit signed integer written seven bits per byte. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct VarLong(pub i64); + +/// A UUID, written as two big-endian 64-bit halves, most significant first. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Uuid(pub u128); + +/// An index into a synced registry, written as a `VarInt`. +/// +/// Which registry it indexes is a property of the field, not of the value, so +/// it lives in the field's documentation rather than in the type. Resolve one +/// against the tables in [`crate::generated::registry`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct RegistryId(pub i32); + +macro_rules! var_codec { + ($name:ident, $inner:ty, $write:ident, $read:ident) => { + impl From<$inner> for $name { + fn from(value: $inner) -> Self { + Self(value) + } + } + + impl From<$name> for $inner { + fn from(value: $name) -> Self { + value.0 + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } + } + + impl Encode for $name { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.$write(self.0); + Ok(()) + } + } + + impl Decode<'_> for $name { + fn decode(reader: &mut Reader<'_>) -> Result { + reader.$read().map(Self) + } + } + }; +} + +var_codec!(VarInt, i32, var_int, var_int); +var_codec!(VarLong, i64, var_long, var_long); +var_codec!(RegistryId, i32, var_int, var_int); + +impl From for Uuid { + fn from(value: u128) -> Self { + Self(value) + } +} + +impl From for u128 { + fn from(value: Uuid) -> Self { + value.0 + } +} + +impl fmt::Display for Uuid { + /// The canonical 8-4-4-4-12 hyphenated form. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex = format!("{:032x}", self.0); + write!( + f, + "{}-{}-{}-{}-{}", + &hex[0..8], + &hex[8..12], + &hex[12..16], + &hex[16..20], + &hex[20..32] + ) + } +} + +impl Encode for Uuid { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.uuid(self.0); + Ok(()) + } +} + +impl Decode<'_> for Uuid { + fn decode(reader: &mut Reader<'_>) -> Result { + reader.uuid().map(Self) + } +} + +/// A block position, packed into one 64-bit word. +/// +/// `BlockPos.asLong` gives x the top 26 bits, z the next 26 and y the low 12, +/// each two's-complement within its own width. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct BlockPos { + /// East-west coordinate, 26 bits. + pub x: i32, + /// Vertical coordinate, 12 bits. + pub y: i32, + /// North-south coordinate, 26 bits. + pub z: i32, +} + +impl BlockPos { + const HORIZONTAL_BITS: u32 = 26; + const VERTICAL_BITS: u32 = 64 - 2 * Self::HORIZONTAL_BITS; + const X_SHIFT: u32 = Self::VERTICAL_BITS + Self::HORIZONTAL_BITS; + const Z_SHIFT: u32 = Self::VERTICAL_BITS; + + /// A position at the given coordinates. + #[must_use] + pub const fn new(x: i32, y: i32, z: i32) -> Self { + Self { x, y, z } + } + + /// The packed form written to the wire. + #[must_use] + pub const fn to_bits(self) -> i64 { + let x = (self.x as i64 & mask(Self::HORIZONTAL_BITS)) << Self::X_SHIFT; + let z = (self.z as i64 & mask(Self::HORIZONTAL_BITS)) << Self::Z_SHIFT; + let y = self.y as i64 & mask(Self::VERTICAL_BITS); + x | z | y + } + + /// Unpack the wire form, sign-extending each field from its own width. + #[must_use] + pub const fn from_bits(bits: i64) -> Self { + Self { + x: sign_extend(bits >> Self::X_SHIFT, Self::HORIZONTAL_BITS), + y: sign_extend(bits, Self::VERTICAL_BITS), + z: sign_extend(bits >> Self::Z_SHIFT, Self::HORIZONTAL_BITS), + } + } +} + +/// A chunk position, packed into one 64-bit word: z high, x low. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct ChunkPos { + /// East-west chunk coordinate. + pub x: i32, + /// North-south chunk coordinate. + pub z: i32, +} + +impl ChunkPos { + /// A position at the given chunk coordinates. + #[must_use] + pub const fn new(x: i32, z: i32) -> Self { + Self { x, z } + } + + /// The packed form written to the wire. + #[must_use] + pub const fn to_bits(self) -> i64 { + ((self.z as i64) << 32) | (self.x as i64 & 0xFFFF_FFFF) + } + + /// Unpack the wire form. + #[must_use] + #[expect( + clippy::cast_possible_truncation, + reason = "taking the low half is what unpacking a packed pair means" + )] + pub const fn from_bits(bits: i64) -> Self { + Self { + x: bits as i32, + z: (bits >> 32) as i32, + } + } +} + +const fn mask(bits: u32) -> i64 { + (1i64 << bits) - 1 +} + +/// Sign-extend the low `bits` of `value` as a two's-complement integer. +#[expect( + clippy::cast_possible_truncation, + reason = "the shifts leave at most `bits` significant bits, and `bits` is under 32" +)] +const fn sign_extend(value: i64, bits: u32) -> i32 { + (value << (64 - bits) >> (64 - bits)) as i32 +} + +impl Encode for BlockPos { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.i64(self.to_bits()); + Ok(()) + } +} + +impl Decode<'_> for BlockPos { + fn decode(reader: &mut Reader<'_>) -> Result { + reader.i64().map(Self::from_bits) + } +} + +impl Encode for ChunkPos { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.i64(self.to_bits()); + Ok(()) + } +} + +impl Decode<'_> for ChunkPos { + fn decode(reader: &mut Reader<'_>) -> Result { + reader.i64().map(Self::from_bits) + } +} + +/// A namespaced name such as `minecraft:stone`, borrowed from the buffer. +/// +/// `Identifier.parse` accepts an unqualified name and fills in the +/// `minecraft` namespace, so [`Identifier::namespace`] answers `minecraft` +/// for a value written without one. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Identifier<'a>(&'a str); + +impl<'a> Identifier<'a> { + /// The namespace assumed when a name is written without one. + pub const DEFAULT_NAMESPACE: &'static str = "minecraft"; + + /// Validate a namespaced name, matching `Identifier.tryParse`. + /// + /// # Errors + /// Returns [`Error::InvalidIdentifier`] when a character is outside the + /// permitted set for its half of the name. + pub fn new(text: &'a str) -> Result { + let (namespace, path) = text.split_once(':').unwrap_or(("", text)); + let valid = namespace.bytes().all(is_namespace_byte) + && !path.is_empty() + && path.bytes().all(is_path_byte); + if valid { + Ok(Self(text)) + } else { + Err(Error::InvalidIdentifier(text.to_owned())) + } + } + + /// The whole name as written, without a namespace if none was written. + #[must_use] + pub const fn as_str(&self) -> &'a str { + self.0 + } + + /// The namespace, defaulting to `minecraft`. + #[must_use] + pub fn namespace(&self) -> &'a str { + self.0 + .split_once(':') + .map_or(Self::DEFAULT_NAMESPACE, |(namespace, _)| namespace) + } + + /// The part after the colon, or the whole name if there is no colon. + #[must_use] + pub fn path(&self) -> &'a str { + self.0.split_once(':').map_or(self.0, |(_, path)| path) + } +} + +const fn is_namespace_byte(byte: u8) -> bool { + matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'-') +} + +const fn is_path_byte(byte: u8) -> bool { + is_namespace_byte(byte) || byte == b'/' +} + +impl fmt::Display for Identifier<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0) + } +} + +impl Encode for Identifier<'_> { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.string(self.0) + } +} + +impl<'a> Decode<'a> for Identifier<'a> { + fn decode(reader: &mut Reader<'a>) -> Result { + Self::new(reader.string()?) + } +} + +/// Which face of a block a ray struck. +/// +/// Discriminants are `Direction`'s declaration order, which is what +/// `FriendlyByteBuf.writeEnum` writes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)] +pub enum Direction { + /// Negative y. + Down = 0, + /// Positive y. + Up = 1, + /// Negative z. + North = 2, + /// Positive z. + South = 3, + /// Negative x. + West = 4, + /// Positive x. + East = 5, +} + +/// Where a player's ray met a block. +/// +/// The offsets are the hit point relative to the block's own corner, which is +/// how `FriendlyByteBuf.writeBlockHitResult` sends it: the absolute position +/// is recovered by adding them to `block_pos`. +#[derive(Debug, Clone, Copy, PartialEq, Encode, Decode)] +pub struct BlockHitResult { + /// The block that was hit. + pub block_pos: BlockPos, + /// The face that was hit. + pub direction: Direction, + /// Hit point's x offset within the block. + pub offset_x: f32, + /// Hit point's y offset within the block. + pub offset_y: f32, + /// Hit point's z offset within the block. + pub offset_z: f32, + /// Whether the ray started inside the block. + pub inside: bool, + /// Whether the ray was stopped by the world border rather than the block. + pub world_border_hit: bool, +} + +/// A value that is either a registry reference or written out in full. +/// +/// A `VarInt` of zero introduces the inline form; anything else is a registry +/// id plus one. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Holder { + /// An id in the registry the field names. + Reference(RegistryId), + /// The value itself, for entries the client does not have. + Inline(T), +} + +impl Encode for Holder { + fn encode(&self, writer: &mut Writer) -> Result<()> { + match self { + Self::Reference(id) => { + let raw = id.0.checked_add(1).ok_or(Error::NegativeLength(id.0))?; + writer.var_int(raw); + Ok(()) + } + Self::Inline(value) => { + writer.var_int(0); + value.encode(writer) + } + } + } +} + +impl<'a, T: Decode<'a>> Decode<'a> for Holder { + fn decode(reader: &mut Reader<'a>) -> Result { + match reader.var_int()? { + 0 => Ok(Self::Inline(T::decode(reader)?)), + raw => Ok(Self::Reference(RegistryId(raw - 1))), + } + } +} + +/// One of two layouts, selected by a leading boolean. +/// +/// `true` selects [`Either::Left`], matching `FriendlyByteBuf.writeEither`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Either { + /// The layout the `true` discriminant selects. + Left(L), + /// The layout the `false` discriminant selects. + Right(R), +} + +impl Encode for Either { + fn encode(&self, writer: &mut Writer) -> Result<()> { + match self { + Self::Left(value) => { + writer.bool(true); + value.encode(writer) + } + Self::Right(value) => { + writer.bool(false); + value.encode(writer) + } + } + } +} + +impl<'a, L: Decode<'a>, R: Decode<'a>> Decode<'a> for Either { + fn decode(reader: &mut Reader<'a>) -> Result { + if reader.bool()? { + Ok(Self::Left(L::decode(reader)?)) + } else { + Ok(Self::Right(R::decode(reader)?)) + } + } +} + +/// A value behind a `VarInt` byte count. +/// +/// The count lets a reader skip a value it cannot parse, so decoding checks +/// that the inner value consumed exactly the bytes the count promised rather +/// than trusting it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct LengthPrefixed(pub T); + +impl Encode for LengthPrefixed { + fn encode(&self, writer: &mut Writer) -> Result<()> { + let mut inner = Writer::new(); + self.0.encode(&mut inner)?; + writer.byte_array(inner.as_slice()) + } +} + +impl<'a, T: Decode<'a>> Decode<'a> for LengthPrefixed { + fn decode(reader: &mut Reader<'a>) -> Result { + let bytes = reader.byte_array()?; + let mut inner = Reader::new(bytes); + let value = T::decode(&mut inner)?; + inner.finish()?; + Ok(Self(value)) + } +} + +// --------------------------------------------------------------------------- +// Primitives and containers +// --------------------------------------------------------------------------- + +macro_rules! fixed_codec { + ($($ty:ty => $write:ident / $read:ident),* $(,)?) => { + $( + impl Encode for $ty { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.$write(*self); + Ok(()) + } + } + + impl Decode<'_> for $ty { + fn decode(reader: &mut Reader<'_>) -> Result { + reader.$read() + } + } + )* + }; +} + +fixed_codec! { + bool => bool / bool, + u8 => u8 / u8, + i8 => i8 / i8, + u16 => u16 / u16, + i16 => i16 / i16, + i32 => i32 / i32, + i64 => i64 / i64, + f32 => f32 / f32, + f64 => f64 / f64, +} + +/// A packet whose body is empty. +impl Encode for () { + fn encode(&self, _writer: &mut Writer) -> Result<()> { + Ok(()) + } +} + +impl Decode<'_> for () { + fn decode(_reader: &mut Reader<'_>) -> Result { + Ok(()) + } +} + +impl Encode for &str { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.string(self) + } +} + +impl<'a> Decode<'a> for &'a str { + fn decode(reader: &mut Reader<'a>) -> Result { + reader.string() + } +} + +impl Encode for &[u8] { + fn encode(&self, writer: &mut Writer) -> Result<()> { + writer.byte_array(self) + } +} + +impl<'a> Decode<'a> for &'a [u8] { + fn decode(reader: &mut Reader<'a>) -> Result { + reader.byte_array() + } +} + +/// A boolean, then the value when it is present. +impl Encode for Option { + fn encode(&self, writer: &mut Writer) -> Result<()> { + match self { + Some(value) => { + writer.bool(true); + value.encode(writer) + } + None => { + writer.bool(false); + Ok(()) + } + } + } +} + +impl<'a, T: Decode<'a>> Decode<'a> for Option { + fn decode(reader: &mut Reader<'a>) -> Result { + if reader.bool()? { + Ok(Some(T::decode(reader)?)) + } else { + Ok(None) + } + } +} + +/// A `VarInt` count, then that many elements. +impl Encode for Vec { + fn encode(&self, writer: &mut Writer) -> Result<()> { + codec::write_count(writer, self.len(), None)?; + for item in self { + item.encode(writer)?; + } + Ok(()) + } +} + +impl<'a, T: Decode<'a>> Decode<'a> for Vec { + fn decode(reader: &mut Reader<'a>) -> Result { + let count = codec::read_count(reader, None)?; + // Capacity is clamped to the bytes actually present: the count comes + // off the wire and every element costs at least one byte, so a huge + // count on a short buffer must not reserve for it. + let mut items = Self::with_capacity(count.min(reader.remaining_len())); + for _ in 0..count { + items.push(T::decode(reader)?); + } + Ok(items) + } +} + +/// A key and a value back to back, which is how a map entry is written. +impl Encode for (K, V) { + fn encode(&self, writer: &mut Writer) -> Result<()> { + self.0.encode(writer)?; + self.1.encode(writer) + } +} + +impl<'a, K: Decode<'a>, V: Decode<'a>> Decode<'a> for (K, V) { + fn decode(reader: &mut Reader<'a>) -> Result { + Ok((K::decode(reader)?, V::decode(reader)?)) + } +} + +// The domain types the extractor recovered from the server's own codecs. They +// live beside the hand-written primitives because a reader looking for `Vec3` +// does not care which side of that line it fell on; the private module exists +// only to give the generated file an import scope of its own. +mod jar { + include!(concat!(env!("OUT_DIR"), "/types.rs")); +} + +pub use jar::*; diff --git a/crates/hyperion-minecraft-proto/tests/derive.rs b/crates/hyperion-minecraft-proto/tests/derive.rs new file mode 100644 index 000000000..9f9c12628 --- /dev/null +++ b/crates/hyperion-minecraft-proto/tests/derive.rs @@ -0,0 +1,227 @@ +//! What `#[derive(Encode, Decode)]` produces. +//! +//! These are the shapes the generator emits, exercised on hand-written types +//! so a failure points at the derive rather than at a packet layout. + +use hyperion_minecraft_proto::{ + Decode, Encode, Error, Identifier, Reader, Result, Uuid, VarInt, Writer, +}; + +fn round_trip<'a, T>(value: &T, bytes: &'a [u8]) +where + T: Encode + Decode<'a> + PartialEq + std::fmt::Debug, +{ + let mut writer = Writer::new(); + value.encode(&mut writer).expect("encode"); + assert_eq!(writer.as_slice(), bytes, "encoding mismatch"); + + let mut reader = Reader::new(bytes); + let decoded = T::decode(&mut reader).expect("decode"); + reader.finish().expect("body fully consumed"); + assert_eq!(&decoded, value, "decoding mismatch"); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +struct Unit; + +#[test] +fn a_unit_struct_occupies_no_bytes() { + round_trip(&Unit, &[]); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +struct Newtype(VarInt); + +#[test] +fn a_newtype_is_its_field() { + round_trip(&Newtype(VarInt(300)), &[0xAC, 0x02]); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +struct Fields<'a> { + count: VarInt, + flag: bool, + name: &'a str, + id: Uuid, +} + +#[test] +fn fields_are_written_in_declaration_order() { + round_trip( + &Fields { + count: VarInt(1), + flag: true, + name: "hi", + id: Uuid(0), + }, + &[ + 0x01, 0x01, 0x02, b'h', b'i', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +struct Bounded<'a> { + #[proto(max_len = 4)] + short: &'a str, + #[proto(max_count = 2)] + few: Vec, + #[proto(max_len = 3)] + maybe: Option<&'a str>, + #[proto(max_len = 2)] + pages: Vec<&'a str>, +} + +#[test] +fn limits_reach_the_type_they_constrain() { + round_trip( + &Bounded { + short: "abcd", + few: vec![VarInt(7)], + maybe: Some("xy"), + pages: vec!["a", "b"], + }, + &[ + 0x04, b'a', b'b', b'c', b'd', // short + 0x01, 0x07, // few + 0x01, 0x02, b'x', b'y', // maybe + 0x02, 0x01, b'a', 0x01, b'b', // pages, one limit per page + ], + ); +} + +#[test] +fn an_over_long_string_is_refused_on_write() { + let value = Bounded { + short: "abcde", + few: Vec::new(), + maybe: None, + pages: Vec::new(), + }; + let mut writer = Writer::new(); + assert_eq!( + value.encode(&mut writer), + Err(Error::StringTooLong { length: 5, max: 4 }) + ); +} + +#[test] +fn an_over_long_list_is_refused_on_read() { + // Three elements where the field permits two: the server would throw, so + // decoding one must not quietly succeed. + let bytes = [0x00, 0x03, 0x01, 0x02, 0x03, 0x00, 0x00]; + let mut reader = Reader::new(&bytes); + assert_eq!( + Bounded::decode(&mut reader), + Err(Error::ListTooLong { length: 3, max: 2 }) + ); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +#[repr(i32)] +enum Intent { + Status = 1, + Login = 2, + Transfer = 3, +} + +#[test] +fn an_enum_is_a_varint_discriminant() { + round_trip(&Intent::Status, &[0x01]); + round_trip(&Intent::Transfer, &[0x03]); +} + +#[test] +fn an_unknown_discriminant_is_an_error_not_a_variant() { + let mut reader = Reader::new(&[0x00]); + assert_eq!( + Intent::decode(&mut reader), + Err(Error::InvalidEnum { + name: "Intent", + value: 0 + }) + ); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +enum Implicit { + First, + Second, + Tenth = 10, + Eleventh, +} + +#[test] +fn implicit_discriminants_count_the_way_rust_does() { + round_trip(&Implicit::First, &[0x00]); + round_trip(&Implicit::Second, &[0x01]); + round_trip(&Implicit::Tenth, &[0x0A]); + round_trip(&Implicit::Eleventh, &[0x0B]); +} + +/// A codec that is not the derive's default shape, reached with `with`. +/// +/// The signatures are the contract the derive calls: a shared reference in +/// and a `Result` out, whatever the field's own type would allow. +#[expect( + clippy::trivially_copy_pass_by_ref, + clippy::unnecessary_wraps, + reason = "the shape is fixed by what `#[proto(with = ...)]` calls" +)] +mod doubled { + use super::{Reader, Result, Writer}; + + pub fn encode(value: &i32, writer: &mut Writer) -> Result<()> { + writer.var_int(value * 2); + Ok(()) + } + + pub fn decode(reader: &mut Reader<'_>) -> Result { + Ok(reader.var_int()? / 2) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +struct Custom { + #[proto(with = doubled)] + value: i32, +} + +#[test] +fn with_replaces_the_whole_field_codec() { + round_trip(&Custom { value: 21 }, &[0x2A]); +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +struct Nested<'a> { + outer: VarInt, + inner: Fields<'a>, + list: Vec>, + id: Option>, +} + +#[test] +fn nested_structs_delegate_to_their_own_codecs() { + let inner = Fields { + count: VarInt(2), + flag: false, + name: "a", + id: Uuid(1), + }; + let mut expected = vec![0x09]; + let mut one = Writer::new(); + inner.encode(&mut one).expect("encode inner"); + expected.extend_from_slice(one.as_slice()); + expected.push(0x01); + expected.extend_from_slice(one.as_slice()); + expected.push(0x00); + round_trip( + &Nested { + outer: VarInt(9), + inner, + list: vec![inner], + id: None, + }, + &expected, + ); +} diff --git a/crates/hyperion-minecraft-proto/tests/live_server.rs b/crates/hyperion-minecraft-proto/tests/live_server.rs index 33bd6dcf1..eaacd1518 100644 --- a/crates/hyperion-minecraft-proto/tests/live_server.rs +++ b/crates/hyperion-minecraft-proto/tests/live_server.rs @@ -20,11 +20,15 @@ use std::{ }; use hyperion_minecraft_proto::{ - Decode, Encode, PROTOCOL_VERSION, Reader, Writer, + Decode, Encode, PROTOCOL_VERSION, Reader, VarInt, Writer, packets::{ - handshake::{ClientIntent, Intention}, - status::{PingRequest, PongResponse, StatusRequest, StatusResponse}, + handshake::serverbound::Intention, + status::{ + clientbound::{PongResponse, StatusResponse}, + serverbound::{PingRequest, StatusRequest}, + }, }, + types::ClientIntent, }; /// Wrap a packet body in the length-prefixed frame the server expects. @@ -75,7 +79,7 @@ fn read_frame(stream: &mut TcpStream) -> (i32, Vec) { fn status_ping_against_real_server() { let address = env::var("HYPERION_MC_SERVER").expect("HYPERION_MC_SERVER must be set"); let (host, port) = address.rsplit_once(':').expect("address must be host:port"); - let port: u16 = port.parse().expect("port must be a number"); + let port: i16 = port.parse().expect("port must be a number"); let mut stream = TcpStream::connect(&address).expect("connect"); stream @@ -84,7 +88,7 @@ fn status_ping_against_real_server() { // Handshake, then status request, in one write the way a real client does. let handshake = Intention { - protocol_version: PROTOCOL_VERSION, + protocol_version: VarInt(PROTOCOL_VERSION), host_name: host, port, intention: ClientIntent::Status, @@ -122,7 +126,7 @@ fn status_ping_against_real_server() { // is a big-endian i64 bug on one side or the other. let nonce = 0x0123_4567_89AB_CDEF_i64; stream - .write_all(&frame(1, &PingRequest { time: nonce })) + .write_all(&frame(1, &PingRequest(nonce))) .expect("send ping"); let (packet_id, body) = read_frame(&mut stream); @@ -130,7 +134,7 @@ fn status_ping_against_real_server() { let mut reader = Reader::new(&body); let pong = PongResponse::decode(&mut reader).expect("decode pong"); reader.finish().expect("pong fully consumed"); - assert_eq!(pong.time, nonce, "server echoed a different value"); + assert_eq!(pong.0, nonce, "server echoed a different value"); - eprintln!("pong echoed {:#x} correctly", pong.time); + eprintln!("pong echoed {:#x} correctly", pong.0); } diff --git a/crates/hyperion-minecraft-proto/tests/types.rs b/crates/hyperion-minecraft-proto/tests/types.rs new file mode 100644 index 000000000..7b7aab967 --- /dev/null +++ b/crates/hyperion-minecraft-proto/tests/types.rs @@ -0,0 +1,123 @@ +//! The wire value types, and how much of the protocol the generator covers. + +use hyperion_minecraft_proto::{ + BlockPos, ChunkPos, Decode, Either, Encode, Error, Holder, Identifier, LengthPrefixed, Reader, + RegistryId, Uuid, VarInt, Writer, +}; + +fn round_trip<'a, T>(value: &T, bytes: &'a [u8]) +where + T: Encode + Decode<'a> + PartialEq + std::fmt::Debug, +{ + let mut writer = Writer::new(); + value.encode(&mut writer).expect("encode"); + assert_eq!(writer.as_slice(), bytes, "encoding mismatch"); + let mut reader = Reader::new(bytes); + assert_eq!(&T::decode(&mut reader).expect("decode"), value); + reader.finish().expect("fully consumed"); +} + +#[test] +fn block_pos_packs_the_way_the_server_does() { + // `BlockPos.asLong` puts x in the top 26 bits, z in the next 26 and y in + // the low 12: (1 << 38) | (3 << 12) | 2. + let packed = (1i64 << 38) | (3i64 << 12) | 2; + assert_eq!(BlockPos::new(1, 2, 3).to_bits(), packed); + round_trip(&BlockPos::new(1, 2, 3), &packed.to_be_bytes()); +} + +#[test] +fn block_pos_sign_extends_each_field_from_its_own_width() { + // Each coordinate is two's-complement within 26 or 12 bits, so a negative + // one only decodes correctly if the sign is extended from that width. + for pos in [ + BlockPos::new(-1, -1, -1), + BlockPos::new(-30_000_000, -2048, 30_000_000), + BlockPos::new(0, 2047, 0), + ] { + assert_eq!(BlockPos::from_bits(pos.to_bits()), pos, "{pos:?}"); + } +} + +#[test] +fn chunk_pos_puts_z_in_the_high_half() { + let packed = (2i64 << 32) | 0xFFFF_FFFF; + assert_eq!(ChunkPos::new(-1, 2).to_bits(), packed); + assert_eq!(ChunkPos::from_bits(packed), ChunkPos::new(-1, 2)); +} + +#[test] +fn an_identifier_defaults_its_namespace() { + let bare = Identifier::new("stone").expect("valid"); + assert_eq!(bare.namespace(), "minecraft"); + assert_eq!(bare.path(), "stone"); + + let full = Identifier::new("hyperion:bed_wars").expect("valid"); + assert_eq!(full.namespace(), "hyperion"); + assert_eq!(full.path(), "bed_wars"); +} + +#[test] +fn an_identifier_rejects_characters_the_server_rejects() { + // `Identifier.tryParse` allows a slash in the path but not in the + // namespace, and no upper case anywhere. + assert!(Identifier::new("minecraft:block/stone").is_ok()); + assert_eq!( + Identifier::new("Minecraft:stone"), + Err(Error::InvalidIdentifier("Minecraft:stone".to_owned())) + ); + assert!(Identifier::new("a/b:stone").is_err()); + assert!(Identifier::new("minecraft:").is_err()); +} + +#[test] +fn a_uuid_displays_in_the_canonical_form() { + assert_eq!( + Uuid(0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF).to_string(), + "01234567-89ab-cdef-0123-456789abcdef" + ); +} + +#[test] +fn a_holder_is_an_id_plus_one_or_zero_and_the_value() { + round_trip(&Holder::::Reference(RegistryId(0)), &[0x01]); + round_trip(&Holder::Inline(VarInt(7)), &[0x00, 0x07]); +} + +#[test] +fn either_writes_true_for_its_left() { + round_trip(&Either::::Left(VarInt(1)), &[0x01, 0x01]); + round_trip(&Either::::Right(false), &[0x00, 0x00]); +} + +#[test] +fn a_length_prefix_must_match_what_the_value_consumes() { + round_trip(&LengthPrefixed(VarInt(300)), &[0x02, 0xAC, 0x02]); + + // A prefix promising three bytes where the value uses two is a layout + // disagreement, not something to read past. + let mut reader = Reader::new(&[0x03, 0xAC, 0x02, 0x00]); + assert_eq!( + LengthPrefixed::::decode(&mut reader), + Err(Error::TrailingBytes(1)) + ); +} + +/// How many packets `build.rs` wrote, and how many it declined. +/// +/// Pinned so that a change which silently stops generating a packet fails +/// here rather than being noticed when a codec goes missing. +const COVERAGE: &str = include_str!(concat!(env!("OUT_DIR"), "/coverage.txt")); + +#[test] +fn the_generator_covers_every_layout_it_can() { + let counts: Vec = COVERAGE + .split_whitespace() + .map(|field| field.parse().expect("coverage counts are numbers")) + .collect(); + let [written, declined] = counts[..] else { + panic!("coverage.txt should hold two counts, found {COVERAGE:?}"); + }; + assert_eq!(written, 177, "packet classes generated"); + assert_eq!(declined, 3, "layouts the generator declined"); +} diff --git a/crates/hyperion-minecraft-proto/tests/wire.rs b/crates/hyperion-minecraft-proto/tests/wire.rs index 0bcbd72da..1b4d1d9b5 100644 --- a/crates/hyperion-minecraft-proto/tests/wire.rs +++ b/crates/hyperion-minecraft-proto/tests/wire.rs @@ -1,17 +1,27 @@ //! Wire-format tests. //! -//! The byte vectors are not invented: each one is either a value the server's -//! own source pins (`VarInt.java`'s limits, `ClientIntent`'s discriminants) or -//! a frame captured from a real Minecraft 26.2 server, recorded in -//! `handshake_frame_matches_capture`. +//! The byte vectors are not invented: every one is a value the server's own +//! source pins, such as `VarInt.java`'s limits or `ServerboundHelloPacket`'s +//! 16-character name. `tests/live_server.rs` covers the other direction, by +//! handing bytes to a real 26.2 server and reading back what it sends. use hyperion_minecraft_proto::{ - Decode, Encode, Error, PROTOCOL_VERSION, Reader, Writer, + Decode, Encode, Error, Identifier, PROTOCOL_VERSION, Reader, Uuid, VarInt, Writer, packets::{ - handshake::{ClientIntent, Intention}, - login::{Hello, HelloRequest, LoginAcknowledged, LoginCompression, LoginDisconnect}, - status::{PingRequest, PongResponse, StatusRequest, StatusResponse}, + common::serverbound::{CookieResponse, PingRequest}, + configuration::clientbound::SelectKnownPacks, + handshake::serverbound::Intention, + login::{ + clientbound::{Hello as HelloRequest, LoginCompression, LoginDisconnect}, + serverbound::{Hello, LoginAcknowledged}, + }, + play, status, + status::{ + clientbound::{PongResponse, StatusResponse}, + serverbound::StatusRequest, + }, }, + types::{ClientIntent, KnownPack}, }; fn encode(value: &T) -> Vec { @@ -133,12 +143,12 @@ fn truncated_input_is_an_error_not_a_default() { ); } -// --- handshake ------------------------------------------------------------ +// --- generated packets ---------------------------------------------------- #[test] fn intention_round_trips() { let packet = Intention { - protocol_version: PROTOCOL_VERSION, + protocol_version: VarInt(PROTOCOL_VERSION), host_name: "localhost", port: 25565, intention: ClientIntent::Login, @@ -151,21 +161,21 @@ fn intention_round_trips() { } #[test] -fn client_intent_rejects_zero() { - // ClientIntent starts at 1; byId throws on anything else. +fn client_intent_discriminants_are_ids_not_ordinals() { + // `ClientIntent.byId` maps 1, 2, 3; the ordinals are 0, 1, 2, and reading + // one for the other would silently pick the wrong intent. + let mut reader = Reader::new(&[0x00]); assert_eq!( - ClientIntent::from_raw(0), + ClientIntent::decode(&mut reader), Err(Error::InvalidEnum { name: "ClientIntent", value: 0 }) ); - assert_eq!(ClientIntent::from_raw(1), Ok(ClientIntent::Status)); - assert_eq!(ClientIntent::from_raw(3), Ok(ClientIntent::Transfer)); + round_trip(&ClientIntent::Status, &[0x01]); + round_trip(&ClientIntent::Transfer, &[0x03]); } -// --- status --------------------------------------------------------------- - #[test] fn status_request_is_empty() { round_trip(&StatusRequest, &[]); @@ -182,27 +192,23 @@ fn status_response_round_trips() { #[test] fn ping_and_pong_round_trip() { let bytes: &[u8] = &[0x00, 0x00, 0x01, 0x9A, 0x2B, 0x3C, 0x4D, 0x5E]; - round_trip( - &PingRequest { - time: 0x0000_019A_2B3C_4D5E, - }, - bytes, - ); - round_trip( - &PongResponse { - time: 0x0000_019A_2B3C_4D5E, - }, - bytes, - ); + round_trip(&PingRequest(0x0000_019A_2B3C_4D5E), bytes); + round_trip(&PongResponse(0x0000_019A_2B3C_4D5E), bytes); } -// --- login ---------------------------------------------------------------- +#[test] +fn one_class_serving_two_states_is_one_type() { + // `ServerboundPingRequestPacket` is registered by both status and play, so + // a value built for one has to be the value the other accepts. + let ping: status::serverbound::PingRequest = play::serverbound::PingRequest(7); + assert_eq!(ping, PingRequest(7)); +} #[test] fn login_hello_round_trips() { - let uuid = 0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF_u128; + let uuid = Uuid(0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF); let mut expected = vec![5, b'N', b'o', b't', b'c', b'h']; - expected.extend_from_slice(&uuid.to_be_bytes()); + expected.extend_from_slice(&uuid.0.to_be_bytes()); round_trip( &Hello { name: "Notch", @@ -214,11 +220,12 @@ fn login_hello_round_trips() { #[test] fn login_hello_rejects_overlong_name() { - // ServerboundHelloPacket reads with a 16-character limit. + // `ServerboundHelloPacket` writes the name with a 16-character limit, and + // the generated struct carries that limit as `#[proto(max_len = 16)]`. let mut writer = Writer::new(); let packet = Hello { name: "a_seventeen_char!", - profile_id: 0, + profile_id: Uuid(0), }; assert!(matches!( packet.encode(&mut writer), @@ -247,19 +254,11 @@ fn login_hello_request_round_trips() { #[test] fn login_compression_round_trips() { - round_trip( - &LoginCompression { - compression_threshold: 256, - }, - &[0x80, 0x02], - ); + round_trip(&LoginCompression(VarInt(256)), &[0x80, 0x02]); // A negative threshold disables compression and needs the full five bytes. - round_trip( - &LoginCompression { - compression_threshold: -1, - }, - &[0xFF, 0xFF, 0xFF, 0xFF, 0x0F], - ); + round_trip(&LoginCompression(VarInt(-1)), &[ + 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, + ]); } #[test] @@ -275,6 +274,51 @@ fn login_acknowledged_is_empty() { round_trip(&LoginAcknowledged, &[]); } +#[test] +fn optional_field_is_boolean_prefixed() { + // `ServerboundCookieResponsePacket.payload` is `Optional`, which + // the derive writes as a discriminant byte and then the value. + round_trip( + &CookieResponse { + key: Identifier::new("hyperion:seen").expect("valid identifier"), + payload: Some(&[0xAB]), + }, + &[ + 0x0D, b'h', b'y', b'p', b'e', b'r', b'i', b'o', b'n', b':', b's', b'e', b'e', b'n', + 0x01, 0x01, 0xAB, + ], + ); + round_trip( + &CookieResponse { + key: Identifier::new("hyperion:seen").expect("valid identifier"), + payload: None, + }, + &[ + 0x0D, b'h', b'y', b'p', b'e', b'r', b'i', b'o', b'n', b':', b's', b'e', b'e', b'n', + 0x00, + ], + ); +} + +#[test] +fn a_list_is_a_count_then_the_elements() { + round_trip( + &SelectKnownPacks { + known_packs: vec![KnownPack { + namespace: "minecraft", + id: "core", + version: "26.2", + }], + }, + &[ + 0x01, // one pack + 0x09, b'm', b'i', b'n', b'e', b'c', b'r', b'a', b'f', b't', // + 0x04, b'c', b'o', b'r', b'e', // + 0x04, b'2', b'6', b'.', b'2', + ], + ); +} + // --- trailing bytes ------------------------------------------------------- #[test] diff --git a/docs/minecraft-26.2-migration.md b/docs/minecraft-26.2-migration.md index 2faf052a2..e4d863db6 100644 --- a/docs/minecraft-26.2-migration.md +++ b/docs/minecraft-26.2-migration.md @@ -140,8 +140,8 @@ Preferring the derived value is what surfaced it. ``` before after packets 256 256 -fully mechanical 124 200 -partial 77 56 +fully mechanical 124 202 +partial 77 54 unrecovered 55 0 data components - 111 with a known layout - 100 @@ -237,6 +237,117 @@ play::clientbound::PacketId::SetEntityMotion.layout() // None -- Vec3.LP_STREAM_CODEC is variable-length ``` +## The packet structs are generated too, out of a committed protocol.json + +The `Wire` table above says what the bytes are. It does not give a Rust type to +put them in, and hand-writing that type for two hundred packets is exactly the +transcription this pipeline exists to delete. So `protocol.json` is committed +into the crate and `build.rs` turns it into the packet structs: + +```rust +/// `minecraft:intention`, sent serverbound as handshake id 0. +/// +/// Layout from `net.minecraft.network.protocol.handshake.ClientIntentionPacket#STREAM_CODEC`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)] +pub struct Intention<'a> { + pub protocol_version: VarInt, + #[proto(max_len = 32767)] + pub host_name: &'a str, + pub port: i16, + pub intention: ClientIntent, +} +``` + +**177 of the 180 packet classes the extractor recovered in full are generated.** +The three it declines are named, with the reason, in a comment at the top of the +file they would have been written into: + +``` +// Layouts the extractor recovered in full but this generator declined, and +// why. Each needs a hand-written codec; none of them is approximated here. +// minecraft:custom_click_action -- a custom codec inside a combinator needs a hand-written packet +// minecraft:damage_event -- two fields would both be named `output` +// minecraft:disguised_chat -- `net.minecraft.network.chat.ChatType$Bound#STREAM_CODEC` contains itself +``` + +Two hundred packets across 180 classes, because `ClientboundKeepAlivePacket` is +one class that configuration and play both register. It is defined once, in +`packets::common`, and re-exported into each state that sends it, so a value +built for one state is the same Rust value in the other. + +### What is committed and what is generated, and why they differ + +| artifact | where it lives | why | +| --- | --- | --- | +| `protocol.json` | committed, 660 KB | the input everything else derives from, and the diff a version bump is reviewed as | +| packet structs | `OUT_DIR`, via `build.rs` | the wire contract, where a stale copy silently desynchronises a stream | +| packet ids, registries, versions, `Wire` table | committed `.rs` | data restated as Rust, greppable, and only wrong in a log line | + +The line is not "generated versus hand-written", it is **what a mistake costs +and whether the projection is total**. The tables are a total function of +`protocol.json`: every packet gets an id, every registry gets its entries, +nothing is filtered. A committed copy of a total projection can be checked by +diffing it, and `nix flake check` does exactly that. + +The packet structs are a *partial* projection — the generator refuses layouts +it cannot express, and the refusal set moves as the extractor improves. That is +precisely the shape that has bitten this pipeline before: an earlier extractor +silently truncated eleven packets and reported them as complete. Making the +filtering happen at build time, from a file whose contents are themselves +guarded, means there is no committed artifact that can disagree with it. + +Volume settles the rest. `registry.rs` is 7,700 lines of `&'static str` that +nobody reads top to bottom but everybody greps; moving it into `OUT_DIR` would +make `rg 'minecraft:diamond_sword'` miss, for no gain. The packet structs are +1,300 lines that people *do* read, and `cargo doc` renders them either way. + +### The derive + +`#[derive(Encode, Decode)]` lives in `crates/hyperion-minecraft-proto-derive`. +A packet body is a fixed sequence of fields with nothing between them, so the +derive is a loop over the fields in declaration order, and anything that is not +that is a compile error rather than a guess. + +| attribute | effect | +| --- | --- | +| `#[proto(max_len = N)]` | limit on the innermost string or byte slice | +| `#[proto(max_count = N)]` | limit on the innermost collection's element count | +| `#[proto(with = path)]` | `path::encode` and `path::decode` for this field | + +`max_len` and `max_count` thread through `Option<_>` and `Vec<_>` to the type +they constrain, so `#[proto(max_len = 1024)] pages: Vec<&'a str>` bounds each +page rather than the list. Fieldless enums are a `VarInt` discriminant; a +variant with fields is refused, because the discriminant alone does not say how +to read what follows. + +The hand-written handshake, status and login codecs are gone: every one of +their packets is generated, and the generated version carries limits the +hand-written one had transcribed by hand. + +### Enums are enums + +`writeEnum` sends `ordinal()` and `ClientIntent` sends ids of its own, so an +enum field used to come out as a `VarInt`. Both are readable off the jar — the +constants in declaration order, and where the class has one, the `byId` switch +naming each constant's number — so eleven of them are generated as Rust enums: + +```rust +/// `net.minecraft.network.protocol.handshake.ClientIntent`, sent as a varint id. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)] +pub enum ClientIntent { + /// `STATUS` + Status = 1, + /// `LOGIN` + Login = 2, + /// `TRANSFER` + Transfer = 3, +} +``` + +The ids are 1, 2, 3 and the ordinals are 0, 1, 2. Reading one for the other +picks the wrong intent silently, which is why the switch is parsed rather than +the position assumed. + ## What Pumpkin and the other prior art actually offer - **Pumpkin-MC/Extractor** is a Kotlin Fabric mod, so it depends on the @@ -314,25 +425,40 @@ Built and verified: URL and SRI hash, JDK requirement. - `nix build .#minecraft-data` — Mojang's generator, sandboxed, 8,899 files. - `nix build .#minecraft-decompiled` — cfr over the packet and codec classes - the extractor reads, selected from the jar rather than listed: 676 files. + the extractor reads, selected from the jar rather than listed: 698 files. - `nix build .#minecraft-protocol` — the extraction above, as JSON, including a shared table of every named codec it resolved. - `nix build .#minecraft-proto-rust` — Rust tables: ids, registries, the data component registry and the wire layouts. - `nix run .#update-minecraft-data` — re-resolves Mojang's manifest and rewrites the pin, reading the protocol number out of the jar rather than guessing it. -- `nix run .#sync-minecraft-proto` — copies generated Rust into the crate. -- `nix flake check` — fails if the committed generated sources drift. +- `nix run .#sync-minecraft-proto` — copies `protocol.json` and the generated + tables into the crate. +- `nix flake check` — fails if either committed artifact drifts. -Both checks added here pass: +The three checks pass: ``` $ nix build --no-link .#checks.aarch64-darwin.minecraft-proto-generated \ + .#checks.aarch64-darwin.minecraft-proto-json \ .#checks.aarch64-darwin.minecraft-protocol $ echo $? 0 ``` +The `protocol.json` guard was watched failing before it was trusted. Editing +the committed copy's protocol number to 999 and re-running gives: + +``` +committed protocol.json is stale; run: nix run .#sync-minecraft-proto +@@ -1,7 +1,7 @@ + { + "version": { + "id": "26.2", + - "protocolVersion": 999, + + "protocolVersion": 776, +``` + `nix flake check` as a whole still fails, on a defect that predates this work and is unrelated to it: `Cargo.lock` carries nine valence crates from two git sources at the same name-version, and cargo-unit refuses to build an aggregate @@ -367,8 +493,10 @@ identical In `crates/hyperion-minecraft-proto`: generated tables for protocol 776, 256 packet ids, 95 registries (6,979 entries), the 111 data component types, and the wire layouts of the 200 packets and 100 components the extractor resolved. -Alongside them, hand-written codecs for handshake, status and login, NBT, text -components and item stacks. +Alongside them, generated structs for 177 packet classes, and hand-written +codecs for NBT, text components and item stacks. + +`crates/hyperion-minecraft-proto-derive` supplies `#[derive(Encode, Decode)]`. ## What the nix audit found @@ -411,18 +539,34 @@ Stated plainly, because these are the parts a reader cannot check by looking: - **Layouts were validated by reading, not by round-tripping.** Only the handshake, status and login packets are exercised against a real server. The - 200 layouts the extractor now emits were checked three ways — against the + 202 layouts the extractor now emits were checked three ways — against the independently-established `ItemLore` and `CustomModelData` shapes, against the previous extractor's output for the packets both call complete, and by reading a dozen of the largest by hand — but no play packet has been sent. +- **Field limits come from the writer, and the server enforces them on the + reader.** The two disagree more often than they look like they should: + `ClientIntentionPacket` writes `hostName` with the default 32767 and reads it + with 255, and `ClientboundHelloPacket` writes `serverId` unbounded and reads + it with 20. The generated struct follows the writer, so it is permissive + where vanilla is strict — a decoder that accepts a 300-character host the + server would reject. It cannot desynchronise a stream, but it is a real + divergence and the fix is to parse the private `(FriendlyByteBuf)` + constructor as a second, independent statement of the layout and cross-check + it against the writer, the way packet ids are already cross-checked. +- **`port` is written as `i16` and read as `u16`.** Same two bytes, same cause + as above: `writeShort` against `readUnsignedShort`. The generated struct says + `i16`. Any other field with asymmetric signedness is misreported the same + way, and the same cross-check would find them all. +- **Three complete layouts are declined rather than generated**, listed in the + file each would have gone into. `damage_event` is the one that is a defect + rather than a limit: the extractor inlines `writeOptionalEntityId(output, + this.sourceCauseId)` without binding the helper's parameter back to the + caller's argument, so two fields are both labelled `output`. Binding it would + recover the packet. - **The extractor being loud is a property of its structure, not a proof.** Every statement of an encode body and every argument of a composite has to be accounted for, and anything unmodelled propagates up as unresolved. That is checked by construction rather than by a test that deliberately breaks it. -- **`port` is reported as `i16` and implemented as `u16`.** The server writes it - with `writeShort` and reads it with `readUnsignedShort`; the extractor follows - the writer, the Rust follows the reader. Same two bytes, but the mismatch is - real and any other field with asymmetric signedness reads the same way. - **`GAME_PROFILE_PROPERTIES` is the one asserted layout.** It was transcribed from the 26.2 source by hand because its encoder is a loop. Everything else in the vocabulary either bottoms out in a netty write or is derived. diff --git a/flake.nix b/flake.nix index 7cf647328..f48bfb6f9 100644 --- a/flake.nix +++ b/flake.nix @@ -66,12 +66,15 @@ pkg: nixpkgs.lib.hasPrefix "minecraft-" (nixpkgs.lib.getName pkg); }; - minecraft = import ./nix/minecraft-data.nix { pkgs = minecraftPkgs; }; - rustToolchain = rustWith [ "rustfmt" "clippy" "rust-src" ]; rustWithMiri = rustWith [ "rustfmt" "clippy" "rust-src" "miri" ]; rustWithCoverage = rustWith [ "rustfmt" "clippy" "rust-src" "llvm-tools-preview" ]; + minecraft = import ./nix/minecraft-data.nix { + pkgs = minecraftPkgs; + rustfmt = rustToolchain; + }; + cargoTools = [ pkgs.cargo-deny pkgs.cargo-machete @@ -420,6 +423,7 @@ # The committed generated sources must match what the pipeline # produces, or the copy cargo reads is a fiction. minecraft-proto-generated = minecraft.generatedUpToDate; + minecraft-proto-json = minecraft.protocolJsonUpToDate; minecraft-protocol = minecraft.protocolJson; }; diff --git a/nix/extract-protocol.py b/nix/extract-protocol.py index a9f3d0579..db490a026 100644 --- a/nix/extract-protocol.py +++ b/nix/extract-protocol.py @@ -556,14 +556,30 @@ def split_chain(expr: str) -> list[str]: return [p for p in parts if p] +def strip_getter(name: str) -> str: + """``getEntityId`` -> ``entityId``, leaving anything else alone. + + Java's accessor prefix carries no information a field name needs, and + dropping it here is what stops it reaching the generated Rust. Names like + ``get3DDataValue`` keep the prefix because the remainder is not an + identifier on its own. + """ + m = re.fullmatch(r"get([A-Z]\w*)", name) + return m.group(1)[0].lower() + m.group(1)[1:] if m else name + + def _getter_name(getter: str) -> str: """Field label from a composite's getter, ``Foo::bar`` or ``f -> f.bar()``.""" getter = getter.strip() m = re.search(r"::(\w+)\s*$", getter) if m: - return m.group(1) + return strip_getter(m.group(1)) + # A lambda projects onto one member, with or without call parentheses. + m = re.search(r"->\s*\w+\.(\w+)\s*(?:\(\s*\))?\s*$", getter) + if m: + return strip_getter(m.group(1)) names = re.findall(r"\.(\w+)\s*\(\s*\)", getter) - return names[-1] if names else getter + return strip_getter(names[-1]) if names else getter def call_args(segment: str) -> tuple[str, str | None]: @@ -1210,6 +1226,18 @@ def _read_buffer_write( ) -> list[dict[str, Any]] | None: label = self._label(argv[0] if argv else "") + # An enum is a `VarInt`, but which numbers are meaningful is knowable + # and worth keeping: a field typed as the enum cannot hold a value the + # server would reject. + if name == "writeEnum" and argv: + wire = self._enum_of_field(argv[0], scope, by_id=False) + if wire is not None: + return [{"name": label, "wire": wire}] + if name == "writeVarInt" and argv: + wire = self._enum_of_field(argv[0], scope, by_id=True) + if wire is not None: + return [{"name": label, "wire": wire}] + if name == "writeUtf": limit = parse_int_literal(argv[1]) if len(argv) > 1 else 32767 return [{"name": label, "wire": prim("string", max=limit)}] @@ -1255,12 +1283,76 @@ def _enum_cardinality(self, expr: str, scope: Scope) -> int | None: target = self.index.resolve(m.group(1), scope.owner) if target is None: return None - body = self.index.body_of(target) + names = self._enum_constants(target) + return len(names) if names else None + + def _enum_constants(self, owner: str) -> list[str] | None: + """Constant names of a Java enum, in declaration order. + + Declaration order is what the wire needs: ``writeEnum`` sends + ``ordinal()``, so position *is* the discriminant unless the class + supplies its own ids. + """ + body = self.index.body_of(owner) + span = self.index.body_span(owner) + if body is None or span is None: + return None + jf, start, _ = span + head = jf.text[max(0, start - RECORD_HEADER_WINDOW) : start] + fqn, nested = split_owner(owner) + simple = nested or fqn.rsplit(".", 1)[-1] + if not re.search(rf"\benum\s+{re.escape(simple)}\b", head): + return None + names = [] + for entry in split_top_level(body.split(";", 1)[0]): + m = re.match(r"^([A-Z][A-Z0-9_]*)\s*(?:\(|$)", entry.strip()) + if m is None: + return None + names.append(m.group(1)) + return names or None + + def _enum_ids(self, owner: str) -> dict[str, int] | None: + """Explicit ids from a ``byId`` switch, where an enum has them. + + `ClientIntent` is sent as 1, 2, 3 rather than as 0, 1, 2, and the only + statement of that in the jar is the switch its `byId` is written as. + """ + body = self.method_body(owner, "byId") if body is None: return None - header = body.split(";", 1)[0] - constants = [c for c in split_top_level(header) if re.match(r"^[A-Z][A-Z0-9_]*\b", c.strip())] - return len(constants) or None + pairs = re.findall(r"case\s+(-?\d+)\s*->\s*([A-Z][A-Z0-9_]*)\s*;", body) + return {name: int(value) for value, name in pairs} or None + + def _enum_wire(self, owner: str, by_id: bool) -> Wire | None: + """A Rust-shaped enum: constant names and the number each one sends.""" + names = self._enum_constants(owner) + if names is None: + return None + ids = self._enum_ids(owner) if by_id else None + if by_id and ids is None: + return None + constants = [] + for index, name in enumerate(names): + value = index if ids is None else ids.get(name) + if value is None: + # A constant the id table does not mention would decode to the + # wrong variant, so the whole enum stays a plain `VarInt`. + return None + constants.append({"name": name, "value": value}) + return { + "kind": "enum", + "type": owner, + "constants": constants, + "note": "varint id" if ids else "varint ordinal", + } + + def _enum_of_field(self, expr: str, scope: Scope, by_id: bool) -> Wire | None: + """The enum behind ``this.hand`` or ``this.intention.id()``.""" + m = re.match(r"^this\.(\w+)" + (r"\.\w+\(\s*\)$" if by_id else r"$"), expr.strip()) + if m is None: + return None + owner = self._field_owner(scope.owner, m.group(1)) + return None if owner is None else self._enum_wire(owner, by_id) def _element_encoder(self, expr: str, scope: Scope, buf: str) -> Wire: """The element writer handed to writeCollection and friends.""" @@ -1310,13 +1402,22 @@ def _label(arg: str) -> str: """A readable field name for a written value. Only a label: the byte layout comes from the writer, never from this. + The field the value came out of is preferred over whatever was called + on it, so ``this.intention.id()`` is ``intention`` rather than ``id``. """ arg = arg.strip() + m = re.search(r"\bthis\.(\w+)", arg) + if m: + return m.group(1) + m = re.search(r"->\s*\w+\.(\w+)\s*(?:\(\s*\))?\s*$", arg) + if m: + return strip_getter(m.group(1)) matches = re.findall(r"\.(\w+)\s*\(\s*\)", arg) if matches: - return matches[-1] + return strip_getter(matches[-1]) matches = re.findall(r"\b(\w+)\b", arg) - return matches[-1] if matches else "" + return strip_getter(matches[-1]) if matches else "" + # --------------------------------------------------------------------------- # Packet table diff --git a/nix/generate-rust.py b/nix/generate-rust.py index 988e1f8ff..9e1b95975 100644 --- a/nix/generate-rust.py +++ b/nix/generate-rust.py @@ -292,7 +292,7 @@ def emit_mod(doc: dict) -> str: pub use data_component::DataComponent; pub use version::{MINECRAFT_VERSION, PROTOCOL_VERSION, WORLD_VERSION}; -pub use wire::{Field, Wire}; +pub use wire::{Constant, Field, Wire}; """ @@ -448,6 +448,12 @@ def value(self, wire: dict) -> str: if kind == "either": return "crate::generated::wire::Wire::Either {{ left: {}, right: {} }}".format( self.expr(wire["left"]), self.expr(wire["right"])) + if kind == "enum": + constants = ", ".join( + 'crate::generated::wire::Constant {{ name: "{}", value: {} }}'.format(c["name"], c["value"]) + for c in wire["constants"]) + return 'crate::generated::wire::Wire::Enum {{ java_class: "{}", constants: &[{}] }}'.format( + wire["type"], constants) if kind == "struct": members = ", ".join( 'crate::generated::wire::Field {{ name: "{}", wire: {} }}'.format(f["name"], self.expr(f["wire"])) @@ -497,6 +503,16 @@ def static_for(self, ref: str) -> str: pub wire: &'static Wire, } +/// One constant of a [`Wire::Enum`], and the number it is sent as. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Constant { + /// Mojang's own name for the constant. + pub name: &'static str, + /// The `VarInt` the server writes for it, which is the declaration index + /// unless the class supplies ids of its own. + pub value: i32, +} + /// The layout of a value on the wire. /// /// Variants name what the server writes rather than how a Rust value is @@ -572,37 +588,45 @@ def static_for(self, ref: str) -> str: /// Registry the id is scoped to. registry: &'static str, /// Layout written when the value is inline rather than referenced. - direct: &'static Wire, + direct: &'static Self, }, /// A boolean, then the value when it is present. - Optional(&'static Wire), + Optional(&'static Self), /// A `VarInt` count, then that many elements. List { /// Layout of one element. - element: &'static Wire, + element: &'static Self, /// Largest count the server accepts. max: Option, }, /// A `VarInt` byte length, then the value within it. LengthPrefixed { /// Layout of the value inside the length. - value: &'static Wire, + value: &'static Self, /// Largest byte length the server accepts. max: Option, }, /// A `VarInt` count, then that many key and value pairs. Map { /// Layout of a key. - key: &'static Wire, + key: &'static Self, /// Layout of a value. - value: &'static Wire, + value: &'static Self, }, /// A boolean selecting which of two layouts follows; true selects `left`. Either { /// Layout written when the discriminant is true. - left: &'static Wire, + left: &'static Self, /// Layout written when the discriminant is false. - right: &'static Wire, + right: &'static Self, + }, + /// A `VarInt` naming one of a fixed set of constants. + Enum { + /// Class the constants come from, e.g. + /// `net.minecraft.world.InteractionHand`. + java_class: &'static str, + /// Every constant, in declaration order. + constants: &'static [Constant], }, /// Fields written back to back with nothing between them. Struct(&'static [Field]), diff --git a/nix/minecraft-data.nix b/nix/minecraft-data.nix index 27d44f21f..e014ffce7 100644 --- a/nix/minecraft-data.nix +++ b/nix/minecraft-data.nix @@ -3,7 +3,10 @@ # Everything downstream of the pinned server jar is a sandboxed derivation, so a # protocol bump is a one-line change to nix/minecraft-version.json plus a # rebuild. The version is the only knob. -{ pkgs }: +# +# `rustfmt` is the repo's pinned toolchain rather than nixpkgs' own: rustfmt.toml +# turns on unstable options, which only the nightly binary accepts. +{ pkgs, rustfmt }: let inherit (pkgs) lib; @@ -101,11 +104,30 @@ let # world/item/component # net/minecraft/core/registries the registry key table, which is what # turns a registry id into a name + # any enum a packet references `writeEnum` sends an ordinal, so the + # declaration order of the constants is + # the discriminant table; enums like + # RecipeBookType carry no StreamCodec of + # their own and would otherwise be missed # - # This is 1137 of the jar's 7434 classes and costs about ten seconds; + # This is about a sixth of the jar's 7434 classes and costs ten seconds; # decompiling everything is minutes for sources nothing reads. + # + # An enum is recognised by `java/lang/Enum` in its constant pool, which + # is in the class file for every enum and for nothing else. + grep -rhoa 'net/minecraft/[A-Za-z0-9/$]*' --include='*.class' net/minecraft/network \ + | sed 's/$/.class/' | sort -u > referenced.txt + : > enums.txt + while read -r candidate; do + if [ -e "$candidate" ] && grep -qa 'java/lang/Enum' "$candidate"; then + printf '%s\n' "$candidate" >> enums.txt + fi + done < referenced.txt + echo " of which $(wc -l < enums.txt) are enums a packet names" >&2 + { find net/minecraft/network net/minecraft/core/registries -name '*.class' grep -rl StreamCodec --include='*.class' net/minecraft + cat enums.txt } | sed 's/\$[^/]*\.class$/.class/' | sort -u > outers.txt # Inner classes are decompiled into the file of their outermost class, so @@ -171,13 +193,19 @@ let # rustfmt runs here rather than on the committed copy: formatting the # copy alone would make it differ from what the generator emits, and the # staleness check compares the two. - nativeBuildInputs = [ codegen pkgs.rustfmt ]; + # + # It needs the repo's own rustfmt.toml. Without --config-path it would + # find no configuration in the sandbox and settle on defaults, so the + # committed tables would disagree with `cargo fmt` -- which rewrites them + # on the next run and fails `fmt --check` on a tree nobody has touched. + nativeBuildInputs = [ codegen rustfmt ]; meta.description = "Generated Rust protocol tables for Minecraft ${pin.id}"; } '' mkdir -p $out generate-minecraft-proto --protocol ${protocolJson}/protocol.json --out $out - find $out -name '*.rs' -exec rustfmt --edition 2024 {} + + find $out -name '*.rs' -exec \ + rustfmt --edition 2024 --config-path ${../rustfmt.toml} {} + ''; # Re-resolves Mojang's manifest and rewrites the pin. Impure by nature, so it @@ -261,19 +289,26 @@ let ''; }; - # Copies generated Rust into the crate. The output is committed so that a - # plain `cargo build` works without nix; the check below keeps the two honest. + # Copies the extraction into the crate: protocol.json, which build.rs turns + # into packet structs, and the tables that stay committed Rust. Both are in + # the tree so that a plain `cargo build` works without nix; the checks below + # are what make the committed copies trustworthy rather than merely present. syncScript = pkgs.writeShellApplication { name = "sync-minecraft-proto"; runtimeInputs = [ pkgs.coreutils pkgs.findutils pkgs.git ]; text = '' root=$(git rev-parse --show-toplevel) - dest="$root/crates/hyperion-minecraft-proto/src/generated" + crate="$root/crates/hyperion-minecraft-proto" + + install -m 644 ${protocolJson}/protocol.json "$crate/protocol.json" + + dest="$crate/src/generated" rm -rf "$dest" mkdir -p "$dest" cp -r ${generatedRust}/. "$dest/" chmod -R u+w "$dest" - echo "synced $(find "$dest" -type f | wc -l | tr -d ' ') files into $dest" >&2 + + echo "synced protocol.json and $(find "$dest" -type f | wc -l | tr -d ' ') tables into $crate" >&2 ''; }; @@ -291,6 +326,22 @@ let exit 1 fi ''; + + # protocol.json is the input build.rs reads, so a stale copy is a stale + # packet struct in every build that does not go through nix. Guarding it is + # what lets the structs live in OUT_DIR instead of in the tree. + protocolJsonUpToDate = pkgs.runCommand "check-minecraft-protocol-json" + { } + '' + committed=${../crates/hyperion-minecraft-proto/protocol.json} + if diff -u "$committed" ${protocolJson}/protocol.json > diff.txt 2>&1; then + touch $out + else + echo "committed protocol.json is stale; run: nix run .#sync-minecraft-proto" >&2 + head -n 200 diff.txt >&2 + exit 1 + fi + ''; in { inherit @@ -304,6 +355,7 @@ in updateScript syncScript generatedUpToDate + protocolJsonUpToDate ; inherit pin; }