mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user