aboutsummaryrefslogtreecommitdiff
path: root/azalea-core/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-12-09 13:29:59 -0600
committerGitHub <noreply@github.com>2025-12-09 13:29:59 -0600
commit26d619c9a329087a23d6577ee74bd764f50cd773 (patch)
tree8020fe902257764a23a445c6ed9987ea4848189d /azalea-core/src
parent84cd261118c9d1e3145d4d1751c0d22098cd8cd8 (diff)
downloadazalea-drasl-26d619c9a329087a23d6577ee74bd764f50cd773.tar.xz
Enchantments (#286)
* start implementing enchants * store parsed registries * more work on enchants * implement deserializer for some entity effects * mostly working definitions for enchants * fix tests * detect equipment changes * fix errors * update changelog * fix some imports * remove outdated todo * add basic test for enchants applying attributes * use git simdnbt
Diffstat (limited to 'azalea-core/src')
-rw-r--r--azalea-core/src/attribute_modifier_operation.rs33
-rw-r--r--azalea-core/src/checksum.rs6
-rw-r--r--azalea-core/src/data_registry.rs80
-rw-r--r--azalea-core/src/identifier.rs78
-rw-r--r--azalea-core/src/lib.rs1
-rw-r--r--azalea-core/src/position.rs18
-rw-r--r--azalea-core/src/registry_holder/block_predicate.rs3
-rw-r--r--azalea-core/src/registry_holder/block_state_provider.rs3
-rw-r--r--azalea-core/src/registry_holder/components.rs321
-rw-r--r--azalea-core/src/registry_holder/dimension_type.rs (renamed from azalea-core/src/registry_holder.rs)89
-rw-r--r--azalea-core/src/registry_holder/enchantment.rs116
-rw-r--r--azalea-core/src/registry_holder/entity_effect.rs269
-rw-r--r--azalea-core/src/registry_holder/float_provider.rs72
-rw-r--r--azalea-core/src/registry_holder/mod.rs230
-rw-r--r--azalea-core/src/registry_holder/value.rs207
-rw-r--r--azalea-core/src/sound.rs2
16 files changed, 1371 insertions, 157 deletions
diff --git a/azalea-core/src/attribute_modifier_operation.rs b/azalea-core/src/attribute_modifier_operation.rs
new file mode 100644
index 00000000..ff92a44a
--- /dev/null
+++ b/azalea-core/src/attribute_modifier_operation.rs
@@ -0,0 +1,33 @@
+use std::str::FromStr;
+
+use azalea_buf::AzBuf;
+use serde::Serialize;
+use simdnbt::{FromNbtTag, borrow::NbtTag};
+
+#[derive(Clone, Copy, PartialEq, AzBuf, Debug, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum AttributeModifierOperation {
+ AddValue,
+ AddMultipliedBase,
+ AddMultipliedTotal,
+}
+
+impl FromStr for AttributeModifierOperation {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let value: AttributeModifierOperation = match s {
+ "add_value" => Self::AddValue,
+ "add_multiplied_base" => Self::AddMultipliedBase,
+ "add_multiplied_total" => Self::AddMultipliedTotal,
+ _ => return Err(()),
+ };
+ Ok(value)
+ }
+}
+impl FromNbtTag for AttributeModifierOperation {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ let v = tag.string()?;
+ Self::from_str(&v.to_str()).ok()
+ }
+}
diff --git a/azalea-core/src/checksum.rs b/azalea-core/src/checksum.rs
index df94d58e..ac13fcc7 100644
--- a/azalea-core/src/checksum.rs
+++ b/azalea-core/src/checksum.rs
@@ -199,10 +199,8 @@ impl<'a, 'r> ser::Serializer for ChecksumSerializer<'a, 'r> {
if name.starts_with("minecraft:") {
let value = self
.registries
- .map
- .get(&Identifier::from(name))
- .and_then(|r| r.get_index(variant_index as usize))
- .map(|r| r.0.to_string())
+ .protocol_id_to_identifier(Identifier::from(name), variant_index)
+ .map(|v| v.to_string())
.unwrap_or_default();
self.serialize_str(&value)?;
return Ok(());
diff --git a/azalea-core/src/data_registry.rs b/azalea-core/src/data_registry.rs
index d3fae125..cf82772a 100644
--- a/azalea-core/src/data_registry.rs
+++ b/azalea-core/src/data_registry.rs
@@ -1,47 +1,61 @@
-use std::{io::Cursor, str::FromStr};
-
use azalea_registry::DataRegistry;
use simdnbt::owned::NbtCompound;
-use crate::{identifier::Identifier, registry_holder::RegistryHolder};
+use crate::{
+ identifier::Identifier,
+ registry_holder::{self, RegistryDeserializesTo, RegistryHolder},
+};
pub trait ResolvableDataRegistry: DataRegistry {
- fn resolve_name(&self, registries: &RegistryHolder) -> Option<Identifier> {
- self.resolve(registries).map(|(name, _)| name.clone())
+ type DeserializesTo: RegistryDeserializesTo;
+
+ fn resolve_name<'a>(&self, registries: &'a RegistryHolder) -> Option<&'a Identifier> {
+ // self.resolve(registries).map(|(name, _)| name.clone())
+ registries.protocol_id_to_identifier(Identifier::from(Self::NAME), self.protocol_id())
}
+
fn resolve<'a>(
&self,
registries: &'a RegistryHolder,
- ) -> Option<(&'a Identifier, &'a NbtCompound)> {
- let name_ident = Identifier::from_str(Self::NAME).unwrap_or_else(|_| {
- panic!(
- "Name for registry should be a valid Identifier: {}",
- Self::NAME
- )
- });
- let registry_values = registries.map.get(&name_ident)?;
- let resolved = registry_values.get_index(self.protocol_id() as usize)?;
- Some(resolved)
+ ) -> Option<(&'a Identifier, &'a Self::DeserializesTo)> {
+ Self::DeserializesTo::get_for_registry(registries, Self::NAME, self.protocol_id())
}
+}
- fn resolve_and_deserialize<T: simdnbt::Deserialize>(
- &self,
- registries: &RegistryHolder,
- ) -> Option<Result<(Identifier, T), simdnbt::DeserializeError>> {
- let (name, value) = self.resolve(registries)?;
-
- let mut nbt_bytes = Vec::new();
- value.write(&mut nbt_bytes);
- let nbt_borrow_compound =
- simdnbt::borrow::read_compound(&mut Cursor::new(&nbt_bytes)).ok()?;
- let value = match T::from_compound((&nbt_borrow_compound).into()) {
- Ok(value) => value,
- Err(err) => {
- return Some(Err(err));
+macro_rules! define_deserializes_to {
+ ($($t:ty => $deserializes_to:ty),* $(,)?) => {
+ $(
+ impl ResolvableDataRegistry for $t {
+ type DeserializesTo = $deserializes_to;
}
- };
+ )*
+ };
+}
+macro_rules! define_default_deserializes_to {
+ ($($t:ty),* $(,)?) => {
+ $(
+ impl ResolvableDataRegistry for $t {
+ type DeserializesTo = NbtCompound;
+ }
+ )*
+ };
+}
- Some(Ok((name.clone(), value)))
- }
+define_deserializes_to! {
+ azalea_registry::DimensionType => registry_holder::dimension_type::DimensionTypeElement,
+ azalea_registry::Enchantment => registry_holder::enchantment::EnchantmentData,
+}
+
+define_default_deserializes_to! {
+ azalea_registry::DamageKind,
+ azalea_registry::Dialog,
+ azalea_registry::WolfSoundVariant,
+ azalea_registry::CowVariant,
+ azalea_registry::ChickenVariant,
+ azalea_registry::FrogVariant,
+ azalea_registry::CatVariant,
+ azalea_registry::PigVariant,
+ azalea_registry::PaintingVariant,
+ azalea_registry::WolfVariant,
+ azalea_registry::Biome,
}
-impl<T: DataRegistry> ResolvableDataRegistry for T {}
diff --git a/azalea-core/src/identifier.rs b/azalea-core/src/identifier.rs
index 117bf26d..a3c886c3 100644
--- a/azalea-core/src/identifier.rs
+++ b/azalea-core/src/identifier.rs
@@ -1,8 +1,9 @@
//! An arbitrary identifier or resource location.
use std::{
- fmt,
+ fmt::{self, Debug, Display},
io::{self, Cursor, Write},
+ num::NonZeroUsize,
str::FromStr,
};
@@ -16,43 +17,62 @@ use simdnbt::{FromNbtTag, ToNbtTag, owned::NbtTag};
#[doc(alias = "ResourceLocation")]
#[derive(Hash, Clone, PartialEq, Eq, Default)]
pub struct Identifier {
- pub namespace: String,
- pub path: String,
+ // empty namespaces aren't allowed so NonZero is fine.
+ colon_index: Option<NonZeroUsize>,
+ inner: Box<str>,
}
static DEFAULT_NAMESPACE: &str = "minecraft";
// static REALMS_NAMESPACE: &str = "realms";
impl Identifier {
- pub fn new(resource_string: &str) -> Identifier {
- let sep_byte_position_option = resource_string.chars().position(|c| c == ':');
- let (namespace, path) = if let Some(sep_byte_position) = sep_byte_position_option {
- if sep_byte_position == 0 {
- (DEFAULT_NAMESPACE, &resource_string[1..])
- } else {
- (
- &resource_string[..sep_byte_position],
- &resource_string[sep_byte_position + 1..],
- )
+ pub fn new(resource_string: impl Into<String>) -> Identifier {
+ let mut resource_string = resource_string.into();
+
+ let colon_index = resource_string.find(':');
+ let colon_index = if let Some(colon_index) = colon_index {
+ if colon_index == 0 {
+ resource_string = resource_string.split_off(1);
}
+ NonZeroUsize::new(colon_index)
} else {
- (DEFAULT_NAMESPACE, resource_string)
+ None
};
- Identifier {
- namespace: namespace.to_string(),
- path: path.to_string(),
+
+ Self {
+ colon_index,
+ inner: resource_string.into(),
+ }
+ }
+
+ pub fn namespace(&self) -> &str {
+ if let Some(colon_index) = self.colon_index {
+ &self.inner[0..colon_index.get()]
+ } else {
+ DEFAULT_NAMESPACE
+ }
+ }
+ pub fn path(&self) -> &str {
+ if let Some(colon_index) = self.colon_index {
+ &self.inner[(colon_index.get() + 1)..]
+ } else {
+ &self.inner
}
}
}
-impl fmt::Display for Identifier {
+impl Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}:{}", self.namespace, self.path)
+ if self.colon_index.is_some() {
+ write!(f, "{}", self.inner)
+ } else {
+ write!(f, "{DEFAULT_NAMESPACE}:{}", self.inner)
+ }
}
}
-impl fmt::Debug for Identifier {
+impl Debug for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}:{}", self.namespace, self.path)
+ write!(f, "{self}")
}
}
impl FromStr for Identifier {
@@ -125,26 +145,26 @@ mod tests {
#[test]
fn basic_identifier() {
let r = Identifier::new("abcdef:ghijkl");
- assert_eq!(r.namespace, "abcdef");
- assert_eq!(r.path, "ghijkl");
+ assert_eq!(r.namespace(), "abcdef");
+ assert_eq!(r.path(), "ghijkl");
}
#[test]
fn no_namespace() {
let r = Identifier::new("azalea");
- assert_eq!(r.namespace, "minecraft");
- assert_eq!(r.path, "azalea");
+ assert_eq!(r.namespace(), "minecraft");
+ assert_eq!(r.path(), "azalea");
}
#[test]
fn colon_start() {
let r = Identifier::new(":azalea");
- assert_eq!(r.namespace, "minecraft");
- assert_eq!(r.path, "azalea");
+ assert_eq!(r.namespace(), "minecraft");
+ assert_eq!(r.path(), "azalea");
}
#[test]
fn colon_end() {
let r = Identifier::new("azalea:");
- assert_eq!(r.namespace, "azalea");
- assert_eq!(r.path, "");
+ assert_eq!(r.namespace(), "azalea");
+ assert_eq!(r.path(), "");
}
#[test]
diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs
index b87e6143..16b23c01 100644
--- a/azalea-core/src/lib.rs
+++ b/azalea-core/src/lib.rs
@@ -3,6 +3,7 @@
#![doc = include_str!("../README.md")]
pub mod aabb;
+pub mod attribute_modifier_operation;
pub mod bitset;
pub mod checksum;
pub mod codec_utils;
diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs
index 03ea49ec..7cd86a03 100644
--- a/azalea-core/src/position.rs
+++ b/azalea-core/src/position.rs
@@ -14,7 +14,7 @@ use std::{
use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError};
use serde::{Serialize, Serializer};
-use simdnbt::Deserialize;
+use simdnbt::borrow::NbtTag;
use crate::{codec_utils::IntArray, direction::Direction, identifier::Identifier, math};
@@ -311,6 +311,13 @@ pub struct Vec3 {
pub z: f64,
}
vec3_impl!(Vec3, f64);
+impl simdnbt::FromNbtTag for Vec3 {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ let pos = tag.list()?.doubles()?;
+ let [x, y, z] = <[f64; 3]>::try_from(pos).ok()?;
+ Some(Self { x, y, z })
+ }
+}
impl Vec3 {
/// Get the distance of this vector to the origin by doing
@@ -489,6 +496,13 @@ pub struct Vec3i {
pub z: i32,
}
vec3_impl!(Vec3i, i32);
+impl simdnbt::FromNbtTag for Vec3i {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ let pos = tag.list()?.ints()?;
+ let [x, y, z] = <[i32; 3]>::try_from(pos).ok()?;
+ Some(Self { x, y, z })
+ }
+}
/// Chunk coordinates are used to represent where a chunk is in the world.
///
@@ -876,7 +890,7 @@ impl fmt::Display for Vec3 {
}
/// A 2D vector.
-#[derive(Clone, Copy, Debug, Default, PartialEq, AzBuf, Deserialize, Serialize)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, AzBuf, simdnbt::Deserialize, Serialize)]
pub struct Vec2 {
pub x: f32,
pub y: f32,
diff --git a/azalea-core/src/registry_holder/block_predicate.rs b/azalea-core/src/registry_holder/block_predicate.rs
new file mode 100644
index 00000000..faa05d10
--- /dev/null
+++ b/azalea-core/src/registry_holder/block_predicate.rs
@@ -0,0 +1,3 @@
+/// TODO: unimplemented
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct BlockPredicate {}
diff --git a/azalea-core/src/registry_holder/block_state_provider.rs b/azalea-core/src/registry_holder/block_state_provider.rs
new file mode 100644
index 00000000..f9ad603d
--- /dev/null
+++ b/azalea-core/src/registry_holder/block_state_provider.rs
@@ -0,0 +1,3 @@
+/// TODO: unimplemented
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct BlockStateProvider {}
diff --git a/azalea-core/src/registry_holder/components.rs b/azalea-core/src/registry_holder/components.rs
new file mode 100644
index 00000000..e7f94cd8
--- /dev/null
+++ b/azalea-core/src/registry_holder/components.rs
@@ -0,0 +1,321 @@
+use std::{any::Any, fmt::Debug, mem::ManuallyDrop, str::FromStr};
+
+use azalea_registry::{EnchantmentEffectComponentKind, SoundEvent};
+use simdnbt::{
+ DeserializeError,
+ borrow::{NbtCompound, NbtList, NbtTag},
+};
+
+use crate::registry_holder::{
+ entity_effect::EntityEffect,
+ get_in_compound,
+ value::{AttributeEffect, ValueEffect},
+};
+
+#[macro_export]
+macro_rules! define_effect_components {
+ ( $( $x:ident: $t:ty ),* $(,)? ) => {
+ #[allow(non_snake_case)]
+ pub union EffectComponentUnion {
+ $( $x: ManuallyDrop<$t>, )*
+ }
+
+ impl EffectComponentUnion {
+ /// # Safety
+ ///
+ /// `kind` must be the correct value for this union.
+ pub unsafe fn drop_as(&mut self, kind: EnchantmentEffectComponentKind) {
+ match kind {
+ $( EnchantmentEffectComponentKind::$x => { unsafe { ManuallyDrop::drop(&mut self.$x) } }, )*
+ }
+ }
+
+ pub fn from_effect_nbt_tag_as(
+ kind: EnchantmentEffectComponentKind,
+ tag: EffectNbtTag,
+ ) -> Result<Self, DeserializeError> {
+ Ok(match kind {
+ $( EnchantmentEffectComponentKind::$x => {
+ Self { $x: ManuallyDrop::new(<$t>::from_effect_nbt_tag(tag)?) }
+ }, )*
+ })
+ }
+
+ /// # Safety
+ ///
+ /// `kind` must be the correct value for this union.
+ pub unsafe fn clone_as(
+ &self,
+ kind: EnchantmentEffectComponentKind,
+ ) -> Self {
+ match kind {
+ $( EnchantmentEffectComponentKind::$x => {
+ Self { $x: unsafe { self.$x.clone() } }
+ }, )*
+ }
+ }
+
+ /// # Safety
+ ///
+ /// `kind` must be the correct value for this union.
+ pub unsafe fn as_kind(&self, kind: EnchantmentEffectComponentKind) -> &dyn ResolvedEffectComponent {
+ match kind {
+ $( EnchantmentEffectComponentKind::$x => { unsafe { &**(&self.$x as &ManuallyDrop<dyn ResolvedEffectComponent>) } }, )*
+ }
+ }
+ }
+
+ $(
+ impl EffectComponentTrait for $t {
+ const KIND: EnchantmentEffectComponentKind = EnchantmentEffectComponentKind::$x;
+ }
+ )*
+ };
+}
+
+define_effect_components!(
+ DamageProtection: DamageProtection,
+ DamageImmunity: ConditionalEffect<DamageImmunity>,
+ Damage: Damage,
+ SmashDamagePerFallenBlock: SmashDamagePerFallenBlock,
+ Knockback: Knockback,
+ ArmorEffectiveness: ArmorEffectiveness,
+ PostAttack: PostAttack,
+ PostPiercingAttack: PostPiercingAttack,
+ HitBlock: ConditionalEntityEffect,
+ ItemDamage: ConditionalValueEffect,
+ Attributes: AttributeEffect,
+ EquipmentDrops: EquipmentDrops,
+ LocationChanged: ConditionalEffect<LocationBasedEffect>,
+ Tick: Tick,
+ AmmoUse: AmmoUse,
+ ProjectilePiercing: ProjectilePiercing,
+ ProjectileSpawned: ProjectileSpawned,
+ ProjectileSpread: ProjectileSpread,
+ ProjectileCount: ProjectileCount,
+ TridentReturnAcceleration: TridentReturnAcceleration,
+ FishingTimeReduction: FishingTimeReduction,
+ FishingLuckBonus: FishingLuckBonus,
+ BlockExperience: BlockExperience,
+ MobExperience: MobExperience,
+ RepairWithXp: RepairWithXp,
+ CrossbowChargeTime: CrossbowChargeTime,
+ CrossbowChargingSounds: CrossbowChargingSounds,
+ TridentSound: TridentSound,
+ PreventEquipmentDrop: PreventEquipmentDrop,
+ PreventArmorChange: PreventArmorChange,
+ TridentSpinAttackStrength: TridentSpinAttackStrength,
+);
+
+/// A trait that's implemented on all effect components so we can access them
+/// from [`EnchantmentData::get`](super::enchantment::EnchantmentData::get).
+pub trait EffectComponentTrait: Any {
+ const KIND: EnchantmentEffectComponentKind;
+}
+
+// this exists because EffectComponentTrait isn't dyn-compatible
+pub trait ResolvedEffectComponent: Any {}
+impl<T: EffectComponentTrait> ResolvedEffectComponent for T {}
+
+/// An alternative to `simdnbt::borrow::NbtTag` used internally when
+/// deserializing effects.
+///
+/// When deserializing effect components from the registry, we're given NBT tags
+/// in either a list of compounds or a list of lists. This means that we can't
+/// just use `from_nbt_tag`, because `borrow::NbtTag` can't be constructed on
+/// its own. To work around this, we have this `EffectNbtTag` struct that we
+/// *can* construct that we use when deserializing.
+#[derive(Debug, Clone, Copy)]
+pub enum EffectNbtTag<'a, 'tape> {
+ Compound(NbtCompound<'a, 'tape>),
+ List(NbtList<'a, 'tape>),
+}
+
+impl<'a, 'tape> EffectNbtTag<'a, 'tape> {
+ pub fn compound(self, error_name: &str) -> Result<NbtCompound<'a, 'tape>, DeserializeError> {
+ if let Self::Compound(nbt) = self {
+ Ok(nbt)
+ } else {
+ Err(DeserializeError::MismatchedFieldType(error_name.to_owned()))
+ }
+ }
+ pub fn list(self, error_name: &str) -> Result<NbtList<'a, 'tape>, DeserializeError> {
+ if let Self::List(nbt) = self {
+ Ok(nbt)
+ } else {
+ Err(DeserializeError::MismatchedFieldType(error_name.to_owned()))
+ }
+ }
+}
+macro_rules! impl_from_effect_nbt_tag {
+ (<$g:tt : $generic_type:tt $(::$generic_type2:tt)* $(+ $generic_type3:tt)+> $ty:ident <$generic_name:ident>) => {
+ impl<$g: $generic_type$(::$generic_type2)* $(+ $generic_type3)+> $ty<$generic_name> {
+ fn from_effect_nbt_tag(nbt: crate::registry_holder::components::EffectNbtTag) -> Result<Self, DeserializeError> {
+ let nbt = nbt.compound(stringify!($ty))?;
+ simdnbt::Deserialize::from_compound(nbt)
+ }
+ }
+ };
+ ($ty:ident) => {
+ impl $ty {
+ pub fn from_effect_nbt_tag(nbt: crate::registry_holder::components::EffectNbtTag) -> Result<Self, DeserializeError> {
+ let nbt = nbt.compound(stringify!($ty))?;
+ simdnbt::Deserialize::from_compound(nbt)
+ }
+ }
+ };
+}
+pub(crate) use impl_from_effect_nbt_tag;
+
+#[derive(Debug, Clone)]
+pub struct ConditionalEffect<T: simdnbt::Deserialize + Debug + Clone> {
+ pub effect: T,
+ // pub requirements
+}
+#[derive(Debug, Clone)]
+pub struct TargetedConditionalEffect<T: simdnbt::Deserialize + Debug + Clone> {
+ pub effect: T,
+ // pub enchanted
+ // pub affected
+ // pub requirements
+}
+
+// makes for cleaner-looking types
+type ConditionalValueEffect = ConditionalEffect<ValueEffect>;
+type ConditionalEntityEffect = ConditionalEffect<EntityEffect>;
+
+impl<T: simdnbt::Deserialize + Debug + Clone> simdnbt::Deserialize for ConditionalEffect<T> {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let effect = get_in_compound(&nbt, "effect")?;
+ Ok(Self { effect })
+ }
+}
+impl_from_effect_nbt_tag!(<T: simdnbt::Deserialize + Debug + Clone> ConditionalEffect<T>);
+
+impl<T: simdnbt::Deserialize + Debug + Clone> simdnbt::Deserialize
+ for TargetedConditionalEffect<T>
+{
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let effect = get_in_compound(&nbt, "effect")?;
+ Ok(Self { effect })
+ }
+}
+
+macro_rules! declare_newtype_components {
+ ( $( $struct_name:ident: $inner_type:ty ),* $(,)? ) => {
+ $(
+ #[derive(Clone, Debug, simdnbt::Deserialize)]
+ pub struct $struct_name(pub $inner_type);
+ impl_from_effect_nbt_tag!($struct_name);
+ )*
+ };
+}
+
+declare_newtype_components! {
+ DamageProtection: ConditionalValueEffect,
+ Damage: ConditionalValueEffect,
+ SmashDamagePerFallenBlock: ConditionalValueEffect,
+ Knockback: ConditionalValueEffect,
+ ArmorEffectiveness: ConditionalValueEffect,
+ PostAttack: TargetedConditionalEffect<EntityEffect>,
+ PostPiercingAttack: ConditionalEffect<EntityEffect>,
+ HitBlock: ConditionalEntityEffect,
+ ItemDamage: ConditionalValueEffect,
+ EquipmentDrops: ConditionalValueEffect,
+ Tick: ConditionalEntityEffect,
+ AmmoUse: ConditionalValueEffect,
+ ProjectilePiercing: ConditionalValueEffect,
+ ProjectileSpawned: ConditionalEntityEffect,
+ ProjectileSpread: ConditionalValueEffect,
+ ProjectileCount: ConditionalValueEffect,
+ TridentReturnAcceleration: ConditionalValueEffect,
+ FishingTimeReduction: ConditionalValueEffect,
+ FishingLuckBonus: ConditionalValueEffect,
+ BlockExperience: ConditionalValueEffect,
+ MobExperience: ConditionalValueEffect,
+ RepairWithXp: ConditionalValueEffect,
+ CrossbowChargeTime: ValueEffect,
+ TridentSpinAttackStrength: ValueEffect,
+
+}
+
+#[derive(Clone, Debug, simdnbt::Deserialize)]
+pub struct DamageImmunity {}
+impl_from_effect_nbt_tag!(DamageImmunity);
+
+#[derive(Clone, Debug)]
+pub struct CrossbowChargingSounds(pub Vec<CrossbowChargingSound>);
+impl simdnbt::FromNbtTag for CrossbowChargingSounds {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ simdnbt::FromNbtTag::from_nbt_tag(tag).map(Self)
+ }
+}
+impl CrossbowChargingSounds {
+ pub fn from_effect_nbt_tag(nbt: EffectNbtTag) -> Result<Self, DeserializeError> {
+ let Ok(nbt) = nbt.list("CrossbowChargingSounds") else {
+ return Ok(Self(vec![simdnbt::Deserialize::from_compound(
+ nbt.compound("CrossbowChargingSounds")?,
+ )?]));
+ };
+
+ Ok(Self(
+ nbt.compounds()
+ .ok_or_else(|| {
+ DeserializeError::MismatchedFieldType("CrossbowChargingSounds".to_owned())
+ })?
+ .into_iter()
+ .map(|c| simdnbt::Deserialize::from_compound(c))
+ .collect::<Result<_, _>>()?,
+ ))
+ }
+}
+
+#[derive(Clone, Debug, simdnbt::Deserialize)]
+pub struct CrossbowChargingSound {
+ pub start: Option<SoundEvent>,
+ pub mid: Option<SoundEvent>,
+ pub end: Option<SoundEvent>,
+}
+
+#[derive(Clone, Debug)]
+pub struct TridentSound(pub Vec<SoundEvent>);
+impl simdnbt::FromNbtTag for TridentSound {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ let sounds = tag.list()?.strings()?;
+
+ sounds
+ .iter()
+ .map(|s| SoundEvent::from_str(&s.to_str()).ok())
+ .collect::<Option<Vec<_>>>()
+ .map(Self)
+ }
+}
+impl TridentSound {
+ pub fn from_effect_nbt_tag(nbt: EffectNbtTag) -> Result<Self, DeserializeError> {
+ let sounds = nbt
+ .list("TridentSound")?
+ .strings()
+ .ok_or_else(|| DeserializeError::MismatchedFieldType("TridentSound".to_owned()))?;
+
+ sounds
+ .iter()
+ .map(|s| SoundEvent::from_str(&s.to_str()).ok())
+ .collect::<Option<Vec<_>>>()
+ .ok_or_else(|| DeserializeError::MismatchedFieldType("TridentSound".to_owned()))
+ .map(Self)
+ }
+}
+
+#[derive(Clone, Debug, simdnbt::Deserialize)]
+pub struct LocationBasedEffect {
+ // TODO
+}
+impl_from_effect_nbt_tag!(LocationBasedEffect);
+
+#[derive(Clone, Debug, simdnbt::Deserialize)]
+pub struct PreventEquipmentDrop {}
+impl_from_effect_nbt_tag!(PreventEquipmentDrop);
+
+#[derive(Clone, Debug, simdnbt::Deserialize)]
+pub struct PreventArmorChange {}
+impl_from_effect_nbt_tag!(PreventArmorChange);
diff --git a/azalea-core/src/registry_holder.rs b/azalea-core/src/registry_holder/dimension_type.rs
index b44d5155..ca158277 100644
--- a/azalea-core/src/registry_holder.rs
+++ b/azalea-core/src/registry_holder/dimension_type.rs
@@ -1,100 +1,13 @@
-//! The data sent to the client in the `ClientboundRegistryDataPacket`.
-//!
-//! This module contains the structures used to represent the registry
-//! sent to the client upon login. This contains a lot of information about
-//! the game, including the types of chat messages, dimensions, and
-//! biomes.
-
-use std::{collections::HashMap, io::Cursor};
+use std::collections::HashMap;
use azalea_buf::AzBuf;
-use indexmap::IndexMap;
use simdnbt::{
Deserialize, FromNbtTag, Serialize, ToNbtTag,
owned::{NbtCompound, NbtTag},
};
-use tracing::error;
use crate::{codec_utils::*, identifier::Identifier};
-/// The base of the registry.
-///
-/// This is the registry that is sent to the client upon login.
-///
-/// Note that `azalea-client` stores registries per-world instead of per-client
-/// like you might expect. This is an optimization for swarms to reduce memory
-/// usage, since registries are expected to be the same for every client in a
-/// world.
-#[derive(Default, Debug, Clone)]
-pub struct RegistryHolder {
- pub map: HashMap<Identifier, IndexMap<Identifier, NbtCompound>>,
-}
-
-impl RegistryHolder {
- pub fn append(&mut self, id: Identifier, entries: Vec<(Identifier, Option<NbtCompound>)>) {
- let map = self.map.entry(id).or_default();
- for (key, value) in entries {
- if let Some(value) = value {
- map.insert(key, value);
- } else {
- map.shift_remove(&key);
- }
- }
- }
-
- /// Get the dimension type registry, or `None` if it doesn't exist.
- ///
- /// You should do some type of error handling if this returns `None`.
- pub fn dimension_type(&self) -> Option<RegistryType<DimensionTypeElement>> {
- let name = Identifier::new("minecraft:dimension_type");
- match self.get(&name) {
- Some(Ok(registry)) => Some(registry),
- Some(Err(err)) => {
- error!(
- "Error deserializing dimension type registry: {err:?}\n{:?}",
- self.map.get(&name)
- );
- None
- }
- None => None,
- }
- }
-
- fn get<T: Deserialize>(
- &self,
- name: &Identifier,
- ) -> Option<Result<RegistryType<T>, simdnbt::DeserializeError>> {
- // this is suboptimal, ideally simdnbt should just have a way to get the
- // owned::NbtCompound as a borrow::NbtCompound
-
- let mut map = HashMap::new();
-
- for (key, value) in self.map.get(name)? {
- // convert the value to T
- let mut nbt_bytes = Vec::new();
- value.write(&mut nbt_bytes);
- let nbt_borrow_compound =
- simdnbt::borrow::read_compound(&mut Cursor::new(&nbt_bytes)).ok()?;
- let value = match T::from_compound((&nbt_borrow_compound).into()) {
- Ok(value) => value,
- Err(err) => {
- return Some(Err(err));
- }
- };
-
- map.insert(key.clone(), value);
- }
-
- Some(Ok(RegistryType { map }))
- }
-}
-
-/// A collection of values for a certain type of registry data.
-#[derive(Debug, Clone)]
-pub struct RegistryType<T> {
- pub map: HashMap<Identifier, T>,
-}
-
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "strict_registry", simdnbt(deny_unknown_fields))]
pub struct TrimMaterialElement {
diff --git a/azalea-core/src/registry_holder/enchantment.rs b/azalea-core/src/registry_holder/enchantment.rs
new file mode 100644
index 00000000..0f78843c
--- /dev/null
+++ b/azalea-core/src/registry_holder/enchantment.rs
@@ -0,0 +1,116 @@
+use std::{any::Any, fmt::Debug, str::FromStr};
+
+use azalea_registry::EnchantmentEffectComponentKind;
+use indexmap::IndexMap;
+use simdnbt::{DeserializeError, borrow::NbtCompound};
+
+use crate::registry_holder::components::{
+ EffectComponentTrait, EffectComponentUnion, EffectNbtTag, ResolvedEffectComponent,
+};
+
+pub struct EnchantmentData {
+ // TODO: make these two deserializable
+ // pub description: TextComponent,
+ // pub exclusive_set: HolderSet<Enchantment, ResourceLocation>,
+ effects: IndexMap<EnchantmentEffectComponentKind, Vec<EffectComponentUnion>>,
+}
+
+impl EnchantmentData {
+ pub fn get<T: EffectComponentTrait>(&self) -> Option<Vec<&T>> {
+ let components = self.get_kind(T::KIND)?;
+ let components_any = components
+ .into_iter()
+ .map(|c| (c as &dyn Any).downcast_ref::<T>())
+ .collect::<Option<_>>()?;
+ Some(components_any)
+ }
+
+ pub fn get_kind(
+ &self,
+ kind: EnchantmentEffectComponentKind,
+ ) -> Option<Vec<&dyn ResolvedEffectComponent>> {
+ self.effects.get(&kind).map(|c| {
+ c.iter()
+ .map(|c| {
+ // SAFETY: we just got the component from the map, so it must be the correct
+ // kind
+ unsafe { c.as_kind(kind) }
+ })
+ .collect()
+ })
+ }
+}
+
+impl simdnbt::Deserialize for EnchantmentData {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let mut effects: IndexMap<EnchantmentEffectComponentKind, Vec<EffectComponentUnion>> =
+ IndexMap::new();
+
+ if let Some(effects_tag) = nbt.compound("effects") {
+ for (key, list) in effects_tag.iter() {
+ let kind = EnchantmentEffectComponentKind::from_str(&key.to_str())
+ .map_err(|_| DeserializeError::UnknownField(key.to_string()))?;
+
+ let mut components = Vec::new();
+ if let Some(tag) = list.compound() {
+ if !tag.is_empty() {
+ let value = EffectComponentUnion::from_effect_nbt_tag_as(
+ kind,
+ EffectNbtTag::Compound(tag),
+ )?;
+ components.push(value);
+ }
+ } else {
+ let list = list.list().ok_or_else(|| {
+ DeserializeError::MismatchedFieldType("effects".to_owned())
+ })?;
+
+ if let Some(tags) = list.compounds() {
+ for tag in tags {
+ let value = EffectComponentUnion::from_effect_nbt_tag_as(
+ kind,
+ EffectNbtTag::Compound(tag),
+ )?;
+ components.push(value);
+ }
+ } else {
+ let value = EffectComponentUnion::from_effect_nbt_tag_as(
+ kind,
+ EffectNbtTag::List(list),
+ )?;
+ components.push(value);
+ }
+ }
+
+ effects.insert(kind, components);
+ }
+ }
+
+ let value = Self { effects };
+ Ok(value)
+ }
+}
+
+impl Debug for EnchantmentData {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("EnchantmentData")
+ .field("effects", &self.effects.keys())
+ .finish()
+ }
+}
+
+impl Clone for EnchantmentData {
+ fn clone(&self) -> Self {
+ let mut effects = IndexMap::with_capacity(self.effects.len());
+ for (kind, effect) in &self.effects {
+ effects.insert(
+ *kind,
+ effect
+ .iter()
+ .map(|e| unsafe { e.clone_as(*kind) })
+ .collect(),
+ );
+ }
+ EnchantmentData { effects }
+ }
+}
diff --git a/azalea-core/src/registry_holder/entity_effect.rs b/azalea-core/src/registry_holder/entity_effect.rs
new file mode 100644
index 00000000..7615c4df
--- /dev/null
+++ b/azalea-core/src/registry_holder/entity_effect.rs
@@ -0,0 +1,269 @@
+use std::{collections::HashMap, str::FromStr};
+
+use azalea_registry::{
+ EnchantmentEntityEffectKind as EntityEffectKind, GameEvent, Holder, ParticleKind, SoundEvent,
+};
+use simdnbt::{
+ Deserialize, DeserializeError,
+ borrow::{NbtCompound, NbtTag},
+};
+
+use crate::{
+ identifier::Identifier,
+ position::{Vec3, Vec3i},
+ registry_holder::{
+ block_predicate::BlockPredicate, block_state_provider::BlockStateProvider,
+ float_provider::FloatProvider, get_in_compound, value::LevelBasedValue,
+ },
+ sound::CustomSound,
+};
+
+#[derive(Debug, Clone)]
+pub enum EntityEffect {
+ AllOf(AllOf),
+ ApplyMobEffect(ApplyMobEffect),
+ ChangeItemDamage(ChangeItemDamage),
+ DamageEntity(DamageEntity),
+ Explode(Explode),
+ Ignite(Ignite),
+ ApplyImpulse(ApplyEntityImpulse),
+ ApplyExhaustion(ApplyExhaustion),
+ PlaySound(PlaySound),
+ ReplaceBlock(ReplaceBlock),
+ ReplaceDisk(ReplaceDisk),
+ RunFunction(RunFunction),
+ SetBlockProperties(SetBlockProperties),
+ SpawnParticles(SpawnParticles),
+ SummonEntity(SummonEntity),
+}
+
+impl Deserialize for EntityEffect {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let kind = get_in_compound(&nbt, "type")?;
+ match kind {
+ EntityEffectKind::AllOf => Deserialize::from_compound(nbt).map(Self::AllOf),
+ EntityEffectKind::ApplyMobEffect => {
+ Deserialize::from_compound(nbt).map(Self::ApplyMobEffect)
+ }
+ EntityEffectKind::ChangeItemDamage => {
+ Deserialize::from_compound(nbt).map(Self::ChangeItemDamage)
+ }
+ EntityEffectKind::DamageEntity => {
+ Deserialize::from_compound(nbt).map(Self::DamageEntity)
+ }
+ EntityEffectKind::Explode => Deserialize::from_compound(nbt).map(Self::Explode),
+ EntityEffectKind::Ignite => Deserialize::from_compound(nbt).map(Self::Ignite),
+ EntityEffectKind::ApplyImpulse => {
+ Deserialize::from_compound(nbt).map(Self::ApplyImpulse)
+ }
+ EntityEffectKind::ApplyExhaustion => {
+ Deserialize::from_compound(nbt).map(Self::ApplyExhaustion)
+ }
+ EntityEffectKind::PlaySound => Deserialize::from_compound(nbt).map(Self::PlaySound),
+ EntityEffectKind::ReplaceBlock => {
+ Deserialize::from_compound(nbt).map(Self::ReplaceBlock)
+ }
+ EntityEffectKind::ReplaceDisk => Deserialize::from_compound(nbt).map(Self::ReplaceDisk),
+ EntityEffectKind::RunFunction => Deserialize::from_compound(nbt).map(Self::RunFunction),
+ EntityEffectKind::SetBlockProperties => {
+ Deserialize::from_compound(nbt).map(Self::SetBlockProperties)
+ }
+ EntityEffectKind::SpawnParticles => {
+ Deserialize::from_compound(nbt).map(Self::SpawnParticles)
+ }
+ EntityEffectKind::SummonEntity => {
+ Deserialize::from_compound(nbt).map(Self::SummonEntity)
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct AllOf {
+ pub effects: Vec<EntityEffect>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ApplyMobEffect {
+ /// IDs of mob effects.
+ pub to_apply: HomogeneousList,
+ pub min_duration: LevelBasedValue,
+ pub max_duration: LevelBasedValue,
+ pub min_amplifier: LevelBasedValue,
+ pub max_amplifier: LevelBasedValue,
+}
+
+// TODO: in vanilla this is just a HolderSetCodec using a RegistryFixedCodec,
+// azalea registries should probably be refactored first tho
+#[derive(Debug, Clone, Default)]
+pub struct HomogeneousList {
+ pub ids: Vec<Identifier>,
+}
+impl simdnbt::FromNbtTag for HomogeneousList {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ if let Some(string) = tag.string() {
+ return Some(Self {
+ ids: vec![Identifier::new(string.to_str())],
+ });
+ }
+ if let Some(list) = tag.list()
+ && let Some(strings) = list.strings()
+ {
+ return Some(Self {
+ ids: strings
+ .iter()
+ .map(|&s| Identifier::new(s.to_str()))
+ .collect(),
+ });
+ }
+ None
+ }
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ChangeItemDamage {
+ pub amount: LevelBasedValue,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct DamageEntity {
+ pub min_damage: LevelBasedValue,
+ pub max_damage: LevelBasedValue,
+ // TODO: convert to a DamageKind after azalea-registry refactor
+ #[simdnbt(rename = "damage_type")]
+ pub damage_kind: Identifier,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct Explode {
+ pub attribute_to_user: Option<bool>,
+ // TODO: convert to a DamageKind after azalea-registry refactor
+ #[simdnbt(rename = "damage_type")]
+ pub damage_kind: Option<Identifier>,
+ pub knockback_multiplier: Option<LevelBasedValue>,
+ pub immune_blocks: Option<HomogeneousList>,
+ pub offset: Option<Vec3>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct Ignite {
+ pub duration: LevelBasedValue,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ApplyEntityImpulse {
+ pub direction: Vec3,
+ pub coordinate_scale: Vec3,
+ pub magnitude: LevelBasedValue,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ApplyExhaustion {
+ pub amount: LevelBasedValue,
+}
+
+#[derive(Debug, Clone)]
+pub struct PlaySound {
+ // #[simdnbt(compact)]
+ pub sound: Vec<Holder<SoundEvent, CustomSound>>,
+ pub volume: FloatProvider,
+ pub pitch: FloatProvider,
+}
+
+impl Deserialize for PlaySound {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let sound = if let Some(list) = nbt.list("sound") {
+ // TODO: this will probably break in the future because it's only handling lists
+ // of strings. you should refactor simdnbt to have an owned NbtTag enum that
+ // contains the borrow types so this works for more than just
+ // strings.
+ list.strings()
+ .ok_or(DeserializeError::MissingField)?
+ .into_iter()
+ .map(|s| {
+ SoundEvent::from_str(&s.to_str())
+ .map(Holder::Reference)
+ .ok()
+ })
+ .collect::<Option<_>>()
+ .ok_or(DeserializeError::MissingField)?
+ } else {
+ vec![get_in_compound(&nbt, "sound")?]
+ };
+
+ let volume = get_in_compound(&nbt, "volume")?;
+ let pitch = get_in_compound(&nbt, "pitch")?;
+
+ Ok(Self {
+ sound,
+ volume,
+ pitch,
+ })
+ }
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ReplaceBlock {
+ pub offset: Option<Vec3i>,
+ pub predicate: Option<BlockPredicate>,
+ pub block_state: BlockStateProvider,
+ pub trigger_game_event: Option<GameEvent>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ReplaceDisk {
+ pub radius: LevelBasedValue,
+ pub height: LevelBasedValue,
+ pub offset: Option<Vec3i>,
+ pub predicate: Option<BlockPredicate>,
+ pub block_state: BlockStateProvider,
+ pub trigger_game_event: Option<GameEvent>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct RunFunction {
+ pub function: Identifier,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct SetBlockProperties {
+ pub properties: HashMap<String, String>,
+ pub offset: Option<Vec3i>,
+ pub trigger_game_event: Option<GameEvent>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct SpawnParticles {
+ pub particle: ParticleKindCodec,
+ pub horizontal_position: SpawnParticlesPosition,
+ pub vertical_position: SpawnParticlesPosition,
+ pub horizontal_velocity: SpawnParticlesVelocity,
+ pub vertical_velocity: SpawnParticlesVelocity,
+ pub speed: Option<FloatProvider>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct ParticleKindCodec {
+ #[simdnbt(rename = "type")]
+ pub kind: ParticleKind,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct SpawnParticlesPosition {
+ #[simdnbt(rename = "type")]
+ pub kind: Identifier,
+ pub offset: Option<f32>,
+ pub scale: Option<f32>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct SpawnParticlesVelocity {
+ pub movement_scale: Option<f32>,
+ pub base: Option<FloatProvider>,
+}
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct SummonEntity {
+ pub entity: HomogeneousList,
+ pub join_team: Option<bool>,
+}
diff --git a/azalea-core/src/registry_holder/float_provider.rs b/azalea-core/src/registry_holder/float_provider.rs
new file mode 100644
index 00000000..6ce6b26d
--- /dev/null
+++ b/azalea-core/src/registry_holder/float_provider.rs
@@ -0,0 +1,72 @@
+use std::ops::Range;
+
+use azalea_registry::FloatProviderKind;
+use simdnbt::{
+ DeserializeError, FromNbtTag,
+ borrow::{NbtCompound, NbtTag},
+};
+
+use crate::registry_holder::get_in_compound;
+
+#[derive(Clone, Debug)]
+pub enum FloatProvider {
+ Constant(f32),
+ Uniform {
+ range: Range<f32>,
+ },
+ ClampedNormal {
+ mean: f32,
+ deviation: f32,
+ min: f32,
+ max: f32,
+ },
+ Trapezoid {
+ min: f32,
+ max: f32,
+ plateau: f32,
+ },
+}
+impl FromNbtTag for FloatProvider {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ if let Some(f) = tag.float() {
+ return Some(Self::Constant(f));
+ }
+ if let Some(c) = tag.compound() {
+ return Self::from_compound(c).ok();
+ }
+ None
+ }
+}
+impl FloatProvider {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let kind = get_in_compound(&nbt, "type")?;
+ match kind {
+ FloatProviderKind::Constant => Ok(Self::Constant(get_in_compound(&nbt, "value")?)),
+ FloatProviderKind::Uniform => {
+ let min_inclusive = get_in_compound(&nbt, "min_inclusive")?;
+ let max_exclusive = get_in_compound(&nbt, "max_exclusive")?;
+ Ok(Self::Uniform {
+ range: min_inclusive..max_exclusive,
+ })
+ }
+ FloatProviderKind::ClampedNormal => {
+ let mean = get_in_compound(&nbt, "mean")?;
+ let deviation = get_in_compound(&nbt, "deviation")?;
+ let min = get_in_compound(&nbt, "min")?;
+ let max = get_in_compound(&nbt, "max")?;
+ Ok(Self::ClampedNormal {
+ mean,
+ deviation,
+ min,
+ max,
+ })
+ }
+ FloatProviderKind::Trapezoid => {
+ let min = get_in_compound(&nbt, "min")?;
+ let max = get_in_compound(&nbt, "max")?;
+ let plateau = get_in_compound(&nbt, "plateau")?;
+ Ok(Self::Trapezoid { min, max, plateau })
+ }
+ }
+ }
+}
diff --git a/azalea-core/src/registry_holder/mod.rs b/azalea-core/src/registry_holder/mod.rs
new file mode 100644
index 00000000..269cf454
--- /dev/null
+++ b/azalea-core/src/registry_holder/mod.rs
@@ -0,0 +1,230 @@
+//! The data sent to the client in the `ClientboundRegistryDataPacket`.
+//!
+//! This module contains the structures used to represent the registry
+//! sent to the client upon login. This contains a lot of information about
+//! the game, including the types of chat messages, dimensions, and
+//! biomes.
+
+pub mod block_predicate;
+pub mod block_state_provider;
+pub mod components;
+pub mod dimension_type;
+pub mod enchantment;
+pub mod entity_effect;
+pub mod float_provider;
+pub mod value;
+
+use std::{collections::HashMap, io::Cursor};
+
+use indexmap::IndexMap;
+use simdnbt::{DeserializeError, FromNbtTag, borrow, owned::NbtCompound};
+use thiserror::Error;
+use tracing::error;
+
+use crate::identifier::Identifier;
+
+/// The base of the registry.
+///
+/// This is the registry that is sent to the client upon login.
+///
+/// Note that `azalea-client` stores registries in `Instance` rather than
+/// per-client like you might expect. This is an optimization for swarms to
+/// reduce memory usage, since registries are expected to be the same for every
+/// client in a world.
+#[derive(Default, Debug, Clone)]
+pub struct RegistryHolder {
+ // if you add new fields here, don't forget to also update `RegistryHolder::append`,
+ // `protocol_id_to_identifier`, and `define_default_deserializes_to!` in
+ // `data_registry.rs`.
+ #[rustfmt::skip] // allow empty line
+
+ /// Attributes about the dimension.
+ pub dimension_type: RegistryType<dimension_type::DimensionTypeElement>,
+
+ pub enchantment: RegistryType<enchantment::EnchantmentData>,
+
+ /// Registries that we haven't implemented deserializable types for.
+ ///
+ /// You can still access these just fine, but they'll be NBT instead of
+ /// nicer structs.
+ pub extra: HashMap<Identifier, RegistryType<NbtCompound>>,
+}
+
+macro_rules! registry_holder {
+ ($($registry:ident),* $(,)?) => {
+ impl RegistryHolder {
+ pub fn append(
+ &mut self,
+ id: Identifier,
+ entries: Vec<(Identifier, Option<NbtCompound>)>,
+ ) {
+
+ if id.namespace() == "minecraft" {
+ match id.path() {
+ $(
+ stringify!($registry) => {
+ return self.$registry.append_nbt(id, entries);
+ }
+ )*
+ _ => {}
+ }
+ }
+
+ self.extra
+ .entry(id.clone())
+ .or_default()
+ .append_nbt(id, entries);
+ }
+
+ pub fn extend(&mut self, other: RegistryHolder) {
+ $(
+ self.$registry = other.$registry;
+ )*
+ self.extra.extend(other.extra);
+ }
+
+ /// Convert a protocol ID for a registry key (like the protocol_id for
+ /// something that implements `DataRegistry`) and convert it to its string
+ /// name.
+ pub fn protocol_id_to_identifier(
+ &self,
+ registry: Identifier,
+ protocol_id: u32,
+ ) -> Option<&Identifier> {
+ let index = protocol_id as usize;
+
+
+ if registry.namespace() == "minecraft" {
+ match registry.path() {
+ $(
+ stringify!($registry) => {
+ return self.$registry.map.get_index(index).map(|(k, _)| k);
+ }
+ )*
+ _ => {}
+ }
+ }
+
+ self.extra
+ .get(&registry)
+ .and_then(|r| r.map.get_index(index))
+ .map(|(k, _)| k)
+ }
+ }
+ };
+}
+
+registry_holder!(dimension_type, enchantment);
+
+fn nbt_to_serializable_type<T: simdnbt::Deserialize>(
+ value: &NbtCompound,
+) -> Result<T, NbtToSerializableTypeError> {
+ // convert the value to T
+ let mut nbt_bytes = Vec::new();
+ value.write(&mut nbt_bytes);
+ let nbt_borrow_compound = simdnbt::borrow::read_compound(&mut Cursor::new(&nbt_bytes))?;
+ T::from_compound((&nbt_borrow_compound).into()).map_err(Into::into)
+}
+
+#[derive(Error, Debug)]
+enum NbtToSerializableTypeError {
+ #[error(transparent)]
+ NbtError(#[from] simdnbt::Error),
+ #[error(transparent)]
+ DeserializeError(#[from] simdnbt::DeserializeError),
+}
+
+/// A collection of values for a certain type of registry data.
+#[derive(Debug, Clone)]
+pub struct RegistryType<T: simdnbt::Deserialize> {
+ pub map: IndexMap<Identifier, T>,
+}
+
+impl<T: simdnbt::Deserialize> Default for RegistryType<T> {
+ fn default() -> Self {
+ Self {
+ map: IndexMap::new(),
+ }
+ }
+}
+
+impl<T: simdnbt::Deserialize> RegistryType<T> {
+ fn append_nbt(&mut self, id: Identifier, entries: Vec<(Identifier, Option<NbtCompound>)>) {
+ let map = &mut self.map;
+ for (key, value) in entries {
+ if let Some(value) = value {
+ match nbt_to_serializable_type(&value) {
+ Ok(value) => {
+ map.insert(key, value);
+ }
+ Err(err) => {
+ error!("Error deserializing {id} entry {key}: {err:?}\n{value:?}");
+ }
+ }
+ } else {
+ map.shift_remove(&key);
+ }
+ }
+ }
+}
+
+pub trait RegistryDeserializesTo: simdnbt::Deserialize {
+ fn get_for_registry<'a>(
+ registries: &'a RegistryHolder,
+ registry_name: &'static str,
+ protocol_id: u32,
+ ) -> Option<(&'a Identifier, &'a Self)>;
+}
+
+impl RegistryDeserializesTo for NbtCompound {
+ fn get_for_registry<'a>(
+ registries: &'a RegistryHolder,
+ registry_name: &'static str,
+ protocol_id: u32,
+ ) -> Option<(&'a Identifier, &'a Self)> {
+ registries
+ .extra
+ .get(&Identifier::new(registry_name))?
+ .map
+ .get_index(protocol_id as usize)
+ }
+}
+impl RegistryDeserializesTo for dimension_type::DimensionTypeElement {
+ fn get_for_registry<'a>(
+ registries: &'a RegistryHolder,
+ registry_name: &'static str,
+ protocol_id: u32,
+ ) -> Option<(&'a Identifier, &'a Self)> {
+ if registry_name != "dimension_type" {
+ error!(
+ "called RegistryDeserializesTo::get_for_registry with the wrong registry: {registry_name}"
+ );
+ }
+ registries
+ .dimension_type
+ .map
+ .get_index(protocol_id as usize)
+ }
+}
+impl RegistryDeserializesTo for enchantment::EnchantmentData {
+ fn get_for_registry<'a>(
+ registries: &'a RegistryHolder,
+ registry_name: &'static str,
+ protocol_id: u32,
+ ) -> Option<(&'a Identifier, &'a Self)> {
+ if registry_name != "enchantment" {
+ error!(
+ "called RegistryDeserializesTo::get_for_registry with the wrong registry: {registry_name}"
+ );
+ }
+ registries.enchantment.map.get_index(protocol_id as usize)
+ }
+}
+
+pub fn get_in_compound<T: FromNbtTag>(
+ compound: &borrow::NbtCompound,
+ key: &str,
+) -> Result<T, DeserializeError> {
+ let value = compound.get(key).ok_or(DeserializeError::MissingField)?;
+ T::from_nbt_tag(value).ok_or(DeserializeError::MissingField)
+}
diff --git a/azalea-core/src/registry_holder/value.rs b/azalea-core/src/registry_holder/value.rs
new file mode 100644
index 00000000..ccd7f9ea
--- /dev/null
+++ b/azalea-core/src/registry_holder/value.rs
@@ -0,0 +1,207 @@
+use azalea_registry::{
+ Attribute, EnchantmentLevelBasedValueKind as LevelBasedValueKind,
+ EnchantmentValueEffectKind as ValueEffectKind,
+};
+use simdnbt::{
+ DeserializeError, FromNbtTag,
+ borrow::{NbtCompound, NbtTag},
+};
+
+use crate::{
+ attribute_modifier_operation::AttributeModifierOperation,
+ identifier::Identifier,
+ registry_holder::{components::impl_from_effect_nbt_tag, get_in_compound},
+};
+
+#[derive(Debug, Clone)]
+pub enum ValueEffect {
+ Set {
+ value: LevelBasedValue,
+ },
+ Add {
+ value: LevelBasedValue,
+ },
+ Multiply {
+ factor: LevelBasedValue,
+ },
+ RemoveBinomial {
+ chance: LevelBasedValue,
+ },
+ AllOf {
+ effects: Vec<ValueEffect>,
+ },
+ Exponential {
+ base: LevelBasedValue,
+ exponent: LevelBasedValue,
+ },
+}
+
+impl simdnbt::Deserialize for ValueEffect {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let kind = get_in_compound(&nbt, "type")?;
+ let value = match kind {
+ ValueEffectKind::Set => {
+ let value = get_in_compound(&nbt, "value")?;
+ Self::Set { value }
+ }
+ ValueEffectKind::Add => {
+ let value = get_in_compound(&nbt, "value")?;
+ Self::Add { value }
+ }
+ ValueEffectKind::Multiply => {
+ let factor = get_in_compound(&nbt, "factor")?;
+ Self::Multiply { factor }
+ }
+ ValueEffectKind::RemoveBinomial => {
+ let chance = get_in_compound(&nbt, "chance")?;
+ Self::RemoveBinomial { chance }
+ }
+ ValueEffectKind::AllOf => {
+ let effects = get_in_compound(&nbt, "effects")?;
+ Self::AllOf { effects }
+ }
+ ValueEffectKind::Exponential => {
+ let base = get_in_compound(&nbt, "base")?;
+ let exponent = get_in_compound(&nbt, "exponent")?;
+ Self::Exponential { base, exponent }
+ }
+ };
+ Ok(value)
+ }
+}
+impl_from_effect_nbt_tag!(ValueEffect);
+
+#[derive(Debug, Clone, simdnbt::Deserialize)]
+pub struct AttributeEffect {
+ pub id: Identifier,
+ pub attribute: Attribute,
+ pub amount: LevelBasedValue,
+ pub operation: AttributeModifierOperation,
+}
+impl_from_effect_nbt_tag!(AttributeEffect);
+
+#[derive(Debug, Clone)]
+pub enum LevelBasedValue {
+ Constant(f32),
+ Exponent {
+ base: Box<LevelBasedValue>,
+ power: Box<LevelBasedValue>,
+ },
+ Linear {
+ base: f32,
+ per_level_above_first: f32,
+ },
+ LevelsSquared {
+ added: f32,
+ },
+ Clamped {
+ value: Box<LevelBasedValue>,
+ min: f32,
+ max: f32,
+ },
+ Fraction {
+ numerator: Box<LevelBasedValue>,
+ denominator: Box<LevelBasedValue>,
+ },
+ Lookup {
+ values: Vec<f32>,
+ fallback: Box<LevelBasedValue>,
+ },
+}
+impl LevelBasedValue {
+ pub fn calculate(&self, n: i32) -> f32 {
+ match self {
+ LevelBasedValue::Constant(v) => *v,
+ LevelBasedValue::Exponent { base, power } => {
+ (base.calculate(n) as f64).powf(power.calculate(n) as f64) as f32
+ }
+ LevelBasedValue::Linear {
+ base,
+ per_level_above_first,
+ } => *base + *per_level_above_first * ((n - 1) as f32),
+ LevelBasedValue::LevelsSquared { added } => (n * n) as f32 + *added,
+ LevelBasedValue::Clamped { value, min, max } => value.calculate(n).clamp(*min, *max),
+ LevelBasedValue::Fraction {
+ numerator,
+ denominator,
+ } => {
+ let value = denominator.calculate(n);
+ if value == 0. {
+ 0.
+ } else {
+ numerator.calculate(n) / value
+ }
+ }
+ LevelBasedValue::Lookup { values, fallback } => values
+ .get((n - 1) as usize)
+ .copied()
+ .unwrap_or_else(|| fallback.calculate(n)),
+ }
+ }
+}
+
+impl Default for LevelBasedValue {
+ fn default() -> Self {
+ Self::Constant(0.)
+ }
+}
+
+impl FromNbtTag for LevelBasedValue {
+ fn from_nbt_tag(tag: NbtTag) -> Option<Self> {
+ if let Some(f) = tag.float() {
+ return Some(Self::Constant(f));
+ }
+ if let Some(c) = tag.compound() {
+ return Self::from_compound(c).ok();
+ }
+ None
+ }
+}
+impl LevelBasedValue {
+ fn from_compound(nbt: NbtCompound) -> Result<Self, DeserializeError> {
+ let kind = get_in_compound(&nbt, "type")?;
+ let value = match kind {
+ LevelBasedValueKind::Exponent => {
+ let base = Box::new(get_in_compound(&nbt, "base")?);
+ let power = Box::new(get_in_compound(&nbt, "power")?);
+ return Ok(Self::Exponent { base, power });
+ }
+ LevelBasedValueKind::Linear => {
+ let base = get_in_compound(&nbt, "base")?;
+ let per_level_above_first = get_in_compound(&nbt, "per_level_above_first")?;
+ Self::Linear {
+ base,
+ per_level_above_first,
+ }
+ }
+ LevelBasedValueKind::LevelsSquared => {
+ let added = get_in_compound(&nbt, "added")?;
+ Self::LevelsSquared { added }
+ }
+ LevelBasedValueKind::Clamped => {
+ let value = Box::new(get_in_compound(&nbt, "value")?);
+ let min = get_in_compound(&nbt, "min")?;
+ let max = get_in_compound(&nbt, "max")?;
+ Self::Clamped { value, min, max }
+ }
+ LevelBasedValueKind::Fraction => {
+ let numerator = Box::new(get_in_compound(&nbt, "numerator")?);
+ let denominator = Box::new(get_in_compound(&nbt, "denominator")?);
+ Self::Fraction {
+ numerator,
+ denominator,
+ }
+ }
+ LevelBasedValueKind::Lookup => {
+ let values = nbt
+ .list("values")
+ .ok_or(DeserializeError::MissingField)?
+ .floats()
+ .ok_or(DeserializeError::MissingField)?;
+ let fallback = Box::new(get_in_compound(&nbt, "fallback")?);
+ Self::Lookup { values, fallback }
+ }
+ };
+ Ok(value)
+ }
+}
diff --git a/azalea-core/src/sound.rs b/azalea-core/src/sound.rs
index ab4bf431..a3b74b43 100644
--- a/azalea-core/src/sound.rs
+++ b/azalea-core/src/sound.rs
@@ -3,7 +3,7 @@ use serde::Serialize;
use crate::identifier::Identifier;
-#[derive(Clone, Debug, PartialEq, AzBuf, Serialize)]
+#[derive(Clone, Debug, PartialEq, AzBuf, Serialize, simdnbt::Deserialize)]
pub struct CustomSound {
pub sound_id: Identifier,
pub range: Option<f32>,