From 84ccc4d0de40313862178608427546881a88ac53 Mon Sep 17 00:00:00 2001 From: cev-api <215493617+cev-api@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:44:33 +1000 Subject: [PATCH 1/2] Customisable Shulker + Ender Chest Capacity --- .../PlayerEnderChestContainer.java.patch | 3 +- .../level/block/EnderChestBlock.java.patch | 24 ++++++++- .../entity/ShulkerBoxBlockEntity.java.patch | 50 +++++++++++++++++-- .../configuration/GlobalConfiguration.java | 19 +++++++ .../craftbukkit/inventory/CraftContainer.java | 24 ++++++++- 5 files changed, 113 insertions(+), 7 deletions(-) diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch index e41f6d341dc6..278109a95180 100644 --- a/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch @@ -6,6 +6,7 @@ private @Nullable EnderChestBlockEntity activeChest; - - public PlayerEnderChestContainer() { +- super(27); + // CraftBukkit start + private final Player owner; + @@ -20,7 +21,7 @@ + } + + public PlayerEnderChestContainer(Player owner) { - super(27); ++ super(io.papermc.paper.configuration.GlobalConfiguration.get().misc.enderChestSlotCount); // Paper - configurable ender chest slot count + this.owner = owner; + // CraftBukkit end } diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch index 30b8c15a5292..c99dfe642b4a 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch @@ -1,6 +1,13 @@ --- a/net/minecraft/world/level/block/EnderChestBlock.java +++ b/net/minecraft/world/level/block/EnderChestBlock.java -@@ -78,16 +_,17 @@ +@@ -13,6 +_,6 @@ + import net.minecraft.world.entity.monster.piglin.PiglinAi; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.inventory.ChestMenu; + import net.minecraft.world.inventory.PlayerEnderChestContainer; + import net.minecraft.world.item.context.BlockPlaceContext; + import net.minecraft.world.level.BlockGetter; +@@ -78,16 +_,28 @@ PlayerEnderChestContainer enderChestInventory = player.getEnderChestInventory(); if (enderChestInventory != null && level.getBlockEntity(pos) instanceof EnderChestBlockEntity enderChestBlockEntity) { BlockPos blockPos = pos.above(); @@ -19,7 +26,20 @@ + enderChestInventory.setActiveChest(enderChestBlockEntity); // Needs to happen before ChestMenu.threeRows as it is required for opening animations + if (level instanceof ServerLevel serverLevel && player.openMenu( + new SimpleMenuProvider( -+ (containerId, playerInventory, player1) -> ChestMenu.threeRows(containerId, playerInventory, enderChestInventory), CONTAINER_TITLE ++ (containerId, playerInventory, player1) -> { ++ final int rows = Math.clamp(enderChestInventory.getContainerSize() / 9, 1, 6); ++ // Paper start - configurable ender chest slot count ++ final net.minecraft.world.inventory.MenuType menuType = switch (rows) { ++ case 1 -> net.minecraft.world.inventory.MenuType.GENERIC_9x1; ++ case 2 -> net.minecraft.world.inventory.MenuType.GENERIC_9x2; ++ case 3 -> net.minecraft.world.inventory.MenuType.GENERIC_9x3; ++ case 4 -> net.minecraft.world.inventory.MenuType.GENERIC_9x4; ++ case 5 -> net.minecraft.world.inventory.MenuType.GENERIC_9x5; ++ default -> net.minecraft.world.inventory.MenuType.GENERIC_9x6; ++ }; ++ return new ChestMenu(menuType, containerId, playerInventory, enderChestInventory, rows); ++ // Paper end - configurable ender chest slot count ++ }, CONTAINER_TITLE + ) + ).isPresent()) { + // Paper end - Fix InventoryOpenEvent cancellation - moved up; diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch index 28174a42b059..65b25d913e9a 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch @@ -1,6 +1,17 @@ --- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -@@ -49,6 +_,42 @@ +@@ -40,15 +40,51 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + public static final int OPENING_TICK_LENGTH = 10; + public static final float MAX_LID_HEIGHT = 0.5F; + public static final float MAX_LID_ROTATION = 270.0F; +- private static final int[] SLOTS = IntStream.range(0, 27).toArray(); ++ private static final int[] SLOTS = IntStream.range(0, io.papermc.paper.configuration.GlobalConfiguration.get().misc.shulkerBoxSlotCount).toArray(); // Paper - configurable shulker box slot count + private static final Component DEFAULT_NAME = Component.translatable("container.shulkerBox"); +- private NonNullList itemStacks = NonNullList.withSize(27, ItemStack.EMPTY); ++ private NonNullList itemStacks = NonNullList.withSize(io.papermc.paper.configuration.GlobalConfiguration.get().misc.shulkerBoxSlotCount, ItemStack.EMPTY); // Paper - configurable shulker box slot count + public int openCount; + private ShulkerBoxBlockEntity.AnimationStatus animationStatus = ShulkerBoxBlockEntity.AnimationStatus.CLOSED; + private float progress; private float progressOld; private final @Nullable DyeColor color; @@ -43,7 +54,7 @@ public ShulkerBoxBlockEntity(@Nullable DyeColor color, BlockPos pos, BlockState blockState) { super(BlockEntityType.SHULKER_BOX, pos, blockState); this.color = color; -@@ -171,6 +_,7 @@ +@@ -171,6 +207,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl } this.openCount++; @@ -51,7 +62,7 @@ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount); if (this.openCount == 1) { this.level.gameEvent(user.getLivingEntity(), GameEvent.CONTAINER_OPEN, this.worldPosition); -@@ -184,6 +_,7 @@ +@@ -184,6 +221,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl public void stopOpen(ContainerUser user) { if (!this.remove && !user.getLivingEntity().isSpectator()) { this.openCount--; @@ -59,3 +70,36 @@ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount); if (this.openCount <= 0) { this.level.gameEvent(user.getLivingEntity(), GameEvent.CONTAINER_CLOSE, this.worldPosition); +@@ -231,7 +269,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + + @Override + public int[] getSlotsForFace(Direction side) { +- return SLOTS; ++ // Paper - configurable shulker box slot count ++ return this.getContainerSize() == SLOTS.length ? SLOTS : IntStream.range(0, this.getContainerSize()).toArray(); + } + + @Override +@@ -254,7 +292,19 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + + @Override + protected AbstractContainerMenu createMenu(int id, Inventory player) { +- return new ShulkerBoxMenu(id, player, this); ++ // Paper start - configurable shulker box slot count ++ final int rows = Math.clamp(this.getContainerSize() / 9, 1, 6); ++ if (rows == 3) { ++ return new ShulkerBoxMenu(id, player, this); ++ } ++ final net.minecraft.world.inventory.MenuType menuType = switch (rows) { ++ case 1 -> net.minecraft.world.inventory.MenuType.GENERIC_9x1; ++ case 2 -> net.minecraft.world.inventory.MenuType.GENERIC_9x2; ++ case 3 -> net.minecraft.world.inventory.MenuType.GENERIC_9x3; ++ case 4 -> net.minecraft.world.inventory.MenuType.GENERIC_9x4; ++ case 5 -> net.minecraft.world.inventory.MenuType.GENERIC_9x5; ++ default -> net.minecraft.world.inventory.MenuType.GENERIC_9x6; ++ }; ++ return new net.minecraft.world.inventory.ChestMenu(menuType, id, player, this, rows); ++ // Paper end - configurable shulker box slot count + } + + public boolean isClosed() { diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index e47f279b66b1..6c05e97964bb 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -350,6 +350,25 @@ private void postProcess() { public boolean enableNether = true; @Comment("Keeps Paper's fix for MC-159283 enabled. Disable to use vanilla End ring terrain.") public boolean fixFarEndTerrainGeneration = true; + @Comment("Number of ender chest slots. Supports 9-54 slots and is normalized to a multiple of 9.") + public int enderChestSlotCount = 27; + @Comment("Number of shulker box slots. Supports 9-54 slots and is normalized to a multiple of 9.") + public int shulkerBoxSlotCount = 27; + + @PostProcess + private void postProcess() { + this.enderChestSlotCount = this.normalizeContainerSlotCount(this.enderChestSlotCount, "misc.ender-chest-slot-count"); + this.shulkerBoxSlotCount = this.normalizeContainerSlotCount(this.shulkerBoxSlotCount, "misc.shulker-box-slot-count"); + } + + private int normalizeContainerSlotCount(final int configured, final String key) { + final int clamped = Math.clamp(configured, 9, 54); + final int normalized = Math.clamp(((clamped + 4) / 9) * 9, 9, 54); + if (configured != normalized) { + LOGGER.warn("Invalid {} value '{}', using '{}'. Valid values are multiples of 9 between 9 and 54.", key, configured, normalized); + } + return normalized; + } } public BlockUpdates blockUpdates; diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java index 2f41a92465b9..b3da62fc63dd 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java @@ -111,6 +111,24 @@ public InventoryView getBukkitView() { public static net.minecraft.world.inventory.MenuType getNotchInventoryType(Inventory inventory) { final InventoryType type = inventory.getType(); switch (type) { + case SHULKER_BOX: + if (inventory.getSize() == 27) { + return net.minecraft.world.inventory.MenuType.SHULKER_BOX; + } + switch (inventory.getSize()) { + case 9: + return net.minecraft.world.inventory.MenuType.GENERIC_9x1; + case 18: + return net.minecraft.world.inventory.MenuType.GENERIC_9x2; + case 36: + return net.minecraft.world.inventory.MenuType.GENERIC_9x4; + case 45: + return net.minecraft.world.inventory.MenuType.GENERIC_9x5; + case 54: + return net.minecraft.world.inventory.MenuType.GENERIC_9x6; + default: + throw new IllegalArgumentException("Unsupported shulker inventory size " + inventory.getSize()); + } case CHEST: case ENDER_CHEST: case BARREL: @@ -178,7 +196,11 @@ private void setupSlots(Container top, net.minecraft.world.entity.player.Invento this.delegate = new BeaconMenu(windowId, bottom); break; case SHULKER_BOX: - this.delegate = new ShulkerBoxMenu(windowId, bottom, top); + if (top.getContainerSize() == 27) { + this.delegate = new ShulkerBoxMenu(windowId, bottom, top); + } else { + this.delegate = new ChestMenu(CraftContainer.getNotchInventoryType(view.getTopInventory()), windowId, bottom, top, top.getContainerSize() / 9); + } break; case BLAST_FURNACE: this.delegate = new BlastFurnaceMenu(windowId, bottom, top, new SimpleContainerData(4)); From eb940447ad1d563f911969cdd6a4d5e8dc2931d9 Mon Sep 17 00:00:00 2001 From: cev-api <215493617+cev-api@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:34:43 +1000 Subject: [PATCH 2/2] Fix shulker lid state desync for expanded shulker inventories --- .../world/level/block/EnderChestBlock.java.patch | 9 +-------- .../block/entity/ShulkerBoxBlockEntity.java.patch | 10 +++++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch index c99dfe642b4a..f99ed203cf62 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/EnderChestBlock.java.patch @@ -1,13 +1,6 @@ --- a/net/minecraft/world/level/block/EnderChestBlock.java +++ b/net/minecraft/world/level/block/EnderChestBlock.java -@@ -13,6 +_,6 @@ - import net.minecraft.world.entity.monster.piglin.PiglinAi; - import net.minecraft.world.entity.player.Player; - import net.minecraft.world.inventory.ChestMenu; - import net.minecraft.world.inventory.PlayerEnderChestContainer; - import net.minecraft.world.item.context.BlockPlaceContext; - import net.minecraft.world.level.BlockGetter; -@@ -78,16 +_,28 @@ +@@ -78,16 +_,30 @@ PlayerEnderChestContainer enderChestInventory = player.getEnderChestInventory(); if (enderChestInventory != null && level.getBlockEntity(pos) instanceof EnderChestBlockEntity enderChestBlockEntity) { BlockPos blockPos = pos.above(); diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch index 65b25d913e9a..44a42f62ef52 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch @@ -1,6 +1,6 @@ --- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -@@ -40,15 +40,51 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl +@@ -40,15 +_,51 @@ public static final int OPENING_TICK_LENGTH = 10; public static final float MAX_LID_HEIGHT = 0.5F; public static final float MAX_LID_ROTATION = 270.0F; @@ -54,7 +54,7 @@ public ShulkerBoxBlockEntity(@Nullable DyeColor color, BlockPos pos, BlockState blockState) { super(BlockEntityType.SHULKER_BOX, pos, blockState); this.color = color; -@@ -171,6 +207,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl +@@ -171,6 +_,7 @@ } this.openCount++; @@ -62,7 +62,7 @@ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount); if (this.openCount == 1) { this.level.gameEvent(user.getLivingEntity(), GameEvent.CONTAINER_OPEN, this.worldPosition); -@@ -184,6 +221,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl +@@ -184,6 +_,7 @@ public void stopOpen(ContainerUser user) { if (!this.remove && !user.getLivingEntity().isSpectator()) { this.openCount--; @@ -70,7 +70,7 @@ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount); if (this.openCount <= 0) { this.level.gameEvent(user.getLivingEntity(), GameEvent.CONTAINER_CLOSE, this.worldPosition); -@@ -231,7 +269,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl +@@ -231,7 +_,8 @@ @Override public int[] getSlotsForFace(Direction side) { @@ -80,7 +80,7 @@ } @Override -@@ -254,7 +292,19 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl +@@ -254,7 +_,21 @@ @Override protected AbstractContainerMenu createMenu(int id, Inventory player) {