diff --git a/crates/horizon-client/src/game/commands.rs b/crates/horizon-client/src/game/commands.rs index ea628917e..d33d19726 100644 --- a/crates/horizon-client/src/game/commands.rs +++ b/crates/horizon-client/src/game/commands.rs @@ -1851,7 +1851,7 @@ impl Game<'_> { if self.ui.difficulty == 0 && t.category == "monster" { return Err(err("commands.summon.failed.peaceful", &[])); } - self.spawn_mob(&short, pos, true); + self.spawn_mob_spawned(&short, pos); name = Text::translate(&format!("entity.minecraft.{short}"), &[]); } else { match short.as_str() { diff --git a/crates/horizon-client/src/game/creatures.rs b/crates/horizon-client/src/game/creatures.rs index f7d4828f1..16b4cbc46 100644 --- a/crates/horizon-client/src/game/creatures.rs +++ b/crates/horizon-client/src/game/creatures.rs @@ -277,6 +277,19 @@ impl Game<'_> { pub(super) fn spawn_mob(&mut self, kind: &str, at: [f64; 3], persistent: bool) -> Option { self.spawn_mob_for(kind, at, persistent, false) } + /// `SummonCommand` and `SpawnEggItem`: vanilla spawns the mob, but it + /// never calls `Mob.setPersistenceRequired` for it, so the mob is not + /// persistent: the despawn rule holds it like any other mob, and only + /// its NBT can make it persistent. + pub(super) fn spawn_mob_spawned(&mut self, kind: &str, at: [f64; 3]) -> Option { + // The spawn is not a natural one (no jockeys, the patrol leader roll + // stays), but the mob is not persistent. + let id = self.spawn_mob_for(kind, at, true, false)?; + if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) { + m.persistent = false; + } + Some(id) + } /// `spawn_mob` for a patrol or raid (`EntitySpawnReason.PATROL` / `EVENT`), /// which never rolls a patrol leader. pub(super) fn spawn_mob_for( @@ -334,7 +347,7 @@ impl Game<'_> { } }; let id = self - .spawn_mob(kind, at, true) + .spawn_mob_spawned(kind, at) .ok_or_else(|| format!("unknown or unsupported mob {kind}"))?; if baby && let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) { m.extra.age = if m.species() == Species::Sniffer { @@ -4075,7 +4088,7 @@ impl Game<'_> { let (species, variant, color) = (parent.species(), parent.extra.variant, parent.extra.color); let target = self.mob_entity(&self.mobs[index]); - let Some(id) = self.spawn_mob(&kind, at, true) else { + let Some(id) = self.spawn_mob_spawned(&kind, at) else { return false; }; if let Some(child) = self.mobs.iter_mut().find(|m| m.id == id) { @@ -4121,7 +4134,7 @@ impl Game<'_> { f64::from(p[1]), f64::from(p[2]) + 0.5, ]; - if self.spawn_mob(&kind, at, true).is_none() { + if self.spawn_mob_spawned(&kind, at).is_none() { return Ok(false); } self.swing_arm(); diff --git a/tools/command_exec_corpus.py b/tools/command_exec_corpus.py index 9a86c44f7..fd527fc76 100644 --- a/tools/command_exec_corpus.py +++ b/tools/command_exec_corpus.py @@ -327,6 +327,11 @@ def entities(): cmd('/data get entity @e[tag=attr,limit=1] Health'), cmd('/attribute @e[tag=attr,limit=1] minecraft:attack_damage base get'), cmd('/kill @e[tag=attr]'), + # A summoned mob is not persistent: vanilla never calls + # `Mob.setPersistenceRequired` for a mob it spawns (#2787). + cmd('/summon minecraft:zombie 4.5 210 12.5'), + cmd('/execute if entity @e[type=minecraft:zombie,nbt={PersistenceRequired:1b}]'), + cmd('/kill @e[type=minecraft:zombie]'), # An End crystal is an entity of its own: a type selector finds it, # reads it and kills it (#1531). cmd('/summon minecraft:end_crystal 4.5 210 8.5'),