From 583f880d2938fc83192d3815c356689bba7147b4 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:49:35 +0200 Subject: [PATCH 1/9] Treat shelves as containers in pipes and inventory checks The shelf block entity implements Container, so vanilla hoppers insert into and extract from its three slots. This lets pipes do the same: wooden_shelves joins the container check and the pipe pull list, and insertion goes through the ordinary inventory path. --- src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java | 3 ++- src/main/java/com/sk89q/craftbook/util/InventoryUtil.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index 19b134e53..f4cd35819 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -360,7 +360,8 @@ private void startPipe(Block block, List items, boolean request) { || facType == Material.CHISELED_BOOKSHELF || facType == Material.CRAFTER || facType == Material.DECORATED_POT - || Tag.SHULKER_BOXES.isTagged(facType)) { + || Tag.SHULKER_BOXES.isTagged(facType) + || Tag.WOODEN_SHELVES.isTagged(facType)) { for (ItemStack stack : ((InventoryHolder) fac.getState()).getInventory().getContents()) { if (!ItemUtil.isStackValid(stack)) diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index 0eacd45b0..26e96bd1f 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -1,6 +1,7 @@ package com.sk89q.craftbook.util; import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.block.BrewingStand; import org.bukkit.block.Chest; @@ -371,7 +372,7 @@ public static boolean doesBlockHaveInventory(Block block) { case CRAFTER: return true; default: - return false; + return Tag.WOODEN_SHELVES.isTagged(block.getType()); } } From 02ad3f132a387e48352bc9cda1c0d043404a60ab Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:57:46 +0200 Subject: [PATCH 2/9] Collapse the duplicated container family into one predicate Adding shelves meant editing the same generic-container list in two places, the pipe pull check and doesBlockHaveInventory, which is how the two drift. hasGenericInventory now owns the family (plain item slots, safe to insert or pull generically); doesBlockHaveInventory is that family plus the four role-slot blocks whose callers route them through dedicated branches. The next container type lands in one place. --- .../sk89q/craftbook/mechanics/pipe/Pipes.java | 13 +---- .../sk89q/craftbook/util/InventoryUtil.java | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index f4cd35819..1ffc291a3 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -23,7 +23,6 @@ import com.sk89q.worldedit.world.block.BlockTypes; import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.Dropper; @@ -351,17 +350,7 @@ private void startPipe(Block block, List items, boolean request) { Block fac = block.getRelative(p.getFacing()); Material facType = fac.getType(); - if (facType == Material.CHEST - || facType == Material.TRAPPED_CHEST - || facType == Material.DROPPER - || facType == Material.DISPENSER - || facType == Material.HOPPER - || facType == Material.BARREL - || facType == Material.CHISELED_BOOKSHELF - || facType == Material.CRAFTER - || facType == Material.DECORATED_POT - || Tag.SHULKER_BOXES.isTagged(facType) - || Tag.WOODEN_SHELVES.isTagged(facType)) { + if (InventoryUtil.hasGenericInventory(facType)) { for (ItemStack stack : ((InventoryHolder) fac.getState()).getInventory().getContents()) { if (!ItemUtil.isStackValid(stack)) diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index 26e96bd1f..6e9046208 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -340,39 +340,43 @@ public static boolean fitsInSlot(ItemStack stack, ItemStack slot) { public static boolean doesBlockHaveInventory(Block block) { switch(block.getType()) { + case FURNACE: + case BLAST_FURNACE: + case SMOKER: + case BREWING_STAND: + return true; + default: + return hasGenericInventory(block.getType()); + } + } + + /** + * Checks whether a material is a container whose whole inventory is plain + * item slots, safe to insert into or pull from generically. Furnaces, + * smokers, blast furnaces and brewing stands are containers too but have + * role-specific slots (fuel, ingredient, result), so their callers route + * them through dedicated branches instead of this family. + * + * @param type The material to check. + * @return If the material is a generic container. + */ + public static boolean hasGenericInventory(Material type) { + if (Tag.SHULKER_BOXES.isTagged(type) || Tag.WOODEN_SHELVES.isTagged(type)) { + return true; + } + switch(type) { case CHEST: case TRAPPED_CHEST: case DROPPER: case DISPENSER: - case FURNACE: - case BREWING_STAND: case HOPPER: - case WHITE_SHULKER_BOX: - case ORANGE_SHULKER_BOX: - case MAGENTA_SHULKER_BOX: - case LIGHT_BLUE_SHULKER_BOX: - case YELLOW_SHULKER_BOX: - case GREEN_SHULKER_BOX: - case PINK_SHULKER_BOX: - case GRAY_SHULKER_BOX: - case LIGHT_GRAY_SHULKER_BOX: - case BLUE_SHULKER_BOX: - case PURPLE_SHULKER_BOX: - case CYAN_SHULKER_BOX: - case BROWN_SHULKER_BOX: - case LIME_SHULKER_BOX: - case BLACK_SHULKER_BOX: - case RED_SHULKER_BOX: - case SHULKER_BOX: - case BLAST_FURNACE: - case SMOKER: case BARREL: case CHISELED_BOOKSHELF: case DECORATED_POT: case CRAFTER: return true; default: - return Tag.WOODEN_SHELVES.isTagged(block.getType()); + return false; } } From 468ccb19e4563af52f86add3eaaa70cc91f8c6ac Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:27:39 +0200 Subject: [PATCH 3/9] Detach pulled stacks from their slot, and resync shelf displays Two findings from testing shelves on a live 26.2 server. Some containers hand out a mirror of the slot from getContents, so removing the slot contents also empties the stack the pipe is carrying and the pull delivers air; pulled stacks are cloned before removal. And shelves render their contents but vanilla only resyncs the display on player interaction, so pipe mutations leave clients showing stale items until the chunk reloads; a state captured after the mutation is written back for shelf pulls and inserts. Capture order matters: updating a state from before the removals writes the old contents back into the world. --- .../com/sk89q/craftbook/mechanics/pipe/Pipes.java | 11 ++++++++++- .../java/com/sk89q/craftbook/util/InventoryUtil.java | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index 1ffc291a3..518615d4f 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -23,6 +23,7 @@ import com.sk89q.worldedit.world.block.BlockTypes; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.Dropper; @@ -359,12 +360,20 @@ private void startPipe(Block block, List items, boolean request) { if(!ItemUtil.doesItemPassFilters(stack, filters, exceptions)) continue; - items.add(stack); + items.add(stack.clone()); ((InventoryHolder) fac.getState()).getInventory().removeItem(stack); if (pipeStackPerPull) break; } + // Shelves render their contents and vanilla only resyncs the + // display on player interaction, so clients keep showing the + // pulled items until the chunk reloads. The state must be + // captured AFTER the removals; updating a state from before + // them writes the old contents back into the world. + if (!items.isEmpty() && Tag.WOODEN_SHELVES.isTagged(facType)) + fac.getState().update(true, false); + PipeSuckEvent event = new PipeSuckEvent(block, new ArrayList<>(items), fac); Bukkit.getPluginManager().callEvent(event); items.clear(); diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index 6e9046208..b8476ba54 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -68,6 +68,13 @@ public static List addItemsToInventory(InventoryHolder container, boo ((Chest) ((DoubleChestInventory) container.getInventory()).getLeftSide().getHolder()).update(true); ((Chest) ((DoubleChestInventory) container.getInventory()).getRightSide().getHolder()).update(true); } + // Shelves render their contents and vanilla only resyncs the + // display on player interaction; a fresh post-insert state is + // written back so clients see the new contents. + if (container instanceof org.bukkit.block.BlockState + && Tag.WOODEN_SHELVES.isTagged(((org.bukkit.block.BlockState) container).getType())) { + ((org.bukkit.block.BlockState) container).getBlock().getState().update(true, false); + } //if(container instanceof BlockState && update) // ((BlockState) container).update(); return leftovers; From 4bd190ed07a3a73785a2546dd3a72e7c029c7376 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:38:10 +0200 Subject: [PATCH 4/9] One sync point for display containers, and it fixes chiseled bookshelves Comparing the shelf handling against its nearest neighbour showed the same bug class has been sitting in chiseled bookshelves: a pipe edit lands in the inventory but the slot_X_occupied blockstate never moves, so the shelf renders wrong and the comparator signal lies. The two publish mechanisms differ (bookshelves use blockstate properties, shelves block-entity data), so syncDisplayedContainer owns both and the pull path, the shared insert path and the bookshelf insert helper all call it. Verified both ways on a 26.2 server: occupancy set on insert, cleared on pull, including non-contiguous slots and enchanted books. --- .../sk89q/craftbook/mechanics/pipe/Pipes.java | 12 +++--- .../sk89q/craftbook/util/InventoryUtil.java | 40 +++++++++++++++---- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index 518615d4f..38f2ee464 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -366,13 +366,11 @@ private void startPipe(Block block, List items, boolean request) { break; } - // Shelves render their contents and vanilla only resyncs the - // display on player interaction, so clients keep showing the - // pulled items until the chunk reloads. The state must be - // captured AFTER the removals; updating a state from before - // them writes the old contents back into the world. - if (!items.isEmpty() && Tag.WOODEN_SHELVES.isTagged(facType)) - fac.getState().update(true, false); + // syncDisplayedContainer captures its state AFTER the removals; + // updating a state from before them writes the old contents + // back into the world. + if (!items.isEmpty()) + InventoryUtil.syncDisplayedContainer(fac); PipeSuckEvent event = new PipeSuckEvent(block, new ArrayList<>(items), fac); Bukkit.getPluginManager().callEvent(event); diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index b8476ba54..de2c629a9 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -68,13 +68,8 @@ public static List addItemsToInventory(InventoryHolder container, boo ((Chest) ((DoubleChestInventory) container.getInventory()).getLeftSide().getHolder()).update(true); ((Chest) ((DoubleChestInventory) container.getInventory()).getRightSide().getHolder()).update(true); } - // Shelves render their contents and vanilla only resyncs the - // display on player interaction; a fresh post-insert state is - // written back so clients see the new contents. - if (container instanceof org.bukkit.block.BlockState - && Tag.WOODEN_SHELVES.isTagged(((org.bukkit.block.BlockState) container).getType())) { - ((org.bukkit.block.BlockState) container).getBlock().getState().update(true, false); - } + if (container instanceof org.bukkit.block.BlockState) + syncDisplayedContainer(((org.bukkit.block.BlockState) container).getBlock()); //if(container instanceof BlockState && update) // ((BlockState) container).update(); return leftovers; @@ -219,6 +214,7 @@ public static List addItemsToChiseledBookshelf(ChiseledBookshelf chis stacks = Arrays.stream(stacks).filter(item -> ItemUtil.isAStorableBook(item)).toArray(ItemStack[]::new); leftovers.addAll(chiseledBookshelf.getInventory().addItem(stacks).values()); + syncDisplayedContainer(chiseledBookshelf.getBlock()); return leftovers; } @@ -367,6 +363,36 @@ public static boolean doesBlockHaveInventory(Block block) { * @param type The material to check. * @return If the material is a generic container. */ + /** + * Publishes a display container's contents after a plugin-side mutation. + * Player interaction is the only thing vanilla syncs on, so a pipe edit + * otherwise leaves clients rendering stale contents. The two mechanisms + * differ: chiseled bookshelves render and emit comparator signal from + * their slot_X_occupied blockstate properties, while shelves render + * straight from block-entity data, which a fresh post-mutation state + * update rebroadcasts. Every other container is a no-op here. + * + * @param block The container block that was mutated. + */ + public static void syncDisplayedContainer(Block block) { + Material type = block.getType(); + if (type == Material.CHISELED_BOOKSHELF) { + org.bukkit.block.BlockState state = block.getState(); + if (!(state instanceof ChiseledBookshelf)) + return; + Inventory inv = ((ChiseledBookshelf) state).getInventory(); + org.bukkit.block.data.BlockData data = block.getBlockData(); + if (!(data instanceof org.bukkit.block.data.type.ChiseledBookshelf)) + return; + org.bukkit.block.data.type.ChiseledBookshelf occupancy = (org.bukkit.block.data.type.ChiseledBookshelf) data; + for (int i = 0; i < inv.getSize(); i++) + occupancy.setSlotOccupied(i, ItemUtil.isStackValid(inv.getItem(i))); + block.setBlockData(occupancy, false); + } else if (Tag.WOODEN_SHELVES.isTagged(type)) { + block.getState().update(true, false); + } + } + public static boolean hasGenericInventory(Material type) { if (Tag.SHULKER_BOXES.isTagged(type) || Tag.WOODEN_SHELVES.isTagged(type)) { return true; From 78acd2d317c0bfc0f86f53432f2da081357d9257 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:00:14 +0200 Subject: [PATCH 5/9] Reattach the hasGenericInventory javadoc, and check the switch before the tags Review feedback: the javadoc had drifted away from its method, and the enum switch is a faster first check than the tag lookups, which now sit in the default branch. --- .../sk89q/craftbook/util/InventoryUtil.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index de2c629a9..78e8e0546 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -353,16 +353,6 @@ public static boolean doesBlockHaveInventory(Block block) { } } - /** - * Checks whether a material is a container whose whole inventory is plain - * item slots, safe to insert into or pull from generically. Furnaces, - * smokers, blast furnaces and brewing stands are containers too but have - * role-specific slots (fuel, ingredient, result), so their callers route - * them through dedicated branches instead of this family. - * - * @param type The material to check. - * @return If the material is a generic container. - */ /** * Publishes a display container's contents after a plugin-side mutation. * Player interaction is the only thing vanilla syncs on, so a pipe edit @@ -393,10 +383,17 @@ public static void syncDisplayedContainer(Block block) { } } + /** + * Checks whether a material is a container whose whole inventory is plain + * item slots, safe to insert into or pull from generically. Furnaces, + * smokers, blast furnaces and brewing stands are containers too but have + * role-specific slots (fuel, ingredient, result), so their callers route + * them through dedicated branches instead of this family. + * + * @param type The material to check. + * @return If the material is a generic container. + */ public static boolean hasGenericInventory(Material type) { - if (Tag.SHULKER_BOXES.isTagged(type) || Tag.WOODEN_SHELVES.isTagged(type)) { - return true; - } switch(type) { case CHEST: case TRAPPED_CHEST: @@ -409,7 +406,7 @@ public static boolean hasGenericInventory(Material type) { case CRAFTER: return true; default: - return false; + return Tag.SHULKER_BOXES.isTagged(type) || Tag.WOODEN_SHELVES.isTagged(type); } } From b88fedc42c10255fdaa72c74ea932f80572aa2df Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:03:56 +0200 Subject: [PATCH 6/9] Enumerate the shulker colours; only the shelf family stays a tag lookup Benchmarked the three shapes: enumerating every stable material as a switch case and leaving only the open-ended shelf family in the default branch is fastest on every input class, and the dominant caller is a non-container block that previously fell through to two tag lookups. The shulker colour set has been fixed since 1.11, so the case list cannot go stale the way a wood-type list would. --- .../sk89q/craftbook/util/InventoryUtil.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index 78e8e0546..32b261b35 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -404,9 +404,29 @@ public static boolean hasGenericInventory(Material type) { case CHISELED_BOOKSHELF: case DECORATED_POT: case CRAFTER: + case WHITE_SHULKER_BOX: + case ORANGE_SHULKER_BOX: + case MAGENTA_SHULKER_BOX: + case LIGHT_BLUE_SHULKER_BOX: + case YELLOW_SHULKER_BOX: + case GREEN_SHULKER_BOX: + case PINK_SHULKER_BOX: + case GRAY_SHULKER_BOX: + case LIGHT_GRAY_SHULKER_BOX: + case BLUE_SHULKER_BOX: + case PURPLE_SHULKER_BOX: + case CYAN_SHULKER_BOX: + case BROWN_SHULKER_BOX: + case LIME_SHULKER_BOX: + case BLACK_SHULKER_BOX: + case RED_SHULKER_BOX: + case SHULKER_BOX: return true; default: - return Tag.SHULKER_BOXES.isTagged(type) || Tag.WOODEN_SHELVES.isTagged(type); + // Only the open-ended family stays a tag lookup: shelf materials + // grow with each new wood type, while the shulker colour set has + // been fixed since 1.11. + return Tag.WOODEN_SHELVES.isTagged(type); } } From 968bad0fbc7f80dde50ec8354a5d6a62e223a374 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:08:41 +0200 Subject: [PATCH 7/9] Pass the caller's state into syncDisplayedContainer Creating a state is expensive on Spigot, so the pull loop hoists one and hands it on, and the insert paths pass the holder they already have. The shelf arm is the one exception and re-captures internally: update() writes the state's captured contents back into the world, so it needs a capture from after the mutation. --- .../sk89q/craftbook/mechanics/pipe/Pipes.java | 10 ++++----- .../sk89q/craftbook/util/InventoryUtil.java | 21 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index 38f2ee464..48fa9937b 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -352,7 +352,8 @@ private void startPipe(Block block, List items, boolean request) { Material facType = fac.getType(); if (InventoryUtil.hasGenericInventory(facType)) { - for (ItemStack stack : ((InventoryHolder) fac.getState()).getInventory().getContents()) { + org.bukkit.block.BlockState facState = fac.getState(); + for (ItemStack stack : ((InventoryHolder) facState).getInventory().getContents()) { if (!ItemUtil.isStackValid(stack)) continue; @@ -361,16 +362,13 @@ private void startPipe(Block block, List items, boolean request) { continue; items.add(stack.clone()); - ((InventoryHolder) fac.getState()).getInventory().removeItem(stack); + ((InventoryHolder) facState).getInventory().removeItem(stack); if (pipeStackPerPull) break; } - // syncDisplayedContainer captures its state AFTER the removals; - // updating a state from before them writes the old contents - // back into the world. if (!items.isEmpty()) - InventoryUtil.syncDisplayedContainer(fac); + InventoryUtil.syncDisplayedContainer(facState); PipeSuckEvent event = new PipeSuckEvent(block, new ArrayList<>(items), fac); Bukkit.getPluginManager().callEvent(event); diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index 32b261b35..77032495e 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -69,7 +69,7 @@ public static List addItemsToInventory(InventoryHolder container, boo ((Chest) ((DoubleChestInventory) container.getInventory()).getRightSide().getHolder()).update(true); } if (container instanceof org.bukkit.block.BlockState) - syncDisplayedContainer(((org.bukkit.block.BlockState) container).getBlock()); + syncDisplayedContainer((org.bukkit.block.BlockState) container); //if(container instanceof BlockState && update) // ((BlockState) container).update(); return leftovers; @@ -214,7 +214,7 @@ public static List addItemsToChiseledBookshelf(ChiseledBookshelf chis stacks = Arrays.stream(stacks).filter(item -> ItemUtil.isAStorableBook(item)).toArray(ItemStack[]::new); leftovers.addAll(chiseledBookshelf.getInventory().addItem(stacks).values()); - syncDisplayedContainer(chiseledBookshelf.getBlock()); + syncDisplayedContainer(chiseledBookshelf); return leftovers; } @@ -362,15 +362,15 @@ public static boolean doesBlockHaveInventory(Block block) { * straight from block-entity data, which a fresh post-mutation state * update rebroadcasts. Every other container is a no-op here. * - * @param block The container block that was mutated. + * @param state The container's state, from the caller that mutated it. */ - public static void syncDisplayedContainer(Block block) { - Material type = block.getType(); + public static void syncDisplayedContainer(org.bukkit.block.BlockState state) { + Material type = state.getType(); if (type == Material.CHISELED_BOOKSHELF) { - org.bukkit.block.BlockState state = block.getState(); if (!(state instanceof ChiseledBookshelf)) return; Inventory inv = ((ChiseledBookshelf) state).getInventory(); + Block block = state.getBlock(); org.bukkit.block.data.BlockData data = block.getBlockData(); if (!(data instanceof org.bukkit.block.data.type.ChiseledBookshelf)) return; @@ -379,7 +379,10 @@ public static void syncDisplayedContainer(Block block) { occupancy.setSlotOccupied(i, ItemUtil.isStackValid(inv.getItem(i))); block.setBlockData(occupancy, false); } else if (Tag.WOODEN_SHELVES.isTagged(type)) { - block.getState().update(true, false); + // update() writes the state's captured contents back into the + // world, so this one must be captured after the mutation; the + // caller's state predates it and would restore the old contents. + state.getBlock().getState().update(true, false); } } @@ -423,9 +426,7 @@ public static boolean hasGenericInventory(Material type) { case SHULKER_BOX: return true; default: - // Only the open-ended family stays a tag lookup: shelf materials - // grow with each new wood type, while the shulker colour set has - // been fixed since 1.11. + // Shelves are matched by tag because new wood types keep adding materials. return Tag.WOODEN_SHELVES.isTagged(type); } } From 78b7d10cf06323f6efbf0efe8e958a9c792597e3 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:09:21 +0200 Subject: [PATCH 8/9] Copper chests join the container family All eight variants run through ChestBlock and the ordinary chest state, so doubles and insertion already behave like chests; the only thing missing was family membership. The set is four oxidation stages waxed or not, closed, so they are switch cases rather than a tag. --- src/main/java/com/sk89q/craftbook/util/InventoryUtil.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index 77032495e..b1e657b8a 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -407,6 +407,14 @@ public static boolean hasGenericInventory(Material type) { case CHISELED_BOOKSHELF: case DECORATED_POT: case CRAFTER: + case COPPER_CHEST: + case EXPOSED_COPPER_CHEST: + case WEATHERED_COPPER_CHEST: + case OXIDIZED_COPPER_CHEST: + case WAXED_COPPER_CHEST: + case WAXED_EXPOSED_COPPER_CHEST: + case WAXED_WEATHERED_COPPER_CHEST: + case WAXED_OXIDIZED_COPPER_CHEST: case WHITE_SHULKER_BOX: case ORANGE_SHULKER_BOX: case MAGENTA_SHULKER_BOX: From be7268d329037e5bcf8a541209dc7556fd91380a Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:49:53 +0200 Subject: [PATCH 9/9] Drop the display resync, the desync was a Paper bug fixed by Paper 13567 --- .../sk89q/craftbook/mechanics/pipe/Pipes.java | 3 -- .../sk89q/craftbook/util/InventoryUtil.java | 36 ------------------- 2 files changed, 39 deletions(-) diff --git a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java index 48fa9937b..4561d1f12 100644 --- a/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java +++ b/src/main/java/com/sk89q/craftbook/mechanics/pipe/Pipes.java @@ -367,9 +367,6 @@ private void startPipe(Block block, List items, boolean request) { break; } - if (!items.isEmpty()) - InventoryUtil.syncDisplayedContainer(facState); - PipeSuckEvent event = new PipeSuckEvent(block, new ArrayList<>(items), fac); Bukkit.getPluginManager().callEvent(event); items.clear(); diff --git a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java index b1e657b8a..0c7ec9cdf 100644 --- a/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java +++ b/src/main/java/com/sk89q/craftbook/util/InventoryUtil.java @@ -68,8 +68,6 @@ public static List addItemsToInventory(InventoryHolder container, boo ((Chest) ((DoubleChestInventory) container.getInventory()).getLeftSide().getHolder()).update(true); ((Chest) ((DoubleChestInventory) container.getInventory()).getRightSide().getHolder()).update(true); } - if (container instanceof org.bukkit.block.BlockState) - syncDisplayedContainer((org.bukkit.block.BlockState) container); //if(container instanceof BlockState && update) // ((BlockState) container).update(); return leftovers; @@ -214,7 +212,6 @@ public static List addItemsToChiseledBookshelf(ChiseledBookshelf chis stacks = Arrays.stream(stacks).filter(item -> ItemUtil.isAStorableBook(item)).toArray(ItemStack[]::new); leftovers.addAll(chiseledBookshelf.getInventory().addItem(stacks).values()); - syncDisplayedContainer(chiseledBookshelf); return leftovers; } @@ -353,39 +350,6 @@ public static boolean doesBlockHaveInventory(Block block) { } } - /** - * Publishes a display container's contents after a plugin-side mutation. - * Player interaction is the only thing vanilla syncs on, so a pipe edit - * otherwise leaves clients rendering stale contents. The two mechanisms - * differ: chiseled bookshelves render and emit comparator signal from - * their slot_X_occupied blockstate properties, while shelves render - * straight from block-entity data, which a fresh post-mutation state - * update rebroadcasts. Every other container is a no-op here. - * - * @param state The container's state, from the caller that mutated it. - */ - public static void syncDisplayedContainer(org.bukkit.block.BlockState state) { - Material type = state.getType(); - if (type == Material.CHISELED_BOOKSHELF) { - if (!(state instanceof ChiseledBookshelf)) - return; - Inventory inv = ((ChiseledBookshelf) state).getInventory(); - Block block = state.getBlock(); - org.bukkit.block.data.BlockData data = block.getBlockData(); - if (!(data instanceof org.bukkit.block.data.type.ChiseledBookshelf)) - return; - org.bukkit.block.data.type.ChiseledBookshelf occupancy = (org.bukkit.block.data.type.ChiseledBookshelf) data; - for (int i = 0; i < inv.getSize(); i++) - occupancy.setSlotOccupied(i, ItemUtil.isStackValid(inv.getItem(i))); - block.setBlockData(occupancy, false); - } else if (Tag.WOODEN_SHELVES.isTagged(type)) { - // update() writes the state's captured contents back into the - // world, so this one must be captured after the mutation; the - // caller's state predates it and would restore the old contents. - state.getBlock().getState().update(true, false); - } - } - /** * Checks whether a material is a container whose whole inventory is plain * item slots, safe to insert into or pull from generically. Furnaces,