diff --git a/crates/core/item/Cargo.toml b/crates/core/item/Cargo.toml index 3bf7522..8c85a90 100644 --- a/crates/core/item/Cargo.toml +++ b/crates/core/item/Cargo.toml @@ -2,7 +2,10 @@ name = "rimecraft-item" version = "0.1.0" edition = "2021" -authors = ["JieningYu "] +authors = [ + "JieningYu ", + "C191239 ", +] description = "Minecraft item primitives" repository = "https://github.com/rimecraft-rs/rimecraft/" license = "AGPL-3.0-or-later" diff --git a/crates/util/edcode-derive-tests/Cargo.toml b/crates/util/edcode-derive-tests/Cargo.toml new file mode 100644 index 0000000..82357c9 --- /dev/null +++ b/crates/util/edcode-derive-tests/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rimecraft-edcode-derive-tests" +version = "0.1.0" +edition = "2021" +authors = ["C191239 "] +description = "Tests for rimecraft-edcode-derive" +repository = "https://github.com/rimecraft-rs/rimecraft/" +license = "AGPL-3.0-or-later" +categories = ["encoding"] + +[badges] +maintenance = { status = "passively-maintained" } + +[dev-dependencies] +rimecraft-edcode = { path = "../edcode", features = ["derive"] } + +[lints] +workspace = true diff --git a/crates/util/edcode-derive-tests/src/lib.rs b/crates/util/edcode-derive-tests/src/lib.rs new file mode 100644 index 0000000..851525a --- /dev/null +++ b/crates/util/edcode-derive-tests/src/lib.rs @@ -0,0 +1,23 @@ +//! Tests for `rimecraft-edcode-derive` crate. + +#[cfg(test)] +mod tests { + use rimecraft_edcode::{bytes::BytesMut, Decode, Encode}; + + #[test] + #[allow(dead_code)] + fn derive_enum() { + #[derive(Encode, Decode, PartialEq, Eq)] + #[repr(u8)] + enum Topics { + Pearl = 15, + Lakers = 24, + Kim = 3, + Someone = 36, + } + + let mut buf = BytesMut::new(); + assert!(Topics::Someone.encode(&mut buf).is_ok()); + assert!(Topics::decode(buf).is_ok_and(|x| x == Topics::Someone)); + } +} diff --git a/crates/util/edcode-derive/Cargo.toml b/crates/util/edcode-derive/Cargo.toml new file mode 100644 index 0000000..7ff9705 --- /dev/null +++ b/crates/util/edcode-derive/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "rimecraft-edcode-derive" +version = "0.1.0" +edition = "2021" +authors = ["C191239 "] +description = "Derive macros for rimecraft-edcode" +repository = "https://github.com/rimecraft-rs/rimecraft/" +license = "AGPL-3.0-or-later" +categories = ["encoding"] + +[badges] +maintenance = { status = "passively-maintained" } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = "2.0" +quote = "1.0" +proc-macro2 = "1.0" + +[lints] +workspace = true + +[lib] +proc-macro = true diff --git a/crates/util/edcode-derive/src/lib.rs b/crates/util/edcode-derive/src/lib.rs new file mode 100644 index 0000000..bac2a9f --- /dev/null +++ b/crates/util/edcode-derive/src/lib.rs @@ -0,0 +1,209 @@ +//! Proc-macros for deriving `rimecraft_edcode` traits. +//! +//! __You shouldn't use this crate directly__, use `rimecraft_edcode` crate +//! with `derive` feature flag instead. + +use proc_macro::TokenStream; +use proc_macro2::TokenTree; +use quote::quote; +use syn::{ + parse_macro_input, spanned::Spanned, Attribute, Data, DataEnum, DeriveInput, Error, Expr, + Fields, Ident, Meta, +}; + +macro_rules! unsupported_object { + ($tr:literal, $ty:literal) => { + concat!("deriving `", $tr, "` to `", $ty, "` is not supported") + }; +} + +macro_rules! fields_disallowed { + () => { + "variants with fields are not supported" + }; +} + +macro_rules! discriminant_required { + () => { + "variants must have explicit discriminant" + }; +} + +macro_rules! unsupported_repr { + () => { + "only `u*` and `i*` (excluding 128-bit types) repr is supported" + }; +} + +macro_rules! repr_required { + () => { + "must specify repr" + }; +} + +/// Common parsing code for deriving to `enum`. +fn parse_derive_enum( + ident: Ident, + attrs: Vec, + data: DataEnum, +) -> Result<(Ident, Ident, Vec, Vec), TokenStream> { + let mut enum_idents: Vec = vec![]; + let mut enum_vals: Vec = vec![]; + for var in data.variants.into_iter() { + if matches!(var.fields, Fields::Unit) { + enum_idents.push(var.ident.clone()); + } else { + return Err(Error::new(var.fields.span(), fields_disallowed!()) + .into_compile_error() + .into()); + } + let has_disc_err = !var.discriminant.clone().is_some_and(|(_, e)| { + enum_vals.push(e); + true + }); + if has_disc_err { + return Err(Error::new(var.span(), discriminant_required!()) + .into_compile_error() + .into()); + } + } + let mut repr_type: Option = None; + for attr in attrs { + if let Meta::List(meta) = attr.meta { + let is_repr = meta + .path + .require_ident() + .is_ok_and(|id| id == &Ident::new("repr", id.span())); + if is_repr { + let mut iter = meta.tokens.into_iter().peekable(); + let span = iter.peek().span(); + macro_rules! ident_helper { + ($span:expr => $( $ty:ident ),*) => { + vec![ + $( Ident::new(stringify!($ty), $span) ),* + ] + }; + } + let supported = iter.next().is_some_and(|x| { + if let TokenTree::Ident(id) = x { + if ident_helper!(span => u8, u16, u32, u64, i8, i16, i32, i64).contains(&id) + { + repr_type = Some(id); + true + } else { + false + } + } else { + false + } + }); + if !supported { + return Err(Error::new(span, unsupported_repr!()) + .into_compile_error() + .into()); + } + } + } + } + let repr_type = repr_type.ok_or_else(|| { + std::convert::Into::::into( + Error::new(ident.span(), repr_required!()).into_compile_error(), + ) + })?; + Ok((ident, repr_type, enum_idents, enum_vals)) +} + +/// Derive `rimecraft_edcode::Encode` to objects. +/// +/// # Enum +/// +/// ## Requirements: +/// - All variants must be field-less. +/// - Enum must explicitly specify its representation through `#[repr()]`, and +/// only `u*` and `i*` (excluding 128-bit types) repr are allowed. +/// - All variants must have explicit discriminant. +#[proc_macro_derive(Encode)] +pub fn derive_encode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match input.data { + Data::Enum(data) => { + let (ident, repr_type, enum_idents, enum_vals) = + match parse_derive_enum(input.ident, input.attrs, data) { + Ok(x) => x, + Err(err) => return err, + }; + let expanded = quote! { + impl ::rimecraft_edcode::Encode for #ident { + fn encode(&self, mut buf: B) -> Result<(), std::io::Error> + where + B: ::rimecraft_edcode::bytes::BufMut, + { + let x:#repr_type = match self { + #( Self::#enum_idents => #enum_vals, )* + }; + ::rimecraft_edcode::Encode::encode(&x, &mut buf)?; + Ok(()) + } + } + }; + expanded.into() + } + Data::Struct(data) => Error::new( + data.struct_token.span, + unsupported_object!("Encode", "struct"), + ) + .to_compile_error() + .into(), + Data::Union(data) => Error::new( + data.union_token.span, + unsupported_object!("Encode", "union"), + ) + .to_compile_error() + .into(), + } +} + +/// Derive `rimecraft_edcode::Decode` to objects. +#[proc_macro_derive(Decode)] +pub fn derive_decode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match input.data { + Data::Enum(data) => { + let (ident, repr_type, enum_idents, enum_vals) = + match parse_derive_enum(input.ident, input.attrs, data) { + Ok(x) => x, + Err(err) => { + return err; + } + }; + let expanded = quote! { + impl ::rimecraft_edcode::Decode for #ident { + fn decode(mut buf: B) -> Result + where + B: ::rimecraft_edcode::bytes::Buf, + { + let x:#repr_type = ::rimecraft_edcode::Decode::decode(&mut buf)?; + let var = match x { + #( #enum_vals => Self::#enum_idents, )* + unknown => return Err(std::io::Error::other(format!("unknown variant {}", unknown))), + }; + Ok(var) + } + } + }; + expanded.into() + } + Data::Struct(data) => Error::new( + data.struct_token.span, + unsupported_object!("Decode", "struct"), + ) + .to_compile_error() + .into(), + Data::Union(data) => Error::new( + data.union_token.span, + unsupported_object!("Decode", "union"), + ) + .into_compile_error() + .into(), + } +} diff --git a/crates/util/edcode/Cargo.toml b/crates/util/edcode/Cargo.toml index 2c0cdfb..e9764ba 100644 --- a/crates/util/edcode/Cargo.toml +++ b/crates/util/edcode/Cargo.toml @@ -14,6 +14,7 @@ maintenance = { status = "passively-maintained" } [dependencies] bytes = "1.5" serde = { version = "1.0", optional = true } +rimecraft-edcode-derive = { path = "../edcode-derive", optional = true } # custom formats fastnbt = { version = "2.4", optional = true } serde_json = { version = "1.0", optional = true } @@ -24,6 +25,7 @@ glam = { version = "0.25", optional = true } [features] # default = ["serde", "nbt", "json", "uuid", "glam"] serde = ["dep:serde"] +derive = ["dep:rimecraft-edcode-derive"] # custom formats fastnbt = ["serde", "dep:fastnbt"] json = ["serde", "dep:serde_json"] diff --git a/crates/util/edcode/src/lib.rs b/crates/util/edcode/src/lib.rs index 0eedda4..57838c3 100644 --- a/crates/util/edcode/src/lib.rs +++ b/crates/util/edcode/src/lib.rs @@ -9,6 +9,9 @@ use std::io; pub use bytes; +#[cfg(feature = "derive")] +pub use rimecraft_edcode_derive::{Decode, Encode}; + /// Describes types that can be encoded into a packet buffer. pub trait Encode { /// Encode into a buffer.