From f8f2f904bc4bb4018b61c51ac9edb39b278e750b Mon Sep 17 00:00:00 2001 From: levzzz <86072227+levzzz5154@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:21:08 +0500 Subject: [PATCH] A summoned or egg-spawned mob is not persistent Vanilla never calls `Mob.setPersistenceRequired` for a mob it spawns: the spawn egg's offspring, the spawn egg on a block and `/summon` all leave the mob to the despawn rule, and only the NBT can make it persistent. Horizon marked every one of them persistent, so they never despawn and they are left out of the mob cap. The three paths now spawn the mob like before, without the persistence flag, and the command execution corpus checks it against vanilla: a summoned zombie is not selected by `nbt={PersistenceRequired:1b}`. Fixes #2787 Co-Authored-By: minimax-m3.1-flash (Claude Code) --- crates/horizon-client/src/game/commands.rs | 2 +- crates/horizon-client/src/game/creatures.rs | 19 ++++++++++++++++--- tools/command_exec_corpus.py | 5 +++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/horizon-client/src/game/commands.rs b/crates/horizon-client/src/game/commands.rs index 43cdfa51c..b0d1ce316 100644 --- a/crates/horizon-client/src/game/commands.rs +++ b/crates/horizon-client/src/game/commands.rs @@ -1818,7 +1818,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 119f67fa7..ed3842cc0 100644 --- a/crates/horizon-client/src/game/creatures.rs +++ b/crates/horizon-client/src/game/creatures.rs @@ -265,6 +265,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( @@ -322,7 +335,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 { @@ -3699,7 +3712,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) { @@ -3745,7 +3758,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'),