diff --git a/res/plugin.yml b/res/plugin.yml index c1d5113..bde296c 100644 --- a/res/plugin.yml +++ b/res/plugin.yml @@ -15,4 +15,8 @@ commands: search: description: Search for a given item in all nearby inventories usage: / {item type} - permission: invtweaks.search \ No newline at end of file + permission: invtweaks.search + capitator: + description: Toggle tree capitation for an axe in your main hand + usage: / + permission: invtweaks.capitator \ No newline at end of file diff --git a/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java b/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java index 239736b..6221c52 100644 --- a/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java +++ b/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java @@ -1,22 +1,25 @@ package dev.w1zzrd.invtweaks; +import dev.w1zzrd.invtweaks.command.CapitatorCommand; import dev.w1zzrd.invtweaks.command.MagnetCommandExecutor; import dev.w1zzrd.invtweaks.command.SearchCommandExecutor; import dev.w1zzrd.invtweaks.command.SortCommandExecutor; -import dev.w1zzrd.invtweaks.listener.MagnetismListener; -import dev.w1zzrd.invtweaks.listener.TabCompletionListener; +import dev.w1zzrd.invtweaks.enchantment.CapitatorEnchantment; +import dev.w1zzrd.invtweaks.listener.*; import dev.w1zzrd.invtweaks.serialization.MagnetConfig; -import dev.w1zzrd.invtweaks.listener.SortListener; -import dev.w1zzrd.invtweaks.listener.StackReplaceListener; import dev.w1zzrd.invtweaks.serialization.MagnetData; import dev.w1zzrd.invtweaks.serialization.SearchConfig; import dev.w1zzrd.invtweaks.serialization.UUIDList; import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.enchantments.Enchantment; import org.bukkit.event.HandlerList; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import java.lang.reflect.Field; +import java.util.Map; import java.util.Objects; import java.util.logging.Logger; @@ -31,18 +34,24 @@ public final class InvTweaksPlugin extends JavaPlugin { public static final String LOG_PLUGIN_NAME = "[InventoryTweaks]"; private static final String PERSISTENT_DATA_NAME = "data"; + private static final String ENCHANTMENT_CAPITATOR_NAME = "Capitator"; + private final Logger logger = Bukkit.getLogger(); // Command executor references in case I need them or something idk private SortCommandExecutor sortCommandExecutor; private MagnetCommandExecutor magnetCommandExecutor; private SearchCommandExecutor searchCommandExecutor; + private CapitatorCommand capitatorCommand; private DataStore data; + private NamespacedKey capitatorEnchantmentKey; + private Enchantment capitatorEnchantment; @Override public void onEnable() { logger.fine(LOG_PLUGIN_NAME + " Plugin enabled"); + initEnchantments(); enablePersistentData(); initCommands(); initEvents(); @@ -55,6 +64,7 @@ public final class InvTweaksPlugin extends JavaPlugin { disableEvents(); disableCommands(); disablePersistentData(); + disableEnchantments(); } @Override @@ -78,18 +88,43 @@ public final class InvTweaksPlugin extends JavaPlugin { return data; } - /** - * Initialize commands registered by this plugin - */ - private void initCommands() { - sortCommandExecutor = new SortCommandExecutor(); - magnetCommandExecutor = new MagnetCommandExecutor(this, "magnet", getPersistentData()); - searchCommandExecutor = new SearchCommandExecutor(this, "search"); + private void initEnchantments() { + capitatorEnchantmentKey = new NamespacedKey(this, ENCHANTMENT_CAPITATOR_NAME); + capitatorEnchantment = new CapitatorEnchantment(ENCHANTMENT_CAPITATOR_NAME, capitatorEnchantmentKey); - // TODO: Bind command by annotation - Objects.requireNonNull(getCommand("sort")).setExecutor(sortCommandExecutor); - Objects.requireNonNull(getCommand("magnet")).setExecutor(magnetCommandExecutor); - Objects.requireNonNull(getCommand("search")).setExecutor(searchCommandExecutor); + try { + final Field acceptingField = Enchantment.class.getDeclaredField("acceptingNew"); + acceptingField.setAccessible(true); + + acceptingField.set(null, true); + + Enchantment.registerEnchantment(capitatorEnchantment); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void disableEnchantments() { + try { + final Field byKeyField = Enchantment.class.getDeclaredField("byKey"); + final Field byNameField = Enchantment.class.getDeclaredField("byName"); + + byKeyField.setAccessible(true); + byNameField.setAccessible(true); + + final Object byKey = byKeyField.get(null); + final Object byName = byNameField.get(null); + + if (byKey instanceof final Map byKeyMap && byName instanceof final Map byNameMap) { + byKeyMap.remove(capitatorEnchantmentKey); + byNameMap.remove(ENCHANTMENT_CAPITATOR_NAME); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + + capitatorEnchantment = null; + capitatorEnchantmentKey = null; } /** @@ -102,17 +137,7 @@ public final class InvTweaksPlugin extends JavaPlugin { pluginManager.registerEvents(new SortListener(), this); pluginManager.registerEvents(new MagnetismListener(magnetCommandExecutor), this); pluginManager.registerEvents(new TabCompletionListener(), this); - } - - /** - * Do whatever is necessary to disable commands and their execution - */ - private void disableCommands() { - magnetCommandExecutor.onDisable(); - - sortCommandExecutor = null; - magnetCommandExecutor = null; - searchCommandExecutor = null; + pluginManager.registerEvents(new TreeCapitatorListener(capitatorEnchantment), this); } /** @@ -123,6 +148,34 @@ public final class InvTweaksPlugin extends JavaPlugin { HandlerList.unregisterAll(this); } + /** + * Initialize commands registered by this plugin + */ + private void initCommands() { + sortCommandExecutor = new SortCommandExecutor(); + magnetCommandExecutor = new MagnetCommandExecutor(this, "magnet", getPersistentData()); + searchCommandExecutor = new SearchCommandExecutor(this, "search"); + capitatorCommand = new CapitatorCommand(capitatorEnchantment); + + // TODO: Bind command by annotation + Objects.requireNonNull(getCommand("sort")).setExecutor(sortCommandExecutor); + Objects.requireNonNull(getCommand("magnet")).setExecutor(magnetCommandExecutor); + Objects.requireNonNull(getCommand("search")).setExecutor(searchCommandExecutor); + Objects.requireNonNull(getCommand("capitator")).setExecutor(capitatorCommand); + } + + /** + * Do whatever is necessary to disable commands and their execution + */ + private void disableCommands() { + magnetCommandExecutor.onDisable(); + + capitatorCommand = null; + searchCommandExecutor = null; + magnetCommandExecutor = null; + sortCommandExecutor = null; + } + /** * Register type serializers/deserializers for configurations and YAML files * diff --git a/src/dev/w1zzrd/invtweaks/command/CapitatorCommand.java b/src/dev/w1zzrd/invtweaks/command/CapitatorCommand.java new file mode 100644 index 0000000..7910065 --- /dev/null +++ b/src/dev/w1zzrd/invtweaks/command/CapitatorCommand.java @@ -0,0 +1,37 @@ +package dev.w1zzrd.invtweaks.command; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +public class CapitatorCommand implements CommandExecutor { + private final Enchantment capitatorEnchantment; + + public CapitatorCommand(final Enchantment capitatorEnchantment) { + this.capitatorEnchantment = capitatorEnchantment; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (sender instanceof final Player caller) { + final ItemStack stack = caller.getInventory().getItemInMainHand(); + + if (stack.getType().name().endsWith("_AXE")) { + if (stack.containsEnchantment(capitatorEnchantment)) { + stack.removeEnchantment(capitatorEnchantment); + sender.spigot().sendMessage(CommandUtils.successMessage("Item is now a regular axe")); + } else { + stack.addEnchantment(capitatorEnchantment, 1); + sender.spigot().sendMessage(CommandUtils.successMessage("Item is now a capitator axe")); + } + } else + sender.spigot().sendMessage(CommandUtils.errorMessage("Only axes can be tree capitators!")); + } else + sender.spigot().sendMessage(CommandUtils.errorMessage("Only players can create tree capitators!")); + + return true; + } +} diff --git a/src/dev/w1zzrd/invtweaks/command/CommandUtils.java b/src/dev/w1zzrd/invtweaks/command/CommandUtils.java index 1d11284..5a247a1 100644 --- a/src/dev/w1zzrd/invtweaks/command/CommandUtils.java +++ b/src/dev/w1zzrd/invtweaks/command/CommandUtils.java @@ -1,6 +1,7 @@ package dev.w1zzrd.invtweaks.command; import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.command.CommandSender; @@ -16,4 +17,16 @@ public final class CommandUtils { return !condition; } + + public static BaseComponent errorMessage(final String message) { + final BaseComponent component = new TextComponent(message); + component.setColor(ChatColor.DARK_RED); + return component; + } + + public static BaseComponent successMessage(final String message) { + final BaseComponent component = new TextComponent(message); + component.setColor(ChatColor.GREEN); + return component; + } } diff --git a/src/dev/w1zzrd/invtweaks/enchantment/CapitatorEnchantment.java b/src/dev/w1zzrd/invtweaks/enchantment/CapitatorEnchantment.java new file mode 100644 index 0000000..d90e36c --- /dev/null +++ b/src/dev/w1zzrd/invtweaks/enchantment/CapitatorEnchantment.java @@ -0,0 +1,55 @@ +package dev.w1zzrd.invtweaks.enchantment; + +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.enchantments.EnchantmentTarget; +import org.bukkit.inventory.ItemStack; + +public final class CapitatorEnchantment extends Enchantment { + private final String name; + + public CapitatorEnchantment(String name, NamespacedKey key) { + super(key); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public int getMaxLevel() { + return 1; + } + + @Override + public int getStartLevel() { + return 1; + } + + @Override + public EnchantmentTarget getItemTarget() { + return EnchantmentTarget.TOOL; + } + + @Override + public boolean isTreasure() { + return false; + } + + @Override + public boolean isCursed() { + return false; + } + + @Override + public boolean conflictsWith(Enchantment other) { + return false; + } + + @Override + public boolean canEnchantItem(ItemStack item) { + return item.getType().name().endsWith("_AXE"); + } +} diff --git a/src/dev/w1zzrd/invtweaks/listener/TreeCapitatorListener.java b/src/dev/w1zzrd/invtweaks/listener/TreeCapitatorListener.java new file mode 100644 index 0000000..3a2a74f --- /dev/null +++ b/src/dev/w1zzrd/invtweaks/listener/TreeCapitatorListener.java @@ -0,0 +1,211 @@ +package dev.w1zzrd.invtweaks.listener; + +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.block.Block; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; + + +import java.util.*; + +import static org.bukkit.Material.*; + +public class TreeCapitatorListener implements Listener { + + private static final int MAX_SEARCH_BLOCKS = 69420; + private static final List targetMaterials = Arrays.asList( + ACACIA_LOG, + OAK_LOG, + BIRCH_LOG, + JUNGLE_LOG, + DARK_OAK_LOG, + SPRUCE_LOG, + + ACACIA_LEAVES, + OAK_LEAVES, + BIRCH_LEAVES, + JUNGLE_LEAVES, + DARK_OAK_LEAVES, + SPRUCE_LEAVES + ); + + private static final List leaves = Arrays.asList( + ACACIA_LEAVES, + OAK_LEAVES, + BIRCH_LEAVES, + JUNGLE_LEAVES, + DARK_OAK_LEAVES, + SPRUCE_LEAVES + ); + + private final Enchantment capitatorEnchantment; + + public TreeCapitatorListener(final Enchantment capitatorEnchantment) { + this.capitatorEnchantment = capitatorEnchantment; + } + + @EventHandler + public void onBlockBreak(final BlockBreakEvent event) { + final ItemStack handTool = event.getPlayer().getInventory().getItemInMainHand(); + if (event.isCancelled() || !handTool.containsEnchantment(capitatorEnchantment)) + return; + + if (handTool.getItemMeta() instanceof final Damageable tool) { + if (!leaves.contains(event.getBlock().getType()) && targetMaterials.contains(event.getBlock().getType())) { + int logBreakCount = 0; + + for (final Block found : findAdjacent(event.getBlock(), getMaxUses(handTool, tool))) + if (event.getPlayer().getGameMode() == GameMode.CREATIVE) + found.setType(AIR); + else { + if (!leaves.contains(found.getType())) + ++logBreakCount; + + found.breakNaturally(handTool); + } + + if (event.getPlayer().getGameMode() != GameMode.CREATIVE && !applyDamage(handTool, logBreakCount)) { + event.getPlayer().getInventory().setItemInMainHand(new ItemStack(AIR)); + event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f); + } + + event.setCancelled(true); + } + } + } + + private static int getMaxUses(final ItemStack stack, final Damageable tool) { + return ((stack.getEnchantmentLevel(Enchantment.DURABILITY) + 1) * (stack.getType().getMaxDurability()) - tool.getDamage()); + } + + private static boolean applyDamage(final ItemStack stack, final int brokenBlocks) { + final ItemMeta meta = stack.getItemMeta(); + + if (meta instanceof final Damageable toolMeta) { + + final int unbreakingDivider = stack.getEnchantmentLevel(Enchantment.DURABILITY) + 1; + final int round = brokenBlocks % unbreakingDivider; + final int dmg = (brokenBlocks / unbreakingDivider) + (round != 0 ? 1 : 0); + + if (dmg > (stack.getType().getMaxDurability() - toolMeta.getDamage())) + return false; + + toolMeta.setDamage(toolMeta.getDamage() + dmg); + + stack.setItemMeta(meta); + } + return true; + } + + private static List findAdjacent(final Block start, final int softMax) { + List frontier = new ArrayList<>(); + final List matches = new ArrayList<>(); + + frontier.add(start); + matches.add(start); + + int total = 1; + int softMaxCount = 1; + + // Keep finding blocks until we have no new matches + while (frontier.size() > 0 && total < MAX_SEARCH_BLOCKS && softMaxCount < softMax) { + final long result = addAdjacents(frontier, matches, total, softMaxCount, softMax); + total = (int) (result >>> 32); + softMaxCount = (int) (result & 0xFFFFFFFFL); + } + + return matches; + } + + private static long addAdjacents(final List frontier, final List collect, int total, int softMax, final int softMaxCap) { + final List newFrontier = new ArrayList<>(); + + OUTER: + for (final Block check : frontier) + for (int x = -1; x <= 1; ++x) + for (int y = -1; y <= 1; ++y) + for (int z = -1; z <= 1; ++z) + if ((x | y | z) != 0) { + final Block offset = offset(collect, check, x, y, z); + + if (offset != null && !binaryContains(newFrontier, offset)) { + binaryInsert(collect, offset); + binaryInsert(newFrontier, offset); + + if (!leaves.contains(offset.getType())) { + ++softMax; + + if (softMax >= softMaxCap) + break OUTER; + } + } + + // Short-circuit for max search + ++total; + if (total == MAX_SEARCH_BLOCKS) + break OUTER; + } + + frontier.clear(); + frontier.addAll(newFrontier); + + return (((long)total) << 32) | (((long) softMax) & 0xFFFFFFFFL); + } + + private static Block offset(final List checked, final Block source, int x, int y, int z) { + final Block offset = source.getWorld().getBlockAt(source.getLocation().add(x, y, z)); + + if (targetMaterials.contains(offset.getType()) && (!leaves.contains(source.getType()) || !leaves.contains(offset.getType())) && !binaryContains(checked, offset)) + return offset; + + return null; + } + + private static boolean binaryContains(final List collection, final Block find) { + return binarySearchBlock(collection, find) >= 0; + } + + private static void binaryInsert(final List collection, final Block insert) { + final int index = binarySearchBlock(collection, insert); + + if (index >= 0) + return; + + collection.add(-(index + 1), insert); + } + + private static void binaryRemove(final List collection, final Block remove) { + final int index = binarySearchBlock(collection, remove); + + if (index < 0) + return; + + collection.remove(index); + } + + private static int binarySearchBlock(final List collection, final Block find) { + return Collections.binarySearch(collection, find, TreeCapitatorListener::blockCompare); + } + + private static int blockCompare(final Block b1, final Block b2) { + return coordinateCompare(b1.getX(), b1.getY(), b1.getZ(), b2.getX(), b2.getY(), b2.getZ()); + } + private static int coordinateCompare(final int x1, final int y1, final int z1, final int x2, final int y2, final int z2) { + final int x = Integer.compare(x1, x2); + if (x != 0) + return x; + + final int y = Integer.compare(y1, y2); + if (y != 0) + return y; + + return Integer.compare(z1, z2); + } +}