diff --git a/CHANGELOG.md b/CHANGELOG.md index fe555606d..66e5d0d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ is breaking anyways, semantic versioning is not followed. - Incorrect `damage_kind` component protocol implementation. - The `AirSupply` metadata component was defaulting to `0` instead of `300`. (@wbbradley) +- Chunk sections with more than 256 unique block states no longer panic (or get corrupted in release builds) when switching to the global palette. +- `dragon_breath`, `effect`, `instant_effect`, `tinted_leaves`, `flash`, `trail`, `dust_pillar` and `block_crumble` particles now read their options instead of failing the `level_particles` packet. +- `BeehiveOccupant::entity_data` is now an `EntityData`, fixing items with the `bees` component failing to parse. +- `NumberFormat::Styled` was read as named NBT, so scoreboards with a styled number format failed to parse. ## [0.16.0+mc26.1] - 2026-03-27 diff --git a/azalea-chat/src/numbers.rs b/azalea-chat/src/numbers.rs index f739c543e..b3b3f1a9c 100644 --- a/azalea-chat/src/numbers.rs +++ b/azalea-chat/src/numbers.rs @@ -26,7 +26,7 @@ impl AzBuf for NumberFormat { match kind { NumberFormatKind::Blank => Ok(NumberFormat::Blank), NumberFormatKind::Styled => Ok(NumberFormat::Styled { - style: simdnbt::owned::read(buf)?, + style: Nbt::azalea_read(buf)?, }), NumberFormatKind::Fixed => Ok(NumberFormat::Fixed { value: FormattedText::azalea_read(buf)?, @@ -48,3 +48,25 @@ impl AzBuf for NumberFormat { Ok(()) } } + +#[cfg(all(test, feature = "azalea-buf"))] +mod tests { + use simdnbt::owned::{BaseNbt, NbtCompound, NbtTag}; + + use super::*; + + #[test] + fn test_styled_roundtrip() { + let style = Nbt::Some(BaseNbt::new( + "", + NbtCompound::from_values(vec![("color".into(), NbtTag::String("red".into()))]), + )); + let format = NumberFormat::Styled { style }; + let mut bytes = Vec::new(); + format.azalea_write(&mut bytes).unwrap(); + + let mut buf = Cursor::new(bytes.as_slice()); + assert_eq!(NumberFormat::azalea_read(&mut buf).unwrap(), format); + assert_eq!(buf.position() as usize, bytes.len()); + } +} diff --git a/azalea-entity/src/particle.rs b/azalea-entity/src/particle.rs index 4949c8bb9..bc025515f 100644 --- a/azalea-entity/src/particle.rs +++ b/azalea-entity/src/particle.rs @@ -1,6 +1,10 @@ use azalea_block::BlockState; use azalea_buf::AzBuf; -use azalea_core::{color::RgbColor, entity_id::MinecraftEntityId, position::BlockPos}; +use azalea_core::{ + color::RgbColor, + entity_id::MinecraftEntityId, + position::{BlockPos, Vec3}, +}; use azalea_inventory::ItemStack; use azalea_registry::builtin::ParticleKind; @@ -26,7 +30,7 @@ pub enum Particle { CopperFireFlame, Crit, DamageIndicator, - DragonBreath, + DragonBreath(PowerParticle), DrippingLava, FallingLava, LandingLava, @@ -34,7 +38,7 @@ pub enum Particle { FallingWater, Dust(DustParticle), DustColorTransition(DustColorTransitionParticle), - Effect, + Effect(SpellParticle), ElderGuardian, EnchantedHit, Enchant, @@ -54,20 +58,20 @@ pub enum Particle { Infested, CherryLeaves, PaleOakLeaves, - TintedLeaves, + TintedLeaves(ColorParticle), SculkSoul, SculkCharge(SculkChargeParticle), SculkChargePop, SoulFireFlame, Soul, - Flash, + Flash(ColorParticle), HappyVillager, Composter, Heart, - InstantEffect, + InstantEffect(SpellParticle), Item(ItemParticle), Vibration(Box), - Trail, + Trail(Box), PauseMobGrowth, ResetMobGrowth, ItemSlime, @@ -129,11 +133,11 @@ pub enum Particle { TrialSpawnerDetection, TrialSpawnerDetectionOminous, VaultConnection, - DustPillar, + DustPillar(BlockParticle), OminousSpawning, RaidOmen, TrialOmen, - BlockCrumble, + BlockCrumble(BlockParticle), Firefly, SulfurCubeGoo, } @@ -154,7 +158,7 @@ impl From for Particle { ParticleKind::Cloud => Self::Cloud, ParticleKind::Crit => Self::Crit, ParticleKind::DamageIndicator => Self::DamageIndicator, - ParticleKind::DragonBreath => Self::DragonBreath, + ParticleKind::DragonBreath => Self::DragonBreath(PowerParticle::default()), ParticleKind::DrippingLava => Self::DrippingLava, ParticleKind::FallingLava => Self::FallingLava, ParticleKind::LandingLava => Self::LandingLava, @@ -164,7 +168,7 @@ impl From for Particle { ParticleKind::DustColorTransition => { Self::DustColorTransition(DustColorTransitionParticle::default()) } - ParticleKind::Effect => Self::Effect, + ParticleKind::Effect => Self::Effect(SpellParticle::default()), ParticleKind::ElderGuardian => Self::ElderGuardian, ParticleKind::EnchantedHit => Self::EnchantedHit, ParticleKind::Enchant => Self::Enchant, @@ -180,17 +184,17 @@ impl From for Particle { ParticleKind::Flame => Self::Flame, ParticleKind::CherryLeaves => Self::CherryLeaves, ParticleKind::PaleOakLeaves => Self::PaleOakLeaves, - ParticleKind::TintedLeaves => Self::TintedLeaves, + ParticleKind::TintedLeaves => Self::TintedLeaves(ColorParticle::default()), ParticleKind::SculkSoul => Self::SculkSoul, ParticleKind::SculkCharge => Self::SculkCharge(SculkChargeParticle::default()), ParticleKind::SculkChargePop => Self::SculkChargePop, ParticleKind::SoulFireFlame => Self::SoulFireFlame, ParticleKind::Soul => Self::Soul, - ParticleKind::Flash => Self::Flash, + ParticleKind::Flash => Self::Flash(ColorParticle::default()), ParticleKind::HappyVillager => Self::HappyVillager, ParticleKind::Composter => Self::Composter, ParticleKind::Heart => Self::Heart, - ParticleKind::InstantEffect => Self::InstantEffect, + ParticleKind::InstantEffect => Self::InstantEffect(SpellParticle::default()), ParticleKind::Item => Self::Item(ItemParticle::default()), ParticleKind::Vibration => Self::Vibration(Default::default()), ParticleKind::ItemSlime => Self::ItemSlime, @@ -256,12 +260,12 @@ impl From for Particle { ParticleKind::TrialSpawnerDetection => Self::TrialSpawnerDetection, ParticleKind::TrialSpawnerDetectionOminous => Self::TrialSpawnerDetectionOminous, ParticleKind::VaultConnection => Self::VaultConnection, - ParticleKind::DustPillar => Self::DustPillar, + ParticleKind::DustPillar => Self::DustPillar(BlockParticle::default()), ParticleKind::OminousSpawning => Self::OminousSpawning, ParticleKind::RaidOmen => Self::RaidOmen, ParticleKind::TrialOmen => Self::TrialOmen, - ParticleKind::Trail => Self::Trail, - ParticleKind::BlockCrumble => Self::BlockCrumble, + ParticleKind::Trail => Self::Trail(Default::default()), + ParticleKind::BlockCrumble => Self::BlockCrumble(BlockParticle::default()), ParticleKind::Firefly => Self::Firefly, ParticleKind::CopperFireFlame => Self::CopperFireFlame, ParticleKind::PauseMobGrowth => Self::PauseMobGrowth, @@ -308,6 +312,25 @@ pub struct ColorParticle { pub color: RgbColor, } +#[derive(AzBuf, Clone, Debug, Default, PartialEq)] +pub struct PowerParticle { + pub power: f32, +} + +#[derive(AzBuf, Clone, Debug, Default, PartialEq)] +pub struct SpellParticle { + pub color: RgbColor, + pub power: f32, +} + +#[derive(AzBuf, Clone, Debug, Default, PartialEq)] +pub struct TrailParticle { + pub target: Vec3, + pub color: RgbColor, + #[var] + pub duration: u32, +} + #[derive(AzBuf, Clone, Debug, Default, PartialEq)] pub struct ItemParticle { pub item: ItemStack, @@ -358,3 +381,37 @@ pub struct GeyserBaseParticle { pub water_blocks: i32, pub burst_impulse_base: f32, } + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use azalea_buf::AzBuf; + + use super::*; + + #[test] + fn test_read_flash_color() { + // flash, then an ARGB int + let bytes = [49, 0xff, 0xff, 0xff, 0xff]; + let mut buf = Cursor::new(bytes.as_slice()); + let particle = Particle::azalea_read(&mut buf).unwrap(); + assert!(matches!(particle, Particle::Flash(_))); + assert_eq!(buf.position() as usize, bytes.len()); + } + + #[test] + fn test_trail_roundtrip() { + let particle = Particle::Trail(Box::new(TrailParticle { + target: Vec3::new(1., -60., 2.), + color: RgbColor::new(255, 0, 0), + duration: 20, + })); + let mut bytes = Vec::new(); + particle.azalea_write(&mut bytes).unwrap(); + assert_eq!(bytes[0], 56); + + let read = Particle::azalea_read(&mut Cursor::new(bytes.as_slice())).unwrap(); + assert_eq!(read, particle); + } +} diff --git a/azalea-inventory/src/components/mod.rs b/azalea-inventory/src/components/mod.rs index b89e32455..847fc297b 100644 --- a/azalea-inventory/src/components/mod.rs +++ b/azalea-inventory/src/components/mod.rs @@ -917,8 +917,7 @@ pub struct BlockState { #[derive(AzBuf, Clone, Debug, PartialEq, Serialize)] pub struct BeehiveOccupant { - #[serde(skip_serializing_if = "is_default")] - pub entity_data: NbtCompound, + pub entity_data: EntityData, #[var] pub ticks_in_hive: i32, #[var] diff --git a/azalea-inventory/tests/components.rs b/azalea-inventory/tests/components.rs index 4a5af7ce5..fff640476 100644 --- a/azalea-inventory/tests/components.rs +++ b/azalea-inventory/tests/components.rs @@ -203,3 +203,31 @@ fn test_glider_checksum() { let c = Glider; assert_eq!(get_checksum(&c, &Default::default()).unwrap().0, 3312760008); } + +#[test] +fn test_bees_typed_entity_data() { + use std::io::Cursor; + + use azalea_buf::AzBuf; + use azalea_inventory::components::{BeehiveOccupant, Bees, EntityData}; + use azalea_registry::builtin::EntityKind; + + let bees = Bees { + occupants: vec![BeehiveOccupant { + entity_data: EntityData { + kind: EntityKind::Bee, + data: NbtCompound::default(), + }, + ticks_in_hive: 1, + min_ticks_in_hive: 1, + }], + }; + let mut bytes = Vec::new(); + bees.azalea_write(&mut bytes).unwrap(); + // 1 occupant, then the bee's entity type id + assert_eq!(&bytes[..2], &[1, 11]); + + let mut buf = Cursor::new(bytes.as_slice()); + assert_eq!(Bees::azalea_read(&mut buf).unwrap(), bees); + assert_eq!(buf.position() as usize, bytes.len()); +} diff --git a/azalea-world/src/palette/container.rs b/azalea-world/src/palette/container.rs index 0bd5da4e0..4c472f751 100644 --- a/azalea-world/src/palette/container.rs +++ b/azalea-world/src/palette/container.rs @@ -37,6 +37,8 @@ pub trait PalletedContainerKind: } fn bits_per_entry_to_palette_kind(bits_per_entry: u8) -> PaletteKind; + + fn global_bits_per_entry() -> u8; } impl PalletedContainerKind for BlockState { type SectionPos = ChunkSectionBlockPos; @@ -53,6 +55,10 @@ impl PalletedContainerKind for BlockState { _ => PaletteKind::Global, } } + + fn global_bits_per_entry() -> u8 { + (u32::BITS - (BlockState::MAX_STATE as u32).leading_zeros()) as u8 + } } impl PalletedContainerKind for Biome { type SectionPos = ChunkSectionBiomePos; @@ -68,6 +74,11 @@ impl PalletedContainerKind for Biome { _ => PaletteKind::Global, } } + + fn global_bits_per_entry() -> u8 { + // Biome registry size is server-side, 16 bits covers any pack + 16 + } } /// A trait for position types that are sometimes valid ways to index into a @@ -243,6 +254,10 @@ impl PalettedContainer { // ``` // assert!(bits_per_entry <= 5, "bits_per_entry must be <= 5"); // ``` + let bits_per_entry = match S::bits_per_entry_to_palette_kind(bits_per_entry) { + PaletteKind::Global => bits_per_entry.max(S::global_bits_per_entry()), + _ => bits_per_entry, + }; let mut new_data = self.create_or_reuse_data(bits_per_entry); new_data.copy_from(&self.palette, &self.storage); *self = new_data; diff --git a/azalea-world/src/palette/tests.rs b/azalea-world/src/palette/tests.rs index 39031a383..48cfab7ce 100644 --- a/azalea-world/src/palette/tests.rs +++ b/azalea-world/src/palette/tests.rs @@ -78,3 +78,33 @@ fn test_coords_from_index() { } } } + +#[test] +fn test_resize_to_global_fits_any_state() { + let mut palette_container = PalettedContainer::::new(); + + for i in 0..256 { + palette_container.set_at_index(i, BlockState::try_from(i as u32).unwrap()); + } + assert_eq!( + PaletteKind::from(&palette_container.palette), + PaletteKind::Hashmap + ); + + let high = BlockState::try_from(BlockState::MAX_STATE).unwrap(); + palette_container.set_at_index(256, high); + assert_eq!( + PaletteKind::from(&palette_container.palette), + PaletteKind::Global + ); + // Same as vanilla's ceillog2(registry size) + assert_eq!(palette_container.bits_per_entry, 15); + + assert_eq!(palette_container.get_at_index(256), high); + for i in 0..256 { + assert_eq!( + palette_container.get_at_index(i), + BlockState::try_from(i as u32).unwrap() + ); + } +}