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:
J1zeeeandNemotron committed 2026-11-08 23:18:05 +03:00
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,
+47 -1
View File
@@ -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());
}
}
+3
View File
@@ -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(_)
+4 -1
View File
@@ -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;
}
+5 -3
View File
@@ -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();
}
}
}
+6 -1
View File
@@ -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) => {