From fdc7d21685e1fd81aa5926fbfc24544237ce57e7 Mon Sep 17 00:00:00 2001 From: YouHaveTrouble Date: Sun, 27 Sep 2026 21:20:27 +0200 Subject: [PATCH] First version of tunnelling enchant (#4) * some work on tunnelling enchant * first working version of tunnelling --- .../youhavetrouble/enchantio/Enchantio.java | 3 + .../enchantio/EnchantioBootstrap.java | 18 ++ .../enchantio/EnchantioConfig.java | 31 ++++ .../enchantio/enchants/EnchantioEnchant.java | 12 ++ .../enchantio/enchants/TunnellingEnchant.java | 171 ++++++++++++++++++ .../listeners/TunnellingListener.java | 140 ++++++++++++++ 6 files changed, 375 insertions(+) create mode 100644 src/main/java/me/youhavetrouble/enchantio/enchants/TunnellingEnchant.java create mode 100644 src/main/java/me/youhavetrouble/enchantio/listeners/TunnellingListener.java diff --git a/src/main/java/me/youhavetrouble/enchantio/Enchantio.java b/src/main/java/me/youhavetrouble/enchantio/Enchantio.java index 250d43f..047e4ec 100644 --- a/src/main/java/me/youhavetrouble/enchantio/Enchantio.java +++ b/src/main/java/me/youhavetrouble/enchantio/Enchantio.java @@ -49,6 +49,9 @@ public final class Enchantio extends JavaPlugin { if (EnchantioConfig.ENCHANTS.containsKey(WardEnchant.KEY)) { getServer().getPluginManager().registerEvents(new WardListener(), this); } + if (EnchantioConfig.ENCHANTS.containsKey(TunnellingEnchant.KEY)) { + getServer().getPluginManager().registerEvents(new TunnellingListener(), this); + } if (EnchantioConfig.ENCHANTS.containsKey(PanicEnchant.KEY)) { getServer().getPluginManager().registerEvents(new PanicListener(), this); diff --git a/src/main/java/me/youhavetrouble/enchantio/EnchantioBootstrap.java b/src/main/java/me/youhavetrouble/enchantio/EnchantioBootstrap.java index abd1cf2..aa21951 100644 --- a/src/main/java/me/youhavetrouble/enchantio/EnchantioBootstrap.java +++ b/src/main/java/me/youhavetrouble/enchantio/EnchantioBootstrap.java @@ -6,14 +6,20 @@ import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.registry.TypedKey; import io.papermc.paper.registry.event.RegistryEvents; +import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; +import io.papermc.paper.registry.tag.TagKey; +import io.papermc.paper.tag.TagEntry; import me.youhavetrouble.enchantio.enchants.EnchantioEnchant; +import net.kyori.adventure.key.Key; +import org.bukkit.block.BlockType; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Collection; +import java.util.Map; import java.util.Set; @SuppressWarnings("UnstableApiUsage") @@ -42,6 +48,18 @@ public class EnchantioBootstrap implements PluginBootstrap { } })); + context.getLifecycleManager().registerEventHandler(LifecycleEvents.TAGS.preFlatten(RegistryKey.BLOCK).newHandler((event) -> { + for (EnchantioEnchant enchant : enchantioEnchants) { + for (Map.Entry>> entry : enchant.getBlockTagsToRegister().entrySet()) { + logger.info("Registering block tag {}", entry.getKey()); + event.registrar().addToTag( + BlockTypeTagKeys.create(entry.getKey()), + entry.getValue() + ); + } + } + })); + context.getLifecycleManager().registerEventHandler(RegistryEvents.ENCHANTMENT.compose().newHandler(event -> { for (EnchantioEnchant enchant : enchantioEnchants) { logger.info("Registering enchantment {}", enchant.getKey()); diff --git a/src/main/java/me/youhavetrouble/enchantio/EnchantioConfig.java b/src/main/java/me/youhavetrouble/enchantio/EnchantioConfig.java index 90e1f62..0445dd6 100644 --- a/src/main/java/me/youhavetrouble/enchantio/EnchantioConfig.java +++ b/src/main/java/me/youhavetrouble/enchantio/EnchantioConfig.java @@ -2,12 +2,14 @@ package me.youhavetrouble.enchantio; import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.registry.TypedKey; +import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys; import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; import io.papermc.paper.registry.tag.TagKey; import io.papermc.paper.tag.TagEntry; import me.youhavetrouble.enchantio.enchants.*; import net.kyori.adventure.key.Key; +import org.bukkit.block.BlockType; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -78,6 +80,9 @@ public class EnchantioConfig { ConfigurationSection wardSection = getConfigSection(enchantsSection, "ward"); WardEnchant.create(wardSection); + ConfigurationSection tunnellingSection = getConfigSection(enchantsSection, "tunnelling"); + TunnellingEnchant.create(tunnellingSection); + ConfigurationSection cursesSection = getConfigSection(configuration, "curses"); migrateEnchantTags(cursesSection); @@ -210,6 +215,32 @@ public class EnchantioConfig { return enchantTagKeys; } + public static Set> getBlockTagKeysFromList(@NotNull List tags) { + Set> blockTagKeys = new HashSet<>(); + for (String blockTag : tags) { + if (blockTag == null) continue; + if (blockTag.startsWith("#")) { + blockTag = blockTag.substring(1); + try { + Key key = Key.key(blockTag); + TagKey tagKey = BlockTypeTagKeys.create(key); + TagEntry tagEntry = TagEntry.tagEntry(tagKey); + blockTagKeys.add(tagEntry); + } catch (IllegalArgumentException e) { + } + continue; + } + try { + Key key = Key.key(blockTag); + TypedKey typedKey = TypedKey.create(RegistryKey.BLOCK, key); + TagEntry tagEntry = TagEntry.valueEntry(typedKey); + blockTagKeys.add(tagEntry); + } catch (IllegalArgumentException | NullPointerException e) { + } + } + return blockTagKeys; + } + public static ConfigurationSection getConfigSection(ConfigurationSection section, String key) { ConfigurationSection value = section.getConfigurationSection(key); if (value == null) { diff --git a/src/main/java/me/youhavetrouble/enchantio/enchants/EnchantioEnchant.java b/src/main/java/me/youhavetrouble/enchantio/enchants/EnchantioEnchant.java index b82debc..342efb5 100644 --- a/src/main/java/me/youhavetrouble/enchantio/enchants/EnchantioEnchant.java +++ b/src/main/java/me/youhavetrouble/enchantio/enchants/EnchantioEnchant.java @@ -1,5 +1,6 @@ package me.youhavetrouble.enchantio.enchants; +import com.google.common.collect.ImmutableMap; import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.registry.TypedKey; import io.papermc.paper.registry.data.EnchantmentRegistryEntry; @@ -7,6 +8,7 @@ import io.papermc.paper.registry.tag.TagKey; import io.papermc.paper.tag.TagEntry; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; +import org.bukkit.block.BlockType; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.EquipmentSlotGroup; @@ -15,6 +17,8 @@ import org.bukkit.inventory.ItemType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.Map; import java.util.Set; @SuppressWarnings("UnstableApiUsage") @@ -57,6 +61,14 @@ public interface EnchantioEnchant { return TagEntry.valueEntry(TypedKey.create(RegistryKey.ENCHANTMENT, getKey())); } + /** + * Get block tags to register to use for this enchantment and what tags/blocks it should contain + */ + @NotNull + default Map>> getBlockTagsToRegister() { + return ImmutableMap.of(); + } + static @Nullable ItemStack findFirstWithEnchant( @NotNull EntityEquipment equipment, @NotNull Enchantment enchantment diff --git a/src/main/java/me/youhavetrouble/enchantio/enchants/TunnellingEnchant.java b/src/main/java/me/youhavetrouble/enchantio/enchants/TunnellingEnchant.java new file mode 100644 index 0000000..e8688cd --- /dev/null +++ b/src/main/java/me/youhavetrouble/enchantio/enchants/TunnellingEnchant.java @@ -0,0 +1,171 @@ +package me.youhavetrouble.enchantio.enchants; + +import io.papermc.paper.registry.data.EnchantmentRegistryEntry; +import io.papermc.paper.registry.tag.TagKey; +import io.papermc.paper.tag.TagEntry; +import me.youhavetrouble.enchantio.EnchantioConfig; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import org.bukkit.block.BlockType; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.EquipmentSlotGroup; +import org.bukkit.inventory.ItemType; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +import static me.youhavetrouble.enchantio.EnchantioConfig.ENCHANTS; + +@SuppressWarnings("UnstableApiUsage") +public class TunnellingEnchant implements EnchantioEnchant { + + public static final Key KEY = Key.key("enchantio", "tunnelling"); + + public static final Key AFFECTED_BLOCKS_KEY = Key.key("enchantio", "affected_by_tunnelling"); + + private final int anvilCost, weight, maxLevel, blocksPerLevel; + private final boolean canBreakToolIncompatibleBlocks; + private final EnchantmentRegistryEntry.EnchantmentCost minimumCost; + private final EnchantmentRegistryEntry.EnchantmentCost maximumCost; + private final Set> supportedItemTags = new HashSet<>(); + private final Set> enchantTagKeys = new HashSet<>(); + private final Map>> affectedBlockTags = new HashMap<>(); + + private TunnellingEnchant( + int anvilCost, + int weight, + EnchantmentRegistryEntry.EnchantmentCost minimumCost, + EnchantmentRegistryEntry.EnchantmentCost maximumCost, + Collection> enchantTagKeys, + Collection> supportedItemTags, + Map>> affectedBlockTags, + int maxLevel, + int blocksPerLevel, + boolean canBreakToolIncompatibleBlocks + ) { + this.anvilCost = anvilCost; + this.weight = weight; + this.minimumCost = minimumCost; + this.maximumCost = maximumCost; + this.maxLevel = maxLevel; + this.blocksPerLevel = blocksPerLevel; + this.canBreakToolIncompatibleBlocks = canBreakToolIncompatibleBlocks; + this.supportedItemTags.addAll(supportedItemTags); + this.enchantTagKeys.addAll(enchantTagKeys); + this.affectedBlockTags.putAll(affectedBlockTags); + } + + @Override + public @NotNull Key getKey() { + return KEY; + } + + @Override + public @NotNull Component getDescription() { + return Component.translatable("enchantio.enchant.tunnelling", "Tunnelling"); + } + + @Override + public int getAnvilCost() { + return anvilCost; + } + + @Override + public int getMaxLevel() { + return maxLevel; + } + + @Override + public int getWeight() { + return weight; + } + + @Override + public EnchantmentRegistryEntry.@NotNull EnchantmentCost getMinimumCost() { + return minimumCost; + } + + @Override + public EnchantmentRegistryEntry.@NotNull EnchantmentCost getMaximumCost() { + return maximumCost; + } + + @Override + public @NotNull Iterable getActiveSlots() { + return Set.of(EquipmentSlotGroup.MAINHAND); + } + + @Override + public @NotNull Set> getSupportedItems() { + return supportedItemTags; + } + + @Override + public @NotNull Set> getEnchantTagKeys() { + return Collections.unmodifiableSet(enchantTagKeys); + } + + @Override + public @NotNull Map>> getBlockTagsToRegister() { + return affectedBlockTags; + } + + public int getBlocksPerLevel() { + return blocksPerLevel; + } + + public boolean canBreakToolIncompatibleBlocks() { + return canBreakToolIncompatibleBlocks; + } + + public static TunnellingEnchant create(ConfigurationSection configurationSection) { + TunnellingEnchant tunnellingEnchant = new TunnellingEnchant( + EnchantioConfig.getInt(configurationSection, "anvilCost", 1), + EnchantioConfig.getInt(configurationSection, "weight", 10), + EnchantmentRegistryEntry.EnchantmentCost.of( + EnchantioConfig.getInt(configurationSection, "minimumCost.base", 40), + EnchantioConfig.getInt(configurationSection, "minimumCost.additionalPerLevel", 3) + ), + EnchantmentRegistryEntry.EnchantmentCost.of( + EnchantioConfig.getInt(configurationSection, "maximumCost.base", 65), + EnchantioConfig.getInt(configurationSection, "maximumCost.additionalPerLevel", 1) + ), + EnchantioConfig.getEnchantmentTagKeysFromList(EnchantioConfig.getStringList( + configurationSection, + "enchantmentTags", + List.of("#in_enchanting_table") + )), + EnchantioConfig.getItemTagEntriesFromList(EnchantioConfig.getStringList( + configurationSection, + "supportedItemTags", + List.of( + "#minecraft:enchantable/mining" + ) + )), + Map.of( + AFFECTED_BLOCKS_KEY, + EnchantioConfig.getBlockTagKeysFromList(EnchantioConfig.getStringList( + configurationSection, + "affectedBlockTags", + List.of( + "#minecraft:base_stone_overworld", + "#minecraft:base_stone_nether", + "#minecraft:ores", + "minecraft:end_stone" + ) + )) + ), + EnchantioConfig.getInt(configurationSection, "maxLevel", 1), + EnchantioConfig.getInt(configurationSection, "blocksPerLevel", 1), + EnchantioConfig.getBoolean(configurationSection, "canBreakToolIncompatibleBlocks", false) + ); + + if (EnchantioConfig.getBoolean(configurationSection, "enabled", true)) { + ENCHANTS.put(TunnellingEnchant.KEY, tunnellingEnchant); + } + + return tunnellingEnchant; + } + +} diff --git a/src/main/java/me/youhavetrouble/enchantio/listeners/TunnellingListener.java b/src/main/java/me/youhavetrouble/enchantio/listeners/TunnellingListener.java new file mode 100644 index 0000000..220dd67 --- /dev/null +++ b/src/main/java/me/youhavetrouble/enchantio/listeners/TunnellingListener.java @@ -0,0 +1,140 @@ +package me.youhavetrouble.enchantio.listeners; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; +import me.youhavetrouble.enchantio.Enchantio; +import me.youhavetrouble.enchantio.EnchantioConfig; +import me.youhavetrouble.enchantio.enchants.EnchantioEnchant; +import me.youhavetrouble.enchantio.enchants.TunnellingEnchant; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockType; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@SuppressWarnings("UnstableApiUsage") +public class TunnellingListener implements Listener { + + private final static Set blockBreakSkips = new HashSet<>(); + private final Enchantment tunnelling = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).get(TunnellingEnchant.KEY); + private final NamespacedKey tunnellingBlockFaceKey = new NamespacedKey("enchantio", "tunellingblockface"); + + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTunnelBlockSideCheck(PlayerInteractEvent event) { + if (tunnelling == null) return; + if (event.getHand() != EquipmentSlot.HAND) return; + if (event.getAction() != Action.LEFT_CLICK_BLOCK) return; + Player player = event.getPlayer(); + if (player.isSneaking() || GameMode.CREATIVE.equals(player.getGameMode())) return; + ItemStack item = event.getItem(); + if (item == null) return; + if (!item.containsEnchantment(tunnelling)) return; + event.getPlayer().getPersistentDataContainer().set(tunnellingBlockFaceKey, PersistentDataType.STRING, event.getBlockFace().toString()); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onTunnelBlockBreak(BlockBreakEvent event) { + if (tunnelling == null) return; + Player player = event.getPlayer(); + if (GameMode.CREATIVE.equals(player.getGameMode())) return; + if (player.isSneaking()) return; + String rawFace = player.getPersistentDataContainer().get(tunnellingBlockFaceKey, PersistentDataType.STRING); + if (rawFace == null) return; + EnchantioEnchant enchant = EnchantioConfig.ENCHANTS.get(TunnellingEnchant.KEY); + if (!(enchant instanceof TunnellingEnchant tunnellingEnchant)) return; + ItemStack item = event.getPlayer().getInventory().getItemInMainHand(); + int enchantLevel = item.getEnchantmentLevel(tunnelling); + if (enchantLevel <= 0) return; + Block block = event.getBlock(); + BlockType blockType = block.getType().asBlockType(); + if (!Registry.BLOCK.getTagValues(BlockTypeTagKeys.create(TunnellingEnchant.AFFECTED_BLOCKS_KEY)).contains(blockType)) return; + if (blockBreakSkips.contains(block)) return; + BlockFace blockFace; + try { + blockFace = BlockFace.valueOf(rawFace); + } catch (IllegalArgumentException e) { + return; + } + Set blocksToBreak = getSquare(block, blockFace, tunnellingEnchant.getBlocksPerLevel() * enchantLevel); + blockBreakSkips.addAll(blocksToBreak); + for (Block b : blocksToBreak) { + BlockBreakEvent breakEvent = new BlockBreakEvent(b, player); + Enchantio.getPlugin(Enchantio.class).getServer().getPluginManager().callEvent(breakEvent); + blockBreakSkips.remove(b); + breakBlock(breakEvent, tunnellingEnchant); + } + + } + + private Set getSquare(Block block, BlockFace face, int range) { + Set blocks = new HashSet<>(); + switch (face) { + case DOWN: + case UP: + blocks.addAll(getBlocksToTunnel(block.getLocation(), range, 0, range)); + break; + case EAST: + case WEST: + blocks.addAll(getBlocksToTunnel(block.getLocation(), 0, range, range)); + break; + case NORTH: + case SOUTH: + blocks.addAll(getBlocksToTunnel(block.getLocation(), range, range, 0)); + break; + default: + break; + } + return blocks; + } + + /** + * Get all blocks that will be broken by Tunnelling enchantment + * @param base location at the centre of selection + * @return All blocks that will get broken + */ + private List getBlocksToTunnel(Location base, int changeX, int changeY, int changeZ) { + List blocks = new ArrayList<>(); + for (int x = (base.getBlockX() - changeX); x <= (base.getBlockX() + changeX); x++) { + for (int y = (base.getBlockY() - changeY); y <= (base.getBlockY() + changeY); y++) { + for (int z = (base.getBlockZ() - changeZ); z <= (base.getBlockZ() + changeZ); z++) { + Location loc = new Location(base.getWorld(), x, y, z); + if (base.equals(loc)) continue; // ignore the base block + Block block = loc.getBlock(); + BlockType blockType = block.getType().asBlockType(); + if (!Registry.BLOCK.getTagValues(BlockTypeTagKeys.create(TunnellingEnchant.AFFECTED_BLOCKS_KEY)).contains(blockType)) continue; + if (block.getType().isAir()) continue; + blocks.add(block); + } + } + } + return blocks; + } + + private void breakBlock(BlockBreakEvent event, TunnellingEnchant enchant) { + if (tunnelling == null) return; + if (event.isCancelled()) return; + Block block = event.getBlock(); + ItemStack item = event.getPlayer().getInventory().getItemInMainHand(); + int enchantLevel = item.getEnchantmentLevel(tunnelling); + if (enchantLevel <= 0) return; + block.breakNaturally(item, true, event.getExpToDrop() > 0, enchant.canBreakToolIncompatibleBlocks()); + item.damage(1, event.getPlayer()); + } + +}