From 8a6eaac9997acc1099cbb0c78d442bb3fbeb7173 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 20:39:58 +0000 Subject: [PATCH] native_summon: return experience orbs' ids, and place passengers without a Pos Follow-up to #5408. A summoned orb spawned but native_summon returned None, since no command Target lookup covers orbs: return the stack's first spawned id. Passengers without Pos were not placed and got lost: share /summon's recursive place helper (place_summoned), which also supplies the Fire and Rotation defaults. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XnU6QT61YPiPzVNysmVHsH --- crates/horizon-client/src/game/commands.rs | 1 + .../horizon-client/src/game/commands/data.rs | 61 ++++++++++--------- .../src/game/native_entities.rs | 47 +++++++++++--- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/crates/horizon-client/src/game/commands.rs b/crates/horizon-client/src/game/commands.rs index f326ee4225..36b69c307e 100644 --- a/crates/horizon-client/src/game/commands.rs +++ b/crates/horizon-client/src/game/commands.rs @@ -3,6 +3,7 @@ mod admin; pub(super) mod compute; mod data; +pub(super) use data::place_summoned; mod effects; pub(super) mod execute; pub(super) mod extra; diff --git a/crates/horizon-client/src/game/commands/data.rs b/crates/horizon-client/src/game/commands/data.rs index 88aa3db240..2a22b539c4 100644 --- a/crates/horizon-client/src/game/commands/data.rs +++ b/crates/horizon-client/src/game/commands/data.rs @@ -28,6 +28,36 @@ fn set_key(tag: &mut Tag, key: &str, value: Tag) { c.insert(JavaString::from(key), value); } } +/// `EntityType.loadEntityRecursive` snaps every entity of the stack, +/// passengers included, to the position (`/summon`, `native_summon`). +pub(in crate::game) fn place_summoned(tag: &mut Tag, pos: [f64; 3]) { + let p = pos.map(|v| Tag::Double(v.to_bits())); + set_key(tag, "Pos", Tag::List(p.to_vec())); + // Entity.load: an absent Fire reads as 0 (until the entity moves). + if tag.get("Fire").is_none() { + set_key(tag, "Fire", Tag::Short(0)); + } + if tag.get("Rotation").is_none() { + set_key( + tag, + "Rotation", + Tag::List(vec![ + Tag::Float(0.0f32.to_bits()), + Tag::Float(0.0f32.to_bits()), + ]), + ); + } + if let Tag::Compound(c) = tag + && let Some(Tag::List(riders)) = c.get_mut(&"Passengers".into()) + { + for r in riders { + if matches!(r, Tag::Compound(_)) { + place_summoned(r, pos); + } + } + } +} + impl Game<'_> { // ---- entity data --------------------------------------------------- @@ -762,36 +792,7 @@ impl Game<'_> { return Err(err("argument.nbt.expected.compound", &[])); } set_key(&mut tag, "id", Tag::String(kind.into())); - // EntityType.loadEntityRecursive snaps every entity of the stack, - // passengers included, to the position. - fn place(tag: &mut Tag, pos: [f64; 3]) { - let p = pos.map(|v| Tag::Double(v.to_bits())); - set_key(tag, "Pos", Tag::List(p.to_vec())); - // Entity.load: an absent Fire reads as 0 (until the entity moves). - if tag.get("Fire").is_none() { - set_key(tag, "Fire", Tag::Short(0)); - } - if tag.get("Rotation").is_none() { - set_key( - tag, - "Rotation", - Tag::List(vec![ - Tag::Float(0.0f32.to_bits()), - Tag::Float(0.0f32.to_bits()), - ]), - ); - } - if let Tag::Compound(c) = tag - && let Some(Tag::List(riders)) = c.get_mut(&"Passengers".into()) - { - for r in riders { - if matches!(r, Tag::Compound(_)) { - place(r, pos); - } - } - } - } - place(&mut tag, pos); + place_summoned(&mut tag, pos); let from = self.mobs.len(); let canceled = self.persist.joins_canceled; let target = self.spawn_entity_from_nbt(tag); diff --git a/crates/horizon-client/src/game/native_entities.rs b/crates/horizon-client/src/game/native_entities.rs index f11c6f56a4..7eb6566fe6 100644 --- a/crates/horizon-client/src/game/native_entities.rs +++ b/crates/horizon-client/src/game/native_entities.rs @@ -5,7 +5,10 @@ //! `item_display` showing an item model from the mod's assets. They act where //! the world is simulated, in the level in place, which each names by its //! `dimension` (`minecraft:overworld`) as `native_set_block` does. -use super::{Game, commands::Target}; +use super::{ + Game, + commands::{Target, place_summoned}, +}; use horizon_core::geometry::Vec3; use horizon_world::nbt::{Compound, Tag}; @@ -134,14 +137,19 @@ impl Game<'_> { } let mut tag = nbt; tag.insert("id".into(), Tag::String(kind.into())); - tag.insert( - "Pos".into(), - Tag::List(pos.map(|v| Tag::Double(v.to_bits())).to_vec()), - ); + // As `/summon` does: every entity of the stack, passengers included, + // stands at `pos`. + let mut tag = Tag::Compound(tag); + place_summoned(&mut tag, pos); let from = self.mobs.len(); - let target = self.spawn_entity_from_nbt(Tag::Compound(tag)); + let first = self.next_entity + 1; + let target = self.spawn_entity_from_nbt(tag); self.native_mobs_joined(from, false); - target.and_then(target_id) + // The root is the first entity spawned, also for kinds no command + // target stands for (experience orbs). + target + .and_then(target_id) + .or((self.next_entity >= first).then_some(first)) } /// Moves entity `id`, one Horizon keeps without simulating (a display, a /// marker, an interaction), to `pos` facing `yaw` and `pitch` in degrees; @@ -281,6 +289,31 @@ mod tests { ); } + #[test] + fn a_passenger_without_a_position_stands_where_the_stack_is_summoned() { + let mut rider = Compound::new(); + rider.insert("id".into(), Tag::String("minecraft:pig".into())); + let mut root = Compound::new(); + root.insert("id".into(), Tag::String("minecraft:pig".into())); + root.insert("Passengers".into(), Tag::List(vec![Tag::Compound(rider)])); + let mut tag = Tag::Compound(root); + place_summoned(&mut tag, [1.5, 70.0, -2.5]); + let pos = Tag::List( + [1.5, 70.0, -2.5] + .map(|v: f64| Tag::Double(v.to_bits())) + .to_vec(), + ); + let Tag::Compound(root) = &tag else { panic!() }; + assert_eq!(root.get(&"Pos".into()), Some(&pos)); + let Some(Tag::List(riders)) = root.get(&"Passengers".into()) else { + panic!("no passengers"); + }; + let Tag::Compound(rider) = &riders[0] else { + panic!() + }; + assert_eq!(rider.get(&"Pos".into()), Some(&pos)); + } + #[test] fn summon_positions_are_summons() { assert!(position_ok([0.5, 64.0, 0.5]));