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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QT61YPiPzVNysmVHsH
This commit is contained in:
Claude committed 2026-10-31 20:39:58 +00:00
1 parent caaab6c1a7
commit 8a6eaac999
3 files changed
+72 -37

No files matched your search

@@ -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;
+31 -30
View File
@@ -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);
@@ -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]));