control: horizon-ctl summon spawns server-ticking mobs (fixes #9207)

When running single player on an integrated server, `horizon-ctl summon`
spawned mobs directly into the client window's entity list rather than
delegating to the server thread. Because the client window does not tick
mobs (running `tick_remote` instead of `tick_world`), summoned mobs
never executed AI goals, took no environmental damage, and were not
visible to server queries.

Delegate `summon` to the integrated server in the owner player's level
when `self.integrated` is active, matching other control world commands
and allowing the server to tick the mob and broadcast it via standard
entity tracking.

Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
This commit is contained in:
Axxio committed 2026-11-04 16:41:46 +01:00
1 parent 6c5cd897fc
commit 9f346f9794
2 files changed
+29 -10

No files matched your search

+18 -1
View File
@@ -851,7 +851,24 @@ impl Game<'_> {
let baby = words.contains(&"baby");
let nums: Vec<f64> = words[2..].iter().filter_map(|w| w.parse().ok()).collect();
let at = (nums.len() >= 3).then(|| [nums[0], nums[1], nums[2]]);
let id = self.summon(arg(1)?, at, baby)?;
let kind = arg(1)?.to_owned();
// Single player on its server thread: spawn in the owner's level
// so the mob enters the server tick loop and is tracked to clients.
if let Some(server) = &self.integrated {
let at = match at {
Some(a) => a,
None => self.default_summon_position()?,
};
let id = server
.call(move |g| {
g.in_owner_level(|g| {
g.summon(&kind, Some(at), baby).map_err(|e| e.to_string())
})
})
.ok_or(super::integrated::NO_ANSWER)??;
return Ok(Some(json!({ "ok": true, "id": id })));
}
let id = self.summon(&kind, at, baby)?;
Ok(Some(json!({ "ok": true, "id": id })))
}
"mobs" => {
+11 -9
View File
@@ -691,19 +691,21 @@ impl Game<'_> {
self.native_living_conversion_post(from.id, &from_kind, id, &transformed_kind);
Some(id)
}
/// Default position for a summon command (3 blocks in front of the player).
pub(super) fn default_summon_position(&self) -> Result<[f64; 3]> {
let p = self.player.as_ref().ok_or("no player yet")?;
let d = view_vector(self.yaw, 0.0);
Ok([
p.position.x + d[0] * 3.0,
p.position.y,
p.position.z + d[2] * 3.0,
])
}
/// The `summon` command: spawn a mob in front of the player or at a position.
pub(super) fn summon(&mut self, kind: &str, at: Option<[f64; 3]>, baby: bool) -> Result<u32> {
let at = match at {
Some(a) => a,
None => {
let p = self.player.as_ref().ok_or("no player yet")?;
let d = view_vector(self.yaw, 0.0);
[
p.position.x + d[0] * 3.0,
p.position.y,
p.position.z + d[2] * 3.0,
]
}
None => self.default_summon_position()?,
};
let id = self
.spawn_mob_spawned(kind, at)