Merge pull request #11268 from DUzzL/fix/11242-hanging-entity-data

Apply item custom data and entity data to placed frames, paintings and armor stands
This commit is contained in:
rdbt authored and GitHub committed 2026-11-06 21:20:07 +00:00
commit 3e5b2ef194
4 files changed
+117 -23

No files matched your search

@@ -110,6 +110,25 @@ pub fn spawn_egg_entity_data<'a>(
Some(data)
}
/// `EntityType.appendDefaultStackConfig` for a hanging entity or armor stand an
/// item places: `Entity.applyImplicitComponents` stores the item's
/// `minecraft:custom_data` as the entity's `data`, then `updateCustomEntityTag`
/// (`TypedEntityData.loadInto`) merges the item's `minecraft:entity_data` over
/// the entity's saved tag `own` when its type is `kind`.
pub fn stack_configured_entity(stack: &crate::items::ItemStack, kind: &str, own: &Tag) -> Tag {
let mut tag = own.clone();
if let (Tag::Compound(map), Some(custom)) = (
&mut tag,
stack.data().and_then(|d| d.other("minecraft:custom_data")),
) {
map.insert("data".into(), custom.clone());
}
match spawn_egg_entity_data(stack, kind, false) {
Some(data) => crate::data_cmd::merged(&tag, data),
None => tag,
}
}
/// `Spawner.getSpawnEntityDisplayName` (`SpawnData.entity.id` of the item's
/// data, which `ItemStack.addDetailsToTooltip` reads for spawners and trial
/// spawners alike): the entity type a spawner item spawns.
@@ -593,6 +612,34 @@ mod tests {
);
}
#[test]
fn a_placed_entity_takes_the_items_custom_data_and_matching_entity_data() {
let item = |components: &str| {
let components = snbt(components);
let (damage, extra) = crate::item_data::decode_components(Some(&components));
crate::items::ItemStack {
damage,
extra,
..crate::items::ItemStack::new("minecraft:armor_stand", 1)
}
};
let own = snbt("{CustomName:\"Named\",ShowArms:0b}");
let stack = item(
"{\"minecraft:custom_data\":{k:1b},\"minecraft:entity_data\":{id:\"minecraft:armor_stand\",ShowArms:1b}}",
);
let tag = stack_configured_entity(&stack, "minecraft:armor_stand", &own);
assert_eq!(tag.get("ShowArms"), Some(&Tag::Byte(1)));
assert_eq!(tag.get("CustomName"), Some(&Tag::String("Named".into())));
assert_eq!(
tag.get("data").and_then(|d| d.get("k")),
Some(&Tag::Byte(1))
);
// Data of another type is not loaded; the custom data still is.
let tag = stack_configured_entity(&stack, "minecraft:item_frame", &own);
assert_eq!(tag.get("ShowArms"), Some(&Tag::Byte(0)));
assert!(tag.get("data").is_some());
}
#[test]
fn a_spawn_egg_data_needs_its_own_type_and_an_op_for_an_op_only_one() {
let egg = |entity_data: &str| {
@@ -522,6 +522,27 @@ impl Game<'_> {
let drop = self.drop_offset_from(0.5, 1.975, (pos[0], pos[1], pos[2]), true);
stand.position.y = f64::from(pos[1]) + drop;
stand.previous = stand.position;
// `EntityType.createDefaultStackConfig`: the item's `custom_data`, then its
// `entity_data` merged over the stand; `ArmorStandItem.useOn` then snaps it
// to the placement yaw with pitch 0.
let own = horizon_client::entity_nbt::armor_stand_to_nbt(&stand, [0; 4], None);
let mut raw = horizon_client::block_entity_item::stack_configured_entity(
stack,
"minecraft:armor_stand",
&own,
);
if let Some(mut loaded) = horizon_client::entity_nbt::armor_stand_from_nbt(&raw, stand.id) {
loaded.first_tick = stand.first_tick;
loaded.yaw = stand.yaw;
loaded.previous_yaw = stand.previous_yaw;
loaded.pitch = 0.0;
loaded.previous = loaded.position;
stand = loaded;
}
if let horizon_world::nbt::Tag::Compound(map) = &mut raw {
map.remove(&"UUID".into());
}
self.persist.raw.insert(stand.id, raw);
let foot = stand.position;
// EntityType.create's spawn yaw, `level.getRandom().nextFloat() * 360`, before
// ArmorStandItem.useOn turns the stand to face the player.
+23 -23
View File
@@ -163,21 +163,6 @@ impl Game<'_> {
"minecraft:glow_item_frame" => true,
_ => return false,
};
let entity_data = stack.extra.as_ref().and_then(|extra| {
extra.other.iter().find_map(|(k, v)| {
(k == "minecraft:entity_data" || k == "entity_data").then_some(v)
})
});
let invisible = entity_data
.and_then(|v| horizon_client::save::number(v.get("Invisible")))
.unwrap_or(0)
!= 0;
// `applyComponentsFromItemStack` sets `CUSTOM_NAME`, then `entity_data`
// loads (`updateCustomEntityTag`): a missing `CustomName` clears it.
let custom_name = match entity_data {
Some(data) => data.get("CustomName").cloned(),
None => stack.data().and_then(|d| d.custom_name.clone()),
};
let pos: [i32; 3] = std::array::from_fn(|i| hit.pos[i] + hit.face[i]);
// `ItemFrameItem.mayPlace`: `Level.isInsideBuildHeight(blockPos)`, so a frame is not
// placed on the top face of the highest block or under the lowest one.
@@ -189,7 +174,7 @@ impl Game<'_> {
self.use_failed = true;
return true;
}
let frame = ItemFrame {
let mut frame = ItemFrame {
id: self.next_entity + 1,
pos,
facing: facing_of(hit.face),
@@ -198,20 +183,29 @@ impl Game<'_> {
rotation: 0,
drop_chance: 1.0,
fixed: false,
invisible,
invisible: false,
check_interval: 0,
custom_name,
// `applyComponentsFromItemStack` sets `CUSTOM_NAME`.
custom_name: stack.data().and_then(|d| d.custom_name.clone()),
};
// `EntityType.createDefaultStackConfig`: the item's `custom_data`, then its
// `entity_data` merged over the frame (`TypedEntityData.loadInto`), before
// `survives` runs on the configured frame.
let kind = frame.kind();
let own = horizon_client::entity_nbt::item_frame_to_nbt(&frame, [0; 4], None);
let configured =
horizon_client::block_entity_item::stack_configured_entity(stack, kind, &own);
if let Some(mut loaded) =
horizon_client::entity_nbt::item_frame_from_nbt(&configured, frame.id)
{
loaded.glow = glow;
frame = loaded;
}
// HangingEntity.survives false: CONSUME, which ItemStack.useOn still counts as a use.
if !self.frame_survives(&frame) {
self.award_stat("used", &stack.item, 1);
return true;
}
let kind = if glow {
"minecraft:glow_item_frame"
} else {
"minecraft:item_frame"
};
if self.hanging_place_cancelled(kind, pos) {
return true;
}
@@ -223,6 +217,12 @@ impl Game<'_> {
let ctx = self.player_context(None);
self.post_game_event("minecraft:entity_place", center(&frame), ctx);
self.world.modified.insert((pos[0] >> 4, pos[2] >> 4));
// The unmodelled rest of the loaded data (`data`, `Tags`...) stays with the frame.
let mut raw = configured;
if let horizon_world::nbt::Tag::Compound(map) = &mut raw {
map.remove(&"UUID".into());
}
self.persist.raw.insert(frame.id, raw);
self.item_frames.push(frame);
if !self.creative {
self.consume_held();
@@ -238,9 +238,35 @@ impl Game<'_> {
return true;
}
}
// `updateCustomEntityTag`: the item's `custom_data` and its `entity_data`
// merged over the painting, so a `variant` there replaces the pick (and
// has to survive, else the use is CONSUME).
let own = horizon_client::entity_nbt::painting_to_nbt(&candidate, [0; 4], None);
let mut raw = horizon_client::block_entity_item::stack_configured_entity(
stack,
"minecraft:painting",
&own,
);
if let Some((_, _, variant)) = horizon_client::entity_nbt::painting_from_nbt(&raw)
&& *candidate.variant != *variant
&& let Some(v) = self
.painting_variants
.get(&horizon_world::datafix::dynamic::namespaced(&variant))
.cloned()
{
(candidate.variant, candidate.width, candidate.height) = (v.id, v.width, v.height);
if !self.painting_survives(&candidate) {
self.award_stat("used", &stack.item, 1);
return true;
}
}
if let horizon_world::nbt::Tag::Compound(map) = &mut raw {
map.remove(&"UUID".into());
}
if self.hanging_place_cancelled("minecraft:painting", pos) {
return true;
}
self.persist.raw.insert(candidate.id, raw);
let (lo, hi) = candidate.bounds();
let at = [
(lo[0] + hi[0]) / 2.0,