mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 02:29:50 -10:00
Fix ender chest and end portal block entities missing from /setblock
ender_chest and end_portal are EntityBlocks in vanilla 26.3 but Horizon's BlockEntity::for_block had no handler for them: they fell through to the container_size() fallback, which returns None for both, so no block entity was created and /data get block answered "not a block entity". Add explicit handlers: ender_chest -> Chest with 27 slots (matching container_size), end_portal -> new EndPortal variant with no data. Register EndPortal in fits/id/contents/slots/slots_mut so it behaves like the other zero-slot entities (DaylightDetector, Bell). Also fix two traps found in review: - close_ender_chest removed the block entity entirely; it now copies the items back into self.ender_items and leaves the entity in place, so a /data get block after opening/closing still reports the entity. - Containers::view excluded Beehive/Campfire/Brushable from being treated as containers (so droppers don't insert into them); EndPortal needed the same exclusion. End-to-end verified: /setblock ender_chest/end_portal then /data get block returns the correct id, and accessing the ender chest's container leaves its block entity intact. Co-Authored-By: Nemotron <[email protected]>
This commit is contained in:
1 parent
5f7cadfff0
commit
3233aaabe9
6 files changed
+67
-6
No files matched your search
@@ -80,6 +80,8 @@ pub enum BlockEntity {
|
||||
Skull(Skull),
|
||||
/// `TheEndGatewayBlockEntity`: its age and configured destination.
|
||||
EndGateway(EndGateway),
|
||||
/// `TheEndPortalBlockEntity`: no data, only identity.
|
||||
EndPortal,
|
||||
/// `DaylightDetectorBlockEntity`: no data, only the ticker
|
||||
/// (`DaylightDetectorBlock.tickEntity`).
|
||||
DaylightDetector,
|
||||
|
||||
@@ -316,7 +316,10 @@ impl SkullSkin {
|
||||
/// The item slots of the container `name`'s block carries, if any.
|
||||
fn container_size(name: &str) -> Option<usize> {
|
||||
match name {
|
||||
"minecraft:chest" | "minecraft:trapped_chest" | "minecraft:barrel" => Some(27),
|
||||
"minecraft:chest"
|
||||
| "minecraft:trapped_chest"
|
||||
| "minecraft:barrel"
|
||||
| "minecraft:ender_chest" => Some(27),
|
||||
n if n.ends_with("copper_chest") => Some(27),
|
||||
"minecraft:hopper" => Some(HOPPER_SLOTS),
|
||||
"minecraft:dispenser" | "minecraft:dropper" => Some(9),
|
||||
@@ -447,6 +450,14 @@ impl BlockEntity {
|
||||
if name == "minecraft:daylight_detector" {
|
||||
return Some(Self::DaylightDetector);
|
||||
}
|
||||
if name == "minecraft:ender_chest" {
|
||||
// EnderChestBlock.newBlockEntity: 27 slots, player-specific inventory.
|
||||
return Some(Self::Chest(vec![None; 27]));
|
||||
}
|
||||
if name == "minecraft:end_portal" {
|
||||
// EndPortalBlock.newBlockEntity: no data, only identity.
|
||||
return Some(Self::EndPortal);
|
||||
}
|
||||
if name.ends_with("copper_golem_statue") {
|
||||
return Some(Self::CopperGolemStatue(Default::default()));
|
||||
}
|
||||
@@ -540,6 +551,7 @@ impl BlockEntity {
|
||||
Self::EndGateway(_) => name == "minecraft:end_gateway",
|
||||
Self::DaylightDetector => name == "minecraft:daylight_detector",
|
||||
Self::Bell => name == "minecraft:bell",
|
||||
Self::EndPortal => name == "minecraft:end_portal",
|
||||
Self::StructureBlock(_) => name == "minecraft:structure_block",
|
||||
Self::TestBlock(_) => name == "minecraft:test_block",
|
||||
Self::TestInstanceBlock(_) => name == "minecraft:test_instance_block",
|
||||
@@ -579,6 +591,7 @@ impl BlockEntity {
|
||||
Self::EndGateway(_) => "minecraft:end_gateway",
|
||||
Self::DaylightDetector => "minecraft:daylight_detector",
|
||||
Self::Bell => "minecraft:bell",
|
||||
Self::EndPortal => "minecraft:end_portal",
|
||||
Self::Brushable(_) => "minecraft:brushable_block",
|
||||
Self::Campfire(_) => "minecraft:campfire",
|
||||
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
|
||||
@@ -614,6 +627,7 @@ impl BlockEntity {
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::EndPortal
|
||||
| Self::Brushable(_) => Vec::new(),
|
||||
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
|
||||
Self::Lectern(l) => l.book.iter().cloned().collect(),
|
||||
@@ -645,6 +659,7 @@ impl BlockEntity {
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::EndPortal
|
||||
| Self::Brushable(_) => &[],
|
||||
Self::Jukebox(j) => &j.items,
|
||||
Self::Crafter(c) => &c.items,
|
||||
@@ -681,6 +696,7 @@ impl BlockEntity {
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Bell
|
||||
| Self::EndPortal
|
||||
| Self::Brushable(_) => &mut [],
|
||||
Self::Jukebox(j) => &mut j.items,
|
||||
Self::Crafter(c) => &mut c.items,
|
||||
@@ -933,4 +949,34 @@ mod tests {
|
||||
assert!((250..450).contains(&paid), "{paid}");
|
||||
assert!((0..100).all(|_| (1..=2).contains(&create_experience(1, 1.7, &mut random))));
|
||||
}
|
||||
|
||||
/// An ender chest placed with /setblock gets its block entity (`EnderChestBlock.newBlockEntity`),
|
||||
/// so `/data get block` answers with the entity and the chunk saves it.
|
||||
#[test]
|
||||
fn an_ender_chest_starts_as_a_block_entity() {
|
||||
let entity = BlockEntity::for_block("minecraft:ender_chest").expect("an ender chest");
|
||||
// `Chest` variant with 27 slots; `id()` reports the chest kind, block name comes from NBT.
|
||||
assert_eq!(entity.id(), "minecraft:chest");
|
||||
assert_eq!(entity.slots().len(), 27);
|
||||
assert!(entity.slots()[0].is_none());
|
||||
assert!(matches!(entity, BlockEntity::Chest(_)));
|
||||
// `fits`: an ender chest entity fits an ender chest block.
|
||||
assert!(entity.fits("minecraft:ender_chest"));
|
||||
// A stone block is not an ender chest.
|
||||
assert!(BlockEntity::for_block("minecraft:stone").is_none());
|
||||
}
|
||||
|
||||
/// An end portal placed with /setblock gets its block entity (`EndPortalBlock.newBlockEntity`),
|
||||
/// so `/data get block` answers with the entity and the chunk saves it.
|
||||
#[test]
|
||||
fn an_end_portal_starts_as_a_block_entity() {
|
||||
let entity = BlockEntity::for_block("minecraft:end_portal").expect("an end portal");
|
||||
assert_eq!(entity.id(), "minecraft:end_portal");
|
||||
assert_eq!(entity.slots().len(), 0);
|
||||
assert!(matches!(entity, BlockEntity::EndPortal));
|
||||
// `fits`: an end portal entity fits an end portal block.
|
||||
assert!(entity.fits("minecraft:end_portal"));
|
||||
// A stone block is not an end portal.
|
||||
assert!(BlockEntity::for_block("minecraft:stone").is_none());
|
||||
}
|
||||
}
|
||||
@@ -8576,6 +8576,7 @@ impl Game<'_> {
|
||||
| BlockEntity::Skull(_)
|
||||
| BlockEntity::DaylightDetector
|
||||
| BlockEntity::Bell
|
||||
| BlockEntity::EndPortal
|
||||
| BlockEntity::Campfire(_)
|
||||
| BlockEntity::ChiseledBookshelf(_)
|
||||
| BlockEntity::Brushable(_)
|
||||
@@ -8640,6 +8641,7 @@ impl Game<'_> {
|
||||
| BlockEntity::Skull(_)
|
||||
| BlockEntity::DaylightDetector
|
||||
| BlockEntity::Bell
|
||||
| BlockEntity::EndPortal
|
||||
| BlockEntity::Campfire(_)
|
||||
| BlockEntity::ChiseledBookshelf(_)
|
||||
| BlockEntity::Brushable(_)
|
||||
@@ -8775,6 +8777,7 @@ impl Game<'_> {
|
||||
| BlockEntity::Skull(_)
|
||||
| BlockEntity::DaylightDetector
|
||||
| BlockEntity::Bell
|
||||
| BlockEntity::EndPortal
|
||||
| BlockEntity::Campfire(_)
|
||||
| BlockEntity::ChiseledBookshelf(_)
|
||||
| BlockEntity::Brushable(_)
|
||||
|
||||
@@ -224,7 +224,10 @@ impl Containers<'_> {
|
||||
// them and a hopper leaves them alone.
|
||||
if matches!(
|
||||
entity,
|
||||
BlockEntity::Beehive(_) | BlockEntity::Campfire(_) | BlockEntity::Brushable(_)
|
||||
BlockEntity::Beehive(_)
|
||||
| BlockEntity::Campfire(_)
|
||||
| BlockEntity::Brushable(_)
|
||||
| BlockEntity::EndPortal
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -147,13 +147,15 @@ impl Game<'_> {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// Closing it copies the items back and leaves no block-entity contents.
|
||||
/// Closing it copies the items back and keeps the block entity, as vanilla's
|
||||
/// `EnderChestBlockEntity` persists across open/close (`close_ender_chest`
|
||||
/// must not remove it, or `/data get block` loses it after a use).
|
||||
pub(super) fn close_ender_chest(&mut self) {
|
||||
let Some(key) = self.ender_open.take() else {
|
||||
return;
|
||||
};
|
||||
if let Some(BlockEntity::Chest(items)) = self.world.block_entities.remove(&key) {
|
||||
self.ender_items = items;
|
||||
if let Some(BlockEntity::Chest(items)) = self.world.block_entities.get_mut(&key) {
|
||||
self.ender_items = items.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +628,10 @@ fn block_entity_id(e: &crate::containers::BlockEntity, block: &str) -> String {
|
||||
match (e, block) {
|
||||
(
|
||||
crate::containers::BlockEntity::Chest(_),
|
||||
"minecraft:trapped_chest" | "minecraft:barrel" | "minecraft:dropper",
|
||||
"minecraft:trapped_chest"
|
||||
| "minecraft:barrel"
|
||||
| "minecraft:dropper"
|
||||
| "minecraft:ender_chest",
|
||||
) => block.to_owned(),
|
||||
(crate::containers::BlockEntity::Chest(_), b) if b.ends_with("shulker_box") => {
|
||||
"minecraft:shulker_box".to_owned()
|
||||
@@ -803,6 +806,7 @@ pub fn encode_block_entity(
|
||||
// Encoded above.
|
||||
BlockEntity::DaylightDetector
|
||||
| BlockEntity::Bell
|
||||
| BlockEntity::EndPortal
|
||||
| BlockEntity::CopperGolemStatue(_)
|
||||
| BlockEntity::Mod(_) => {}
|
||||
// PotentSulfurBlockEntity.saveAdditional.
|
||||
@@ -1134,6 +1138,7 @@ pub fn decode_block_entity(tag: &Tag) -> Option<((i32, i32, i32), crate::contain
|
||||
| BlockEntity::Skull(_)
|
||||
| BlockEntity::DaylightDetector
|
||||
| BlockEntity::Bell
|
||||
| BlockEntity::EndPortal
|
||||
| BlockEntity::Mod(_) => {}
|
||||
// `BlockEntity.loadAdditional`: the components the block entity keeps.
|
||||
BlockEntity::CopperGolemStatue(components) => {
|
||||
|
||||
Reference in new issue
Block a user