aboutsummaryrefslogtreecommitdiff
path: root/azalea-core/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-08-10 18:55:23 -0500
committerGitHub <noreply@github.com>2025-08-10 18:55:23 -0500
commit7120842f9d2c659a2f12d8922299c2a761bc5582 (patch)
tree0d7976ceec82d914e4c75f23adcdd5839f9960a4 /azalea-core/src
parent3b659833c1ad4cca89b4cd553193edcb6d223163 (diff)
downloadazalea-drasl-7120842f9d2c659a2f12d8922299c2a761bc5582.tar.xz
Send correct data component checksums (#234)
* start implementing data component crc32 hashes * start doing serde impls for checksums * make more components hashable * make all data components serializable * support recursive components * fix simdnbt dep * update changelog * clippy
Diffstat (limited to 'azalea-core/src')
-rw-r--r--azalea-core/src/checksum.rs823
-rw-r--r--azalea-core/src/codec_utils.rs83
-rw-r--r--azalea-core/src/direction.rs8
-rw-r--r--azalea-core/src/filterable.rs3
-rw-r--r--azalea-core/src/hit_result.rs19
-rw-r--r--azalea-core/src/lib.rs2
-rw-r--r--azalea-core/src/position.rs39
-rw-r--r--azalea-core/src/resource_location.rs3
-rw-r--r--azalea-core/src/sound.rs3
9 files changed, 957 insertions, 26 deletions
diff --git a/azalea-core/src/checksum.rs b/azalea-core/src/checksum.rs
new file mode 100644
index 00000000..8265906f
--- /dev/null
+++ b/azalea-core/src/checksum.rs
@@ -0,0 +1,823 @@
+use std::{cmp::Ordering, fmt, hash::Hasher};
+
+use azalea_buf::AzBuf;
+use crc32c::Crc32cHasher;
+use serde::{Serialize, ser};
+use thiserror::Error;
+use tracing::error;
+
+use crate::{registry_holder::RegistryHolder, resource_location::ResourceLocation};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, AzBuf)]
+pub struct Checksum(pub u32);
+
+pub struct ChecksumSerializer<'a, 'r> {
+ hasher: &'a mut Crc32cHasher,
+ registries: &'r RegistryHolder,
+}
+impl<'a, 'r> ChecksumSerializer<'a, 'r> {
+ pub fn checksum(&mut self) -> Checksum {
+ Checksum(self.hasher.finish() as u32)
+ }
+}
+
+impl<'a, 'r> ser::Serializer for ChecksumSerializer<'a, 'r> {
+ type Ok = ();
+
+ // The error type when some error occurs during serialization.
+ type Error = ChecksumError;
+
+ type SerializeSeq = ChecksumListSerializer<'a, 'r>;
+ type SerializeTuple = ChecksumListSerializer<'a, 'r>;
+ type SerializeTupleStruct = ChecksumListSerializer<'a, 'r>;
+ type SerializeTupleVariant = ChecksumMapSerializer<'a, 'r>;
+ type SerializeMap = ChecksumMapSerializer<'a, 'r>;
+ type SerializeStruct = ChecksumMapSerializer<'a, 'r>;
+ type SerializeStructVariant = ChecksumMapSerializer<'a, 'r>;
+
+ fn serialize_bool(self, v: bool) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(13);
+ self.hasher.write(&[v as u8]);
+ Ok(())
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ assert!(self.hasher.finish() == 0);
+ Ok(ChecksumMapSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ entries: Vec::new(),
+ })
+ }
+ fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
+ assert!(self.hasher.finish() == 0);
+ self.serialize_map(Some(len))
+ }
+
+ fn serialize_i8(self, v: i8) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(6);
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+
+ fn serialize_i16(self, v: i16) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(7);
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+
+ fn serialize_i32(self, v: i32) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(8);
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+
+ fn serialize_i64(self, v: i64) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(9);
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+
+ fn serialize_u8(self, v: u8) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.serialize_i8(v as i8)
+ }
+
+ fn serialize_u16(self, v: u16) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.serialize_i16(v as i16)
+ }
+
+ fn serialize_u32(self, v: u32) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.serialize_i32(v as i32)
+ }
+
+ fn serialize_u64(self, v: u64) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_f32(self, v: f32) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(10);
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+
+ fn serialize_f64(self, v: f64) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(11);
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+
+ fn serialize_char(self, v: char) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.serialize_u32(v as u32)
+ }
+
+ fn serialize_str(self, v: &str) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(12);
+ let utf16 = v.encode_utf16().collect::<Vec<_>>();
+ self.hasher.write(&(utf16.len() as u32).to_le_bytes());
+ for c in utf16 {
+ self.hasher.write(&c.to_le_bytes());
+ }
+ Ok(())
+ }
+
+ fn serialize_bytes(self, v: &[u8]) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ self.hasher.write_u8(14);
+ self.hasher.write(v);
+ self.hasher.write_u8(15);
+ Ok(())
+ }
+
+ fn serialize_none(self) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ println!("serialize none");
+ self.hasher.write_u8(1);
+ Ok(())
+ }
+
+ fn serialize_some<T>(self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ // check if t
+
+ value.serialize(self)?;
+ Ok(())
+ }
+
+ fn serialize_unit(self) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ Ok(())
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ update_hasher_for_map(self.hasher, &[]);
+ Ok(())
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<()> {
+ self.serialize_str(variant)
+ }
+
+ fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ name: &'static str,
+ variant_index: u32,
+ _variant: &'static str,
+ value: &T,
+ ) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ // we can't have custom handlers with serde's traits, so we use this silly hack
+ // to make serializing data-driven registries work
+ if name.starts_with("minecraft:") {
+ let value = self
+ .registries
+ .map
+ .get(&ResourceLocation::from(name))
+ .and_then(|r| r.get_index(variant_index as usize))
+ .map(|r| r.0.to_string())
+ .unwrap_or_default();
+ self.serialize_str(&value)?;
+ return Ok(());
+ }
+
+ value.serialize(ChecksumSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ })
+ }
+
+ fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
+ assert!(self.hasher.finish() == 0);
+ println!("serialize seq with len: {:?}", len);
+ Ok(ChecksumListSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ values: Vec::with_capacity(len.unwrap_or_default()),
+ list_kind: ListKind::Normal,
+ })
+ }
+
+ fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
+ assert!(self.hasher.finish() == 0);
+ Ok(ChecksumListSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ values: Vec::with_capacity(len),
+ list_kind: ListKind::Normal,
+ })
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ name: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ assert!(self.hasher.finish() == 0);
+ let list_kind = if name == "azalea:int_array" {
+ self.hasher.write_u8(16);
+ ListKind::Int
+ } else if name == "azalea:long_array" {
+ self.hasher.write_u8(18);
+ ListKind::Long
+ } else {
+ ListKind::Normal
+ };
+ Ok(ChecksumListSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ values: Vec::with_capacity(len),
+ list_kind,
+ })
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ assert!(self.hasher.finish() == 0);
+ Ok(ChecksumMapSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ entries: Vec::with_capacity(len),
+ })
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ Ok(ChecksumMapSerializer {
+ hasher: self.hasher,
+ registries: self.registries,
+ entries: Vec::with_capacity(len),
+ })
+ }
+}
+
+pub struct ChecksumListSerializer<'a, 'r> {
+ hasher: &'a mut Crc32cHasher,
+ registries: &'r RegistryHolder,
+ values: Vec<Checksum>,
+ /// If you set this to not be the default, you should also update the hasher
+ /// before creating the list serializer.
+ list_kind: ListKind,
+}
+impl<'a, 'r> ser::SerializeSeq for ChecksumListSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ if self.list_kind == ListKind::Normal {
+ // elements are hashed individually
+ self.values.push(get_checksum(value, self.registries)?);
+ } else {
+ value.serialize(IntOrLongArrayChecksumSerializer {
+ hasher: self.hasher,
+ })?;
+ }
+
+ Ok(())
+ }
+
+ fn end(self) -> Result<()> {
+ match self.list_kind {
+ ListKind::Normal => {
+ assert!(self.hasher.finish() == 0);
+ update_hasher_for_list(self.hasher, &self.values);
+ }
+ ListKind::Int => {
+ self.hasher.write_u8(17);
+ }
+ ListKind::Long => {
+ self.hasher.write_u8(19);
+ }
+ }
+
+ Ok(())
+ }
+}
+/// Minecraft sometimes serializes u8/i32/i64 lists differently, so we have to
+/// keep track of that when serializing the arrays.
+///
+/// Byte arrays aren't included here as they're handled with `serialize_bytes`.
+#[derive(Default, PartialEq, Eq)]
+enum ListKind {
+ #[default]
+ Normal,
+ Int,
+ Long,
+}
+
+impl<'a, 'r> ser::SerializeTuple for ChecksumListSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<()> {
+ ser::SerializeSeq::end(self)
+ }
+}
+impl<'a, 'r> ser::SerializeTupleStruct for ChecksumListSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<()> {
+ ser::SerializeSeq::end(self)
+ }
+}
+
+pub struct ChecksumMapSerializer<'a, 'r> {
+ // this is only written to at the end
+ hasher: &'a mut Crc32cHasher,
+ registries: &'r RegistryHolder,
+ // we have to keep track of the elements like this because they're sorted at the end
+ entries: Vec<(Checksum, Checksum)>,
+}
+impl<'a, 'r> ser::SerializeMap for ChecksumMapSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_key<T>(&mut self, key: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ // this 0 is a placeholder
+ self.entries
+ .push((get_checksum(key, self.registries)?, Checksum(0)));
+ Ok(())
+ }
+
+ // It doesn't make a difference whether the colon is printed at the end of
+ // `serialize_key` or at the beginning of `serialize_value`. In this case
+ // the code is a bit simpler having it here.
+ fn serialize_value<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ // placeholder gets replaced here
+ self.entries
+ .last_mut()
+ .expect("entry should've already been added")
+ .1 = get_checksum(value, self.registries)?;
+ Ok(())
+ }
+
+ fn end(self) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ update_hasher_for_map(self.hasher, &self.entries);
+ Ok(())
+ }
+}
+impl<'a, 'r> ser::SerializeTupleVariant for ChecksumMapSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_field<T>(&mut self, _value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ // TODO
+ error!("tuple variants are not supported when serializing checksums");
+ Ok(())
+ }
+
+ fn end(self) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ Ok(())
+ }
+}
+impl<'a, 'r> ser::SerializeStruct for ChecksumMapSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.entries.push((
+ get_checksum(key, self.registries)?,
+ get_checksum(value, self.registries)?,
+ ));
+ Ok(())
+ }
+
+ fn end(self) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ update_hasher_for_map(self.hasher, &self.entries);
+ Ok(())
+ }
+}
+impl<'a, 'r> ser::SerializeStructVariant for ChecksumMapSerializer<'a, 'r> {
+ type Ok = ();
+ type Error = ChecksumError;
+
+ fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ self.entries.push((
+ get_checksum(key, self.registries)?,
+ get_checksum(value, self.registries)?,
+ ));
+ Ok(())
+ }
+
+ fn end(self) -> Result<()> {
+ assert!(self.hasher.finish() == 0);
+ update_hasher_for_map(self.hasher, &self.entries);
+ Ok(())
+ }
+}
+
+/// A hasher that can only serialize i32 and i64.
+struct IntOrLongArrayChecksumSerializer<'a> {
+ hasher: &'a mut Crc32cHasher,
+}
+impl<'a> ser::Serializer for IntOrLongArrayChecksumSerializer<'a> {
+ type Ok = ();
+ type Error = ChecksumError;
+ // unused
+ type SerializeSeq = ChecksumListSerializer<'a, 'a>;
+ type SerializeTuple = ChecksumListSerializer<'a, 'a>;
+ type SerializeTupleStruct = ChecksumListSerializer<'a, 'a>;
+ type SerializeTupleVariant = ChecksumMapSerializer<'a, 'a>;
+ type SerializeMap = ChecksumMapSerializer<'a, 'a>;
+ type SerializeStruct = ChecksumMapSerializer<'a, 'a>;
+ type SerializeStructVariant = ChecksumMapSerializer<'a, 'a>;
+
+ fn serialize_bool(self, _v: bool) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_i8(self, _v: i8) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_i16(self, _v: i16) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_i32(self, v: i32) -> Result<()> {
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+ fn serialize_i64(self, v: i64) -> Result<()> {
+ self.hasher.write(&v.to_le_bytes());
+ Ok(())
+ }
+ fn serialize_u8(self, _v: u8) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_u16(self, _v: u16) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_u32(self, v: u32) -> Result<()> {
+ self.serialize_i32(v as i32)
+ }
+ fn serialize_u64(self, v: u64) -> Result<()> {
+ self.serialize_i64(v as i64)
+ }
+ fn serialize_f32(self, _v: f32) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_f64(self, _v: f64) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_char(self, _v: char) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_str(self, _v: &str) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_bytes(self, _v: &[u8]) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_none(self) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_some<T>(self, _v: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ unimplemented!()
+ }
+ fn serialize_unit(self) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ ) -> Result<()> {
+ unimplemented!()
+ }
+ fn serialize_newtype_struct<T>(self, _name: &'static str, _value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ unimplemented!()
+ }
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ unimplemented!()
+ }
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+ unimplemented!()
+ }
+ fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
+ unimplemented!()
+ }
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ unimplemented!()
+ }
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ unimplemented!()
+ }
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ unimplemented!()
+ }
+ fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+ unimplemented!()
+ }
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ unimplemented!()
+ }
+}
+
+#[derive(Error, Debug)]
+#[error("Checksum serialization error")]
+pub struct ChecksumError;
+impl ser::Error for ChecksumError {
+ fn custom<T>(msg: T) -> Self
+ where
+ T: fmt::Display,
+ {
+ eprintln!("Serialization error: {msg}");
+ ChecksumError
+ }
+}
+type Result<T> = std::result::Result<T, ChecksumError>;
+
+pub fn get_checksum<T: Serialize + ?Sized>(
+ value: &T,
+ registries: &RegistryHolder,
+) -> Result<Checksum> {
+ let mut hasher = Crc32cHasher::default();
+ value.serialize(ChecksumSerializer {
+ hasher: &mut hasher,
+ registries,
+ })?;
+ Ok(Checksum(hasher.finish() as u32))
+}
+
+fn update_hasher_for_list(h: &mut Crc32cHasher, values: &[Checksum]) {
+ h.write_u8(4);
+ for v in values {
+ h.write(&v.0.to_le_bytes());
+ }
+ h.write_u8(5);
+}
+fn update_hasher_for_map(h: &mut Crc32cHasher, entries: &[(Checksum, Checksum)]) {
+ println!("getting checksum for map with {} entries", entries.len());
+ h.write_u8(2);
+ let mut entries = entries.to_vec();
+ entries.sort_by(|a, b| match a.0.cmp(&b.0) {
+ Ordering::Equal => a.1.cmp(&b.1),
+ other => other,
+ });
+ for (k, v) in entries {
+ h.write(&k.0.to_le_bytes());
+ h.write(&v.0.to_le_bytes());
+ }
+ h.write_u8(3);
+}
+
+// impl AzaleaChecksum for i8 {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(6);
+// h.write(&self.to_le_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for i16 {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(7);
+// h.write(&self.to_le_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for i32 {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(8);
+// h.write(&self.to_le_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for i64 {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(9);
+// h.write(&self.to_le_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for f32 {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(10);
+// h.write(&self.to_le_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for f64 {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(11);
+// h.write(&self.to_le_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for &str {
+// fn azalea_checksum(&self) -> HashCode {
+// println!("doing checksum for str: {self:?}");
+// let mut h = Crc32cHasher::default();
+// h.write_u8(12);
+// h.write(&(self.len() as u32).to_le_bytes());
+// h.write(&self.as_bytes());
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for String {
+// fn azalea_checksum(&self) -> HashCode {
+// println!("doing checksum for String: {self:?}");
+// let mut h = Crc32cHasher::default();
+// h.write_u8(12);
+
+// let utf16 = self.encode_utf16().collect::<Vec<_>>();
+// h.write(&(utf16.len() as u32).to_le_bytes());
+// for c in utf16 {
+// h.write(&c.to_le_bytes());
+// }
+
+// println!("doing checksum for string: {self:?}");
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for bool {
+// fn azalea_checksum(&self) -> HashCode {
+// println!("doing checksum for bool: {self:?}");
+// let mut h = Crc32cHasher::default();
+// h.write_u8(13);
+// h.write_u8(*self as u8);
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for Vec<u8> {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(14);
+// h.write(self);
+// h.write_u8(15);
+
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for Vec<i8> {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(14);
+// for item in self {
+// h.write(&[*item as u8]);
+// }
+// h.write_u8(15);
+
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for Vec<u32> {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(16);
+// for item in self {
+// h.write(&item.to_le_bytes());
+// }
+// h.write_u8(17);
+
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for Vec<i32> {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(16);
+// for item in self {
+// h.write(&item.to_le_bytes());
+// }
+// h.write_u8(17);
+
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for Vec<u64> {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(18);
+// for item in self {
+// h.write(&item.to_le_bytes());
+// }
+// h.write_u8(19);
+
+// HashCode(h.finish() as u32)
+// }
+// }
+// impl AzaleaChecksum for Vec<i64> {
+// fn azalea_checksum(&self) -> HashCode {
+// let mut h = Crc32cHasher::default();
+// h.write_u8(18);
+// for item in self {
+// h.write(&item.to_le_bytes());
+// }
+// h.write_u8(19);
+
+// HashCode(h.finish() as u32)
+// }
+// }
diff --git a/azalea-core/src/codec_utils.rs b/azalea-core/src/codec_utils.rs
new file mode 100644
index 00000000..0014f86d
--- /dev/null
+++ b/azalea-core/src/codec_utils.rs
@@ -0,0 +1,83 @@
+//! Some functions that are useful to have when implementing
+//! `Serialize`/`Deserialize`, which Azalea uses to imitate Minecraft codecs.
+
+use azalea_buf::SerializableUuid;
+use serde::{Serialize, Serializer, ser::SerializeTupleStruct};
+use uuid::Uuid;
+
+/// Intended to be used for skipping serialization if the value is the default.
+///
+/// ```no_run
+/// #[serde(skip_serializing_if = "is_default")]
+/// ```
+pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
+ *t == Default::default()
+}
+
+/// Intended to be used for skipping serialization if the value is `true`.
+///
+/// ```no_run
+/// #[serde(skip_serializing_if = "is_true")]
+/// ```
+pub fn is_true(t: &bool) -> bool {
+ *t
+}
+
+/// If the array has a single item, don't serialize as an array
+///
+/// ```no_run
+/// #[serde(serialize_with = "flatten_array")]
+/// ```
+pub fn flatten_array<S: Serializer, T: Serialize>(x: &Vec<T>, s: S) -> Result<S::Ok, S::Error> {
+ if x.len() == 1 {
+ x[0].serialize(s)
+ } else {
+ x.serialize(s)
+ }
+}
+
+/// Minecraft writes UUIDs as an IntArray<4>
+pub fn uuid<'a, S: Serializer>(
+ uuid: impl Into<&'a Option<Uuid>>,
+ serializer: S,
+) -> Result<S::Ok, S::Error> {
+ if let Some(uuid) = uuid.into() {
+ let arr: [u32; 4] = uuid.to_int_array();
+ let arr: [i32; 4] = [arr[0] as i32, arr[1] as i32, arr[2] as i32, arr[3] as i32];
+ IntArray(arr).serialize(serializer)
+ } else {
+ serializer.serialize_unit()
+ }
+}
+
+/// An internal type that makes the i32 array be serialized differently.
+///
+/// Azalea currently only uses this when writing checksums, but Minecraft also
+/// uses this internally when converting types to NBT.
+pub struct IntArray<const N: usize>(pub [i32; N]);
+/// An internal type that makes the i64 array be serialized differently.
+///
+/// Azalea currently only uses this when writing checksums, but Minecraft also
+/// uses this internally when converting types to NBT.
+pub struct LongArray<const N: usize>(pub [i64; N]);
+
+impl<const N: usize> Serialize for IntArray<N> {
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+ // see checksum::serialize_tuple_struct
+ let mut seq = serializer.serialize_tuple_struct("azalea:int_array", N)?;
+ for &item in &self.0 {
+ seq.serialize_field(&item)?;
+ }
+ seq.end()
+ }
+}
+impl<const N: usize> Serialize for LongArray<N> {
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+ // see checksum::serialize_tuple_struct
+ let mut seq = serializer.serialize_tuple_struct("azalea:long_array", N)?;
+ for &item in &self.0 {
+ seq.serialize_field(&item)?;
+ }
+ seq.end()
+ }
+}
diff --git a/azalea-core/src/direction.rs b/azalea-core/src/direction.rs
index 9f51ceaf..c794c79a 100644
--- a/azalea-core/src/direction.rs
+++ b/azalea-core/src/direction.rs
@@ -2,8 +2,9 @@ use azalea_buf::AzBuf;
use crate::position::{BlockPos, Vec3};
-#[derive(Clone, Copy, Debug, AzBuf, Default, Eq, PartialEq)]
-#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+#[derive(
+ Clone, Copy, Debug, AzBuf, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize,
+)]
pub enum Direction {
#[default]
Down = 0,
@@ -90,8 +91,7 @@ impl Direction {
///
/// Note that azalea_block has a similar enum named `FacingCardinal` that is
/// used for block states.
-#[derive(Clone, Copy, Debug, AzBuf, PartialEq, Eq, Hash)]
-#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+#[derive(Clone, Copy, Debug, AzBuf, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
pub enum CardinalDirection {
North,
South,
diff --git a/azalea-core/src/filterable.rs b/azalea-core/src/filterable.rs
index 1c432b28..893ebc3e 100644
--- a/azalea-core/src/filterable.rs
+++ b/azalea-core/src/filterable.rs
@@ -1,10 +1,13 @@
use std::io::{self, Cursor, Write};
use azalea_buf::{AzaleaRead, AzaleaReadLimited, AzaleaReadVar, AzaleaWrite};
+use serde::Serialize;
/// Used for written books.
+#[derive(Serialize)]
pub struct Filterable<T> {
pub raw: T,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub filtered: Option<T>,
}
diff --git a/azalea-core/src/hit_result.rs b/azalea-core/src/hit_result.rs
index fe759d43..96ff32c9 100644
--- a/azalea-core/src/hit_result.rs
+++ b/azalea-core/src/hit_result.rs
@@ -1,5 +1,3 @@
-use bevy_ecs::entity::Entity;
-
use crate::{
direction::Direction,
position::{BlockPos, Vec3},
@@ -8,12 +6,14 @@ use crate::{
/// The block or entity that our player is looking at and can interact with.
///
/// If there's nothing, it'll be a [`BlockHitResult`] with `miss` set to true.
+#[cfg(feature = "bevy_ecs")]
#[derive(Debug, Clone, PartialEq)]
pub enum HitResult {
Block(BlockHitResult),
Entity(EntityHitResult),
}
+#[cfg(feature = "bevy_ecs")]
impl HitResult {
pub fn miss(&self) -> bool {
match self {
@@ -86,18 +86,21 @@ impl BlockHitResult {
Self { block_pos, ..*self }
}
}
+#[cfg(feature = "bevy_ecs")]
+impl From<BlockHitResult> for HitResult {
+ fn from(value: BlockHitResult) -> Self {
+ HitResult::Block(value)
+ }
+}
+#[cfg(feature = "bevy_ecs")]
#[derive(Debug, Clone, PartialEq)]
pub struct EntityHitResult {
pub location: Vec3,
- pub entity: Entity,
+ pub entity: bevy_ecs::entity::Entity,
}
-impl From<BlockHitResult> for HitResult {
- fn from(value: BlockHitResult) -> Self {
- HitResult::Block(value)
- }
-}
+#[cfg(feature = "bevy_ecs")]
impl From<EntityHitResult> for HitResult {
fn from(value: EntityHitResult) -> Self {
HitResult::Entity(value)
diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs
index 9f6e9386..9fdf4b6c 100644
--- a/azalea-core/src/lib.rs
+++ b/azalea-core/src/lib.rs
@@ -4,6 +4,8 @@
pub mod aabb;
pub mod bitset;
+pub mod checksum;
+pub mod codec_utils;
pub mod color;
pub mod cursor3d;
pub mod data_registry;
diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs
index dd6a37e0..8ba381a5 100644
--- a/azalea-core/src/position.rs
+++ b/azalea-core/src/position.rs
@@ -13,8 +13,12 @@ use std::{
};
use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError};
+use serde::{Serialize, Serializer};
+use simdnbt::Deserialize;
-use crate::{direction::Direction, math, resource_location::ResourceLocation};
+use crate::{
+ codec_utils::IntArray, direction::Direction, math, resource_location::ResourceLocation,
+};
macro_rules! vec3_impl {
($name:ident, $type:ty) => {
@@ -295,8 +299,7 @@ macro_rules! vec3_impl {
/// Used to represent an exact position in the world where an entity could be.
///
/// For blocks, [`BlockPos`] is used instead.
-#[derive(Clone, Copy, Debug, Default, PartialEq, AzBuf)]
-#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+#[derive(Clone, Copy, Debug, Default, PartialEq, AzBuf, serde::Deserialize, serde::Serialize)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
@@ -362,7 +365,6 @@ impl Vec3 {
///
/// For entities (if the coordinates are floating-point), use [`Vec3`] instead.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
-#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct BlockPos {
pub x: i32,
pub y: i32,
@@ -425,6 +427,24 @@ impl BlockPos {
(self - other).length()
}
}
+impl serde::Serialize for BlockPos {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // makes sure it gets serialized correctly for the checksum
+ IntArray([self.x, self.y, self.z]).serialize(serializer)
+ }
+}
+impl<'de> serde::Deserialize<'de> for BlockPos {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let [x, y, z] = <[i32; 3]>::deserialize(deserializer)?;
+ Ok(BlockPos { x, y, z })
+ }
+}
/// Similar to [`BlockPos`] but it's serialized as 3 varints instead of one
/// 64-bit integer, so it can represent a bigger range of numbers.
@@ -661,10 +681,10 @@ impl From<ChunkSectionBlockPos> for u16 {
impl nohash_hasher::IsEnabled for ChunkSectionBlockPos {}
/// A block pos with an attached world
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct GlobalPos {
// this is actually a ResourceKey in Minecraft, but i don't think it matters?
- pub world: ResourceLocation,
+ pub dimension: ResourceLocation,
pub pos: BlockPos,
}
@@ -819,8 +839,7 @@ impl fmt::Display for Vec3 {
}
/// A 2D vector.
-#[derive(Clone, Copy, Debug, Default, PartialEq, AzBuf)]
-#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+#[derive(Clone, Copy, Debug, Default, PartialEq, AzBuf, Deserialize, Serialize)]
pub struct Vec2 {
pub x: f32,
pub y: f32,
@@ -900,7 +919,7 @@ impl AzaleaRead for BlockPos {
impl AzaleaRead for GlobalPos {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
Ok(GlobalPos {
- world: ResourceLocation::azalea_read(buf)?,
+ dimension: ResourceLocation::azalea_read(buf)?,
pos: BlockPos::azalea_read(buf)?,
})
}
@@ -929,7 +948,7 @@ impl AzaleaWrite for BlockPos {
impl AzaleaWrite for GlobalPos {
fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
- ResourceLocation::azalea_write(&self.world, buf)?;
+ ResourceLocation::azalea_write(&self.dimension, buf)?;
BlockPos::azalea_write(&self.pos, buf)?;
Ok(())
diff --git a/azalea-core/src/resource_location.rs b/azalea-core/src/resource_location.rs
index c6e39150..1591f678 100644
--- a/azalea-core/src/resource_location.rs
+++ b/azalea-core/src/resource_location.rs
@@ -7,7 +7,6 @@ use std::{
};
use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
-#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use simdnbt::{FromNbtTag, ToNbtTag, owned::NbtTag};
@@ -77,7 +76,6 @@ impl AzaleaWrite for ResourceLocation {
}
}
-#[cfg(feature = "serde")]
impl Serialize for ResourceLocation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -87,7 +85,6 @@ impl Serialize for ResourceLocation {
}
}
-#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for ResourceLocation {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
diff --git a/azalea-core/src/sound.rs b/azalea-core/src/sound.rs
index f12205e0..3f7b86c3 100644
--- a/azalea-core/src/sound.rs
+++ b/azalea-core/src/sound.rs
@@ -1,8 +1,9 @@
use azalea_buf::AzBuf;
+use serde::Serialize;
use crate::resource_location::ResourceLocation;
-#[derive(Clone, Debug, PartialEq, AzBuf)]
+#[derive(Clone, Debug, PartialEq, AzBuf, Serialize)]
pub struct CustomSound {
pub location: ResourceLocation,
pub fixed_range: Option<f32>,