fix(entity): consolidate hurt sound parity for slime and shared dispatch families (#1935)

* fix(entity): derive hurt sound parity from generated mapping

Keep slime hurt sounds in runtime because they depend on entity size.\n\nMove enderman and undead-family hurt sound mappings into generated data so living.rs dispatches through a shared lookup instead of owning a hardcoded table.

* refactor(entity): derive hurt sound lookup from entities data

The Extractor companion now emits hurt_sound through entities.json. Keep Pumpkin's hurt_sound_for_entity_type helper narrow and generated while moving its source of truth to entity data.

Slime remains a runtime special case because its hurt sound depends on size rather than static entity metadata.

* refactor(entity): fold hurt sound helper into entity codegen

Move hurt-sound helper generation into the existing entity codegen path.

Remove the standalone helper builder and generated file now that hurt_sound is sourced from entities.json. Runtime dispatch stays unchanged and slime remains a runtime size-based exception.

* refactor(entity): inline hurt sound into EntityType

Remove the standalone entity_hurt_sound feature and helper path.

Hurt sounds now live directly on generated EntityType values, while runtime behavior stays the same and slime remains the size-based runtime exception.
This commit is contained in:
BitForge
2026-04-08 13:33:45 -04:00
committed by GitHub
parent 4c12e33152
commit 4916c05997
8 changed files with 307 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
use std::{collections::BTreeMap, fs};
use heck::ToPascalCase;
use proc_macro2::TokenStream;
use pumpkin_util::HeightMap;
use quote::{ToTokens, format_ident, quote};
@@ -14,6 +15,8 @@ pub struct EntityType {
/// Numeric registry ID for this entity type.
pub id: u16,
pub attributes: Option<Vec<BTreeMap<String, f64>>>,
/// Static hurt sound event name when it is safely derivable from extracted entity data.
pub hurt_sound: Option<String>,
/// Whether this entity can be attacked by players or other entities.
pub attackable: Option<bool>,
/// Whether this entity is classified as a mob (affects spawning mechanics).
@@ -110,6 +113,13 @@ impl ToTokens for NamedEntityType<'_> {
quote! { None }
};
let hurt_sound = if let Some(sound_name) = entity.hurt_sound.as_ref() {
let sound_ident = format_ident!("{}", sound_name.to_pascal_case());
quote! { Some(Sound::#sound_ident) }
} else {
quote! { None }
};
let spawn_restriction_location = match entity.spawn_restriction.location {
SpawnLocation::InLava => quote! {SpawnLocation::InLava},
SpawnLocation::InWater => quote! {SpawnLocation::InWater},
@@ -174,6 +184,7 @@ impl ToTokens for NamedEntityType<'_> {
EntityType {
id: #id,
attributes: #attributes_field,
hurt_sound: #hurt_sound,
attackable: #attackable,
mob: #mob,
saveable: #saveable,
@@ -227,6 +238,7 @@ pub fn build() -> TokenStream {
use crate::tag::Taggable;
use crate::tag::RegistryKey;
use crate::attributes::Attributes;
use crate::sound::Sound;
use pumpkin_util::loot_table::*;
use pumpkin_util::HeightMap;
use std::hash::Hash;
@@ -235,6 +247,7 @@ pub fn build() -> TokenStream {
pub struct EntityType {
pub id: u16,
pub attributes: &'static [(Attributes, f64)],
pub hurt_sound: Option<Sound>,
pub attackable: Option<bool>,
pub mob: bool,
pub saveable: bool,
@@ -399,6 +412,7 @@ pub fn build() -> TokenStream {
}
}
}
impl IDSetContent for EntityType {
fn registry_id(&self) -> u16 {
Taggable::registry_id(self)

View File

@@ -82,7 +82,7 @@ biome = []
chunk_status = []
entity_pose = []
entity_status = []
entity_type = []
entity_type = ["sound"]
spawn_egg = []
world_event = []
message_type = []

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ use crate::block::OnLandedUponArgs;
use crate::entity::attributes::AttributeInstance;
use crate::entity::attributes::Modifier;
use crate::entity::attributes::ModifierOperation;
use crate::entity::mob::slime::SlimeEntity;
use crate::entity::{EntityBaseFuture, NbtFuture};
use crate::server::Server;
use crate::world::loot::{LootContextParameters, LootTableExt};
@@ -126,6 +127,10 @@ impl LivingEntity {
&Block::SLIME_BLOCK,
];
fn hurt_sound_for_entity(entity_type: &'static EntityType) -> Sound {
entity_type.hurt_sound.unwrap_or(Sound::EntityGenericHurt)
}
pub fn new(entity: Entity) -> Self {
let water_movement_speed_multiplier = if entity.entity_type == &EntityType::POLAR_BEAR {
0.98
@@ -1739,6 +1744,14 @@ impl LivingEntity {
pub fn get_movement(&self) -> Vector3<f64> {
self.entity.movement.load()
}
fn hurt_sound(&self) -> Sound {
if self.entity.entity_type == &EntityType::SLIME {
SlimeEntity::hurt_sound_for_size(self.entity.data.load(Relaxed))
} else {
Self::hurt_sound_for_entity(self.entity.entity_type)
}
}
}
impl NBTStorage for LivingEntity {
@@ -1938,7 +1951,7 @@ impl EntityBase for LivingEntity {
if play_sound {
world
.play_sound(
Sound::EntityGenericHurt,
self.hurt_sound(),
SoundCategory::Players,
&self.entity.pos.load(),
)
@@ -2343,4 +2356,55 @@ mod tests {
);
}
}
#[test]
fn hurt_sound_for_entity_uses_zombie_family_sounds() {
let cases = [
(&EntityType::ZOMBIE, Sound::EntityZombieHurt),
(&EntityType::DROWNED, Sound::EntityDrownedHurt),
(&EntityType::HUSK, Sound::EntityHuskHurt),
(
&EntityType::ZOMBIE_VILLAGER,
Sound::EntityZombieVillagerHurt,
),
];
for (entity_type, expected) in cases {
assert_eq!(LivingEntity::hurt_sound_for_entity(entity_type), expected);
}
}
#[test]
fn hurt_sound_for_entity_uses_enderman_hurt_sound() {
assert_eq!(
LivingEntity::hurt_sound_for_entity(&EntityType::ENDERMAN),
Sound::EntityEndermanHurt
);
}
#[test]
fn hurt_sound_for_entity_uses_skeleton_family_sounds() {
let cases = [
(&EntityType::SKELETON, Sound::EntitySkeletonHurt),
(&EntityType::BOGGED, Sound::EntityBoggedHurt),
(&EntityType::PARCHED, Sound::EntityParchedHurt),
(
&EntityType::WITHER_SKELETON,
Sound::EntityWitherSkeletonHurt,
),
(&EntityType::STRAY, Sound::EntityStrayHurt),
];
for (entity_type, expected) in cases {
assert_eq!(LivingEntity::hurt_sound_for_entity(entity_type), expected);
}
}
#[test]
fn hurt_sound_for_entity_defaults_to_generic_hurt() {
assert_eq!(
LivingEntity::hurt_sound_for_entity(&EntityType::CREEPER),
Sound::EntityGenericHurt
);
}
}

View File

@@ -31,6 +31,7 @@ pub mod creeper;
pub mod enderman;
pub mod silverfish;
pub mod skeleton;
pub mod slime;
pub mod zombie;
pub struct MobEntity {

View File

@@ -0,0 +1,64 @@
use std::sync::Arc;
use std::sync::atomic::Ordering::Relaxed;
use pumpkin_data::sound::Sound;
use pumpkin_nbt::compound::NbtCompound;
use crate::entity::{
Entity, NBTStorage, NbtFuture,
mob::{Mob, MobEntity},
};
pub struct SlimeEntity {
entity: Arc<MobEntity>,
}
impl SlimeEntity {
pub fn new(entity: Entity) -> Arc<Self> {
Arc::new(Self {
entity: Arc::new(MobEntity::new(entity)),
})
}
pub(crate) const fn hurt_sound_for_size(size: i32) -> Sound {
if size == 1 {
Sound::EntitySlimeHurtSmall
} else {
Sound::EntitySlimeHurt
}
}
}
impl NBTStorage for SlimeEntity {
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.entity.living_entity.entity.read_nbt_non_mut(nbt).await;
self.entity
.living_entity
.entity
.data
.store(nbt.get_int("Size").unwrap_or(0), Relaxed);
})
}
}
impl Mob for SlimeEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.entity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uses_small_hurt_sound_only_for_smallest_slimes() {
assert_eq!(
SlimeEntity::hurt_sound_for_size(1),
Sound::EntitySlimeHurtSmall
);
assert_eq!(SlimeEntity::hurt_sound_for_size(0), Sound::EntitySlimeHurt);
assert_eq!(SlimeEntity::hurt_sound_for_size(2), Sound::EntitySlimeHurt);
}
}

View File

@@ -24,6 +24,7 @@ use crate::{
bogged::BoggedSkeletonEntity, parched::ParchedSkeletonEntity,
skeleton::SkeletonEntity, stray::StraySkeletonEntity, wither::WitherSkeletonEntity,
},
slime::SlimeEntity,
zombie::{drowned::DrownedEntity, husk::HuskEntity, zombie::ZombieEntity},
},
passive::{
@@ -80,6 +81,7 @@ pub async fn from_type(
id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)),
id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),
id if id == EntityType::SILVERFISH.id => SilverfishEntity::new(entity).await,
id if id == EntityType::SLIME.id => SlimeEntity::new(entity),
// Fallback Entity
_ => {
if entity_type.attributes.is_empty() {