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);
- }
-}