diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-12-09 13:29:59 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-09 13:29:59 -0600 |
| commit | 26d619c9a329087a23d6577ee74bd764f50cd773 (patch) | |
| tree | 8020fe902257764a23a445c6ed9987ea4848189d /azalea-core/src | |
| parent | 84cd261118c9d1e3145d4d1751c0d22098cd8cd8 (diff) | |
| download | azalea-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.rs | 33 | ||||
| -rw-r--r-- | azalea-core/src/checksum.rs | 6 | ||||
| -rw-r--r-- | azalea-core/src/data_registry.rs | 80 | ||||
| -rw-r--r-- | azalea-core/src/identifier.rs | 78 | ||||
| -rw-r--r-- | azalea-core/src/lib.rs | 1 | ||||
| -rw-r--r-- | azalea-core/src/position.rs | 18 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/block_predicate.rs | 3 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/block_state_provider.rs | 3 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/components.rs | 321 | ||||
| -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.rs | 116 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/entity_effect.rs | 269 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/float_provider.rs | 72 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/mod.rs | 230 | ||||
| -rw-r--r-- | azalea-core/src/registry_holder/value.rs | 207 | ||||
| -rw-r--r-- | azalea-core/src/sound.rs | 2 |
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(®istry) + .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>, |
