Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 23 additions & 1 deletion azalea-chat/src/numbers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?,
Expand All @@ -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());
}
}
91 changes: 74 additions & 17 deletions azalea-entity/src/particle.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -26,15 +30,15 @@ pub enum Particle {
CopperFireFlame,
Crit,
DamageIndicator,
DragonBreath,
DragonBreath(PowerParticle),
DrippingLava,
FallingLava,
LandingLava,
DrippingWater,
FallingWater,
Dust(DustParticle),
DustColorTransition(DustColorTransitionParticle),
Effect,
Effect(SpellParticle),
ElderGuardian,
EnchantedHit,
Enchant,
Expand All @@ -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<VibrationParticle>),
Trail,
Trail(Box<TrailParticle>),
PauseMobGrowth,
ResetMobGrowth,
ItemSlime,
Expand Down Expand Up @@ -129,11 +133,11 @@ pub enum Particle {
TrialSpawnerDetection,
TrialSpawnerDetectionOminous,
VaultConnection,
DustPillar,
DustPillar(BlockParticle),
OminousSpawning,
RaidOmen,
TrialOmen,
BlockCrumble,
BlockCrumble(BlockParticle),
Firefly,
SulfurCubeGoo,
}
Expand All @@ -154,7 +158,7 @@ impl From<ParticleKind> 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,
Expand All @@ -164,7 +168,7 @@ impl From<ParticleKind> 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,
Expand All @@ -180,17 +184,17 @@ impl From<ParticleKind> 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,
Expand Down Expand Up @@ -256,12 +260,12 @@ impl From<ParticleKind> 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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
3 changes: 1 addition & 2 deletions azalea-inventory/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
28 changes: 28 additions & 0 deletions azalea-inventory/tests/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
15 changes: 15 additions & 0 deletions azalea-world/src/palette/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -243,6 +254,10 @@ impl<S: PalletedContainerKind> PalettedContainer<S> {
// ```
// 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;
Expand Down
30 changes: 30 additions & 0 deletions azalea-world/src/palette/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,33 @@ fn test_coords_from_index() {
}
}
}

#[test]
fn test_resize_to_global_fits_any_state() {
let mut palette_container = PalettedContainer::<BlockState>::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()
);
}
}