From a915d5ad80457860681ab5b989b2fdd099bd2bf1 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Sun, 22 Sep 2024 20:12:56 +0200 Subject: [PATCH 1/6] Compatibility with most plugin using wesjd anvil gui --- .../cuanvil/dependency/gui/ExternGuiTester.kt | 24 +++++++++ .../gui/version/v1_17R1_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_18R1_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_18R2_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_19R1_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_19R2_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_19R3_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_20R1_ExternGuiTester.kt | 16 ++++++ .../gui/version/v1_20R2_ExternGuiTester.kt | 17 +++++++ .../gui/version/v1_20R3_ExternGuiTester.kt | 17 +++++++ .../gui/version/v1_20R4_ExternGuiTester.kt | 17 +++++++ .../gui/version/v1_21R1_ExternGuiTester.kt | 16 ++++++ .../io/delilaheve/AnvilEventListener.kt | 2 +- .../cuanvil/dependency/DependencyManager.kt | 12 +++++ .../dependency/gui/GuiTesterSelector.kt | 51 +++++++++++++++++++ 15 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt create mode 100644 nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_17R1_ExternGuiTester.kt create mode 100644 nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R1_ExternGuiTester.kt create mode 100644 nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R2_ExternGuiTester.kt create mode 100644 nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R1_ExternGuiTester.kt create mode 100644 nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R2_ExternGuiTester.kt create mode 100644 nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R3_ExternGuiTester.kt create mode 100644 nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R1_ExternGuiTester.kt create mode 100644 nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R2_ExternGuiTester.kt create mode 100644 nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R3_ExternGuiTester.kt create mode 100644 nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R4_ExternGuiTester.kt create mode 100644 nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_21R1_ExternGuiTester.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt diff --git a/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt new file mode 100644 index 0000000..3e11a43 --- /dev/null +++ b/nms/nms-common/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/ExternGuiTester.kt @@ -0,0 +1,24 @@ +package xyz.alexcrea.cuanvil.dependency.gui + +import org.bukkit.inventory.InventoryView + +interface ExternGuiTester { + + val wesjdAnvilGuiName: String? + + fun getContainerClass(inventory: InventoryView): Class? + + fun testIfGui(inventory: InventoryView): Boolean { + val clazz = getContainerClass(inventory) + if(clazz == null) return false + + val expectedWesjdGuiPath = "anvilgui.version.$wesjdAnvilGuiName" + + val clazzName = clazz.name + val isWejdsGui = clazzName.contains(expectedWesjdGuiPath) + + return isWejdsGui + } + + +} \ No newline at end of file diff --git a/nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_17R1_ExternGuiTester.kt b/nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_17R1_ExternGuiTester.kt new file mode 100644 index 0000000..8e352e0 --- /dev/null +++ b/nms/v1_17R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_17R1_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_17R1_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_17_R1" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} \ No newline at end of file diff --git a/nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R1_ExternGuiTester.kt b/nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R1_ExternGuiTester.kt new file mode 100644 index 0000000..659a0f6 --- /dev/null +++ b/nms/v1_18R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R1_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_18_R1.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_18R1_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_18_R1" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} \ No newline at end of file diff --git a/nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R2_ExternGuiTester.kt b/nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R2_ExternGuiTester.kt new file mode 100644 index 0000000..1447716 --- /dev/null +++ b/nms/v1_18R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_18R2_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_18_R2.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_18R2_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_18_R2" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} \ No newline at end of file diff --git a/nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R1_ExternGuiTester.kt b/nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R1_ExternGuiTester.kt new file mode 100644 index 0000000..c151924 --- /dev/null +++ b/nms/v1_19R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R1_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_19_R1.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_19R1_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_19_R1" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} \ No newline at end of file diff --git a/nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R2_ExternGuiTester.kt b/nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R2_ExternGuiTester.kt new file mode 100644 index 0000000..ac46674 --- /dev/null +++ b/nms/v1_19R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R2_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_19_R2.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_19R2_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_19_R2" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} \ No newline at end of file diff --git a/nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R3_ExternGuiTester.kt b/nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R3_ExternGuiTester.kt new file mode 100644 index 0000000..7ce5abd --- /dev/null +++ b/nms/v1_19R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_19R3_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_19_R3.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_19R3_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_19_R3" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} \ No newline at end of file diff --git a/nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R1_ExternGuiTester.kt b/nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R1_ExternGuiTester.kt new file mode 100644 index 0000000..dae3b98 --- /dev/null +++ b/nms/v1_20R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R1_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_20R1_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_20_R1" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} diff --git a/nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R2_ExternGuiTester.kt b/nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R2_ExternGuiTester.kt new file mode 100644 index 0000000..6a8358a --- /dev/null +++ b/nms/v1_20R2/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R2_ExternGuiTester.kt @@ -0,0 +1,17 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester +import kotlin.jvm.javaClass + +class v1_20R2_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_20_R2" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} diff --git a/nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R3_ExternGuiTester.kt b/nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R3_ExternGuiTester.kt new file mode 100644 index 0000000..80362e2 --- /dev/null +++ b/nms/v1_20R3/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R3_ExternGuiTester.kt @@ -0,0 +1,17 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester +import kotlin.jvm.javaClass + +class v1_20R3_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_20_R3" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} diff --git a/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R4_ExternGuiTester.kt b/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R4_ExternGuiTester.kt new file mode 100644 index 0000000..16e867c --- /dev/null +++ b/nms/v1_20R4/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_20R4_ExternGuiTester.kt @@ -0,0 +1,17 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester +import kotlin.jvm.javaClass + +class v1_20R4_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_20_R4" + + override fun getContainerClass(view: InventoryView): Class? { + if (view !is CraftInventoryView) return null + val container = view.handle + + return container.javaClass + } +} diff --git a/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_21R1_ExternGuiTester.kt b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_21R1_ExternGuiTester.kt new file mode 100644 index 0000000..9b73bc4 --- /dev/null +++ b/nms/v1_21R1/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/version/v1_21R1_ExternGuiTester.kt @@ -0,0 +1,16 @@ +package xyz.alexcrea.cuanvil.dependency.gui.version + +import org.bukkit.craftbukkit.inventory.CraftInventoryView +import org.bukkit.inventory.InventoryView +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester + +class v1_21R1_ExternGuiTester: ExternGuiTester { + override val wesjdAnvilGuiName = "Wrapper1_21_R1" + + override fun getContainerClass(view: InventoryView): Class? { + if(view !is CraftInventoryView<*>) return null + val container = view.handle + + return container.javaClass + } +} diff --git a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt index e2f92e9..bdb1ac9 100644 --- a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt +++ b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt @@ -56,7 +56,7 @@ class AnvilEventListener(private val packetManager: PacketManager) : Listener { /** * Event handler logic for when an anvil contains items to be combined */ - @EventHandler(priority = HIGHEST) + @EventHandler(priority = HIGHEST, ignoreCancelled = true) fun anvilCombineCheck(event: PrepareAnvilEvent) { // Test if the event should bypass custom anvil. if(DependencyManager.tryEventPreAnvilBypass(event)) return diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt index f122068..06d4393 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DependencyManager.kt @@ -6,6 +6,8 @@ import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.PrepareAnvilEvent import org.bukkit.inventory.AnvilInventory import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.dependency.gui.ExternGuiTester +import xyz.alexcrea.cuanvil.dependency.gui.GuiTesterSelector import xyz.alexcrea.cuanvil.dependency.packet.PacketManager import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerSelector import xyz.alexcrea.cuanvil.dependency.scheduler.BukkitScheduler @@ -17,6 +19,8 @@ object DependencyManager { var isFolia: Boolean = false lateinit var scheduler: TaskScheduler lateinit var packetManager: PacketManager + var externGuiTester: ExternGuiTester? = null + var enchantmentSquaredCompatibility: EnchantmentSquaredDependency? = null var ecoEnchantCompatibility: EcoEnchantDependency? = null var disenchantmentCompatibility: DisenchantmentDependency? = null @@ -35,6 +39,7 @@ object DependencyManager { // Packet Manager val forceProtocolib = ConfigHolder.DEFAULT_CONFIG.config.getBoolean("force_protocolib", false) packetManager = PacketManagerSelector.selectPacketManager(forceProtocolib) + externGuiTester = GuiTesterSelector.selectGuiTester // Enchantment Squared dependency if(pluginManager.isPluginEnabled("EnchantsSquared")){ @@ -81,6 +86,10 @@ object DependencyManager { if(disenchantmentCompatibility?.testPrepareAnvil(event) == true) bypass = true + + // Test if the inventory is a gui(version specific) + if(!bypass && (externGuiTester?.testIfGui(event.view) == true)) bypass = true + return bypass } @@ -89,6 +98,9 @@ object DependencyManager { if(disenchantmentCompatibility?.testAnvilResult(event, inventory) == true) bypass = true + // Test if the inventory is a gui(version specific) + if(!bypass && (externGuiTester?.testIfGui(event.view) == true)) bypass = true + return bypass } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt new file mode 100644 index 0000000..b1c27a1 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/gui/GuiTesterSelector.kt @@ -0,0 +1,51 @@ +package xyz.alexcrea.cuanvil.dependency.gui + +import xyz.alexcrea.cuanvil.dependency.gui.version.*; +import xyz.alexcrea.cuanvil.update.UpdateUtils + +object GuiTesterSelector { + + val selectGuiTester: ExternGuiTester? + get() { + val versionParts = UpdateUtils.currentMinecraftVersionArray() + if (versionParts[0] != 1) return null + + return when (versionParts[1]) { + // Can't support 1.16.5 bc 1.16.5 paper userdev do not exist + + 17 -> when (versionParts[2]) { + 0, 1 -> v1_17R1_ExternGuiTester() + else -> null + } + + 18 -> when (versionParts[2]) { + 0, 1 -> v1_18R1_ExternGuiTester() + 2 -> v1_18R2_ExternGuiTester() + else -> null + } + + 19 -> when (versionParts[2]) { + 0, 1, 2 -> v1_19R1_ExternGuiTester() + 3 -> v1_19R2_ExternGuiTester() + 4 -> v1_19R3_ExternGuiTester() + else -> null + } + + 20 -> when (versionParts[2]) { + 0, 1 -> v1_20R1_ExternGuiTester() + 2 -> v1_20R2_ExternGuiTester() + 3, 4 -> v1_20R3_ExternGuiTester() + 5, 6 -> v1_20R4_ExternGuiTester() + else -> null + } + + 21 -> when (versionParts[2]) { + 0, 1 -> v1_21R1_ExternGuiTester() + else -> null + } + + else -> null + } + } + +} \ No newline at end of file From a00bb919f43fddda79e1a3fb702c582908c13022 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:55:55 +0200 Subject: [PATCH 2/6] Add merge level limit (#32) Add merge level limit (aka "max before merge" or "disable merge over") --- .../cuanvil/gui/config/MainConfigGui.java | 24 ++++++-- .../global/EnchantMergeLimitConfigGui.java | 60 +++++++++++++++++++ .../io/delilaheve/util/ConfigOptions.kt | 34 +++++++++++ .../io/delilaheve/util/EnchantmentUtil.kt | 6 +- src/main/resources/config.yml | 11 ++++ 5 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java index 4f98cc3..e2224f7 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java @@ -30,7 +30,7 @@ public class MainConfigGui extends ChestGui { public void init(PacketManager packetManager) { Pattern pattern = new Pattern( GuiSharedConstant.EMPTY_GUI_FULL_LINE, - "012304567", + "012345678", "Q00000000" ); PatternPane pane = new PatternPane(0, 0, 9, 3, pattern); @@ -62,6 +62,18 @@ public class MainConfigGui extends ChestGui { GuiItem enchantLimitItem = GuiGlobalItems.goToGuiItem(enchantLimitItemstack, new EnchantLimitConfigGui()); pane.bindItem('2', enchantLimitItem); + // enchant level limit item + ItemStack enchantMergeLimitItemstack = new ItemStack(Material.ENCHANTED_BOOK); + ItemMeta enchantMergeLimitMeta = enchantMergeLimitItemstack.getItemMeta(); + assert enchantMergeLimitMeta != null; + + enchantMergeLimitMeta.setDisplayName("§aEnchantment Merge Limit"); + enchantMergeLimitMeta.setLore(Collections.singletonList("§7Click here to open enchantment merge limit menu")); + enchantMergeLimitItemstack.setItemMeta(enchantMergeLimitMeta); + + GuiItem enchantMergeLimitItem = GuiGlobalItems.goToGuiItem(enchantMergeLimitItemstack, new EnchantMergeLimitConfigGui()); + pane.bindItem('3', enchantMergeLimitItem); + // enchant cost item ItemStack enchantCostItemstack = new ItemStack(Material.EXPERIENCE_BOTTLE); ItemMeta enchantCostMeta = enchantCostItemstack.getItemMeta(); @@ -72,7 +84,7 @@ public class MainConfigGui extends ChestGui { enchantCostItemstack.setItemMeta(enchantCostMeta); GuiItem enchantCostItem = GuiGlobalItems.goToGuiItem(enchantCostItemstack, new EnchantCostConfigGui()); - pane.bindItem('3', enchantCostItem); + pane.bindItem('4', enchantCostItem); // Enchantment Conflicts item ItemStack enchantConflictItemstack = new ItemStack(Material.OAK_FENCE); @@ -84,7 +96,7 @@ public class MainConfigGui extends ChestGui { enchantConflictItemstack.setItemMeta(enchantConflictMeta); GuiItem enchantConflictItem = GuiGlobalItems.goToGuiItem(enchantConflictItemstack, EnchantConflictGui.getInstance()); - pane.bindItem('4', enchantConflictItem); + pane.bindItem('5', enchantConflictItem); // Group config items ItemStack groupItemstack = new ItemStack(Material.CHEST); @@ -97,7 +109,7 @@ public class MainConfigGui extends ChestGui { GuiItem groupConfigItem = GuiGlobalItems.goToGuiItem(groupItemstack, GroupConfigGui.getInstance()); - pane.bindItem('5', groupConfigItem); + pane.bindItem('6', groupConfigItem); // Unit repair item ItemStack unirRepairItemstack = new ItemStack(Material.DIAMOND); @@ -109,7 +121,7 @@ public class MainConfigGui extends ChestGui { unirRepairItemstack.setItemMeta(unitRepairMeta); GuiItem unitRepairItem = GuiGlobalItems.goToGuiItem(unirRepairItemstack, UnitRepairConfigGui.getInstance()); - pane.bindItem('6', unitRepairItem); + pane.bindItem('7', unitRepairItem); // Custom recipe item ItemStack customRecipeItemstack = new ItemStack(Material.CRAFTING_TABLE); @@ -121,7 +133,7 @@ public class MainConfigGui extends ChestGui { customRecipeItemstack.setItemMeta(customRecipeMeta); GuiItem customRecipeItem = GuiGlobalItems.goToGuiItem(customRecipeItemstack, CustomRecipeConfigGui.getInstance()); - pane.bindItem('7', customRecipeItem); + pane.bindItem('8', customRecipeItem); // quit item ItemStack quitItemstack = new ItemStack(Material.BARRIER); diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java new file mode 100644 index 0000000..a657cf6 --- /dev/null +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java @@ -0,0 +1,60 @@ +package xyz.alexcrea.cuanvil.gui.config.global; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import org.bukkit.Material; +import org.jetbrains.annotations.Nullable; +import xyz.alexcrea.cuanvil.config.ConfigHolder; +import xyz.alexcrea.cuanvil.enchant.CAEnchantment; +import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui; +import xyz.alexcrea.cuanvil.util.CasedStringUtil; + +import java.util.Arrays; +import java.util.Locale; + +public class EnchantMergeLimitConfigGui extends AbstractEnchantConfigGui { + + private static final String SECTION_NAME = "disable-merge-over"; + + private static EnchantMergeLimitConfigGui INSTANCE = null; + + @Nullable + public static EnchantMergeLimitConfigGui getInstance() { + return INSTANCE; + } + + /** + * Constructor of this Global gui for enchantment level limit settings. + */ + public EnchantMergeLimitConfigGui() { + super("§8Enchantment Maximum Merge Level"); + if(INSTANCE == null) INSTANCE = this; + + init(); + } + + @Override + public IntSettingsGui.IntSettingFactory createFactory(CAEnchantment enchant) { + String key = enchant.getKey().getKey().toLowerCase(Locale.ROOT); + String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key); + + return new IntSettingsGui.IntSettingFactory(prettyKey + " Merge Limit", this, + SECTION_NAME + '.' + key, ConfigHolder.DEFAULT_CONFIG, + Arrays.asList( + "§7Maximum merge level for for " + prettyKey, + "", + "§7For example, if set to §e2§7, §alvl1 §7+ §alvl1 §7of will give a §alvl2", + "§7But §alvl2 §7+ §alvl2 §7will not give a §clv3§7.", + "§7Will still not merge above max enchantment level", + "§e-1 §7(default) will set the merge limit to enchantment's maximum level" + ), + -1, 255, -1, + 1, 5, 10, 50, 100); + } + + @Override + public GuiItem itemFromFactory(CAEnchantment enchantment, IntSettingsGui.IntSettingFactory inventoryFactory) { + return inventoryFactory.getItem( + Material.ENCHANTED_BOOK, + inventoryFactory.getTitle()); + } +} diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index 37fdf5a..c6e7286 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -41,6 +41,8 @@ object ConfigOptions { const val ENCHANT_LIMIT_ROOT = "enchant_limits" const val ENCHANT_VALUES_ROOT = "enchant_values" + const val DISABLE_MERGE_OVER_ROOT = "disable-merge-over" + // Keys for specific enchantment values private const val KEY_BOOK = "book" private const val KEY_ITEM = "item" @@ -110,6 +112,9 @@ object ConfigOptions { // Default value for an enchantment multiplier private const val DEFAULT_ENCHANT_VALUE = 0 + // Default max before merge disabled (negative mean enabled) + const val DEFAULT_MAX_BEFORE_MERGE_DISABLED = -1; + // ------------- // Get methods // ------------- @@ -374,4 +379,33 @@ object ConfigOptions { return DEFAULT_ENCHANT_VALUE } + /** + * Get the given [enchantmentName]'s level before merge is disabled + * a negative value would mean never disabled + */ + fun maxBeforeMergeDisabled(enchantment: CAEnchantment): Int { + return maxBeforeMergeDisabled(enchantment.enchantmentName) + } + + /** + * Get the given [enchantmentName]'s level before merge is disabled + * a negative value would mean never disabled + */ + private fun maxBeforeMergeDisabled(enchantmentName: String) : Int { + // find if set + val path = "${DISABLE_MERGE_OVER_ROOT}.$enchantmentName" + + val value = CustomAnvil.instance + .config + .getInt(path, DEFAULT_MAX_BEFORE_MERGE_DISABLED) + .takeIf { it in ENCHANT_LIMIT_RANGE } + ?: DEFAULT_MAX_BEFORE_MERGE_DISABLED; + + if((value == DEFAULT_MAX_BEFORE_MERGE_DISABLED) && (enchantmentName == "sweeping_edge")){ + return maxBeforeMergeDisabled("sweeping") + } + + return value + } + } diff --git a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt index 95bdd36..2cde74c 100644 --- a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt +++ b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt @@ -76,7 +76,11 @@ object EnchantmentUtil { } // ... and they're the same level else { - // try to increase the enchantment level by 1 + // We test if it is allowed to merge at this level + val maxBeforeDisabled = ConfigOptions.maxBeforeMergeDisabled(enchantment) + if((maxBeforeDisabled > 0) && (oldLevel >= maxBeforeDisabled)) return@forEach + + // Now we increase the enchantment level by 1 var newLevel = oldLevel + 1 newLevel = max(min(newLevel, maxLevel), oldLevel) this[enchantment] = newLevel diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 462866d..82b28a8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -271,6 +271,17 @@ enchant_values: item: 8 book: 4 +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla) + sharpness: -1 + # If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied + #unbreaking: 2 + # Whether to show debug logging debug_log: false From 702925452634cb2c6cb053381aa8a80dc1c29b15 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:57:09 +0200 Subject: [PATCH 3/6] Improve registry and config (#33) Use namespace instead of name to identify enchantments --- defaultconfigs/1.18/config.yml | 184 +++++---- defaultconfigs/1.18/enchant_conflict.yml | 120 +++--- defaultconfigs/1.21/config.yml | 184 +++++---- defaultconfigs/1.21/enchant_conflict.yml | 356 ++++++------------ defaultconfigs/1.21/item_groups.yml | 205 +++++----- defaultconfigs/1.21/unit_repair_item.yml | 2 - .../xyz/alexcrea/cuanvil/api/ConflictAPI.java | 2 +- .../alexcrea/cuanvil/api/ConflictBuilder.java | 21 +- .../alexcrea/cuanvil/api/EnchantmentApi.java | 23 +- .../cuanvil/enchant/CAEnchantment.java | 19 +- .../enchant/CAEnchantmentRegistry.java | 42 ++- .../config/global/EnchantCostConfigGui.java | 18 +- .../config/global/EnchantLimitConfigGui.java | 15 +- .../global/EnchantMergeLimitConfigGui.java | 13 +- .../EnchantConflictSubSettingGui.java | 2 +- .../settings/EnchantCostSettingsGui.java | 76 ++-- .../alexcrea/cuanvil/update/Update_1_21.java | 15 +- .../io/delilaheve/util/ConfigOptions.kt | 100 +++-- .../cuanvil/group/EnchantConflictGroup.kt | 3 + .../cuanvil/group/EnchantConflictManager.kt | 23 +- src/main/resources/config.yml | 164 ++++---- src/main/resources/enchant_conflict.yml | 120 +++--- 22 files changed, 822 insertions(+), 885 deletions(-) diff --git a/defaultconfigs/1.18/config.yml b/defaultconfigs/1.18/config.yml index 5b47ec4..dd24558 100644 --- a/defaultconfigs/1.18/config.yml +++ b/defaultconfigs/1.18/config.yml @@ -97,49 +97,46 @@ default_limit: 5 # # Valid range of 1 - 255 for each enchantment enchant_limits: - aqua_affinity: 1 - binding_curse: 1 - channeling: 1 - flame: 1 - infinity: 1 - mending: 1 - multishot: 1 - silk_touch: 1 - vanishing_curse: 1 - depth_strider: 3 # anything more than 3 is treated as 3 by the game - protection: 4 - fire_protection: 4 - blast_protection: 4 - projectile_protection: 4 - feather_falling: 4 - thorns: 3 - respiration: 3 - sharpness: 5 - smite: 5 - bane_of_arthropods: 5 - knockback: 2 - fire_aspect: 2 - looting: 3 - sweeping: 3 - sweeping_edge: 3 - efficiency: 5 - unbreaking: 3 - fortune: 3 - power: 5 - punch: 2 - luck_of_the_sea: 3 - lure: 3 - frost_walker: 2 - impaling: 5 - riptide: 3 - loyalty: 3 - piercing: 4 - quick_charge: 3 - soul_speed: 3 - swift_sneak: 3 - density: 5 - breach: 4 - wind_burst: 3 + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 # Multipliers used to calculate the enchantment's value in repair/combining # @@ -153,135 +150,137 @@ enchant_limits: # With default values protection 4 would have a value of 4 when # coming from either a book (4 * 1) or an item (4 * 1) enchant_values: - aqua_affinity: + minecraft:aqua_affinity: item: 4 book: 2 - bane_of_arthropods: + minecraft:bane_of_arthropods: item: 2 book: 1 - binding_curse: + minecraft:binding_curse: item: 8 book: 4 - blast_protection: + minecraft:blast_protection: item: 4 book: 2 - channeling: + minecraft:channeling: item: 8 book: 4 - depth_strider: + minecraft:depth_strider: item: 4 book: 2 - efficiency: + minecraft:efficiency: item: 1 book: 1 - flame: + minecraft:flame: item: 4 book: 2 - feather_falling: + minecraft:feather_falling: item: 2 book: 1 - fire_aspect: + minecraft:fire_aspect: item: 4 book: 2 - fire_protection: + minecraft:fire_protection: item: 2 book: 1 - fortune: + minecraft:fortune: item: 4 book: 2 - frost_walker: + minecraft:frost_walker: item: 4 book: 2 - impaling: + minecraft:impaling: item: 4 book: 2 - infinity: + minecraft:infinity: item: 8 book: 4 - knockback: + minecraft:knockback: item: 2 book: 1 - looting: + minecraft:looting: item: 4 book: 2 - loyalty: + minecraft:loyalty: item: 1 book: 1 - luck_of_the_sea: + minecraft:luck_of_the_sea: item: 4 book: 2 - lure: + minecraft:lure: item: 4 book: 2 - mending: + minecraft:mending: item: 4 book: 2 - multishot: + minecraft:multishot: item: 4 book: 2 - piercing: + minecraft:piercing: item: 1 book: 1 - power: + minecraft:power: item: 1 book: 1 - projectile_protection: + minecraft:projectile_protection: item: 2 book: 1 - protection: + minecraft:protection: item: 1 book: 1 - punch: + minecraft:punch: item: 4 book: 2 - quick_charge: + minecraft:quick_charge: item: 2 book: 1 - respiration: + minecraft:respiration: item: 4 book: 2 - riptide: + minecraft:riptide: item: 4 book: 2 - silk_touch: + minecraft:silk_touch: item: 8 book: 4 - sharpness: + minecraft:sharpness: item: 1 book: 1 - smite: + minecraft:smite: item: 2 book: 1 - soul_speed: + minecraft:soul_speed: item: 8 book: 4 - swift_sneak: + minecraft:swift_sneak: item: 8 book: 4 - sweeping: + minecraft:sweeping: item: 4 book: 2 - sweeping_edge: + minecraft:sweeping_edge: item: 4 book: 2 - thorns: + minecraft:thorns: item: 8 book: 4 - unbreaking: + minecraft:unbreaking: item: 2 book: 1 - vanishing_curse: + minecraft:vanishing_curse: item: 8 book: 4 - density: - item: 1 - book: 1 - breach: - item: 4 - book: 2 - wind_burst: - item: 4 - book: 2 + +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla) + minecraft:sharpness: -1 + # If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied + #minecraft:unbreaking: 2 # Whether to show debug logging debug_log: false @@ -296,4 +295,3 @@ debug_log_verbose: false force_protocolib: false configVersion: 1.6.2 -lowMinecraftVersion: '1.21' diff --git a/defaultconfigs/1.18/enchant_conflict.yml b/defaultconfigs/1.18/enchant_conflict.yml index ed154ab..0e8b3f3 100644 --- a/defaultconfigs/1.18/enchant_conflict.yml +++ b/defaultconfigs/1.18/enchant_conflict.yml @@ -17,158 +17,158 @@ # ---------------------------------------------------- restriction_aqua_affinity: - enchantments: [ aqua_affinity ] + enchantments: [ minecraft:aqua_affinity ] notAffectedGroups: [ enchanted_book, helmets ] restriction_bane_of_arthropods: - enchantments: [ bane_of_arthropods ] + enchantments: [ minecraft:bane_of_arthropods ] notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_blast_protection: - enchantments: [ blast_protection ] + enchantments: [ minecraft:blast_protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_channeling: - enchantments: [ channeling ] + enchantments: [ minecraft:channeling ] notAffectedGroups: [ enchanted_book, trident ] restriction_binding_curse: - enchantments: [ binding_curse ] + enchantments: [ minecraft:binding_curse ] notAffectedGroups: [ enchanted_book, wearable ] restriction_vanishing_curse: - enchantments: [ vanishing_curse ] + enchantments: [ minecraft:vanishing_curse ] notAffectedGroups: [ enchanted_book, can_vanish ] restriction_depth_strider: - enchantments: [ depth_strider ] + enchantments: [ minecraft:depth_strider ] notAffectedGroups: [ enchanted_book, boots ] restriction_efficiency: - enchantments: [ efficiency ] + enchantments: [ minecraft:efficiency ] notAffectedGroups: [ enchanted_book, tools, shears ] restriction_feather_falling: - enchantments: [ feather_falling ] + enchantments: [ minecraft:feather_falling ] notAffectedGroups: [ enchanted_book, boots ] restriction_fire_aspect: - enchantments: [ fire_aspect ] + enchantments: [ minecraft:fire_aspect ] notAffectedGroups: [ enchanted_book, swords ] restriction_fire_protection: - enchantments: [ fire_protection ] + enchantments: [ minecraft:fire_protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_flame: - enchantments: [ flame ] + enchantments: [ minecraft:flame ] notAffectedGroups: [ enchanted_book, bow ] restriction_fortune: - enchantments: [ fortune ] + enchantments: [ minecraft:fortune ] notAffectedGroups: [ enchanted_book, tools ] restriction_frost_walker: - enchantments: [ frost_walker ] + enchantments: [ minecraft:frost_walker ] notAffectedGroups: [ enchanted_book, boots ] restriction_impaling: - enchantments: [ impaling ] + enchantments: [ minecraft:impaling ] notAffectedGroups: [ enchanted_book, trident ] restriction_infinity: - enchantments: [ infinity ] + enchantments: [ minecraft:infinity ] notAffectedGroups: [ enchanted_book, bow ] restriction_knockback: - enchantments: [ knockback ] + enchantments: [ minecraft:knockback ] notAffectedGroups: [ enchanted_book, swords ] restriction_looting: - enchantments: [ looting ] + enchantments: [ minecraft:looting ] notAffectedGroups: [ enchanted_book, swords ] restriction_loyalty: - enchantments: [ loyalty ] + enchantments: [ minecraft:loyalty ] notAffectedGroups: [ enchanted_book, trident ] restriction_lure: - enchantments: [ lure ] + enchantments: [ minecraft:lure ] notAffectedGroups: [ enchanted_book, fishing_rod ] restriction_mending: - enchantments: [ mending ] + enchantments: [ minecraft:mending ] notAffectedGroups: [ enchanted_book, can_unbreak ] -restriction_multishot: - enchantments: [ multishot ] +restriction_minecraft_multishot: + enchantments: [ minecraft:multishot ] notAffectedGroups: [ enchanted_book, crossbow ] restriction_piercing: - enchantments: [ piercing ] + enchantments: [ minecraft:piercing ] notAffectedGroups: [ enchanted_book, crossbow ] restriction_power: - enchantments: [ power ] + enchantments: [ minecraft:power ] notAffectedGroups: [ enchanted_book, bow ] restriction_projectile_protection: - enchantments: [ projectile_protection ] + enchantments: [ minecraft:projectile_protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_protection: - enchantments: [ protection ] + enchantments: [ minecraft:protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_punch: - enchantments: [ punch ] + enchantments: [ minecraft:punch ] notAffectedGroups: [ enchanted_book, bow ] restriction_quick_charge: - enchantments: [ quick_charge ] + enchantments: [ minecraft:quick_charge ] notAffectedGroups: [ enchanted_book, crossbow ] restriction_respiration: - enchantments: [ respiration ] + enchantments: [ minecraft:respiration ] notAffectedGroups: [ enchanted_book, helmets ] restriction_riptide: - enchantments: [ riptide ] + enchantments: [ minecraft:riptide ] notAffectedGroups: [ enchanted_book, trident ] restriction_sharpness: - enchantments: [ sharpness ] + enchantments: [ minecraft:sharpness ] notAffectedGroups: [ enchanted_book, melee_weapons ] -restriction_silk_touch: - enchantments: [ silk_touch ] +restriction__silk_touch: + enchantments: [ minecraft:silk_touch ] notAffectedGroups: [ enchanted_book, tools ] restriction_smite: - enchantments: [ smite ] + enchantments: [ minecraft:smite ] notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_soul_speed: - enchantments: [ soul_speed ] + enchantments: [ minecraft:soul_speed ] notAffectedGroups: [ enchanted_book, boots ] restriction_sweeping_edge: - enchantments: [ sweeping, sweeping_edge ] + enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ] notAffectedGroups: [ enchanted_book, swords ] # Do not exist in 1.18, that mean useInFuture will be set to true # useInFuture set to true also mean it will not warn if there is an issue restriction_swift_sneak: useInFuture: true - enchantments: [ swift_sneak ] + enchantments: [ minecraft:swift_sneak ] notAffectedGroups: [ enchanted_book, leggings ] restriction_thorns: - enchantments: [ thorns ] + enchantments: [ minecraft:thorns ] notAffectedGroups: [ enchanted_book, armors ] -restriction_unbreaking: - enchantments: [ unbreaking ] +restriction__unbreaking: + enchantments: [ minecraft:unbreaking ] notAffectedGroups: [ enchanted_book, can_unbreak ] # ---------------------------------------------------- @@ -180,60 +180,60 @@ restriction_unbreaking: sword_enchant_conflict: enchantments: - - bane_of_arthropods - - smite - - sharpness + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 protection_enchant_conflict: enchantments: - - blast_protection - - fire_protection - - projectile_protection - - protection + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict1: enchantments: - - channeling - - riptide + - minecraft:channeling + - minecraft:riptide notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict2: enchantments: - - loyalty - - riptide + - minecraft:loyalty + - minecraft:riptide notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 boot_conflict: enchantments: - - depth_strider - - frost_walker + - minecraft:depth_strider + - minecraft:frost_walker notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 tool_conflict: enchantments: - - fortune - - silk_touch + - minecraft:fortune + - minecraft:silk_touch notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 bow_conflict: enchantments: - - mending - - infinity + - minecraft:mending + - minecraft:infinity notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 crossbow_conflict: enchantments: - - multishot - - piercing + - minecraft:multishot + - minecraft:piercing notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 diff --git a/defaultconfigs/1.21/config.yml b/defaultconfigs/1.21/config.yml index 5b47ec4..dd24558 100644 --- a/defaultconfigs/1.21/config.yml +++ b/defaultconfigs/1.21/config.yml @@ -97,49 +97,46 @@ default_limit: 5 # # Valid range of 1 - 255 for each enchantment enchant_limits: - aqua_affinity: 1 - binding_curse: 1 - channeling: 1 - flame: 1 - infinity: 1 - mending: 1 - multishot: 1 - silk_touch: 1 - vanishing_curse: 1 - depth_strider: 3 # anything more than 3 is treated as 3 by the game - protection: 4 - fire_protection: 4 - blast_protection: 4 - projectile_protection: 4 - feather_falling: 4 - thorns: 3 - respiration: 3 - sharpness: 5 - smite: 5 - bane_of_arthropods: 5 - knockback: 2 - fire_aspect: 2 - looting: 3 - sweeping: 3 - sweeping_edge: 3 - efficiency: 5 - unbreaking: 3 - fortune: 3 - power: 5 - punch: 2 - luck_of_the_sea: 3 - lure: 3 - frost_walker: 2 - impaling: 5 - riptide: 3 - loyalty: 3 - piercing: 4 - quick_charge: 3 - soul_speed: 3 - swift_sneak: 3 - density: 5 - breach: 4 - wind_burst: 3 + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 # Multipliers used to calculate the enchantment's value in repair/combining # @@ -153,135 +150,137 @@ enchant_limits: # With default values protection 4 would have a value of 4 when # coming from either a book (4 * 1) or an item (4 * 1) enchant_values: - aqua_affinity: + minecraft:aqua_affinity: item: 4 book: 2 - bane_of_arthropods: + minecraft:bane_of_arthropods: item: 2 book: 1 - binding_curse: + minecraft:binding_curse: item: 8 book: 4 - blast_protection: + minecraft:blast_protection: item: 4 book: 2 - channeling: + minecraft:channeling: item: 8 book: 4 - depth_strider: + minecraft:depth_strider: item: 4 book: 2 - efficiency: + minecraft:efficiency: item: 1 book: 1 - flame: + minecraft:flame: item: 4 book: 2 - feather_falling: + minecraft:feather_falling: item: 2 book: 1 - fire_aspect: + minecraft:fire_aspect: item: 4 book: 2 - fire_protection: + minecraft:fire_protection: item: 2 book: 1 - fortune: + minecraft:fortune: item: 4 book: 2 - frost_walker: + minecraft:frost_walker: item: 4 book: 2 - impaling: + minecraft:impaling: item: 4 book: 2 - infinity: + minecraft:infinity: item: 8 book: 4 - knockback: + minecraft:knockback: item: 2 book: 1 - looting: + minecraft:looting: item: 4 book: 2 - loyalty: + minecraft:loyalty: item: 1 book: 1 - luck_of_the_sea: + minecraft:luck_of_the_sea: item: 4 book: 2 - lure: + minecraft:lure: item: 4 book: 2 - mending: + minecraft:mending: item: 4 book: 2 - multishot: + minecraft:multishot: item: 4 book: 2 - piercing: + minecraft:piercing: item: 1 book: 1 - power: + minecraft:power: item: 1 book: 1 - projectile_protection: + minecraft:projectile_protection: item: 2 book: 1 - protection: + minecraft:protection: item: 1 book: 1 - punch: + minecraft:punch: item: 4 book: 2 - quick_charge: + minecraft:quick_charge: item: 2 book: 1 - respiration: + minecraft:respiration: item: 4 book: 2 - riptide: + minecraft:riptide: item: 4 book: 2 - silk_touch: + minecraft:silk_touch: item: 8 book: 4 - sharpness: + minecraft:sharpness: item: 1 book: 1 - smite: + minecraft:smite: item: 2 book: 1 - soul_speed: + minecraft:soul_speed: item: 8 book: 4 - swift_sneak: + minecraft:swift_sneak: item: 8 book: 4 - sweeping: + minecraft:sweeping: item: 4 book: 2 - sweeping_edge: + minecraft:sweeping_edge: item: 4 book: 2 - thorns: + minecraft:thorns: item: 8 book: 4 - unbreaking: + minecraft:unbreaking: item: 2 book: 1 - vanishing_curse: + minecraft:vanishing_curse: item: 8 book: 4 - density: - item: 1 - book: 1 - breach: - item: 4 - book: 2 - wind_burst: - item: 4 - book: 2 + +# Disable enchantment merging for level above the set value +# Enchantment merging is when, for example, 2 unbreaking II book combine to give sharpness III +# But Enchantment above this value can still be applied. following the previous example, we could still apply a unbreaking III book to a sword +# Even if disable-merge-over of unbreaking is set to 2 +# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. +disable-merge-over: + # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla) + minecraft:sharpness: -1 + # If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied + #minecraft:unbreaking: 2 # Whether to show debug logging debug_log: false @@ -296,4 +295,3 @@ debug_log_verbose: false force_protocolib: false configVersion: 1.6.2 -lowMinecraftVersion: '1.21' diff --git a/defaultconfigs/1.21/enchant_conflict.yml b/defaultconfigs/1.21/enchant_conflict.yml index dc3e951..0e8b3f3 100644 --- a/defaultconfigs/1.21/enchant_conflict.yml +++ b/defaultconfigs/1.21/enchant_conflict.yml @@ -17,278 +17,159 @@ # ---------------------------------------------------- restriction_aqua_affinity: - enchantments: - - aqua_affinity - notAffectedGroups: - - enchanted_book - - helmets + enchantments: [ minecraft:aqua_affinity ] + notAffectedGroups: [ enchanted_book, helmets ] restriction_bane_of_arthropods: - enchantments: - - bane_of_arthropods - notAffectedGroups: - - enchanted_book - - melee_weapons - - mace + enchantments: [ minecraft:bane_of_arthropods ] + notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_blast_protection: - enchantments: - - blast_protection - notAffectedGroups: - - enchanted_book - - armors + enchantments: [ minecraft:blast_protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_channeling: - enchantments: - - channeling - notAffectedGroups: - - enchanted_book - - trident + enchantments: [ minecraft:channeling ] + notAffectedGroups: [ enchanted_book, trident ] restriction_binding_curse: - enchantments: - - binding_curse - notAffectedGroups: - - enchanted_book - - wearable + enchantments: [ minecraft:binding_curse ] + notAffectedGroups: [ enchanted_book, wearable ] restriction_vanishing_curse: - enchantments: - - vanishing_curse - notAffectedGroups: - - enchanted_book - - can_vanish + enchantments: [ minecraft:vanishing_curse ] + notAffectedGroups: [ enchanted_book, can_vanish ] restriction_depth_strider: - enchantments: - - depth_strider - notAffectedGroups: - - enchanted_book - - boots + enchantments: [ minecraft:depth_strider ] + notAffectedGroups: [ enchanted_book, boots ] restriction_efficiency: - enchantments: - - efficiency - notAffectedGroups: - - enchanted_book - - tools - - shears + enchantments: [ minecraft:efficiency ] + notAffectedGroups: [ enchanted_book, tools, shears ] restriction_feather_falling: - enchantments: - - feather_falling - notAffectedGroups: - - enchanted_book - - boots + enchantments: [ minecraft:feather_falling ] + notAffectedGroups: [ enchanted_book, boots ] restriction_fire_aspect: - enchantments: - - fire_aspect - notAffectedGroups: - - enchanted_book - - swords - - mace + enchantments: [ minecraft:fire_aspect ] + notAffectedGroups: [ enchanted_book, swords ] restriction_fire_protection: - enchantments: - - fire_protection - notAffectedGroups: - - enchanted_book - - armors + enchantments: [ minecraft:fire_protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_flame: - enchantments: - - flame - notAffectedGroups: - - enchanted_book - - bow + enchantments: [ minecraft:flame ] + notAffectedGroups: [ enchanted_book, bow ] restriction_fortune: - enchantments: - - fortune - notAffectedGroups: - - enchanted_book - - tools + enchantments: [ minecraft:fortune ] + notAffectedGroups: [ enchanted_book, tools ] restriction_frost_walker: - enchantments: - - frost_walker - notAffectedGroups: - - enchanted_book - - boots + enchantments: [ minecraft:frost_walker ] + notAffectedGroups: [ enchanted_book, boots ] restriction_impaling: - enchantments: - - impaling - notAffectedGroups: - - enchanted_book - - trident + enchantments: [ minecraft:impaling ] + notAffectedGroups: [ enchanted_book, trident ] restriction_infinity: - enchantments: - - infinity - notAffectedGroups: - - enchanted_book - - bow + enchantments: [ minecraft:infinity ] + notAffectedGroups: [ enchanted_book, bow ] restriction_knockback: - enchantments: - - knockback - notAffectedGroups: - - enchanted_book - - swords + enchantments: [ minecraft:knockback ] + notAffectedGroups: [ enchanted_book, swords ] restriction_looting: - enchantments: - - looting - notAffectedGroups: - - enchanted_book - - swords + enchantments: [ minecraft:looting ] + notAffectedGroups: [ enchanted_book, swords ] restriction_loyalty: - enchantments: - - loyalty - notAffectedGroups: - - enchanted_book - - trident + enchantments: [ minecraft:loyalty ] + notAffectedGroups: [ enchanted_book, trident ] restriction_lure: - enchantments: - - lure - notAffectedGroups: - - enchanted_book - - fishing_rod + enchantments: [ minecraft:lure ] + notAffectedGroups: [ enchanted_book, fishing_rod ] restriction_mending: - enchantments: - - mending - notAffectedGroups: - - enchanted_book - - can_unbreak + enchantments: [ minecraft:mending ] + notAffectedGroups: [ enchanted_book, can_unbreak ] -restriction_multishot: - enchantments: - - multishot - notAffectedGroups: - - enchanted_book - - crossbow +restriction_minecraft_multishot: + enchantments: [ minecraft:multishot ] + notAffectedGroups: [ enchanted_book, crossbow ] restriction_piercing: - enchantments: - - piercing - notAffectedGroups: - - enchanted_book - - crossbow + enchantments: [ minecraft:piercing ] + notAffectedGroups: [ enchanted_book, crossbow ] restriction_power: - enchantments: - - power - notAffectedGroups: - - enchanted_book - - bow + enchantments: [ minecraft:power ] + notAffectedGroups: [ enchanted_book, bow ] restriction_projectile_protection: - enchantments: - - projectile_protection - notAffectedGroups: - - enchanted_book - - armors + enchantments: [ minecraft:projectile_protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_protection: - enchantments: - - protection - notAffectedGroups: - - enchanted_book - - armors + enchantments: [ minecraft:protection ] + notAffectedGroups: [ enchanted_book, armors ] restriction_punch: - enchantments: - - punch - notAffectedGroups: - - enchanted_book - - bow + enchantments: [ minecraft:punch ] + notAffectedGroups: [ enchanted_book, bow ] restriction_quick_charge: - enchantments: - - quick_charge - notAffectedGroups: - - enchanted_book - - crossbow + enchantments: [ minecraft:quick_charge ] + notAffectedGroups: [ enchanted_book, crossbow ] restriction_respiration: - enchantments: - - respiration - notAffectedGroups: - - enchanted_book - - helmets + enchantments: [ minecraft:respiration ] + notAffectedGroups: [ enchanted_book, helmets ] restriction_riptide: - enchantments: - - riptide - notAffectedGroups: - - enchanted_book - - trident + enchantments: [ minecraft:riptide ] + notAffectedGroups: [ enchanted_book, trident ] restriction_sharpness: - enchantments: - - sharpness - notAffectedGroups: - - enchanted_book - - melee_weapons + enchantments: [ minecraft:sharpness ] + notAffectedGroups: [ enchanted_book, melee_weapons ] -restriction_silk_touch: - enchantments: - - silk_touch - notAffectedGroups: - - enchanted_book - - tools +restriction__silk_touch: + enchantments: [ minecraft:silk_touch ] + notAffectedGroups: [ enchanted_book, tools ] restriction_smite: - enchantments: - - smite - notAffectedGroups: - - enchanted_book - - melee_weapons - - mace + enchantments: [ minecraft:smite ] + notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_soul_speed: - enchantments: - - soul_speed - notAffectedGroups: - - enchanted_book - - boots + enchantments: [ minecraft:soul_speed ] + notAffectedGroups: [ enchanted_book, boots ] restriction_sweeping_edge: - enchantments: - - sweeping - - sweeping_edge - notAffectedGroups: - - enchanted_book - - swords + enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ] + notAffectedGroups: [ enchanted_book, swords ] # Do not exist in 1.18, that mean useInFuture will be set to true # useInFuture set to true also mean it will not warn if there is an issue restriction_swift_sneak: useInFuture: true - enchantments: - - swift_sneak - notAffectedGroups: - - enchanted_book - - leggings + enchantments: [ minecraft:swift_sneak ] + notAffectedGroups: [ enchanted_book, leggings ] restriction_thorns: - enchantments: - - thorns - notAffectedGroups: - - enchanted_book - - armors + enchantments: [ minecraft:thorns ] + notAffectedGroups: [ enchanted_book, armors ] -restriction_unbreaking: - enchantments: - - unbreaking - notAffectedGroups: - - enchanted_book - - can_unbreak +restriction__unbreaking: + enchantments: [ minecraft:unbreaking ] + notAffectedGroups: [ enchanted_book, can_unbreak ] # ---------------------------------------------------- # Now we have conflicts about enchantment Incompatibility @@ -299,86 +180,61 @@ restriction_unbreaking: sword_enchant_conflict: enchantments: - - bane_of_arthropods - - smite - - sharpness - notAffectedGroups: [] + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 protection_enchant_conflict: enchantments: - - blast_protection - - fire_protection - - projectile_protection - - protection - notAffectedGroups: [] + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict1: enchantments: - - channeling - - riptide - notAffectedGroups: [] + - minecraft:channeling + - minecraft:riptide + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict2: enchantments: - - loyalty - - riptide - notAffectedGroups: [] + - minecraft:loyalty + - minecraft:riptide + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 boot_conflict: enchantments: - - depth_strider - - frost_walker - notAffectedGroups: [] + - minecraft:depth_strider + - minecraft:frost_walker + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 tool_conflict: enchantments: - - fortune - - silk_touch - notAffectedGroups: [] + - minecraft:fortune + - minecraft:silk_touch + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 bow_conflict: enchantments: - - mending - - infinity - notAffectedGroups: [] + - minecraft:mending + - minecraft:infinity + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 crossbow_conflict: enchantments: - - multishot - - piercing - notAffectedGroups: [] - maxEnchantmentBeforeConflict: 1 -restriction_density: - enchantments: - - density - notAffectedGroups: - - mace - - enchanted_book -restriction_breach: - enchantments: - - breach - notAffectedGroups: - - mace - - enchanted_book -restriction_wind_burst: - enchantments: - - wind_burst - notAffectedGroups: - - mace - - enchanted_book -mace_enchant_conflict: - enchantments: - - density - - breach - - smite - - bane_of_arthropods + - minecraft:multishot + - minecraft:piercing + notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 # ---------------------------------------------------- diff --git a/defaultconfigs/1.21/item_groups.yml b/defaultconfigs/1.21/item_groups.yml index 0add107..3c1eb5d 100644 --- a/defaultconfigs/1.21/item_groups.yml +++ b/defaultconfigs/1.21/item_groups.yml @@ -20,16 +20,16 @@ nothing: example_include: type: include items: - - stone - - polished_granite + - stone + - polished_granite # This group contain everything except polished granite and elements of example_include example_exclude: type: exclude items: - - polished_granite + - polished_granite groups: - - example_include + - example_include # Default configuration should be vanilla enchantment conflict group # there may have error, if you find one you can fix it ! @@ -38,176 +38,173 @@ example_exclude: swords: type: include items: - - wooden_sword - - stone_sword - - iron_sword - - diamond_sword - - golden_sword - - netherite_sword + - wooden_sword + - stone_sword + - iron_sword + - diamond_sword + - golden_sword + - netherite_sword axes: type: include items: - - wooden_axe - - stone_axe - - iron_axe - - diamond_axe - - golden_axe - - netherite_axe + - wooden_axe + - stone_axe + - iron_axe + - diamond_axe + - golden_axe + - netherite_axe melee_weapons: type: include groups: - - swords - - axes + - swords + - axes helmets: type: include items: - - leather_helmet - - chainmail_helmet - - iron_helmet - - diamond_helmet - - golden_helmet - - netherite_helmet - - turtle_helmet + - leather_helmet + - chainmail_helmet + - iron_helmet + - diamond_helmet + - golden_helmet + - netherite_helmet + - turtle_helmet chestplate: type: include items: - - leather_chestplate - - chainmail_chestplate - - iron_chestplate - - diamond_chestplate - - golden_chestplate - - netherite_chestplate + - leather_chestplate + - chainmail_chestplate + - iron_chestplate + - diamond_chestplate + - golden_chestplate + - netherite_chestplate leggings: type: include items: - - leather_leggings - - chainmail_leggings - - iron_leggings - - diamond_leggings - - golden_leggings - - netherite_leggings + - leather_leggings + - chainmail_leggings + - iron_leggings + - diamond_leggings + - golden_leggings + - netherite_leggings boots: type: include items: - - leather_boots - - chainmail_boots - - iron_boots - - diamond_boots - - golden_boots - - netherite_boots + - leather_boots + - chainmail_boots + - iron_boots + - diamond_boots + - golden_boots + - netherite_boots armors: type: include groups: - - helmets - - chestplate - - leggings - - boots + - helmets + - chestplate + - leggings + - boots wearable: type: include items: - - elytra - - carved_pumpkin - - skeleton_skull - - wither_skeleton_skull - - zombie_head - - player_head - - creeper_head - - dragon_head - - piglin_head + - elytra + - carved_pumpkin + - skeleton_skull + - wither_skeleton_skull + - zombie_head + - player_head + - creeper_head + - dragon_head + # do not exist in 1.18 but exist in future update + - piglin_head groups: - - armors + - armors tools: type: include items: - - wooden_pickaxe - - stone_pickaxe - - iron_pickaxe - - diamond_pickaxe - - golden_pickaxe - - netherite_pickaxe - - wooden_shovel - - stone_shovel - - iron_shovel - - diamond_shovel - - golden_shovel - - netherite_shovel - - wooden_hoe - - stone_hoe - - iron_hoe - - diamond_hoe - - golden_hoe - - netherite_hoe + - wooden_pickaxe + - stone_pickaxe + - iron_pickaxe + - diamond_pickaxe + - golden_pickaxe + - netherite_pickaxe + - wooden_shovel + - stone_shovel + - iron_shovel + - diamond_shovel + - golden_shovel + - netherite_shovel + - wooden_hoe + - stone_hoe + - iron_hoe + - diamond_hoe + - golden_hoe + - netherite_hoe groups: - - axes + - axes enchanted_book: type: include items: - - enchanted_book + - enchanted_book trident: type: include items: - - trident + - trident bow: type: include items: - - bow + - bow crossbow: type: include items: - - crossbow + - crossbow fishing_rod: type: include items: - - fishing_rod + - fishing_rod shears: type: include items: - - shears + - shears can_unbreak: type: include items: - - elytra - - flint_and_steel - - shield - - carrot_on_a_stick - - warped_fungus_on_a_stick - - brush + - elytra + - flint_and_steel + - shield + - carrot_on_a_stick + - warped_fungus_on_a_stick + # do not exist in 1.18 but exist in future update + - brush groups: - - melee_weapons - - tools - - armors - - trident - - bow - - crossbow - - fishing_rod - - shears - - mace + - melee_weapons + - tools + - armors + - trident + - bow + - crossbow + - fishing_rod + - shears can_vanish: type: include items: - - compass + - compass groups: - - wearable - - can_unbreak -mace: - type: include - items: - - mace + - wearable + - can_unbreak diff --git a/defaultconfigs/1.21/unit_repair_item.yml b/defaultconfigs/1.21/unit_repair_item.yml index ec548c6..2902cce 100644 --- a/defaultconfigs/1.21/unit_repair_item.yml +++ b/defaultconfigs/1.21/unit_repair_item.yml @@ -188,5 +188,3 @@ warped_planks: wooden_shovel: 0.25 wooden_hoe: 0.25 shield: 0.25 -breeze_rod: - mace: 0.25 diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java index ad01827..8047382 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java @@ -119,7 +119,7 @@ public class ConflictAPI { private static List extractEnchantments(@NotNull ConflictBuilder builder){ List result = new ArrayList<>(builder.getEnchantmentNames()); for (NamespacedKey enchantmentKey : builder.getEnchantmentKeys()) { - result.add(enchantmentKey.getKey()); + result.add(enchantmentKey.toString()); } return result; diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java index 3e63b36..e3fd2d6 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java @@ -10,6 +10,7 @@ import xyz.alexcrea.cuanvil.enchant.CAEnchantment; import xyz.alexcrea.cuanvil.group.*; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -372,7 +373,7 @@ public class ConflictBuilder { */ protected void appendEnchantments(@NotNull EnchantConflictGroup conflict){ for (String enchantmentName : getEnchantmentNames()){ - if(appendEnchantment(conflict, EnchantmentApi.getByName(enchantmentName))){ + if(appendEnchantments(conflict, EnchantmentApi.getListByName(enchantmentName)) == 0){ CustomAnvil.instance.getLogger().warning("Could not find enchantment " + enchantmentName + " for conflict " + getName()); ConflictAPI.logConflictOrigin(this); } @@ -399,6 +400,24 @@ public class ConflictBuilder { return true; } + /** + * Append a list of enchantments. + * + * @param conflict The conflict target + * @param enchantments List of enchantment to add + * @return Number of enchantment added + */ + protected static int appendEnchantments(@NotNull EnchantConflictGroup conflict, @NotNull List enchantments){ + int numberValid = 0; + for (CAEnchantment enchantment : enchantments) { + if(appendEnchantment(conflict, enchantment)){ + numberValid++; + } + } + + return numberValid; + } + /** * Extract group abstract material group. * diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java index 01c5ed8..32a62c3 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java +++ b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java @@ -18,6 +18,7 @@ import xyz.alexcrea.cuanvil.gui.config.global.EnchantCostConfigGui; import xyz.alexcrea.cuanvil.gui.config.global.EnchantLimitConfigGui; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -104,7 +105,7 @@ public class EnchantmentApi { * @return True if successful. */ public static boolean unregisterEnchantment(@NotNull NamespacedKey key){ - CAEnchantment enchantment = CAEnchantmentRegistry.getInstance().getByKey(key); + CAEnchantment enchantment = CAEnchantment.getByKey(key); return unregisterEnchantment(enchantment); } @@ -126,7 +127,7 @@ public class EnchantmentApi { */ @Nullable public static CAEnchantment getByKey(@NotNull NamespacedKey key){ - return CAEnchantmentRegistry.getInstance().getByKey(key); + return CAEnchantment.getByKey(key); } /** @@ -134,10 +135,22 @@ public class EnchantmentApi { * * @param name The name used to fetch * @return The custom anvil enchantment of this name. null if not found. + * @deprecated use {@link #getListByName(String)} */ + @Deprecated(since = "1.6.3") @Nullable public static CAEnchantment getByName(@NotNull String name){ - return CAEnchantmentRegistry.getInstance().getByName(name); + return CAEnchantment.getByName(name); + } + + /** + * Get list of enchantment using the provided name. + * + * @param name The name used to fetch + * @return List of custom anvil enchantments of this name. May be empty if not found. + */ + public static List getListByName(@NotNull String name){ + return CAEnchantment.getListByName(name); } /** @@ -167,9 +180,9 @@ public class EnchantmentApi { private static void writeDefaultConfig(FileConfiguration defaultConfig, CAEnchantment enchantment) { - defaultConfig.set("enchant_limits." + enchantment.getKey().getKey(), enchantment.defaultMaxLevel()); + defaultConfig.set("enchant_limits." + enchantment.getKey(), enchantment.defaultMaxLevel()); - String basePath = "enchant_values." + enchantment.getKey().getKey(); + String basePath = "enchant_values." + enchantment.getKey(); EnchantmentRarity rarity = enchantment.defaultRarity(); defaultConfig.set(basePath + ".item", rarity.getItemValue()); diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java index 0303733..1d94ba2 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java @@ -12,6 +12,7 @@ import xyz.alexcrea.cuanvil.group.EnchantConflictGroup; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -226,12 +227,24 @@ public interface CAEnchantment { } /** - * Gets a list of all the unoptimised enchantments. - * @param name The enchantment name - * @return List of enchantment. + * Gets the enchantment by the provided name. + * @param name Name to fetch. + * @return Registered enchantment. null if absent. + * + * @deprecated use {@link #getListByName(String)} */ + @Deprecated(since = "1.6.3") static @Nullable CAEnchantment getByName(@NotNull String name){ return CAEnchantmentRegistry.getInstance().getByName(name); } + /** + * Gets list of enchantment using the provided name. + * @param name Name to fetch. + * @return List of registered enchantment. + */ + static List getListByName(@NotNull String name){ + return CAEnchantmentRegistry.getInstance().getListByName(name); + } + } diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java index 01262f9..1225209 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java +++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java @@ -22,7 +22,7 @@ public class CAEnchantmentRegistry { // Register enchantment functions private final HashMap byKeyMap; - private final HashMap byNameMap; + private final HashMap> byNameMap; private final SortedSet nameSortedEnchantments; @@ -62,6 +62,8 @@ public class CAEnchantmentRegistry { } + private static boolean hasWarnedRegistering = false; + /** * Can be used to register new enchantment. *

@@ -73,19 +75,25 @@ public class CAEnchantmentRegistry { public boolean register(@NotNull CAEnchantment enchantment){ if(byKeyMap.containsKey(enchantment.getKey())){ CustomAnvil.instance.getLogger().log(Level.WARNING, - "Duplicate registered enchantment. This should NOT happen.", + "Duplicate registered enchantment. This should NOT happen any time.\n" + + "If you are a custom anvil developer. You maybe custom anvil detected your enchantment as a bukkit enchantment. " + + "maybe remove enchantment with the same key before registering yours", new IllegalStateException(enchantment.getKey()+" enchantment was already registered")); return false; } - if(byNameMap.containsKey(enchantment.getName())){ + + if((!hasWarnedRegistering) && byNameMap.containsKey(enchantment.getName())){ + hasWarnedRegistering = true; + CustomAnvil.instance.getLogger().log(Level.WARNING, - "Duplicate registered enchantment name. There will have issue. " + - "\nI hope this do not happen to you on a production server. If it do, there is probably a plugin trying to register an enchantment with the same name than another one", - new IllegalStateException(enchantment.getKey()+" enchantment name was already registered")); + "Duplicate registered enchantment name. Please check that configuration is using namespace."); } byKeyMap.put(enchantment.getKey(), enchantment); - byNameMap.put(enchantment.getName(), enchantment); + + byNameMap.putIfAbsent(enchantment.getName(), new ArrayList<>()); + byNameMap.get(enchantment.getName()).add(enchantment); + nameSortedEnchantments.add(enchantment); if(!enchantment.isGetOptimised()){ @@ -112,7 +120,7 @@ public class CAEnchantmentRegistry { public boolean unregister(@Nullable CAEnchantment enchantment){ if(enchantment == null) return false; byKeyMap.remove(enchantment.getKey()); - byNameMap.remove(enchantment.getName()); + byNameMap.get(enchantment.getName()).remove(enchantment); nameSortedEnchantments.remove(enchantment); @@ -135,10 +143,26 @@ public class CAEnchantmentRegistry { * Gets the enchantment by the provided name. * @param name Name to fetch. * @return Registered enchantment. null if absent. + * + * @deprecated use {@link #getListByName(String)} */ + @Deprecated(since = "1.6.3") @Nullable public CAEnchantment getByName(@NotNull String name){ - return byNameMap.get(name); + List enchantments = getListByName(name); + if(enchantments.isEmpty()) return null; + + return enchantments.get(0); + } + + /** + * Gets list of enchantment using the provided name. + * @param name Name to fetch. + * @return List of registered enchantment. + */ + @NotNull + public List getListByName(@NotNull String name){ + return byNameMap.getOrDefault(name, Collections.emptyList()); } /** diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java index 6c44fac..a614536 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java +++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java @@ -44,24 +44,16 @@ public class EnchantCostConfigGui extends AbstractEnchantConfigGui displayLore, - int min, int max, int defaultItemVal, int defaultBookVal, - int... steps) { - return new EnchantCostSettingFactory( - title, parent, - configPath, config, - displayLore, - min, max, defaultItemVal, defaultBookVal, steps); - } - /** * A factory for an enchantment cost setting gui that hold setting's information. */ public static class EnchantCostSettingFactory extends IntSettingsGui.IntSettingFactory { int defaultBookVal; + @NotNull CAEnchantment enchantment; /** * Constructor for an enchantment cost setting gui factory. * - * @param title The title of the gui. - * @param parent Parent gui to go back when completed. - * @param configPath Configuration path of this setting. - * @param config Configuration holder of this setting. - * @param displayLore Gui display item lore. - * @param min Minimum value of this setting. - * @param max Maximum value of this setting. - * @param defaultItemVal Default item value if not found on the config. - * @param defaultBookVal Default book value if not found on the config. - * @param steps List of step the value can increment/decrement. - * List's size should be between 1 (included) and 3 (included). - * it is visually preferable to have an odd number of step. - * If step only contain 1 value, no step item should be displayed. + * @param title The title of the gui. + * @param parent Parent gui to go back when completed. + * @param configPath Configuration path of this setting. + * @param config Configuration holder of this setting. + * @param displayLore Gui display item lore. + * @param min Minimum value of this setting. + * @param max Maximum value of this setting. + * @param enchantment Enchantment to change the cost to + * @param steps List of step the value can increment/decrement. + * List's size should be between 1 (included) and 3 (included). + * it is visually preferable to have an odd number of step. + * If step only contain 1 value, no step item should be displayed. */ - protected EnchantCostSettingFactory( + public EnchantCostSettingFactory( @NotNull String title, ValueUpdatableGui parent, @NotNull String configPath, @NotNull ConfigHolder config, @Nullable List displayLore, - int min, int max, int defaultItemVal, int defaultBookVal, - int... steps) { + @NotNull CAEnchantment enchantment, + int min, int max, int... steps) { super(title, parent, configPath, config, displayLore, - min, max, defaultItemVal, steps); - this.defaultBookVal = defaultBookVal; + min, max, enchantment.defaultRarity().getItemValue(), + steps); + + this.defaultBookVal = enchantment.defaultRarity().getBookValue(); + this.enchantment = enchantment; } /** @@ -311,14 +285,14 @@ public class EnchantCostSettingsGui extends IntSettingsGui { */ @Override public int getConfiguredValue() { - return this.config.getConfig().getInt(this.configPath + ITEM_PATH, this.defaultVal); + return ConfigOptions.INSTANCE.enchantmentValue(enchantment, false); } /** * @return The configured value for the enchant setting book value. */ public int getConfiguredBookValue() { - return this.config.getConfig().getInt(this.configPath + BOOK_PATH, this.defaultBookVal); + return ConfigOptions.INSTANCE.enchantmentValue(enchantment, true); } @Override diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java b/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java index 3211497..af21989 100644 --- a/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java +++ b/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java @@ -45,13 +45,13 @@ public class Update_1_21 { addToStringList(groupConfig, "can_unbreak.groups", "mace"); // Add new enchant conflicts - addToStringList(conflictConfig, "restriction_density.enchantments", "density"); + addToStringList(conflictConfig, "restriction_density.enchantments", "minecraft:density"); addToStringList(conflictConfig, "restriction_density.notAffectedGroups", "mace", "enchanted_book"); - addToStringList(conflictConfig, "restriction_breach.enchantments", "breach"); + addToStringList(conflictConfig, "restriction_breach.enchantments", "minecraft:breach"); addToStringList(conflictConfig, "restriction_breach.notAffectedGroups", "mace", "enchanted_book"); - addToStringList(conflictConfig, "restriction_wind_burst.enchantments", "wind_burst"); + addToStringList(conflictConfig, "restriction_wind_burst.enchantments", "minecraft:wind_burst"); addToStringList(conflictConfig, "restriction_wind_burst.notAffectedGroups", "mace", "enchanted_book"); // Add mace to conflicts @@ -59,13 +59,14 @@ public class Update_1_21 { addToStringList(conflictConfig, "restriction_smite.notAffectedGroups", "mace"); addToStringList(conflictConfig, "restriction_bane_of_arthropods.notAffectedGroups", "mace"); - addToStringList(conflictConfig, "mace_enchant_conflict.enchantments", "density", "breach", "smite", "bane_of_arthropods"); + addToStringList(conflictConfig, "mace_enchant_conflict.enchantments", + "minecraft:density", "minecraft:breach", "minecraft:smite", "minecraft:bane_of_arthropods"); conflictConfig.set("mace_enchant_conflict.maxEnchantmentBeforeConflict", 1); // Add level limit - baseConfig.set("enchant_limits.density", 5); - baseConfig.set("enchant_limits.breach", 4); - baseConfig.set("enchant_limits.wind_burst", 3); + baseConfig.set("enchant_limits.minecraft:density", 5); + baseConfig.set("enchant_limits.minecraft:breach", 4); + baseConfig.set("enchant_limits.minecraft:wind_burst", 3); // Add enchant values baseConfig.set("enchant_values.density.item", 1); diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt index c6e7286..f192ff9 100644 --- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -301,21 +301,28 @@ object ConfigOptions { * Get the given [enchantment]'s limit */ fun enchantLimit(enchantment: CAEnchantment): Int { - return enchantLimit(enchantment.enchantmentName) + // Test namespace + var limit = enchantLimit(enchantment.key.toString()) + if(limit != null) return limit; + + // Test legacy (name only) + limit = enchantLimit(enchantment.enchantmentName) + if(limit != null) return limit; + + // get default (and test old legacy if present) + return getDefaultLevel(enchantment.enchantmentName) } /** * Get the given [enchantmentName]'s limit */ - private fun enchantLimit(enchantmentName: String): Int { - val default = getDefaultLevel(enchantmentName) + private fun enchantLimit(enchantmentName: String): Int? { val path = "${ENCHANT_LIMIT_ROOT}.$enchantmentName" return CustomAnvil.instance .config - .getInt(path, default) + .getInt(path, ENCHANT_LIMIT_RANGE.first-1) .takeIf { it in ENCHANT_LIMIT_RANGE } - ?: default } /** @@ -324,7 +331,9 @@ object ConfigOptions { private fun getDefaultLevel(enchantmentName: String, // compatibility with 1.20.5. TODO better update system ) : Int { if(enchantmentName == "sweeping_edge"){ - return enchantLimit("sweeping") + val limit = enchantLimit("sweeping") + if(limit != null) return limit + } return defaultEnchantLimit } @@ -337,7 +346,17 @@ object ConfigOptions { enchantment: CAEnchantment, isFromBook: Boolean ): Int { - return enchantmentValue(enchantment.enchantmentName, isFromBook) + // Test namespace + var limit = enchantmentValue(enchantment.key.toString(), isFromBook) + if(limit != null) return limit; + + // Test legacy (name only) + limit = enchantmentValue(enchantment.enchantmentName, isFromBook) + if(limit != null) return limit; + + // get default (and test old legacy if present) + return getDefaultValue(enchantment, isFromBook) + } /** @@ -347,36 +366,36 @@ object ConfigOptions { private fun enchantmentValue( enchantmentName: String, isFromBook: Boolean - ): Int { - val default = getDefaultValue(enchantmentName, isFromBook) - + ): Int? { val typeKey = if (isFromBook) KEY_BOOK else KEY_ITEM val path = "${ENCHANT_VALUES_ROOT}.${enchantmentName}.$typeKey" return CustomAnvil.instance .config - .getInt(path, default) + .getInt(path, DEFAULT_ENCHANT_VALUE - 1) .takeIf { it >= DEFAULT_ENCHANT_VALUE } - ?: DEFAULT_ENCHANT_VALUE } /** * Get default value if enchantment do not exist on config */ - private fun getDefaultValue(enchantmentName: String, // compatibility with 1.20.5. TODO better update system + private fun getDefaultValue(enchantment: CAEnchantment, // compatibility with 1.20.5. TODO better update system isFromBook: Boolean) : Int { - if(enchantmentName == "sweeping_edge"){ - return enchantmentValue("sweeping", isFromBook) + + val enchantmentName = enchantment.key.toString() + if(enchantmentName == "minecraft:sweeping_edge"){ + var limit = enchantmentValue("minecraft:sweeping", isFromBook) + if(limit != null) return limit + + // legacy name + limit = enchantmentValue("sweeping", isFromBook) + if(limit != null) return limit } - val enchantment = CAEnchantment.getByName(enchantmentName) - if(enchantment != null){ - val rarity = enchantment.defaultRarity() - - return if(isFromBook) rarity.bookValue - else rarity.itemValue - } - - return DEFAULT_ENCHANT_VALUE + val rarity = enchantment.defaultRarity() + return if(isFromBook) + rarity.bookValue + else + rarity.itemValue } /** @@ -384,28 +403,39 @@ object ConfigOptions { * a negative value would mean never disabled */ fun maxBeforeMergeDisabled(enchantment: CAEnchantment): Int { - return maxBeforeMergeDisabled(enchantment.enchantmentName) + val key = enchantment.key.toString() + var value = maxBeforeMergeDisabled(key) + if(value != null) return value + + // Legacy name + val legacy = enchantment.enchantmentName + value = maxBeforeMergeDisabled(legacy) + if(value != null) return value + + if(key == "minecraft:sweeping_edge"){ + value = maxBeforeMergeDisabled("minecraft:sweeping") + if(value != null) return value + + // legacy name of legacy enchantment name + value = maxBeforeMergeDisabled("sweeping") + if(value != null) return value + } + + return DEFAULT_MAX_BEFORE_MERGE_DISABLED } /** * Get the given [enchantmentName]'s level before merge is disabled * a negative value would mean never disabled */ - private fun maxBeforeMergeDisabled(enchantmentName: String) : Int { + private fun maxBeforeMergeDisabled(enchantmentName: String) : Int? { // find if set val path = "${DISABLE_MERGE_OVER_ROOT}.$enchantmentName" - val value = CustomAnvil.instance + return CustomAnvil.instance .config - .getInt(path, DEFAULT_MAX_BEFORE_MERGE_DISABLED) + .getInt(path, ENCHANT_LIMIT_RANGE.min() - 1) .takeIf { it in ENCHANT_LIMIT_RANGE } - ?: DEFAULT_MAX_BEFORE_MERGE_DISABLED; - - if((value == DEFAULT_MAX_BEFORE_MERGE_DISABLED) && (enchantmentName == "sweeping_edge")){ - return maxBeforeMergeDisabled("sweeping") - } - - return value } } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt index c58c6bf..56e923f 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt @@ -15,6 +15,9 @@ class EnchantConflictGroup( fun addEnchantment(enchant: CAEnchantment) { enchantments.add(enchant) } + fun addEnchantments(enchants: List) { + enchantments.addAll(enchants) + } fun allowed(enchants: Set, mat: Material): Boolean { if (enchantments.size < minBeforeBlock) { diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt index abebcbc..0c0850b 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt @@ -30,9 +30,9 @@ class EnchantConflictManager { const val DEFAULT_GROUP_NAME = "joinedGroup" // 1.20.5 compatibility TODO better update system - private val SWEEPING_EDGE_ENCHANT = + private val SWEEPING_EDGE_ENCHANT = Collections.singletonList( CAEnchantment.getByKey(NamespacedKey.minecraft("sweeping_edge")) ?: - CAEnchantment.getByKey(Enchantment.SWEEPING_EDGE.key) + CAEnchantment.getByKey(Enchantment.SWEEPING_EDGE.key)) } @@ -94,14 +94,14 @@ class EnchantConflictManager { // Read and add enchantment to conflict val enchantList = section.getStringList(ENCH_LIST_PATH) for (enchantName in enchantList) { - val enchant = getEnchantByName(enchantName) - if (enchant == null) { + val enchants = getEnchantByIdentifier(enchantName) + if (enchants.isEmpty()) { if (!futureUse) { //TODO future use will be deprecated once the new update system is finished CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict $conflictName") } continue } - conflict.addEnchantment(enchant) + conflict.addEnchantments(enchants) } if (conflict.getEnchants().isEmpty()) { if (!futureUse) { //TODO future use will be deprecated once the new update system is finished @@ -112,16 +112,23 @@ class EnchantConflictManager { return conflict } - private fun getEnchantByName(enchantName: String): CAEnchantment? { + private fun getEnchantByIdentifier(enchantName: String): List { + val key = NamespacedKey.fromString(enchantName) + if(key != null){ + val enchantment = CAEnchantment.getByKey(key) + if(enchantment != null) return Collections.singletonList(enchantment) + + } // Temporary solution for 1.20.5 when(enchantName){ - "sweeping", "sweeping_edge" -> { + "minecraft:sweeping", "sweeping", + "minecraft:sweeping_edge", "sweeping_edge" -> { return SWEEPING_EDGE_ENCHANT } } - return CAEnchantment.getByName(enchantName) + return CAEnchantment.getListByName(enchantName) } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 82b28a8..dd24558 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -97,46 +97,46 @@ default_limit: 5 # # Valid range of 1 - 255 for each enchantment enchant_limits: - aqua_affinity: 1 - binding_curse: 1 - channeling: 1 - flame: 1 - infinity: 1 - mending: 1 - multishot: 1 - silk_touch: 1 - vanishing_curse: 1 - depth_strider: 3 # anything more than 3 is treated as 3 by the game - protection: 4 - fire_protection: 4 - blast_protection: 4 - projectile_protection: 4 - feather_falling: 4 - thorns: 3 - respiration: 3 - sharpness: 5 - smite: 5 - bane_of_arthropods: 5 - knockback: 2 - fire_aspect: 2 - looting: 3 - sweeping: 3 - sweeping_edge: 3 - efficiency: 5 - unbreaking: 3 - fortune: 3 - power: 5 - punch: 2 - luck_of_the_sea: 3 - lure: 3 - frost_walker: 2 - impaling: 5 - riptide: 3 - loyalty: 3 - piercing: 4 - quick_charge: 3 - soul_speed: 3 - swift_sneak: 3 + minecraft:aqua_affinity: 1 + minecraft:binding_curse: 1 + minecraft:channeling: 1 + minecraft:flame: 1 + minecraft:infinity: 1 + minecraft:mending: 1 + minecraft:multishot: 1 + minecraft:silk_touch: 1 + minecraft:vanishing_curse: 1 + minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game + minecraft:protection: 4 + minecraft:fire_protection: 4 + minecraft:blast_protection: 4 + minecraft:projectile_protection: 4 + minecraft:feather_falling: 4 + minecraft:thorns: 3 + minecraft:respiration: 3 + minecraft:sharpness: 5 + minecraft:smite: 5 + minecraft:bane_of_arthropods: 5 + minecraft:knockback: 2 + minecraft:fire_aspect: 2 + minecraft:looting: 3 + minecraft:sweeping: 3 + minecraft:sweeping_edge: 3 + minecraft:efficiency: 5 + minecraft:unbreaking: 3 + minecraft:fortune: 3 + minecraft:power: 5 + minecraft:punch: 2 + minecraft:luck_of_the_sea: 3 + minecraft:lure: 3 + minecraft:frost_walker: 2 + minecraft:impaling: 5 + minecraft:riptide: 3 + minecraft:loyalty: 3 + minecraft:piercing: 4 + minecraft:quick_charge: 3 + minecraft:soul_speed: 3 + minecraft:swift_sneak: 3 # Multipliers used to calculate the enchantment's value in repair/combining # @@ -150,124 +150,124 @@ enchant_limits: # With default values protection 4 would have a value of 4 when # coming from either a book (4 * 1) or an item (4 * 1) enchant_values: - aqua_affinity: + minecraft:aqua_affinity: item: 4 book: 2 - bane_of_arthropods: + minecraft:bane_of_arthropods: item: 2 book: 1 - binding_curse: + minecraft:binding_curse: item: 8 book: 4 - blast_protection: + minecraft:blast_protection: item: 4 book: 2 - channeling: + minecraft:channeling: item: 8 book: 4 - depth_strider: + minecraft:depth_strider: item: 4 book: 2 - efficiency: + minecraft:efficiency: item: 1 book: 1 - flame: + minecraft:flame: item: 4 book: 2 - feather_falling: + minecraft:feather_falling: item: 2 book: 1 - fire_aspect: + minecraft:fire_aspect: item: 4 book: 2 - fire_protection: + minecraft:fire_protection: item: 2 book: 1 - fortune: + minecraft:fortune: item: 4 book: 2 - frost_walker: + minecraft:frost_walker: item: 4 book: 2 - impaling: + minecraft:impaling: item: 4 book: 2 - infinity: + minecraft:infinity: item: 8 book: 4 - knockback: + minecraft:knockback: item: 2 book: 1 - looting: + minecraft:looting: item: 4 book: 2 - loyalty: + minecraft:loyalty: item: 1 book: 1 - luck_of_the_sea: + minecraft:luck_of_the_sea: item: 4 book: 2 - lure: + minecraft:lure: item: 4 book: 2 - mending: + minecraft:mending: item: 4 book: 2 - multishot: + minecraft:multishot: item: 4 book: 2 - piercing: + minecraft:piercing: item: 1 book: 1 - power: + minecraft:power: item: 1 book: 1 - projectile_protection: + minecraft:projectile_protection: item: 2 book: 1 - protection: + minecraft:protection: item: 1 book: 1 - punch: + minecraft:punch: item: 4 book: 2 - quick_charge: + minecraft:quick_charge: item: 2 book: 1 - respiration: + minecraft:respiration: item: 4 book: 2 - riptide: + minecraft:riptide: item: 4 book: 2 - silk_touch: + minecraft:silk_touch: item: 8 book: 4 - sharpness: + minecraft:sharpness: item: 1 book: 1 - smite: + minecraft:smite: item: 2 book: 1 - soul_speed: + minecraft:soul_speed: item: 8 book: 4 - swift_sneak: + minecraft:swift_sneak: item: 8 book: 4 - sweeping: + minecraft:sweeping: item: 4 book: 2 - sweeping_edge: + minecraft:sweeping_edge: item: 4 book: 2 - thorns: + minecraft:thorns: item: 8 book: 4 - unbreaking: + minecraft:unbreaking: item: 2 book: 1 - vanishing_curse: + minecraft:vanishing_curse: item: 8 book: 4 @@ -278,9 +278,9 @@ enchant_values: # -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent. disable-merge-over: # Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla) - sharpness: -1 + minecraft:sharpness: -1 # If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied - #unbreaking: 2 + #minecraft:unbreaking: 2 # Whether to show debug logging debug_log: false diff --git a/src/main/resources/enchant_conflict.yml b/src/main/resources/enchant_conflict.yml index ed154ab..0e8b3f3 100644 --- a/src/main/resources/enchant_conflict.yml +++ b/src/main/resources/enchant_conflict.yml @@ -17,158 +17,158 @@ # ---------------------------------------------------- restriction_aqua_affinity: - enchantments: [ aqua_affinity ] + enchantments: [ minecraft:aqua_affinity ] notAffectedGroups: [ enchanted_book, helmets ] restriction_bane_of_arthropods: - enchantments: [ bane_of_arthropods ] + enchantments: [ minecraft:bane_of_arthropods ] notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_blast_protection: - enchantments: [ blast_protection ] + enchantments: [ minecraft:blast_protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_channeling: - enchantments: [ channeling ] + enchantments: [ minecraft:channeling ] notAffectedGroups: [ enchanted_book, trident ] restriction_binding_curse: - enchantments: [ binding_curse ] + enchantments: [ minecraft:binding_curse ] notAffectedGroups: [ enchanted_book, wearable ] restriction_vanishing_curse: - enchantments: [ vanishing_curse ] + enchantments: [ minecraft:vanishing_curse ] notAffectedGroups: [ enchanted_book, can_vanish ] restriction_depth_strider: - enchantments: [ depth_strider ] + enchantments: [ minecraft:depth_strider ] notAffectedGroups: [ enchanted_book, boots ] restriction_efficiency: - enchantments: [ efficiency ] + enchantments: [ minecraft:efficiency ] notAffectedGroups: [ enchanted_book, tools, shears ] restriction_feather_falling: - enchantments: [ feather_falling ] + enchantments: [ minecraft:feather_falling ] notAffectedGroups: [ enchanted_book, boots ] restriction_fire_aspect: - enchantments: [ fire_aspect ] + enchantments: [ minecraft:fire_aspect ] notAffectedGroups: [ enchanted_book, swords ] restriction_fire_protection: - enchantments: [ fire_protection ] + enchantments: [ minecraft:fire_protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_flame: - enchantments: [ flame ] + enchantments: [ minecraft:flame ] notAffectedGroups: [ enchanted_book, bow ] restriction_fortune: - enchantments: [ fortune ] + enchantments: [ minecraft:fortune ] notAffectedGroups: [ enchanted_book, tools ] restriction_frost_walker: - enchantments: [ frost_walker ] + enchantments: [ minecraft:frost_walker ] notAffectedGroups: [ enchanted_book, boots ] restriction_impaling: - enchantments: [ impaling ] + enchantments: [ minecraft:impaling ] notAffectedGroups: [ enchanted_book, trident ] restriction_infinity: - enchantments: [ infinity ] + enchantments: [ minecraft:infinity ] notAffectedGroups: [ enchanted_book, bow ] restriction_knockback: - enchantments: [ knockback ] + enchantments: [ minecraft:knockback ] notAffectedGroups: [ enchanted_book, swords ] restriction_looting: - enchantments: [ looting ] + enchantments: [ minecraft:looting ] notAffectedGroups: [ enchanted_book, swords ] restriction_loyalty: - enchantments: [ loyalty ] + enchantments: [ minecraft:loyalty ] notAffectedGroups: [ enchanted_book, trident ] restriction_lure: - enchantments: [ lure ] + enchantments: [ minecraft:lure ] notAffectedGroups: [ enchanted_book, fishing_rod ] restriction_mending: - enchantments: [ mending ] + enchantments: [ minecraft:mending ] notAffectedGroups: [ enchanted_book, can_unbreak ] -restriction_multishot: - enchantments: [ multishot ] +restriction_minecraft_multishot: + enchantments: [ minecraft:multishot ] notAffectedGroups: [ enchanted_book, crossbow ] restriction_piercing: - enchantments: [ piercing ] + enchantments: [ minecraft:piercing ] notAffectedGroups: [ enchanted_book, crossbow ] restriction_power: - enchantments: [ power ] + enchantments: [ minecraft:power ] notAffectedGroups: [ enchanted_book, bow ] restriction_projectile_protection: - enchantments: [ projectile_protection ] + enchantments: [ minecraft:projectile_protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_protection: - enchantments: [ protection ] + enchantments: [ minecraft:protection ] notAffectedGroups: [ enchanted_book, armors ] restriction_punch: - enchantments: [ punch ] + enchantments: [ minecraft:punch ] notAffectedGroups: [ enchanted_book, bow ] restriction_quick_charge: - enchantments: [ quick_charge ] + enchantments: [ minecraft:quick_charge ] notAffectedGroups: [ enchanted_book, crossbow ] restriction_respiration: - enchantments: [ respiration ] + enchantments: [ minecraft:respiration ] notAffectedGroups: [ enchanted_book, helmets ] restriction_riptide: - enchantments: [ riptide ] + enchantments: [ minecraft:riptide ] notAffectedGroups: [ enchanted_book, trident ] restriction_sharpness: - enchantments: [ sharpness ] + enchantments: [ minecraft:sharpness ] notAffectedGroups: [ enchanted_book, melee_weapons ] -restriction_silk_touch: - enchantments: [ silk_touch ] +restriction__silk_touch: + enchantments: [ minecraft:silk_touch ] notAffectedGroups: [ enchanted_book, tools ] restriction_smite: - enchantments: [ smite ] + enchantments: [ minecraft:smite ] notAffectedGroups: [ enchanted_book, melee_weapons ] restriction_soul_speed: - enchantments: [ soul_speed ] + enchantments: [ minecraft:soul_speed ] notAffectedGroups: [ enchanted_book, boots ] restriction_sweeping_edge: - enchantments: [ sweeping, sweeping_edge ] + enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ] notAffectedGroups: [ enchanted_book, swords ] # Do not exist in 1.18, that mean useInFuture will be set to true # useInFuture set to true also mean it will not warn if there is an issue restriction_swift_sneak: useInFuture: true - enchantments: [ swift_sneak ] + enchantments: [ minecraft:swift_sneak ] notAffectedGroups: [ enchanted_book, leggings ] restriction_thorns: - enchantments: [ thorns ] + enchantments: [ minecraft:thorns ] notAffectedGroups: [ enchanted_book, armors ] -restriction_unbreaking: - enchantments: [ unbreaking ] +restriction__unbreaking: + enchantments: [ minecraft:unbreaking ] notAffectedGroups: [ enchanted_book, can_unbreak ] # ---------------------------------------------------- @@ -180,60 +180,60 @@ restriction_unbreaking: sword_enchant_conflict: enchantments: - - bane_of_arthropods - - smite - - sharpness + - minecraft:bane_of_arthropods + - minecraft:smite + - minecraft:sharpness notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 protection_enchant_conflict: enchantments: - - blast_protection - - fire_protection - - projectile_protection - - protection + - minecraft:blast_protection + - minecraft:fire_protection + - minecraft:projectile_protection + - minecraft:protection notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict1: enchantments: - - channeling - - riptide + - minecraft:channeling + - minecraft:riptide notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 trident_conflict2: enchantments: - - loyalty - - riptide + - minecraft:loyalty + - minecraft:riptide notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 boot_conflict: enchantments: - - depth_strider - - frost_walker + - minecraft:depth_strider + - minecraft:frost_walker notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 tool_conflict: enchantments: - - fortune - - silk_touch + - minecraft:fortune + - minecraft:silk_touch notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 bow_conflict: enchantments: - - mending - - infinity + - minecraft:mending + - minecraft:infinity notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 crossbow_conflict: enchantments: - - multishot - - piercing + - minecraft:multishot + - minecraft:piercing notAffectedGroups: [ ] maxEnchantmentBeforeConflict: 1 From 7c283dc7f8407d94724dbc572e31053f6bd44ea9 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 15 Oct 2024 06:46:05 +0200 Subject: [PATCH 4/6] Splitted main logic to be more readable --- .../io/delilaheve/AnvilEventListener.kt | 580 ------------------ src/main/kotlin/io/delilaheve/CustomAnvil.kt | 7 +- .../dependency/DisenchantmentDependency.kt | 6 +- .../cuanvil/listener/AnvilCloseListener.kt | 23 + .../cuanvil/listener/AnvilResultListener.kt | 292 +++++++++ .../cuanvil/listener/PrepareAnvilListener.kt | 206 +++++++ .../alexcrea/cuanvil/util/AnvilColorUtil.kt | 88 +++ .../alexcrea/cuanvil/util/CustomRecipeUtil.kt | 54 ++ 8 files changed, 672 insertions(+), 584 deletions(-) delete mode 100644 src/main/kotlin/io/delilaheve/AnvilEventListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt diff --git a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt deleted file mode 100644 index bdb1ac9..0000000 --- a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt +++ /dev/null @@ -1,580 +0,0 @@ -package io.delilaheve - -import io.delilaheve.util.ConfigOptions -import io.delilaheve.util.EnchantmentUtil.combineWith -import io.delilaheve.util.ItemUtil.canMergeWith -import io.delilaheve.util.ItemUtil.findEnchantments -import io.delilaheve.util.ItemUtil.isEnchantedBook -import io.delilaheve.util.ItemUtil.repairFrom -import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe -import io.delilaheve.util.ItemUtil.unitRepair -import org.bukkit.ChatColor -import org.bukkit.GameMode -import org.bukkit.Material -import org.bukkit.entity.HumanEntity -import org.bukkit.entity.Player -import org.bukkit.event.Event -import org.bukkit.event.EventHandler -import org.bukkit.event.EventPriority.HIGHEST -import org.bukkit.event.Listener -import org.bukkit.event.inventory.ClickType -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.event.inventory.InventoryCloseEvent -import org.bukkit.event.inventory.PrepareAnvilEvent -import org.bukkit.inventory.AnvilInventory -import org.bukkit.inventory.InventoryView.Property.REPAIR_COST -import org.bukkit.inventory.ItemStack -import xyz.alexcrea.cuanvil.config.ConfigHolder -import xyz.alexcrea.cuanvil.dependency.DependencyManager -import xyz.alexcrea.cuanvil.dependency.packet.PacketManager -import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.calculatePenalty -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.getRightValues -import xyz.alexcrea.cuanvil.util.AnvilXpUtil.setAnvilInvXp -import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair -import java.util.regex.Matcher -import java.util.regex.Pattern -import kotlin.math.min - - -/** - * Listener for anvil events - */ -class AnvilEventListener(private val packetManager: PacketManager) : Listener { - - companion object { - // Anvil's output slot - const val ANVIL_INPUT_LEFT = 0 - const val ANVIL_INPUT_RIGHT = 1 - const val ANVIL_OUTPUT_SLOT = 2 - - // static slot container - private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0) - private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0) - } - - /** - * Event handler logic for when an anvil contains items to be combined - */ - @EventHandler(priority = HIGHEST, ignoreCancelled = true) - fun anvilCombineCheck(event: PrepareAnvilEvent) { - // Test if the event should bypass custom anvil. - if(DependencyManager.tryEventPreAnvilBypass(event)) return - - val inventory = event.inventory - val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return - val second = inventory.getItem(ANVIL_INPUT_RIGHT) - - // Should find player - val player = event.view.player - if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return - - // Test custom recipe - val recipe = getCustomRecipe(first, second) - CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") - if(recipe != null){ - val amount = getCustomRecipeAmount(recipe, first, second) - - val resultItem: ItemStack = recipe.resultItem!!.clone() - resultItem.amount *= amount - - event.result = resultItem - setAnvilInvXp(inventory, event.view, recipe.xpCostPerCraft * amount, true) - - return - } - - // Test rename lonely item - if (second == null) { - val resultItem = first.clone() - var anvilCost = handleRename(resultItem, inventory, player) - - // Test/stop if nothing changed. - if (first == resultItem) { - CustomAnvil.log("no right item, But input is same as output") - event.result = null - return - } - - event.result = resultItem - - anvilCost += calculatePenalty(first, null, resultItem) - - setAnvilInvXp(inventory, event.view, anvilCost) - return - } - - // Test for merge - if (first.canMergeWith(second)) { - val newEnchants = first.findEnchantments() - .combineWith(second.findEnchantments(), first, player) - val resultItem = first.clone() - resultItem.setEnchantmentsUnsafe(newEnchants) - - // Calculate enchantment cost - var anvilCost = getRightValues(second, resultItem) - // Calculate repair cost - if (!first.isEnchantedBook() && !second.isEnchantedBook()) { - // we only need to be concerned with repair when neither item is a book - val repaired = resultItem.repairFrom(first, second) - anvilCost += if (repaired) ConfigOptions.itemRepairCost else 0 - } - - // Test/stop if nothing changed. - if (first == resultItem) { - CustomAnvil.log("Mergable with second, But input is same as output") - event.result = null - return - } - // As calculatePenalty edit result, we need to calculate penalty after checking equality - anvilCost += calculatePenalty(first, second, resultItem) - // Calculate rename cost - anvilCost += handleRename(resultItem, inventory, player) - - // Finally, we set result - event.result = resultItem - - setAnvilInvXp(inventory, event.view, anvilCost) - return - } - - // Test for unit repair - val unitRepairAmount = first.getRepair(second) - if (unitRepairAmount != null) { - val resultItem = first.clone() - var anvilCost = handleRename(resultItem, inventory, player) - - val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount) - if (repairAmount > 0) { - anvilCost += repairAmount * ConfigOptions.unitRepairCost - } - // We do not care about right item penalty for unit repair - anvilCost += calculatePenalty(first, null, resultItem, true) - - // Test/stop if nothing changed. - if (first == resultItem) { - CustomAnvil.log("unit repair, But input is same as output") - event.result = null - return - } - event.result = resultItem - - setAnvilInvXp(inventory, event.view, anvilCost) - } else { - CustomAnvil.log("no anvil fuse type found") - event.result = null - } - - } - - private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { - // Rename item and add renaming cost - resultItem.itemMeta?.let { - val displayName = ChatColor.stripColor(it.displayName) - var inventoryName = ChatColor.stripColor(inventory.renameText) - - var sumCost = 0 - - var useColor = false - if(ConfigOptions.renameColorPossible){ - val resultString = StringBuilder(inventoryName) - - useColor = handleRenamingColor(resultString, player) - - if(useColor) { - inventoryName = resultString.toString() - - sumCost+= ConfigOptions.useOfColorCost - } - } - - if ((!useColor && (!displayName.contentEquals(inventoryName))) || (useColor && !(it.displayName).contentEquals(inventoryName))) { - it.setDisplayName(inventoryName) - resultItem.itemMeta = it - - sumCost+= ConfigOptions.itemRenameCost - } - - return sumCost - } - return 0 - } - - private fun handleRenamingColor(textToColor: StringBuilder, player: HumanEntity): Boolean { - val usePermission = ConfigOptions.permissionNeededForColor - val canUseColorCode = ConfigOptions.allowColorCode && (!usePermission || player.hasPermission("ca.color.code")) - val canUseHexColor = ConfigOptions.allowHexadecimalColor && (!usePermission || player.hasPermission("ca.color.hex")) - - if((!canUseColorCode) && (!canUseHexColor)) return false - - var useColor = false - // Handle color code - if(canUseColorCode){ - var nbReplacement = replaceAll(textToColor, "&", "§", 2) - nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2) - - if(nbReplacement > 0) useColor = true - } - - if(canUseHexColor){ - val nbReplacement = replaceHexToColor(textToColor, 7) - - if(nbReplacement > 0) useColor = true - } - - return useColor - } - - /** - * Replace every instance of "from" to "to". - * @param builder The builder to replace the string from. - * @param from The source that should be replaced. - * @param to The string that should replace. - * @param endOffset Amount of character that should be ignored at the end. - * @return The number of replacement was that was done. - */ - private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int { - var index = builder.indexOf(from) - var numberOfChanges = 0 - - while (index != -1 && index < builder.length - endOffset) { - builder.replace(index, index + from.length, to) - index += to.length - index = builder.indexOf(from, index) - - numberOfChanges+=1 - } - - return numberOfChanges - } - - val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string - /** - * Replace every hex color formatted like #000000 to the minecraft format - * @param builder The builder to replace the hex color from. - * @param endOffset Amount of character that should be ignored at the end. - * @return The number of replacement was that was done. - */ - private fun replaceHexToColor(builder: StringBuilder, endOffset: Int): Int { - val matcher: Matcher = HEX_PATTERN.matcher(builder) - - var numberOfChanges = 0 - var startIndex = 0 - - while(matcher.find(startIndex)){ - startIndex = matcher.start() - if(startIndex >= builder.length - endOffset) break - - builder.replace(startIndex, startIndex + 1, "§x") - startIndex+=2 - for (i in 0..5) { - builder.insert(startIndex, '§') - startIndex+=2 - } - - numberOfChanges+=1 - } - - return numberOfChanges - } - - /** - * Event handler logic for when a player is trying to pull an item out of the anvil - */ - @EventHandler(ignoreCancelled = true) - fun anvilExtractionCheck(event: InventoryClickEvent) { - val player = event.whoClicked as? Player ?: return - if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return - val inventory = event.inventory as? AnvilInventory ?: return - - if (event.rawSlot != ANVIL_OUTPUT_SLOT) { - return - } - // Test if the event should bypass custom anvil. - if(DependencyManager.tryClickAnvilResultBypass(event, inventory)) return - - val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return - val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return - val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT) - - // Test custom recipe - val recipe = getCustomRecipe(leftItem, rightItem) - if(recipe != null){ - event.result = Event.Result.ALLOW - onCustomCraft( - event, recipe, player, - leftItem, rightItem, output, inventory) - return - } - - val canMerge = leftItem.canMergeWith(rightItem) - val unitRepairResult = leftItem.getRepair(rightItem) - val allowed = (rightItem == null) - || (canMerge) - || (unitRepairResult != null) - - // True if there was no change or not allowed - if ((output == inventory.getItem(ANVIL_INPUT_LEFT)) - || !allowed - ) { - event.result = Event.Result.DENY - return - } - if (rightItem == null) { - event.result = Event.Result.ALLOW - return - } - if (canMerge) { - event.result = Event.Result.ALLOW - } else if (unitRepairResult != null) { - onUnitRepairExtract( - leftItem, rightItem, output, - unitRepairResult, event, player, inventory - ) - - return - } - } - - private fun onCustomCraft(event: InventoryClickEvent, - recipe: AnvilCustomRecipe, - player: Player, - leftItem: ItemStack, - rightItem: ItemStack?, - output: ItemStack, - inventory: AnvilInventory) { - event.result = Event.Result.DENY - - if(recipe.leftItem == null) return // in case it changed - - val amount = getCustomRecipeAmount(recipe, leftItem, rightItem) - val xpCost = amount * recipe.xpCostPerCraft - - if ((player.gameMode != GameMode.CREATIVE) && (player.level < xpCost)) return - - // We give the item manually - // But first we check if we should give the item - val slotDestination = getActionSlot(event, player) - if (slotDestination.type == SlotType.NO_SLOT) return - - // If not creative middle click... - if (event.click != ClickType.MIDDLE) { - // We remove what should be removed - leftItem.amount -= amount * recipe.leftItem!!.amount - inventory.setItem(ANVIL_INPUT_LEFT, leftItem) - - if(rightItem != null){ - if(recipe.rightItem == null) return // in case it changed - - rightItem.amount -= amount * recipe.rightItem!!.amount - inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) - } - - if(player.gameMode != GameMode.CREATIVE){ - player.level -= xpCost - } - - // Then we try to find the new values for the anvil - val newAmount = getCustomRecipeAmount(recipe, leftItem, rightItem) - - CustomAnvil.verboseLog("new amount is $newAmount") - if(newAmount <= 0 || recipe.exactCount){ - inventory.setItem(ANVIL_OUTPUT_SLOT, null) - }else{ - val resultItem: ItemStack = recipe.resultItem!!.clone() - resultItem.amount *= newAmount - - val newXp = newAmount * newAmount - - inventory.repairCost = newXp - event.view.setProperty(REPAIR_COST, newXp) - - inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem) - - player.updateInventory() - } - } - - // Finally, we add the item to the player - if (slotDestination.type == SlotType.CURSOR) { - player.setItemOnCursor(output) - } else {// We assume SlotType == SlotType.INVENTORY - player.inventory.setItem(slotDestination.slot, output) - } - - - } - - private fun onUnitRepairExtract( - leftItem: ItemStack, - rightItem: ItemStack, - output: ItemStack, - unitRepairResult: Double, - event: InventoryClickEvent, - player: Player, - inventory: AnvilInventory - ) { - val resultCopy = leftItem.clone() - val resultAmount = resultCopy.unitRepair( - rightItem.amount, unitRepairResult - ) - - // To avoid vanilla, we cancel the event for unit repair - event.result = Event.Result.DENY - event.isCancelled = true - // And we give the item manually - // But first we check if we should give the item - val slotDestination = getActionSlot(event, player) - if (slotDestination.type == SlotType.NO_SLOT) return - - // Test repair cost - var repairCost = 0 - if (player.gameMode != GameMode.CREATIVE) { - // Get repairCost - leftItem.itemMeta?.let { leftMeta -> - val leftName = leftMeta.displayName - output.itemMeta?.let { - // Rename cost - if (!leftName.contentEquals(it.displayName)) { - repairCost += ConfigOptions.itemRenameCost - - // Color cost - if(it.displayName.contains('§')){ - repairCost += ConfigOptions.useOfColorCost - } - } - } - } - - repairCost += calculatePenalty(leftItem, null, resultCopy) - repairCost += resultAmount * ConfigOptions.unitRepairCost - - if ( - !ConfigOptions.doRemoveCostLimit && - ConfigOptions.doCapCost) { - - repairCost = min(repairCost, ConfigOptions.maxAnvilCost) - } - - if ((inventory.maximumRepairCost <= repairCost) - || (player.level < repairCost) - ) return - } - // If not creative middle click... - if (event.click != ClickType.MIDDLE) { - // We remove what should be removed - inventory.setItem(ANVIL_INPUT_LEFT, null) - rightItem.amount -= resultAmount - inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) - inventory.setItem(ANVIL_OUTPUT_SLOT, null) - player.level -= repairCost - } - - // Finally, we add the item to the player - if (slotDestination.type == SlotType.CURSOR) { - player.setItemOnCursor(output) - } else {// We assume SlotType == SlotType.INVENTORY - player.inventory.setItem(slotDestination.slot, output) - } - } - - /** - * Get the destination slot or "NO_SLOT" slot container if there is no slot available - */ - private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer { - if (event.isShiftClick) { - val inventory = player.inventory - val firstEmpty = inventory.firstEmpty() - if (firstEmpty == -1) { - return NO_SLOT - } - //check hotbare full - var slotIndex = 8 - while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { - slotIndex-- - } - if (slotIndex >= 0) { - return SlotContainer(SlotType.INVENTORY, slotIndex) - } - slotIndex = 35 //4*9 - 1 (max of player inventory) - while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { - slotIndex-- - } - if (slotIndex < 9) { - return NO_SLOT - } - return SlotContainer(SlotType.INVENTORY, slotIndex) - } else { - if (player.itemOnCursor.type != Material.AIR) { - return NO_SLOT - } - return CURSOR_SLOT - } - } - - private fun getCustomRecipe ( - leftItem: ItemStack, - rightItem: ItemStack?) : AnvilCustomRecipe? { - - val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null - - CustomAnvil.verboseLog("Testing " + recipeList.size+" recipe...") - for (recipe in recipeList) { - if(recipe.testItem(leftItem, rightItem)){ - return recipe - } - } - - return null - } - - private fun getCustomRecipeAmount( - recipe: AnvilCustomRecipe, - leftItem: ItemStack, - rightItem: ItemStack? - ): Int{ - return if(recipe.exactCount) { - if(leftItem.amount != recipe.leftItem!!.amount){ - 0 - }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){ - 0 - }else{ - 1 - } - } - else { - // test amount - val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us - val maxResultAmount = resultItem.type.maxStackSize/resultItem.amount - val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount - val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount } - - CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount") - - min(min(maxResultAmount, maxLeftAmount), maxRightAmount) - } - } - - - @EventHandler - fun onAnvilClose(event: InventoryCloseEvent){ - val player = event.player - if(event.inventory !is AnvilInventory) return - if(player is Player && GameMode.CREATIVE != player.gameMode){ - packetManager.setInstantBuild(player, false) - } - - } - -} - - - - -private class SlotContainer(val type: SlotType, val slot: Int) -private enum class SlotType { - CURSOR, - INVENTORY, - NO_SLOT - -} diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt index cbeb5cd..285e1c5 100644 --- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt +++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt @@ -13,7 +13,10 @@ import xyz.alexcrea.cuanvil.dependency.DependencyManager import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry import xyz.alexcrea.cuanvil.gui.config.MainConfigGui import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant +import xyz.alexcrea.cuanvil.listener.AnvilCloseListener +import xyz.alexcrea.cuanvil.listener.AnvilResultListener import xyz.alexcrea.cuanvil.listener.ChatEventListener +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.update.PluginSetDefault import xyz.alexcrea.cuanvil.update.Update_1_21 import xyz.alexcrea.cuanvil.update.plugin.PluginUpdates @@ -110,7 +113,9 @@ class CustomAnvil : JavaPlugin() { DependencyManager.loadDependency() // Register anvil events - server.pluginManager.registerEvents(AnvilEventListener(DependencyManager.packetManager), this) + server.pluginManager.registerEvents(PrepareAnvilListener(), this) + server.pluginManager.registerEvents(AnvilResultListener(), this) + server.pluginManager.registerEvents(AnvilCloseListener(DependencyManager.packetManager), this) // Load metrics Metrics(this, bstatsPluginId) diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt index be27c05..92ee146 100644 --- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt @@ -4,13 +4,13 @@ import cz.kominekjan.disenchantment.events.DisenchantClickEvent import cz.kominekjan.disenchantment.events.DisenchantEvent import cz.kominekjan.disenchantment.events.ShatterClickEvent import cz.kominekjan.disenchantment.events.ShatterEvent -import io.delilaheve.AnvilEventListener import io.delilaheve.CustomAnvil import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.PrepareAnvilEvent import org.bukkit.inventory.AnvilInventory import org.bukkit.inventory.ItemStack import org.bukkit.plugin.RegisteredListener +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener import xyz.alexcrea.cuanvil.util.AnvilXpUtil class DisenchantmentDependency { @@ -96,7 +96,7 @@ class DisenchantmentDependency { } fun testAnvilResult(event: InventoryClickEvent, inventory: AnvilInventory): Boolean { - val previousResultSlot = inventory.getItem(AnvilEventListener.ANVIL_OUTPUT_SLOT)?.clone() + val previousResultSlot = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT)?.clone() // Test event if change the result itemClickEvent.onDisenchantmentClickEvent(event) @@ -115,7 +115,7 @@ class DisenchantmentDependency { } private fun testAnvilInventoryChange(inventory: AnvilInventory, previous: ItemStack?): Boolean { - val currentResult = inventory.getItem(AnvilEventListener.ANVIL_OUTPUT_SLOT) + val currentResult = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT) return currentResult == previous } diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt new file mode 100644 index 0000000..60a0339 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt @@ -0,0 +1,23 @@ +package xyz.alexcrea.cuanvil.listener + +import org.bukkit.GameMode +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.inventory.AnvilInventory +import xyz.alexcrea.cuanvil.dependency.packet.PacketManager + +class AnvilCloseListener(private val packetManager: PacketManager) : Listener { + + @EventHandler + fun onAnvilClose(event: InventoryCloseEvent){ + val player = event.player + if(event.inventory !is AnvilInventory) return + if(player is Player && GameMode.CREATIVE != player.gameMode){ + packetManager.setInstantBuild(player, false) + } + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt new file mode 100644 index 0000000..fbc603d --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt @@ -0,0 +1,292 @@ +package xyz.alexcrea.cuanvil.listener + +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.ItemUtil.canMergeWith +import io.delilaheve.util.ItemUtil.unitRepair +import org.bukkit.GameMode +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.ClickType +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_LEFT +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_RIGHT +import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT_SLOT +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe +import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.CustomRecipeUtil +import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair +import kotlin.math.min + +class AnvilResultListener: Listener { + + companion object { + // static slot container + private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0) + private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0) + } + + /** + * Event handler logic for when a player is trying to pull an item out of the anvil + */ + @EventHandler(ignoreCancelled = true) + fun anvilExtractionCheck(event: InventoryClickEvent) { + val player = event.whoClicked as? Player ?: return + if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + val inventory = event.inventory as? AnvilInventory ?: return + + if (event.rawSlot != ANVIL_OUTPUT_SLOT) { + return + } + // Test if the event should bypass custom anvil. + if(DependencyManager.tryClickAnvilResultBypass(event, inventory)) return + + val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return + val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return + val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT) + + // Test custom recipe + val recipe = CustomRecipeUtil.getCustomRecipe(leftItem, rightItem) + if(recipe != null){ + event.result = Event.Result.ALLOW + onCustomCraft( + event, recipe, player, + leftItem, rightItem, output, inventory) + return + } + + val canMerge = leftItem.canMergeWith(rightItem) + val unitRepairResult = leftItem.getRepair(rightItem) + val allowed = (rightItem == null) + || (canMerge) + || (unitRepairResult != null) + + // True if there was no change or not allowed + if ((output == inventory.getItem(ANVIL_INPUT_LEFT)) + || !allowed + ) { + event.result = Event.Result.DENY + return + } + if (rightItem == null) { + event.result = Event.Result.ALLOW + return + } + if (canMerge) { + event.result = Event.Result.ALLOW + } else if (unitRepairResult != null) { + onUnitRepairExtract( + leftItem, rightItem, output, + unitRepairResult, event, player, inventory + ) + + return + } + } + + + private fun onCustomCraft(event: InventoryClickEvent, + recipe: AnvilCustomRecipe, + player: Player, + leftItem: ItemStack, + rightItem: ItemStack?, + output: ItemStack, + inventory: AnvilInventory + ) { + event.result = Event.Result.DENY + + if(recipe.leftItem == null) return // in case it changed + + val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) + val xpCost = amount * recipe.xpCostPerCraft + + if ((player.gameMode != GameMode.CREATIVE) && (player.level < xpCost)) return + + // We give the item manually + // But first we check if we should give the item + val slotDestination = getActionSlot(event, player) + if (slotDestination.type == SlotType.NO_SLOT) return + + // Handle not creative middle click... + if (event.click != ClickType.MIDDLE && + !handleCustomCraftClick(event, recipe, inventory, player, leftItem, rightItem, amount, xpCost)) return; + + // Finally, we add the item to the player + if (slotDestination.type == SlotType.CURSOR) { + player.setItemOnCursor(output) + } else {// We assume SlotType == SlotType.INVENTORY + player.inventory.setItem(slotDestination.slot, output) + } + } + + private fun handleCustomCraftClick(event: InventoryClickEvent, recipe: AnvilCustomRecipe, + inventory: AnvilInventory, player: Player, + leftItem: ItemStack, rightItem: ItemStack?, + amount: Int, xpCost: Int): Boolean { + // We remove what should be removed + if(rightItem != null){ + if(recipe.rightItem == null) return false// in case it changed + + rightItem.amount -= amount * recipe.rightItem!!.amount + inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) + } + + leftItem.amount -= amount * recipe.leftItem!!.amount + inventory.setItem(ANVIL_INPUT_LEFT, leftItem) + + if(player.gameMode != GameMode.CREATIVE){ + player.level -= xpCost + } + + // Then we try to find the new values for the anvil + val newAmount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem) + + CustomAnvil.verboseLog("new amount is $newAmount") + if(newAmount <= 0 || recipe.exactCount){ + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + }else{ + val resultItem: ItemStack = recipe.resultItem!!.clone() + resultItem.amount *= newAmount + + val newXp = newAmount * newAmount + + inventory.repairCost = newXp + event.view.setProperty(InventoryView.Property.REPAIR_COST, newXp) + + inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem) + + player.updateInventory() + } + return true + } + + private fun onUnitRepairExtract( + leftItem: ItemStack, + rightItem: ItemStack, + output: ItemStack, + unitRepairResult: Double, + event: InventoryClickEvent, + player: Player, + inventory: AnvilInventory + ) { + val resultCopy = leftItem.clone() + val resultAmount = resultCopy.unitRepair( + rightItem.amount, unitRepairResult + ) + + // To avoid vanilla, we cancel the event for unit repair + event.result = Event.Result.DENY + event.isCancelled = true + // And we give the item manually + // But first we check if we should give the item + val slotDestination = getActionSlot(event, player) + if (slotDestination.type == SlotType.NO_SLOT) return + + // Test repair cost + val repairCost = getUnitRepairCost(inventory, player, leftItem, output, resultCopy, resultAmount) + if(repairCost == Int.MIN_VALUE) return + + // If not creative middle click... + if (event.click != ClickType.MIDDLE) { + // We remove what should be removed + inventory.setItem(ANVIL_INPUT_LEFT, null) + rightItem.amount -= resultAmount + inventory.setItem(ANVIL_INPUT_RIGHT, rightItem) + inventory.setItem(ANVIL_OUTPUT_SLOT, null) + player.level -= repairCost + } + + // Finally, we add the item to the player + if (slotDestination.type == SlotType.CURSOR) { + player.setItemOnCursor(output) + } else {// We assume SlotType == SlotType.INVENTORY + player.inventory.setItem(slotDestination.slot, output) + } + } + + private fun getUnitRepairCost(inventory: AnvilInventory, player: Player, + leftItem: ItemStack, output: ItemStack, + resultCopy: ItemStack, resultAmount: Int): Int { + if (player.gameMode == GameMode.CREATIVE) return 0 + + var repairCost = 0; + // Get repairCost + leftItem.itemMeta?.let { leftMeta -> + val leftName = leftMeta.displayName + output.itemMeta?.let { + // Rename cost + if (!leftName.contentEquals(it.displayName)) { + repairCost += ConfigOptions.itemRenameCost + + // Color cost + if(it.displayName.contains('§')){ + repairCost += ConfigOptions.useOfColorCost + } + } + } + } + + repairCost += AnvilXpUtil.calculatePenalty(leftItem, null, resultCopy) + repairCost += resultAmount * ConfigOptions.unitRepairCost + + if ( + !ConfigOptions.doRemoveCostLimit && + ConfigOptions.doCapCost + ) { + repairCost = min(repairCost, ConfigOptions.maxAnvilCost) + } + + if ((inventory.maximumRepairCost <= repairCost) + || (player.level < repairCost) + ) return Int.MIN_VALUE + + return repairCost + } + + /** + * Get the destination slot or "NO_SLOT" slot container if there is no slot available + */ + private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer { + if (event.isShiftClick) { + val inventory = player.inventory + val firstEmpty = inventory.firstEmpty() + if (firstEmpty == -1) { + return NO_SLOT + } + //check hotbare full + var slotIndex = 8 + while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { + slotIndex-- + } + if (slotIndex >= 0) { + return SlotContainer(SlotType.INVENTORY, slotIndex) + } + slotIndex = 35 //4*9 - 1 (max of player inventory) + while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) { + slotIndex-- + } + if (slotIndex < 9) { + return NO_SLOT + } + return SlotContainer(SlotType.INVENTORY, slotIndex) + } + else if (player.itemOnCursor.type != Material.AIR) return NO_SLOT + return CURSOR_SLOT + } + + private class SlotContainer(val type: SlotType, val slot: Int) + private enum class SlotType { + CURSOR, + INVENTORY, + NO_SLOT + + } + +} diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt new file mode 100644 index 0000000..57de82f --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt @@ -0,0 +1,206 @@ +package xyz.alexcrea.cuanvil.listener + +import io.delilaheve.CustomAnvil +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.EnchantmentUtil.combineWith +import io.delilaheve.util.ItemUtil.canMergeWith +import io.delilaheve.util.ItemUtil.findEnchantments +import io.delilaheve.util.ItemUtil.isEnchantedBook +import io.delilaheve.util.ItemUtil.repairFrom +import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe +import io.delilaheve.util.ItemUtil.unitRepair +import org.bukkit.ChatColor +import org.bukkit.entity.HumanEntity +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.dependency.DependencyManager +import xyz.alexcrea.cuanvil.util.AnvilColorUtil +import xyz.alexcrea.cuanvil.util.AnvilXpUtil +import xyz.alexcrea.cuanvil.util.CustomRecipeUtil +import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair + +/** + * Listener for anvil events + */ +class PrepareAnvilListener : Listener { + + companion object { + + // Anvil's output slot + const val ANVIL_INPUT_LEFT = 0 + const val ANVIL_INPUT_RIGHT = 1 + const val ANVIL_OUTPUT_SLOT = 2 + } + + /** + * Event handler logic for when an anvil contains items to be combined + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun anvilCombineCheck(event: PrepareAnvilEvent) { + // Test if the event should bypass custom anvil. + if(DependencyManager.tryEventPreAnvilBypass(event)) return + + val inventory = event.inventory + val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return + val second = inventory.getItem(ANVIL_INPUT_RIGHT) + + // Should find player + val player = event.view.player + if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return + + // Test custom recipe + if(testCustomRecipe(event, inventory, first, second)) return + + // Test rename lonely item + if(second == null) { + doRenaming(event, inventory, player, first) + return + } + + // Test for merge + if (first.canMergeWith(second)) { + doMerge(event, inventory, player, first, second) + return + } + + // Test for unit repair + if(testUnitRepair(event, inventory, player, first, second)) return + + CustomAnvil.log("no anvil fuse type found") + event.result = null + + } + + private fun testCustomRecipe(event: PrepareAnvilEvent, inventory: AnvilInventory, first: ItemStack, second: ItemStack?): Boolean { + val recipe = CustomRecipeUtil.getCustomRecipe(first, second) + CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}") + if(recipe == null) return false + + val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second) + + val resultItem: ItemStack = recipe.resultItem!!.clone() + resultItem.amount *= amount + + event.result = resultItem + AnvilXpUtil.setAnvilInvXp(inventory, event.view, recipe.xpCostPerCraft * amount, true) + + return true + } + + private fun doRenaming(event: PrepareAnvilEvent, inventory: AnvilInventory, + player: HumanEntity, first: ItemStack) { + val resultItem = first.clone() + var anvilCost = handleRename(resultItem, inventory, player) + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("no right item, But input is same as output") + event.result = null + return + } + + event.result = resultItem + + anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem) + + AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost) + } + + private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int { + // Rename item and add renaming cost + resultItem.itemMeta?.let { + val displayName = ChatColor.stripColor(it.displayName) + var inventoryName = ChatColor.stripColor(inventory.renameText) + + var sumCost = 0 + + var useColor = false + if(ConfigOptions.renameColorPossible){ + val resultString = StringBuilder(inventoryName) + + useColor = AnvilColorUtil.handleRenamingColor(resultString, player) + + if(useColor) { + inventoryName = resultString.toString() + + sumCost+= ConfigOptions.useOfColorCost + } + } + + if ((!useColor && (!displayName.contentEquals(inventoryName))) || (useColor && !(it.displayName).contentEquals(inventoryName))) { + it.setDisplayName(inventoryName) + resultItem.itemMeta = it + + sumCost+= ConfigOptions.itemRenameCost + } + + return sumCost + } + return 0 + } + + private fun doMerge(event: PrepareAnvilEvent, inventory: AnvilInventory, + player: HumanEntity, + first: ItemStack, second: ItemStack) { + val newEnchants = first.findEnchantments() + .combineWith(second.findEnchantments(), first, player) + val resultItem = first.clone() + resultItem.setEnchantmentsUnsafe(newEnchants) + + // Calculate enchantment cost + var anvilCost = AnvilXpUtil.getRightValues(second, resultItem) + // Calculate repair cost + if (!first.isEnchantedBook() && !second.isEnchantedBook()) { + // we only need to be concerned with repair when neither item is a book + val repaired = resultItem.repairFrom(first, second) + anvilCost += if (repaired) ConfigOptions.itemRepairCost else 0 + } + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("Mergable with second, But input is same as output") + event.result = null + return + } + // As calculatePenalty edit result, we need to calculate penalty after checking equality + anvilCost += AnvilXpUtil.calculatePenalty(first, second, resultItem) + // Calculate rename cost + anvilCost += handleRename(resultItem, inventory, player) + + // Finally, we set result + event.result = resultItem + + AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost) + } + + private fun testUnitRepair(event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity, + first: ItemStack, second: ItemStack): Boolean { + val unitRepairAmount = first.getRepair(second) ?: return false + + val resultItem = first.clone() + var anvilCost = handleRename(resultItem, inventory, player) + + val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount) + if (repairAmount > 0) { + anvilCost += repairAmount * ConfigOptions.unitRepairCost + } + // We do not care about right item penalty for unit repair + anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, true) + + // Test/stop if nothing changed. + if (first == resultItem) { + CustomAnvil.log("unit repair, But input is same as output") + event.result = null + return true + } + event.result = resultItem + + AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost) + return true + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt new file mode 100644 index 0000000..0e216e8 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt @@ -0,0 +1,88 @@ +package xyz.alexcrea.cuanvil.util + +import io.delilaheve.util.ConfigOptions +import org.bukkit.entity.HumanEntity +import java.util.regex.Matcher +import java.util.regex.Pattern + +object AnvilColorUtil { + private val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string + + fun handleRenamingColor(textToColor: StringBuilder, player: HumanEntity): Boolean { + val usePermission = ConfigOptions.permissionNeededForColor + val canUseColorCode = ConfigOptions.allowColorCode && (!usePermission || player.hasPermission("ca.color.code")) + val canUseHexColor = ConfigOptions.allowHexadecimalColor && (!usePermission || player.hasPermission("ca.color.hex")) + + if((!canUseColorCode) && (!canUseHexColor)) return false + + var useColor = false + // Handle color code + if(canUseColorCode){ + var nbReplacement = replaceAll(textToColor, "&", "§", 2) + nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2) + + if(nbReplacement > 0) useColor = true + } + + if(canUseHexColor){ + val nbReplacement = replaceHexToColor(textToColor, 7) + + if(nbReplacement > 0) useColor = true + } + + return useColor + } + + /** + * Replace every instance of "from" to "to". + * @param builder The builder to replace the string from. + * @param from The source that should be replaced. + * @param to The string that should replace. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int { + var index = builder.indexOf(from) + var numberOfChanges = 0 + + while (index != -1 && index < builder.length - endOffset) { + builder.replace(index, index + from.length, to) + index += to.length + index = builder.indexOf(from, index) + + numberOfChanges+=1 + } + + return numberOfChanges + } + + /** + * Replace every hex color formatted like #000000 to the minecraft format + * @param builder The builder to replace the hex color from. + * @param endOffset Amount of character that should be ignored at the end. + * @return The number of replacement was that was done. + */ + private fun replaceHexToColor(builder: StringBuilder, endOffset: Int): Int { + val matcher: Matcher = HEX_PATTERN.matcher(builder) + + var numberOfChanges = 0 + var startIndex = 0 + + while(matcher.find(startIndex)){ + startIndex = matcher.start() + if(startIndex >= builder.length - endOffset) break + + builder.replace(startIndex, startIndex + 1, "§x") + startIndex+=2 + for (i in 0..5) { + builder.insert(startIndex, '§') + startIndex+=2 + } + + numberOfChanges+=1 + } + + return numberOfChanges + } + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt new file mode 100644 index 0000000..3ec5e71 --- /dev/null +++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt @@ -0,0 +1,54 @@ +package xyz.alexcrea.cuanvil.util + +import io.delilaheve.CustomAnvil +import org.bukkit.inventory.ItemStack +import xyz.alexcrea.cuanvil.config.ConfigHolder +import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe +import kotlin.math.min + +object CustomRecipeUtil { + + fun getCustomRecipe ( + leftItem: ItemStack, + rightItem: ItemStack?) : AnvilCustomRecipe? { + + val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null + + CustomAnvil.verboseLog("Testing " + recipeList.size + " recipe...") + for (recipe in recipeList) { + if(recipe.testItem(leftItem, rightItem)){ + return recipe + } + } + + return null + } + + fun getCustomRecipeAmount( + recipe: AnvilCustomRecipe, + leftItem: ItemStack, + rightItem: ItemStack? + ): Int{ + return if(recipe.exactCount) { + if(leftItem.amount != recipe.leftItem!!.amount){ + 0 + }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){ + 0 + }else{ + 1 + } + } + else { + // test amount + val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us + val maxResultAmount = resultItem.type.maxStackSize/resultItem.amount + val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount + val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount } + + CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount") + + min(min(maxResultAmount, maxLeftAmount), maxRightAmount) + } + } + +} \ No newline at end of file From 13b7e73d8c6aa9a1f48ce4c4fa1a33983ca32174 Mon Sep 17 00:00:00 2001 From: alexcrea <42614139+alexcrea@users.noreply.github.com> Date: Tue, 15 Oct 2024 08:27:26 +0200 Subject: [PATCH 5/6] Excellent enchants compatibility (#34) Add compatibility with [ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/) --- build.gradle.kts | 4 + libs/ExcellentEnchants-4.2.2.jar | Bin 0 -> 424362 bytes libs/nightcore-2.6.4.jar | Bin 0 -> 446550 bytes .../enchant/wrapped/CAEEEnchantment.java | 46 ++++++++ .../cuanvil/dependency/DependencyManager.kt | 16 +++ .../dependency/DisenchantmentDependency.kt | 1 - .../dependency/ExcellentEnchantsDependency.kt | 107 ++++++++++++++++++ src/main/resources/plugin.yml | 7 +- 8 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 libs/ExcellentEnchants-4.2.2.jar create mode 100644 libs/nightcore-2.6.4.jar create mode 100644 src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEEnchantment.java create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/ExcellentEnchantsDependency.kt diff --git a/build.gradle.kts b/build.gradle.kts index 28cb586..507b783 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,10 @@ dependencies { compileOnly("com.willfp:EcoEnchants:12.5.1") compileOnly("com.willfp:eco:6.70.1") + // ExcellentEnchants + compileOnly(files("libs/nightcore-2.6.4.jar")) + compileOnly(files("libs/ExcellentEnchants-4.2.2.jar")) + // Disenchantment compileOnly("cz.kominekjan:Disenchantment:v5.4.0") diff --git a/libs/ExcellentEnchants-4.2.2.jar b/libs/ExcellentEnchants-4.2.2.jar new file mode 100644 index 0000000000000000000000000000000000000000..23d9630877624f38bf9eec367f3b50c8c1b7be2f GIT binary patch literal 424362 zcmb5UV|1;}wk;exS#efu+qP|M#kOtRwr$(CZQD-ndiS~b+PU93?e3?|=hv(;s%o?` zd-d6?%Si%*AOk=^KmdSNFsK9ke;!Bx5CCamC4Opg84=p=aR2~d069r;h<`=^{m)@? z|79@p-x2>iSejo(Ttrw=iAGxFL3(mRN|Ksp4px$yVtR6>L4j_OarelcMnalOTzbx_ zyiws+no4p?@zx$j0b-O~dS+6QZkZ9R%#LDeQhe@7Vr&_fMohX{fsWzuX#WW4UrSPm z?EI1cJK;a?|5+07f7%b|-)}ATtW9YDyM6rUpnvx9uOKTU2M0Y9BL`grYZ^Bz%Ptj7 z*$pvdZ(XyWfDia5*ll^_G4bHRa)X9wa?L!7`{cDhRY-&*jTtSD_{8MOd9YL|G?o=b zmZn2R5Y#F2_a+S8pAlIR3Gk$N${JQW`aX;&nKvKPY2t{+dATShBHcgH2SQzj3*ga` z{qqF~*VQezfY_GJ(-+^vojpDN<`&x>$FthaCY^;x_}fhE2Bt=)+%UU^>Vfs#uvPOA zXv~kFjad_#8tls)9k#mHc>z8^wJ@*)o`?~(r&{uqy<(27CR1gsvqr?sG%!tQrX&ia zpjUJ1rh%v3T*A65a!uz`UUnBArFTq&%uo?A#ZCFx2P94Y?3#_Z&g>zp-fk21O*Gk0 zh~Lcp0)$$I@F(8ozDrnbe&UP1F?oLIpe*(Y!~>V`2K}jDGG28~&zoDBH%lU84WiK# z0tY8L#P}Ufn5g}kcH_DI?|P;ld`J;ROapN)?~Cy>xzE3@-xGBRWPv+k)W2>v{fyF7 zMx@4&ZG|fb{1xd>Ub7xVw>G@S$DP)F7h;VNkyxj61+0uY48Fdu&uTv03Y_GjvznLR ztYsrSE`>yAtmQBj*2ad+Jv}hr!8PWnwrF-pmY2^~q9n4=pw-A(W;o59F+$nB12*}U zejKW{?BK&=m<4m7rtJauAFv{a)-_FHwW=uWm82Js? z5z}EGij|9%7x*RtP&R^{Wj0wU7qf*h443HRQ~}%Wj;A-148kMuk`N1(Pz$CXuxs3f z=US;MK<-;)X1^JNd|&yLUHz?UI#iXzjYtiN#m^UY=UkK#;`s=YG)*DNvD3**Na zN*nOA3zJNKuE72*6n)%LtkMF+^YP)!g4b9lv+m;7#9rc`dYkKO9+6k}D+ z_|{4gS74|YsQj!5cSdcOvqqM?s9_(mwR9Xc46dk;PxA2SN1gYTeSWqDx;3a5iod+c zk2>ai*R!!{Agl)X#=%896H`joJXvx^C+6GgqAYQ?x3B?cy5XVSE9Zyuaaikk4ZS1! z&iQl3JGI`fo;kZ(2t*!tOpJnb`KeV0<4<-^&IjY%tivv9*(t=8)P@}E+g}01nBN?2 ze#p^P-Bgg^kItSA%o_=9fIhrk=Gn_?TVmD6ULbKO6>J>ElQ!IgPH%G1TVp8_vW6+m zLgU2R6vGub^jkygmSX9enU4-5_ZEsCpM%k2>Nj0*a&*@~)2Z$4cq7mee|1+(rs>O9 zvrFrc4D0qC?LUAQ9k3pK0{j;SApbjb`M-g}$l)KLh>GJj2d0M)`rx~!Bk#O!mij}l zNwiJVIywVLvW|~BXC6tEvD&i6uu@B#JQsrxH{1$8A6oL#h0c9PmwT15f$3{6&&@OPKTXCatzqK#%M`g*L{ zkAC|lq#++T2BIx2V#Ec30VU1?)QiE~GfX%&mUp+)ZiiVSH-pbtKsAi@SX0WGLtqnV z0WTJA2hvNzu#A2uB9#%DyyJ0}Va+vZ5NjC@fI(J(SX$;bQ=6n&fXqgcykzS%yR)5` zId6Av0k(g0#(=3~j2qtyGoBgYkLBZJs17ubw*wK48deMZEHYtr&@1#m^(<8Qgn$DC z0MPtuaQ~&Wr z+v$9n_ZO&`L*@-{S{9L`C_D>j`twCksFw4N#=%HQtzl}JFlGF*na1ca-R01AXUc?? zfqi}B(B>a!7|yFjlarN+?(A3T%LV8Lc8)#DP&$(fQp){3>d#TAoK%EN&GJBmWl3Cj-6MBb}7ehak;b!DO*2Y}!VzK%Y04gW= zBS-m&aq1j3bOrB0t;|C(XrTwBFz8DkvExWGemBf_*Idu_cAz*LnB!>pA z8G@&orbO2hHsiI$YaP<0i1Qf~Krt?q8yupJnix-m}^;hT!k+E@u9r}@a~ zVf!S%yR2;`>oe{ACCbb+=<;IY+re@%uH-(%#slGE-uwf$-+V#xOE*d~8N->BSiyIa=Q@xT>+{Ge)EaDjOr`+OdUzrdq%}T)5dn&i10#@x8hongUfrE5 zIWav2&1Ble(h}0goyNu3JOt_1r8N;w!; zdebJOhBJ@$L(VKdR#vZ=gv1lY+wBQ3!Rq0>05tqEB0vAtRF8oXMo52~>iF*g^j|j> z)L)irYw2WS_AhSAEz2!CKnK4?{l<%>SfXJ*m`E)MMifp!hNN$sWnsDZO7)?z?i2GM z-(2GI(k*f&0PCjE2b+9{iga9gYdq@#6WOPR%~s(IoRtHHR@kaP9v$xZQc0nFRS4le zh_N1Wj~^=_Ye~Lz%R<^{;g8I1d_X{p1M8}v3k~uaL~T!ngRY++I=0i7L?bvK`2_>H zN?<5eG`w($LjTC@*;n5$C-9m~un^RefDGVqib#)sUvu4wD4&6A*txIZ=ytcBOT1%X z-ky@gxxL2WQ24+_(*E`H98eCY;5twIaj@8#!_&e0&ks(wrk>>RAIkstF!?{V`Okl2 z{uU0v!Rfyx{r@=x{y##j%}h)kja+T*|5E>dE1uy0C*H`_z{t|l$lB4!+Q3xL+VS7Z zNb&!a(ZI&aO3&Kx-&!lzzm-`)&%sFWKW1^DF|gEgaG277^i*1U`ktP+CrOxK!RL#W z#)}0JFhoJ*tA~LfHAeJ@B$J63Wnf4PV?=^)s_H=53|cOtaT27aDdYg#Y^qkCYhSKf zYS+B7wh;c{ed%INNE;(c3w->ce4~H3_ISzmxOSO-n{IoVa@fIj{mqxBU>4*Lf)WNK zE=Cf`HCDG2cVgeFc~5~UJD>g;cvvLUve1(m8=ItU5(G+4Q3+HAA6Vdt?PhHvS)t2W zv{muu>9$TTojy_2_LbgIwzT*BaesL%oK#UTbVgFxZ2D~;Gorg^whS?(zGgJX@F#3w7A>w0QB zxszgQcZ3{>VCN`OCfmS#KCNa&AQndOFyki{7Rg16nhv}i*AydhOHT6(#&PF=dKD?(^0}coah3O z;K~oD;z9ibn{9d+BbbdO51x`1FKp*s15QvxDC~qr_T7gOw$NaT$19H-o>;E9w zF;S=epdlY*psWO~G#dR3XueM;9K5X5(WU*c*2-6kq87q&W?y0|jqP2$spt;0tUHa% z#-t}?vqJj(TZBs{7YFu|q?jYKlNP%c&;Sxw;6S|&VWfhM{|}N35tc)0Tk6>I+@{v% zcz+kh!b&+mr)6vWniGR0GZfPpGBbx*=5zS|QV>d$`*Pf5IizLsBnLrx0BBX_oEJ^Z zG(Rb%Wt=tXVN@>k8sDyWWnI(S86&r#%sP#2eWs(OoUyo>^kXLvU{B`Cazjkig>rI7 z)QJnbm-iXU2B^%CS^V+SpHjz;W=3MzY@Ymtw_hOMWXy0z$My5+*4i`hY>WgSqb2u! zYd|NX;ARK|6c(x*6&6H3*`y5%Z-I>8JxB^n4r{S^_*;Dt?$|mdJ{?p`hyAq3tztBu zTBUq6l{8gVH`P*y473QXO!{ehSyZi=?Bnnh+(s$A)}Mit)(0xAu)16jDsQyKteA-M z7DLLd8bi!LyaP>vovjzG`d<;ydL=QyYa97*m)4>Mk|fOA0?cZ*+(;BR{wyx0G>{K7 zpw@|WCEy5JMm1ZF_E}w2fp=3tEn<Au~5-4Wg((EZZwv__xrP zM~VH`LnBlfF#G0+f&K-7R(R=-6 zoUueRN(7VHm0MMJdD=uO685a;I)kDzv*YbTV9S{ny|Lu(e0u|2L(%PP*J{T1p3JtI zP&>#m%a}TgF^~$1DW+(e_7S+s_v!sc?m+asf3ourOf<}~bsr4Nbe1OtxVNE z^qVJq?dtt_^<9gj#8!Vzd8#tt*Xm7TF=SP{!{<@BtLr6wO{6Qx*`@Ogn<}g)u;=K% zuiW;s7W8q)P+mR((%p^WW_eCRaV`$`yUMgM@>Rb>`7Yd_0rKh7g>(R?@-s`^s?Gek zXWWi{RwoZKY()QZB0pB!p8n>peqaHs6t{R1Ge4oeTnS8+dQdNJ{OJMd?(1m_jGx;x zvv18T88>Vrl7YKKUsS>BccJJ;d;Ntk(mLG2^$c9IOruPi2_L{Qo#U%NEJKfG?4ZSGsiq==Fr#`JE`BlB56%i6Nrq ztVSM)|3?&pr0`4~0U6tgLH}_cjYD8o41@TEIGu}QYVzDi%{fcLEU=9JVAb0`q(PfP zZD1oExn90Km!2t^^v^Y=-jIHX&mdh|3Yp-nD@Q-u+qH~Hk(Y;3-NfF*kYgQAi-H+} zw1QcEF1aI-qLmGf0f?HJoT9p+;fPMa9PeU+Nvg<#@;EY6fbsqK0lML0D_BJJWzalf z-MMg@HP2C?z?#3kX)?YRj^eEdgFTAg*$M183X>@6jkt_%-ok{Xlqh66<(jMhWA=)U zwzhg#S-!oZSyh+Tb0)2+-(}jIfy?A#SJ|*mv%<3m(W&X$MQCbQguozjuNmj7FmzK5$AIE@2(PkWzlU_N;`4 z@aVN@TzGV|$dpBC{yX;)4jiG(r+N|n*$V;%N@MWy_*TyH@!B{>4NiJv^@$P%ni9Tz z4*Uf?{w9~Qe{>Qy>C<0ijbZdzf+#O?>Kucr2)EGe%C_Q?R>3VQk+^^y`Xc2Q?O^m& zcN^NP3{?6rl~0<}(J7ggtF z0^R`6m`A*@I)2XYlAgP81`b1a^2QRP3nni%AM7b}( z==l|g$YzoweNyUJo%?X=**`ykFf4yywzAoWlx{z@)rY*HdY3Qg;+Kn!*lrg`YO!Tk zHXjr?1kCExWOpcpwm?jwO>xT5&Q<+Fqf<3>me5q+7H{5QHET2HEJ&kg#dV@wmlR&Q zVa|kJ-(lJ!!He*yh~6|nkX_%v;q~0?+?HB#YMxY$@W|NS?9KB;_y4i&NBp!i^JoM$LQFS)UF=bPev&CFQjh89)VDKl-Am$PbdTXE~ zxefLFux1FgJtcQi+b|EnXC}uakLFvau~!286fg+~`4OIbl3AViTW;8!s%)mpDj}tw zXM5Jd2^~WoPw-TpKW6UWGVMME)1A-s9kbmIVI*`2CwPSoOsYtCE`frZ_|(ijIEzq) z^q3cpYN~H(4*|hGvLhb!t)h3B>NPW0ci~~O)mxf^hxqx-2<76?a(_Rj#3v4}{4qW^ z<&0Ge_nAPGp?2XY<*DoA#YJX4gNyvU#eX}$ zMV-56gf5A?qC|W3ZU^Nt+wpU6{tnijeBQ^}xhwi9*6P7Hzh`O8 zx`i#;HPT661yA?E73o~_Q2ce3Ko7l&50zx4+C zVg@?eoxS+MDbEcJHUpx;K7~=e8I<*D47-#wapX-`@*iBg^NmUJuO<8P_)*hgK$t$B zR0Zbt{FHL5Wk!YX-3!pS_Kiz|0UCx72dh?Iu`GTd7W&}BB$FPux!-?rVbNN7b{{iVXYLm1meOeBrd`Z}&Bjo~(l$mDD$#P5jw@nDEP5g;S|e&)_R&~rq>1w5 zp>g97-So2CskDiE?iP8L)QIDBxkxEdHNFjN)Hstc^!%NlmT4W=m z=ZxYwLrgJBP>@f8OrkSixHT=S<%wj@%If5a%H!tSRn9K@&JUJxCbjc7t08~2O6lP; zO><4xi#OZQN&eu8Dq|a4v+gsa%q7lsgHEdptK7nbK5nMsOAkxunUW#6l!{>VY-tLK zE$gX@9B)$|W%UU+n(Cu633fe&9ql$1I*tP|tU#bQ8)UI>8Vn&_TgqSeRzR_d4!owg zx@3={-%`sqGKI8niodZ>Dc_NNN@jD;Xg4FkDRXRjQ6*?kKys6hCeH_JX$q&t#XTX= zpCFqF&D-hfkQRVOy^-0oMxp=U=&qu=;Pd((0Vb}hxadWF(%c9T`=97}mZ7Ax?(qnTES|;fOu?eTxxy!A_Na-mE zkWNGGY8iaSctrN}${Cp09u5tUVr>{{x8|$*dSfIm1X0%X+u@t71X(x;${kB@Np>U& zbVmUqjgFA^6DX#iWJnrACX74oXe_=}8OoG|Yoo zE9HZWJ~C+<4GLzreORbsgEG;lp&8`5?DyRC1#Jz>*$FRjOCNGYmb`_LbOi-<#gx2* zOx|0fApX=ZGSqVHc zZ&q3iEhm^#kX+rz(GKb4h_>8du_6!f^oWIV3VQv37`>9^5xLjm^|($@UstH;i+dW1du7V%Uip!p+X7hNS$FzO~tP5>be%#g%5Ki zYvYTgo*fWS7DrgCp5qZtpV*z}a}k#&vNMkl#{#Y3mb7z+tc#iF3Q-`oqL-?F{>QU; zG%GR>lH8IY>4teaojU7C6{SsX77Fvwb zWTtwj`BQ)>k3`G2u(l5~AbdhQUTNjnoW!_CBA0mq9LxQ5kqE~4_RzEw&0(&!UalyW!U}Dr!4z&g zUEnvtZk&;6A$q*XmlwkHhKMIDr}@ zi#P#tT+5eF$Yxq?q>M(kkckU)``I#+xXeRFYGSY-R*?v>%0YS@RiXBKulI9`!X-Ge zOpV1g3Sz&XD$XZN-{dPk;n|8^9FBl=LJ+<^;4ItX&d-ibbfrAZdu2KRWvX7EaLo^S z;qam047zhg8c(xdxke5g+rb5svf=WggS|(?pK=Q1-q7*O_P3Fono0fHArpsr2Ptlo z$%-tGtsx2-4UH(eksr*hP!jd>`}Br4e2xL(=BEmEyv+poqGRPVIT|n8@vqBzJg_dg zgdA~==Gj$$jy+lZ$u`;cj61l&b9kJ1XK;vQ7nsn3*Esf%wV7f`E~6y^4d3Hlc-I*6 zz*lXH%d5Tb;-1rUSI4+o^fE4)6wr2zi=6s$1hva8Z3Hi4Xs=GB8Pd6Hgju}k2WBR8 zp#5_ z76;tc!*x#qe&JTy_rePK#pdTVN`6gfIWWQop2LvkJ&gauo^cDk=}+sF%Zf=C=(~*s zmJ3ukd>is1><_fb#J`t}LWs{Goc@Y^lEi?X)jDzc9V1 zjb6d_ieSna1Tlc0SGg#Q;!0yE+ZEz$9@m5MP^@CpC!G_hLw&xKy}{UJj`msP3KFAV z0*$#*?C)nujVR&}wwk;?9E~hzs`j+V9AiscF(*~z!~8r=+eck7CsX8QX|cl0(MKBX zs)y8k_`Ozp6y{wAy^NBJf~YGJKWV1ed5-Yo46+~49aV~nmdx`hywhEIFNPIzE`B9M zJR2k4@&Yv6B|on;6f?Y0N9dbIqmbJ=uD}D~jlXf!$Sy+aM)**4jsR_t+Fpv5fHw*? z4Jk*Ah|J(f%qTNnZ!le9@MV}1>9xm>ncb0@*)a*deVn1jn7nOV{&-32!MwA+*VDbR zUTBOphelb2tSHuqm)s(ep$G{52mBvpT3WXt1px&Bz~ZmE`d=&4eE*j8`afk?etQ!q ztG_bx|B_$TAw7{*(SLfh8rGzuX#z9&{`e7LL4%m$BdhfTJc0<$O#jTx`Pq-Cia zoA@c2H$631wJbN7TU=OSCKb===qQ)Icr|L+XlgcXYgqKKGG59!vzi3-e0%qHz8rVm zeE9s_xb9Gy6Dqrjd`bj=~pk-)I0Fp|}8#|PJUC%`xB0(2+Dln_{!5{DHN;hGBZ z%Vj*p5E~~ChEk7s5>FpX`CV-aVWKA_EjYJN9knN3elAXFUSnyaAoNi%bdw15Xb+)&g@%QO!CtTls5vJSUs{R$hSa?RfKCLcdh`R?W#Lv$JJ~7I zc#oMUSIIzt4BEFPQOEXFeF}iy2otrLdQR19Z%R@s${f3c-r<85qDiiJQY!R|EzXTC z;DjB~Ae=8)mcNACqlNsh@(A<9F1G&^M$pn(VgXa0kWddD0hrN&QRTrYa;0MmG3AHl zX)PJVmur!~FlcSj5!)?DtWqFU)CK2}FRrc4xgBzWTg3_X*>2FZJfZ9SPA3iNkmNDa zAgV;EgS$YdFgQyrBXyPxPiG-RuTCdINbWV0njuhvTpg`7BEUD0oEA?`1V>Y}uegU) zKO**M2&L}SfriS8ngA_y zzjFxrcZ%(Y`D7GOJpB{ZMssitD>!_b418+oU@IEOF(XwCW%48pJvGtz4T`cu3?wr^QlZR2at0zF=(I9S z{G^@F2~3fvD33C%uU3V@2)pz=Lz9teYp|ce{zKwcjb2INPM8(C$}CG`c|!!ktayIF z4H`2g*RWY?(Do!x{$M9Ex_zoWH+CKT-6R5?p{_17bN%1t45b7L0y1`WH3)Yaozd5e zla#@uff{R`MG6X#t?v4woS?f;gzk=-`QSAsWEx%50jvyk?wx5kQ#hSTIGEh7oLyQi z%MmWSSwP#w`&+KR*ljXX5AS5ysZ8(1IkA(YQU+CmUG0#zNq4^pczVyWdSQ)6?Knmc zhIP&ykC9t4MJ-4$?P~-dq+5jt!R;!t$T`8+OYP3GN#ZrDSnv|H^Gbi?33$R| z=qGJKJyA`DLbEGbwEiI7-9vgR^j;BM_u@6N9@O?MaBMG?+{(-f25T%cR+^HLanHae z?tBdNUCWWyrOv=QaaFY^GmMQ3#-}H9wN-}$r?gzki}1Ek?D;qaN;*KYic0>K$p))NJ%V`C$1JRUb@xx0OVzBY3Du; zZ4ahA%D;}g|xvO1_caUQ7fDxY~b8VmnM%aq{>lrb8 zbuf~<5IR->i-K3dbO>=z&d8u8Ls*E5%Pf_ssF}*=@#dE|5pG$VY6!|3hdZYV#Vnp> z*6Gb*uMBLaoqb9GAGUpFH=0(`A8BJ~YY@^&tgmiA{z3$wtfC-xcPH+_OwZw2 zUmtmYr3wu^Cr)x$_=cG3fCxHn0@CxErM282EF2?p4hUi_me<#IGK@Z z%$k7Dy$1_Ib50_U1UpF|3-?U2)#O)u5_`zy=%yr%{81ur<;B;TVIBq3IRAH#u%d@q z1=fe9!S{xWxN<=_`2$YA^g^h_cSS@dq8gzWg6t@lUsYi{)3Zvs*R%1WqBk-i*?^WO zR1VmHiK$8;BNMLu)J^`IiS~mu(@&;_H|x}m)6`AD+llrAZEpB*QDMlQWFulOX@QB^ zLe?J_HO+3zzTF7MTYxXhbfNqDVjTiJw#)5L{{ zClJuLxlJb)uk8&Nn2(<%Unr@JC66@TVH+&P94(pVjEP6a4CC{OA{2%pg)=@`LbT3^b$dzcbitp>Qfi zV3*;8Dzw5)qUAof>X3bY?YJPn%&XvFM5rPHZeRmBYk&lX0%&Y%#`+oOdjJEjhJqrK zM3{d2K2665;qHd`GIN*&g}|PPy2VmNWRAK0;iWlEoxsx4gH{bWN}cwv)j0ih-Zukh zClQM_LtmojMNh(|Xj8Jvg6FwO9SR7A%k%gx%4c?;PXKdJ%GeIQsC@`#JX_>Sk25?& z)uyzJ-Nx~QYwtd7bN}=Uzi%4P3HN^a{#@2I~>M78ku_Gzj z;WV5H7;nNkqjRzKgH-uxV)@}t4ppTFXj?ZCC3bI{Ay8bqQ(>aVIhd0ad5Bw=r|b?p zZI`;6U{jZ{8`!yPg`JH2Eu-!hp$^cUjZsY2V+zd!f-WAxcLVf3i%fSVhQOEOZ0&0f z&3JZ6?qCP!h5>c*q=F2GU$jmLD&}io7KUv>gIsZbwbOohF#FB;o}QKNB&%eh?YtTt z-+{af4`iK@3vWc<{ymxnKD)ZE3>GMrZ7yoPbeL7RW_ z|A(8QNfXi)X$2u$-N3>MRgOpuh{zEC_r!1=n*baPQ!@e{0d8GjR9;zo^76C3dWEU# z@APe#jAH4G?6b7T?u1!H0r$ceuN{|+?H8{cMjO?82^|AG>-d><)|;tLH^!fgv=`@x zv7bzDTmVP$EWlWJj!Fe4n@HK!_c{Ujmda9Zs)7@H_wD6}G)QV2%{jl!ptgsW22DYLgb)1OlWZUaK5OS=2uF6ca(d5%R0=~^~`a3IM)Z#Ruhr^J=y+noUtWp~ytKE%R;@L} z^HthS4JY$;sGYBTJzT9!#7wdP3nMz=`z+)iYb$uwD@jH($nb?Xk*U&4E#(6my#}zt`&-&F8%~wjN6?JZX z&SJYXv-FXyV);@-Sq4!eG-sv!(ctQ%GSM)+heeo~PjH?ajd5w7CaF`dQP?$a4enGT zi<_tkn|yw?4F5&7TD^gCYFI!Ko%9ag@%}vAYES~y!G%p&CzS7#%2}K?Qh0DB^5{?OY^6Gh0e+CSNbBYASfCJf zLqbp#6FH5Wac62&t9fkI`5H{PMK6=6tJHS;Zy-lq63X5=7cJnGx3( zDdd4D$`W&mc3LfBvZ{JLs}|iw7_{}Y8^!A5{8I`1trWCd@`2) zW^Aaw2T)vK)_<>hV)T%LhjuMTFBZhX#@|Qx*jYRNvz`-xqV|bzqF&f&|*8 z9m?C&b3tZf`IeB~D!F3VO^M~FM&qR+mx@kP9A*ltQB2&x_Ujt8lAiA0X=oI&2-`YdbI*b;3@eCbz4zp?~7fw}aB6T9_p!lj% z-eR;SB-2}99i?KauTmjuoLebYx<~EkQvwHF*C_8g-MX(!poc`(lG~Hu0qi;vrhBh`VLON(LkCfgj5Q^v&^(ClO zobwlJ*b0}vul^QqS~Md}Q51DrqA*=>L;gG8lT2Vyh|7h6 z2DwQ^aFKO)9CWJ(ax0n)e6L!$be!oZc;&~h55U*VaN+9d?W$h!>gna@$6v$qzyV($ z2Ha(?{x}_Aig!5fp)W=H0fO3)=rZy_d#yqZR0pNm(QQvw9Lh|?nu3csd#kqNc~ zVzV}wK}8jkBFec8IOCNWYs?oH?YT4v!H=Yl>BlP#4S$^vTn=vAoW&y=U`%SG}v%v09$ z(?PdQ3VQ<}S`!#2!BJk}JyWs7+5)-6zH6!_v|WNuSMB(MWAF!Et~ZOWr_u^EWTI-# zt2UOVGPFh=%LCqB=Q5i~n=3SpL&c|P;-Z}z0u4Es zfA4g#vYhhopplzGKRVeVOV14Frd>0OQlocA${~hkWY4lQmK9ksPa!pS7p?l zGU0`dhV1H7^_1JWi-{z?vZh&dB-e)GY4-bA*>OB4cww+KQt587LAxim)edf=xW59< zw!g0nVRlLaGH(SomrKTnO%Bx00_lxb8Mp##)l+>WIG6J{K=G|SnHB3T9mXwe^mNzr z-jUO;MVa$#E7aa)UC(%*blpAmlD{25z5674#HhHl!=I8@vSbE54#?5Hh1=vS*~N`y zo1yHqM;rwS0xZ!1ny{h6p&+;&#YyJyZ;v@?LXCK=C=>!Ee3W->D(8y?imt!+D1zL< zJ`%(U#&3UdFhUl&laRe4&R(5Ip-*79M5M#~2&^8Itio(~zO%=(ZT^IWd(r+GCfhj2 z(kNXPG^|>ItnzpsiZAmMQl(HS@}#1V$GD8IgVRew8^*o{n{u{I!}52pRILRBRsxhp zgPk5$6`h7Px(G?DStU0hEGx!cIDM_=bmF2ZK0URJY!p86703=8YX#X029yvr#Mi!J zkCl3QQ5hmXE_$WoKMS9RK4+R^uQS@3?LVt4l}$t5BDjcMu8hWV0F>PA>5G#t(k9RmP7_hvx*6SRD~FE^jk(_Js~l!gW8Pg5KCr z$u7ANJBpf!@yL&!y&DQAlY!a9wL>F6zo4iph)BJ;+K7E%GCjwznE=c9PjTyZLn#v} zt~}NlHm$6|n=-%^0R{bjSJoguPccUvB_Nz`yYIdT=*Arsc=*2OM+a!&K9w%F*6OtL zalbEH`7R0AozXX5^h*(W9`B$H8R{B+z@zMM_+ z9YtVO6GG%x|0TgPqzoYP9C0~iM?jY&tTLi2b^ojSmMwLpGzZFNA4}WjOLtFAMM}M* z&kdgs?wrwP(5yR)v$!1mUM3IjWiBWkf8vkT>K1Ll>F^0ZLcAw(VS z7XnKzOm2_N2x{~L2vs*H2b<1mg^mjpsn7ub8t0=I2%N9?o7Ezz{(wxQ08H>5t*Ej-H=)!}cCpdV&K8fr-xY8w48j zKnNU_+c?jx9i{H+8v^v0{P&AI((NLSm+B} zU|K4#g$2J(6|QaFUB$F9)ertf6s!R|s3~Lb*m#is+#79m<^wlNN5MWq%j0CXDJjyj zZ5kl6Xx&gZ>C5Y6^gqXLg2!3NM_b(2d`?m)gdH+s1&NFlc$J);N9Ik>p+4 zN2!UyQxX^`BE>WXF&DD3PkHL?}d*G$x&>PMS4f8xqpAU9~#HVTxV&wjI7iWr6H zy?1^%J~ig2*>mNZTQ%LRV~Yiy?%H`kpdO^dE8i=nQ1v-6@PO&{=)&9W@`sY2x~;c* z$}%||I*uGZ(ra#V#6>U;N{KpoQuROG(aUEVPI*RLQOjqR9@4cSvDz@!L6bfmbUs}| z*3YqJeTC5g(cQd^D6TpAM#SGs#-cXSJw&VjZmx`B($f=cg~ww3;jre|&Bj)|W6 zUL_1#@GL{;CI_VzcZ z1`1&wq1;vG#&{Tm!5(~40Y6W(4LaWdmeelnoD@sCTZU#eReDd@m&3lgqAuehW19DM z{M3ZWX&S$R2U8oj=uHZ4r8;rvFEA)%))||4S}xP*qeZ;VSx9aoG!ifa&ujgyeWl zNRTD^j>HkjjcVX-uHZrO)+$KbnxB(ZfKY72?Nn;>3jx-|4SzQFv8M_U_ z^$6}SMTxg7R!Q;fn8bT}&k;+`8$^Ye>=q@c0Um`JbB*lQkIpCsgh1QF&L$V6u$;OR zhNWJ18J3x1gSZHHG95xHNx6O7?V}Pl(A#c0Mh$DRf>RNzl-AH4MLf&wq7g1##W|q~ z*a^kdc2H!2+1kxr9enBuGsOaRD{d>kjnT@UbjyvncVhn8=u zs%n0=TiYIwySZ*Bh&&obk)>Od6QWEl#o5a+b#oo+>3Qh9V=fZ?AvC)ql{*O(V4rJ! zG?OwH6#s7D^Qo^RXn`;JlZX}!*ycAM|AQAdjH6v)jBdT$gKxl#{T&_usCeXcL zP$4;V?up|J7e)c&Ju4lDlkxP$2uVJP=A%@(Dy&}_MVllX#9uoAduh$`r^7Dzg*(ww z(_glYHGjvZ&>jJd9fWy=jhVZ`8@SA+AGwF@E zA3=%D^ju;r$jlJ1|I9pOkn$R^h`}Dxk>~Ed+S3$Qq#rf;@(t_PZ2&oMPE$e4^XJ~U5XU(MOK5oHyvGOIF)V=FM> zSt;P*%PVQ|S~M1PJbG& z?@?^LDSke&Sl@YSyvci}<+}1H`}#=#?on=eBX(5wNGkF2|Ld2#a*_84DfzNCA2DAK zJ7@XX0av~cu~-*1AJ)}a_)(JmAZoIyI`auA@xtYO_`%^%R1^wRfIe&KzTzqE5QS;W z`DTI8hnHQ@9YXj0wd--(g0KT!p>=FL2V8roW6A|NffI;Ax|iRA0LU${EkSkjV4%)T zI>8wrjg_pG%n1o0)wKtx#%PaQy3(?g<=^xoAbA>|0M8A%5xn6~33{aQ%P1oE@>Z@I zEm3q*?wTaeNq=3LiN5z?l%gfhE9V#H9%wC+b%?jC-Vg}G;2zv3fZ2N=#dyf5j5L;t zw$qCs_Ln5#`UXpOur;3Rbs6(zl|cD<-b2 znKd_x-`%*`D=$07J)tExyDmXDWyiK9qwV81WU_XM*>1dV7%~ewxk;UB{))HZAA2;A zIDwiLQ#^<2i^1cYm=q2+y||%mMxLcu?&s%uap2-JP+fWCyT8AsEni!RM9&3nB%_%E zxA|#WKR`Mu9e30hytXZ6^zC4ogR!Kus@C+iq-lN zg97Kta^~}3zW3*YiB6}eO{=O+uaw?klJ0Kg-7vQTyaK*_#g6jw0P;eA*~dwKGbe1# z^}v4RNqr}Te5TCqTxi|%rhVFkeD2Um^v9OOZ63ft6i5;FuMYA-w-O1*CdX^&!OYwD z|CY$^KO()|r>k}*)*AOw`ffnY{U4;AV{~TGwx&}-#kOtRwr$(V7pKCCzSy>H+p5?` z#b%{qbxxo2U2)x zvB^ST#Us9fUCy_myvb*uEqz4&f?p|4hQ&kJh;;un-ZnI+a*IGj?$1nWI01}a$zH+Yr@ywI5l!GZ^!kvDsq+9#^E|?cslTLe%BNaj9S^mk z(SaQe&U36RY0#+S)(`|0HQvzy9&fP46IAfYN0RAhubE?{wn+%DTTa*;R-UWn1>yQY zO6l=t7)y;DhG;ny21PJzaTVQi>x>p?M3Z;zkgPf~IQCGpCrl{OiBHTMKj;-K_wvLB zOMV#tx!fo;=_>qST=k3gXWq)UU&W!?Dfu@nIWjTJ${k0*c4467khwDH6EpTtHz8z;bZs( z-undJ_|=|SGX9-*a66*RABrDYY^2?80E7p2*;Xlx#=KCDv9q&p4!iElM+jH*w3=@S zA#^Jr->&zC8+~GEe=8YsYqB1{l$hf`%Pm7`!$!kaZ?s8z20%I#1vM&uzfDROcmeyz zkqTYdqyO{OuWI~n{k;Dx5;IeF z5=KJj1c0H7DGASXUn^LQH7)z)es;NBJ@6jqcu%(<_fY=!enJMh*{5&b5w@B}d~I0n z%QL;g8Bc%in%PWdS+Hh3Pg`5ei+B6eQ_~dB0(t40n?A833?Ee+0!_0R7+6>4+-y~J zk#&li3*tK<;SORvU_)!mT}m9K+hvvr>IUTk>P}&5_L}kD9tfvx$cf;B~&(sI+~nYI^a|Dd~udBG&mOw8Vv=%^vzUa z+F1gPD?@>Y(i365Xg~yET?wDl6K%oP5YW3yeDQV*``;jZpcY{3Iy#j|7$wsvy}MFOExI(8GYMH# zHF}f(I7dQV?5k^y)t6${-Am;S5>of&{$#2I`E7AH#R5%9%!a{>NOFLNuG1E${bkSP zd$wn(%2qAdY-|(WxDqJ-&)A~Ld~uoSvN(v& zA=s_INYN)_Rt2K5!>1zLa8?l0HAkHXCNOcaa$8GgW*56v3kUWnmcKyy4_+>mj44-2 zBQ_EgV4{d7YlD|uxOrFb>cN5}9Qx3%ylY^NoqngEyeP;ys_ytE{eYe4t7(c0 z#UquE9WJOhBg-ffLxnwaf?yCYBTS^T7M*fp-Ky<-<>ra)p@0w&vM!PaQd}x+&Xc*~ zT^wCGlMhITdX|`Ww9RW)0^l_}{N$DoOEFdkFJbgFRrr&r&n4PG3CU0~3(983T^OkC zRlzu&*#(2^uHqK?%!VB`j1e_`6x4_HJyds=6KI?ME>mT^47%179+@){xV!x2RclKu z= z?AG4szQXH_>&0zvn<)bjVsf_5^A+}B$(&@v_&7|uNp`T(aE4$%RMf>5s%@H*coJ~+ z@k^XP&xfk58m7RGJCFt30K)HOkT$2z(upmD1NKIiHPt+W3#V#a+^8kHUB;6en3 z=oLS>GrgzlvKzEe!44ii##foXT;bWLa7egOygC=sDf};>lnVK?z6>J6jBs!5fJ*^j zk1_-93Dt`U=Um$TWLF_u$xrH74Tyrpc84%rCVF}|>NLpq`Hx0Nt|$rTT%1uj!6FtP$W6h++j?P81h(e z7l7C%5(l4P{wNvHRYwc=kTi9r?2`9T#t!z!On)N$3q=ZK{7i+pj%cX!Y4>NvzW<#Z z$!n)7Tr@m1Yl2s5BA+~rudLq>?W0>9*;%#d9#llyjbm#b6tq~*C5`5MNTi?N=T!rt zP}5}YGkb(6mL<~S`U8PgUR+~1)km9ELcr%^qbajy=375 z->yujYvxE?wmOr@26Fs~%OG^8+NRPl+g3*}EsipiK{?d3oI!u|Unrt!O|OqF8i$$! zBz#}Je)?n%>DBhG>YdX!1H7s1o;kT7%Zjgl{ai6Nj93~gwVX7;YzDjdh)*6HuPu&I z7WIqTgqZi;n-c-ZXDe!>(qwYYgOQR;hr-Jae1o|`;RPCtq2Hm-v(rk!-jssh8}#M- zt_mL~lIoJ#Ne9yn5)C*GB+a7gvRURcPcRPc*R=QHbkrhWN0J^P-_aLDoZ5JjQ$8!s z_8tHX<}JE-01`sSqhi8C5GT|)w@@B=%h{_!`^$oLVjeS#sh);jwC$adxYpm(KkBvn zU7#992_6tuW2|x|o7)ZkjEFntX6qk#Lc)6iN;kc7OO(G{hi}O@!?<|3=v{FnkeG+v z>X3{jrsREdByl4zF76Uh%FTis`dYqOd-6%V?}FUM#_%SC1E$^^+`sk1t2gK!QgKyy<8Mrx_XHt{b|I51FNdS z8mX`dc$6!TLNW7m8qT=Ilpc}&!JS19#wqi>t5n7E-qe8wWUkEEdNy3N>)^^Ps~`5_ z-S32NRqtN6bP($^_}1*9LPI}GO~ z$$a(ZOUZl{#=V8-Q^|eR=U2#mW#>Kg$7xZ7zy7Mps#6zfPf4UR$@R%kZKS~xY7$+8 ziZaRNW!4$3C6o~P|Jt60|4b`RiPe#EeVY$8+I;uHdOo5Eo94kQs6A8YYh;#G>-RBs zngY!jP3;<^Xpnw2{v$A}#h*#q);>8AgR%+Q5=M`_$y}9=PuYfUht8`Epln>aVC%0o z|5gIa@N2ogZimEf&>-wOguwkJ2yiqw3n}C(2zL?0hMOSD&3u3s3S47gc17Sdgwt2T z>xk1Q(bSjF&oW>r(NoTHM(@;6Xg5%x&h-*?I+MRhfoa+pO4^826&Mik0%7ww8;w*I z?7!OO@voTeN%hrW=$2<_H=uj=aTy_tJsoRmJre z{-L@d-w`u3$4s%&a`@CI_WkF(6kw(ib4@nzmHUXQ0Dh*QAm{~3ez0ZiL-m3gxo|}Q{B&cf=FGjY0u%xFPq8B*2fMa9oF5$oUTEP80Y)cgTpNDNo z8hN=^NE*iA3VE}JAzJ_#oY_ExITs^%0~p&|i-J~Bkm~@BO<7mL;2UMFP(@T<1fE;+ zu$@XV!yx!4jv+pRFmE*}Pm2MGSj-N7S>r}a^1q(TeHb%dt7oUc1%7I-LDRwX92er# zcD(67n;_jeyhMlXm@j;Y0jEFUUifDQejbZs{p|w9h$4;lG{7Fg7uzsGF_2H$pza~v z^R9WyVeG*SyB6Q`4u3MB=t+&dM&I)$-|lNfb>iHUZ-YGh{NMUV=e&3o!wV`w zET>~^2_rElYnT5N{#4jXXIW5ql^}dlmb-R1M7kMrJ%ikGseR1qe!SZ9y?c1<1vxM- zVH%2q<;^&5T1OV-f%C+&dHZ!#GtnL9?Y{A+K&jF9I%Hwq-4lRR65l5UBaKc?1(%+> zjF@tyV<20}X5h?gB2~kagUWA$m19^|8fVe=xr;Gh|Nhf9`%I<$nYel1`ItdjYd)eC73;<@V^6z`$Ba4)Qus%9nBvwn`d;o>H|bO<8tU zfqX}-8hE9>8b|gJ8H3>rC2Ta*lGFPQU14M8jG=;@ChRO`@DFie|kprYIhFhbsr zIjcTnN@T++^V=iV(NXV$piF5d(NV9?CKZ3_!euWtoGC4- z!1VlCQx-H`WHV*{#3e@4s2t{!50{e}1~ifBWg; z|AcVK8#{b4eVza5o^S#EADWy#Pg11Kns&4(A zh5NV7{H=i9)N4{lON-xKKd$}VQr^ZrN(2$}qH|)2f??o|17y^Y5!#;9%u6Ks_noJi z^;UAp>_@}Q-DDMU6{_r4@A3Fg-JV@UmY5e ziYmOJuiVNyM9d@J!vZ3hl$ZnFxrEA=RLim3p+Zw(|EBZJIa;`kOcJ19g+fu*qGoW| zZca{qb3y7X$m)%O*^J?ZLRZMicFeyPta>XRXZ`G~ljF`_V3ww^uxo|I^Rhi2ORI_u%pyFI1TZ!@#Yv@E5*+-{kxh)3O z6&9A(-ibIG&ZJMr1b7C9+WUgtA`lF9Lh`(R-$2RAPK~b!+sexslO0wpP#vTW@OH|W zZAh51L!M3vc7qQ|dw?@MNE#(}2G)5wa2xBMLhVUGx#njoCGLh`bHGPilAH{`%i>_ z!y`iTMR6i&pp#o#ICNyMWpU;=5k>?8)Ohk4nCP;Z_IUFT(1l@uK;!^6324ZP%JiP> zb$r%ji(t;_WOmI0Hb|6Qf)zl#8(plUuv1iS)pr*pQfz)(y|yCD5wlqsEl>{Uw*lAp z0UIufh`Ju8h!_6Rruk}>J0v{pov$mIjSe}h`xk7~J__-;#TDl&rm8qF#xOL$)ktJV z-ZM!leG;pqunsHf+!5K6c3T2!gFA?IdI}zTLwXc=XKGq9T0YF$9j@glksG&WMin@E zg8+Yx=7~wb2rEIZ_;X*Ipi}&TS*1IMUi8-_)|-W@=>8tOI2yXA$c{AMaG)1(LZG=j z%$>}UdR&Wf(2-&-Q^)q!fUkPj7xXLF$_2nQl`C_Ps^WJTlH}TFqnoL7lInP71tlB^ zZ5Q4l7@utnt`-k`s+>P~Gio1v3hR@+W!}!COq7Tr?=^QCTMOUvNinV&M&~QtMFq_r zx`SImnZypyG6wW**OzTJUZf<}peX)Y5Fo#?_92YqGO^&(hQFqR7*60Lefsv62P8!C z{3bZLLzf5J*NWfbQEXZHR%g!)WeJT=BuH~(>KU&pJ#TmYadRFpxfF1tMdz-4_rh#o=u^de7!)L)(p)*ZrVuOrB0&g;qDjzlK~(3m*BErQL>ujFX!-XgQZHQ{2I?%Zz)N z5Ps4YKFk-jt1ueu_Jfhy5EE7L~!=)2AA1iL?sIr#WnYlQtjN18OJu^?K#Q#FfQxu0?)^gH(Vj9rJ6 zil(zZ5RK>+*^RxHgPsxU1xJU{ir;=GueDkTwTB@2Asxke$XA{NdRF{E;~xav@fnnl@rZ*Km0qzoe6vX zRPttQV#=GW|jf9H+S?_^78QZYmKE5yUhChRU^A=p+c~`?ozQgs@Aw%^n=5^ zTOJO_Y{xQ^*NX$(8Gy|1ZnM#*SExcO@I!a+a*8F@K_D^Y1Ky8*1L4kjS`{W#nmV?0KEhO`G<0JraSZ$3 zhh6?FRL4Uo^8AE~wAIefG$=AMP_TH9#B|{j#S;L}u{! zs#*ca@eWHdJCmGPEFO1x?eVxCNrdDitR=D>Lqu0zJD2Fr8ZR##;{M1GyOT$ml7z@E zQpo8RpWdb^%jP*N55go!guO;`%~Dug=uG+59VGYUApI1ar%_R!>c1`8W{+G+XH!b! zrE}$%o40^gTZ)MERV0ptImI%0nhxa~CQvok7$_h@co@MVIcj&oOq!zfEV}M2mP$y_ z2EJ?oYiUTI#JhR`IcB{j<8?%OF>bK?V(=o#(A(;?yMe%;czPT) zF(x!bKLJ>*UIDOoh^(rnc;Wa4GM-N)I^*u5nnEsSN$EMa~idO3}VZAfcU$ZdKzx8f9+h>4bo%HdE z_f(?;Ug^$$v5%7Dj}|Eb+bxmYlxNcDpE}1s?t90j&sp@xnF5iS2Ez}PtIseGH!}N^ zG7IW#iYA1N_*tYpbea}WB~IXJL)nb`SJJLs5l>`SnPM|!(=5Xs)LE2#q4p35m}Jkm zwZzYDiWvBvzzcM{bmH_8(g+eh=vYJ9ZrY^n)&hxl3G7aOtD#G)5hNX^z;2(l*j?(2 z(NciYF0s#{4_{fxs5+D{Zmhj2(!+EhAflX1AD^)MST^j}C>g^HKDKflf%z{-Mk8I# zz827?jP}byEu&%gZ4ypui>R&4nJMjQOApNRIUS?sUsX#aO}Pk@tvXT~IYK*~KY|i= zaJB`aJ-}2w6`^1ixTZmKb*PvY@G{Kr&7TS3p4Lt?PlJN3`Dn5I_=XCv;|s6hcX4|B zVFXbIDzRkPXiW`-pNdPQ-#j&4CfE!T%yQw!D8XnUR>N&Rq8mXhS5L18r=;r2I{;Ap zZD5c4WTiL!cf@wMN(V?=Q}g{1rt z%p;|nd-{CH!4W5Jp!sZ2R!VLbz!8duCY}PF7Z?@xWHo11q;Abk%>BgoZq5T9rWT&y zx!>g3-{`sD?17!ntc_<5J4~R!dSvPcxuE2)*UNyR^|&iE9BmiRtA@davnpO)$HP{t zfLMuY7S4OhaM(N!eMZ_40iJS>(B-bWfaWJDk-F1Q_$vUz@7rz!rsY&#)9>sZq3cBL&ckqR zBVgex^?CmE${(G@#q2Oe`lX)F^BN>*>I2T&2&)1U1Hk7W!D}D<`IY7{pLF=Hkcc1I ztCoi6+Z#UczwNepqa9`UeJO)6dtUF4s4IvjA=(`W1?+@-$EjX#kPsTTEH+(Xm#G$u z1__j>q1k!FAHTO$il4Yy;q=_nZ;qolkL4sYRgv+{4uxujXjvoVq)$aRnJ zVr>qXYf3Kn;yh&fE>ZS=?}wF@`(FT)OoDKsjoCaFKT1`*E45Z;n!Yz{H@%iOZ#S!! zcUF#CVd+WtU%!_$XZ3>E1((0lb{~fSkk6?U2hmOlARyZ0|4}~w|J4KhNBL}R53u_$ zHS~Yp;;Y2vADHZaV_yHC%?&F%gr;QyuEwKiN( z)v-Rai>4kLnxu4<08D5w)&bvzm7r5o{)!ZFT2q6{Y}qL+6lZR=byBO{0y(=PITtkz zu_3V`d8bdFE;2vk_C2P7Y}T~W3o$=d-yih+jvw`>J$e#*Kb|iULAsv|gn#C5Oo`Kz z^6%f;YKL|@BpEKWTRTterw9~k=iuVAvS-h%T14hrhEiv&FW@hA|S@bFyYny-1Q$A!Kp)zrCKd2fT)H8vz=WA@Q+Fz z`AkqFC?3mA8Mk4#H&Fm>{|F&ds<^SXP-W2CHKV=2RE66ZUWDoJ=q3|6%ELU3wGLEfZ=sY^m?JT)e z3}hu;;2b{!94rqt@dx1CxMf>eu{SCtdEIGNiwnRyu@O>7$rjS)f=5}MruMBjnA;|l zc$vN2eUkE9Bd9R790=@kK-mHnAoOV-ObA7Pdx3+8lwARI0(MKo2mvJ)#fAF^vb6ZR z+X-5YIOj<$=hUMfm0^1zqVhm*9PJ_)2a_YPzs3os`8iAnjEnodb3aKvb^2Lpa)#$3 zrb+65eWl4t;1#eL%8FIc1mk;pdSqM~bL0dBf%`BDP`SBPRMgdFJ87nr%|*s4ajb7` ze3)a1Tkf7}t<~0~^r{Ma5y?eSDC5azPfQ6#9*hdqVU;JM{`X#(+$s5EhX&!q-)W`C z&CA^tmQ9gL(aF^KLRsN1TM}a^TpAmJnD-0nu5vvurKRmx;i{3p;3y?fNP|?tx>3XX zU5fT<^8YMQad8SjDICv;00bCG?&ym7kxeRN>y;GGgM@O2en2pW8FeZt2WqcziC5{5 zC}@}-dQRV8$jxbY@WBz{G@`Hm*|WL)`)MC+0ybU^kRFco3^y)>+C{k|QT9bjr6V2i zgkhweRU^x1wPS1`&K_c`G~^q_8+1Q-P*$M)uJ{_45@`b5ZtvNu9U92@mPUQ|pGe)Q&wyNcg%3@V$_sWaSN`=*>3&F)zaPD-R z!xsb~y>Qg>xix*~hXqo;l(lbMQcM$Kis z*5{N&TvS^+W_RZdokV5Nn{=|h24bZ+)Q)3yfm2@KXxV4}c`n4%4<*4-NoiQAZw$)K{!BGn| zfksMOF$Pw{nlTy~b+fO?I&_%j}ZWc<@uBpjHm zl1^iF7p2wX__skFj$Rj>k;|;YyKmT4_Oc$rtxyIRd1&p}!fADJ5QUW$V5!>#_`km$ zASKvqO7}?6V=76S2GHw!KhhIv-SM6=f?wx5`Fxw|Ex5Ggwl>Il`{s2;bmHD6qq3R{ zTmk57eAm6<*(S0g?hbFY`ny5q2&J?DkMd?2^wdh;`-fqV+V8ByJ~IbuYcK}lqMcSU z;!p&|hI4yt!o(eZd)WJVjdPX^8vpVI;`Om*Z%hNv#-MK(V(6++lFj|HJ2b$E7MPK# z%iQp@o`N{ez!a9tgAfx_eM5#81acM_v>GHz#xFh-q{fUNU*YheNmJ&9cD)a_M~0@R zCETC+#H3h*D>q-;9y;LxJMly_OjdOEcuXAmg;;?iJa%GONX^55+SU%7CskH>>V`1+ z<`bxB?hlf?>mtqjlTivr<`knRNdj?))+bu0ysu>)1rc3v_%#zW{T!Sm#rD*uvO9JW zu?&9ube-}5yPrV|jr`rJ$jc+%ST^9_SI%xLN;VYeX{vpxO>}Pke!vif(R2IG^qhJ#P zT(n>0Lz50)`qfgWms-?hK^}b=t=VbP>}<0A7m(LQU7K)I(gjV-KJ3N~i6D5V`w9C2 zgWzGY(tt)APj_4=SamhMKmC67el(5${@UyRN%<3NTY1M6t5%J)#6ZvaDY{{bHW{in z{utOVADa10KIKXiAC-UjoM4XDrhh7m*?|tOMs;GGX2ZXMDzk_&Cu>exutq)+L&oO4 zR)p;|IG>Vug$3*ZHC&Y6nxjuNWHTiA4$abd&L(@4dup9p<$vQ_&tW9IM_YgkJaFs#ncfheG|2 zdK)W_LEHRPck|sE5hY7h;jLIOnC;pj+A8kB;)h$lJ0=;E!{ZwxHeEo@@f z_0Zs~pBk?v0H-$lQK{B2X@J+S3_(gGamzJWzKk=*EJ1d7qoU{HIoIH3V0@C3nLahm zG{m#hHmiP{RE@$f3Pp>h>I656je;1209rj|4FTjOPIUlUV32A@6lq5}I7(mQjEIh> zbtfH-s*3>v^DBq&qxt3o>`jhJ9;g!v1MXr)1p@B)Yi2`Bne%tB(P#Etbw}g1%*JU> zB6&*Rj#gkG_7th&rJ91e{aA+IxU-Z28bfb-y42;=eNpy@O<4{|!`R!KIpGJA+YLX+ z9MdDclqs^>&^Dy8;3--oZ-pgLuCN%W!YZYR*0`@a`dHx9+7@=gWG{;jQc8fw-cHbo zFe)@Tqvk-;6#b}!iiwF1RHsCBGE)D9$Vk5HFxJR?O61ILzU)a0Y>D}5N+kMFPHXGW zN{NYB7=>w-4%CBn?oQBSNe*g(SWI*ux!DUD-SXmg-LtA1CifcD&yh;>QJSUf{0-2@Z?(cWSC}IbfZpecK<3i}6-=}OPk{@?o4{icILiBz`@{)=A ztfcj@)9Lm))q^Y(o6UlE-39_|HxTzvojyT7OPpor7V_^9wvS%p84FdogdbeF58;*K@B1k2(cb zN}c#^K5_Vh;SI*otI=@yC)>&2)eX--t4soXQHepgT+IO|JkyaQ^`^FMCxjAS007nk z?k_l~0Dw8i%>Yl0gD|fYEr_Bqp+F!d0EV(GhhO?nqo;(()^&|qkQYwxh~qDcZ%W4O zKYNF-`SwTpi=OpuJr5o1u`%bd%T?T7`IGO7u>E2MJ-rO_8I3b${9g&^F^f_f9UP~a z2tUFE$NUH#_Gjkp2(RJ}Qs)f;nmk*ft#v>L=p7{w(crV+dylp3rdLWAL(ZtCuN^DO zZ6OYym-YK12F{0WSe#A(_(S+AJb%nwW2}+N!KeA{!Sg5QRH29hWkFLOd*!sZpk`Ih<19 zp70gWDfw5CmsZ4c9lpOT>FaWPSL zNEJ$F`mF-W*+-LWxtVt!JJQV^iDO!Lc<_&XjSzcGq7tboG6w2;OuycgJ6*NldAmZx zqMC*#-GF*PWQ_EnDU5KAg58-4U1FV{1bqyp9XCviZgSH)PrEeL29Zct_>Gzog3XcC zB?EuveALkc|9BnB{G?K5N#ztH;=BQaC4{#RSN!aHHAN%z(t}5OhCNR7M_R%>O7w?* z25Xs4^oKj~_-vF~Vegd6(uBIdiZm_t((m3G!`h;$s@=sEPHC=iz{5p`c0@7v>&X47 zd#=w-VQ9@I1hysxcEu07ehS zz&&P70H_8c3=Pksksm+%$2nL@%eI^@0HE6!6jp;(u9OvIT9jdVB^J9Dm0CXjJS)Z} zP{V^zY0fFHu~JeO8Qd_n1o#~1c=U>d$qH{I%c9@M%i=8s62XP1OWO;nwzr_sj**el z&JOsyFq7nFME>y-xp(z=sS5zC7hU-w4u2$ZRN;Uw|A?_dd2vi|BH%!k;j*fl)Mtea zc#UJCXHQ~^{ziXzI} zA|f_M1I6EH)UBg4_l~{L((I>yuQ>jINb99>kEms_UVGB<+kWdKswBeZAdOlb zjR>L7J&Q44pUfy~maFodzx5GnJ2(gQd5MM6pCH_^QK>>TuOL!Bix4mH#{lo^V;wD4 z$(E|ox{1Y_Dta3Tui1qdVm`~M!(kK+0=TXiA7j#7q=3j&k>Y5!Vjcz|14R{8TGhkV ztY?w~ z@JeXt0jEU0mb8)@8VQJ({BbOyh|v;1N5^@2-vRlID1Iywy6K>9er@BjnNYTV&h1H? zHk))7mbRqVu!OOb3pq=MZvXAZ6E2KR430SVO^^JR%>r$?!4^13Z4va;{x0csz>6{N zsBv4kyFa$BTCT=JLo~Q*Qp<#S7f!A5IroOM11+=|zh}3`$oY-eKwt`huB+M2gRF~n z8;6o^GI8+4guiwOm%K}QB=Z(SWVT5q|Knmd?kITCco**DnBDhwVNU_EdhiNBJ|vM; zHD$7LOD14B&f|EQz@fQE(LHOhh|hhUmK8b}_HK8AUL`qHaA;(cQv z!cBW1y?@3bU+ANc6?09R#dhb`$uXIKgAfklCo}?iORV5Hg@8z#Q3JraX7YfERD?z| z%2m8RbEkihZBrNtb>e@0xS`(1xtO~=AeTie(aTt3!Dh$CkRq$f);BS*GnE1QdXox} z>=v>J0Am$z>3a{kN2(Do)(!)F#>2+acl&Ela{DOBePl+yZBBA)ogj@$?Y|UqiHsJj za*4aVP99_UNDNf72+*o@TO1+x#KY0Jhjy$WTRNsXK_akZz;7G12L!r3PM0$;!hK`N zN!>i9zM=}xhl$#8z%FbHeQuN-4hgBVqhT}Qv(U~caG}|Zd?njt+;4}nrvYd3XnyyE(s_#~_ zJFZ$7E^d4OrC^-E$q=S^%kCQ7?f+~(oQEENr|)ar&XOK;3*}_F_NLvEF$<;gJyit!`w$P3+b?* zmOt?7l_dw+*q~xfA3{eK!Y_&!3%zzlX4#Vay5+x2J9qVD_;#zX9MpLyN#HYew!LcXS z50>I?32b`mIV+!xT{(`$vf$o7%@<`bZrnM8zs=vkKFzv2cnY0v?f0_^>0ScY?pQu9 zSvJtO>Z_Y|?cOy(Z75 z%aGH2@p}kKEin*V@AxymX;_q{w$qm|0F(npSgPe^)P$k%b6j+oS9xgBj@N|IQPf16 z$(GTp?|R?4^-4r^At33QZx1V}#s~eU@U@(de1{fDe?;2S46lmR&yP$M0wXNEF za5i|u*`KL%F>~=wO9#sKA0_q9U>URZHWl%3%?=FSIy%ufgBjlWUr~vie&n(4Yt{sb zE7I^%^f1BFY3tAlstSc6kVmeRwDNrNBKUN&~_U14n6LpSr7pziOT!V1p5nv77MDmfNQ z;9_lNI5GQJ`mytYF%9JSx}KC547k~_oH4_(eMJ1?v%+-FxK@TWdY#7~faX=OQfXl$ zFNVW$@NU+TkQmzFuz5pTtYJ`_E4rj3F)wA7#nlc&X$Iz8Kpbu)yO-Q}_TAp&Tlf&Ehr(5u8Z*gGp(Un0@g|XJjDx8#ysx zu=UaPVf!SzIl8 zf4Dh0*LL$*oC?LTdq~HJ-pT*b=+`^j*r`P1^CXJjSqLKN;{=WddrW@Y!MCJGjM%)d zdn)A7XiPYDO(nG|>=Usb)Yb~S_Kg6WzEtNi@|cAuN5N>>#Eqmouv}s9K9FSDDkM{r zR*wQ@;*M;ko_$ZThCAOP#1Wi!6tIqeB~4s2Y3@ex+}$jzFG72IfOn5t=8E2ctFcs7 zqZ2hdFs#K3WL^E~);-KVRrNX2iR@5jefYk4q_G`UgCjmF+Gd1y+gV?ke^ijLIidMy zVrzTiLz(P)#7H!H8$bwmL;uUB4EyNbz=QuZ@jD09D)_Cm9rtI>5QAYPhusi|>yD4R zHyl`3!|dscU3uxB;2Arfkh4{;UFz@=e)1zf16W#4mH;AZ$2QnqREhfQ2k5n&ppLtz z6vZ3*QZJt5--%c^E>$8v5=u!dmHG>7f7|TLu7zUY!>RoP(wnp*xE_+-=(@(!PI42WWB8*1o2{F0qnPKn;DQI5KlTnk9eDW|u^8U{1< ze6q6+cMV$gY7)-!T=ezzuQLv3i=af=_@P?WfFyRaD7>oa-loVVup$vWtDYZb(|{p-Pn_Lup%2@;RLn zXd~hK0?{ZTy-V+Jqv*12he)6(5(xP~n%vQX28htQA}$rV(z{cfm`iNDHCA<=$mtT7 zmy^vik~s=zrM0-As`Gy4x3d^)tJ7biRgNN#gt|*`CJML|T=vsW3AkJcy~jV%cQ7XQ zDHH*T{9mnmr1s-Zu*c)_vyUJ-sN`3Ux7!KGZ?xOGJb|A>-V+ixB3+s|#6zQHPYQZY z03G^@eWI-6h~IL?id5M%-nwNWEk_y~o=cZJ@>@pF}ZJ2MA(}^n;vk; zuZwZ_5ebFYK@SwITA#@13}q}atm*G6d6j5-6~E7ux_OnHZm~hiL%CrU`IY$LLesZxi8rw|;*%`$p z5^U--BU|ur?AmTxtZb63@W3koS^M`Rg2ot<(zIY4;Ocl)6058JSL}j7GJ!&iib|i!lwfZ$ z4Gqp_!Es_3e5So4Oa5y$pnP{3QFtULt7;FdXz9rT^M$jwy-}E+cu13vGYvtGpCvQp z%)sVR2qhkCW=WBuWZFb_#~0U((qj|nU~;Y>W(`->*k>c}w2_&iXDL{nP)gUt4?)rn zvdeN#@kWd`{a6MW8vPxy)}O~)*1rJA4~{e&0yt^k2j?!ES~MHW7o(}?n>8L}?IOG+ z1&L6zYk#WfQRFA<_EuLlH565Jr=)AlXDD1v12*>TNDNp?iX~V}n5z!O=B+RXodf-H zejT2*WyKYF@bZuinY$L)UV5dbr5OiR^W6PBp`om+*CR z95{A=^cUdX|5}~#rHW%biQyk7x zFV@~FD$XEGw+@=%?(XjH!QEYhySozzH13TAcXxMdG`PDo?oM#>GjlaF>p$ma&RXB< z+rF!+x9ZvZQD2H{f}WXn_Aup@NJ?bvJXa|(*1g|F=hL%XK&JC){LYaaf>~7 z;xrB-*H2D7ao*usv=o03yeb)|f(fS_2U(BMLg|`@z2t>Pc=%$Bm6~s+n_0u2s%Cve z@PKrO((tHQ3hM-iVqCtDBu5)H?#>}qekv_R_@4n!z8*Ga;IPpCx0JcXcB!KMYp4?n zXef_A{2)BH`Bw#XZLC7rLRRy1T#CEmO+Mx7DCd3D)*31htcUp-oOxWm9WtyZ?N1lm z^CRCx(y$i~cIsVlnY?i^;V&MIYM_xVx3LF0YQX&?F7m6?c>%SZ@S-&?z0_( z%8LL%liReC8>(-fhd(`nKxx3T7v_xy(k)4eEQ{e&RRp4r_%f;Fy;P6)wV?>*me;+B{(<6@a*D({vLSUWkM0H z{63CGc^FcU)CgMt_{&>-zW>L)=t0pNqnE1dHyJu}IC)|OViidLh8ym4Tw>U&??WVK zlTKda{msE_B*W|Yf-Z$9pvBLC%{Qp*QySw1-o%6>&UqIEMa#%Iw{0`*1qiCxN@Mzc zZ$yZ?R9jzs`r?>&EsE#0qXbZ-3V)!-UWChLtvpp|c|cWr7^6ys%jjgI0*kX>_IK}B z)1Y{PYowW?SQev*Ih)!s{0~_{SYWDWmNb8*VYXqmcI9EjIQ8#n)HiTkHkMuYTi?^3 zl9DZPOh3knA38?I<4E>$_1hdtQf^3vrFsNP7|aVDz*IH@{OE$`!okXNPrw-EJ&sV% zv0=8jxvXdmZrheE`KmFp5MKu_kZyLrl0@@2ibWD|Lt0Ar^eei?g)3 z3w_Ep2EDqSJ>BNuuz)6<2*X)UYeKB}SV)DHa5e8xZkB%}@+D@pM6sHbN0JvDGcCkh z#>9YM8kxQZ0pPFt$1IsxwV6fd?}IV+a=Z!zi{7NeVEp~V#jnTkQ`_BRa$D@|l*(4! zl#Q`)C$xoCMifalI(r2U>9qtFufnH3&0QI+!(93lH*^-X=5mX+9A#7(HS!(jsLTNp z9&@P$c;?ORL&t&ht3PHuOj{ht8hr%0k4ut@nN>D|v0MkcE6>AVpJIQ+;Qg(N0wShh zEK`Rky&3%878bc-(%hn3KjT!MWA7k_Q7BY=^9MP*d9#&k4{qEp=I^IubvO+KU>KN< z9WZQFEOGs6vavHcYYgQyk8_FDs}e*iLm}B~ zrff=zKwK;V>xK-2-1zF21llQ?YH&D({-opRB6TDwTcu>*3E3PO0St>3^V68k&6TM)9glDmgv{6zJ@FZ>quG0UVAbJ8QR4WYLl zwLSCdbmEY(#cZL=e6_Y?&7rn#alf0jmeM98_w9pK|w@km=4 zi@!tAQhfsvxSh(qVf?!-Q#Q^XPl&nRAN0EmRUegtiFsJ^elsed4c%p+0|CDSMk3P?E*h1nzxRe@E)43kcbGLE z+?%7uWGr^^hfeUSX4i!oQz)ePyvC1FzTf0+N>X|12)$rmS{s8myj5{;$cyJthkI?YV90W#ipOpGZ>2XmW)TYDcaAo@X%_{K1+WB>uv(`X zK8gZ(kK>6^F(^3krgy{ON+hoKMP*F243)Mi;VY#0y~Zd8J1>E5Ryg7Fz&HNUC2eC& zo8vZYYHC*MnNznRmI5wa3A=`fC3CduF><|RVm)`{%KXG!KSF(Jipa46Ci-O|fZ;jY zLc<{zTlK?D*2S}2yfYc$W1@DSC?S`m{R+?d}t-ZO$|>=bAm8LWl*ffN}#cvh@^ZlZR^ ztmvGkP@aq{glw=A?^63^cy_LysJ9@?TC8UDR3fT#M2(gl2zMMEGjz4=I0LcG^Sl^Q z1|$RRiEo*PpkJ26?)wOs-%$^FxIMTpF8DXvOUv_%ds6CrkR>d`4ASDCX%|!}ospy9 zCawDGu#^X@a>%5;+auF$2E$FM!5w}1jpR2~O$lO{cX}aV{Bkg#oo6Pmct1D$x#v0g zWUn2P^tQyjxmmxmZx-GNCFTn==q>vW@>)VO39Q&lPITZ5uNcvqXnLOUrl}Ok#Nf{M z7}Kywy}H%|>soq7qI4onf(HJQSmE5Rs|JPgEIn2Clo4^fLrHD_1Ybq20!KfQ_}4&sVb%5}Ax zs7sT~ztYjnUTAo?D|1|3Ao@WJ=gtrsr)B76Qb^#WRWKdA9{S<;Ux}=*=jj&gH(M7( zM26X+FC2A2a157_1U2IzTWAjhT4%H+RIQcz9^>hm*+MPy2bH-#!gb6^KLP$ua#`J#?Zo=p{3#d|PZT2ko7XmsR$n`SrL&ROW9*rt1#KnP51JVqEoX z&Zt;OkCnli!HP@H-u|9wp?9oEWXSq($aP&Y0H%wO5 zOO0O>n1ML7CI=^ex4fIYp`S671Ag{Yl=Td1nK&r*(Nno31jrj4z8GvO#_a#%w2}o_;>Rf z87NXOI{gxuje#I1Sc}nyynF|zWjs(<;1V|!8WxO*_Sab0D4yrIO?8L5i%sYeWrXjt zj#ON{9(O%+!GvJ_CziJR6+JB2GFboGQpLs(eDXT-gl7cqr$slP5)h1tY?${oXI+?Qh>@9G)c!M15 zwJSVOu~6ir06WE~VIC<*)D+jyEca@qy0Gix~im&6nIVt(#?PtC2hb!m83EW}2t` zYw>RD9$9dBvB!ymv)cr5f*S=SNs^mU&RN6V4X)Iu(Vi9HG7y3|Jmh;=oyIVLfAE2V z;4k5rI{$TlP#Q*@(CMe&C{>|Jd z2M2+-?fsHPnV1Q#AauR2aYP&WifVGI^OOWW4c}i&%~^wT>c|MBJsPDRd-sAu@(Mc9 zh%EI{9ykh%;Qo-ISZ#_fLSs}|l})c5lm$yWj#MSbpXJRvG~JEKw};%)<|cD9Oc**i zneqJ)EY;6B{*F$aTfsmodgq?OQkD>PD51jtMX5*-UU~v3PdkvL2M`l`?IyzWK=d#mZd6v5ka3d76QXfPl9-?~KyN~!SLT%8~DZ)~j1_9NZCa8OD{ znYQO)X@D^yrY`Lze^x?gGs`Hm;Blr%LsTr3%Y8zQ&M$w;Qc7@8bbrlZ@>6glEYEK& z3{EObT4I$rq{@b9A;8atDUqsI{-Wkl>~2g&cMBHrYq1!sMr%rCKCB>Rt|qTI7h=>8 z{xvtc1TN*TrZHeuu0si)Hgu zd025Z$;|UzyJY0d#yxL1sF*KtKLYF8VlGHb3eM_n8%}F_3`72$hq=I95bI1Yd6HTdjkyjbYbo-%2nC{6(UEHYs*?Aa@i8OqU8jcG? zS_9t!Gk+;a$+el3@orEuSrn|n9tG~7gmFi0J#~)&$@44x7R((h#hEq9u0c0fK`DLL zW&ZsaS8D=}U{XurRtvo>D`&X1+C$(3l$tAw2C;!JZ5*e!M{3F zXRj=CCqRAN#@QPJWai5SoKW$1Ys`!vAB1u><_QpUtn8Wf2dLV`qsi-RM6*lFH6pQT zf-k2`%Np5|AY8MZ&f+)h!mCBDG=qAoA7c#vX4;L9P;M^6`!aRMZOXRj7(iO2=5;YA zeMm3QBk^||%@7<*Z3)t`O<6f+;sXQdF-Td&*Y))D_4zaL`PT?O-z&EH_X6V-mgfmz zx+5JeL9#2^Q}5yK?}jOz{_AoZ1gsq?j`Js;qE5vEb20G;3)9`RKN+37f`^Cy44Xb3 zJIrxkk`qo$=-we16V5eXqTPF*XtU$-0nhG{Yxt9&#}l6nAq~jcg1W&{%lIl?7&#yP6WY* ziPWmAxFcH0y!4^Fji6t9@w_x#|9KPvYXM_}PTf2{E~f=F&>aLozcf+xB-xUi-R zr?J5Db>@3K-dPe3B;k9p68R?VYeg5SGqq4;ppVJCCw1%Ks!@h(fuY+=7AsSCDy2;z zzp_EwInnMQx3TM2ry(Zl%wJ#k_RG{sV%Oj-tQyoc{(226+z;*-Yt5t^AhxVW{>_&O z4QBcc&xkbq7U2+j1~TzDVd}g_P`;|rq3=6Vz_3+5E8ENT+l>D7PoIj;)p)LE()e!g z7t;%<4W2Buul#Hsp$o*estn+ra?=p5Al~OPS`eP1X_rCsbpO;_s$irW;aK%RsiWUy zC^04F*G;?1J%mh!Ew`^aO%{y}o(I!uYO&utS7z2xEl+922er&4<6WNYLizThIay$s zFQK@4m0_>Ph%TG`F@QRqG8R5kwoq+S%w&w;0i2*_m$eiSHC|Xl=9@QTl6f zgk{@+IRzFJSNKijkj$bEtP__DrHd6a)8p}FZM6X_hgY6>?cYt+TUh%QN^sf;gMF)G^n zLfJ-T!-4Vc$!k>f+}H(k#(Zxx-mbo+^{rY#pf%#3i_mm+k{d)m(;PO=*gsnzJdt;Q z?1<*ud)xd2n`cRUx7KI-4skf=Sm-t_Yhgu}Ip)MGW-CuZ;RYk(RZ>otMJ!b!vT(*iGON>oK_*OLs$yIK{&G8xiTE*(v z?fpt;U2$}{G6BBSnjx2D(&9?kvD!^*e`oXi6>1L}2{n501PUd;9!yv zU;e8S*J?F5YnLkKwre+~2d#?S$`-oWOlN#PEzaZ)1Rqf(vBdy!_WT6PIAmGX=lhb? zCGVW*eSCKx6TR}gBBQ`+1))i-boVz1a$f4L5J_3rX*K}XKR|N zeLI+WZiIl0)tT5nSc#->GA0Ug601a4DF4Mump*w9^Fp3XJ(RDmM);$ErIQm@*H+DO zt3tZ|3QWnIo6)7vk2wYV)e^r^V!9w+geq?`Mt670?E7$%YA8ttWR*ZzUl(Pt&pp;l zMFH5hYXD|(*l$0QisG2$s~0*A8q~0h!mkV;JQFqgyA(P?(;>mBNjw}4Kx0#e< zP9|S>%5RgsEB*a5Q^nfNYk zS~6U^qMd2rD{Q51dIZ2ep`{`#QzY^B_dp-vNsQ(~@1_Y5N$Yl`YalNjiP9~I@9jF& z-+R~7CqYz*PVz=}K+5U&qs;`%UEHo);nOWqm)>DD{?PKs^@T@ISWl-41461-F9uvsKXxcwdnb*5g8dy@OoRHPJc4 zBX=Z|zfYUbxkvcX1u%SAWCYr+ju=HBcoRXkA^SN))eN4Y4aMRPD^~sFX^7vPDCiF3 zda~q>)4##n0I=*;%Oz;@4#=&oHQpP(5F;!{TOGgNLusw42}+3+k<>$`?UB8(#S6xe ziHhVOc`vgvYcHLcA*RhKufn?zHzoy2iAXcQivdvfbH&QzUT*}W+y#gKRVta3$4&2H zM$umRKz|bDQ-we112EpBxSs8j1I2rbw~30j? z&KT8pONYGNo$lnT+xjK95gmPvY!3=p3G=F7xjK@0|6&yS*f;2xV%=J&l6w0rGMk%n z@kkxEF)sFjfUp{G>GeQvPyn3)nn|S(_61&JleHT6)17EAxG9fqOUUPAsa?98ynBf0 z1Qqsn#u0jeCfQSGsLYzc$Ux1bm^td_dd;a=6%g-*^V=l2QA73-D(kCogqYyq^QlRR zD{lmjJim;;jX``y5FZ1vp8!;)Shd+TlV1=bZ!DWcYE2aSQ*Ddw3&u0Y z73Un)&lMij7q!26E&8Mf%j)Z>=rc`HNT`Tx8xXP;mTyfB z3wCP$&$AzmyMw@Vb}iF9{epF|XRubel%`%Rw)+wD9J8nNS(E%G20`=Wr8Psmo`(61 zHC>8rXG@ArXES#-l*PZV{{ga-L)c|ne8%ECKf8DT&(V0L|IHn*jESqcg`>rPk8E`7 zoT+1IWB%KOZurTd*Du{{wg*Mof3|Vd$-<)-jh23ls@`cboP7tNl)t*<-QO>r|BV*9V-PzR_*z0OY z67e7TWx7(H!v{RK$SjzgffSR9$@a=7}a&u3>{Sc`WggR!S2x*Y*6h`HR1ATIl8XYtJ&*gSdTUlv7mA>06G2U;N~ zjs92xUqJlykq?7je#J_RCtx!2>AO889~K@FF)@(|ulm=Kf6&OSCJe-Cb&8T-`SF;m zKtBq1_@ye2FS2*9?PnK=0Q&h1XQpj6xEjvn+Fr(`A%^tC!;XMEq&}ksH^GjLQB+$f`iRgg^GR44!t=-ppS_qP@OL<+2nFASzy^y*2zViIc1hKVE6*qY5vrRz-`v`1Xt zR$btB_00$h^QRJ8;97s*hpbeA%%lkb%v9$|OR^wyAUcK;%jDaH&%+~%00g^#ccd&P zVahQO7_Fk~w-OKlIrQm99?NH(f%$YJhS{D)5TE!r`U~qSxR1{}x9?L`XAP1SEZ+e} zm0r6WdVR8PD^s}hu%*yP%N%%f=GFm@lAhh~6Od`n5D0c#k;-n38ke|N3KX3CIlipucYUjZ`>tU2q ztQ<83$9IV;K~l)%RClTSweg_t_H;udAXA+a7<3 z6qAZxX$M}WZe#n`i>@`Nq|suXP(Fsp|Dtwp9M%8sYW=1fUaJPM7Wq%5Ha953@^{#?Ne&2$WrJapXzf)w;dJT1O ztx||WH5N-&fRDKGFeXHFAEQIuqjfwRV!Xjn7dMIKYtem;3(vtZKoUa#azV=8tH}D3 z^F9i!qmS{NU*drJ9mB}yKDRvEY?w`s2vPeRJepCWmfmpN;d(;JM6&sQKc%}jngq1B z>+^WHX9RiWr-0;-W(0zNH8=~4TnWh*IaiB!rUru06EvR2*jedxBE^-%>M23aT@WF_ zfv%A4c9F2^0e~2*RT15&xxtaIkw$mF9%f{&KZJ=f+aH@2%_wLhLq21j=`>^|{W)fS zn?TX1=DQy88Jec(03fkf9|~0CXz=W7ajf8bkAik93NmEFBvq#jQ?hk_ecx2h7AX3f z=N+&l`wxeP?|q6p>X&T+-~vDbDq6V z^?ZQseur0S2QG{Jl@V)yV4G;m)gtKi26>ChbU;2VrQ09nDh>Byq!b|egRLqDim)~7xMg+IyseZ6KAPHYNz0y6eC34@MY zj{@3aS!NiCR^|<`O8z+AyjuL8P=rJq_qexr>iPQa$N747Pl(2ibN+^<7HNv1Suu_% z@aMmOa)`Z2qxR}T8ojAG0e39_vDJMe)f#sC~JBdQ! zb3waXTbo8WO&C%(aAISxX5n#vF8iT-bU9fV8ww*`K7drtT&z^*uuts?q6gJ!f*~fi zMiS=i%mC`D4loe|YnPgcVv+{OtqX5*#=~BmK-|vN{d~gR-QL{X9tiHiNA0OvkwNe@ zE(LfJ@#Oz9@kUsS>N<<~9b&~d3I6@rCXRT8B2-5es(W@0{BhplQ)R%TWp z%jLYY0e^RG$-Raq$Ymsl%`#M$ZR+~zrK*Tntpx<4ThGA4OYtv=s-p7I_+Fzqau!V4 z{gt(htitSD{mTRM9izB@PA3+CS5@A33)Oa6gF|Vj2+$U{yo(8fSRV_ug1@^PK=NCq z7p(a(ZEO@g`13L}15}8~#*3v&Mhr(M;7~CzQ)@5fk9tD0mZg(tOG>&k~j2-#+<1LjyMe_Qce`=QygpB8dbtu{Ou~NAr zCAEEa1B--mnm$Z`=SbH`wy3-tHTK#@VJ|J&?a3avYtG0whN@r?z`?JU2fdV& zu~+^qTe~8y8Cg0(5apGfTxscH&N?!qFgEo|%7v&R!KuAri+T`pF)`gY0@aOS%FO6j zIR%0p(#r!Oq5#JR5gRjd<&>#f(cxI?hl;krpRp5f48kkPS{+goCE4_vI7sYKGB>F} zxqSqHuIo2AzU7!`orUX!IG#NwM4psjy!@AgZ=j;E8(NDZ!$|sM^SV~>1^!n4UbZ(5 zsPc#KlV+oHe|}4Z0C+wXmxoZhNTI=IJjSNoy+b=*O?xY*+V&-ITS`#c?EaHzxj}h?yKnu75sMG`I5e z+YqPY!F%|^`FqJv8pVpWCmXnmG1~?R!@4f+5rFPv@)6T6EQwUK+%X=hVR0=dNsN>r>* z%4M&JbuYI!V5s;?KX27+DkqfdfK+Bi52DC_7>|CKs&p5ZS{l9Xm%Ngdjk4EJ;^AOn zq)S3JKlM%pmYMUFXjJvFZs>mfZp%DpyP~^ppdl*GtbhG|-(4S%Wh104gUq=vVFpSX#2?)>`cs%(> zNB>p$?F$6gU(Q(uPFHNjChV2@fHo_b4}jII|59)0(ms5PNUWhUeMw~n+2iaxH+q0T z4jKJ%JvjU+v4Yd@nlXoBHtB^er*u(-%r`S05GWVw<&^k?En0wtJnMFD{js%|y)IN= z+xZo%Sb02XXofiGV#@ZLtBMY4Z!*aTj981sLFmM7YHP#ul|LdEx}OV1T@~?+`kbO&iHm69CXaf%q~sK2 z`7S3@e?DIeyctctUVnX4?f(mwEO?9Zg$q(keFRTGA>gM#z7jBOXauJH3t|d>=HXLu zr0MYDVg`9*#(s=%3MCyzU7obVlL=YB&UDATQQfnE2jI7S_vyDLXM)28u7iobrrq>Y z`Mbm98WHtShCh~rr>l3yIo~`07XFo@r{~)IBzdC{L{j=LQDXMcU)cbTG4ql89&IET z3l+J{jp4Mb;=pqQU3Pj z%Ms3hJ%^WD2Y2!vSDm&`$h~cT;JptDtny&+Rvu4~%#(YA}(a%xL)e=I{1t z!AFbB?DxCe?T;@BKb`WTP1aDDq_viyZn_TPwp$19lIk45&;xYUZZy}`Rg?0jo6X|` zcZCsoBj0P`KDmIq$z}qvlwn5Q>Q(H=iW)1X(8UaQ`tlb>@g~NlA_}Mzenb@0z2Dpk z1jdb>GA>Z60B<*1qvWqUx5O;bZ(54?g(M}B9Jt1~qn79=#T&NFnQ|ojAxlsR6-EMnh``ra!z;hTobAXrt&Cad>Q-Ppu(7BXPJl>CeWexnG z()(Z%N+!nPPFQ2Q(8q;g<{c0AbQ5GBYncl!qxK=Cub~N>p5bWh%9>`QTpLviJK*VS zp{+N?QV8FK@jETnm$O>x2xB?Vv?7li|{91uEeO*d%bI9mn!PC ziQZI1{Y;i#+bQAnn!%B!-pFjT*b&EQbqNPooM~2>w$+nCRp!`hX^E)uFKe9Sh4fro zvvtY^PaWXZSY%YHFp3P*zzS>D$FyROn?+uY6dSyDrkpzC(&D`Y>u+j1#t}QdtGNo9 zq!?H!YPnO%+Olm$H8WHXcC`oZH~Y0Y?Qme^Qp~E^IAA*h8hgGWx3g`=5ya;U7)6jr z>O#?B9rD>0TnxY~cUZt4(boS%P<%C7ezoxuVND!{6z_Fa|23@e`cV$WFd~t_Rv$qX za(LjnY2of|`g#|6uMs>osu&QU#&UE26cx<4I3FigWa_j-CVsllT7jgid->|>Rw~co zUTlR2h+C2ea9|>dS0<(Na0H)*Er z5@cBuQ+1W!U{-Nbzl!absbkj6X;qJm@0?3Y>E=?51s{%0l8M==-Qe?D;}(NJ2?a)2 zHdg8{No`i`(q4t%z2Qb&plZZq0WqgVKGY!vBgNuq>zPfd8^&}^4v+-Gh@M}}`aFI3 zRGH5%2OIzdG=lapi}KAAtRCc=a^IH6{2J8Ih#x{qq>cY|xpUjVJ!o!PxIwTulu?Ko zoMs?WOzw=nC%L0k3nHgGa=erb_026jxH=xE3}zv3w*SIotA>nZaQ`!1`046GkEsFi z?VmhH2#CyoC-pE;$N|96MV-j(dyypxI=()^jBI)w?_SqiGKQUHSyh zfP81%TW(Iq5b4Jr!FTy>)0DiFdm`Ry<|O};Xn-8$!oR}P%$oW;F4Ka*!h=1RS-jz0!f- zld}t^^KB644^`Xn{#lmzJ3VAqS8ZmQ}J*n?^R3=i&i)?N?AcOHAxv~r$G z1dK6P1S75ZZhgm!+nWCwjUQZ|4cm#rDWf+`h=ouRo{wM`HpOJ z!Elou&vV3f9lL+sY6*A*)KHm9$jVINu242xHnX$Ya`OhJ9z8rOpf?QD+Ow*Q4R$+Q#!bpv$7`f93vlcF$Ue( z#UN;tJgBtj%2ey|aahlFpw|ucJm+)=DaE}TS#Pxf(q?x*PnKY152@w+w1QwY5&^EM$;OEv$CwKATF4lPc}zn(rC9EI+6CVYbw zazQp3L~#sl5hn8+qxO?ao1kRxzoV^c5@Xq}mVf?OGBqM94;8~RtJh?-ftTG17~3uVWeAs3qb;zq)rF{OwI~xvAf*^ z<~m04Fwmw=)CyOw0zZvLVy`D#)yZLvJlXCbfs}MC2rB!qk66iL?2t}d6am!myI&(p z3vgzWi6O*0v1#Hc%_Lx}=h(Eca z-lduv2NZi_gDSfCGHEImy7WZ`x1gcx{m+$L!TTXo)ZKwQZl;T6>I`QG8o4 zKuxKO?1Ic{a(dRkNVD~fIj5B75Mri%@Z6Az#lrEhKr`iv6VWX9Am@Vtw+-r;GS-CR z+e~ncQ>98(af__`PHL1Yp>aeJz{j>|1R+dn6~$X4AmooK7#?{%LfnmvJlI2FNR*KL zFS?yx(E-95qd+q5L~P74GfdlAGt_RPK?sF+OUO}~H*?O2^Q`>< ztd1!DXGm8*hUf0@LkKtI04j-f%VN&jmzoSE;r8zWpIKUZ@83~rd)P@f)baqiTE`=* zVq_{U;NnzI4~P6|`DF^d5mkuAYTe=esge>$MmT^LH;cUswpR0+AFFlu0g(az$=#-< zvJ|o5yR4U=6~n>tX$$$d`Nn5a82nb%E|WI{NYm=C$Eq+<6CHVb_KMx9It_Fc8DL3A z^}}jh+-jE=$yH;B%_IRH%o6@w)HAS7e0TP*#VvG+;+`n#m|m7IW5jrEw1%D znj6^ND*IA)w{D((cu3Yltk_kr9~$&$H8T{~bR>ZOQ(dB-Q8Kqd3z;z|p$F~p!=X0h zYncpV!*{5M_4CsdES2NlafkclD7!>&F0)Vt(^? zc??}E&#B@;S-MS^$VLV4;wMJQJWy<)T^&BA41yVbyF&W*0>o z(Y|LqH?1iHdDrrBF+zg(BaQfFosw7D(ooasP|!803#bsC@Ze_{fVIP&=eq7VsX(9>F{i5EdQJ|(ExB=^ zj@sPNX|a5H=-9EWaf+8b!1H4yMB@%`amJCss`N?ecW;Fi^>Ev$Ra+?ivmCUGh;VSY zbr*i_KxzLqo+(-%=ssT0fv?0a+Fwn8K_4C+JGbz0dVXCPC`Q;(Q4_cc9g_d5+QQHzU#q%1vr1A{Nw?eLj)rW8ogd}8krs~S44^ahsoO6?UxWW02s z$_1uHlMKBlvrtUz7D_EiFeB#k8~>SwuGDr47z8ORLX}=h{8R) zLc*StHPYFdQK#QAjL$CJn;qU?NLSztc>#N?3a zg1t5ql-f$_Lw*bO?}d(^=YKYQ4oZOcUMfdaa#Of@qYFH=EB*0C5~p$B@v!)**EKFF zoM3|vY^zY^ zt{u(jl6M&XwqOpTNge%JLEwv{fn00Q+6nq=FnTlTJwxZ(R6jWni{uZu(qlin277+F zfrxs~s(=Nh1$_b;qt0KQ*ut$hFuM-TD_Rg%yaW->xdiIjZ8xYL7|*om>CFiXCOL(m zCUu?sq93N$$mp-VGwzojjkzw5A+eY}MN4Iy3q@@6?Lqi`Yiwvm4vZo2CGlx;NNK7! zF;`PGbMvwMOlJ%JNrzOa8Q$h(;zSQRWOMbcbrT7K{4v6DS%@f*x6u%*>3IMI~ltX0VvK z#LUbL7Bi2&KJ(A?>5e%U^UqXNRP5OMqV9IQYh~t>o{4+2IxL?&{42tFJ1~jy{ zIv5}fy>Qm{4kqtof5qJD9d?OALR#%Y6u*9D>D#wrgj}aj8;pzN34k}kdV3eExpK3( z@y8AL&Ck6T#O3naAeKmj)?sr9Xx!&j=nNsiX?=W=O^wY8s$81Z2E{j-<+KdX3#jwA z`pAtfPf(65TatCY=F)Gg13KlcR35s=64aE9W9bJ1nBzlaX!W=MvA@m4O6fZP+%9PU z+jimq)BUZYiJ_f~?Z3OOD%CyQ(FQR-B2BzZtQhC8e~5*mB8Ybi3qg=Ma2Qi4t(bU#p2e{n9v6wV{A{NY*P^`{q!GEIm}X6 zWgW_twoFP!$0emsn3wLBG$S4&$UIQc(aSFdOyo&mPFLHI*qs}j++2VB)%eoq@w9-E z4>^iaA%8`iz{J8Jpv|+t^sK+4lDSyZk|epBu|3h9OD3Qlhgi!WTVbMJsNWFAN@=#_ z3umoW4{4W{WlXt7XYGv-iuZ%+9(HA(y`WbZ zsS^?5qydmc%%>x~YWxh8W~aVIbN#X!l9OUgyVokxyD3KC>3ItPV&Iq34Tx&zcK_Lt z)b5veV5EOSZLlWDgNT$*L6T#nBr7lrc2(koPIo`y>uHa>vPvH?XS!bd>`oCpn@}K~pr7qoi zXc7(BRMi4C1JSLOuJE*Fo*VzVoB!Zp%KN#TUW7Mt*S|k5bH`IDvmTs&3fgXr!kjU1 z2g9YJ&kDCf|0`dEHfdBcW>ESTr!)COL#MB0{}~FD#D8xA^N1a-Q>~WtF$dSxtSW-< zs&pIEK$4N8w#US#FgyvU2&x;LO%YSBsjP=njgmCt~fO!RO_jlb^E~r0hU_om?D4d<5NDc9j)`1|h#E(xpe!oY66l0tXsf?T5UAFqtH*`mq~y3UM` zS_P3dA0?&*ExyrwSsqR6*LpOhr>ZCd(L=D>Bq_ya6t~CxA({s<1j!z40!W9KiT#r9 z=4|02R;3}&dAt}f4kSUQhLTsSn19Z|fG&0GxdKv_3X+*YUfIA`^m8BUwpVwDuR`)G z6uJ1bXlabmnFVl`P*VaJ3UDlG+nNYbL{JW&`+-m`*J~V%2`N4H@bGL=y4-$`*^)7z z4GuS)dgrhmcPtm1uNxZMQTwVh8?XpaaQ`63=cIN7kw~u@Koe5JMD_ON@VLqfVdAvO zX$Bw>N0Xa8t`yTH4lmA%=r|}HVyI8%q1mi6U-SMpbh$flQ!C|FtNF>)RwVUCAbT4T3V9cE zp9nb?5Xu3$5w65e<%+F6Ggny7r6_YE7icgZ6@G7~G@%qo;_%N%e~}X|E!kH=H7>X^ zfs6q8()Zj!y%FlxGI0S7bnX>(c=S?k$GRZdUfY{VM~^h5r(tb`8}MkB>xZ%DBPzbl zl%#k>zYfb7A`4da7Vi(fLQ!k-A&!MxvH@QiymWxdmcRNUI}J9Q+OFdSg!?5}teXFu zTCaK<1}0nC0SCGDePOUvFA1g7wkWaC_g;a*c-eR<_($nhL|AZ;rRt=NsCkG%*Z24O zFtJNYnV7g2zyb`;;Xd7$Oce*DY%=~*YyMM+n=_QffJsIcy3TZmBUvoVZStl&^MMw0 zZ$Bay-v$|Ta!x<|F!`Ha5i@dEW)|9~5sbeiP^{{VOuN_Bw%1Y4hB2!=#C41bz{(QS z+i4X6Q!Y2AO~Asi0p<)0{Sjd>mnDBZDvk?#I9Bi%#A9%^Vdd{&yq$Dw|AHJj03CufCwzx5QV z0gcWjB2^Z<#c2{x@JLZ;8xy+l8JrtILG{cod8#e-xL1jpF=>c3yhMyBQ9{;~NKJ8b zk@GozrX=6~jS1n2JhAFZ)V1mHD~;KJawRMjHG>U#VJNhaRH1OxgUOwGb6lAKz=sfY zc>KBZGzCwhcvg6(`vSt@mc7s`X!_+&r2nV1qAUw?)E82+%M4E>R)W?GmPT$kQZKKV zlCj&gVgQp{fTXJpQEx!a_)j~uUjJf91Gdj6J?MnOqMbInsFP}rIJ1csD zcyD+0QT8CFO`g<&U0F4eEg>Pd)EJcx>fG-v_WgB6*3cIea8!8r&V)w|8rMJ%uA{8C zP_YgaoW$?*q*$2#L)Zp4Lg0Bm@T;}+>s^Iwbf%&l#OJ_{!k>#CBty0T^5_?yU_&yG zKDBmL2G=*>u*bWXoo7&+e}I>Pb~&FBH2aHBMl~CB1ec(i-ES^74!|?w5U5L>M{7PS zP$w7+r??u4N*g>bEOG?@li7#sy7yWG2Rk(HZipmFXQ0|IZ@i!s6*-;;WZ;&CR3R&w)>NO1E$?8~EcXQ;XfR&@X z{V1>w%N@?JA%R-hAm4 zDe>B$zlDh@H2R#nj4QwaXys$&gO)vJheQhAYARX1os; z)|mc{l&W#}X(?E%0J@c`tWWO=tDrKOiHY->IV-kPAv<u*e*H3bc3^yS2Qtmv~zPd|2Ijh^8bX_^SLf-u%UeqBo*dX zB^w4G@mzF9i%0yn1SxC~%o{wiVTwenJhQ{EussTEmDTw{5mr)8ye z=Ck;Dw*xu)mz^nQQnj(4u`i!#C;O(?$HV%C7x(pF2xN$mx;j$jAN3kfB~_-iY@2+B zj-z4MZxU4&+D$E4W7MrSB28A3ab}2g{W3I=eAHx+#^SI{&dGXbD4frjDo!Mt%8X7x zz}YTf7Q3!4lRlj&VaGy6&1QZIj(ujW?u$8cg%K(hIZ@v?8sx%=lG!b2X|jG8q=-vu zWr3s`?0JN6n4Qq7$E^3W%(GB;WY9C`5C@GtZJ;J?tUQGg)d_$tJMhzsoqi&}(V#QM z)?&CXGhuIJ{2&qPt|&{18s@AMg@KA=T+wHNkdFD~6RhZdI41_|=Gxw)!6t|v6Ogtm zZO|DtQ|4KyQv;~vm7%OxSaQ2s8lEWw$~}IB%mNl}9Y)z;DcdE70UqnRcY8L$An$~9 zm|hEB%trT_DJ9kPKxP0vO;=NCN|{eiwNW-RJn@)3+4qNU>Hcqc88o>P1Jo6!&Gp=r z5)~q7Y?BQ%Y?IhUBXqzKKO3L7RqUOwu)KaI9{Mnpw#4R1Q^ffghm6h{@O%n;q*OxI zBhhWdws_`YS)upwFt^gy2g!XBR? z$s~72oUO$ztCYDeWDf3_6B1=tmc55?5^=V{zhOyPaB>+G}&Dx}^P3oH$;G}r;n;Vy3 zH_)glWV5GxpveC_34%c~F53+FMm^p30TBV)x+wAfU!#%gK3*aszqaoo+zpXI^!ZU+ z1IB-05+fx|jD_=?xRN1LwTk6rU0(k>BOSMAp^Irb*NOG@LG6k#=|&{WW907^99{R= zPgfk?(V7!8=!sfZ`JWuk77?XEY6=6c+(IZIC;2ksyd z1B?~R%|GtNuXJt4M7`z|n2Oq{Rb`V7qC}dZV93^}cX^s^S|`Z9P?bmo3erClSDAdm z43P}(xOVO^6l3HWo85ot0m_*}yVc5-X zb~10Nfu^g>>(FY}Y}}oqbXYJhI45`={N&hhGgVZ$&3wboZqaT!xV)FRO9f!4&ca6#faz8#GnISyhq5e8|uc|1E$|+J{K^Ny{K5mi$l0y|4HWvWp=)}&C}X#kNToQSV-j@?PA;T z))r5lvIl9DBIbpsBm5>wBe9JJ6lvL+&hL-&2-hBN z!{8}&C@&rS0lXn8v<0uDb$%ag{$A9!AG*jkxz|XOwjk#xZkveKDjS(0dY7byYGtg> zN^wW(3VrvJB1_WYM0Q|#PL#Ho?WXOKSAphA=rts5ig1=w!!LV^P)8Y$>$HRJ5I>KT z<_a>>S2SN|Ztdl$Bd_qT9q#cJIoC4zf)eviF6ZDEZQG#6V?kYoe^$8#i1m?E!N3jh z%TJF>><0c7zKh&k;0?N~aJh@b(*eronFr;Zne@Hp;%rTrDLh+yQQb}Tl5!%eJB?&Q zx<;ql&$6G8TeISR_fz+1OM|K&LOIeO?cnHYAiz>xoH7E(lG8uqsIu>r%`w6YO2*&@i`5hpdl2 zSI|*icwQ*nwU%%9{W%ed$E1};UT9Kk*g5(0bT0OF0o8?b!GMP=)C;5I&j2~;G*ZBZ zhx0bHv(jO9j;%$w(fNK=WBdVc2;VM)F_wYx$Y1~1Qmew^9~YjQ-;WD7$}+SzoGzrV)xjF1tvwAB-mG{Ie|SG z38Qe_R(D6a!Qt2R=dOtWG>%{efe6;q{&t|Ha29vbcI?VnLo~J3b*vhE-4+2FhVwSJ zscs;0M<9I2HNksW^G_6W*o5SjFwf4F3}=w%h8(;S`N*uVoJ48uN7=^l)D16eMv6>u z#!^bF4E85LF!)0C7kZG_7_Yxo!W`8@k@4$CHy?gK-Rsv6*SSVK6Q4jgp7($c#U*w_ zigz*P-#-?&Eb{!_fu^eYOt%QCFWwa=7TSNE{BsA`>2~F{h|22C-XpV~_LCMIyo1IV z{RX4)4*i57)`Q5VT4e*F3DL+({hu@nwQ|() z-=NfE=&mU};>w6D3$+lJE&e*YlB>QG{&NIApaeh!f2vgN|52s-Z$ZYD44hpY4gR+V zr_V#M8}9sPiigb0*rdIWR90HEjMSBQSU>ekQKTTwXD9%d-F2Ou9&vIep8GSzlL<&| zenDD4>YU27t!F7DLL89z8dQ}q6ZD>ym9_D}(zx%g=PXq*A>)g@QH$!** z-Pun0oR&ZZ$~3rhbZZL6k&ZYJ52*2Z6c@W!BbJ#~Xo3+z)acme_lv$yxVQwO0dQ|X zzTKQQ0N2S~HDVl7gw8Vev_yzU2xR`?{%~pk5Hub8zM<@vw%sXHt&PS75GA>{G@o(! zde4aPm){6)7?jrkG>;KHZ~c%`qSP>>RiRJoL%P?xk&hIay0|QvT&lg0ruKsx?Rd+vbURm^^8;+|eT`uL$>=3HKh|t^qf2kkwms(2Q)p zLrR^>QB8M+QFyot9eMZW(H}_R?2V#l7*d(JG13hx>@Be0kRa}8LHB@?#rhk4->=## z0#c)NY|=I3IZ;6)S#ya@4Z@7grJf(0s=r2MBALfo-0kEd@{W&N(?Q?_Zs0$}*ErN# z{mCr+ZXu4FM7gRTA(tlTSj)VISKKHS^){TtZX&;!urXdhC-YjH`I8YM9YPOqw2X&FVus-;e0$^036 zsAm+Kp8o?qJmLaM%cYz9Qs!zux0`xwOzo=6DV79#0;lRnlZ<@adkraMoG&$O0KINM zJ278Hp?uC~?qX>gg7wD1t$*EXoYX|$xy%aZLOpFdCImU0yIjjAV_PcWZGa-d{-7?{l!3YJj!*z zxER8j_L!gX7$0?Lp&ZZ^c%{}}wr$6y3)(y$*X{j|wlYf`tNLnUm9h)4Pyq4-jZB{= zYQGAZR0XV!r1jd!tlm49HfzS$4G7%Pcx#34(=77gYPGD4$~YzThTs(y(@Jr4kk6}Z zTRu_BX4mCI;I|cC!h_Wu#*=?z?hf{ntpw9msaZz-?3Gf*>30 z?ch8;*1;jN$#d&vfi00y99u8rl)92T@;1zsw>REUBt-_zRhFbNaRUM(L&t!AtPu#fGQCsmF#*&<% zoiE^icx$!xFc#UO3U)%ww;ISm%r>nP)n#&V&`EF~mIdAD=nYgy^ICA`71*TbelqICs8#m_Ov?LXp&u{?WVQM)jg+oT7suFL3W#C{C*rpQn}Wi zsu;KK7n0^D)@@U@fS)Y{cvP-BXI*}q&u#D0PF)}=_x|K}{+Uxr1)%Sio;w8HzxW&H zPfx9ipFy5X%>Q|%_rD6k(*KaK_V+0$+LZq)Zw+&O-ZGqgmS9S%W4v!!nv*otZ-P@VrzWp*6{O)UP zW_IxFX8mQoh56->uj>u$JM<<$E~s9w5l>?Y=`+DY;k}7E>D1d8ga%8|G@jYrG{ZI- zYO^@;brP8*5D6la9Ko4!jJ2_fHEhmo*l@V9SEx>wsDqgZnb8Sja@g--ECO|=C&g!u0$kH`NPq1A738_KtUW99XU#h6 z{RY};FSZ6s_0l*7(*g%i#@HpR(%MmCWQ`7^mhy=I))>?@kU93$`{5!W<1*hiQv`Gf z>C!w`egz>A;pD#A}uGF+$ z(2ZiD7(LB?aA@((pdVfsf{K>==3k-e{eDF#qxB!i_Kd^`%My7uQg~RfSCwqID7j>@ z8En%ZtDu7whtzk=Xb|KvD5l$AY(}C!3w{? zIFG1_NmYX&igoS)r=+D1iCNFNJ5R2)ujFx$3AtK9rJ_bfcaO?`D*A%rsp@^G0yo~B zIG;Ae*k^CzpDO7}wSp=Xxq5lSS1u41)P>1$;ud!LK|~G?*e>827UwUuj_&--^<);O z57~5y2s{Nq!l?qN z2hEvlosN~Hln9pPR=-i-x$^IkIyBY}xbaJuLc#l$`m zbF*vtxC-CfQ+Aud)TrYKNx1Y}g{zMpj$_mZ0~X?NX%lVIyP@R*GP{o&bh4uNZ=R*M z*+A=G&+96<1YFV$P6Ue*-m2IUCMCh zFWAo`=xtl^H+A+v3zRQ3ZLxU8ukhH+CFYCX@m28dS<*zKqTMmSz`WSGc6L`nwBN*M zl_s6e$&(B%u8eR&`cq}Uc|$sEDydiKIzK1hIRTC zF^aQe?sxOgzYqBlp;yb$dC7$@%c)~Chh=YO==DE+tItj>1!|70XZ z?HrB%J;d{AwDHCj!+001?MSL~z+03^!w!pEinL}`ow*g3!5zF7@t$|oS>qp<)EvdF zbnQPM3FREsiEL1JK$HP|i5>q$yFxBb?1!aXL7v zTx$~MS+qmVeju=6o)Hx8J8L$GK1nOuSR=Fs~DaPvvo9j$xZJ z#mmQ~=9ogpo3iC{(4o>BHJh5GBpF&v>>+%H9aTfW3z=+uSJjw>)rWDFG>@4M83k17 zrcw+SiM8Uht`>1vuGF*4lmFs5%gkK&$#eiNu)0&5nR&gE4-X5P$4$|aM9^RxVu4Eb zOeLfW(Xh{vTV(TTMAH;gMBr#!?TcRg`