A server's parrot or slime sent as a baby stays grown; fix the baby flag test

AgeableMob.isBaby is canBeABaby() && getAge() < 0, so a vanilla client
ignores DATA_BABY_ID on a frog, parrot, slime, magma cube, camel husk or
zombie nautilus. The client now does too (drops::can_be_baby). The test
gives each type a model, which MobTypes::load needs, and checks a parrot.
baby_index reuses doors::is_zombie.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
some-du6eandClaude Opus 5.5 committed 2026-11-01 12:54:20 -06:00
1 parent 868069dea7
commit 02be02992c
2 files changed
+31 -10

No files matched your search

+5 -3
View File
@@ -250,10 +250,12 @@ pub(super) fn baby_index(species: Species) -> Option<u8> {
use Species as S;
match species {
S::Piglin => Some(17),
S::Zombie | S::Husk | S::Drowned | S::ZombieVillager | S::ZombifiedPiglin | S::Zoglin => {
s if horizon_client::mobs::finalize::ageable(s)
|| horizon_client::mobs::doors::is_zombie(s)
|| s == S::Zoglin =>
{
Some(16)
}
s if horizon_client::mobs::finalize::ageable(s) => Some(16),
_ => None,
}
}
@@ -473,7 +475,7 @@ fn mob_meta(
meta.push((15, serializer::BYTE, Value::Byte(flags)));
}
defaults.push((15, serializer::BYTE, Value::Byte(0)));
// AgeableMob.DATA_BABY_ID (and Zombie's, Piglin's).
// AgeableMob.DATA_BABY_ID (and Zombie's, Zoglin's, Piglin's).
if let Some(index) = baby_index(m.species()) {
if m.is_baby() {
meta.push((index, serializer::BOOLEAN, Value::Bool(true)));
@@ -5,8 +5,8 @@
use super::Game;
use horizon_client::mobs::render::{PROFESSIONS, VILLAGER_TYPES};
use horizon_client::mobs::{
Mob, Species, Who, armadillo, bat, bee, camel, creaking, equine, finalize, illusioner, panda,
phantom, sniffer, sync, tame, zombie_villager,
Mob, Species, Who, armadillo, bat, bee, camel, creaking, drops, equine, finalize, illusioner,
panda, phantom, sniffer, sync, tame, zombie_villager,
};
use horizon_client::particles::{ParticleOptions, ParticleType};
use horizon_protocol::entity_data::{Particle, ParticleData, Value};
@@ -82,7 +82,15 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
m.extra.left_handed = flags & 2 != 0;
m.ai.aggressive = flags & 4 != 0;
}
(i, Value::Bool(b)) if Some(i) == baby => m.extra.age = if *b { -1 } else { 0 },
// AgeableMob.isBaby: `canBeABaby` too, so a parrot or a slime
// sent as one is still drawn grown.
(i, Value::Bool(b)) if Some(i) == baby => {
m.extra.age = if *b && drops::can_be_baby(species) {
-1
} else {
0
};
}
// AgeableMob.AGE_LOCKED, the entry after DATA_BABY_ID (not
// Zombie's or Piglin's).
(17, Value::Bool(locked)) if baby == Some(16) && finalize::ageable(species) => {
@@ -630,20 +638,26 @@ mod tests {
}
/// #6133: a server's baby happy ghast (a hatched dried ghast) and baby
/// nautilus come with AgeableMob.DATA_BABY_ID (16) like any other animal.
/// nautilus come with AgeableMob.DATA_BABY_ID (16) like any other animal;
/// a parrot never is one (`Parrot.canBeABaby`).
#[test]
fn a_happy_ghast_and_a_nautilus_take_their_babyhood() {
use horizon_client::mobs::MobTypes;
use std::{fs, sync::Arc};
let root = std::env::temp_dir().join(format!("horizon-baby-flag-{}", std::process::id()));
fs::create_dir(&root).unwrap();
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("entity-types.json"),
r#"{"minecraft:happy_ghast": {}, "minecraft:nautilus": {}}"#,
r#"{"minecraft:happy_ghast": {}, "minecraft:nautilus": {}, "minecraft:parrot": {}}"#,
)
.unwrap();
fs::write(
root.join("entity-models.json"),
r#"{"minecraft:happy_ghast#main": {}, "minecraft:nautilus#main": {},
"minecraft:parrot#main": {}}"#,
)
.unwrap();
fs::write(root.join("entity-models.json"), "{}").unwrap();
let mut types = MobTypes::load(&root).unwrap();
fs::remove_dir_all(root).unwrap();
for kind in ["minecraft:happy_ghast", "minecraft:nautilus"] {
@@ -656,6 +670,11 @@ mod tests {
apply(&mut m, &[(16, s::BOOLEAN, Value::Bool(false))]);
assert!(!m.is_baby(), "{kind}");
}
let t = Arc::new(types.types.remove("minecraft:parrot").unwrap());
let mut parrot = Mob::new(1, t, horizon_core::geometry::Vec3::ZERO, 0.0);
client_defaults(&mut parrot);
apply(&mut parrot, &[(16, s::BOOLEAN, Value::Bool(true))]);
assert!(!parrot.is_baby());
}
#[test]