feat(codec): implement Encode and Decode for some pumpkin-util items and convenience codec macros (#2111)

* added identifier codec

* added codec impls for `Vector2<T>` and `Vector3<T>`

* added codec impl for `BlockPos`

* added codec impls for `BlockBox` and `EulerAngle`

* fixed formatting and clippy

* added `BlockPos` test

* fixed clippy and formatting

* fixed codec mapping macros and started using them in tests

* fixed formatting
This commit is contained in:
SomeYellowGuy
2026-05-15 12:15:49 +05:30
committed by GitHub
parent b2b10f3323
commit f383c88c76
13 changed files with 440 additions and 47 deletions

1
Cargo.lock generated
View File

@@ -3008,6 +3008,7 @@ dependencies = [
"num-traits",
"p384",
"proc-macro2",
"pumpkin-codecs",
"pumpkin-nbt",
"quote",
"rsa",

View File

@@ -133,6 +133,75 @@ where
}
}
/// A wrapper around a `Vec` that cannot have it be empty, similar to Minecraft's `ExtraCodecs.nonEmptyList`.
pub struct NonEmptyVec<T>(Vec<T>);
impl<T> From<NonEmptyVec<T>> for Vec<T> {
/// Returns the wrapped `Vec` of this `NonEmptyVec`.
fn from(value: NonEmptyVec<T>) -> Self {
value.0
}
}
impl<T> Encode for NonEmptyVec<T>
where
T: Encode,
{
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
if self.0.is_empty() {
DataResult::new_error("List must have contents")
} else {
self.0.encode(ops, prefix)
}
}
}
impl<T> Decode for NonEmptyVec<T>
where
T: Decode,
{
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
Vec::<T>::decode(input, ops).flat_map(|(v, c)| Self::flat_try_from(v).map(|v| (v, c)))
}
}
impl<T> FlatTryFrom<Vec<T>> for NonEmptyVec<T> {
fn flat_try_from(value: Vec<T>) -> DataResult<Self> {
if value.is_empty() {
DataResult::new_error("List must have contents")
} else {
DataResult::new_success(Self(value))
}
}
}
// Utility functions
/// Tries to check a list to have a fixed size `size`, returning the appropriate
/// [`DataResult`].
///
/// # Arguments
/// - `list`: The list to validate.
/// - `size`: The required size.
///
/// # Returns
/// A successful result if `list.len() == size`; otherwise, it returns a partial/non-result.
/// If this result is partial, it will also be `size` elements long.
pub fn validate_fixed_size<T>(list: Vec<T>, size: usize) -> DataResult<Vec<T>> {
if list.len() == size {
DataResult::new_success(list)
} else {
let message = format!("Input is not a list of {size} elements");
if list.len() > size {
DataResult::new_partial_error(message, list.into_iter().take(size).collect())
} else {
DataResult::new_error(message)
}
}
}
//
#[cfg(test)]
mod test {
use crate::assert_decode;

View File

@@ -99,7 +99,10 @@ where
mod test {
use crate::codec::*;
use crate::json_ops::JsonOps;
use crate::{assert_decode, assert_encode_success};
use crate::{
FlatTryFrom, assert_decode, assert_encode_success, comap_flat_map_codec_impl,
flat_xmap_codec_impl,
};
use serde_json::json;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
@@ -140,32 +143,25 @@ mod test {
}
}
impl Encode for StringInteger {
fn encode<O: DynamicOps>(
&self,
ops: &'static O,
prefix: O::Value,
) -> DataResult<O::Value> {
impl From<&StringInteger> for String {
fn from(value: &StringInteger) -> Self {
// This will always succeed.
self.0.to_string().encode(ops, prefix)
value.0.to_string()
}
}
impl Decode for StringInteger {
fn decode<O: DynamicOps>(
input: O::Value,
ops: &'static O,
) -> DataResult<(Self, O::Value)> {
String::decode(input, ops).flat_map(|s| {
// Try to parse an integer.
s.0.parse().map_or_else(
|_| DataResult::new_error("Could not parse String"),
|i| DataResult::new_success((Self(i), s.1)),
)
})
impl FlatTryFrom<String> for StringInteger {
fn flat_try_from(value: String) -> DataResult<Self> {
// Try to parse an integer.
value.parse().map_or_else(
|_| DataResult::new_error("Could not parse String"),
|i| DataResult::new_success(Self(i)),
)
}
}
comap_flat_map_codec_impl!(String => StringInteger, StringInteger::flat_try_from, String::from);
let mut map = HashMap::<StringInteger, bool>::new();
// Calculate the map for the first 20 numbers.
@@ -198,41 +194,31 @@ mod test {
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Hash)]
struct Letter(String);
fn check_letter(string: String) -> DataResult<Letter> {
// Try to parse a single letter.
if string.len() == 1 {
DataResult::new_success(Letter(string))
} else {
DataResult::new_error(format!("Not a letter: {string}"))
}
}
impl Display for Letter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Encode for Letter {
fn encode<O: DynamicOps>(
&self,
ops: &'static O,
prefix: O::Value,
) -> DataResult<O::Value> {
// This will always succeed.
check_letter(self.0.clone()).flat_map(|l| l.0.encode(ops, prefix))
impl FlatTryFrom<String> for Letter {
fn flat_try_from(value: String) -> DataResult<Self> {
// Try to parse a single letter.
if value.len() == 1 {
DataResult::new_success(Self(value))
} else {
DataResult::new_error(format!("Not a letter: {value}"))
}
}
}
impl Decode for Letter {
fn decode<O: DynamicOps>(
input: O::Value,
ops: &'static O,
) -> DataResult<(Self, O::Value)> {
String::decode(input, ops).flat_map(|(s, v)| check_letter(s).map(|l| (l, v)))
impl FlatTryFrom<&Letter> for String {
fn flat_try_from(value: &Letter) -> DataResult<Self> {
Letter::flat_try_from(value.0.clone()).map(|l| l.0)
}
}
flat_xmap_codec_impl!(String => Letter, Letter::flat_try_from, String::flat_try_from);
type LetterData = HashMap<Letter, u64>;
let mut map = LetterData::new();

View File

@@ -140,3 +140,121 @@ impl<T: Decode> FieldDecode for T {
decoded_option.map(|o| o.unwrap_or(default))
}
}
/// Provides a fallible/infallible encode conversion from a first type to a second type via `backward`.
///
/// You probably do not need to use this directly.
#[macro_export]
macro_rules! encode_impl {
(infallible $second_type:ty, $backward:path) => {
impl $crate::Encode for $second_type {
fn encode<O: $crate::DynamicOps>(
&self,
ops: &'static O,
prefix: O::Value,
) -> $crate::DataResult<O::Value> {
$backward(self).encode(ops, prefix)
}
}
};
(fallible $second_type:ty, $backward:path) => {
impl $crate::Encode for $second_type {
fn encode<O: $crate::DynamicOps>(
&self,
ops: &'static O,
prefix: O::Value,
) -> $crate::DataResult<O::Value> {
$backward(self).flat_map(|m| m.encode(ops, prefix))
}
}
};
}
/// Provides a fallible/infallible decode conversion from a first type to a second type via `forward`.
///
/// You probably do not need to use this directly.
#[macro_export]
macro_rules! decode_impl {
(infallible $first_type:ty, $second_type:ty, $forward:path) => {
impl $crate::Decode for $second_type {
fn decode<O: $crate::DynamicOps>(
input: O::Value,
ops: &'static O,
) -> $crate::DataResult<(Self, O::Value)> {
<$first_type>::decode(input, ops).map(|(s, p)| ($forward(s), p))
}
}
};
(fallible $first_type:ty, $second_type:ty, $forward:path) => {
impl $crate::Decode for $second_type {
fn decode<O: $crate::DynamicOps>(
input: O::Value,
ops: &'static O,
) -> $crate::DataResult<(Self, O::Value)> {
<$first_type>::decode(input, ops).flat_map(|(s, p)| $forward(s).map(|m| (m, p)))
}
}
};
}
/// Provides easy `xmap`-like `Encode` and `Decode` implementations of a *second type*
/// by using the already-existing implementations of a *first type*.
///
/// The macro is written as `xmap_codec_impl!(first => second, forward, backward)`,
/// where:
/// - `forward` is the infallible conversion `fn(first) -> second` (for decoding).
/// - `backward` is the infallible conversion `fn(&second) -> first` (for encoding).
#[macro_export]
macro_rules! xmap_codec_impl {
($first_type:ty => $second_type:ty, $forward:path, $backward:path) => {
$crate::encode_impl!(infallible $second_type, $backward);
$crate::decode_impl!(infallible $first_type, $second_type, $forward);
};
}
/// Provides easy `comapFlatMap`-like `Encode` and `Decode` implementations of a *second type*
/// by using the already-existing implementations of a *first type*.
///
/// The macro is written as `comap_flat_map_codec_impl!(first => second, forward, backward)`,
/// where:
/// - `forward` is the fallible conversion `fn(first) -> DataResult<second>` (for decoding).
/// - `backward` is the infallible conversion `fn(&second) -> first` (for encoding).
#[macro_export]
macro_rules! comap_flat_map_codec_impl {
($first_type:ty => $second_type:ty, $forward:path, $backward:path) => {
$crate::encode_impl!(infallible $second_type, $backward);
$crate::decode_impl!(fallible $first_type, $second_type, $forward);
};
}
/// Provides easy `flatComapMap`-like `Encode` and `Decode` implementations of a *second type*
/// by using the already-existing implementations of a *first type*.
///
/// The macro is written as `flat_comap_map_codec_impl!(first => second, forward, backward)`,
/// where:
/// - `forward` is the infallible conversion `fn(first) -> second` (for decoding).
/// - `backward` is the fallible conversion `fn(&second) -> DataResult<first>` (for encoding).
#[macro_export]
macro_rules! flat_comap_map_codec_impl {
($first_type:ty => $second_type:ty, $forward:path, $backward:path) => {
$crate::encode_impl!(fallible $second_type, $backward);
$crate::decode_impl!(infallible $first_type, $second_type, $forward);
};
}
/// Provides easy `flatXmap`-like `Encode` and `Decode` implementations of a *second type*
/// by using the already-existing implementations of a *first type*.
///
/// The macro is written as `flat_xmap_codec_impl!(first => second, forward, backward)`,
/// where:
/// - `forward` is the fallible conversion `fn(first) -> DataResult<second>` (for decoding).
/// - `backward` is the fallible conversion `fn(&second) -> DataResult<first>` (for encoding).
#[macro_export]
macro_rules! flat_xmap_codec_impl {
($first_type:ty => $second_type:ty, $forward:path, $backward:path) => {
$crate::encode_impl!(fallible $second_type, $backward);
$crate::decode_impl!(fallible $first_type, $second_type, $forward);
};
}

View File

@@ -830,6 +830,7 @@ dependencies = [
"num-traits",
"p384",
"proc-macro2",
"pumpkin-codecs",
"pumpkin-nbt",
"quote",
"rsa",

View File

@@ -7,6 +7,7 @@ license.workspace = true
[dependencies]
pumpkin-nbt.workspace = true
pumpkin-codecs.workspace = true
serde.workspace = true
serde_json.workspace = true
bytes.workspace = true

View File

@@ -1,5 +1,6 @@
use std::{borrow::Cow, fmt::Display};
use pumpkin_codecs::{DataResult, FlatTryFrom, comap_flat_map_codec_impl};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -382,9 +383,23 @@ impl<'de> Deserialize<'de> for Identifier {
}
}
comap_flat_map_codec_impl!(String => Identifier, Identifier::flat_try_from, ToString::to_string);
impl FlatTryFrom<String> for Identifier {
fn flat_try_from(value: String) -> DataResult<Self> {
Self::parse(&value).map_or_else(
|_| DataResult::new_error(format!("Not a valid resource location: {value}")),
DataResult::new_success,
)
}
}
#[cfg(test)]
mod test {
use crate::identifier::{Identifier, IdentifierError};
use pumpkin_codecs::json_ops::JsonOps;
use pumpkin_codecs::{assert_decode, assert_encode_success};
use serde_json::json;
#[test]
fn new() -> Result<(), IdentifierError> {
@@ -416,4 +431,28 @@ mod test {
Ok(())
}
#[test]
fn codec() {
assert_encode_success!(
Identifier::from_static("abc", "def"),
JsonOps,
json!("abc:def")
);
assert_encode_success!(
Identifier::from_static("", "no_namespace"),
JsonOps,
json!(":no_namespace")
);
assert_encode_success!(
Identifier::vanilla_static("example"),
JsonOps,
json!("minecraft:example")
);
assert_decode!(Identifier, json!("abc:def"), JsonOps, is_success);
assert_decode!(Identifier, json!("vanilla"), JsonOps, is_success);
assert_decode!(Identifier, json!("2 + 3"), JsonOps, is_error);
assert_decode!(Identifier, json!("a._b-c:/4_-/5.9"), JsonOps, is_success);
}
}

View File

@@ -5,6 +5,8 @@ use crate::{
vector3::{Axis, Vector3},
},
};
use pumpkin_codecs::codec::list::validate_fixed_size;
use pumpkin_codecs::{DataResult, FlatTryFrom, IntStream, comap_flat_map_codec_impl};
/// Represents an axis-aligned 3D block bounding box in integer coordinates.
#[derive(Clone, Copy, Debug)]
@@ -262,3 +264,34 @@ impl BlockBox {
Some(result)
}
}
impl From<&BlockBox> for IntStream {
fn from(value: &BlockBox) -> Self {
let Vector3 {
x: min_x,
y: min_y,
z: min_z,
} = value.min;
let Vector3 {
x: max_x,
y: max_y,
z: max_z,
} = value.max;
Self(vec![min_x, min_y, min_z, max_x, max_y, max_z])
}
}
impl FlatTryFrom<IntStream> for BlockBox {
fn flat_try_from(value: IntStream) -> DataResult<Self> {
validate_fixed_size(value.0, 6).map(|v| {
let [min_x, min_y, min_z, max_x, max_y, max_z]: [i32; 6] =
v.try_into().unwrap_or_else(|_| unreachable!());
Self {
min: Vector3::new(min_x, min_y, min_z),
max: Vector3::new(max_x, max_y, max_z),
}
})
}
}
comap_flat_map_codec_impl!(IntStream => BlockBox, BlockBox::flat_try_from, IntStream::from);

View File

@@ -1,3 +1,5 @@
use pumpkin_codecs::codec::list::validate_fixed_size;
use pumpkin_codecs::{DataResult, FlatTryFrom, comap_flat_map_codec_impl};
use pumpkin_nbt::tag::NbtTag;
use serde::{Deserialize, Serialize};
@@ -81,3 +83,25 @@ impl From<NbtTag> for EulerAngle {
Self::ZERO
}
}
impl From<&EulerAngle> for Vec<f32> {
fn from(value: &EulerAngle) -> Self {
let EulerAngle { pitch, yaw, roll } = value;
vec![*pitch, *yaw, *roll]
}
}
impl FlatTryFrom<Vec<f32>> for EulerAngle {
fn flat_try_from(value: Vec<f32>) -> DataResult<Self> {
validate_fixed_size(value, 6).map(|v| {
let [x, y, z]: [f32; 3] = v.try_into().unwrap_or_else(|_| unreachable!());
Self {
pitch: x,
yaw: y,
roll: z,
}
})
}
}
comap_flat_map_codec_impl!(Vec<f32> => EulerAngle, EulerAngle::flat_try_from, Vec::<f32>::from);

View File

@@ -404,6 +404,41 @@ pub fn java_string_hash(string: &str) -> i32 {
result
}
macro_rules! vector_codec_impl {
($vector:ty, $number:literal, $($components:ident),+ ) => {
impl<T> From<&$vector> for Vec<T> where T: Clone {
fn from(value: &$vector) -> Self {
vec![
$( value.$components.clone(), )+
]
}
}
impl<T> FlatTryFrom<Vec<T>> for $vector {
fn flat_try_from(value: Vec<T>) -> DataResult<Self> {
validate_fixed_size(value, 2)
.map(|v| {
let [ $( $components, )+ ]: [T; $number] = v.try_into().unwrap_or_else(|_| unreachable!());
Self { $( $components, )+ }
})
}
}
impl<T> Encode for $vector where T: Encode + Clone {
fn encode<O: DynamicOps>(&self, ops: &'static O, prefix: O::Value) -> DataResult<O::Value> {
Vec::<T>::from(self).encode(ops, prefix)
}
}
impl<T> Decode for $vector where T: Decode {
fn decode<O: DynamicOps>(input: O::Value, ops: &'static O) -> DataResult<(Self, O::Value)> {
<Vec<T>>::decode(input, ops).flat_map(|(s, p)| Self::flat_try_from(s).map(|v| (v, p)))
}
}
};
}
pub(crate) use vector_codec_impl;
/// Tests the Java-style string and array hash implementations.
///
/// This verifies that `java_string_hash` and `java_array_hash` produce the expected

View File

@@ -7,6 +7,8 @@ use std::hash::Hash;
use crate::math::vector2::Vector2;
use num_traits::Euclid;
use pumpkin_codecs::codec::list::validate_fixed_size;
use pumpkin_codecs::{DataResult, FlatTryFrom, IntStream, comap_flat_map_codec_impl};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// An iterator that yields all `BlockPos` positions within a cuboid region.
@@ -631,6 +633,24 @@ impl<'de> Deserialize<'de> for BlockPos {
}
}
impl From<&BlockPos> for IntStream {
fn from(value: &BlockPos) -> Self {
let Vector3 { x, y, z } = value.0;
Self(vec![x, y, z])
}
}
impl FlatTryFrom<IntStream> for BlockPos {
fn flat_try_from(value: IntStream) -> DataResult<Self> {
validate_fixed_size(value.0, 3).map(|v| {
let [x, y, z]: [i32; 3] = v.try_into().unwrap_or_else(|_| unreachable!());
Self(Vector3::new(x, y, z))
})
}
}
comap_flat_map_codec_impl!(IntStream => BlockPos, BlockPos::flat_try_from, IntStream::from);
impl fmt::Display for BlockPos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}, {}", self.0.x, self.0.y, self.0.z)
@@ -685,3 +705,61 @@ pub const fn pack_local_chunk_section(block_pos: &BlockPos) -> i16 {
let y = get_local_cord(block_pos.0.y);
vector3::packed_local(&Vector3::new(x, y, z))
}
#[cfg(test)]
mod test {
use crate::math::position::BlockPos;
use pumpkin_codecs::{assert_decode, assert_encode_success};
use pumpkin_nbt::nbt_ops::NbtOps;
use pumpkin_nbt::tag::NbtTag;
#[test]
fn codec() {
assert_encode_success!(
BlockPos::new(1, 2, 3),
NbtOps,
NbtTag::IntArray(vec![1, 2, 3])
);
assert_encode_success!(
BlockPos::new(-1000, 200, 4521),
NbtOps,
NbtTag::IntArray(vec![-1000, 200, 4521])
);
assert_decode!(
BlockPos,
NbtTag::IntArray(vec![1, 2, 3]),
NbtOps,
is_success
);
assert_decode!(BlockPos, NbtTag::List(vec![]), NbtOps, is_error);
assert_decode!(
BlockPos,
NbtTag::List(vec![NbtTag::Int(1), NbtTag::Float(2.0), NbtTag::Int(3)]),
NbtOps,
is_success
);
assert_decode!(
BlockPos,
NbtTag::List(vec![
NbtTag::Int(1),
NbtTag::Float(2.0),
NbtTag::String("69".to_string())
]),
NbtOps,
is_error
);
assert_decode!(
BlockPos,
NbtTag::List(vec![
NbtTag::Int(1),
NbtTag::Float(2.0),
NbtTag::Int(3),
NbtTag::Byte(3)
]),
NbtOps,
is_error
);
}
}

View File

@@ -1,9 +1,11 @@
use std::ops::{Add, Div, Mul, Neg, Sub};
use super::vector3::Vector3;
use crate::math::vector_codec_impl;
use bytes::BufMut;
use num_traits::Float;
use super::vector3::Vector3;
use pumpkin_codecs::codec::list::validate_fixed_size;
use pumpkin_codecs::{DataResult, Decode, DynamicOps, Encode, FlatTryFrom};
/// A 2-dimensional vector with generic numeric components.
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Default)]
@@ -182,3 +184,5 @@ impl serde::Serialize for Vector2<f32> {
serializer.serialize_bytes(&buf)
}
}
vector_codec_impl!(Vector2<T>, 2, x, y);

View File

@@ -1,10 +1,12 @@
use bytes::BufMut;
use std::ops::{Add, AddAssign, Div, Mul, Sub};
use num_traits::{Float, Num};
use super::position::BlockPos;
use super::vector2::Vector2;
use crate::math::vector_codec_impl;
use num_traits::{Float, Num};
use pumpkin_codecs::codec::list::validate_fixed_size;
use pumpkin_codecs::{DataResult, Decode, DynamicOps, Encode, FlatTryFrom};
/// A 3-dimensional vector with components of type `T`.
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Default)]
@@ -764,6 +766,8 @@ impl serde::Serialize for Vector3<i32> {
}
}
vector_codec_impl!(Vector3<T>, 3, x, y, z);
/// Packs a chunk position vector into a single 64-bit integer.
///
/// The packing format is: