From 57b5b8fe8406cdcd9846c15e7cae565c40e8ce60 Mon Sep 17 00:00:00 2001 From: akastijn Date: Fri, 27 Mar 2026 20:46:09 +0100 Subject: [PATCH] Refactor book byte enforcement by replacing BookByteChunkLimitListener with simplified BookByteLimitListener. Adjust byte limits and improve oversized book handling logic. --- .../com/alttd/playerutils/PlayerUtils.java | 2 +- .../BookByteChunkLimitListener.java | 285 ------------------ .../BookByteLimitListener.java | 103 +++++++ .../event_listeners/BookWriteEvent.java | 26 +- .../alttd/playerutils/util/BookByteUtils.java | 54 +++- .../util/ChunkBookByteTracker.java | 43 --- 6 files changed, 166 insertions(+), 347 deletions(-) delete mode 100644 src/main/java/com/alttd/playerutils/event_listeners/BookByteChunkLimitListener.java create mode 100644 src/main/java/com/alttd/playerutils/event_listeners/BookByteLimitListener.java delete mode 100644 src/main/java/com/alttd/playerutils/util/ChunkBookByteTracker.java diff --git a/src/main/java/com/alttd/playerutils/PlayerUtils.java b/src/main/java/com/alttd/playerutils/PlayerUtils.java index b66df1b..f75ef7d 100644 --- a/src/main/java/com/alttd/playerutils/PlayerUtils.java +++ b/src/main/java/com/alttd/playerutils/PlayerUtils.java @@ -43,7 +43,7 @@ public final class PlayerUtils extends JavaPlugin { pluginManager.registerEvents(new BlockBlockUseEvent(), this); pluginManager.registerEvents(new PlayerJoin(this), this); pluginManager.registerEvents(new BookWriteEvent(), this); - pluginManager.registerEvents(new BookByteChunkLimitListener(this), this); + pluginManager.registerEvents(new BookByteLimitListener(), this); RotateBlockEvent rotateBlockEvent = new RotateBlockEvent(); pluginManager.registerEvents(rotateBlockEvent, this); diff --git a/src/main/java/com/alttd/playerutils/event_listeners/BookByteChunkLimitListener.java b/src/main/java/com/alttd/playerutils/event_listeners/BookByteChunkLimitListener.java deleted file mode 100644 index 9e27d31..0000000 --- a/src/main/java/com/alttd/playerutils/event_listeners/BookByteChunkLimitListener.java +++ /dev/null @@ -1,285 +0,0 @@ -package com.alttd.playerutils.event_listeners; - -import com.alttd.playerutils.PlayerUtils; -import com.alttd.playerutils.util.BookByteUtils; -import com.alttd.playerutils.util.ChunkBookByteTracker; -import lombok.extern.slf4j.Slf4j; -import org.bukkit.Chunk; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Item; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.entity.ItemDespawnEvent; -import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.entity.EntityPickupItemEvent; -import org.bukkit.event.inventory.*; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemStack; - -import java.util.Objects; - -@Slf4j -@SuppressWarnings("ClassCanBeRecord") -public class BookByteChunkLimitListener implements Listener { - - private final PlayerUtils plugin; - private static final int CHUNK_CAP = BookByteUtils.MAX_BOOK_BYTES * 3; // 65k * 3 - - public BookByteChunkLimitListener(PlayerUtils plugin) { - this.plugin = plugin; - } - - // World item drops ------------------------------------------------- - @EventHandler - public void onPlayerDropItem(PlayerDropItemEvent event) { - Item item = event.getItemDrop(); - ItemStack stack = item.getItemStack(); - if (!BookByteUtils.isWrittenBook(stack)) { - return; - } - int bytes = BookByteUtils.computeBytes(stack); - if (bytes <= 0) { - return; - } - Chunk chunk = item.getLocation().getChunk(); - if (!ChunkBookByteTracker.tryAddBytes(chunk, bytes, CHUNK_CAP, plugin)) { - log.warn("Player {} [{}] tried to drop a book of {} bytes to a chunk that is already saturated", event.getPlayer().getName(), event.getPlayer().getUniqueId(), bytes); - event.setCancelled(true); - Player p = event.getPlayer(); - p.sendRichMessage("You can't drop this here."); - } - } - - @EventHandler - public void onPickup(EntityPickupItemEvent event) { - Item item = event.getItem(); - ItemStack stack = event.getItem().getItemStack(); - if (!BookByteUtils.isWrittenBook(stack)) { - return; - } - int bytes = BookByteUtils.computeBytes(stack); - if (bytes <= 0) { - return; - } - ChunkBookByteTracker.removeBytes(item.getLocation().getChunk(), bytes, plugin); - } - - @EventHandler - public void onItemDespawn(ItemDespawnEvent event) { - Item item = event.getEntity(); - ItemStack stack = item.getItemStack(); - if (!BookByteUtils.isWrittenBook(stack)) { - return; - } - int bytes = BookByteUtils.computeBytes(stack); - if (bytes <= 0) { - return; - } - ChunkBookByteTracker.removeBytes(item.getLocation().getChunk(), bytes, plugin); - } - - // Player death drops ------------------------------------------------- - @EventHandler - public void onPlayerDeath(PlayerDeathEvent event) { - if (event.getDrops().isEmpty()) { - return; - } - Chunk chunk = event.getEntity().getLocation().getChunk(); - int current = ChunkBookByteTracker.getBytes(chunk, plugin); - int available = CHUNK_CAP - current; - if (available <= 0) { - // remove all large books from drops - event.getDrops().removeIf(stack -> { - if (!BookByteUtils.isWrittenBook(stack)) { - return false; - } - int bytes = BookByteUtils.computeBytes(stack); - boolean isByteLimitExceeded = bytes > 0; - if (isByteLimitExceeded) { - log.warn("Player {} [{}] tried to drop a book of {} bytes by dying in a chunk that is already saturated", event.getEntity().getName(), event.getEntity().getUniqueId(), bytes); - } - return isByteLimitExceeded; // despawn - }); - return; - } - int usedFromDeath = 0; - for (int i = 0; i < event.getDrops().size(); i++) { - ItemStack stack = event.getDrops().get(i); - if (!BookByteUtils.isWrittenBook(stack)) { - continue; - } - int bytes = BookByteUtils.computeBytes(stack); - if (bytes <= 0) { - continue; - } - if (usedFromDeath + bytes > available) { - // remove excess - log.warn("Player {} [{}] tried to drop a book of {} bytes by dying in a chunk that would be saturated", event.getEntity().getName(), event.getEntity().getUniqueId(), bytes); - event.getDrops().set(i, null); - } else { - usedFromDeath += bytes; - } - } - event.getDrops().removeIf(Objects::isNull); - if (usedFromDeath > 0) { - ChunkBookByteTracker.tryAddBytes(chunk, usedFromDeath, CHUNK_CAP, plugin); // will fit by construction - } - } - - // Container interactions -------------------------------------------- - @SuppressWarnings("DuplicatedCode") - @EventHandler - public void onInventoryClick(InventoryClickEvent event) { - InventoryView view = event.getView(); - Inventory top = view.getTopInventory(); - boolean topIsContainer = event.getRawSlot() < top.getSize(); - - // Shift-click from bottom to top (adding to container) - if (!topIsContainer) { - if (!event.isShiftClick()) { - return; - } - ItemStack current = event.getCurrentItem(); - if (current == null) { - return; - } - if (!BookByteUtils.isWrittenBook(current)) { - return; - } - int addBytes = BookByteUtils.computeBytes(current); - if (addBytes <= 0) { - return; - } - Chunk chunk = getContainerChunk(top); - if (chunk == null) { - return; - } - if (!ChunkBookByteTracker.tryAddBytes(chunk, addBytes, CHUNK_CAP, plugin)) { - log.warn("Player {} [{}] tried to shift click a book of {} bytes into a container in a chunk that is already saturated", event.getWhoClicked().getName(), event.getWhoClicked().getUniqueId(), addBytes); - event.setCancelled(true); - sendCantStore(event.getWhoClicked()); - } - return; - } - - // Interactions within the top container inventory - Chunk chunk = getContainerChunk(top); - if (chunk == null) { - return; - } - - ItemStack current = event.getCurrentItem(); // item in the clicked slot (may be removed) - ItemStack cursor = event.getCursor(); // item on cursor (may be added) - - int removeBytes = (BookByteUtils.isWrittenBook(current)) ? BookByteUtils.computeBytes(current) : 0; - int addBytes = !cursor.getType().isAir() && BookByteUtils.isWrittenBook(cursor) ? BookByteUtils.computeBytes(cursor) : 0; - - // If shift-click from top to bottom: only removal occurs - if (event.isShiftClick()) { - if (removeBytes <= 0) { - return; - } - ChunkBookByteTracker.removeBytes(chunk, removeBytes, plugin); - return; - } - - // If nothing book-related, ignore - if (removeBytes <= 0 && addBytes <= 0) { - return; - } - - int currentTracked = ChunkBookByteTracker.getBytes(chunk, plugin); - int projected = currentTracked - removeBytes + addBytes; - if (projected > CHUNK_CAP) { - log.warn("Player {} [{}] tried to add a book of {} bytes to a chunk that would be saturated", event.getWhoClicked().getName(), event.getWhoClicked().getUniqueId(), addBytes); - event.setCancelled(true); - sendCantStore(event.getWhoClicked()); - return; - } - // Apply adjustments - ChunkBookByteTracker.setBytes(chunk, Math.max(0, projected), plugin); - } - - @EventHandler - public void onInventoryDrag(InventoryDragEvent event) { - // If any of the added slots are in the top inventory, block if needed - InventoryView view = event.getView(); - Inventory top = view.getTopInventory(); - ItemStack cursor = event.getOldCursor(); - if (cursor.getType().isAir()) { - return; - } - if (!BookByteUtils.isWrittenBook(cursor)) { - return; - } - int bytes = BookByteUtils.computeBytes(cursor); - if (bytes <= 0) { - return; - } - boolean affectsTop = event.getRawSlots().stream().anyMatch(slot -> slot < top.getSize()); - if (!affectsTop) { - return; - } - Chunk chunk = getContainerChunk(top); - if (chunk == null) { - return; - } - if (!ChunkBookByteTracker.tryAddBytes(chunk, bytes, CHUNK_CAP, plugin)) { - log.warn("Player {} [{}] tried to add a book of {} bytes to a chunk that is already saturated", event.getWhoClicked().getName(), event.getWhoClicked().getUniqueId(), bytes); - event.setCancelled(true); - sendCantStore(event.getWhoClicked()); - } - } - - @EventHandler - public void onInventoryMoveItem(InventoryMoveItemEvent event) { - // hopper or other automation - ItemStack stack = event.getItem(); - if (!BookByteUtils.isWrittenBook(stack)) { - return; - } - int bytes = BookByteUtils.computeBytes(stack); - if (bytes <= 0) { - return; - } - Inventory dest = event.getDestination(); - // Only enforce when destination is a block inventory in world - Chunk destChunk = InventoryChunkResolver.getInventoryChunk(dest); - if (destChunk == null) { - return; - } - if (!ChunkBookByteTracker.tryAddBytes(destChunk, bytes, CHUNK_CAP, plugin)) { - event.setCancelled(true); - return; - } - // will move; remove from source if it was tracked in a container within a chunk - Chunk srcChunk = InventoryChunkResolver.getInventoryChunk(event.getSource()); - if (srcChunk != null) { - ChunkBookByteTracker.removeBytes(srcChunk, bytes, plugin); - } - } - - private void sendCantStore(HumanEntity who) { - who.sendRichMessage("You can't store this here."); - } - - private Chunk getContainerChunk(Inventory inv) { - return InventoryChunkResolver.getInventoryChunk(inv); - } - - // Helper to resolve container inventory chunks - private static class InventoryChunkResolver { - static Chunk getInventoryChunk(Inventory inv) { - if (inv == null) { - return null; - } - if (inv.getHolder() instanceof org.bukkit.block.BlockState state) { - return state.getLocation().getChunk(); - } - return null; - } - } -} diff --git a/src/main/java/com/alttd/playerutils/event_listeners/BookByteLimitListener.java b/src/main/java/com/alttd/playerutils/event_listeners/BookByteLimitListener.java new file mode 100644 index 0000000..1df7fca --- /dev/null +++ b/src/main/java/com/alttd/playerutils/event_listeners/BookByteLimitListener.java @@ -0,0 +1,103 @@ +package com.alttd.playerutils.event_listeners; + +import com.alttd.playerutils.util.BookByteUtils; +import lombok.extern.slf4j.Slf4j; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Bukkit; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Item; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.ItemSpawnEvent; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryMoveItemEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.inventory.ItemStack; + +@Slf4j +public class BookByteLimitListener implements Listener { + + private boolean isOversizedBook(ItemStack stack) { + boolean isOversizedBook = BookByteUtils.shouldCountForBookByteLimit(stack) + && BookByteUtils.computeBytes(stack) > BookByteUtils.MAX_BOOK_BYTES; + if (isOversizedBook) { + log.warn("Player tried to drop an oversized book"); + Component message = MiniMessage.miniMessage().deserialize( + "Player tried to drop an oversized book"); + Bukkit.broadcast(message, "staffutils.patrol"); + } + return isOversizedBook; + } + + private boolean isOversizedBook(ItemStack stack, HumanEntity humanEntity) { + boolean isOversizedBook = BookByteUtils.shouldCountForBookByteLimit(stack) + && BookByteUtils.computeBytes(stack) > BookByteUtils.MAX_BOOK_BYTES; + if (isOversizedBook) { + log.warn("{} [{}] tried to drop an oversized book", humanEntity.getName(), humanEntity.getUniqueId()); + Component message = MiniMessage.miniMessage().deserialize( + "Player tried to drop an oversized book", + Placeholder.unparsed("player", humanEntity.getName())); + Bukkit.broadcast(message, "staffutils.patrol"); + } + return isOversizedBook; + } + + @EventHandler + public void onItemSpawn(ItemSpawnEvent event) { + Item item = event.getEntity(); + if (isOversizedBook(item.getItemStack())) { + event.setCancelled(true); + } + } + + @EventHandler + public void onPlayerDrop(PlayerDropItemEvent event) { + if (isOversizedBook(event.getItemDrop().getItemStack(), event.getPlayer())) { + event.setCancelled(true); + } + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + InventoryAction action = event.getAction(); + + switch (action) { + case PLACE_ALL, PLACE_ONE, PLACE_SOME, SWAP_WITH_CURSOR -> { + if (isOversizedBook(event.getCursor(), event.getWhoClicked())) { + event.setCancelled(true); + } + } + case MOVE_TO_OTHER_INVENTORY -> { + if (isOversizedBook(event.getCurrentItem(), event.getWhoClicked())) { + event.setCancelled(true); + } + } + case HOTBAR_SWAP -> { + ItemStack hotbarItem = event.getWhoClicked().getInventory().getItem(event.getHotbarButton()); + if (isOversizedBook(hotbarItem, event.getWhoClicked())) { + event.setCancelled(true); + } + } + default -> { + } + } + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + if (isOversizedBook(event.getOldCursor(), event.getWhoClicked())) { + event.setCancelled(true); + } + } + + @EventHandler + public void onInventoryMoveItem(InventoryMoveItemEvent event) { + if (isOversizedBook(event.getItem())) { + event.setCancelled(true); + } + } +} diff --git a/src/main/java/com/alttd/playerutils/event_listeners/BookWriteEvent.java b/src/main/java/com/alttd/playerutils/event_listeners/BookWriteEvent.java index cc6844d..09dea40 100644 --- a/src/main/java/com/alttd/playerutils/event_listeners/BookWriteEvent.java +++ b/src/main/java/com/alttd/playerutils/event_listeners/BookWriteEvent.java @@ -2,6 +2,10 @@ package com.alttd.playerutils.event_listeners; import com.alttd.playerutils.util.BookByteUtils; import lombok.extern.slf4j.Slf4j; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -11,25 +15,29 @@ import org.bukkit.inventory.meta.BookMeta; @Slf4j public class BookWriteEvent implements Listener { - private static final int WARN_BOOK_BYTES = 10_000; - @EventHandler public void onPlayerEditBook(PlayerEditBookEvent event) { Player player = event.getPlayer(); - if (player.hasPermission("playerutils.book-write.bypass")) { - return; - } - BookMeta meta = event.getNewBookMeta(); int totalBytes = BookByteUtils.computeBytes(meta); if (totalBytes > BookByteUtils.MAX_BOOK_BYTES) { - log.warn("Player {} [{}] tried to write a book with {} bytes", player.getName(), player.getUniqueId(), totalBytes); + log.warn("Player {} [{}] tried to write a book with {} bytes", + player.getName(), player.getUniqueId(), totalBytes); event.setCancelled(true); - } else if (totalBytes > WARN_BOOK_BYTES) { - log.warn("Player {} [{}] wrote a book with {} bytes", player.getName(), player.getUniqueId(), totalBytes); + Component message = MiniMessage.miniMessage().deserialize( + "Player tried to write a book with bytes", + Placeholder.unparsed("player", player.getName()), Placeholder.parsed("bytes", String.valueOf(totalBytes))); + Bukkit.broadcast(message, "staffutils.patrol"); + } else if (totalBytes > BookByteUtils.BIG_BOOK_BYTES) { + log.warn("Player {} [{}] wrote a large book with {} bytes", + player.getName(), player.getUniqueId(), totalBytes); + Component message = MiniMessage.miniMessage().deserialize( + "Player wrote a book with bytes", + Placeholder.unparsed("player", player.getName()), Placeholder.parsed("bytes", String.valueOf(totalBytes))); + Bukkit.broadcast(message, "staffutils.patrol"); } } } diff --git a/src/main/java/com/alttd/playerutils/util/BookByteUtils.java b/src/main/java/com/alttd/playerutils/util/BookByteUtils.java index 9c6806d..d362c1a 100644 --- a/src/main/java/com/alttd/playerutils/util/BookByteUtils.java +++ b/src/main/java/com/alttd/playerutils/util/BookByteUtils.java @@ -2,8 +2,11 @@ package com.alttd.playerutils.util; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Material; +import org.bukkit.block.ShulkerBox; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.BlockStateMeta; import java.nio.charset.StandardCharsets; @@ -16,16 +19,36 @@ public final class BookByteUtils { } // 65,000 bytes per book (below CoreProtect hard limit ~65,535) - public static final int MAX_BOOK_BYTES = 65_000; + public static final int MAX_BOOK_BYTES = 30_000; + public static final int BIG_BOOK_BYTES = 10_000; - public static boolean isWrittenBook(ItemStack stack) { + public static boolean shouldCountForBookByteLimit(ItemStack stack) { if (stack == null) { return false; } - if (!(stack.getItemMeta() instanceof BookMeta meta)) { + Material type = stack.getType(); + if (type == Material.WRITTEN_BOOK || type == Material.WRITABLE_BOOK) { + return true; + } + if (stack.getItemMeta() instanceof BookMeta) { + return true; + } + if (!(stack.getItemMeta() instanceof BlockStateMeta bsm) || !(bsm.getBlockState() instanceof ShulkerBox shulker)) { return false; } - return meta.hasAuthor() || meta.hasPages() || meta.hasTitle(); + for (ItemStack content : shulker.getInventory().getContents()) { + if (content == null) { + continue; + } + Material contentType = content.getType(); + if (contentType == Material.WRITTEN_BOOK || contentType == Material.WRITABLE_BOOK) { + return true; + } + if (content.getItemMeta() instanceof BookMeta) { + return true; + } + } + return false; } /** @@ -60,11 +83,24 @@ public final class BookByteUtils { if (stack == null) { return 0; } - if (!(stack.getItemMeta() instanceof BookMeta meta)) { - return 0; + // Direct written book + if (stack.getItemMeta() instanceof BookMeta meta) { + int perBook = computeBytes(meta); + return perBook * Math.max(1, stack.getAmount()); } - // written books do not stack; still, be safe and multiply by amount if it ever changes - int perBook = computeBytes(meta); - return perBook * Math.max(1, stack.getAmount()); + // Shulker box: sum bytes of contained written books + if (stack.getItemMeta() instanceof BlockStateMeta bsm && bsm.getBlockState() instanceof ShulkerBox shulker) { + int total = 0; + for (ItemStack content : shulker.getInventory().getContents()) { + if (content == null) { + continue; + } + if (content.getItemMeta() instanceof BookMeta bookMeta) { + total += computeBytes(bookMeta) * Math.max(1, content.getAmount()); + } + } + return total; + } + return 0; } } diff --git a/src/main/java/com/alttd/playerutils/util/ChunkBookByteTracker.java b/src/main/java/com/alttd/playerutils/util/ChunkBookByteTracker.java deleted file mode 100644 index 650e363..0000000 --- a/src/main/java/com/alttd/playerutils/util/ChunkBookByteTracker.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.alttd.playerutils.util; - -import com.alttd.playerutils.PlayerUtils; -import org.bukkit.Chunk; -import org.bukkit.NamespacedKey; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataType; - -public final class ChunkBookByteTracker { - - private static NamespacedKey BYTES_KEY(PlayerUtils plugin) { - return NamespacedKey.fromString("big_book_bytes", plugin); - } - - private ChunkBookByteTracker() {} - - public static int getBytes(Chunk chunk, PlayerUtils plugin) { - PersistentDataContainer pdc = chunk.getPersistentDataContainer(); - Integer bytes = pdc.get(BYTES_KEY(plugin), PersistentDataType.INTEGER); - return bytes == null ? 0 : bytes; - } - - public static void setBytes(Chunk chunk, int bytes, PlayerUtils plugin) { - PersistentDataContainer pdc = chunk.getPersistentDataContainer(); - pdc.set(BYTES_KEY(plugin), PersistentDataType.INTEGER, Math.max(0, bytes)); - } - - public static boolean tryAddBytes(Chunk chunk, int add, int cap, PlayerUtils plugin) { - int current = getBytes(chunk, plugin); - if ((long) current + add > cap) { - return false; - } - setBytes(chunk, current + add, plugin); - return true; - } - - public static void removeBytes(Chunk chunk, int remove, PlayerUtils plugin) { - int current = getBytes(chunk, plugin); - int newVal = current - remove; - if (newVal < 0) newVal = 0; - setBytes(chunk, newVal, plugin); - } -}