Compare commits

...

3 Commits

7 changed files with 400 additions and 3 deletions

View File

@ -1,6 +1,7 @@
package com.alttd.playerutils;
import com.alttd.playerutils.commands.PlayerUtilsCommand;
import com.alttd.playerutils.commands.playerutils_subcommands.AprilFools;
import com.alttd.playerutils.commands.playerutils_subcommands.GhastSpeed;
import com.alttd.playerutils.commands.playerutils_subcommands.RotateBlock;
import com.alttd.playerutils.config.Config;
@ -13,12 +14,17 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.util.concurrent.TimeUnit;
import com.alttd.playerutils.util.AprilFoolsPrank;
public final class PlayerUtils extends JavaPlugin {
private PlayerUtilsCommand playerUtilsCommand;
private AprilFoolsPrank aprilFoolsPrank;
@Override
public void onEnable() {
// initialize prank utility
aprilFoolsPrank = new AprilFoolsPrank(this);
registerCommands();
registerEvents();
reloadConfigs();
@ -32,6 +38,8 @@ public final class PlayerUtils extends JavaPlugin {
private void registerCommands() {
playerUtilsCommand = new PlayerUtilsCommand(this);
// add april fools test command
playerUtilsCommand.addSubCommand(new AprilFools(aprilFoolsPrank));
}
private void registerEvents() {
@ -42,6 +50,8 @@ public final class PlayerUtils extends JavaPlugin {
pluginManager.registerEvents(new LimitArmorStands(this), this);
pluginManager.registerEvents(new BlockBlockUseEvent(), this);
pluginManager.registerEvents(new PlayerJoin(this), this);
pluginManager.registerEvents(new BookWriteEvent(), this);
pluginManager.registerEvents(new BookByteLimitListener(), this);
RotateBlockEvent rotateBlockEvent = new RotateBlockEvent();
pluginManager.registerEvents(rotateBlockEvent, this);
@ -59,7 +69,11 @@ public final class PlayerUtils extends JavaPlugin {
}
private void registerSchedulers() {
// periodic key storage save (async)
Bukkit.getScheduler().runTaskTimerAsynchronously(this, KeyStorage.STORAGE::save,
TimeUnit.MINUTES.toSeconds(5) * 20, TimeUnit.MINUTES.toSeconds(5) * 20);
// April 1st prank scheduler
aprilFoolsPrank.schedule();
}
}

View File

@ -0,0 +1,48 @@
package com.alttd.playerutils.commands.playerutils_subcommands;
import com.alttd.playerutils.commands.SubCommand;
import com.alttd.playerutils.config.Messages;
import com.alttd.playerutils.util.AprilFoolsPrank;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
public class AprilFools extends SubCommand {
private final AprilFoolsPrank prank;
public AprilFools(AprilFoolsPrank prank) {
this.prank = prank;
}
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
if (!(commandSender instanceof Player player)) {
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY);
return true;
}
boolean ok = prank.playExplosionAround(player);
if (ok) {
commandSender.sendRichMessage("<green>April Fools test triggered. Listen closely...");
} else {
commandSender.sendRichMessage("<red>Failed to trigger. You must be in the overworld named 'world'.");
}
return true;
}
@Override
public String getName() {
return "aprilfools";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return "<gray>/playerutils aprilfools</gray> <dark_gray>-</dark_gray> <gray>Play a fake explosion near you (testing).";
}
}

View File

@ -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(
"<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.MAX_BOOK_BYTES;
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 (isOversizedBook(event.getItem())) {
event.setCancelled(true);
}
}
}

View File

@ -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.MAX_BOOK_BYTES) {
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.BIG_BOOK_BYTES) {
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");
}
}
}

View File

@ -1,6 +1,7 @@
package com.alttd.playerutils.event_listeners;
import com.alttd.playerutils.data_objects.GHAST_SPEED;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Entity;
@ -10,15 +11,13 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDismountEvent;
import org.bukkit.event.entity.EntityMountEvent;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.UUID;
@Slf4j
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<>();
public GhastSpeedEvent() {

View File

@ -0,0 +1,84 @@
package com.alttd.playerutils.util;
import com.alttd.playerutils.PlayerUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Encapsulates the April Fools' prank logic.
* - schedule(): registers the timed prank that only runs on April 1st and only during overworld night.
* - playExplosionAround(Player): immediately plays the explosion sound around the target for testing (no date/time checks),
* but still requires the target to be in the overworld named "world" to match intended environment.
*/
public class AprilFoolsPrank {
private final PlayerUtils plugin;
public AprilFoolsPrank(PlayerUtils plugin) {
this.plugin = plugin;
}
/**
* Register the timed prank task. Safe to call on the main thread during plugin enable.
*/
public void schedule() {
// April 1st prank: during overworld night, every 2 minutes pick one player and play an explosion somewhere in a 30-block radius
Bukkit.getScheduler().runTaskTimer(plugin, () -> {
LocalDate now = LocalDate.now(ZoneId.systemDefault());
if (now.getMonth() != Month.APRIL || now.getDayOfMonth() != 1) {
return; // only active on April 1st
}
// World world = Bukkit.getWorld("world");
World world = Bukkit.getWorld("lobby");
if (world == null) return; // overworld not present
long time = world.getTime() % 24000L;
if (time < 13000L || time > 23000L) {
return; // only at night
}
List<Player> players = world.getPlayers();
if (players.isEmpty()) return;
Player target = players.get(ThreadLocalRandom.current().nextInt(players.size()));
playOnce(world, target);
}, 20L, 20L * 60L * 2L); // start after 1s, repeat every 2 minutes
}
/**
* Trigger the prank once around the given player for testing. Returns true if executed.
* This method ignores the date and time checks so it can be tested easily, but it still
* requires the player to be in the overworld named "world".
*/
public boolean playExplosionAround(Player target) {
if (target == null) return false;
World world = target.getWorld();
// if (!"world".equalsIgnoreCase(world.getName())) {
if (!"lobby".equalsIgnoreCase(world.getName())) {
return false; // only intended for overworld
}
playOnce(world, target);
return true;
}
private void playOnce(World world, Player target) {
Location base = target.getLocation();
double radius = 30.0;
double r = ThreadLocalRandom.current().nextDouble(radius);
double theta = ThreadLocalRandom.current().nextDouble(Math.PI * 2);
double dx = r * Math.cos(theta);
double dz = r * Math.sin(theta);
Location soundLoc = new Location(world, base.getX() + dx, base.getY(), base.getZ() + dz);
world.playSound(soundLoc, Sound.ENTITY_GENERIC_EXPLODE, 0.8f, 1.0f);
}
}

View File

@ -0,0 +1,106 @@
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;
/**
* 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)
public static final int MAX_BOOK_BYTES = 30_000;
public static final int BIG_BOOK_BYTES = 10_000;
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;
}
}