5 Commits
11 changed files with 562 additions and 14 deletions
+4
View File
@@ -47,4 +47,8 @@ dependencies {
compileOnly("org.projectlombok:lombok:1.18.38") compileOnly("org.projectlombok:lombok:1.18.38")
annotationProcessor("org.projectlombok:lombok:1.18.38") annotationProcessor("org.projectlombok:lombok:1.18.38")
implementation(platform("com.intellectualsites.bom:bom-newest:1.56"))
compileOnly("com.intellectualsites.plotsquared:plotsquared-core")
compileOnly("com.intellectualsites.plotsquared:plotsquared-bukkit") { isTransitive = false }
} }
+2 -1
View File
@@ -14,8 +14,9 @@ dependencyResolutionManagement {
password = nexusPass password = nexusPass
} }
} }
maven("https://repo.destro.xyz/snapshots")
maven("https://jitpack.io") maven("https://jitpack.io")
// PlotSquared
maven("https://repo.papermc.io/repository/maven-public/")
} }
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
} }
@@ -2,29 +2,42 @@ package com.alttd.playerutils;
import com.alttd.playerutils.commands.PlayerUtilsCommand; import com.alttd.playerutils.commands.PlayerUtilsCommand;
import com.alttd.playerutils.commands.playerutils_subcommands.GhastSpeed; import com.alttd.playerutils.commands.playerutils_subcommands.GhastSpeed;
import com.alttd.playerutils.commands.playerutils_subcommands.RandomPlot;
import com.alttd.playerutils.commands.playerutils_subcommands.RotateBlock; import com.alttd.playerutils.commands.playerutils_subcommands.RotateBlock;
import com.alttd.playerutils.config.Config; import com.alttd.playerutils.config.Config;
import com.alttd.playerutils.config.KeyStorage; import com.alttd.playerutils.config.KeyStorage;
import com.alttd.playerutils.config.Messages; import com.alttd.playerutils.config.Messages;
import com.alttd.playerutils.event_listeners.*; import com.alttd.playerutils.event_listeners.*;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j
public final class PlayerUtils extends JavaPlugin { public final class PlayerUtils extends JavaPlugin {
private PlayerUtilsCommand playerUtilsCommand; private PlayerUtilsCommand playerUtilsCommand;
@Override @Override
public void onEnable() { public void onEnable() {
registerCommands();
registerEvents();
reloadConfigs(); reloadConfigs();
registerCommands();
registerRandomPlot();
registerEvents();
registerSchedulers(); registerSchedulers();
} }
private void registerRandomPlot() {
if (!getServer().getPluginManager().isPluginEnabled("PlotSquared")) {
log.warn("PlotSquared not found — random plot command will not be registered.");
return;
}
playerUtilsCommand.addSubCommand(new RandomPlot(this));
log.info("PlotSquared found - registered random plot command.");
}
@Override @Override
public void onDisable() { public void onDisable() {
KeyStorage.STORAGE.save(); KeyStorage.STORAGE.save();
@@ -42,6 +55,8 @@ public final class PlayerUtils extends JavaPlugin {
pluginManager.registerEvents(new LimitArmorStands(this), this); pluginManager.registerEvents(new LimitArmorStands(this), this);
pluginManager.registerEvents(new BlockBlockUseEvent(), this); pluginManager.registerEvents(new BlockBlockUseEvent(), this);
pluginManager.registerEvents(new PlayerJoin(this), this); pluginManager.registerEvents(new PlayerJoin(this), this);
pluginManager.registerEvents(new BookWriteEvent(), this);
pluginManager.registerEvents(new BookByteLimitListener(), this);
RotateBlockEvent rotateBlockEvent = new RotateBlockEvent(); RotateBlockEvent rotateBlockEvent = new RotateBlockEvent();
pluginManager.registerEvents(rotateBlockEvent, this); pluginManager.registerEvents(rotateBlockEvent, this);
@@ -42,7 +42,7 @@ import java.util.stream.Collectors;
.forEach(team -> team.removePlayer(player)); .forEach(team -> team.removePlayer(player));
if (args[1].equalsIgnoreCase("off")) { if (args[1].equalsIgnoreCase("off")) {
turnOffGlow(commandSender, player, otherPlayer); turnOffGlow(commandSender, player, otherPlayer, board);
return true; return true;
} }
@@ -88,10 +88,15 @@ import java.util.stream.Collectors;
} }
} }
private void turnOffGlow(CommandSender commandSender, Player player, boolean otherPlayer) { private void turnOffGlow(CommandSender commandSender, Player player, boolean otherPlayer, Scoreboard board) {
player.sendRichMessage(Messages.GLOW.GLOW_OFF); player.sendRichMessage(Messages.GLOW.GLOW_OFF);
player.setGlowing(false); player.setGlowing(false);
board.getTeams().stream()
.filter(team -> team.getName().startsWith("Glow-"))
.filter(team -> team.hasPlayer(player))
.forEach(team -> team.removePlayer(player));
if (otherPlayer) { if (otherPlayer) {
commandSender.sendRichMessage(Messages.GLOW.GLOW_OFF_FOR_PLAYER, Placeholder.component("player", player.name())); commandSender.sendRichMessage(Messages.GLOW.GLOW_OFF_FOR_PLAYER, Placeholder.component("player", player.name()));
} }
@@ -0,0 +1,181 @@
package com.alttd.playerutils.commands.playerutils_subcommands;
import com.alttd.playerutils.PlayerUtils;
import com.alttd.playerutils.commands.SubCommand;
import com.alttd.playerutils.config.Config;
import com.alttd.playerutils.config.Messages;
import com.plotsquared.bukkit.player.BukkitPlayer;
import com.plotsquared.bukkit.util.BukkitUtil;
import com.plotsquared.core.PlotSquared;
import com.plotsquared.core.events.TeleportCause;
import com.plotsquared.core.plot.Plot;
import com.plotsquared.core.plot.PlotArea;
import lombok.RequiredArgsConstructor;
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 net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@Slf4j
@RequiredArgsConstructor
public class RandomPlot extends SubCommand {
private static final String PERMISSION = "playerutils.randomplot";
private final PlayerUtils plugin;
private final MiniMessage miniMessage = MiniMessage.miniMessage();
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
if (!(commandSender instanceof Player player)) {
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY);
return true;
}
String worldName = player.getWorld().getName();
if (!Config.RANDOM_PLOT.ALLOWED_WORLDS.contains(worldName)) {
player.sendRichMessage(Messages.RANDOM_PLOT.WORLD_NOT_ALLOWED);
return true;
}
if (!player.hasPermission(PERMISSION)) {
player.sendRichMessage(Messages.GENERIC.NO_PERMISSION, Placeholder.parsed("permission", PERMISSION));
return true;
}
if (!Bukkit.getPluginManager().isPluginEnabled("PlotSquared")) {
player.sendRichMessage(Messages.RANDOM_PLOT.PLOT_SQUARED_NOT_ENABLED);
return true;
}
List<Plot> plots = collectPlots(worldName);
if (plots.isEmpty()) {
player.sendRichMessage(Messages.RANDOM_PLOT.NO_PLOTS_FOUND);
return true;
}
Plot target = plots.get(ThreadLocalRandom.current().nextInt(plots.size()));
player.sendRichMessage(Messages.RANDOM_PLOT.TELEPORT_START);
startCountdown(player, target);
return true;
}
// ── PlotSquared helpers ───────────────────────────────────────────────────
private List<Plot> collectPlots(String worldName) {
try {
return PlotSquared.get().getPlotAreaManager().getPlotAreasSet(worldName)
.stream()
.map(PlotArea::getPlots)
.flatMap(Collection::stream)
.toList();
} catch (Exception e) {
log.error("Failed to retrieve plots from PlotSquared for world '{}'", worldName, e);
}
return List.of();
}
private void teleportToPlot(Player player, Plot plot) {
try {
BukkitPlayer bPlayer = BukkitUtil.adapt(player);
plot.teleportPlayer(bPlayer, TeleportCause.PLUGIN, success -> {
if (Boolean.TRUE.equals(success)) {
player.sendRichMessage(Messages.RANDOM_PLOT.TELEPORT_SUCCESS,
Placeholder.parsed("plot_id", plot.getId().toSeparatedString(";")),
Placeholder.parsed("plot_owner", getOwner(plot)));
}
});
} catch (Exception e) {
log.error("Failed to teleport {} to plot {}", player.getName(), plot.getId(), e);
}
}
private String getOwner(Plot plot) {
String ownerName;
UUID owner = plot.getOwner();
if (owner == null) {
ownerName = "Unknown";
} else {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(owner);
ownerName = offlinePlayer.getName();
if (ownerName == null) {
ownerName = "Unknown";
}
}
return ownerName;
}
// ── Countdown ─────────────────────────────────────────────────────────────
private void startCountdown(Player player, Plot plot) {
final Location origin = player.getLocation().clone();
final int[] secondsLeft = {Config.RANDOM_PLOT.COUNTDOWN_SECONDS};
new BukkitRunnable() {
@Override
public void run() {
if (!player.isOnline()) {
cancel();
return;
}
// Movement check — cancel if player moved more than 0.5 blocks on any axis
Location current = player.getLocation();
if (Math.abs(current.getX() - origin.getX()) > 0.5
|| Math.abs(current.getY() - origin.getY()) > 0.5
|| Math.abs(current.getZ() - origin.getZ()) > 0.5) {
cancel();
player.sendRichMessage(Messages.RANDOM_PLOT.TELEPORT_CANCELLED);
return;
}
if (secondsLeft[0] <= 0) {
cancel();
teleportToPlot(player, plot);
return;
}
// Show countdown title for current second
String rawTitle = Config.RANDOM_PLOT.COUNTDOWN_TITLES
.getOrDefault(secondsLeft[0], String.valueOf(secondsLeft[0]));
Component title = miniMessage.deserialize(rawTitle);
Component subtitle = miniMessage.deserialize(Config.RANDOM_PLOT.COUNTDOWN_SUBTITLE);
player.showTitle(Title.title(
title,
subtitle,
Title.Times.times(Duration.ZERO, Duration.ofMillis(1200), Duration.ofMillis(200))
));
secondsLeft[0]--;
}
}.runTaskTimer(plugin, 0L, 20L);
}
@Override
public String getName() {
return "randomplot";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return Messages.HELP.RANDOM_PLOT;
}
}
@@ -5,9 +5,11 @@ import org.bukkit.configuration.ConfigurationSection;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Set; import java.util.Set;
@Slf4j public class Config extends AbstractConfig{ @Slf4j
public class Config extends AbstractConfig {
static Config config; static Config config;
@@ -92,4 +94,46 @@ import java.util.Set;
WAYPOINT_TRANSMIT_RANGE = config.getDouble(prefix, "waypoint_transmit_range", WAYPOINT_TRANSMIT_RANGE); WAYPOINT_TRANSMIT_RANGE = config.getDouble(prefix, "waypoint_transmit_range", WAYPOINT_TRANSMIT_RANGE);
} }
} }
public static class RANDOM_PLOT {
private static final String prefix = "random-plot.";
// Command
public static List<String> ALLOWED_WORLDS = List.of("plotworld");
// Permissions
public static int COUNTDOWN_SECONDS = 3;
public static HashMap<Integer, String> COUNTDOWN_TITLES = new HashMap<>();
public static String COUNTDOWN_SUBTITLE = "<dark_grey>Preparing teleportation</dark_grey>";
@SuppressWarnings("unused")
private static void load() {
ALLOWED_WORLDS = config.getStringList(prefix, "allowed-worlds", ALLOWED_WORLDS);
COUNTDOWN_SECONDS = config.getInt(prefix + "countdown.", "seconds", COUNTDOWN_SECONDS);
COUNTDOWN_SUBTITLE = config.getString(prefix + "countdown.", "subtitle", COUNTDOWN_SUBTITLE);
// Countdown titles — integer keys map to display strings
COUNTDOWN_TITLES.clear();
ConfigurationSection titlesSection =
config.getConfigurationSection("random-plot.countdown.titles");
if (titlesSection != null) {
for (String key : titlesSection.getKeys(false)) {
try {
COUNTDOWN_TITLES.put(Integer.parseInt(key),
titlesSection.getString(key, key));
} catch (NumberFormatException ignored) {
log.warn("Invalid countdown title key: {}", key);
}
}
} else {
config.yaml.addDefault("random-plot.countdown.titles.3", "<color:#08FBFF>Go!");
config.yaml.addDefault("random-plot.countdown.titles.2", "<color:#08FBFF>2");
config.yaml.addDefault("random-plot.countdown.titles.1", "<color:#08FBFF>1");
COUNTDOWN_TITLES.put(3, "<color:#08FBFF>2");
COUNTDOWN_TITLES.put(2, "<color:#08FBFF>1");
COUNTDOWN_TITLES.put(1, "<color:#08FBFF>Go!");
}
}
}
} }
@@ -1,7 +1,5 @@
package com.alttd.playerutils.config; package com.alttd.playerutils.config;
import org.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
@@ -35,6 +33,7 @@ public class Messages extends AbstractConfig {
public static String KEY = "<green>Receive a key that you are owed: <gold>/pu key</gold></green>"; public static String KEY = "<green>Receive a key that you are owed: <gold>/pu key</gold></green>";
public static String GHAST_SPEED = "<green>Set the speed of a ghast: <gold>/pu ghastspeed <speed></gold></green>"; public static String GHAST_SPEED = "<green>Set the speed of a ghast: <gold>/pu ghastspeed <speed></gold></green>";
public static String RECOUNT_ARMOR_STANDS = "<green>Recount armor stands in current chunk: <gold>/pu recount</gold></green>"; public static String RECOUNT_ARMOR_STANDS = "<green>Recount armor stands in current chunk: <gold>/pu recount</gold></green>";
public static String RANDOM_PLOT = "<green>Get a random plot: <gold>/pu randomplot</gold></green>";
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void load() { private static void load() {
@@ -197,4 +196,25 @@ public class Messages extends AbstractConfig {
SUCCESS = config.getString(prefix, "success", SUCCESS); SUCCESS = config.getString(prefix, "success", SUCCESS);
} }
} }
public static class RANDOM_PLOT {
private static final String prefix = "random-plot.";
public static String WORLD_NOT_ALLOWED = "<red>You must be in an allowed world to use this command.</red>";
public static String PLOT_SQUARED_NOT_ENABLED = "<red>PlotSquared is not available on this server.</red>";
public static String TELEPORT_START = "<green>Starting teleport countdown...</green>";
public static String TELEPORT_SUCCESS = "<gold>You have arrived at <plot_id> by <plot_owner> random plot!</gold>";
public static String TELEPORT_CANCELLED = "<red>Teleport cancelled! You moved.</red>";
public static String NO_PLOTS_FOUND = "<red>No plots found in this world.</red>";
@SuppressWarnings("unused")
private static void load() {
WORLD_NOT_ALLOWED = config.getString(prefix, "world-not-allowed", WORLD_NOT_ALLOWED);
PLOT_SQUARED_NOT_ENABLED = config.getString(prefix, "plotsquared-not-enabled", PLOT_SQUARED_NOT_ENABLED);
TELEPORT_START = config.getString(prefix, "teleport-start", TELEPORT_START);
TELEPORT_SUCCESS = config.getString(prefix, "teleport-success", TELEPORT_SUCCESS);
TELEPORT_CANCELLED = config.getString(prefix, "teleport-cancelled", TELEPORT_CANCELLED);
NO_PLOTS_FOUND = config.getString(prefix, "no-plots-found", NO_PLOTS_FOUND);
}
}
} }
@@ -0,0 +1,105 @@
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.getMAX_BOOK_BYTES();
if (isOversizedBook) {
log.warn("Player tried to drop an oversized book");
Component message = MiniMessage.miniMessage().deserialize(
"<red>Player tried to drop an oversized book</red>");
Bukkit.broadcast(message, "staffutils.patrol");
}
return isOversizedBook;
}
private boolean isOversizedBook(ItemStack stack, HumanEntity humanEntity) {
boolean isOversizedBook = BookByteUtils.shouldCountForBookByteLimit(stack)
&& BookByteUtils.computeBytes(stack) > BookByteUtils.getMaxBookBytes(humanEntity);
if (isOversizedBook) {
log.warn("{} [{}] tried to drop an oversized book", humanEntity.getName(), humanEntity.getUniqueId());
Component message = MiniMessage.miniMessage().deserialize(
"<red>Player <player> tried to drop an oversized book</red>",
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 (event.getSource().getHolder() instanceof HumanEntity humanEntity && isOversizedBook(event.getItem(), humanEntity)) {
event.setCancelled(true);
} else if (isOversizedBook(event.getItem())) {
event.setCancelled(true);
}
}
}
@@ -0,0 +1,43 @@
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;
import org.bukkit.event.player.PlayerEditBookEvent;
import org.bukkit.inventory.meta.BookMeta;
@Slf4j
public class BookWriteEvent implements Listener {
@EventHandler
public void onPlayerEditBook(PlayerEditBookEvent event) {
Player player = event.getPlayer();
BookMeta meta = event.getNewBookMeta();
int totalBytes = BookByteUtils.computeBytes(meta);
if (totalBytes > BookByteUtils.getMaxBookBytes(player)) {
log.warn("Player {} [{}] tried to write a book with {} bytes",
player.getName(), player.getUniqueId(), totalBytes);
event.setCancelled(true);
Component message = MiniMessage.miniMessage().deserialize(
"<red>Player <player> tried to write a book with <bytes> bytes</red>",
Placeholder.unparsed("player", player.getName()), Placeholder.parsed("bytes", String.valueOf(totalBytes)));
Bukkit.broadcast(message, "staffutils.patrol");
} else if (totalBytes > BookByteUtils.getBigBookBytes(player)) {
log.warn("Player {} [{}] wrote a large book with {} bytes",
player.getName(), player.getUniqueId(), totalBytes);
Component message = MiniMessage.miniMessage().deserialize(
"<red>Player <player> wrote a book with <bytes> bytes</red>",
Placeholder.unparsed("player", player.getName()), Placeholder.parsed("bytes", String.valueOf(totalBytes)));
Bukkit.broadcast(message, "staffutils.patrol");
}
}
}
@@ -1,6 +1,7 @@
package com.alttd.playerutils.event_listeners; package com.alttd.playerutils.event_listeners;
import com.alttd.playerutils.data_objects.GHAST_SPEED; import com.alttd.playerutils.data_objects.GHAST_SPEED;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.attribute.Attribute; import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
@@ -10,15 +11,13 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDismountEvent; import org.bukkit.event.entity.EntityDismountEvent;
import org.bukkit.event.entity.EntityMountEvent; import org.bukkit.event.entity.EntityMountEvent;
import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
@Slf4j
public class GhastSpeedEvent implements Listener { public class GhastSpeedEvent implements Listener {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(GhastSpeedEvent.class);
private final HashMap<UUID, GHAST_SPEED> lastSetSpeed = new HashMap<>(); private final HashMap<UUID, GHAST_SPEED> lastSetSpeed = new HashMap<>();
public GhastSpeedEvent() { public GhastSpeedEvent() {
@@ -0,0 +1,131 @@
package com.alttd.playerutils.util;
import lombok.Getter;
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.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.BlockStateMeta;
import java.nio.charset.StandardCharsets;
/**
* Utility to compute the UTF-8 byte size of a written book's contents.
*/
public final class BookByteUtils {
private BookByteUtils() {
}
// 65,000 bytes per book (below CoreProtect hard limit ~65,535)
@Getter
private static final int MAX_BOOK_BYTES = 30_000;
@Getter
private static final int BIG_BOOK_BYTES = 10_000;
public static int getBigBookBytes(HumanEntity humanEntity) {
return calcBookBytes(humanEntity, BIG_BOOK_BYTES);
}
public static int getMaxBookBytes(HumanEntity humanEntity) {
return calcBookBytes(humanEntity, MAX_BOOK_BYTES);
}
private static int calcBookBytes(HumanEntity humanEntity, int bookBytes) {
if (humanEntity.hasPermission("playerutils.bigbook.bypass")) {
return Integer.MAX_VALUE;
} else if (humanEntity.hasPermission("playerutils.bigbook.double")) {
return bookBytes * 2;
} else if (humanEntity.hasPermission("playerutils.bigbook.quadruple")) {
return bookBytes * 4;
} else {
return bookBytes;
}
}
public static boolean shouldCountForBookByteLimit(ItemStack stack) {
if (stack == null) {
return false;
}
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;
}
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;
}
/**
* Compute the number of bytes used by the provided BookMeta.
*/
public static int computeBytes(BookMeta meta) {
if (meta == null) {
return 0;
}
int totalBytes = 0;
String title = meta.getTitle();
if (title != null) {
totalBytes += title.getBytes(StandardCharsets.UTF_8).length;
}
for (Component page : meta.pages()) {
if (page == null) {
continue;
}
String pageString = MiniMessage.miniMessage().serialize(page);
if (pageString.isEmpty()) {
continue;
}
totalBytes += pageString.getBytes(StandardCharsets.UTF_8).length;
}
return totalBytes;
}
/**
* Compute the number of bytes used by a book item stack. If the item is not a written book, returns 0.
*/
public static int computeBytes(ItemStack stack) {
if (stack == null) {
return 0;
}
// Direct written book
if (stack.getItemMeta() instanceof BookMeta meta) {
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;
}
}