Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
19c4682
add chickens
Purdze Aug 7, 2026
4166559
add enderman, slime and witch
Purdze Aug 7, 2026
dff8798
add husk, drowned, zombie villager, stray and bogged
Purdze Aug 7, 2026
d23b209
cleanup
Purdze Aug 8, 2026
fbbfc2e
Merge branch 'chickens' into enderman-slime-witch
Purdze Aug 8, 2026
0391f33
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
dfb3fe1
cleanup
Purdze Aug 8, 2026
0e9c35c
cleanup
Purdze Aug 8, 2026
08266d5
Merge branch 'chickens' into enderman-slime-witch
Purdze Aug 8, 2026
9a481a6
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
cfdb58a
cleanup
Purdze Aug 8, 2026
f9b392a
Merge branch 'chickens' into enderman-slime-witch
Purdze Aug 8, 2026
3373266
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
d336db5
cleanup
Purdze Aug 8, 2026
769ffc5
Merge branch 'chickens' into enderman-slime-witch
Purdze Aug 8, 2026
f78d65d
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
42b3550
cleanup
Purdze Aug 8, 2026
804b0d8
Merge branch 'chickens' into enderman-slime-witch
Purdze Aug 8, 2026
c776d84
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
5aa40bd
cleanup
Purdze Aug 8, 2026
58c5a5b
Merge branch 'chickens' into enderman-slime-witch
Purdze Aug 8, 2026
5c4ab53
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
6f63020
Merge remote-tracking branch 'origin/master' into enderman-slime-witch
Purdze Aug 8, 2026
ecb5bc8
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
83d6bad
fix entity mirroring
Purdze Aug 8, 2026
b1e2dae
Merge branch 'entity-mirror' into enderman-slime-witch
Purdze Aug 8, 2026
3392676
cleanup
Purdze Aug 8, 2026
5f62b00
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
38f40bd
cleanup
Purdze Aug 8, 2026
e39acfb
Merge branch 'entity-mirror' into enderman-slime-witch
Purdze Aug 8, 2026
1843665
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
84439d7
cleanup
Purdze Aug 8, 2026
66a3c65
Merge branch 'entity-mirror' into enderman-slime-witch
Purdze Aug 8, 2026
ab4cff5
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
5ef4468
cleanup
Purdze Aug 8, 2026
06a2f8c
Merge branch 'entity-mirror' into enderman-slime-witch
Purdze Aug 8, 2026
208efd8
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
5b1c629
cleanup
Purdze Aug 8, 2026
17a3b23
Merge branch 'entity-mirror' into enderman-slime-witch
Purdze Aug 8, 2026
d9cbe45
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
34fb725
Merge remote-tracking branch 'origin/master' into enderman-slime-witch
Purdze Aug 8, 2026
e0cf1c7
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
d7a61dd
cleanup
Purdze Aug 8, 2026
2aecd43
Merge branch 'enderman-slime-witch' into undead-variants
Purdze Aug 8, 2026
a88779f
Merge remote-tracking branch 'origin/master' into undead-variants
Purdze Aug 8, 2026
ecc2428
cleanup
Purdze Aug 8, 2026
d24c0cb
cleanup
Purdze Aug 8, 2026
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
19 changes: 2 additions & 17 deletions pomme-client/src/app/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,15 +1085,12 @@ impl AppCore {
mesh.z_size,
);
}
NetworkEvent::EntityBabyFlag { id, is_baby } => {
game.entity_store.set_baby(id, is_baby);
NetworkEvent::EntityData { id, index, value } => {
game.entity_store.apply_entity_data(id, index, value);
}
NetworkEvent::EntityPose { id, is_crouching } => {
game.entity_store.set_crouching(id, is_crouching);
}
NetworkEvent::SheepWoolData { id, color, sheared } => {
game.entity_store.set_sheep_wool(id, color, sheared);
}
NetworkEvent::SheepEatStart { id } => {
game.entity_store.start_sheep_eat(id);
}
Expand All @@ -1115,12 +1112,6 @@ impl AppCore {
NetworkEvent::EntityVariant { id, kind, variant } => {
game.entity_store.set_variant(id, kind, variant);
}
NetworkEvent::MobFlag { id, flag, value } => {
game.entity_store.set_mob_flag(id, flag, value);
}
NetworkEvent::SlimeSize { id, size } => {
game.entity_store.set_slime_size(id, size);
}
NetworkEvent::VillagerData {
id,
kind,
Expand All @@ -1130,15 +1121,9 @@ impl AppCore {
game.entity_store
.set_villager_data(id, kind, profession, level);
}
NetworkEvent::VillagerUnhappy { id, counter } => {
game.entity_store.set_villager_unhappy(id, counter);
}
NetworkEvent::EntityCustomName { id, name } => {
game.entity_store.set_custom_name(id, name);
}
NetworkEvent::EntityAggressive { id, aggressive } => {
game.entity_store.set_aggressive(id, aggressive);
}
NetworkEvent::EntitySwing { id } => {
game.entity_store.start_swing(id);
}
Expand Down
23 changes: 17 additions & 6 deletions pomme-client/src/app/phases/in_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,7 @@ pub fn update_game(
flap: extras.flap,
flap_speed: extras.flap_speed,
is_creepy: e.is_creepy,
is_converting: e.is_converting,
// TODO: derive from the main-hand item (vanilla
// `isHoldingItem`) once mob equipment tracking lands.
is_holding_item: e.witch_drinking,
Expand Down Expand Up @@ -2319,6 +2320,7 @@ pub fn update_game(
flap: 0.0,
flap_speed: 0.0,
is_creepy: false,
is_converting: false,
is_holding_item: false,
nose_wobble_speed: 0.0,
body_transform: None,
Expand Down Expand Up @@ -2821,9 +2823,15 @@ fn entity_extras(entity_id: i32, e: &crate::entity::LivingEntity, alpha: f32) ->
..Default::default()
},
EntityKind::Sheep => sheep_extras(entity_id, e, alpha),
EntityKind::Villager => villager_extras(e),
// Spider eyes overlay is always visible (slot 0).
EntityKind::Spider => EntityExtras {
EntityKind::Villager => villager_like_extras(e, &VILLAGER_TYPE_HAT),
EntityKind::ZombieVillager => villager_like_extras(e, &ZOMBIE_VILLAGER_TYPE_HAT),
EntityKind::Bogged => EntityExtras {
overlay_tints: SLOT0_TINTS,
variant_index: e.is_sheared as u32,
..Default::default()
},
// Always-visible slot-0 overlay (spider eyes, drowned/stray clothing).
EntityKind::Spider | EntityKind::Drowned | EntityKind::Stray => EntityExtras {
overlay_tints: SLOT0_TINTS,
..Default::default()
},
Expand Down Expand Up @@ -2916,6 +2924,8 @@ fn sheep_extras(entity_id: i32, e: &crate::entity::LivingEntity, alpha: f32) ->
/// `.png.mcmeta` files under `textures/entity/villager/` (hardcoded — no
/// resource-pack support). 0 = none, 1 = partial, 2 = full.
const VILLAGER_TYPE_HAT: [u8; 7] = [2, 0, 0, 0, 2, 0, 0]; // desert, snow = full
// `zombie_villager/type/` ships no `.mcmeta` files at all.
const ZOMBIE_VILLAGER_TYPE_HAT: [u8; 7] = [0; 7];
const VILLAGER_PROFESSION_HAT: [u8; 15] = [
0, // none
0, // armorer
Expand All @@ -2936,14 +2946,15 @@ const VILLAGER_PROFESSION_HAT: [u8; 15] = [

/// Overlay slots: 0 = biome type (full model), 1 = biome type (no-hat model),
/// 2 = profession, 3 = profession level. Mirrors vanilla
/// `VillagerProfessionLayer.submit`.
fn villager_extras(e: &crate::entity::LivingEntity) -> EntityExtras {
/// `VillagerProfessionLayer.submit`, shared by villager and zombie villager
/// (which differ only in their type-hat `.mcmeta` tables).
fn villager_like_extras(e: &crate::entity::LivingEntity, type_hat_table: &[u8; 7]) -> EntityExtras {
use crate::entity::villager::VillagerProfession;

let kind = e.villager_kind as usize;
let profession = e.villager_profession as usize;

let type_hat = VILLAGER_TYPE_HAT[kind];
let type_hat = type_hat_table[kind];
let prof_hat = VILLAGER_PROFESSION_HAT[profession];
let type_hat_visible = prof_hat == 0 || (prof_hat == 1 && type_hat != 2);

Expand Down
141 changes: 82 additions & 59 deletions pomme-client/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,35 @@ use crate::physics::collision::resolve_collision;
use crate::world::block::{FluidKind, fluid};
use crate::world::chunk::ChunkStore;

/// Kind-gated boolean mob states; each flag belongs to one mob kind and
/// [`EntityStore::set_mob_flag`] drops writes for a mismatched entity.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MobFlag {
CreeperPowered,
EndermanCreepy,
WitchDrinking,
/// A scalar synched-entity-data value, forwarded raw from the wire;
/// [`EntityStore::apply_entity_data`] gives it meaning per (kind, index).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MetaValue {
Bool(bool),
Int(i32),
Byte(u8),
Float(f32),
Long(i64),
}

/// Kinds whose entity-data index 16 is the baby flag: `AgeableMob`
/// descendants plus the zombie family (which defines its own baby flag at
/// the same index). NOT baby at 16: Bogged (sheared), Skeleton (stray
/// conversion), Witch (Raider celebrating), fish (from-bucket).
fn is_baby_kind(kind: EntityKind) -> bool {
matches!(
kind,
EntityKind::Pig
| EntityKind::Cow
| EntityKind::Sheep
| EntityKind::Chicken
| EntityKind::Villager
| EntityKind::Slime
| EntityKind::Zombie
| EntityKind::Husk
| EntityKind::Drowned
| EntityKind::ZombieVillager
)
}

const INTERPOLATION_STEPS: i32 = 3;
Expand Down Expand Up @@ -48,9 +70,11 @@ pub struct LivingEntity {
pub is_crouching: bool,
pub on_ground: bool,
pub wool_color: Option<u8>,
/// Sheep wool shorn / bogged mushrooms shorn.
pub is_sheared: bool,
/// Registry/wire variant slot; meaning is per-kind (pool index for
/// cow/chicken). Normalized in `EntityStore::set_variant`.
/// Registry/wire variant slot; meaning is per-kind. Holder-backed values
/// are pre-resolved to pool indices by the net handler; raw-int kinds are
/// normalized in `EntityStore::apply_entity_data`.
pub variant: u32,
/// Chicken wing-flap state (vanilla `Chicken.aiStep`): `flap` is the
/// unbounded wing-cycle phase, `flap_speed` the 0..1 amplitude.
Expand All @@ -66,6 +90,8 @@ pub struct LivingEntity {
/// Enderman screaming flag — raises the head and jitters the render
/// position.
pub is_creepy: bool,
/// Zombie-family conversion (drowning / villager cure) — body-yaw shake.
pub is_converting: bool,
/// Witch drinking flag — swings the nose down toward the potion.
pub witch_drinking: bool,
pub villager_kind: VillagerKind,
Expand Down Expand Up @@ -138,6 +164,7 @@ impl LivingEntity {
prev_squish: 0.0,
slime_size: 1,
is_creepy: false,
is_converting: false,
witch_drinking: false,
villager_kind: VillagerKind::default(),
villager_profession: VillagerProfession::default(),
Expand Down Expand Up @@ -619,9 +646,43 @@ impl EntityStore {
}
}

pub fn set_baby(&mut self, id: i32, is_baby: bool) {
if let Some(entity) = self.living.get_mut(&id) {
entity.is_baby = is_baby;
/// Resolves a raw synched-entity-data scalar per (kind, index), the
/// direct analogue of vanilla's per-class `onSyncedDataUpdated`. Index
/// arithmetic follows the registration chain: `Entity` 0-7,
/// `LivingEntity` 8-14, `Mob` 15, `AgeableMob` 16 baby + 17 age-locked,
/// first subclass field 18. Where an index moved between supported
/// versions both spots are accepted (the other index carries an
/// incompatible type or kind on each version).
pub fn apply_entity_data(&mut self, id: i32, index: u8, value: MetaValue) {
use MetaValue::{Bool, Byte, Int};
let Some(entity) = self.living.get_mut(&id) else {
return;
};
match (entity.entity_type, index, value) {
// Mob flags byte: bit 0x04 = aggressive.
(_, 15, Byte(f)) => entity.aggressive = f & 0x04 != 0,
(k, 16, Bool(b)) if is_baby_kind(k) => entity.is_baby = b,
// Skeleton: powder-snow stray conversion; drives the vanilla
// `isShaking` body jitter.
(EntityKind::Skeleton, 16, Bool(b)) => entity.is_converting = b,
(EntityKind::Bogged, 16, Bool(b)) => entity.is_sheared = b,
// Slime size: 16 on 1.21.9-26.1.x, 18 since Slime joined
// AgeableMob in 26.2.
(EntityKind::Slime, 16 | 18, Int(s)) => entity.slime_size = s.clamp(1, 127) as u8,
// Sheep wool byte (low nibble = DyeColor, bit 0x10 = sheared):
// 17 on 1.21.9-26.1.x, 18 on 26.2.
(EntityKind::Sheep, 17 | 18, Byte(w)) => {
entity.wool_color = Some(w & 0x0F);
entity.is_sheared = w & 0x10 != 0;
}
(EntityKind::Creeper, 17, Bool(b)) => entity.powered = b,
(EntityKind::Enderman, 17, Bool(b)) => entity.is_creepy = b,
(EntityKind::Witch, 17, Bool(b)) => entity.witch_drinking = b,
// Zombie-family underwater conversion / zombie villager curing.
(EntityKind::Zombie | EntityKind::Husk | EntityKind::Drowned, 18, Bool(b))
| (EntityKind::ZombieVillager, 19, Bool(b)) => entity.is_converting = b,
(EntityKind::Villager, 18, Int(c)) => entity.unhappy_counter = c,
_ => {}
}
}

Expand All @@ -631,15 +692,6 @@ impl EntityStore {
}
}

pub fn set_sheep_wool(&mut self, id: i32, color: u8, sheared: bool) {
if let Some(entity) = self.living.get_mut(&id)
&& entity.entity_type == EntityKind::Sheep
{
entity.wool_color = Some(color);
entity.is_sheared = sheared;
}
}

/// `kind` is the mob the emitting handler arm resolved the value for;
/// metadata indices are overloaded across kinds, so a mismatched entity
/// ignores the write.
Expand All @@ -651,29 +703,6 @@ impl EntityStore {
}
}

/// Applies a [`MobFlag`] write, dropping it when the entity isn't the
/// flag's mob (metadata indices are overloaded across kinds, so the net
/// handler emits every candidate flag for an ambiguous boolean).
pub fn set_mob_flag(&mut self, id: i32, flag: MobFlag, value: bool) {
let Some(entity) = self.living.get_mut(&id) else {
return;
};
match (flag, entity.entity_type) {
(MobFlag::CreeperPowered, EntityKind::Creeper) => entity.powered = value,
(MobFlag::EndermanCreepy, EntityKind::Enderman) => entity.is_creepy = value,
(MobFlag::WitchDrinking, EntityKind::Witch) => entity.witch_drinking = value,
_ => {}
}
}

pub fn set_slime_size(&mut self, id: i32, size: i32) {
if let Some(entity) = self.living.get_mut(&id)
&& entity.entity_type == EntityKind::Slime
{
entity.slime_size = size.clamp(1, 127) as u8;
}
}

pub fn set_villager_data(
&mut self,
id: i32,
Expand All @@ -682,22 +711,17 @@ impl EntityStore {
level: u32,
) {
if let Some(entity) = self.living.get_mut(&id)
&& entity.entity_type == EntityKind::Villager
&& matches!(
entity.entity_type,
EntityKind::Villager | EntityKind::ZombieVillager
)
{
entity.villager_kind = kind;
entity.villager_profession = profession;
entity.villager_level = level;
}
}

pub fn set_villager_unhappy(&mut self, id: i32, counter: i32) {
if let Some(entity) = self.living.get_mut(&id)
&& entity.entity_type == EntityKind::Villager
{
entity.unhappy_counter = counter;
}
}

pub fn start_sheep_eat(&mut self, id: i32) {
if let Some(entity) = self.living.get_mut(&id)
&& entity.entity_type == EntityKind::Sheep
Expand All @@ -720,12 +744,6 @@ impl EntityStore {
}
}

pub fn set_aggressive(&mut self, id: i32, aggressive: bool) {
if let Some(entity) = self.living.get_mut(&id) {
entity.aggressive = aggressive;
}
}

/// Begins an arm swing (server `Animate` packet). Restarts when idle or
/// past the halfway point (vanilla `LivingEntity.swing`); `swing_time`
/// counts down, so that is `swing_time <= SWING_DURATION / 2`.
Expand Down Expand Up @@ -846,5 +864,10 @@ pub fn is_living_mob(kind: &EntityKind) -> bool {
| EntityKind::Enderman
| EntityKind::Slime
| EntityKind::Witch
| EntityKind::Husk
| EntityKind::Drowned
| EntityKind::ZombieVillager
| EntityKind::Stray
| EntityKind::Bogged
)
}
Loading
Loading