Merge pull request #1798 from socksprox/fix/mob-combat-poses

🤖 Agent: thank you! Merged after the contribution gate (checked against vanilla 26.3; one integration branch of main + its batch: clippy -D warnings, tests, the affected parity harnesses (111/112, all pass), in game on the server thread and in-frame, lan_e2e in both host modes) and this head's verify.
This commit is contained in:
Ludovic Marie
2026-10-25 23:10:07 +01:00
committed by GitHub
7 changed files with 116 additions and 38 deletions
+22
View File
@@ -162,6 +162,13 @@ fn mob_meta(
meta.push((3, serializer::BOOLEAN, Value::Bool(true)));
}
}
// LivingEntity.DATA_LIVING_ENTITY_FLAGS: using an item 1, in the off
// hand 2 (a skeleton draws its bow, a witch raises her potion).
let using = i8::from(m.extra.using_item > 0) | i8::from(m.extra.using_off_hand) << 1;
if using != 0 {
meta.push((8, serializer::BYTE, Value::Byte(using)));
}
defaults.push((8, serializer::BYTE, Value::Byte(0)));
// LivingEntity.DATA_HEALTH_ID: defined as 1, then set to the max health.
let health = m.health.max(0.0);
if health != 1.0 {
@@ -293,6 +300,21 @@ fn mob_meta(
meta.push((18, serializer::BOOLEAN, Value::Bool(true)));
}
}
// Pillager.IS_CHARGING_CROSSBOW: the charge pose and the held
// item's loading animation.
"minecraft:pillager" => {
if e.flag {
meta.push((17, serializer::BOOLEAN, Value::Bool(true)));
}
defaults.push((17, serializer::BOOLEAN, Value::Bool(false)));
}
// SpellcasterIllager.DATA_SPELL_CASTING_ID: the casting pose.
"minecraft:evoker" | "minecraft:illusioner" => {
if e.variant2 != 0 {
meta.push((17, serializer::BYTE, Value::Byte(e.variant2 as i8)));
}
defaults.push((17, serializer::BYTE, Value::Byte(0)));
}
// Enderman.DATA_CARRY_STATE.
"minecraft:enderman" => {
if let Some(state) = e.carried {
+2 -2
View File
@@ -259,7 +259,7 @@ impl Game<'_> {
// ItemInHandLayer submits with OverlayTexture.NO_OVERLAY: no hurt tint.
for (item, off_hand) in [(&mob.extra.main_hand, false), (&mob.extra.off_hand, true)] {
let Some(item) = item else { continue };
let left = off_hand != mob.extra.main_hand_left;
let left = off_hand != mob.main_arm_left();
let Some(t) =
render::held_item_transform(mob, alpha as f32, camera, self.res.mob_data(), left)
else {
@@ -305,7 +305,7 @@ impl Game<'_> {
.player_model
.as_ref()
.is_some_and(|p| std::ptr::eq(p, mob));
owner.main_arm_left = mob.extra.main_hand_left;
owner.main_arm_left = mob.main_arm_left();
if off_hand == mob.extra.using_off_hand && mob.extra.using_item > 0 {
owner.using_this_item = true;
owner.use_duration = 72_000;
+24 -34
View File
@@ -1904,25 +1904,12 @@ impl Game<'_> {
}
}
}
// LivingEntity.onSyncedDataUpdated(DATA_LIVING_ENTITY_FLAGS):
// another player starts using the item in a hand (eating, a
// bow drawn, a shield up), or stops.
// LivingEntity.DATA_LIVING_ENTITY_FLAGS: for another player
// too, remote_mob_data's apply reads index 8 above.
let other = self
.remote
.as_ref()
.is_some_and(|r| r.level_players.contains_key(&p.id) && r.entity_id != p.id);
if other && let Some(m) = self.mobs.iter_mut().find(|m| m.id == p.id as u32) {
for (index, _, value) in &p.values {
if let (8, Value::Byte(flags)) = (index, value) {
m.extra.using_off_hand = flags & 2 != 0;
if flags & 1 == 0 {
m.extra.using_item = 0;
} else if m.extra.using_item == 0 {
m.extra.using_item = 1;
}
}
}
}
// LivingEntity.onSyncedDataUpdated(SLEEPING_POS_ID): another
// player lies down in their bed (drawn lying in it), or gets up.
if other {
@@ -3406,25 +3393,28 @@ impl Game<'_> {
m.head_yaw += wrap_degrees(*yaw - m.head_yaw) / *steps as f32;
*steps -= 1;
}
// LivingEntity.updatingUsingItem for another player: the ticks the
// item has been used before this one.
if player && m.extra.using_item > 0 {
let item = if m.extra.using_off_hand {
m.extra.off_hand.clone()
} else {
m.extra.main_hand.clone()
};
if let Some(item) = item {
let eye = if m.extra.crouching { 1.27 } else { 1.62 };
let at = [m.position.x, m.position.y, m.position.z];
using.push((
item,
m.extra.using_item as u32 - 1,
at,
eye,
m.head_pitch,
m.yaw,
));
// LivingEntity.updatingUsingItem: the ticks the item has been
// used before this one. Another player's use particles; a mob's
// drawn bow only needs the count (its pull follows it).
if m.extra.using_item > 0 {
if player {
let item = if m.extra.using_off_hand {
m.extra.off_hand.clone()
} else {
m.extra.main_hand.clone()
};
if let Some(item) = item {
let eye = if m.extra.crouching { 1.27 } else { 1.62 };
let at = [m.position.x, m.position.y, m.position.z];
using.push((
item,
m.extra.using_item as u32 - 1,
at,
eye,
m.head_pitch,
m.yaw,
));
}
}
m.extra.using_item += 1;
}
@@ -20,6 +20,17 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin) {
// client, which draws the flames. A client doesn't count the
// fire down: the flag goes off when the server's fire is out.
(0, Value::Byte(flags)) => m.fire_ticks = i32::from(flags & 1),
// LivingEntity.DATA_LIVING_ENTITY_FLAGS: using an item 1, in the
// off hand 2. The flag starts and stops the use; the client ticks
// the count itself (a bow's pull follows it).
(8, Value::Byte(flags)) => {
m.extra.using_off_hand = flags & 2 != 0;
if flags & 1 == 0 {
m.extra.using_item = 0;
} else if m.extra.using_item == 0 {
m.extra.using_item = 1;
}
}
// LivingEntity.DATA_HEALTH_ID: the boss bars and the death
// animation follow it.
(9, Value::Float(health)) => m.health = *health,
@@ -53,6 +64,13 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin) {
(Species::Slime | Species::MagmaCube, 18, Value::Int(size)) => {
m.extra.size = (*size).clamp(1, 127);
}
// Pillager.IS_CHARGING_CROSSBOW: the charge pose (Raider's
// IS_CELEBRATING takes 16).
(Species::Pillager, 17, Value::Bool(charging)) => m.extra.flag = *charging,
// SpellcasterIllager.DATA_SPELL_CASTING_ID: the casting pose.
(Species::Evoker | Species::Illusioner, 17, Value::Byte(spell)) => {
m.extra.variant2 = (*spell).max(0) as u8;
}
// Villager.DATA_VILLAGER_DATA and ZombieVillager's (after
// Zombie's three): what VillagerProfessionLayer draws.
(Species::Villager, 19, Value::Ids(data))
@@ -239,4 +257,42 @@ mod tests {
);
assert_eq!(name(&z), ("desert", "farmer", 3));
}
#[test]
fn the_mob_flags_drive_the_aggressive_poses() {
let Some(mut m) = mob("minecraft:skeleton") else {
return;
};
apply(&mut m, &[(15, s::BYTE, Value::Byte(2 | 4))]);
assert!(m.ai.aggressive && m.extra.left_handed && !m.no_ai);
apply(&mut m, &[(15, s::BYTE, Value::Byte(0))]);
assert!(!m.ai.aggressive && !m.extra.left_handed);
}
#[test]
fn the_living_flags_start_and_stop_a_mob_using_an_item() {
let Some(mut m) = mob("minecraft:skeleton") else {
return;
};
apply(&mut m, &[(8, s::BYTE, Value::Byte(1))]);
assert_eq!(m.extra.using_item, 1);
assert!(!m.extra.using_off_hand);
// Using in the off hand.
apply(&mut m, &[(8, s::BYTE, Value::Byte(3))]);
assert!(m.extra.using_off_hand);
apply(&mut m, &[(8, s::BYTE, Value::Byte(0))]);
assert_eq!(m.extra.using_item, 0);
}
#[test]
fn a_pillager_charges_and_a_spellcaster_casts() {
let Some(mut p) = mob("minecraft:pillager") else {
return;
};
apply(&mut p, &[(17, s::BOOLEAN, Value::Bool(true))]);
assert!(p.extra.flag);
let mut e = mob("minecraft:evoker").unwrap();
apply(&mut e, &[(17, s::BYTE, Value::Byte(2))]);
assert_eq!(e.extra.variant2, 2);
}
}
+5
View File
@@ -1646,6 +1646,11 @@ impl Mob {
pub fn species(&self) -> Species {
self.kind.species
}
/// `Mob.getMainArm`: the left arm when the mob is left-handed, a player's
/// `mainHand` option for a player mob.
pub fn main_arm_left(&self) -> bool {
self.extra.main_hand_left || self.extra.left_handed
}
/// The player hit this mob (melee): LivingEntity.hurt with knockback.
pub fn hurt(
&mut self,
+2 -2
View File
@@ -642,7 +642,7 @@ pub fn setup(
} else {
ArmPose::Empty
};
let mut h = if m.extra.main_hand_left {
let mut h = if m.main_arm_left() {
Hum::new(ArmPose::Empty, main)
} else {
Hum::new(main, ArmPose::Empty)
@@ -693,7 +693,7 @@ pub fn setup(
} else {
ArmPose::Empty
};
let mut h = if m.extra.main_hand_left {
let mut h = if m.main_arm_left() {
Hum::new(off, pose)
} else {
Hum::new(pose, off)
+5
View File
@@ -621,8 +621,10 @@ impl BowAttack {
}
}
}
/// `LivingEntity.isHolding(Items.BOW)`: the bow can sit in either hand.
fn holding_bow(m: &Mob) -> bool {
m.extra.main_hand.as_deref() == Some("minecraft:bow")
|| m.extra.off_hand.as_deref() == Some("minecraft:bow")
}
impl Goal for BowAttack {
goal_clone!();
@@ -715,7 +717,10 @@ impl Goal for BowAttack {
} else {
self.attack_time -= 1;
if self.attack_time <= 0 && self.see_time >= -60 {
// mob.startUsingItem(ProjectileUtil.getWeaponHoldingHand):
// the off hand when the main one doesn't hold the bow.
m.extra.using_item = 1;
m.extra.using_off_hand = m.extra.main_hand.as_deref() != Some("minecraft:bow");
}
}
}