().configureEach {
+ sourceCompatibility = "21"
+ targetCompatibility = "21"
+
+ options.encoding = "UTF-8"
+}
+
+kotlin {
+ compilerOptions {
+ apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2)
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
+}
diff --git a/nms/v1_21R7/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R7_PacketManager.kt b/nms/v1_21R7/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R7_PacketManager.kt
new file mode 100644
index 0000000..59ae9ce
--- /dev/null
+++ b/nms/v1_21R7/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/packet/versions/V1_21R7_PacketManager.kt
@@ -0,0 +1,33 @@
+package xyz.alexcrea.cuanvil.dependency.packet.versions
+
+import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket
+import net.minecraft.world.entity.player.Abilities
+import org.bukkit.craftbukkit.entity.CraftPlayer
+import org.bukkit.entity.Player
+import xyz.alexcrea.cuanvil.dependency.packet.PacketManager
+import xyz.alexcrea.cuanvil.dependency.packet.PacketManagerBase
+
+class V1_21R7_PacketManager : PacketManagerBase(), PacketManager {
+ override val canSetInstantBuild: Boolean
+ get() = true
+
+ override fun setInstantBuild(player: Player, instantBuild: Boolean) {
+ val nmsPlayer = (player as CraftPlayer).handle
+ val playerAbilities = nmsPlayer.abilities
+ val sendedAbilities: Abilities
+ if (playerAbilities.instabuild == instantBuild) {
+ sendedAbilities = playerAbilities
+ } else {
+ sendedAbilities = Abilities()
+ sendedAbilities.invulnerable = playerAbilities.invulnerable
+ sendedAbilities.flying = playerAbilities.flying
+ sendedAbilities.mayfly = playerAbilities.mayfly
+ sendedAbilities.instabuild = instantBuild
+ sendedAbilities.mayBuild = playerAbilities.mayBuild
+ sendedAbilities.flyingSpeed = playerAbilities.flyingSpeed
+ sendedAbilities.walkingSpeed = playerAbilities.walkingSpeed
+ }
+ val packet = ClientboundPlayerAbilitiesPacket(sendedAbilities)
+ nmsPlayer.connection.send(packet)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6ffcb1a..9de7d8c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,2 +1,22 @@
-rootProject.name = "UnsafeEnchants"
+rootProject.name = "CustomAnvil"
+// NMS subproject
+include("nms:nms-common")
+findProject(":nms:nms-common")?.name = "nms-common"
+include("nms:nms-paper")
+findProject(":nms:nms-paper")?.name = "nms-paper"
+
+
+val reobfNMS = providers.gradleProperty("subprojects.reobfnms")
+ .get().split(",")
+
+for (nmsPart in reobfNMS) {
+ include("nms:$nmsPart")
+ findProject(":nms:$nmsPart")?.name = nmsPart
+}
+
+// compatibility subprojects
+include(":impl:LegacyEcoEnchant")
+findProject(":impl:LegacyEcoEnchant")?.name = "LegacyEcoEnchant"
+include("impl:ExcellentEnchant5_4")
+findProject(":impl:ExcellentEnchant5_4")?.name = "ExcellentEnchant5_4"
\ No newline at end of file
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilder.java b/src/main/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilder.java
new file mode 100644
index 0000000..4292fa0
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/AnvilRecipeBuilder.java
@@ -0,0 +1,283 @@
+package xyz.alexcrea.cuanvil.api;
+
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe;
+
+/**
+ * A Builder for custom craft using anvil.
+ */
+@SuppressWarnings("unused")
+public class AnvilRecipeBuilder {
+
+ private @NotNull String name;
+ private boolean exactCount;
+
+ private int levelCostPerCraft;
+ private int linearXpCostPerCraft;
+
+ private boolean removeExactLinearXp;
+
+ private @Nullable ItemStack leftItem;
+ private @Nullable ItemStack rightItem;
+ private @Nullable ItemStack resultItem;
+
+ /**
+ * Instantiates a new Anvil recipe builder.
+ * exact count default to true.
+ * xp level and linear cost per craft default to 0.
+ *
+ * @param name The recipe name
+ */
+ public AnvilRecipeBuilder(@NotNull String name) {
+ this.name = name;
+
+ this.exactCount = true;
+ this.levelCostPerCraft = 0;
+ this.linearXpCostPerCraft = 0;
+ this.removeExactLinearXp = false;
+
+ this.leftItem = null;
+ this.rightItem = null;
+ this.resultItem = null;
+ }
+
+ /**
+ * Gets the recipe name.
+ *
+ * @return This recipe builder instance.
+ */
+ @NotNull
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the recipe name.
+ *
+ * @param name The recipe name
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Get if the recipe is exact count. (default 0)
+ *
+ * Exact count mean the recipe can only be crafted 1 by 1.
+ * If set to false, then it will craft as much as possible in 1 go and will keep unused material onto the anvil inventory.
+ *
+ * @return If the recipe is exact count.
+ */
+ public boolean isExactCount() {
+ return exactCount;
+ }
+
+ /**
+ * Sets if the recipe is exact count.
+ *
+ * Exact count mean the recipe can only be crafted 1 by 1.
+ * If set to false, then it will craft as much as possible in 1 go and will keep unused material onto the anvil inventory.
+ *
+ * @param exactCount If the recipe is exact count
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setExactCount(boolean exactCount) {
+ this.exactCount = exactCount;
+ return this;
+ }
+
+ /**
+ * Get the xp level cost per craft. (default 0)
+ *
+ * @return The xp level cost per craft
+ * @deprecated use {@link #getLevelCostPerCraft() getLevelCostPerCraft} instead
+ */
+ @Deprecated(since = "1.13.0")
+ public int getXpCostPerCraft() {
+ return getLevelCostPerCraft();
+ }
+
+ /**
+ * Sets the xp level cost per craft.
+ *
+ * @param xpCostPerCraft The xp level cost per craft
+ * @return This recipe builder instance.
+ * @deprecated use {@link #setLevelCostPerCraft(int) setLevelCostPerCraft} instead
+ */
+ @Deprecated(since = "1.13.0")
+ public AnvilRecipeBuilder setXpCostPerCraft(int xpCostPerCraft) {
+ return setLevelCostPerCraft(xpCostPerCraft);
+ }
+
+ /**
+ * Get the xp level cost per craft. (default 0)
+ *
+ * @return The xp level cost per craft
+ */
+ public int getLevelCostPerCraft() {
+ return levelCostPerCraft;
+ }
+
+ /**
+ * Sets the xp level cost per craft.
+ *
+ * @param levelCostPerCraft The xp level cost per craft
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setLevelCostPerCraft(int levelCostPerCraft) {
+ this.levelCostPerCraft = levelCostPerCraft;
+ return this;
+ }
+
+ /**
+ * Get the linear xp cost (not xp level cost) per craft.
+ *
+ * @return The xp level cost per craft
+ */
+ public int getLinearXpCostPerCraft() {
+ return linearXpCostPerCraft;
+ }
+
+ /**
+ * Sets the linear xp cost (not xp level cost) per craft.
+ *
+ * @param linearXpCostPerCraft The linear xp cost per craft
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setLinearXpCostPerCraft(int linearXpCostPerCraft) {
+ this.linearXpCostPerCraft = linearXpCostPerCraft;
+ return this;
+ }
+
+ /**
+ * Get if the linear xp should get removed by an exact amount.
+ *
+ * If false (default) level cost will be the level that would be reached by a player with this amount of xp.
+ * If true will require the level that has at least the specified level of xp then on click remove only the necessary xp
+ *
+ * linear xp cost are applied after level cost
+ * @return if we should remove the exact amount of linear xp
+ */
+ public boolean isRemoveExactLinearXp() {
+ return removeExactLinearXp;
+ }
+
+ /**
+ * Set if the linear xp should get removed by an exact amount.
+ *
+ * If false (default) level cost will be the level that would be reached by a player with this amount of xp.
+ * If true will require the level that has at least the specified level of xp then on click remove only the necessary xp
+ *
+ * linear xp cost are applied after level cost
+ * @param removeExactLinearXp if we should remove the exact amount of linear xp
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setRemoveExactLinearXp(boolean removeExactLinearXp) {
+ this.removeExactLinearXp = removeExactLinearXp;
+ return this;
+ }
+
+ /**
+ * Get the left item of the recipe.
+ * If null (default) then the recipe will not be able to be registered.
+ *
+ * @return The left item
+ */
+ @Nullable
+ public ItemStack getLeftItem() {
+ return leftItem;
+ }
+
+ /**
+ * Set the left item.
+ * If null (default) then the recipe will not be able to be registered.
+ *
+ * @param leftItem the left item
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setLeftItem(ItemStack leftItem) {
+ this.leftItem = leftItem;
+ return this;
+ }
+
+ /**
+ * Get the recipe right item.
+ * null on default new instance.
+ *
+ * @return The right item
+ */
+ @Nullable
+ public ItemStack getRightItem() {
+ return rightItem;
+ }
+
+ /**
+ * Set the recipe right item.
+ * null on default new instance.
+ *
+ * @param rightItem the right item
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setRightItem(ItemStack rightItem) {
+ this.rightItem = rightItem;
+ return this;
+ }
+
+ /**
+ * Get the recipe result item.
+ * If null (default) then the recipe will not be able to be registered.
+ *
+ * @return The result item
+ */
+ @Nullable
+ public ItemStack getResultItem() {
+ return resultItem;
+ }
+
+ /**
+ * Set the recipe result item.
+ * If null (default) then the recipe will not be able to be registered.
+ *
+ * @param resultItem The result item
+ * @return This recipe builder instance.
+ */
+ public AnvilRecipeBuilder setResultItem(ItemStack resultItem) {
+ this.resultItem = resultItem;
+ return this;
+ }
+
+ /**
+ * Build the anvil custom recipe.
+ * Should probably use {@link #registerIfAbsent() registerIfAbsent} or {@link ConflictAPI#addConflict(ConflictBuilder) addConflict}.
+ *
+ * @return A new anvil custom recipe base on this builder.
+ */
+ @Nullable // null if missing argument
+ public AnvilCustomRecipe build() {
+ if (leftItem == null || resultItem == null) return null;
+
+ return new AnvilCustomRecipe(
+ this.name,
+ this.exactCount,
+ this.levelCostPerCraft,
+ this.linearXpCostPerCraft,
+ this.removeExactLinearXp,
+ this.leftItem, this.rightItem, this.resultItem
+ );
+ }
+
+ /**
+ * Register this recipe if absent.
+ * Equivalent to {@link ConflictAPI#addConflict(ConflictBuilder)}
+ *
+ * @return True if successful.
+ */
+ public boolean registerIfAbsent() {
+ return CustomAnvilRecipeApi.addRecipe(this);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java
new file mode 100644
index 0000000..fe2715e
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictAPI.java
@@ -0,0 +1,196 @@
+package xyz.alexcrea.cuanvil.api;
+
+import io.delilaheve.CustomAnvil;
+import org.bukkit.NamespacedKey;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.group.EnchantConflictGroup;
+import xyz.alexcrea.cuanvil.gui.config.global.EnchantConflictGui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Custom Anvil api for conflict registry.
+ */
+@SuppressWarnings("unused")
+public class ConflictAPI {
+
+ private ConflictAPI() {
+ }
+
+ private static Object saveChangeTask = null;
+ private static Object reloadChangeTask = null;
+
+ /**
+ * Write and add a conflict.
+ * Will not write the conflict if it already exists.
+ * Will not be successful if the conflict is empty.
+ *
+ * @param builder The conflict builder to be based on
+ * @return True if successful.
+ */
+ public static boolean addConflict(@NotNull ConflictBuilder builder) {
+ return addConflict(builder, false);
+ }
+
+ /**
+ * Write and add a conflict.
+ * Will not write the conflict if it already exists.
+ * Will not be successful if the conflict is empty.
+ *
+ * @param builder The conflict builder to be based on
+ * @param overrideDeleted If we should write even if the conflict was previously deleted.
+ * @return True if successful.
+ */
+ public static boolean addConflict(@NotNull ConflictBuilder builder, boolean overrideDeleted) {
+ FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig();
+
+ // Test if conflict can be added
+ if (!overrideDeleted && ConfigHolder.CONFLICT_HOLDER.isDeleted(builder.getName())) return false;
+ if (config.contains(builder.getName())) return false;
+
+ if (!writeConflict(builder, false)) return false;
+
+ EnchantConflictGroup conflict = builder.build();
+ // Register conflict
+ ConfigHolder.CONFLICT_HOLDER.getConflictManager().addConflict(conflict);
+
+ // Add conflict to gui
+ EnchantConflictGui conflictGui = EnchantConflictGui.getCurrentInstance();
+ if (conflictGui != null) conflictGui.updateValueForGeneric(conflict, true);
+
+ return true;
+ }
+
+ /**
+ * Write a conflict to the config file and plan an update of conflicts.
+ *
+ * You may want to use {@link #addConflict(ConflictBuilder)} instead as it is more performance in most case as this function will reload every conflict.
+ *
+ * @param builder the builder
+ * @return true if was written successfully.
+ */
+ public static boolean writeConflict(@NotNull ConflictBuilder builder) {
+ return writeConflict(builder, true);
+ }
+
+ /**
+ * Write a conflict to the config file.
+ *
+ * You should use {@link #addConflict(ConflictBuilder)} or {@link #writeConflict(ConflictBuilder)} instead
+ *
+ * @param builder The builder
+ * @param updatePlanned If we should plan a global update for conflicts
+ * @return true if was written successfully.
+ */
+ public static boolean writeConflict(@NotNull ConflictBuilder builder, boolean updatePlanned) {
+ FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig();
+
+ String name = builder.getName();
+ if (name.contains(".")) {
+ CustomAnvil.instance.getLogger().warning("Conflict " + name + " contain \".\" in its name but should not. this conflict is ignored.");
+ logConflictOrigin(builder);
+ return false;
+ }
+
+ String basePath = name + ".";
+
+ List enchantments = extractEnchantments(builder);
+ List excludedGroups = new ArrayList<>(builder.getExcludedGroupNames());
+ if (!enchantments.isEmpty()) config.set(basePath + "enchantments", enchantments);
+ if (!excludedGroups.isEmpty()) config.set(basePath + "notAffectedGroups", excludedGroups);
+ if (builder.getMaxBeforeConflict() > 0)
+ config.set(basePath + "maxEnchantmentBeforeConflict", builder.getMaxBeforeConflict());
+
+ if (!config.isConfigurationSection(name)) return false;
+
+ prepareSaveTask();
+ if (updatePlanned) prepareUpdateTask();
+
+ return true;
+ }
+
+ /**
+ * Extract every enchantment names from a builder.
+ *
+ * @param builder The builder storing the enchantments
+ * @return Builder's stored enchantment.
+ */
+ @NotNull
+ private static List extractEnchantments(@NotNull ConflictBuilder builder) {
+ List result = new ArrayList<>(builder.getEnchantmentNames());
+ for (NamespacedKey enchantmentKey : builder.getEnchantmentKeys()) {
+ result.add(enchantmentKey.toString());
+ }
+
+ return result;
+ }
+
+ /**
+ * Remove a conflict.
+ *
+ * @param conflict The conflict to remove
+ * @return True if successful.
+ */
+ public static boolean removeConflict(@NotNull EnchantConflictGroup conflict) {
+ // Remove from registry
+ ConfigHolder.CONFLICT_HOLDER.getConflictManager().removeConflict(conflict);
+
+ // Delete and save to file
+ ConfigHolder.CONFLICT_HOLDER.delete(conflict.getName());
+ prepareSaveTask();
+
+ // Remove from gui
+ EnchantConflictGui conflictGui = EnchantConflictGui.getCurrentInstance();
+ if (conflictGui != null) conflictGui.removeGeneric(conflict);
+
+ return true;
+ }
+
+ /**
+ * Prepare a task to save conflict configuration.
+ */
+ private static void prepareSaveTask() {
+ if (saveChangeTask != null) return;
+
+ saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> {
+ ConfigHolder.CONFLICT_HOLDER.saveToDisk(true);
+ saveChangeTask = null;
+ });
+ }
+
+ /**
+ * Prepare a task to reload every conflict.
+ */
+ private static void prepareUpdateTask() {
+ if (reloadChangeTask != null) return;
+
+ reloadChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> {
+ ConfigHolder.CONFLICT_HOLDER.reload();
+ EnchantConflictGui conflictGui = EnchantConflictGui.getCurrentInstance();
+ if (conflictGui != null) conflictGui.reloadValues();
+
+ reloadChangeTask = null;
+ });
+ }
+
+ static void logConflictOrigin(@NotNull ConflictBuilder builder) {
+ CustomAnvil.instance.getLogger().warning("Conflict " + builder.getName() + " came from " + builder.getSourceName() + ".");
+ }
+
+ /**
+ * Get every registered conflict.
+ *
+ * @return An immutable collection of conflict.
+ */
+ @NotNull
+ public static List getRegisteredConflict() {
+ List mutableList = ConfigHolder.CONFLICT_HOLDER.getConflictManager().getConflictList();
+ return Collections.unmodifiableList(mutableList);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java
new file mode 100644
index 0000000..1460766
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/ConflictBuilder.java
@@ -0,0 +1,459 @@
+package xyz.alexcrea.cuanvil.api;
+
+import io.delilaheve.CustomAnvil;
+import org.bukkit.NamespacedKey;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.group.*;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+//TODO add conflict after level
+/**
+ * A Builder for material conflict.
+ */
+@SuppressWarnings("unused")
+public class ConflictBuilder {
+
+ private final @Nullable Plugin source;
+ private @NotNull String name;
+
+ private final @NotNull Set enchantmentNames;
+ private final @NotNull Set enchantmentKeys;
+
+ private final @NotNull Set excludedGroupNames;
+
+ private int maxBeforeConflict;
+
+ /**
+ * Instantiates a new Conflict builder.
+ *
+ * @param name The conflict name
+ * @param maxBeforeConflict Maximum number of conflicting enchantment before conflict is active
+ * @param source The conflict source
+ */
+ public ConflictBuilder(@NotNull String name, int maxBeforeConflict, @Nullable Plugin source) {
+ this.source = source;
+ this.name = name;
+
+ this.enchantmentNames = new HashSet<>();
+ this.enchantmentKeys = new HashSet<>();
+
+ this.excludedGroupNames = new HashSet<>();
+
+ this.maxBeforeConflict = maxBeforeConflict;
+ }
+
+ /**
+ * Instantiates a new Conflict builder.
+ *
+ * @param name The conflict name
+ * @param source The conflict source
+ */
+ public ConflictBuilder(@NotNull String name, @Nullable Plugin source) {
+ this(name, 0, source);
+ }
+
+ /**
+ * Instantiates a new Conflict builder.
+ *
+ * @param name The conflict name
+ */
+ public ConflictBuilder(@NotNull String name) {
+ this(name, null);
+ }
+
+ /**
+ * Gets conflict source.
+ *
+ * @return The conflict source.
+ */
+ @Nullable
+ public Plugin getSource() {
+ return source;
+ }
+
+ /**
+ * Gets conflict source name.
+ *
+ * @return The conflict source name.
+ */
+ @NotNull
+ public String getSourceName() {
+ if (source == null) return "an unknown source";
+
+ return source.getName();
+ }
+
+ /**
+ * Gets conflict name.
+ *
+ * @return The conflict name.
+ */
+ @NotNull
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets stored conflicting enchantment names.
+ *
+ * @return The stored enchantment names.
+ */
+ @NotNull
+ public Set getEnchantmentNames() {
+ return enchantmentNames;
+ }
+
+ /**
+ * Gets stored conflicting enchantment keys.
+ *
+ * @return The stored enchantment keys.
+ */
+ @NotNull
+ public Set getEnchantmentKeys() {
+ return enchantmentKeys;
+ }
+
+ /**
+ * Gets stored excluded group names.
+ *
+ * @return The stored group names.
+ */
+ @NotNull
+ public Set getExcludedGroupNames() {
+ return excludedGroupNames;
+ }
+
+ /**
+ * Gets maximum number of conflicting enchantment before conflict is active.
+ *
+ * This value represent how many enchantment contained on this conflict can be applied to before conflict is considered active.
+ * That mean new enchantment will not be able to be added to the item and present enchantment will not have its level upgraded.
+ *
+ * In vanilla. material restriction have this value set to 0 and enchantment conflict set to 1.
+ *
+ * @return the max number of conflicting enchantment before conflict. 0 by default.
+ */
+ public int getMaxBeforeConflict() {
+ return maxBeforeConflict;
+ }
+
+ /**
+ * Sets conflict name.
+ *
+ * @param name The name
+ * @return This conflict builder instance.
+ */
+ public ConflictBuilder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Sets maximum number of conflicting enchantment before conflict is active.
+ *
+ * This value represent how many enchantment contained on this conflict can be applied to before conflict is considered active.
+ * That mean new enchantment will not be able to be added to the item and present enchantment will not have its level upgraded.
+ *
+ * In vanilla. material restriction have this value set to 0 and enchantment conflict set to 1.
+ *
+ * @param maxBeforeConflict The max before conflict
+ * @return This conflict builder instance.
+ */
+ public ConflictBuilder setMaxBeforeConflict(int maxBeforeConflict) {
+ this.maxBeforeConflict = maxBeforeConflict;
+ return this;
+ }
+
+ /**
+ * Add a conflicting enchantment by name.
+ *
+ * @param enchantmentName The enchantment name
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder addEnchantment(@NotNull String enchantmentName) {
+ enchantmentNames.add(enchantmentName);
+ return this;
+ }
+
+ /**
+ * Add a conflicting enchantment by key.
+ *
+ * @param enchantmentKey The enchantment key
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder addEnchantment(@NotNull NamespacedKey enchantmentKey) {
+ enchantmentKeys.add(enchantmentKey);
+ return this;
+ }
+
+ /**
+ * Add a conflicting enchantment by instance.
+ *
+ * @param enchantment The enchantment
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder addEnchantment(@NotNull CAEnchantment enchantment) {
+ addEnchantment(enchantment.getKey());
+ return this;
+ }
+
+ /**
+ * Remove conflicting enchantment by name.
+ *
+ * @param enchantmentName The enchantment name
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder removeEnchantment(@NotNull String enchantmentName) {
+ enchantmentNames.remove(enchantmentName);
+ return this;
+ }
+
+ /**
+ * Remove conflicting enchantment by key.
+ *
+ * @param enchantmentKey The enchantment key
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder removeEnchantment(@NotNull NamespacedKey enchantmentKey) {
+ enchantmentKeys.remove(enchantmentKey);
+ return removeEnchantment(enchantmentKey.getKey());
+ }
+
+ /**
+ * Remove enchantment by instance.
+ *
+ * @param enchantment The enchantment
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder removeEnchantment(@NotNull CAEnchantment enchantment) {
+ return removeEnchantment(enchantment.getKey());
+ }
+
+ /**
+ * Add an excluded group by name.
+ *
+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict.
+ *
+ * This allows to create conflict only for some item. Material restriction can be written like that.
+ *
+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment
+ * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0.
+ * Then only pickaxe will be able to have efficiency.
+ *
+ * @param groupName The group name
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder addExcludedGroup(@NotNull String groupName) {
+ excludedGroupNames.add(groupName);
+ return this;
+ }
+
+ /**
+ * Add an excluded group by instance.
+ *
+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict.
+ *
+ * This allows to create conflict only for some item. Material restriction can be written like that.
+ *
+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment
+ * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0.
+ * Then only pickaxe will be able to have efficiency.
+ *
+ * @param group The group
+ * @return this conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder addExcludedGroup(@NotNull AbstractMaterialGroup group) {
+ return addExcludedGroup(group.getName());
+ }
+
+ /**
+ * Remove an excluded group by name.
+ *
+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict.
+ *
+ * This allows to create conflict only for some item. Material restriction can be written like that.
+ *
+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment
+ * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0.
+ * Then only pickaxe will be able to have efficiency.
+ *
+ * @param groupName The group name
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder removeExcludedGroup(@NotNull String groupName) {
+ excludedGroupNames.remove(groupName);
+ return this;
+ }
+
+ /**
+ * Remove an excluded group by instance.
+ *
+ * If left item of an anvil craft is included on one of the excluded group it will ignore this conflict.
+ *
+ * This allows to create conflict only for some item. Material restriction can be written like that.
+ *
+ * For example: If we exclude a material group containing every pickaxe and add efficiency enchantment
+ * with {@link #setMaxBeforeConflict(int) maxBeforeConflict} set to 0.
+ * Then only pickaxe will be able to have efficiency.
+ *
+ * @param group The group
+ * @return This conflict builder instance.
+ */
+ @NotNull
+ public ConflictBuilder removeExcludedGroup(@NotNull AbstractMaterialGroup group) {
+ return removeExcludedGroup(group.getName());
+ }
+
+ /**
+ * Copy this conflict builder.
+ *
+ * @return A copy of this conflict builder.
+ */
+ @NotNull
+ public ConflictBuilder copy() {
+ ConflictBuilder copy = new ConflictBuilder(this.name, this.source);
+
+ copy.setMaxBeforeConflict(this.maxBeforeConflict);
+
+ // Set Enchantments
+ for (NamespacedKey key : this.enchantmentKeys) {
+ copy.addEnchantment(key);
+ }
+ for (String enchantName : this.enchantmentNames) {
+ copy.addEnchantment(enchantName);
+ }
+
+ // Set Groups
+ for (String groupName : this.excludedGroupNames) {
+ copy.addExcludedGroup(groupName);
+ }
+
+ return copy;
+ }
+
+ /**
+ * Build a new Enchant conflict group by this builder.
+ *
+ * @return An Enchant conflict group with this builder parameters.
+ */
+ public EnchantConflictGroup build() {
+ AbstractMaterialGroup materials = extractGroups();
+ EnchantConflictGroup conflict = new EnchantConflictGroup(getName(), materials, getMaxBeforeConflict());
+ appendEnchantments(conflict);
+
+ return conflict;
+ }
+
+ /**
+ * Register this conflict if not yet registered.
+ * Equivalent to {@link ConflictAPI#addConflict(ConflictBuilder, boolean) ConflictAPI.addConflict(this, true)}}
+ *
+ * @return True if successful.
+ */
+ public boolean registerIfAbsent() {
+ return ConflictAPI.addConflict(this, true);
+ }
+
+ /**
+ * Register this conflict if not yet registered or deleted.
+ * Equivalent to {@link ConflictAPI#addConflict(ConflictBuilder) ConflictAPI.addConflict(this)}
+ *
+ * @return True if successful.
+ */
+ public boolean registerIfNew() {
+ return ConflictAPI.addConflict(this);
+ }
+
+ /**
+ * Append builders stored enchantments into conflict.
+ *
+ * @param conflict The conflict target
+ */
+ protected void appendEnchantments(@NotNull EnchantConflictGroup conflict) {
+ for (String enchantmentName : getEnchantmentNames()) {
+ if (appendEnchantments(conflict, EnchantmentApi.getListByName(enchantmentName)) == 0) {
+ CustomAnvil.instance.getLogger().warning("Could not find enchantment " + enchantmentName + " for conflict " + getName());
+ ConflictAPI.logConflictOrigin(this);
+ }
+ }
+ for (NamespacedKey enchantmentKey : getEnchantmentKeys()) {
+ if (!appendEnchantment(conflict, EnchantmentApi.getByKey(enchantmentKey))) {
+ CustomAnvil.instance.getLogger().warning("Could not find enchantment " + enchantmentKey + " for conflict " + getName());
+ ConflictAPI.logConflictOrigin(this);
+ }
+ }
+ }
+
+ /**
+ * Append an enchantment.
+ *
+ * @param conflict The conflict target
+ * @param enchantment The enchantment
+ * @return True if successful.
+ */
+ protected static boolean appendEnchantment(@NotNull EnchantConflictGroup conflict, @Nullable CAEnchantment enchantment) {
+ if (enchantment == null)
+ return false;
+ conflict.addEnchantment(enchantment);
+ 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.
+ *
+ * @return The abstract material group from the builder.
+ */
+ protected AbstractMaterialGroup extractGroups() {
+ ItemGroupManager itemGroupManager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager();
+ IncludeGroup group = new IncludeGroup(EnchantConflictManager.DEFAULT_GROUP_NAME);
+
+ for (String groupName : getExcludedGroupNames()) {
+ AbstractMaterialGroup materialGroup = itemGroupManager.get(groupName);
+
+ if (materialGroup == null) {
+ CustomAnvil.instance.getLogger().warning("Material group " + groupName + " do not exist but is ask by conflict " + getName());
+ ConflictAPI.logConflictOrigin(this);
+ continue;
+ }
+
+ group.addToPolicy(materialGroup);
+ }
+
+ return group;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApi.java
new file mode 100644
index 0000000..8f80aa3
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/CustomAnvilRecipeApi.java
@@ -0,0 +1,126 @@
+package xyz.alexcrea.cuanvil.api;
+
+import io.delilaheve.CustomAnvil;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.gui.config.global.CustomRecipeConfigGui;
+import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Custom Anvil api for custom anvil recipes.
+ */
+@SuppressWarnings("unused")
+public class CustomAnvilRecipeApi {
+
+ private CustomAnvilRecipeApi(){}
+
+ private static Object saveChangeTask = null;
+
+ /**
+ * Write and add a custom anvil recipe.
+ * Will not write the recipe if it already exists.
+ *
+ * @param builder The recipe builder to be based on
+ * @return True if successful.
+ */
+ public static boolean addRecipe(@NotNull AnvilRecipeBuilder builder){
+ return addRecipe(builder, false);
+ }
+
+ /**
+ * Write and add a custom anvil recipe.
+ * Will not write the recipe if it already exists.
+ *
+ * @param builder The recipe builder to be based on
+ * @param overrideDeleted If we should write even if the recipe was previously deleted.
+ * @return True if successful.
+ */
+ public static boolean addRecipe(@NotNull AnvilRecipeBuilder builder, boolean overrideDeleted){
+ FileConfiguration config = ConfigHolder.CUSTOM_RECIPE_HOLDER.getConfig();
+ String name = builder.getName();
+
+ if(!overrideDeleted && ConfigHolder.CUSTOM_RECIPE_HOLDER.isDeleted(builder.getName())) return false;
+ if(config.contains(builder.getName())) return false;
+
+ if(builder.getName().contains(".")) {
+ CustomAnvil.instance.getLogger().warning("Custom anvil recipe " + name + " contain \".\" in its name but should not. this recipe is ignored.");
+ return false;
+ }
+
+ AnvilCustomRecipe recipe = builder.build();
+ if(recipe == null){
+ CustomAnvil.instance.getLogger().warning("Custom anvil recipe " + name + " could not be parsed.");
+ if(builder.getLeftItem() == null){
+ CustomAnvil.instance.getLogger().warning("It look like left item of the recipe is null.");
+ }
+ if(builder.getResultItem() == null){
+ CustomAnvil.instance.getLogger().warning("It look like result item of the recipe is null.");
+ }
+ return false;
+ }
+
+ // Add to registry
+ ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().cleanAddNew(recipe);
+
+ // Save to file
+ recipe.saveToFile(false, false);
+ prepareSaveTask();
+
+ // Add from gui
+ CustomRecipeConfigGui recipeConfigGui = CustomRecipeConfigGui.getCurrentInstance();
+ if(recipeConfigGui != null) recipeConfigGui.updateValueForGeneric(recipe, true);
+
+ return true;
+ }
+
+ // TODO remove by name and/or by builder (as name is keept) (and maybe create a get by name)
+ /**
+ * Remove a custom anvil recipe.
+ *
+ * @param recipe The recipe to remove
+ * @return True if successful.
+ */
+ public static boolean removeRecipe(@NotNull AnvilCustomRecipe recipe){
+ // Remove from registry
+ boolean result = ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().cleanRemove(recipe);
+ if(!result) return false;
+
+ // Delete and save to file
+ ConfigHolder.CUSTOM_RECIPE_HOLDER.delete(recipe.getName());
+ prepareSaveTask();
+
+ // Remove from gui
+ CustomRecipeConfigGui recipeConfigGui = CustomRecipeConfigGui.getCurrentInstance();
+ if(recipeConfigGui != null) recipeConfigGui.removeGeneric(recipe);
+
+ return true;
+ }
+
+ /**
+ * Prepare a task to save custom recipe configuration.
+ */
+ private static void prepareSaveTask() {
+ if(saveChangeTask != null) return;
+
+ saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, ()->{
+ ConfigHolder.CONFLICT_HOLDER.saveToDisk(true);
+ saveChangeTask = null;
+ });
+ }
+
+ /**
+ * Get every registered recipes.
+ * @return An immutable collection of recipes.
+ */
+ @NotNull
+ public static List getRegisteredRecipes(){
+ List mutableList = ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().getRecipeList();
+ return Collections.unmodifiableList(mutableList);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java
new file mode 100644
index 0000000..ac98225
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/EnchantmentApi.java
@@ -0,0 +1,235 @@
+package xyz.alexcrea.cuanvil.api;
+
+import io.delilaheve.CustomAnvil;
+import io.delilaheve.util.ConfigOptions;
+import org.bukkit.NamespacedKey;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.enchantments.Enchantment;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+import xyz.alexcrea.cuanvil.enchant.bulk.BulkCleanEnchantOperation;
+import xyz.alexcrea.cuanvil.enchant.bulk.BulkGetEnchantOperation;
+import xyz.alexcrea.cuanvil.enchant.wrapped.CABukkitEnchantment;
+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;
+
+/**
+ * Custom Anvil api for enchantment registry.
+ */
+@SuppressWarnings("unused")
+public class EnchantmentApi {
+
+ private static Object saveChangeTask = null;
+
+ private EnchantmentApi() {}
+
+ /**
+ * Register an enchantment.
+ *
+ * @param enchantment The enchantment to register
+ * @return True if successful.
+ */
+ public static boolean registerEnchantment(@NotNull CAEnchantment enchantment){
+ if(!CAEnchantmentRegistry.getInstance().register(enchantment)) return false;
+
+ // Add enchantment to gui.
+ if(EnchantCostConfigGui.getInstance() != null){
+ EnchantCostConfigGui.getInstance().updateValueForGeneric(enchantment, true);
+ }
+ if(EnchantLimitConfigGui.getInstance() != null){
+ EnchantLimitConfigGui.getInstance().updateValueForGeneric(enchantment, true);
+ }
+
+ // Write default if do not exist
+ writeDefaultConfig(enchantment, false);
+
+ return true;
+ }
+
+ /**
+ * Register an enchantment by minecraft registered enchantment instance.
+ *
+ * @param enchantment The enchantment to register
+ * @param defaultRarity The default rarity of the provided enchantment
+ * @return True if successful.
+ */
+ public static boolean registerEnchantment(@NotNull Enchantment enchantment, @Nullable EnchantmentRarity defaultRarity){
+ if(defaultRarity == null)
+ return registerEnchantment(new CABukkitEnchantment(enchantment));
+
+ return registerEnchantment(new CABukkitEnchantment(enchantment, defaultRarity));
+ }
+
+ /**
+ * Register an enchantment by minecraft registered enchantment instance.
+ *
+ * Please note that this function assume the provided enchantment is registered into minecraft registry.
+ *
+ * @param enchantment The enchantment to register
+ * @return True if successful.
+ */
+ public static boolean registerEnchantment(@NotNull Enchantment enchantment){
+ return registerEnchantment(new CABukkitEnchantment(enchantment));
+ }
+
+ /**
+ * Unregister an enchantment.
+ *
+ * @param enchantment The enchantment to unregister
+ * @return True if successful.
+ */
+ public static boolean unregisterEnchantment(@Nullable CAEnchantment enchantment){
+ // Remove from gui
+ if(EnchantCostConfigGui.getInstance() != null){
+ EnchantCostConfigGui.getInstance().removeGeneric(enchantment);
+ }
+ if(EnchantLimitConfigGui.getInstance() != null){
+ EnchantLimitConfigGui.getInstance().removeGeneric(enchantment);
+ }
+
+ return CAEnchantmentRegistry.getInstance().unregister(enchantment);
+ }
+
+ /**
+ * Unregister an enchantment by its key.
+ *
+ * @param key The enchantment key to unregister
+ * @return True if successful.
+ */
+ public static boolean unregisterEnchantment(@NotNull NamespacedKey key){
+ CAEnchantment enchantment = CAEnchantment.getByKey(key);
+ return unregisterEnchantment(enchantment);
+ }
+
+ /**
+ * Unregister an enchantment by his bukkit enchantment.
+ *
+ * @param enchantment The enchantment to unregister
+ * @return True if successful.
+ */
+ public static boolean unregisterEnchantment(@NotNull Enchantment enchantment){
+ return unregisterEnchantment(enchantment.getKey());
+ }
+
+ /**
+ * Get by key an enchantment.
+ *
+ * @param key The key used to fetch
+ * @return The custom anvil enchantment of this key. null if not found.
+ */
+ @Nullable
+ public static CAEnchantment getByKey(@NotNull NamespacedKey key){
+ return CAEnchantment.getByKey(key);
+ }
+
+ /**
+ * Get by name an enchantment.
+ *
+ * @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 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);
+ }
+
+ /**
+ * Get every registered custom anvil enchantments.
+ * @return An immutable map of enchantment key as map key and custom anvil enchantment as value.
+ */
+ @NotNull
+ public static Map getRegisteredEnchantments(){
+ return Collections.unmodifiableMap(CAEnchantmentRegistry.getInstance().registeredEnchantments());
+ }
+
+ /**
+ * Write the default level and rarity configuration of the enchantment.
+ * @param enchantment The enchantment to write default configuration
+ * @param override If it should override old configuration
+ * @return Return false if override is false and a configuration exist. true otherwise.
+ */
+ public static boolean writeDefaultConfig(CAEnchantment enchantment, boolean override){
+ FileConfiguration config = ConfigHolder.DEFAULT_CONFIG.getConfig();
+
+ if(tryWriteDefaultConfig(config, enchantment, override)){
+ prepareSaveTask();
+ }
+ return true;
+ }
+
+ private static boolean tryWriteDefaultConfig(FileConfiguration defaultConfig, CAEnchantment enchantment, boolean override) {
+ boolean hasChange = false;
+
+ String levelPath = ConfigOptions.ENCHANT_LIMIT_ROOT + "." + enchantment.getKey();
+ if(override || !defaultConfig.isSet(levelPath)){
+ defaultConfig.set(levelPath, enchantment.defaultMaxLevel());
+ hasChange = true;
+ }
+
+ String basePath = ConfigOptions.ENCHANT_VALUES_ROOT + "." + enchantment.getKey();
+ EnchantmentRarity rarity = enchantment.defaultRarity();
+
+ String itemPath = basePath + ".item";
+ String bookPath = basePath + ".book";
+ if(override || !defaultConfig.isSet(itemPath)){
+ defaultConfig.set(itemPath, rarity.getItemValue());
+ hasChange = true;
+ }
+ if(override || !defaultConfig.isSet(bookPath)){
+ defaultConfig.set(bookPath, rarity.getBookValue());
+ hasChange = true;
+ }
+
+ return hasChange;
+ }
+
+ /**
+ * Prepare a task to save custom recipe configuration.
+ */
+ private static void prepareSaveTask() {
+ if(saveChangeTask != null) return;
+
+ saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, ()->{
+ ConfigHolder.DEFAULT_CONFIG.saveToDisk(true);
+ saveChangeTask = null;
+ });
+ }
+
+ /**
+ * Add a bulk get operator.
+ * @param operation An optimised get enchantments operation
+ */
+ public static void addBulkGet(@NotNull BulkGetEnchantOperation operation){
+ CAEnchantmentRegistry.getInstance().getOptimisedGetOperators().add(operation);
+ }
+
+ /**
+ * Add a bulk clean operator.
+ * @param operation An optimised clean enchantments operation
+ */
+ public static void addBulkClean(@NotNull BulkCleanEnchantOperation operation){
+ CAEnchantmentRegistry.getInstance().getOptimisedCleanOperators().add(operation);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java
new file mode 100644
index 0000000..cd71c7a
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/MaterialGroupApi.java
@@ -0,0 +1,251 @@
+package xyz.alexcrea.cuanvil.api;
+
+import io.delilaheve.CustomAnvil;
+import io.delilaheve.util.ConfigOptions;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup;
+import xyz.alexcrea.cuanvil.group.ExcludeGroup;
+import xyz.alexcrea.cuanvil.group.IncludeGroup;
+import xyz.alexcrea.cuanvil.group.ItemGroupManager;
+import xyz.alexcrea.cuanvil.gui.config.global.GroupConfigGui;
+
+import java.util.*;
+
+/**
+ * Custom Anvil api for material group registry.
+ */
+@SuppressWarnings("unused")
+public class MaterialGroupApi {
+
+ private MaterialGroupApi() {
+ }
+
+ private static Object saveChangeTask = null;
+ private static Object reloadChangeTask = null;
+
+ /**
+ * Write and add a group.
+ * Will not write the group if it already exists.
+ * Will not be successful if the group is empty.
+ *
+ * @param group The group to add
+ * @return true if successful.
+ */
+ public static boolean addMaterialGroup(@NotNull AbstractMaterialGroup group) {
+ return addMaterialGroup(group, false);
+ }
+
+ /**
+ * Write and add a group.
+ * Will not write the group if it already exists.
+ * Will not be successful if the group is empty.
+ *
+ * @param group The group to add
+ * @param overrideDeleted If we should write even if the group was previously deleted.
+ * @return true if successful.
+ */
+ public static boolean addMaterialGroup(@NotNull AbstractMaterialGroup group, boolean overrideDeleted) {
+ ItemGroupManager itemGroupManager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager();
+
+ // Test if it exists/existed
+ if (!overrideDeleted && ConfigHolder.ITEM_GROUP_HOLDER.isDeleted(group.getName())) return false;
+ if (itemGroupManager.get(group.getName()) != null) return false;
+
+ // Add group
+ itemGroupManager.getGroupMap().put(group.getName(), group);
+
+ if (!writeMaterialGroup(group, false)) return false;
+
+ if (group instanceof IncludeGroup includeGroup) {
+ GroupConfigGui configGui = GroupConfigGui.getCurrentInstance();
+ if (configGui != null) configGui.updateValueForGeneric(includeGroup, true);
+ }
+
+ if (ConfigOptions.INSTANCE.getVerboseDebugLog()) {
+ CustomAnvil.instance.getLogger().info("Registered group " + group.getName());
+ }
+
+ return true;
+ }
+
+ /**
+ * Write a material group to the config file and plan an update of groups.
+ *
+ * You may want to use {@link #addMaterialGroup(AbstractMaterialGroup)} instead as it is more performance in most case as this function will reload every conflict.
+ *
+ * @param group the group to write
+ * @return true if was written successfully.
+ */
+ public static boolean writeMaterialGroup(@NotNull AbstractMaterialGroup group) {
+ return writeMaterialGroup(group, true);
+ }
+
+ /**
+ * Write a material group to the config file.
+ *
+ * You should use {@link #addMaterialGroup(AbstractMaterialGroup)} or {@link #writeMaterialGroup(AbstractMaterialGroup)} instead
+ *
+ * @param group the group to write
+ * @param updatePlanned if we should plan a global update for material groups
+ * @return true if was written successfully.
+ */
+ public static boolean writeMaterialGroup(@NotNull AbstractMaterialGroup group, boolean updatePlanned) {
+ String name = group.getName();
+ if (name.contains(".")) {
+ CustomAnvil.instance.getLogger().warning("Group " + name + " contain . in its name but should not. this material group is ignored.");
+ return false;
+ }
+
+ boolean changed;
+ if (group instanceof IncludeGroup includeGroup) {
+ changed = writeKnownGroup("include", includeGroup);
+ } else if (group instanceof ExcludeGroup excludeGroup) {
+ throw new UnsupportedOperationException("exclude group is temporarily disable for the time being. sorry");
+ // This code do not do what is intended ? idk why do it exist
+ //changed = writeKnownGroup("exclude", excludeGroup);
+ } else {
+ changed = writeUnknownGroup(group);
+ }
+ if (!changed) return false;
+
+ prepareSaveTask();
+ if (updatePlanned) prepareUpdateTask();
+
+ return true;
+ }
+
+ private static boolean writeKnownGroup(@NotNull String groupType, @NotNull AbstractMaterialGroup group) {
+ FileConfiguration config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig();
+
+ String basePath = group.getName() + ".";
+ Set materialSet = group.getNonGroupInheritedMaterials();
+ Set groupSet = group.getGroups();
+
+ boolean empty = true;
+ if (!materialSet.isEmpty()) {
+ config.set(basePath + ItemGroupManager.MATERIAL_LIST_PATH, materialSetToStringList(materialSet));
+ empty = false;
+ } else {
+ config.set(basePath + ItemGroupManager.MATERIAL_LIST_PATH, null);
+ }
+ if (!groupSet.isEmpty()) {
+ config.set(basePath + ItemGroupManager.GROUP_LIST_PATH, materialGroupSetToStringList(groupSet));
+ empty = false;
+ } else {
+ config.set(basePath + ItemGroupManager.GROUP_LIST_PATH, null);
+ }
+
+ if (empty) {
+ config.set(basePath + ItemGroupManager.GROUP_TYPE_PATH, null);
+ return false;
+ }
+
+ config.set(basePath + ItemGroupManager.GROUP_TYPE_PATH, groupType);
+ return true;
+ }
+
+ private static boolean writeUnknownGroup(@NotNull AbstractMaterialGroup group) {
+ FileConfiguration config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig();
+
+ String basePath = group.getName() + ".";
+ Set materials = group.getMaterials();
+
+ if (materials.isEmpty()) return false;
+
+ config.set(basePath + ItemGroupManager.GROUP_TYPE_PATH, "include");
+ config.set(basePath + ItemGroupManager.MATERIAL_LIST_PATH, materialSetToStringList(materials));
+
+ return true;
+ }
+
+ public static List materialSetToStringList(@NotNull Set materials) {
+ return materials.stream().map(NamespacedKey::toString).toList();
+ }
+
+ public static List materialGroupSetToStringList(@NotNull Set groups) {
+ return groups.stream().map(AbstractMaterialGroup::getName).toList();
+ }
+
+ /**
+ * Remove a material group.
+ * Caution ! It will not be removed from depending conflict or other material group at runtime.
+ * For that reason, it is not recommended to use this function.
+ *
+ * @param group The recipe to remove
+ * @return True if the group was present.
+ */
+ public static boolean removeGroup(@NotNull AbstractMaterialGroup group) {
+ // Remove from registry
+ AbstractMaterialGroup removed = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().groupMap.remove(group.getName());
+ if (removed == null) return false;
+
+ // Delete and save to file
+ ConfigHolder.ITEM_GROUP_HOLDER.delete(group.getName());
+ prepareSaveTask();
+
+ // Remove from gui
+ if (group instanceof IncludeGroup includeGroup) {
+ GroupConfigGui configGui = GroupConfigGui.getCurrentInstance();
+ if (configGui != null) configGui.removeGeneric(includeGroup);
+ }
+
+ return true;
+ }
+
+ /**
+ * Prepare a task to reload every conflict.
+ */
+ private static void prepareSaveTask() {
+ if (saveChangeTask != null) return;
+
+ saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> {
+ ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(true);
+ saveChangeTask = null;
+ });
+ }
+
+ /**
+ * Prepare a task to save configuration.
+ */
+ private static void prepareUpdateTask() {
+ if (reloadChangeTask != null) return;
+
+ reloadChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, () -> {
+ ConfigHolder.ITEM_GROUP_HOLDER.reload();
+
+ GroupConfigGui configGui = GroupConfigGui.getCurrentInstance();
+ if (configGui != null) configGui.reloadValues();
+
+ reloadChangeTask = null;
+ });
+
+ }
+
+ /**
+ * Get by name a group.
+ *
+ * @param groupName the group name used to fetch
+ * @return the abstract group of this name. null if not found.
+ */
+ @Nullable
+ public static AbstractMaterialGroup getGroup(@NotNull String groupName) {
+ return ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().get(groupName);
+ }
+
+ /**
+ * Get every registered material groups.
+ *
+ * @return An immutable map of group name as its key and group as mapped value.
+ */
+ @NotNull
+ public static Map getRegisteredGroups() {
+ return Collections.unmodifiableMap(ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap());
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/UnitRepairApi.java b/src/main/java/xyz/alexcrea/cuanvil/api/UnitRepairApi.java
new file mode 100644
index 0000000..bc50c16
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/UnitRepairApi.java
@@ -0,0 +1,218 @@
+package xyz.alexcrea.cuanvil.api;
+
+import io.delilaheve.CustomAnvil;
+import kotlin.Triple;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.gui.config.global.UnitRepairConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.UnitRepairElementListGui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Custom Anvil api for unit repair.
+ */
+@SuppressWarnings("unused")
+public class UnitRepairApi {
+
+ private UnitRepairApi(){}
+
+ private static Object saveChangeTask = null;
+
+ /**
+ * Write and add a custom anvil unit repair recipe.
+ * Will not write the recipe if it already exists or was deleted.
+ * Set the value to minecraft default value (0.25 = 25%)
+ *
+ * @param unit The unit material used to repair the bellow item.
+ * @param repairable The item to be repaired.
+ * @return true if successful.
+ */
+ public static boolean addUnitRepair(@NotNull Material unit, @NotNull Material repairable){
+ return addUnitRepair(unit, repairable, 0.25, false);
+ }
+
+ /**
+ * Write and add a custom anvil unit repair recipe.
+ * Will not write the recipe if it already exists or was deleted.
+ *
+ * @param unit The unit material used to repair the bellow item.
+ * @param repairable The item to be repaired.
+ * @param value The amount to be repaired by every unit. (1% = 0.01)
+ * @return true if successful.
+ */
+ public static boolean addUnitRepair(@NotNull Material unit, @NotNull Material repairable, double value){
+ return addUnitRepair(unit, repairable, value, false);
+ }
+
+ /**
+ * Write and add a custom anvil unit repair recipe.
+ * Will not write the recipe if it already exists.
+ *
+ * @param unit The unit material used to repair the bellow item.
+ * @param repairable The item to be repaired.
+ * @param value The amount to be repaired by every unit. (1% = 0.01)
+ * @param overrideDeleted If we should write even if the recipe was previously deleted.
+ * @return true if successful.
+ */
+ public static boolean addUnitRepair(@NotNull Material unit, @NotNull Material repairable, double value, boolean overrideDeleted){
+ FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig();
+ String path = unit.name().toLowerCase() + "." + repairable.name().toLowerCase();
+
+ if(!overrideDeleted && ConfigHolder.UNIT_REPAIR_HOLDER.isDeleted(path)) return false;
+ if(config.contains(path)) return false;
+
+ // Set unit repair
+ return setUnitRepair(unit, repairable, value);
+ }
+
+ /**
+ * Write and add a custom anvil unit repair recipe.
+ * Do not check if it previously existed or exist.
+ *
+ * @param unit The unit material used to repair the bellow item.
+ * @param repairable The item to be repaired.
+ * @param value The amount to be repaired by every unit. (1% = 0.01)
+ * @return true if successful.
+ */
+ public static boolean setUnitRepair(@NotNull Material unit, @NotNull Material repairable, double value){
+ FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig();
+
+ String repairableName = repairable.name().toLowerCase();
+ String path = unit.name().toLowerCase() + "." + repairableName;
+
+ // Add to config then prepare save
+ config.set(path, value);
+ prepareSaveTask();
+
+ // Add to gui
+ UnitRepairConfigGui repairConfigGui = UnitRepairConfigGui.getCurrentInstance();
+ if(repairConfigGui != null) {
+ UnitRepairElementListGui elementGui = repairConfigGui.getInstanceOrCreate(unit).getStored();
+
+ if(elementGui != null) elementGui.updateValueForGeneric(repairableName, true);
+ repairConfigGui.updateValueForGeneric(unit, true);
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove a custom anvil unit repair recipe.
+ *
+ * @param unit The unit material used to repair the bellow item.
+ * @param repairable The item used to be repaired.
+ * @return true if successful.
+ */
+ public static boolean removeUnitRepair(@NotNull Material unit, @NotNull Material repairable){
+ // Delete every possible variation and save to file
+ String unitName = unit.name();
+ String repairableName = repairable.name();
+
+ FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig();
+ config.set(unitName.toLowerCase() + "." + repairableName.toUpperCase(), null);
+ config.set(unitName.toUpperCase() + "." + repairableName.toLowerCase(), null);
+ config.set(unitName.toUpperCase() + "." + repairableName.toUpperCase(), null);
+ config.set(unitName.toLowerCase() + "." + repairableName.toLowerCase(), null);
+
+ // Test if it was the last value of this section
+ boolean lastValue = false;
+ if(config.isConfigurationSection(unitName.toLowerCase())) {
+ ConfigurationSection section = config.getConfigurationSection(unitName.toLowerCase());
+
+ if(section != null && section.getKeys(false).isEmpty()) {
+ lastValue = true;
+ config.set(unitName.toLowerCase(), null);
+ }
+
+ } else if (config.isConfigurationSection(unitName.toUpperCase())) {
+ ConfigurationSection section = config.getConfigurationSection(unitName.toUpperCase());
+ if(section != null && section.getKeys(false).isEmpty()) {
+ lastValue = true;
+ config.set(unitName.toUpperCase(), null);
+ }
+
+ } else lastValue = true;
+
+
+ // We only need to "delete" as the lower case to be counted as deleted
+ ConfigHolder.UNIT_REPAIR_HOLDER.delete(unitName.toLowerCase() + "." + repairableName.toLowerCase());
+ prepareSaveTask();
+
+ // Remove from gui
+ UnitRepairConfigGui repairConfigGui = UnitRepairConfigGui.getCurrentInstance();
+ if(repairConfigGui != null) {
+ UnitRepairElementListGui elementGui = repairConfigGui.getInstanceOrCreate(unit).getStored();
+
+ if(elementGui != null) elementGui.removeGeneric(repairableName);
+ if(lastValue){
+ repairConfigGui.removeGeneric(unit);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Prepare a task to save custom unit repair recipe configuration.
+ */
+ private static void prepareSaveTask() {
+ if(saveChangeTask != null) return;
+
+ saveChangeTask = DependencyManager.scheduler.scheduleGlobally(CustomAnvil.instance, ()->{
+ ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(true);
+ saveChangeTask = null;
+ });
+ }
+
+ /**
+ * Get every unit repair recipes.
+ * @return An immutable collection of unit repair recipes.
+ *
+ * Each element of the provided triple represent a part of the recipe
+ *
+ * - First object is the unit material used to repair the bellow item.
+ *
- Second object is the item to be repaired.
+ *
- Last object is the amount to be repaired by every unit. (1% = 0.01)
+ *
+ */
+ @NotNull
+ public static List> getUnitRepairs(){
+ List> mutableList = new ArrayList<>();
+
+ FileConfiguration config = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig();
+ for (String unitKey : config.getKeys(false)) {
+ // Test if config section exist
+ if(!config.isConfigurationSection(unitKey)) continue;
+
+ // Test if unit is a material
+ Material unit = Material.getMaterial(unitKey.toUpperCase());
+ if(unit == null) continue;
+
+ // Iterate over reparable items
+ ConfigurationSection section = config.getConfigurationSection(unitKey);
+ for (String repairableKey : section.getKeys(false)) {
+ // Test if value section exist
+ if(!section.isDouble(repairableKey)) continue;
+
+ // Test if repairable is valid a material
+ Material repairable = Material.getMaterial(repairableKey.toUpperCase());
+ if(repairable == null) continue;
+
+ // Add the values
+ mutableList.add(new Triple<>(unit, repairable, section.getDouble(repairableKey)));
+
+ }
+ }
+
+ return Collections.unmodifiableList(mutableList);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEvent.java
new file mode 100644
index 0000000..67d27a8
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAConfigReadyEvent.java
@@ -0,0 +1,36 @@
+package xyz.alexcrea.cuanvil.api.event;
+
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+/**
+ * Called when the configuration of CustomAnvil is ready.
+ * It is called either on the plugin startup or on the plugin config reload.
+ *
+ * If you want to listen to the first trigger of this event (first configuration load. aka plugin load)
+ * you will need to register the listener on your plugin onEnable or earlier
+ *
+ * This event indicate that can start to register your recipes, item groups and conflicts.
+ * The vanilla and custom enchantments should already have been provided to CustomAnvil.
+ * Configuration can be changed any time after this event is triggered but never before.
+ *
+ * use {@link xyz.alexcrea.cuanvil.api.ConflictAPI ConflictApi},
+ * {@link xyz.alexcrea.cuanvil.gui.config.global.CustomRecipeConfigGui CustomRecipeConfigGui},
+ * {@link xyz.alexcrea.cuanvil.api.MaterialGroupApi MaterialGroupApi}
+ * and {@link xyz.alexcrea.cuanvil.api.UnitRepairApi UnitRepairApi}
+ * to add/remove/edit configurations
+ */
+public class CAConfigReadyEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEvent.java
new file mode 100644
index 0000000..3ffe372
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/CAEnchantRegistryReadyEvent.java
@@ -0,0 +1,29 @@
+package xyz.alexcrea.cuanvil.api.event;
+
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+/**
+ * Called when custom anvil is ready to accept registration on custom enchantment.
+ *
+ * If you want to listen this event
+ * you will need to register the listener on your plugin onEnable or earlier
+ *
+ * Custom enchantments may be registered later but may cause issue if registered too later
+ * (after configuration loading phase. see {@link CAConfigReadyEvent})
+ *
+ * use {@link xyz.alexcrea.cuanvil.api.EnchantmentApi EnchantmentApi} to register and unregister your custom enchantments
+ */
+public class CAEnchantRegistryReadyEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java
new file mode 100644
index 0000000..fe5e199
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAClickResultBypassEvent.java
@@ -0,0 +1,63 @@
+package xyz.alexcrea.cuanvil.api.event.listener;
+
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called before custom anvil process the click on the result on the anvil inventory.
+ *
+ * This event is called after checking that the inventory is an anvil inventory and that the click is on the result slot
+ * but before checking if the player has the custom anvil affected permission.
+ *
+ * This event being cancelled will make CustomAnvil abort the click on result process.
+ *
+ * Most of the time you would likely need {@link CAPreAnvilBypassEvent} or {@link CAEarlyPreAnvilBypassEvent}
+ * for this event to be useful.
+ *
+ * There is also {@link CATreatAnvilResult2Event} that may be better for some use case.
+ */
+public class CAClickResultBypassEvent extends Event implements Cancellable {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ private boolean cancelled = false;
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @NotNull
+ private final InventoryClickEvent event;
+
+ /**
+ * Get the bukkit inventory click event causing to this event
+ *
+ * @return The click event causing to this event
+ */
+ @NotNull
+ public InventoryClickEvent getEvent() {
+ return event;
+ }
+
+ public CAClickResultBypassEvent(@NotNull InventoryClickEvent event) {
+ this.event = event;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java
new file mode 100644
index 0000000..e92b4cd
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAEarlyPreAnvilBypassEvent.java
@@ -0,0 +1,63 @@
+package xyz.alexcrea.cuanvil.api.event.listener;
+
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.inventory.PrepareAnvilEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called before custom anvil process the prepare anvil event.
+ *
+ * This event will always get called when CustomAnvil need to handle
+ *
+ * This event being cancelled will make CustomAnvil abort the anvil process.
+ *
+ * You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful.
+ *
+ * It is also recommended that you read about {@link CAPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event}
+ * as your use case may be more prone to use theses.
+ */
+public class CAEarlyPreAnvilBypassEvent extends Event implements Cancellable {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ private boolean cancelled = false;
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @NotNull
+ private final PrepareAnvilEvent event;
+
+ /**
+ * Get the bukkit pre anvil event causing this event
+ *
+ * @return The pre anvil event causing to this event
+ */
+ @NotNull
+ public PrepareAnvilEvent getEvent() {
+ return event;
+ }
+
+ public CAEarlyPreAnvilBypassEvent(@NotNull PrepareAnvilEvent event) {
+ this.event = event;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java
new file mode 100644
index 0000000..9103a4b
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CAPreAnvilBypassEvent.java
@@ -0,0 +1,66 @@
+package xyz.alexcrea.cuanvil.api.event.listener;
+
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.inventory.PrepareAnvilEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called before custom anvil process the prepare anvil event.
+ *
+ * This event is called after {@link CAEarlyPreAnvilBypassEvent},
+ * after checking that there is at least an item on the left slot
+ * and after checking if any of the 2 item is marked as immutable
+ * but before checking if the player has the custom anvil affected permission.
+ *
+ * This event being cancelled will make CustomAnvil abort the anvil process.
+ *
+ * You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful.
+ *
+ * It is also recommended that you read about {@link CAEarlyPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event}
+ * as your use case may be more prone to use theses.
+ */
+public class CAPreAnvilBypassEvent extends Event implements Cancellable {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ private boolean cancelled = false;
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @NotNull
+ private final PrepareAnvilEvent event;
+
+ /**
+ * Get the bukkit pre anvil event causing this event
+ *
+ * @return The pre anvil event causing this event
+ */
+ @NotNull
+ public PrepareAnvilEvent getEvent() {
+ return event;
+ }
+
+ public CAPreAnvilBypassEvent(@NotNull PrepareAnvilEvent event) {
+ this.event = event;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java
new file mode 100644
index 0000000..30c5380
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResult2Event.java
@@ -0,0 +1,196 @@
+package xyz.alexcrea.cuanvil.api.event.listener;
+
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.anvil.AnvilCost;
+import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
+
+/**
+ * Called after custom anvil processed the click on the result on the anvil inventory.
+ * This event should be used to modify the result of an anvil use.
+ *
+ * You may also want to check {@link CAClickResultBypassEvent},
+ * {@link CAPreAnvilBypassEvent}
+ * and {@link CAEarlyPreAnvilBypassEvent} for your use case
+ *
+ * A null result will cancel this event
+ */
+@SuppressWarnings("unused")
+public class CATreatAnvilResult2Event extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ @NotNull
+ private final InventoryView view;
+
+ private final AnvilUseType useType;
+
+ @Nullable
+ private final ItemStack left;
+ @Nullable
+ private final ItemStack right;
+
+ @Nullable
+ private ItemStack result;
+
+ private final AnvilCost cost;
+
+ @ApiStatus.Internal
+ public CATreatAnvilResult2Event(
+ @NotNull InventoryView view,
+ Inventory inv,
+ AnvilUseType useType,
+ @Nullable ItemStack result,
+ AnvilCost cost) {
+ this.view = view;
+ this.useType = useType;
+
+ this.left = inv.getItem(0); // TODO use view here
+ this.right = inv.getItem(1);
+ this.result = result;
+ this.cost = cost;
+ }
+
+ /**
+ * Get the bukkit inventory view.
+ *
+ * Temporarily marked as internal as it will get changed to anvil view on legacy removal
+ * so signature will change
+ *
+ * @return The inventory view of this event.
+ */
+ @ApiStatus.Internal
+ public @NotNull InventoryView getView() {
+ return view;
+ }
+
+
+ /**
+ * Get the type of use source of the result.
+ *
+ * @return The craft use type.
+ */
+ public AnvilUseType getUseType() {
+ return useType;
+ }
+
+ /**
+ * Get the left item of the anvil use
+ *
+ * @return the left item
+ */
+ public @Nullable ItemStack getLeftItem() {
+ return left;
+ }
+
+ /**
+ * Get the right item of the anvil use
+ *
+ * @return the right item
+ */
+ public @Nullable ItemStack getRightItem() {
+ return right;
+ }
+
+ /**
+ * Get the current result
+ *
+ * note that it will not be null unless another listener previously set it to null.
+ *
+ * @return The current result.
+ */
+ public @Nullable ItemStack getResult() {
+ return result;
+ }
+
+ /**
+ * Set the current result
+ *
+ * note that a null result will cancel this anvil use.
+ *
+ * @param result The new result
+ */
+ public void setResult(@Nullable ItemStack result) {
+ this.result = result;
+ }
+
+ /**
+ * Get the level cost displayed on the anvil.
+ *
Important note:
+ * the final price are re calculated on click for the following use case:
+ *
+ * - Custom craft
+ * - Unit repair
+ * - Lore edit
+ *
+ * This value will be used as final price for:
+ * Item merge
+ * Item rename
+ *
+ *
+ * @return The current cost.
+ * @deprecated use #{@link #getCost()} instead
+ */
+ @Deprecated(forRemoval = true, since = "1.17.0")
+ public int getLevelCost() {
+ return cost.asXpCost();
+ }
+
+ /**
+ * Set the level cost displayed on the anvil.
+ * Important note:
+ * the final price are re calculated on click for the following use case:
+ *
+ * - Custom craft
+ * - Unit repair
+ * - Lore edit
+ *
+ * This value will be used as final price for:
+ * Item merge
+ * Item rename
+ *
+ *
+ * @param levelCost The new cost.
+ * @deprecated use #{@link #getCost()} and set value on this instead
+ */
+ @Deprecated(forRemoval = true, since = "1.17.0")
+ public void setLevelCost(int levelCost) {
+ cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost());
+ }
+
+ /**
+ * Allow access to the current cost of the event
+ * Note that modifying this object will change the event resulting cost
+ *
+ * Important note:
+ * the final price are re calculated on click for the following use case:
+ *
+ * - Custom craft
+ * - Unit repair
+ * - Lore edit
+ *
+ * This value will be used as final price for:
+ * Item merge
+ * Item rename
+ *
+ * @return the current anvil cost
+ */
+ public AnvilCost getCost() {
+ return cost;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java
new file mode 100644
index 0000000..80965b5
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/api/event/listener/CATreatAnvilResultEvent.java
@@ -0,0 +1,162 @@
+package xyz.alexcrea.cuanvil.api.event.listener;
+
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.inventory.PrepareAnvilEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.anvil.AnvilCost;
+import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
+
+/**
+ * Called after custom anvil processed the click on the result on the anvil inventory.
+ * This event should be used to modify the result of an anvil use.
+ *
+ * You may also want to check {@link CAClickResultBypassEvent},
+ * {@link CAPreAnvilBypassEvent}
+ * and {@link CAEarlyPreAnvilBypassEvent} for your use case
+ *
+ * A null result will cancel this pre anvil event
+ *
+ * @deprecated Prepare anvil Event cannot be provided as it can be called on result and therefore not have prepared anvil event
+ * use {@link CATreatAnvilResult2Event} instead
+ */
+@SuppressWarnings("unused")
+@Deprecated(forRemoval = true, since = "1.17.0")
+public class CATreatAnvilResultEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ @NotNull
+ private final PrepareAnvilEvent event;
+
+ private final AnvilUseType useType;
+
+ @Nullable
+ private ItemStack result;
+
+ private final AnvilCost cost;
+
+ public CATreatAnvilResultEvent(@NotNull PrepareAnvilEvent event, AnvilUseType useType, @Nullable ItemStack result, AnvilCost cost) {
+ this.event = event;
+ this.useType = useType;
+ this.result = result;
+ this.cost = cost;
+ }
+
+ /**
+ * Get the bukkit inventory click event causing to this event.
+ *
+ * @return The click event causing to this event.
+ */
+ public @NotNull PrepareAnvilEvent getEvent() {
+ return event;
+ }
+
+ /**
+ * Get the type of use source of the result.
+ *
+ * @return The craft use type.
+ */
+ public AnvilUseType getUseType() {
+ return useType;
+ }
+
+ /**
+ * Get the current result
+ *
+ * note that it will not be null unless another listener previously set it to null.
+ *
+ * @return The current result.
+ */
+ public @Nullable ItemStack getResult() {
+ return result;
+ }
+
+ /**
+ * Set the current result
+ *
+ * note that a null result will cancel this anvil use.
+ *
+ * @param result The new result
+ */
+ public void setResult(@Nullable ItemStack result) {
+ this.result = result;
+ }
+
+ /**
+ * Get the level cost displayed on the anvil.
+ *
Important note:
+ * the final price are re calculated on click for the following use case:
+ *
+ * - Custom craft
+ * - Unit repair
+ * - Lore edit
+ *
+ * This value will be used as final price for:
+ * Item merge
+ * Item rename
+ *
+ *
+ * @return The current cost.
+ * @deprecated use #{@link #getCost()} instead
+ */
+ @Deprecated(forRemoval = true, since = "1.17.0")
+ public int getLevelCost() {
+ return cost.asXpCost();
+ }
+
+ /**
+ * Set the level cost displayed on the anvil.
+ * Important note:
+ * the final price are re calculated on click for the following use case:
+ *
+ * - Custom craft
+ * - Unit repair
+ * - Lore edit
+ *
+ * This value will be used as final price for:
+ * Item merge
+ * Item rename
+ *
+ *
+ * @param levelCost The new cost.
+ * @deprecated use #{@link #getCost()} and set value on this instead
+ */
+ @Deprecated(forRemoval = true, since = "1.17.0")
+ public void setLevelCost(int levelCost) {
+ cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost());
+ }
+
+ /**
+ * Allow access to the current cost of the event
+ * Note that modifying this object will change the event resulting cost
+ *
+ * Important note:
+ * the final price are re calculated on click for the following use case:
+ *
+ * - Custom craft
+ * - Unit repair
+ * - Lore edit
+ *
+ * This value will be used as final price for:
+ * Item merge
+ * Item rename
+ *
+ * @return the current anvil cost
+ */
+ public AnvilCost getCost() {
+ return cost;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java b/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java
new file mode 100644
index 0000000..f6a7e80
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/config/ConfigHolder.java
@@ -0,0 +1,416 @@
+package xyz.alexcrea.cuanvil.config;
+
+import com.google.common.io.Files;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.group.EnchantConflictManager;
+import xyz.alexcrea.cuanvil.group.ItemGroupManager;
+import xyz.alexcrea.cuanvil.recipe.CustomAnvilRecipeManager;
+import xyz.alexcrea.cuanvil.util.MetricsUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Level;
+
+@SuppressWarnings("unused")
+public abstract class ConfigHolder {
+
+ // Available configuration:
+ public static DefaultConfigHolder DEFAULT_CONFIG;
+ public static ItemGroupConfigHolder ITEM_GROUP_HOLDER;
+ public static ConflictConfigHolder CONFLICT_HOLDER;
+ public static UnitRepairHolder UNIT_REPAIR_HOLDER;
+ public static CustomAnvilCraftHolder CUSTOM_RECIPE_HOLDER;
+
+ /**
+ * Load default configuration.
+ * @return True if successful.
+ */
+ public static boolean loadDefaultConfig() {
+ DEFAULT_CONFIG = new DefaultConfigHolder();
+
+ return DEFAULT_CONFIG.reloadFromDisk(true);
+ }
+
+ /**
+ * Load non default configuration.
+ * @return True if successful.
+ */
+ public static boolean loadNonDefaultConfig() {
+ ITEM_GROUP_HOLDER = new ItemGroupConfigHolder();
+ CONFLICT_HOLDER = new ConflictConfigHolder();
+ UNIT_REPAIR_HOLDER = new UnitRepairHolder();
+ CUSTOM_RECIPE_HOLDER = new CustomAnvilCraftHolder();
+
+ return removeNonDefaultFromDisk(true);
+ }
+
+ public static boolean reloadAllFromDisk(boolean hardfail) {
+ boolean sucess = DEFAULT_CONFIG.reloadFromDisk(hardfail);
+ if (!sucess) return false;
+
+ return removeNonDefaultFromDisk(hardfail);
+ }
+
+ private static boolean removeNonDefaultFromDisk(boolean hardfail){
+ boolean sucess = ITEM_GROUP_HOLDER.reloadFromDisk(hardfail);
+ if (!sucess) return false;
+ sucess = CONFLICT_HOLDER.reloadFromDisk(hardfail);
+ if (!sucess) return false;
+ sucess = UNIT_REPAIR_HOLDER.reloadFromDisk(hardfail);
+ if (!sucess) return false;
+ sucess = CUSTOM_RECIPE_HOLDER.reloadFromDisk(hardfail);
+
+ return sucess;
+ }
+
+
+ // usefull part of the file
+ private static final File BACKUP_FOLDER = new File(CustomAnvil.instance.getDataFolder(), "backup");
+
+ protected FileConfiguration configuration;
+
+ protected ConfigHolder() {
+
+ }
+
+ public abstract boolean reloadFromDisk(boolean hardFail);
+
+ public abstract void reload();
+
+ public FileConfiguration getConfig() {
+ return configuration;
+ }
+
+ // Config name and files
+ protected abstract String getConfigFileName();
+
+ protected String getConfigFileExtension() {
+ return ".yml";
+ }
+
+ protected File getConfigFile() {
+ return new File(CustomAnvil.instance.getDataFolder(), getConfigFileName() + getConfigFileExtension());
+ }
+
+ protected File getFirstBackup() {
+ return new File(BACKUP_FOLDER, getConfigFileName() + "-first" + getConfigFileExtension());
+ }
+
+ protected File getLastBackup() {
+ return new File(BACKUP_FOLDER, getConfigFileName() + "-latest" + getConfigFileExtension());
+ }
+
+ // Save logic
+ public boolean saveToDisk(boolean doBackup) {
+ CustomAnvil.Companion.log("Saving "+getConfigFileName());
+ if (doBackup) {
+ if (!saveBackup()) {
+ CustomAnvil.instance.getLogger().severe("Could not save backup. see above.");
+ return false;
+ }
+ }
+ File base = getConfigFile();
+ // if file exist and can't be deleted the file, then we gave up.
+ if (base.exists() && !base.delete()) {
+ CustomAnvil.instance.getLogger().severe("Could not save config: can't delete existing file.");
+ return false;
+ }
+ FileConfiguration config = getConfig();
+ try {
+ config.save(base);
+ } catch (IOException e) {
+ e.printStackTrace();
+ CustomAnvil.instance.getLogger().severe("Could not save config...");
+ return false;
+ }
+
+ CustomAnvil.Companion.log(getConfigFileName()+" saved successfully");
+ return true;
+ }
+
+ protected boolean saveBackup() {
+ File base = getConfigFile();
+ if (!base.exists()) return true; // We did back up everything we had to (nothing in this case)
+ boolean sufficientSuccess = false;
+
+ BACKUP_FOLDER.mkdirs();
+ // save first backup if do not exist
+ File firstBackup = getFirstBackup();
+ if (!firstBackup.exists()) {
+ try {
+ Files.copy(base, firstBackup);
+ sufficientSuccess = true;
+ } catch (IOException e) {
+ CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not copy backup saving config " + base.getName(), e);
+ MetricsUtil.INSTANCE.trackError(e);
+ }
+ }
+ // save last backup
+ File lastBackup = getLastBackup();
+ // if file exist and can't be deleted the file, then we gave up.
+ if (lastBackup.exists() && !lastBackup.delete()) {
+ return sufficientSuccess;
+ }
+
+ try {
+ Files.move(base, lastBackup);
+ sufficientSuccess = true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return sufficientSuccess;
+ }
+
+ public static class DefaultConfigHolder extends ConfigHolder {
+
+ @Override
+ protected String getConfigFileName() {
+ return "config";
+ }
+
+ @Override
+ public boolean reloadFromDisk(boolean hardFail) {
+ CustomAnvil.instance.saveDefaultConfig();
+ CustomAnvil.instance.reloadConfig();
+ this.configuration = CustomAnvil.instance.getConfig();
+ return true;
+ }
+
+ @Override
+ public void reload() {
+ }// Nothing to do
+
+ }
+
+ // Abstract class for non default config
+ public abstract static class ResourceConfigHolder extends ConfigHolder {
+
+ String resourceName;
+
+ private ResourceConfigHolder(String resourceName) {
+ this.resourceName = resourceName;
+ }
+
+ @Override
+ protected String getConfigFileName() {
+ return resourceName;
+ }
+
+ @Override
+ public boolean reloadFromDisk(boolean hardFail) {
+ YamlConfiguration configuration = CustomAnvil.instance.reloadResource(
+ getConfigFileName() + getConfigFileExtension(), hardFail);
+ if (configuration == null) return false;
+
+ this.configuration = configuration;
+ reload();
+
+ return true;
+ }
+
+ }
+
+ public abstract static class DeletableResource extends ResourceConfigHolder{
+
+ private static final String DELETED_FOLDER_PATH = "deleted";
+
+ private final @NotNull File parent;
+ private final @NotNull File deletedConfigFile;
+
+ private @Nullable YamlConfiguration deletedListConfig;
+ private DeletableResource(String resourceName) {
+ super(resourceName);
+ this.parent = new File(CustomAnvil.instance.getDataFolder(), DELETED_FOLDER_PATH);
+ this.deletedConfigFile = new File(this.parent, "deleted_" + resourceName + getConfigFileExtension());
+ }
+
+ @Override
+ public boolean reloadFromDisk(boolean hardFail) {
+ if(!super.reloadFromDisk(hardFail)) return false;
+ loadDeletedListFile(hardFail);
+
+ return true;
+ }
+
+ private void loadDeletedListFile(boolean hardFail){
+ this.deletedListConfig = CustomAnvil.instance.reloadResource(this.deletedConfigFile, hardFail);
+
+ }
+
+ /**
+ * Test if the provided element was deleted.
+ * @param objectPath The object path to delete.
+ * @return True if successful.
+ */
+ public boolean isDeleted(String objectPath){
+ if(this.deletedListConfig == null) return false;
+
+ return this.deletedListConfig.getBoolean(objectPath, false);
+ }
+
+ /**
+ * Delete a certain object by its path. do not save the config.
+ * @param objectPath The object path to delete.
+ * @return True if successful.
+ */
+ public boolean delete(String objectPath){
+ return delete(objectPath, false, false);
+ }
+
+ /**
+ * Delete a certain object by its path.
+ * @param objectPath The object path to delete.
+ * @param doSave If we should save the config after deleting.
+ * @param doBackup If we should create a backup.
+ * @return True if successful.
+ */
+ public boolean delete(String objectPath, boolean doSave, boolean doBackup){
+ // Create deleted list if it does not yet exist
+ if(this.deletedListConfig == null){
+ this.parent.mkdirs();
+ try {
+ this.deletedConfigFile.createNewFile();
+ } catch (IOException e) {
+ CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not create " + this.deletedConfigFile.getPath(), e);
+ MetricsUtil.INSTANCE.trackError(e);
+ }
+ loadDeletedListFile(false);
+
+ // Something was wrong somehow
+ if(this.deletedListConfig == null) return false;
+ }
+
+ // Add to the deleted config
+ this.deletedListConfig.set(objectPath, true);
+ this.getConfig().set(objectPath, null);
+
+ // Save the deleted config (may not be the most efficient, but I will handle it later)
+ if(doSave){
+ return saveToDisk(doBackup);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean saveToDisk(boolean doBackup) {
+ boolean deletedSaveSuccess = saveDeletedList();
+
+ return super.saveToDisk(doBackup) && deletedSaveSuccess;
+ }
+
+ /**
+ * Save list of deleted elements.
+ * @return true if successful.
+ */
+ public boolean saveDeletedList() {
+ if(this.deletedListConfig == null) return true;
+
+ try {
+ this.deletedListConfig.save(this.deletedConfigFile);
+ } catch (IOException e) {
+ CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not save " + this.deletedConfigFile.getPath(), e);
+ MetricsUtil.INSTANCE.trackError(e);
+ return false;
+ }
+
+ return true;
+ }
+
+
+ }
+
+
+ // Class for itemGroupsManager config
+ public static class ItemGroupConfigHolder extends DeletableResource {
+ private static final String FILE_NAME = "item_groups";
+
+ ItemGroupManager itemGroupsManager;
+
+ private ItemGroupConfigHolder() {
+ super(FILE_NAME);
+ }
+
+ public ItemGroupManager getItemGroupsManager() {
+ return itemGroupsManager;
+ }
+
+ @Override
+ public void reload() {
+ // not the most efficient way for in game reload TODO optimise
+ this.itemGroupsManager = new ItemGroupManager();
+ this.itemGroupsManager.prepareGroups(this.configuration);
+
+ if (CONFLICT_HOLDER.getConfig() != null) {
+ CONFLICT_HOLDER.reload();
+ }
+ }
+
+ }
+
+ // Class for enchant conflict config
+ public static class ConflictConfigHolder extends DeletableResource {
+ private static final String FILE_NAME = "enchant_conflict";
+
+ EnchantConflictManager conflictManager;
+
+ private ConflictConfigHolder() {
+ super(FILE_NAME);
+ }
+
+ public EnchantConflictManager getConflictManager() {
+ return conflictManager;
+ }
+
+ // We assume this is called after item group manager reload;,
+ @Override
+ public void reload() {
+ // not the most efficient way for in game reload TODO optimise
+ this.conflictManager = new EnchantConflictManager();
+ this.conflictManager.prepareConflicts(this.configuration, ITEM_GROUP_HOLDER.getItemGroupsManager());
+ }
+
+ }
+
+ // Class for unit repair config
+ public static class UnitRepairHolder extends DeletableResource {
+ private static final String ITEM_GROUP_FILE_NAME = "unit_repair_item";
+
+ private UnitRepairHolder() {
+ super(ITEM_GROUP_FILE_NAME);
+ }
+
+ @Override
+ public void reload() {
+ } // Do nothing
+
+ }
+
+
+ // Class for custom anvil craft
+ public static class CustomAnvilCraftHolder extends DeletableResource {
+ private static final String CUSTOM_RECIPE_FILE_NAME = "custom_recipes";
+ CustomAnvilRecipeManager recipeManager;
+
+ private CustomAnvilCraftHolder() {
+ super(CUSTOM_RECIPE_FILE_NAME);
+ }
+
+ public CustomAnvilRecipeManager getRecipeManager() {
+ return recipeManager;
+ }
+
+ @Override
+ public void reload() {
+ this.recipeManager = new CustomAnvilRecipeManager();
+ this.recipeManager.prepareRecipes(this.configuration);
+ }
+ }
+
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java b/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java
new file mode 100644
index 0000000..d374999
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/config/WorkPenaltyType.java
@@ -0,0 +1,66 @@
+package xyz.alexcrea.cuanvil.config;
+
+import com.google.common.collect.ImmutableMap;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
+
+import java.util.EnumMap;
+
+public class WorkPenaltyType {
+
+ public record WorkPenaltyPart(
+ boolean penaltyIncrease,
+ boolean penaltyAdditive,
+ boolean exclusivePenaltyIncrease,
+ boolean exclusivePenaltyAdditive
+ ) {
+
+ @Override
+ public boolean equals(Object obj) {
+ if(!(obj instanceof WorkPenaltyPart other)) return false;
+
+ return other.penaltyIncrease == this.penaltyIncrease &&
+ other.penaltyAdditive == this.penaltyAdditive &&
+ other.exclusivePenaltyIncrease == this.exclusivePenaltyIncrease &&
+ other.exclusivePenaltyAdditive == this.exclusivePenaltyAdditive;
+ }
+
+ public WorkPenaltyPart(boolean penaltyIncrease, boolean penaltyAdditive) {
+ this(penaltyIncrease, penaltyAdditive, false, false);
+ }
+ }
+
+ private final EnumMap partMap;
+
+ public WorkPenaltyType(@Nullable EnumMap partMap) {
+ this.partMap = new EnumMap<>(partMap != null ? partMap : new EnumMap<>(AnvilUseType.class));
+ }
+
+ public ImmutableMap getPartMap() {
+ return ImmutableMap.copyOf(partMap);
+ }
+
+ public WorkPenaltyPart getPenaltyInfo(AnvilUseType type) {
+ return partMap.getOrDefault(type, type.getDefaultPenalty());
+ }
+
+ public boolean isPenaltyIncreasing(AnvilUseType type) {
+ return partMap.getOrDefault(type, type.getDefaultPenalty()).penaltyIncrease;
+ }
+
+ public boolean isPenaltyAdditive(AnvilUseType type) {
+ return partMap.getOrDefault(type, type.getDefaultPenalty()).penaltyAdditive;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WorkPenaltyType that)) return false;
+
+ for (AnvilUseType type : AnvilUseType.getEntries()) {
+ if(!getPenaltyInfo(type).equals(that.getPenaltyInfo(type))) return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java
new file mode 100644
index 0000000..821838f
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/AdditionalTestEnchantment.java
@@ -0,0 +1,34 @@
+package xyz.alexcrea.cuanvil.enchant;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+public interface AdditionalTestEnchantment {
+
+ /**
+ * Test if the provided enchantments can be compatible with this enchantment. only non-Custom Anvil conflict.
+ * @param enchantments Immutable map of validated enchantments for the item.
+ * @param itemType Material namespaced key of the tested item.
+ * @return If there is a conflict with the enchantments.
+ */
+ boolean isEnchantConflict(
+ @NotNull Map enchantments,
+ @NotNull NamespacedKey itemType);
+
+ /**
+ * Test if the provided item can be compatible with this enchantment. only non-Custom Anvil conflict.
+ * @param enchantments Immutable map of validated enchantments for the item.
+ * @param itemType Material namespaced key of the tested item.
+ * @param item Provide a new instance of the used item stack with the partial enchantment applied.
+ * @return If there is a conflict with the enchantment and the item.
+ */
+ boolean isItemConflict(
+ @NotNull Map enchantments,
+ @NotNull NamespacedKey itemType,
+ @NotNull ItemStack item);
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java
new file mode 100644
index 0000000..ea657ac
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantment.java
@@ -0,0 +1,249 @@
+package xyz.alexcrea.cuanvil.enchant;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.enchant.bulk.BulkCleanEnchantOperation;
+import xyz.alexcrea.cuanvil.enchant.bulk.BulkGetEnchantOperation;
+import xyz.alexcrea.cuanvil.group.EnchantConflictGroup;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represent an enchantment compatible with Custom Anvil.
+ * One issue with custom anvil is: it does not handle well duplicate key name (ignoring namespace)
+ * as the plugin was initially coded with vanilla enchantment in head
+ */
+@SuppressWarnings("unused")
+public interface CAEnchantment {
+
+
+ /**
+ * Get the default rarity of this enchant.
+ * @return The default rarity of this enchant.
+ */
+ @NotNull
+ EnchantmentRarity defaultRarity();
+
+ /**
+ * Get the enchantment key.
+ * @return The enchantment key.
+ */
+ @NotNull
+ NamespacedKey getKey();
+
+ /**
+ * Get the enchantment name.
+ * @return The enchantment name.
+ */
+ @NotNull
+ String getName();
+
+ /**
+ * Get the default maximum level of this enchantment.
+ * @return The default maximum level of this enchantment.
+ */
+ int defaultMaxLevel();
+
+ /**
+ * Check if the enchantment have specialised get bulk operation.
+ * @return If the enchantment is optimised for get bulk operation.
+ */
+ boolean isGetOptimised();
+
+ /**
+ * Check if the enchantment have specialised clean bulk operation.
+ * @return If the enchantment is optimised for clean bulk operation.
+ */
+ boolean isCleanOptimised();
+
+ /**
+ * Check if the player is allowed to use this enchantment.
+ * @param player The player to test.
+ * @return If the player is allowed to use this enchantment.
+ */
+ boolean isAllowed(@NotNull HumanEntity player);
+
+ /**
+ * Add a conflict to this enchantment conflict list.
+ * @param conflict The conflict to add.
+ */
+ void addConflict(@NotNull EnchantConflictGroup conflict);
+
+ /**
+ * Remove a conflict from the conflict list of this enchantment.
+ * @param conflict The conflict to remove from this enchantment.
+ */
+ void removeConflict(@NotNull EnchantConflictGroup conflict);
+
+ /**
+ * Clear Custom Anvil conflicts for this enchantment.
+ */
+ void clearConflict();
+
+ /**
+ * Get a collection of Custom Anvil conflict containing this enchantment.
+ * @return A collection of Custom Anvil conflict containing this enchantment.
+ */
+ @NotNull Collection getConflicts();
+
+ /**
+ * Get current level of the enchantment.
+ * @param item Item to search the level for. Should not get changed.
+ * @return Current leve of this enchantment on item. or 0 if absent.
+ */
+ default int getLevel(@NotNull ItemStack item){
+ ItemMeta meta = item.getItemMeta();
+ if(meta == null) return 0;
+
+ return getLevel(item, meta);
+ }
+
+ /**
+ * Get current level of the enchantment.
+ * @param item Item to search the level for. Should not get changed.
+ * @param meta Meta of the provided item. Should not get changed.
+ * @return Current leve of this enchantment on item. or 0 if absent.
+ */
+ int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta);
+
+ /**
+ * Check if this enchantment is present on the provided level.
+ * @param item The item to set the enchantment level.
+ * @return If the enchantment have been found.
+ */
+ boolean isEnchantmentPresent(@NotNull ItemStack item);
+
+ /**
+ * Check if this enchantment is present on the provided level.
+ * @param item The item to set the enchantment level.
+ * @param meta Meta of the provided item. It will not be changed and not be set on the item.
+ * @return If the enchantment have been found.
+ */
+ boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta);
+
+ /**
+ * Force add an enchantment at the provided level.
+ * @param item The item to set the enchantment level.
+ * @param level The level to set the enchantment to.
+ */
+ void addEnchantmentUnsafe(@NotNull ItemStack item, int level);
+
+ /**
+ * Remove this enchantment from the provided ItemStack.
+ * @param item The item to remove the enchantment.
+ */
+ void removeFrom(@NotNull ItemStack item);
+
+ // Static functions
+ /**
+ * Clear every enchantment from this item.
+ * @param item Item to be cleared from enchantments.
+ */
+ static void clearEnchants(@NotNull ItemStack item){
+ // Optimised enchantment clean using item stack
+ for (BulkCleanEnchantOperation cleanOperator : CAEnchantmentRegistry.getInstance().getOptimisedCleanOperators()) {
+ cleanOperator.bulkClear(item);
+ }
+
+ ItemMeta meta = item.getItemMeta();
+ if(meta == null) return;
+
+ // Optimised enchantment clean using item meta
+ for (BulkCleanEnchantOperation cleanOperator : CAEnchantmentRegistry.getInstance().getOptimisedCleanOperators()) {
+ cleanOperator.bulkClear(item, meta);
+ }
+
+ item.setItemMeta(meta);
+
+ // Clean unoptimised enchants
+ for (CAEnchantment enchant : CAEnchantmentRegistry.getInstance().unoptimisedCleanValues()) {
+ if(enchant.isEnchantmentPresent(item)){
+ enchant.removeFrom(item);
+ }
+ }
+
+ }
+
+ /**
+ * Get enchantments of an item.
+ * @param item Item to get enchantment from.
+ * @return A map of the set enchantments and there's respective levels.
+ */
+ static Map getEnchants(@NotNull ItemStack item){
+ Map enchantments = new HashMap<>();
+ CAEnchantmentRegistry registry = CAEnchantmentRegistry.getInstance();
+
+ ItemMeta meta = item.getItemMeta();
+ if(meta == null) return enchantments;
+
+ // Optimised enchantment get
+ for (BulkGetEnchantOperation getOperator : CAEnchantmentRegistry.getInstance().getOptimisedGetOperators()) {
+ getOperator.bulkGet(enchantments, item, meta);
+ }
+
+ // Unoptimised enchantment get
+ findEnchantsFromSelectedList(item, meta, enchantments, registry.unoptimisedGetValues());
+
+ return enchantments;
+ }
+
+
+ /**
+ * Find enchantments of an item. only test the enchantment from the list.
+ * @param item Item to get enchantment from.
+ * @param meta Meta of the provided item.
+ * @param enchantments Map of enchantment to complete.
+ * @param enchantmentToTest Enchantment to test
+ */
+ static void findEnchantsFromSelectedList(
+ @NotNull ItemStack item,
+ @NotNull ItemMeta meta,
+ @NotNull Map enchantments,
+ @NotNull Collection enchantmentToTest){
+ for (CAEnchantment enchantment : enchantmentToTest) {
+ if(enchantment.isEnchantmentPresent(item, meta)){
+ enchantments.put(enchantment, enchantment.getLevel(item, meta));
+ }
+ }
+
+ }
+
+ /**
+ * Gets an array of all the registered enchantments.
+ *
+ * @param key The enchantment key
+ * @return Array of enchantment.
+ */
+ static @Nullable CAEnchantment getByKey(@NotNull NamespacedKey key){
+ return CAEnchantmentRegistry.getInstance().getByKey(key);
+ }
+
+ /**
+ * 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/CAEnchantmentBase.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentBase.java
new file mode 100644
index 0000000..05718d5
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentBase.java
@@ -0,0 +1,115 @@
+package xyz.alexcrea.cuanvil.enchant;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.group.EnchantConflictGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Default implementation of an enchantment compatible with Custom Anvil.
+ * One issue with custom anvil is: it does not handle well duplicate key name (ignoring namespace)
+ * as the plugin was initially coded with vanilla enchantment in head
+ */
+public abstract class CAEnchantmentBase implements CAEnchantment {
+
+ @NotNull
+ private final NamespacedKey key;
+ @NotNull
+ private final String name;
+ @NotNull
+ private final EnchantmentRarity defaultRarity;
+ private final int defaultMaxLevel;
+
+ private final List conflicts;
+
+ /**
+ * Constructor of Wrapped Enchantment.
+ * @param key The enchantment's key.
+ * @param defaultRarity Default rarity the enchantment should be.
+ * @param defaultMaxLevel Default max level the enchantment can be applied with.
+ */
+ protected CAEnchantmentBase(
+ @NotNull NamespacedKey key,
+ @Nullable EnchantmentRarity defaultRarity,
+ int defaultMaxLevel){
+ this.key = key;
+ this.name = key.getKey();
+ this.defaultMaxLevel = defaultMaxLevel;
+
+ this.defaultRarity = Objects.requireNonNullElse(defaultRarity, EnchantmentRarity.COMMON);
+
+ this.conflicts = new ArrayList<>();
+ }
+
+ @NotNull
+ @Override
+ public final EnchantmentRarity defaultRarity(){
+ return defaultRarity;
+ }
+
+ @NotNull
+ @Override
+ public final NamespacedKey getKey(){
+ return key;
+ }
+
+ @NotNull
+ @Override
+ public final String getName(){
+ return name;
+ }
+
+ @Override
+ public final int defaultMaxLevel(){
+ return defaultMaxLevel;
+ }
+
+ @Override
+ public boolean isGetOptimised(){
+ return false;
+ }
+
+ @Override
+ public boolean isCleanOptimised(){
+ return false;
+ }
+
+ @Override
+ public boolean isAllowed(@NotNull HumanEntity player){
+ return true;
+ }
+
+ public boolean isEnchantmentPresent(@NotNull ItemStack item){
+ ItemMeta meta = item.getItemMeta();
+ if(meta == null) return false;
+ return isEnchantmentPresent(item, meta);
+ }
+
+ @Override
+ public void addConflict(@NotNull EnchantConflictGroup conflict){
+ this.conflicts.add(conflict);
+ }
+
+ @Override
+ public void removeConflict(@NotNull EnchantConflictGroup conflict){
+ this.conflicts.remove(conflict);
+ }
+
+ @Override
+ public void clearConflict(){
+ this.conflicts.clear();
+ }
+
+ @Override
+ public @NotNull List getConflicts() {
+ return conflicts;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java
new file mode 100644
index 0000000..854ed55
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/CAEnchantmentRegistry.java
@@ -0,0 +1,250 @@
+package xyz.alexcrea.cuanvil.enchant;
+
+import io.delilaheve.CustomAnvil;
+import org.bukkit.NamespacedKey;
+import org.bukkit.enchantments.Enchantment;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.enchant.bulk.BukkitEnchantBulkOperation;
+import xyz.alexcrea.cuanvil.enchant.bulk.BulkCleanEnchantOperation;
+import xyz.alexcrea.cuanvil.enchant.bulk.BulkGetEnchantOperation;
+import xyz.alexcrea.cuanvil.enchant.wrapped.CABukkitEnchantment;
+import xyz.alexcrea.cuanvil.util.MetricsUtil;
+
+import java.util.*;
+import java.util.logging.Level;
+
+public class CAEnchantmentRegistry {
+
+ private static final CAEnchantmentRegistry instance = new CAEnchantmentRegistry();
+
+ public static CAEnchantmentRegistry getInstance() {
+ return instance;
+ }
+
+ // Register enchantment functions
+ private final HashMap byKeyMap;
+ private final HashMap> byNameMap;
+
+ private final SortedSet nameSortedEnchantments;
+
+ private final List unoptimisedGetValues;
+ private final List unoptimisedCleanValues;
+
+ private final List optimisedGetOperators;
+ private final List optimisedCleanOperators;
+
+ private CAEnchantmentRegistry() {
+ byKeyMap = new HashMap<>();
+ byNameMap = new HashMap<>();
+
+ nameSortedEnchantments = new TreeSet<>(Comparator.comparing(CAEnchantment::getName));
+
+ unoptimisedGetValues = new ArrayList<>();
+ unoptimisedCleanValues = new ArrayList<>();
+
+ optimisedGetOperators = new ArrayList<>();
+ optimisedCleanOperators = new ArrayList<>();
+ }
+
+ /**
+ * This should only be called on main of custom anvil.
+ * If called more than one time, chance of thing being broken will be high.
+ */
+ public void registerBukkit() {
+ // Register enchantment
+ for (Enchantment enchantment : Enchantment.values()) {
+ register(new CABukkitEnchantment(enchantment));
+ }
+
+ // Add bukkit enchantment bulk operation
+ BukkitEnchantBulkOperation bukkitOperation = new BukkitEnchantBulkOperation();
+ optimisedGetOperators.add(bukkitOperation);
+ optimisedCleanOperators.add(bukkitOperation);
+ }
+
+ private static boolean hasWarnedRegistering = false;
+
+ /**
+ * Can be used to register new enchantment.
+ *
+ * No guarantee that the enchantment will be present on the config gui if registered late.
+ * (By late I mean after custom anvil startup.)
+ *
+ * @param enchantment The enchantment to be registered.
+ * @return If the operation was successful.
+ */
+ public boolean register(@NotNull CAEnchantment enchantment) {
+ if (byKeyMap.containsKey(enchantment.getKey())) {
+ if (Objects.equals(enchantment, byKeyMap.get(enchantment.getKey()))) {
+ // We are trying to register the exact same enchantment. so we just skip it.
+ return false;
+ }
+
+ if (ConfigHolder.DEFAULT_CONFIG.getConfig().getBoolean("caution_secret_do_not_log_duplicated_registered_key", false)) {
+ return false;
+ }
+
+ var error = new IllegalStateException("enchantment " + enchantment.getKey() + " was already registered");
+ CustomAnvil.instance.getLogger().log(Level.WARNING,
+ "Duplicate distinct registered enchantment. This should NOT happen any time.\n" +
+ "If you are a custom anvil developer: Maybe custom anvil detected your enchantment as a bukkit enchantment. " +
+ "you should maybe remove enchantment with the same key before registering yours",
+ error);
+ MetricsUtil.INSTANCE.trackError(error);
+ return false;
+ }
+
+ if ((!hasWarnedRegistering) && byNameMap.containsKey(enchantment.getName())) {
+ hasWarnedRegistering = true;
+
+ CustomAnvil.instance.getLogger().log(Level.WARNING,
+ "Duplicate registered enchantment name. Please check that configuration is using namespace.");
+ }
+
+ byKeyMap.put(enchantment.getKey(), enchantment);
+
+ byNameMap.putIfAbsent(enchantment.getName(), new ArrayList<>());
+ byNameMap.get(enchantment.getName()).add(enchantment);
+
+ nameSortedEnchantments.add(enchantment);
+
+ if (!enchantment.isGetOptimised()) {
+ unoptimisedGetValues.add(enchantment);
+ }
+ if (!enchantment.isCleanOptimised()) {
+ unoptimisedCleanValues.add(enchantment);
+ }
+
+ return true;
+ }
+
+ /**
+ * Can be used to unregister new enchantment.
+ * Please be cautious with this function.
+ * It should probably rarely be used.
+ *
+ * No guarantee that the enchantment will absent if the config guis if unregistered late.
+ * (By late I mean after custom anvil startup.)
+ *
+ * @param enchantment The enchantment to be unregistered.
+ * @return If the operation was successful.
+ */
+
+ public boolean unregister(@Nullable CAEnchantment enchantment) {
+ if (enchantment == null) return false;
+ byKeyMap.remove(enchantment.getKey());
+ byNameMap.get(enchantment.getName()).remove(enchantment);
+
+ nameSortedEnchantments.remove(enchantment);
+
+ unoptimisedGetValues.remove(enchantment);
+ unoptimisedCleanValues.remove(enchantment);
+ return true;
+ }
+
+ /**
+ * Gets the enchantment by the provided key.
+ *
+ * @param key Key to fetch.
+ * @return Registered enchantment. null if absent.
+ */
+ @Nullable
+ public CAEnchantment getByKey(@NotNull NamespacedKey key) {
+ return byKeyMap.get(key);
+ }
+
+ /**
+ * 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) {
+ 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());
+ }
+
+ /**
+ * Gets an array of all the registered enchantments.
+ *
+ * @return Array of enchantments.
+ */
+ @NotNull
+ public Collection values() {
+ return byKeyMap.values();
+ }
+
+ /**
+ * Gets a map of all the registered enchantments.
+ *
+ * @return Immutable map of enchantments.
+ */
+ public Map registeredEnchantments() {
+ return Collections.unmodifiableMap(byKeyMap);
+ }
+
+ /**
+ * Gets a list of all the unoptimised get operation enchantments.
+ *
+ * @return List of unoptimised enchantments.
+ */
+ @NotNull
+ public List unoptimisedGetValues() {
+ return unoptimisedGetValues;
+ }
+
+ /**
+ * Gets a list of all the unoptimised clean operation enchantments.
+ *
+ * @return List of unoptimised enchantments.
+ */
+ @NotNull
+ public List unoptimisedCleanValues() {
+ return unoptimisedCleanValues;
+ }
+
+ /**
+ * Get "clean optimised operation" for get enchantments.
+ *
+ * @return Mutable "clean enchantments optimised operation" list.
+ */
+ public List getOptimisedCleanOperators() {
+ return optimisedCleanOperators;
+ }
+
+ /**
+ * Get "get optimised operation" for get enchantments.
+ *
+ * @return Mutable "get enchantments optimised operation" list.
+ */
+ public List getOptimisedGetOperators() {
+ return optimisedGetOperators;
+ }
+
+ /**
+ * Get custom anvil enchantment sorted by name.
+ *
+ * @return An immutable sorted set of every registered enchantment sorted by name.
+ */
+ public SortedSet getNameSortedEnchantments() {
+ return Collections.unmodifiableSortedSet(nameSortedEnchantments);
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentProperties.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentProperties.java
new file mode 100644
index 0000000..1137feb
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentProperties.java
@@ -0,0 +1,61 @@
+package xyz.alexcrea.cuanvil.enchant;
+
+// to bind EnchantmentRarity to an enchantment...
+public enum EnchantmentProperties {
+
+ AQUA_AFFINITY(EnchantmentRarity.RARE),
+ BANE_OF_ARTHROPODS(EnchantmentRarity.UNCOMMON),
+ BINDING_CURSE(EnchantmentRarity.VERY_RARE),
+ BLAST_PROTECTION(EnchantmentRarity.RARE),
+ BREACH(EnchantmentRarity.RARE),
+ CHANNELING(EnchantmentRarity.VERY_RARE),
+ DENSITY(EnchantmentRarity.UNCOMMON),
+ DEPTH_STRIDER(EnchantmentRarity.RARE),
+ EFFICIENCY(EnchantmentRarity.COMMON),
+ FLAME(EnchantmentRarity.RARE),
+ FEATHER_FALLING(EnchantmentRarity.UNCOMMON),
+ FIRE_ASPECT(EnchantmentRarity.RARE),
+ FIRE_PROTECTION(EnchantmentRarity.UNCOMMON),
+ FORTUNE(EnchantmentRarity.RARE),
+ FROST_WALKER(EnchantmentRarity.RARE),
+ IMPALING(EnchantmentRarity.RARE),
+ INFINITY(EnchantmentRarity.VERY_RARE),
+ KNOCKBACK(EnchantmentRarity.UNCOMMON),
+ LOOTING(EnchantmentRarity.RARE),
+ LOYALTY(EnchantmentRarity.COMMON),
+ LUCK_OF_THE_SEA(EnchantmentRarity.RARE),
+ LURE(EnchantmentRarity.RARE),
+ MENDING(EnchantmentRarity.RARE),
+ MULTISHOT(EnchantmentRarity.RARE),
+ PIERCING(EnchantmentRarity.COMMON),
+ POWER(EnchantmentRarity.COMMON),
+ PROJECTILE_PROTECTION(EnchantmentRarity.UNCOMMON),
+ PROTECTION(EnchantmentRarity.COMMON),
+ PUNCH(EnchantmentRarity.RARE),
+ QUICK_CHARGE(EnchantmentRarity.UNCOMMON),
+ RESPIRATION(EnchantmentRarity.RARE),
+ RIPTIDE(EnchantmentRarity.RARE),
+ SILK_TOUCH(EnchantmentRarity.VERY_RARE),
+ SHARPNESS(EnchantmentRarity.COMMON),
+ SMITE(EnchantmentRarity.UNCOMMON),
+ SOUL_SPEED(EnchantmentRarity.VERY_RARE),
+ SWIFT_SNEAK(EnchantmentRarity.VERY_RARE),
+ SWEEPING(EnchantmentRarity.RARE),
+ SWEEPING_EDGE(EnchantmentRarity.RARE),
+ THORNS(EnchantmentRarity.VERY_RARE),
+ UNBREAKING(EnchantmentRarity.UNCOMMON),
+ VANISHING_CURSE(EnchantmentRarity.VERY_RARE),
+ WIND_BURST(EnchantmentRarity.RARE),
+ ;
+
+ private final EnchantmentRarity rarity;
+
+ EnchantmentProperties(EnchantmentRarity rarity) {
+ this.rarity = rarity;
+ }
+
+ public EnchantmentRarity getRarity() {
+ return rarity;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentRarity.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentRarity.java
new file mode 100644
index 0000000..3718f39
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/EnchantmentRarity.java
@@ -0,0 +1,52 @@
+package xyz.alexcrea.cuanvil.enchant;
+
+// because spigot (1.18) do not look like to provide access to enchantment rarity I need to do it myself...
+public class EnchantmentRarity {
+
+ public static final EnchantmentRarity NO_RARITY = new EnchantmentRarity(0, 0);
+ public static final EnchantmentRarity COMMON = new EnchantmentRarity(1);
+ public static final EnchantmentRarity UNCOMMON = new EnchantmentRarity(2);
+ public static final EnchantmentRarity RARE = new EnchantmentRarity(4);
+ public static final EnchantmentRarity VERY_RARE = new EnchantmentRarity(8);
+
+ private final int itemValue;
+ private final int bookValue;
+
+ private EnchantmentRarity(int itemValue, int bookValue) {
+ this.itemValue = itemValue;
+ this.bookValue = bookValue;
+ }
+
+ private EnchantmentRarity(int itemValue) {
+ this(itemValue, Math.max(1, itemValue / 2));
+ }
+
+ public final int getBookValue() {
+ return bookValue;
+ }
+
+ public final int getItemValue() {
+ return itemValue;
+ }
+
+
+ public static EnchantmentRarity getRarity(int itemValue, int bookValue){
+ int expectedBook = Math.max(1, itemValue / 2);
+ if((expectedBook == bookValue) && (itemValue != 0)) return getRarity(itemValue);
+
+ if(itemValue == 0 && bookValue == 0) return NO_RARITY;
+ return new EnchantmentRarity(itemValue, bookValue);
+ }
+
+ public static EnchantmentRarity getRarity(int itemValue){
+ return switch (itemValue) {
+ case 0 -> NO_RARITY;
+ case 1 -> COMMON;
+ case 2 -> UNCOMMON;
+ case 4 -> RARE;
+ case 8 -> VERY_RARE;
+ default -> new EnchantmentRarity(itemValue);
+ };
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BukkitEnchantBulkOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BukkitEnchantBulkOperation.java
new file mode 100644
index 0000000..73e4185
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BukkitEnchantBulkOperation.java
@@ -0,0 +1,66 @@
+package xyz.alexcrea.cuanvil.enchant.bulk;
+
+import io.delilaheve.CustomAnvil;
+import io.delilaheve.util.ConfigOptions;
+import io.delilaheve.util.ItemUtil;
+import org.bukkit.Material;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.api.EnchantmentApi;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.util.Map;
+
+public class BukkitEnchantBulkOperation implements BulkGetEnchantOperation, BulkCleanEnchantOperation {
+
+ @Override
+ public void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta) {
+ boolean isBook = ItemUtil.INSTANCE.isEnchantedBook(item);
+
+ if (isBook) {
+ ((EnchantmentStorageMeta) meta).getStoredEnchants().forEach((enchantment, level) ->
+ addEnchantment(enchantmentMap, enchantment, level)
+ );
+ }
+ if(!isBook || ConfigOptions.INSTANCE.getAddBookEnchantmentAsStoredEnchantment()){
+ item.getEnchantments().forEach((enchantment, level) ->
+ addEnchantment(enchantmentMap, enchantment, level)
+ );
+ }
+ }
+
+ public void addEnchantment(@NotNull Map enchantmentMap, @NotNull Enchantment enchantment, int level) {
+ CAEnchantment enchant = EnchantmentApi.getByKey(enchantment.getKey());
+ if (enchant == null) {
+ CustomAnvil.instance.getLogger().warning("Enchantment of key " + enchantment.getKey() +
+ " somehow not found in CustomAnvil ?");
+ return;
+ }
+
+ enchantmentMap.put(enchant, level);
+ }
+
+ @Override
+ public void bulkClear(@NotNull ItemStack item) {
+ if (item.getType() != Material.ENCHANTED_BOOK || ConfigOptions.INSTANCE.getAddBookEnchantmentAsStoredEnchantment()) {
+
+ item.getEnchantments().forEach((enchantment, level) ->
+ item.removeEnchantment(enchantment)
+ );
+ }
+ }
+
+ @Override
+ public void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ if (item.getType() == Material.ENCHANTED_BOOK) {
+ EnchantmentStorageMeta bookMeta = (EnchantmentStorageMeta) meta;
+ bookMeta.getStoredEnchants().forEach((enchantment, leve) ->
+ bookMeta.removeStoredEnchant(enchantment)
+ );
+ }
+
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkCleanEnchantOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkCleanEnchantOperation.java
new file mode 100644
index 0000000..4b1a225
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkCleanEnchantOperation.java
@@ -0,0 +1,28 @@
+package xyz.alexcrea.cuanvil.enchant.bulk;
+
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Bulk operation for clean enchantments operations.
+ */
+public interface BulkCleanEnchantOperation {
+
+ /**
+ * Bulk clear part of the enchantments from this item.
+ * The item can be edited freely. If you need the meta it is preferred to use {@link #bulkClear(ItemStack, ItemMeta)} if possible
+ * @param item The item to clear enchantment from.
+ */
+ void bulkClear(@NotNull ItemStack item);
+
+ /**
+ * Bulk clear part of the enchantments from this item meta.
+ * Item should not be edited as meta will be applied later.
+ * If you need to edit the item and do not need the meta use {@link #bulkClear(ItemStack)}
+ * @param item The item source of the item meta. should not be edited.
+ * @param meta The item meta to clear enchantment from.
+ */
+ void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta);
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkGetEnchantOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkGetEnchantOperation.java
new file mode 100644
index 0000000..7c66e8a
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/BulkGetEnchantOperation.java
@@ -0,0 +1,23 @@
+package xyz.alexcrea.cuanvil.enchant.bulk;
+
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.util.Map;
+
+/**
+ * Bulk operation for get enchantments operations.
+ */
+public interface BulkGetEnchantOperation {
+
+ /**
+ * Bulk get part of the stored enchantment of this item.
+ * @param enchantmentMap Mutable map of collected enchantment. should b
+ * @param item The item to get enchantment from. Should not get edited.
+ * @param meta The item meta to get enchantment from. Should not get edited.
+ */
+ void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta);
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/EnchantSquaredBulkOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/EnchantSquaredBulkOperation.java
new file mode 100644
index 0000000..57ecf60
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/EnchantSquaredBulkOperation.java
@@ -0,0 +1,37 @@
+package xyz.alexcrea.cuanvil.enchant.bulk;
+
+import me.athlaeos.enchantssquared.managers.CustomEnchantManager;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.dependency.plugins.EnchantmentSquaredDependency;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class EnchantSquaredBulkOperation implements BulkGetEnchantOperation, BulkCleanEnchantOperation {
+
+ @Override
+ public void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta) {
+ EnchantmentSquaredDependency enchantmentSquared = DependencyManager.INSTANCE.getEnchantmentSquaredCompatibility();
+ if(enchantmentSquared != null){
+ enchantmentSquared.getEnchantmentsSquared(item, enchantmentMap);
+ }
+ }
+
+
+ @Override
+ public void bulkClear(@NotNull ItemStack item) {
+ EnchantmentSquaredDependency enchantmentSquared = DependencyManager.INSTANCE.getEnchantmentSquaredCompatibility();
+ if(enchantmentSquared != null){
+ CustomEnchantManager.getInstance().setItemEnchants(item, Collections.emptyMap());
+ }
+ }
+
+ @Override
+ public void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ // item meta is not preferred for enchantment squared clear
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/SuperEnchantBulkOperation.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/SuperEnchantBulkOperation.java
new file mode 100644
index 0000000..8bc729a
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/bulk/SuperEnchantBulkOperation.java
@@ -0,0 +1,47 @@
+package xyz.alexcrea.cuanvil.enchant.bulk;
+
+import com.maddoxh.superEnchants.items.EnchantApplicator;
+import com.maddoxh.superEnchants.items.EnchantReader;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.api.EnchantmentApi;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.util.Map;
+
+public class SuperEnchantBulkOperation implements BulkGetEnchantOperation, BulkCleanEnchantOperation {
+
+ private Plugin plugin;
+ public SuperEnchantBulkOperation(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public void bulkGet(@NotNull Map enchantmentMap, @NotNull ItemStack item, @NotNull ItemMeta meta) {
+ EnchantReader.INSTANCE.readEnchants(item).forEach((ench, level) -> {
+ var enchantment = EnchantmentApi.getByKey(NamespacedKey.fromString(ench, plugin));
+ if(enchantment == null) {
+ CustomAnvil.log("Enchantment " + ench + " not found in custom anvil");
+ return;
+ }
+
+ enchantmentMap.put(enchantment, level);
+ }
+ );
+ }
+
+ @Override
+ public void bulkClear(@NotNull ItemStack item) {
+ EnchantApplicator.INSTANCE.clearAllCustomEnchants(item);
+ }
+
+ @Override
+ public void bulkClear(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ // item meta is not preferred for enchantment squared clear
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CABukkitEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CABukkitEnchantment.java
new file mode 100644
index 0000000..0e630ea
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CABukkitEnchantment.java
@@ -0,0 +1,174 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import io.delilaheve.CustomAnvil;
+import io.delilaheve.util.ConfigOptions;
+import io.delilaheve.util.ItemUtil;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.enchantments.EnchantmentTarget;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantmentBase;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentProperties;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.logging.Level;
+
+/**
+ * Custom Anvil enchantment implementation for vanilla registered enchantment.
+ */
+public class CABukkitEnchantment extends CAEnchantmentBase {
+
+ public final @NotNull Enchantment bukkit;
+
+ public CABukkitEnchantment(@NotNull Enchantment bukkit, @Nullable EnchantmentRarity rarity) {
+ super(bukkit.getKey(),
+ rarity,
+ bukkit.getMaxLevel());
+ this.bukkit = bukkit;
+ }
+
+ public CABukkitEnchantment(@NotNull Enchantment bukkit) {
+ this(bukkit, getRarity(bukkit));
+ }
+
+ @Override
+ public boolean isGetOptimised() {
+ return true;
+ }
+
+ @Override
+ public boolean isCleanOptimised() {
+ return true;
+ }
+
+ @Override
+ public int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ if (ItemUtil.INSTANCE.isEnchantedBook(item)) {
+ return ((EnchantmentStorageMeta) meta).getStoredEnchantLevel(this.bukkit);
+ } else {
+ return meta.getEnchantLevel(this.bukkit);
+ }
+ }
+
+ @Override
+ public boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ if (ItemUtil.INSTANCE.isEnchantedBook(item)) {
+ EnchantmentStorageMeta bookMeta = ((EnchantmentStorageMeta) meta);
+
+ return bookMeta.getStoredEnchants().containsKey(this.bukkit) ||
+ (ConfigOptions.INSTANCE.getAddBookEnchantmentAsStoredEnchantment() && item.containsEnchantment(this.bukkit));
+ } else {
+ return item.containsEnchantment(this.bukkit);
+ }
+ }
+
+ @Override
+ public void addEnchantmentUnsafe(@NotNull ItemStack item, int level) {
+ if (ItemUtil.INSTANCE.isEnchantedBook(item)) {
+ EnchantmentStorageMeta bookMeta = ((EnchantmentStorageMeta) item.getItemMeta());
+
+ assert bookMeta != null;
+ bookMeta.addStoredEnchant(this.bukkit, level, true);
+ item.setItemMeta(bookMeta);
+ } else {
+ item.addUnsafeEnchantment(this.bukkit, level);
+ }
+
+ }
+
+ @Override
+ public void removeFrom(@NotNull ItemStack item) {
+ if (ItemUtil.INSTANCE.isEnchantedBook(item)) {
+ EnchantmentStorageMeta bookMeta = ((EnchantmentStorageMeta) item.getItemMeta());
+
+ assert bookMeta != null;
+ bookMeta.removeStoredEnchant(this.bukkit);
+ bookMeta.removeEnchant(this.bukkit);
+ item.setItemMeta(bookMeta);
+ } else {
+ item.removeEnchantment(this.bukkit);
+ }
+
+ }
+
+ @NotNull
+ public static EnchantmentRarity getRarity(Enchantment enchantment) {
+ try {
+ return EnchantmentProperties.valueOf(enchantment.getKey().getKey().toUpperCase(Locale.ENGLISH)).getRarity();
+ } catch (IllegalArgumentException ignored) {
+ return findRarity(enchantment);
+ }
+ }
+
+ @NotNull
+ protected Enchantment getEnchant() {
+ return this.bukkit;
+ }
+
+ private static Method getAnvilCostMethod;
+
+ static {
+ Class clazz = Enchantment.class;
+ try {
+ getAnvilCostMethod = clazz.getDeclaredMethod("getAnvilCost");
+ getAnvilCostMethod.setAccessible(true);
+
+ CustomAnvil.Companion.log("Detected getAnvilCost method");
+ } catch (NoSuchMethodException e) {
+ getAnvilCostMethod = null;
+ }
+
+ }
+
+ private static final Map targetToGroup = new HashMap<>();
+ static {
+ targetToGroup.put(EnchantmentTarget.ARMOR, "armors");
+ targetToGroup.put(EnchantmentTarget.ARMOR_HEAD, "helmets");
+ targetToGroup.put(EnchantmentTarget.ARMOR_TORSO, "chestplate");
+ targetToGroup.put(EnchantmentTarget.ARMOR_LEGS, "leggings");
+ targetToGroup.put(EnchantmentTarget.ARMOR_FEET, "boots");
+ targetToGroup.put(EnchantmentTarget.BOW, "bow");
+ targetToGroup.put(EnchantmentTarget.BREAKABLE, "can_unbreak");
+ targetToGroup.put(EnchantmentTarget.CROSSBOW, "crossbow");
+ targetToGroup.put(EnchantmentTarget.FISHING_ROD, "fishing_rod");
+ targetToGroup.put(EnchantmentTarget.TOOL, "tools");
+ targetToGroup.put(EnchantmentTarget.TRIDENT, "trident");
+ targetToGroup.put(EnchantmentTarget.VANISHABLE, "can_vanish");
+ targetToGroup.put(EnchantmentTarget.WEAPON, "swords");
+ targetToGroup.put(EnchantmentTarget.WEARABLE, "wearable");
+ }
+
+ private static EnchantmentRarity findRarity(Enchantment enchantment) {
+ if (getAnvilCostMethod == null) return EnchantmentRarity.COMMON;
+
+ try {
+ int itemCost = (int) getAnvilCostMethod.invoke(enchantment);
+
+ return EnchantmentRarity.getRarity(itemCost);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ CustomAnvil.instance.getLogger().log(Level.SEVERE, "could not find cost for enchantment " + enchantment.getKey(), e);
+
+ return EnchantmentRarity.COMMON;
+ }
+
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CABukkitEnchantment other)) {
+ return false;
+ }
+
+ return Objects.equals(this.bukkit, other.getEnchant());
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java
new file mode 100644
index 0000000..783798d
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEPreV5Enchantment.java
@@ -0,0 +1,61 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment;
+import su.nightexpress.excellentenchants.api.enchantment.Definition;
+import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+
+public class CAEEPreV5Enchantment extends CABukkitEnchantment implements AdditionalTestEnchantment {
+
+ @NotNull CustomEnchantment eeenchantment;
+ @NotNull Definition definition;
+
+ public CAEEPreV5Enchantment(@NotNull CustomEnchantment enchantment) {
+ super(enchantment.getBukkitEnchantment(), getRarity(enchantment.getBukkitEnchantment()));
+ this.eeenchantment = enchantment;
+ try {
+ this.definition = (Definition) getDefinition.invoke(enchantment);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ private final static Method getDefinition;
+ static {
+ try {
+ getDefinition = CustomEnchantment.class.getMethod("getDefinition");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ if (!definition.hasConflicts()) return false;
+
+ Set conflicts = definition.getConflicts();
+
+ for (CAEnchantment caEnchantment : enchantments.keySet()) {
+ if (conflicts.contains(caEnchantment.getName())) return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) {
+ if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false;
+
+ return !definition.getSupportedItems().is(item);
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java
new file mode 100644
index 0000000..2d8f945
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5Enchantment.java
@@ -0,0 +1,128 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment;
+import su.nightexpress.excellentenchants.api.item.ItemSet;
+import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+
+public class CAEEV5Enchantment extends CABukkitEnchantment implements AdditionalTestEnchantment {
+
+ @NotNull CustomEnchantment eeenchantment;
+ @NotNull Object definition;
+
+ public CAEEV5Enchantment(@NotNull CustomEnchantment enchantment) {
+ super(enchantment.getBukkitEnchantment(), EnchantmentRarity.getRarity(getAnvilCost(enchantment)));
+ this.eeenchantment = enchantment;
+ this.definition = getDefinition(enchantment);
+
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ if (!hasConflicts()) return false;
+
+ Set conflicts = getExclusiveSet();
+
+ for (CAEnchantment caEnchantment : enchantments.keySet()) {
+ if (conflicts.contains(caEnchantment.getName())) return true;
+ if (conflicts.contains(caEnchantment.getKey().toString())) return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) {
+ if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false;
+
+ String key = itemType.getKey();
+ ItemSet primary = eeenchantment.getPrimaryItems();
+ if (primary.getMaterials().contains(key)) return false;
+
+ ItemSet supported = eeenchantment.getSupportedItems();
+ if (supported.getMaterials().contains(key)) return false;
+
+ return true;
+ }
+
+
+ private static final Method getDefinitonMethod;
+
+ private static final Method getAnvilCostMethod;
+ private static final Method hasConflictsMethod;
+ private static final Method getExclusiveSetMethod;
+ static {
+ var enchClazz = CustomEnchantment.class;
+ try {
+ getDefinitonMethod = enchClazz.getDeclaredMethod("getDefinition");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+
+ Class> definitionClazz;
+ try {
+ definitionClazz = Class.forName("su.nightexpress.excellentenchants.api.EnchantDefinition");
+ } catch (ClassNotFoundException e) {
+ try {
+ definitionClazz = Class.forName("su.nightexpress.excellentenchants.api.wrapper.EnchantDefinition");
+ } catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ // Now definition methods
+ try {
+ getAnvilCostMethod = definitionClazz.getDeclaredMethod("getAnvilCost");
+ hasConflictsMethod = definitionClazz.getDeclaredMethod("hasConflicts");
+ getExclusiveSetMethod = definitionClazz.getDeclaredMethod("getExclusiveSet");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ private static Object getDefinition(CustomEnchantment enchantment) {
+ try {
+ return getDefinitonMethod.invoke(enchantment);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static int getAnvilCost(CustomEnchantment enchantment) {
+ try {
+ return (int) getAnvilCostMethod.invoke(getDefinition(enchantment));
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean hasConflicts() {
+ try {
+ return (boolean) hasConflictsMethod.invoke(definition);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ private Set getExclusiveSet() {
+ try {
+ return (Set) getExclusiveSetMethod.invoke(definition);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java
new file mode 100644
index 0000000..7fb8627
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEV5_4Enchantment.java
@@ -0,0 +1,29 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import org.bukkit.NamespacedKey;
+import org.jetbrains.annotations.NotNull;
+import su.nightexpress.excellentenchants.api.enchantment.CustomEnchantment;
+import xyz.alexcrea.cuanvil.dependency.plugins.ExcellentEnchant5_4EnchantSettings;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.util.Map;
+
+public class CAEEV5_4Enchantment extends CAEEV5Enchantment {
+
+ public CAEEV5_4Enchantment(@NotNull CustomEnchantment enchantment) {
+ super(enchantment);
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemMat) {
+ if(super.isEnchantConflict(enchantments, itemMat)) return true;
+
+ var limit = ExcellentEnchant5_4EnchantSettings.anvilLimit();
+ var count = enchantments.keySet().stream()
+ .filter(key -> key instanceof CAEEV5_4Enchantment)
+ .count();
+
+ return count > limit;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java
new file mode 100644
index 0000000..32d1346
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEcoEnchant.java
@@ -0,0 +1,79 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import com.willfp.ecoenchants.enchant.EcoEnchant;
+import com.willfp.ecoenchants.target.EnchantmentTarget;
+import com.willfp.ecoenchants.type.EnchantmentType;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CAEcoEnchant extends CABukkitEnchantment implements AdditionalTestEnchantment {
+
+ private final @NotNull EcoEnchant ecoEnchant;
+
+ public CAEcoEnchant(@NotNull EcoEnchant enchant) {
+ super(enchant.getEnchantment(), EnchantmentRarity.COMMON);
+ this.ecoEnchant = enchant;
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ if (enchantments.isEmpty()) return false;
+
+ // Check if there is only self
+ if (enchantments.size() == 1 && this.equals(enchantments.keySet().stream().findFirst().get()))
+ return false;
+
+ if (this.ecoEnchant.getConflictsWithEverything()) {
+ return true;
+ }
+
+ HashMap typeAmountMap = new HashMap<>();
+
+ for (CAEnchantment other : enchantments.keySet()) {
+ if (other instanceof CABukkitEnchantment otherVanilla
+ && this.ecoEnchant.conflictsWith(otherVanilla.getEnchant())) {
+ return true;
+ }
+
+ if (other instanceof CAEcoEnchant ecoOther) {
+ EnchantmentType type = ecoOther.ecoEnchant.getType();
+ typeAmountMap.putIfAbsent(type, 0);
+
+ int amount = typeAmountMap.get(type) + 1;
+ if (amount > type.getLimit()) {
+ return true;
+ }
+
+ typeAmountMap.put(type, amount);
+ }
+
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments,
+ @NotNull NamespacedKey itemType,
+ @NotNull ItemStack item) {
+ if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) {
+ return false;
+ }
+
+ for (EnchantmentTarget target : this.ecoEnchant.getTargets()) {
+ if (target.matches(item)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEnchantSquaredEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEnchantSquaredEnchantment.java
new file mode 100644
index 0000000..8f1058e
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEnchantSquaredEnchantment.java
@@ -0,0 +1,79 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import me.athlaeos.enchantssquared.enchantments.CustomEnchant;
+import me.athlaeos.enchantssquared.managers.CustomEnchantManager;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.dependency.DependencyManager;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantmentBase;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class CAEnchantSquaredEnchantment extends CAEnchantmentBase {
+
+ public final @NotNull CustomEnchant enchant;
+
+ public CAEnchantSquaredEnchantment(@NotNull CustomEnchant enchant) {
+ super(Objects.requireNonNull(
+ Objects.requireNonNull(DependencyManager.INSTANCE.getEnchantmentSquaredCompatibility()).getKeyFromEnchant(enchant)),
+ EnchantmentRarity.COMMON,
+ enchant.getMaxLevel());
+ this.enchant = enchant;
+
+ }
+
+ public @NotNull CustomEnchant getEnchant() {
+ return enchant;
+ }
+
+ @Override
+ public boolean isGetOptimised() {
+ return true;
+ }
+
+ @Override
+ public boolean isCleanOptimised() {
+ return true;
+ }
+
+ @Override
+ public boolean isAllowed(@NotNull HumanEntity human) {
+ return this.enchant.hasPermission(human);
+ }
+
+ @Override
+ public int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ return CustomEnchantManager.getInstance().getEnchantStrength(item, this.enchant.getType());
+ }
+
+ @Override
+ public boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ Map enchants = CustomEnchantManager.getInstance().getItemsEnchantsFromPDC(item);
+ return enchants.containsKey(this.enchant);
+ }
+
+ @Override
+ public void addEnchantmentUnsafe(@NotNull ItemStack item, int level) {
+ CustomEnchantManager.getInstance().addEnchant(item, this.enchant.getType(), level);
+ }
+
+ @Override
+ public void removeFrom(@NotNull ItemStack item) {
+ CustomEnchantManager.getInstance().removeEnchant(item, this.enchant.getType());
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CAEnchantSquaredEnchantment other)) {
+ return false;
+ }
+
+ return this.enchant.equals(other.getEnchant());
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java
new file mode 100644
index 0000000..552ecd4
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAIncompatibleAllEnchant.java
@@ -0,0 +1,36 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.enchant.*;
+
+import java.util.Map;
+
+/**
+ * Represent an enchantment incompatible with every other enchantments
+ */
+public class CAIncompatibleAllEnchant extends CABukkitEnchantment implements AdditionalTestEnchantment {
+
+ public CAIncompatibleAllEnchant(@NotNull Enchantment enchantment, @Nullable EnchantmentRarity rarity) {
+ super(enchantment, rarity);
+ }
+
+ public CAIncompatibleAllEnchant(@NotNull Enchantment enchantment) {
+ super(enchantment);
+ }
+
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ return !enchantments.isEmpty() && !(enchantments.size() == 1 && enchantments.containsKey(this));
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) {
+ return false;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java
new file mode 100644
index 0000000..74068d4
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEEEnchantment.java
@@ -0,0 +1,44 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import su.nightexpress.excellentenchants.api.enchantment.EnchantmentData;
+import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+
+import java.util.Map;
+import java.util.Set;
+
+public class CALegacyEEEnchantment extends CABukkitEnchantment implements AdditionalTestEnchantment {
+
+ @NotNull EnchantmentData eeenchantment;
+
+ public CALegacyEEEnchantment(@NotNull EnchantmentData enchantment) {
+ super(enchantment.getEnchantment(), EnchantmentRarity.getRarity(enchantment.getAnvilCost()));
+ this.eeenchantment = enchantment;
+
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ if (!eeenchantment.hasConflicts()) return false;
+
+ Set conflicts = eeenchantment.getConflicts();
+
+ for (CAEnchantment caEnchantment : enchantments.keySet()) {
+ if (conflicts.contains(caEnchantment.getName())) return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) {
+ if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) return false;
+
+ return !eeenchantment.getSupportedItems().is(item);
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java
new file mode 100644
index 0000000..cb24def
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CALegacyEcoEnchant.java
@@ -0,0 +1,68 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import com.willfp.ecoenchants.enchantments.EcoEnchant;
+import com.willfp.ecoenchants.enchantments.meta.EnchantmentTarget;
+import com.willfp.ecoenchants.enchantments.meta.EnchantmentType;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+import xyz.alexcrea.cuanvil.util.MaterialUtil;
+
+import java.util.Map;
+
+public class CALegacyEcoEnchant extends CABukkitEnchantment implements AdditionalTestEnchantment {
+
+ private final @NotNull EcoEnchant ecoEnchant;
+
+ public CALegacyEcoEnchant(@NotNull EcoEnchant ecoEnchant, @NotNull Enchantment enchantment) {
+ super(enchantment, EnchantmentRarity.COMMON);
+ this.ecoEnchant = ecoEnchant;
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ if (enchantments.isEmpty()) return false;
+
+ EnchantmentType type = this.ecoEnchant.getType();
+ boolean isSingular = type.isSingular();
+
+ for (CAEnchantment other : enchantments.keySet()) {
+ if (other instanceof CABukkitEnchantment otherVanilla
+ && this.ecoEnchant.conflictsWith(otherVanilla.getEnchant())) {
+ return true;
+ }
+
+ if (isSingular &&
+ other != this &&
+ (other instanceof CALegacyEcoEnchant otherEco) &&
+ type.equals(otherEco.ecoEnchant.getType())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments,
+ @NotNull NamespacedKey itemType,
+ @NotNull ItemStack item) {
+ if (Material.ENCHANTED_BOOK.getKey().equals(itemType)) {
+ return false;
+ }
+
+ var mat = MaterialUtil.INSTANCE.getMatFromKey(itemType);
+ for (EnchantmentTarget target : this.ecoEnchant.getTargets()) {
+ if (target.getMaterials().contains(mat)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java
new file mode 100644
index 0000000..6039dc8
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CASuperEnchantEnchantment.java
@@ -0,0 +1,76 @@
+package xyz.alexcrea.cuanvil.enchant.wrapped;
+
+import com.maddoxh.superEnchants.enchants.CustomEnchant;
+import com.maddoxh.superEnchants.enchants.EnchantManager;
+import com.maddoxh.superEnchants.items.EnchantApplicator;
+import com.maddoxh.superEnchants.items.EnchantReader;
+import com.maddoxh.superEnchants.util.ConflictChecker;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.enchant.AdditionalTestEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantmentBase;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CASuperEnchantEnchantment extends CAEnchantmentBase implements AdditionalTestEnchantment {
+
+ private @NotNull CustomEnchant enchant;
+ private @NotNull EnchantManager enchantManager;
+
+ public CASuperEnchantEnchantment(@NotNull CustomEnchant enchant, @NotNull Plugin plugin, @NotNull EnchantManager enchantManager) {
+ super(NamespacedKey.fromString(enchant.getId(), plugin), EnchantmentRarity.COMMON, enchant.getMaxLevel());
+
+ this.enchant = enchant;
+ this.enchantManager = enchantManager;
+ }
+
+ @Override
+ public int getLevel(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ return EnchantReader.INSTANCE.getEnchantLevel(item, enchant.getId());
+ }
+
+ @Override
+ public boolean isEnchantmentPresent(@NotNull ItemStack item, @NotNull ItemMeta meta) {
+ return EnchantReader.INSTANCE.hasEnchant(item, enchant.getId());
+ }
+
+ @Override
+ public void addEnchantmentUnsafe(@NotNull ItemStack item, int level) {
+ EnchantApplicator.INSTANCE.applyEnchant(item, enchant.getId(), level);
+ }
+
+ @Override
+ public void removeFrom(@NotNull ItemStack item) {
+ EnchantApplicator.INSTANCE.removeEnchant(item, enchant.getId());
+ }
+
+ @Override
+ public boolean isEnchantConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType) {
+ var idMap = new HashMap();
+
+ enchantments.forEach((enchant, level) -> {
+ if(!(enchant instanceof CASuperEnchantEnchantment superEnch)) return;
+ idMap.put(superEnch.enchant.getId(), level);
+ });
+
+ return ConflictChecker.INSTANCE.hasConflict(
+ idMap,
+ enchant.getId(),
+ enchantManager
+ ) != null;
+ }
+
+ @Override
+ public boolean isItemConflict(@NotNull Map enchantments, @NotNull NamespacedKey itemType, @NotNull ItemStack item) {
+ if(Material.ENCHANTED_BOOK.equals(item.getType())) return false;
+
+ return !enchant.canApplyTo(item.getType());
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/ValueUpdatableGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/ValueUpdatableGui.java
new file mode 100644
index 0000000..16bc082
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/ValueUpdatableGui.java
@@ -0,0 +1,11 @@
+package xyz.alexcrea.cuanvil.gui;
+
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+
+public interface ValueUpdatableGui {
+
+ void updateGuiValues();
+
+ Gui getConnectedGui();
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java
new file mode 100644
index 0000000..cc4fddc
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/MainConfigGui.java
@@ -0,0 +1,154 @@
+package xyz.alexcrea.cuanvil.gui.config;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import xyz.alexcrea.cuanvil.dependency.packet.PacketManager;
+import xyz.alexcrea.cuanvil.gui.config.global.*;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+
+import java.util.Collections;
+
+public class MainConfigGui extends ChestGui {
+
+ private static final MainConfigGui INSTANCE = new MainConfigGui();
+
+ public static MainConfigGui getInstance() {
+ return INSTANCE;
+ }
+
+ private MainConfigGui() {
+ super(3, "§8Anvil Config", CustomAnvil.instance);
+ }
+
+ public void init(PacketManager packetManager) {
+ Pattern pattern = new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "012345678",
+ "Q00000000"
+ );
+ PatternPane pane = new PatternPane(0, 0, 9, 3, pattern);
+ addPane(pane);
+
+ GuiGlobalItems.addBackgroundItem(pane);
+
+ // Basic config item
+ ItemStack basicConfigItemstack = new ItemStack(Material.COMMAND_BLOCK);
+ ItemMeta basicConfigMeta = basicConfigItemstack.getItemMeta();
+ assert basicConfigMeta != null;
+
+ basicConfigMeta.setDisplayName("§aBasic Config Menu");
+ basicConfigMeta.setLore(Collections.singletonList("§7Click here to open basic config menu"));
+ basicConfigItemstack.setItemMeta(basicConfigMeta);
+
+ GuiItem basicConfigItem = GuiGlobalItems.goToGuiItem(basicConfigItemstack, new BasicConfigGui(packetManager));
+ pane.bindItem('1', basicConfigItem);
+
+ // enchant level limit item
+ ItemStack enchantLimitItemstack = new ItemStack(Material.ENCHANTED_BOOK);
+ ItemMeta enchantLimitMeta = enchantLimitItemstack.getItemMeta();
+ assert enchantLimitMeta != null;
+
+ enchantLimitMeta.setDisplayName("§aEnchantment Level Limit");
+ enchantLimitMeta.setLore(Collections.singletonList("§7Click here to open enchantment level limit menu"));
+ enchantLimitItemstack.setItemMeta(enchantLimitMeta);
+
+ 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();
+ assert enchantCostMeta != null;
+
+ enchantCostMeta.setDisplayName("§aEnchantment Cost");
+ enchantCostMeta.setLore(Collections.singletonList("§7Click here to open enchantment costs menu"));
+ enchantCostItemstack.setItemMeta(enchantCostMeta);
+
+ GuiItem enchantCostItem = GuiGlobalItems.goToGuiItem(enchantCostItemstack, new EnchantCostConfigGui());
+ pane.bindItem('4', enchantCostItem);
+
+ // Enchantment Conflicts item
+ ItemStack enchantConflictItemstack = new ItemStack(Material.OAK_FENCE);
+ ItemMeta enchantConflictMeta = enchantConflictItemstack.getItemMeta();
+ assert enchantConflictMeta != null;
+
+ enchantConflictMeta.setDisplayName("§aEnchantment Conflict");
+ enchantConflictMeta.setLore(Collections.singletonList("§7Click here to open enchantment conflict menu"));
+ enchantConflictItemstack.setItemMeta(enchantConflictMeta);
+
+ GuiItem enchantConflictItem = GuiGlobalItems.goToGuiItem(enchantConflictItemstack, EnchantConflictGui.getInstance());
+ pane.bindItem('5', enchantConflictItem);
+
+ // Group config items
+ ItemStack groupItemstack = new ItemStack(Material.CHEST);
+ ItemMeta groupMeta = groupItemstack.getItemMeta();
+ assert groupMeta != null;
+
+ groupMeta.setDisplayName("§aItem Groups");
+ groupMeta.setLore(Collections.singletonList("§7Click here to open item group menu"));
+ groupItemstack.setItemMeta(groupMeta);
+
+ GuiItem groupConfigItem = GuiGlobalItems.goToGuiItem(groupItemstack, GroupConfigGui.getInstance());
+
+ pane.bindItem('6', groupConfigItem);
+
+ // Unit repair item
+ ItemStack unirRepairItemstack = new ItemStack(Material.DIAMOND);
+ ItemMeta unitRepairMeta = unirRepairItemstack.getItemMeta();
+ assert unitRepairMeta != null;
+
+ unitRepairMeta.setDisplayName("§aUnit Repair");
+ unitRepairMeta.setLore(Collections.singletonList("§7Click here to open anvil unit repair menu"));
+ unirRepairItemstack.setItemMeta(unitRepairMeta);
+
+ GuiItem unitRepairItem = GuiGlobalItems.goToGuiItem(unirRepairItemstack, UnitRepairConfigGui.getInstance());
+ pane.bindItem('7', unitRepairItem);
+
+ // Custom recipe item
+ ItemStack customRecipeItemstack = new ItemStack(Material.CRAFTING_TABLE);
+ ItemMeta customRecipeMeta = customRecipeItemstack.getItemMeta();
+ assert customRecipeMeta != null;
+
+ customRecipeMeta.setDisplayName("§aCustom recipes");
+ customRecipeMeta.setLore(Collections.singletonList("§7Click here to open anvil custom recipe menu"));
+ customRecipeItemstack.setItemMeta(customRecipeMeta);
+
+ GuiItem customRecipeItem = GuiGlobalItems.goToGuiItem(customRecipeItemstack, CustomRecipeConfigGui.getInstance());
+ pane.bindItem('8', customRecipeItem);
+
+ // quit item
+ ItemStack quitItemstack = new ItemStack(Material.BARRIER);
+ ItemMeta quitMeta = quitItemstack.getItemMeta();
+ assert quitMeta != null;
+
+ quitMeta.setDisplayName("§cQuit");
+ quitItemstack.setItemMeta(quitMeta);
+
+ GuiItem quitItem = new GuiItem(quitItemstack, event -> {
+ event.setCancelled(true);
+ event.getWhoClicked().closeInventory();
+ }, CustomAnvil.instance);
+ pane.bindItem('Q', quitItem);
+
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectEnchantmentContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectEnchantmentContainer.java
new file mode 100644
index 0000000..1174209
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectEnchantmentContainer.java
@@ -0,0 +1,15 @@
+package xyz.alexcrea.cuanvil.gui.config;
+
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+
+import java.util.Set;
+
+public interface SelectEnchantmentContainer {
+
+ Set getSelectedEnchantments();
+
+ boolean setSelectedEnchantments(Set enchantments);
+
+ Set illegalEnchantments();
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectGroupContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectGroupContainer.java
new file mode 100644
index 0000000..49f8b3b
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectGroupContainer.java
@@ -0,0 +1,45 @@
+package xyz.alexcrea.cuanvil.gui.config;
+
+import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+public interface SelectGroupContainer {
+
+ Set getSelectedGroups();
+
+ boolean setSelectedGroups(Set groups);
+
+ Set illegalGroups();
+
+ static List getGroupLore(SelectGroupContainer container, String containerType, String groupAction){
+ // Prepare group lore
+ ArrayList groupLore = new ArrayList<>();
+ groupLore.add("§7Allow you to select a list of §3Groups §7that this " + containerType + " should " + groupAction);
+ Set grouos = container.getSelectedGroups();
+ if (grouos.isEmpty()) {
+ groupLore.add("§7There is no "+groupAction+"d group for this "+containerType+".");
+ } else {
+ groupLore.add("§7List of "+groupAction+"d groups for this "+containerType+":");
+ Iterator groupIterator = grouos.iterator();
+
+ boolean greaterThanMax = grouos.size() > 5;
+ int maxindex = (greaterThanMax ? 4 : grouos.size());
+ for (int i = 0; i < maxindex; i++) {
+ // format string like "- Melee Weapons"
+ String formattedName = CasedStringUtil.snakeToUpperSpacedCase(groupIterator.next().getName());
+ groupLore.add("§7- §3" + formattedName);
+
+ }
+ if (greaterThanMax) {
+ groupLore.add("§7And " + (grouos.size() - 4) + " more...");
+ }
+ }
+ return groupLore;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java
new file mode 100644
index 0000000..3756341
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/SelectMaterialContainer.java
@@ -0,0 +1,43 @@
+package xyz.alexcrea.cuanvil.gui.config;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.*;
+
+public interface SelectMaterialContainer {
+
+ Set getSelectedMaterials();
+
+ boolean setSelectedMaterials(Set materials);
+
+ Set illegalMaterials();
+
+ static List getMaterialLore(SelectMaterialContainer container, String containerType, String action){
+ // Prepare material lore
+ ArrayList groupLore = new ArrayList<>();
+ groupLore.add("§7Allow you to select a list of §ematerials §7that this " + containerType + " should " + action);
+ Set materialSet = container.getSelectedMaterials();
+ if (materialSet.isEmpty()) {
+ groupLore.add("§7There is no "+action+"d material for this "+containerType+".");
+ } else {
+ groupLore.add("§7List of "+action+"d materials for this "+containerType+":");
+ Iterator materialIterator = materialSet.iterator();
+
+ boolean greaterThanMax = materialSet.size() > 5;
+ int maxindex = (greaterThanMax ? 4 : materialSet.size());
+ for (int i = 0; i < maxindex; i++) {
+ // format string like "- Stone Sword"
+ String formattedName = CasedStringUtil.snakeToUpperSpacedCase(materialIterator.next().getKey().toLowerCase());
+ groupLore.add("§7- §e" + formattedName);
+
+ }
+ if (greaterThanMax) {
+ groupLore.add("§7And " + (materialSet.size() - 4) + " more...");
+ }
+ }
+ return groupLore;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/AbstractAskGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/AbstractAskGui.java
new file mode 100644
index 0000000..66dd936
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/AbstractAskGui.java
@@ -0,0 +1,42 @@
+package xyz.alexcrea.cuanvil.gui.config.ask;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+
+public abstract class AbstractAskGui extends ChestGui {
+
+ protected PatternPane pane;
+ AbstractAskGui(int rows, @NotNull String name,
+ Gui backOnCancel){
+ super(rows, name, CustomAnvil.instance);
+
+ Pattern pattern = getGuiPattern();
+ this.pane = new PatternPane(0, 0, pattern.getLength(), pattern.getHeight(), pattern);
+ addPane(this.pane);
+
+ this.pane.bindItem('0', GuiGlobalItems.backgroundItem());
+ this.pane.bindItem('B', new GuiItem(GuiSharedConstant.CANCEL_ITEM, GuiGlobalActions.openGuiAction(backOnCancel), CustomAnvil.instance));
+
+ }
+
+ /**
+ * Used to get the gui pattern.
+ * Reserved character are:
+ *
+ * - B: "cancel" button.
+ * - 0: default background item.
+ *
+ *
+ * @return The gui's pattern.
+ */
+ protected abstract Pattern getGuiPattern();
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java
new file mode 100644
index 0000000..5839663
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/ConfirmActionGui.java
@@ -0,0 +1,84 @@
+package xyz.alexcrea.cuanvil.gui.config.ask;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.util.MetricsUtil;
+
+import java.util.Arrays;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+
+public class ConfirmActionGui extends AbstractAskGui {
+
+ public ConfirmActionGui(@NotNull String title, String actionDescription,
+ Gui backOnCancel, Gui backOnConfirm, Supplier onConfirm,
+ boolean permanent) {
+ super(3, title, backOnCancel);
+
+ // Save item
+ this.pane.bindItem('S', new GuiItem(
+ (permanent ? GuiSharedConstant.CONFIRM_PERMANENT_ITEM : GuiSharedConstant.CONFIRM_ITEM),
+ event -> {
+ event.setCancelled(true);
+ HumanEntity player = event.getWhoClicked();
+
+ if (!player.hasPermission(CustomAnvil.editConfigPermission)) {
+ player.closeInventory();
+ player.sendMessage(GuiGlobalActions.NO_EDIT_PERM);
+ return;
+ }
+
+ boolean success;
+ try {
+ success = onConfirm.get();
+ } catch (Exception e) {
+ CustomAnvil.instance.getLogger().log(Level.WARNING, "Could not process confirmation supplier.", e);
+ MetricsUtil.INSTANCE.trackError(e);
+ success = false;
+ }
+
+ if (!success) {
+ event.getWhoClicked().sendMessage("§cAction could not be completed. ");
+ }
+ backOnConfirm.show(player);
+
+ }, CustomAnvil.instance));
+
+ // Info item
+ ItemStack infoItem = new ItemStack(Material.PAPER);
+ ItemMeta infoMeta = infoItem.getItemMeta();
+
+ infoMeta.setDisplayName("§eAre you sure ?");
+ if(actionDescription != null){
+ infoMeta.setLore(Arrays.asList(actionDescription.split("\n")));
+ }
+
+ infoItem.setItemMeta(infoMeta);
+
+ pane.bindItem('I', new GuiItem(infoItem, GuiGlobalActions.stayInPlace, CustomAnvil.instance));
+ }
+ public ConfirmActionGui(@NotNull String title, String actionDescription,
+ Gui backOnCancel, Gui backOnConfirm, Supplier onConfirm){
+ this(title, actionDescription, backOnCancel, backOnConfirm, onConfirm, true);
+ }
+
+
+ @Override
+ protected Pattern getGuiPattern() {
+ return new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "00B0I0S00",
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE
+ );
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java
new file mode 100644
index 0000000..66411bd
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/ask/SelectItemTypeGui.java
@@ -0,0 +1,99 @@
+package xyz.alexcrea.cuanvil.gui.config.ask;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.util.MaterialUtil;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+
+public class SelectItemTypeGui extends AbstractAskGui {
+
+ private ItemStack selectedItem;
+ public SelectItemTypeGui(@NotNull String title,
+ @NotNull String actionDescription,
+ @NotNull Gui backOnCancel,
+ @NotNull BiConsumer onSave,
+ boolean materialOnly) {
+ super(3, title, backOnCancel);
+ this.selectedItem = null;
+
+ // Save item
+ GuiItem confirmItem = new GuiItem(GuiSharedConstant.CONFIRM_ITEM, event -> {
+ event.setCancelled(true);
+ HumanEntity player = event.getWhoClicked();
+
+ if (!player.hasPermission(CustomAnvil.editConfigPermission)) {
+ player.closeInventory();
+ player.sendMessage(GuiGlobalActions.NO_EDIT_PERM);
+ return;
+ }
+
+ onSave.accept(this.selectedItem, player);
+
+ }, CustomAnvil.instance);
+ this.pane.bindItem('S', GuiGlobalItems.backgroundItem());
+
+ // Select item
+ ItemStack selectItem = setDisplayMeta(new ItemStack(Material.BARRIER), actionDescription);
+
+ AtomicReference selectGuiItem = new AtomicReference<>();
+ selectGuiItem.set(new GuiItem(selectItem, event -> {
+ event.setCancelled(true);
+
+ ItemStack cursor = event.getWhoClicked().getItemOnCursor();
+ if(MaterialUtil.INSTANCE.isAir(cursor)) return;
+
+ ItemStack finalItem;
+ if(materialOnly){
+ finalItem = setDisplayMeta(new ItemStack(cursor.getType()), actionDescription);
+ }else{
+ finalItem = cursor.clone();
+ }
+ this.selectedItem = finalItem.clone();
+
+ selectGuiItem.get().setItem(finalItem);
+ this.pane.bindItem('S', confirmItem);
+
+ update();
+ }, CustomAnvil.instance));
+
+ this.pane.bindItem('V', selectGuiItem.get());
+
+ // Temporary leave item
+ GuiItem temporaryLeave = GuiGlobalItems.temporaryCloseGuiToSelectItem(Material.YELLOW_STAINED_GLASS_PANE, this);
+
+ this.pane.bindItem('s', temporaryLeave);
+
+ }
+
+ private ItemStack setDisplayMeta(ItemStack item, String actionDescription){
+ ItemMeta meta = item.getItemMeta();
+
+ meta.setDisplayName("§ePlace an item here");
+ meta.setLore(Arrays.asList(actionDescription.split("\n")));
+
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ @Override
+ protected Pattern getGuiPattern() {
+ return new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "0000V000s",
+ "B0000000S"
+ );
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/AbstractEnchantConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/AbstractEnchantConfigGui.java
new file mode 100644
index 0000000..6bd7ea3
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/AbstractEnchantConfigGui.java
@@ -0,0 +1,117 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry;
+import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui;
+import xyz.alexcrea.cuanvil.gui.config.list.SettingGuiListConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.SettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Abstract Global Config gui for enchantment setting configuration.
+ *
+ * @param Type of the factory of the type of setting the gui should edit.
+ */
+public abstract class AbstractEnchantConfigGui extends SettingGuiListConfigGui{
+
+ /**
+ * Constructor for a gui displaying available enchantment to edit a enchantment setting.
+ *
+ * @param title Title of the gui.
+ */
+ protected AbstractEnchantConfigGui(String title) {
+ super(title);
+ }
+
+ @Override
+ public void updateGuiValues() { //TODO maybe optimise it.
+ reloadValues();
+ }
+
+ @Override
+ protected Collection getEveryDisplayableInstanceOfGeneric() {
+ return CAEnchantmentRegistry.getInstance().getNameSortedEnchantments();
+ }
+
+ @Override
+ protected Pattern getBackgroundPattern(){
+ return new Pattern(
+ GuiSharedConstant.UPPER_FILLER_FULL_PLANE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ "B11L1R111"
+ );
+ }
+
+ @Override
+ public void updateValueForGeneric(CAEnchantment generic, boolean shouldUpdate) {
+ updateValueForGeneric(generic, shouldUpdate, true);
+ }
+
+ public void updateValueForGeneric(CAEnchantment generic, boolean shouldUpdate, boolean prepareSorting) {
+ if(!prepareSorting) {
+ super.updateValueForGeneric(generic, shouldUpdate);
+ return;
+ }
+
+ if(!this.factoryMap.containsKey(generic)){
+ // We need to sort elements again
+ super.updateValueForGeneric(generic, false);
+
+ // Clear page then refill all of them
+ this.firstPage.clear();
+ this.pages.clear();
+ this.pages.add(this.firstPage);
+
+ for (CAEnchantment enchantment : getEveryDisplayableInstanceOfGeneric()) {
+ GuiItem item = this.guiItemMap.get(enchantment);
+
+ if(item == null) {
+ updateValueForGeneric(enchantment, false, false);
+ }else {
+ addToPage(item);
+ }
+
+ }
+
+ if(shouldUpdate) update();
+
+ }else{
+ super.updateValueForGeneric(generic, shouldUpdate);
+
+ }
+
+ }
+
+
+ // Unused methods
+ @Override
+ protected GuiItem prepareCreateNewItem() {
+ return null;
+ }
+
+ @Override
+ protected List getCreateItemLore() {
+ return Collections.emptyList();
+ }
+ @Override
+ protected Consumer getCreateClickConsumer() {
+ return null;
+ }
+
+ @Override
+ protected String createItemName() {
+ return null;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java
new file mode 100644
index 0000000..51936c7
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/BasicConfigGui.java
@@ -0,0 +1,371 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import io.delilaheve.util.ConfigOptions;
+import kotlin.ranges.IntRange;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil;
+import xyz.alexcrea.cuanvil.dependency.packet.PacketManager;
+import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui;
+import xyz.alexcrea.cuanvil.gui.config.MainConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.BoolSettingsGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.WorkPenaltyTypeSettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Global config to edit basic basic settings.
+ */
+public class BasicConfigGui extends ChestGui implements ValueUpdatableGui {
+
+ private static BasicConfigGui INSTANCE = null;
+
+ @Nullable
+ public static BasicConfigGui getInstance() {
+ return INSTANCE;
+ }
+
+ private final PacketManager packetManager;
+ /**
+ * Constructor of this Global gui for basic settings.
+ */
+ public BasicConfigGui(PacketManager packetManager) {
+ super(4, "§8Basic Config", CustomAnvil.instance);
+ if(INSTANCE == null) INSTANCE = this;
+
+ this.packetManager = packetManager;
+ init();
+ }
+
+ PatternPane pane;
+
+ /**
+ * Initialise Basic gui
+ */
+ private void init() {
+ Pattern pattern = new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "LT0IWS0cp",
+ "CR0U0r0hP",
+ "B00000000"
+ );
+ pane = new PatternPane(0, 0, 9, 4, pattern);
+ addPane(pane);
+
+ GuiGlobalItems.addBackItem(pane, MainConfigGui.getInstance());
+ GuiGlobalItems.addBackgroundItem(pane);
+
+ prepareValues();
+ updateGuiValues();
+ }
+
+ private BoolSettingsGui.BoolSettingFactory capAnvilCost; // L character
+ private GuiItem noCapRepairItem;
+ private IntSettingsGui.IntSettingFactory maxAnvilCost; // C character
+ private GuiItem noMaxCostItem;
+
+ private BoolSettingsGui.BoolSettingFactory removeAnvilCostLimit; // R character
+ private BoolSettingsGui.BoolSettingFactory replaceTooExpensive; // T character
+
+ private IntSettingsGui.IntSettingFactory itemRepairCost; // I character
+ private IntSettingsGui.IntSettingFactory unitRepairCost; // U character
+ private IntSettingsGui.IntSettingFactory itemRenameCost; // r character
+ private IntSettingsGui.IntSettingFactory sacrificeIllegalEnchantCost; // S character
+
+ private BoolSettingsGui.BoolSettingFactory allowColorCode; // c character
+ private BoolSettingsGui.BoolSettingFactory allowHexColor; // h character
+
+ private BoolSettingsGui.BoolSettingFactory permissionNeededForColor; // p character
+ private GuiItem noPermissionNeededItem;
+ private IntSettingsGui.IntSettingFactory useOfColorCost; // P character
+ private GuiItem noColorCostItem;
+
+ /**
+ * Prepare basic gui displayed items factory and static items..
+ */
+ protected void prepareValues() {
+ // cap anvil cost
+ this.capAnvilCost = new BoolSettingsGui.BoolSettingFactory("§8Cap Anvil Cost ?", this,
+ ConfigHolder.DEFAULT_CONFIG,
+ ConfigOptions.CAP_ANVIL_COST, ConfigOptions.DEFAULT_CAP_ANVIL_COST,
+ "§7All anvil cost will be capped to §aMax Anvil Cost§7 if enabled.",
+ "§7In other words:",
+ "§7For any anvil cost greater than §aMax Anvil Cost§7, Cost will be set to §aMax Anvil Cost§7.");
+ // cap anvil cost not needed
+ ItemStack item = new ItemStack(Material.BARRIER);
+ ItemMeta meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§cCap Anvil Cost ?");
+ meta.setLore(Collections.singletonList("§7This config only work if §cLimit Repair Cost§7 is disabled."));
+ item.setItemMeta(meta);
+ this.noCapRepairItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance);
+
+
+ // repair cost item
+ IntRange range = ConfigOptions.MAX_ANVIL_COST_RANGE;
+ this.maxAnvilCost = new IntSettingsGui.IntSettingFactory("§8Max Anvil Cost", this,
+ ConfigOptions.MAX_ANVIL_COST, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7Max cost the Anvil can get to.",
+ "§7Valid values include §e0 §7to §e1000§7.",
+ "§7Cost will be displayed as §cToo Expensive§7:",
+ "§7- If Cost is above §e39",
+ "§7- And §eReplace Too Expensive§7 is disabled"
+ ),
+ range.getFirst(), range.getLast(),
+ ConfigOptions.DEFAULT_MAX_ANVIL_COST,
+ 1, 5, 10);
+ // max anvil cost not needed
+ item = new ItemStack(Material.BARRIER);
+ meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§cMax Anvil Cost");
+ meta.setLore(Collections.singletonList("§7This config only work if §cLimit Repair Cost§7 is disabled."));
+ item.setItemMeta(meta);
+ this.noMaxCostItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance);
+
+
+ // remove repair limit item
+ this.removeAnvilCostLimit = new BoolSettingsGui.BoolSettingFactory("§8Remove Anvil Cost Limit ?", this,
+ ConfigHolder.DEFAULT_CONFIG,
+ ConfigOptions.REMOVE_ANVIL_COST_LIMIT, ConfigOptions.DEFAULT_REMOVE_ANVIL_COST_LIMIT,
+ "§7Whether the anvil's cost limit should be removed entirely.",
+ "§7The anvil will still visually display §cToo Expensive§7 if §eReplace Too Expensive§7 is disabled.",
+ "§7However, the action will be completable if xp requirement is meet.");
+
+ // replace too expensive item
+ this.replaceTooExpensive = new BoolSettingsGui.BoolSettingFactory("§8Replace Too Expensive ?", this,
+ ConfigHolder.DEFAULT_CONFIG,
+ ConfigOptions.REPLACE_TOO_EXPENSIVE, ConfigOptions.DEFAULT_REPLACE_TOO_EXPENSIVE,
+ getReplaceToExpensiveLore());
+
+ // ------------
+ // Cost config
+ // ------------
+
+ // item repair cost
+ range = ConfigOptions.REPAIR_COST_RANGE;
+ this.itemRepairCost = new IntSettingsGui.IntSettingFactory("§8Item Repair Cost", this,
+ ConfigOptions.ITEM_REPAIR_COST, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7XP Level amount added to the anvil when the item",
+ "§7is repaired by another item of the same type."
+ ),
+ range.getFirst(), range.getLast(),
+ ConfigOptions.DEFAULT_ITEM_REPAIR_COST,
+ 1, 5, 10, 50, 100);
+
+ // unit repair cost
+ this.unitRepairCost = new IntSettingsGui.IntSettingFactory("§8Unit Repair Cost", this,
+ ConfigOptions.UNIT_REPAIR_COST, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7XP Level amount added to the anvil when the item is repaired by an §eunit§7.",
+ "§7For example: a Diamond on a Diamond Sword.",
+ "§7What's considered unit for what can be edited on the unit repair configuration."
+ ),
+ range.getFirst(), range.getLast(),
+ ConfigOptions.DEFAULT_UNIT_REPAIR_COST,
+ 1, 5, 10, 50, 100);
+
+ // item rename cost
+ range = ConfigOptions.ITEM_RENAME_COST_RANGE;
+ this.itemRenameCost = new IntSettingsGui.IntSettingFactory("§8Rename Cost", this,
+ ConfigOptions.ITEM_RENAME_COST, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7XP Level amount added to the anvil when the item is renamed."
+ ),
+ range.getFirst(), range.getLast(),
+ ConfigOptions.DEFAULT_ITEM_RENAME_COST,
+ 1, 5, 10, 50, 100);
+
+ // sacrifice illegal enchant cost
+ range = ConfigOptions.SACRIFICE_ILLEGAL_COST_RANGE;
+ this.sacrificeIllegalEnchantCost = new IntSettingsGui.IntSettingFactory("§8Sacrifice Illegal Enchant Cost", this,
+ ConfigOptions.SACRIFICE_ILLEGAL_COST, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7XP Level amount added to the anvil when a sacrifice enchantment",
+ "§7conflict With one of the left item enchantment"
+ ),
+ range.getFirst(), range.getLast(),
+ ConfigOptions.DEFAULT_SACRIFICE_ILLEGAL_COST,
+ 1, 5, 10, 50, 100);
+
+ // -------------
+ // Color config
+ // -------------
+
+ // Allow us of color code
+ this.allowColorCode = new BoolSettingsGui.BoolSettingFactory("§8Allow Use Of Color Code ?", this,
+ ConfigHolder.DEFAULT_CONFIG,
+ ConfigOptions.ALLOW_COLOR_CODE, ConfigOptions.DEFAULT_ALLOW_COLOR_CODE,
+ "§7Whether players can use color code.",
+ "§7Color code a formatted like §a&a§7 and is used in the rename field of the anvil.",
+ "§7Player may need permission to use color code if §ePlayer need permission to use color§7 is enabled.");
+
+ // Allow us of hexadecimal color
+ this.allowHexColor = new BoolSettingsGui.BoolSettingFactory("§8Allow Use Of Hexadecimal Color ?", this,
+ ConfigHolder.DEFAULT_CONFIG,
+ ConfigOptions.ALLOW_HEXADECIMAL_COLOR, ConfigOptions.DEFAULT_ALLOW_HEXADECIMAL_COLOR,
+ "§7Whether players can use hexadecimal color.",
+ "§7Color code a formatted like §2#012345 §7and is used in the rename field of the anvil.",
+ "§7Player may need permission to use color code if §ePermission Needed For Color§7 is enabled.");
+
+ // Permission needed for color
+ this.permissionNeededForColor = new BoolSettingsGui.BoolSettingFactory("§8Need Permission To Use Color ?", this,
+ ConfigHolder.DEFAULT_CONFIG,
+ ConfigOptions.PERMISSION_NEEDED_FOR_COLOR, ConfigOptions.DEFAULT_PERMISSION_NEEDED_FOR_COLOR,
+ "§7Whether players should have permission to be able to use colors.",
+ "§7Give player §eca.color.code§7 Permission to allow use of color code.",
+ "§7Give player §eca.color.hex§7 Permission to allow use of hexadecimal color.");
+
+ // Permission needed for color not necessary
+ item = new ItemStack(Material.BARRIER);
+ meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§cNeed Permission To Use Color ?");
+ meta.setLore(Arrays.asList("§7This config can do something only if one of the following config is enabled:",
+ "§7- §aAllow Use Of Color Code",
+ "§7- §aAllow Use Of Hexadecimal Color"));
+ item.setItemMeta(meta);
+ this.noPermissionNeededItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance);
+
+ // Cost of using color
+ range = ConfigOptions.USE_OF_COLOR_COST_RANGE;
+ this.useOfColorCost = new IntSettingsGui.IntSettingFactory("§8Cost Of Using Color", this,
+ ConfigOptions.USE_OF_COLOR_COST, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7XP level cost when using color code or hexadecimal color using the anvil.",
+ "§7conflict With one of the left item enchantment"
+ ),
+ range.getFirst(), range.getLast(),
+ ConfigOptions.DEFAULT_USE_OF_COLOR_COST,
+ 1, 5, 10, 50, 100);
+
+ // Permission needed for color not necessary
+ item = new ItemStack(Material.BARRIER);
+ meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§cCost Of Using Color");
+ meta.setLore(Arrays.asList("§7This config can do something only if one of the following config is enabled:",
+ "§7- §aAllow Use Of Color Code",
+ "§7- §aAllow Use Of Hexadecimal Color"));
+ item.setItemMeta(meta);
+ this.noColorCostItem = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance);
+
+ }
+
+ @NotNull
+ private String[] getReplaceToExpensiveLore() {
+ ArrayList lore = new ArrayList<>();
+ lore.add("§7Whenever anvil cost is above §e39§7 should display the true price and not §cToo Expensive§7.");
+ lore.add("§7However, when bypassing §cToo Expensive§7, anvil price will be displayed as §aGreen§7.");
+ lore.add("§7Even if cost is displayed as §aGreen§7:");
+ lore.add("§7If the player do not have the required xp level, the action will not be completable.");
+
+ if(!this.packetManager.getCanSetInstantBuild()){
+ lore.add("");
+ lore.add("§4/!\\§cCaution§4/!\\ §cYou need ProtocoLib installed and working or a paper server.");
+ lore.add("§cCurrently ProtocoLib is not detected.");
+ }
+
+ String[] loreAsArray = new String[lore.size()];
+ return lore.toArray(loreAsArray);
+ }
+
+ @Override
+ public void updateGuiValues() {
+ // limit and cap anvil cost item
+ GuiItem capAnvilCostItem;
+ GuiItem maxAnvilCostItem;
+ if (!this.removeAnvilCostLimit.getConfiguredValue()) {
+ capAnvilCostItem = this.capAnvilCost.getItem("Cap Anvil Cost");
+ maxAnvilCostItem = this.maxAnvilCost.getItem(Material.EXPERIENCE_BOTTLE, "Max Anvil Cost");
+ } else {
+ capAnvilCostItem = this.noCapRepairItem;
+ maxAnvilCostItem = this.noMaxCostItem;
+ }
+
+ pane.bindItem('L', capAnvilCostItem);
+ pane.bindItem('C', maxAnvilCostItem);
+
+ // remove repair limit item
+ GuiItem removeRepairLimitItem = this.removeAnvilCostLimit.getItem("Remove Anvil Cost Limit");
+ pane.bindItem('R', removeRepairLimitItem);
+
+ // replace too expensive item
+ GuiItem replaceToExpensiveItem = this.replaceTooExpensive.getItem();
+ pane.bindItem('T', replaceToExpensiveItem);
+
+
+ // item repair cost
+ GuiItem itemRepairCostItem = this.itemRepairCost.getItem(Material.ANVIL);
+ pane.bindItem('I', itemRepairCostItem);
+
+ // unit repair cost
+ GuiItem unitRepairCostItem = this.unitRepairCost.getItem(Material.DIAMOND);
+ pane.bindItem('U', unitRepairCostItem);
+
+ // item rename cost
+ GuiItem itemRenameCostItem = this.itemRenameCost.getItem(Material.NAME_TAG);
+ pane.bindItem('r', itemRenameCostItem);
+
+ // sacrifice illegal enchant cost
+ GuiItem illegalCostItem = this.sacrificeIllegalEnchantCost.getItem(Material.ENCHANTED_BOOK);
+ pane.bindItem('S', illegalCostItem);
+
+ // work penalty type
+ GuiItem workPenaltyType = WorkPenaltyTypeSettingGui.getDisplayItem(this, Material.DAMAGED_ANVIL, "§aWork Penalty Type");
+ pane.bindItem('W', workPenaltyType);
+
+ // allow color code
+ GuiItem allowColorCodeItem = this.allowColorCode.getItem();
+ pane.bindItem('c', allowColorCodeItem);
+
+ // allow hex color
+ GuiItem allowHexColorItem = this.allowHexColor.getItem();
+ pane.bindItem('h', allowHexColorItem);
+
+ // True if player could place color
+ if(ConfigOptions.INSTANCE.getRenameColorPossible()){
+ // use permission for color
+ GuiItem permissionNeededItem = this.permissionNeededForColor.getItem();
+ pane.bindItem('p', permissionNeededItem);
+
+ // using color cost
+ GuiItem useColorCostItem = this.useOfColorCost.getItem(Material.EXPERIENCE_BOTTLE, "Use color");
+ pane.bindItem('P', useColorCostItem);
+ }else{
+ pane.bindItem('p', this.noPermissionNeededItem);
+ pane.bindItem('P', this.noColorCostItem);
+ }
+
+
+ update();
+ }
+
+ @Override
+ public Gui getConnectedGui() {
+ return this;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/CustomRecipeConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/CustomRecipeConfigGui.java
new file mode 100644
index 0000000..e21ad75
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/CustomRecipeConfigGui.java
@@ -0,0 +1,119 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.elements.CustomRecipeSubSettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class CustomRecipeConfigGui extends MappedGuiListConfigGui> {
+
+ private static CustomRecipeConfigGui INSTANCE = new CustomRecipeConfigGui();
+
+ @Nullable
+ public static CustomRecipeConfigGui getCurrentInstance() {
+ return INSTANCE;
+ }
+
+ @NotNull
+ public static CustomRecipeConfigGui getInstance() {
+ if (INSTANCE == null) INSTANCE = new CustomRecipeConfigGui();
+
+ return INSTANCE;
+ }
+
+ private CustomRecipeConfigGui() {
+ super("Custom Recipe Config");
+
+ init();
+ }
+
+ @Override
+ protected ItemStack createItemForGeneric(AnvilCustomRecipe recipe) {
+ // Get base item to display
+ ItemStack craftResultItem = recipe.getResultItem();
+ ItemStack displayedItem;
+ if (craftResultItem == null) {
+ displayedItem = new ItemStack(Material.BARRIER);
+ } else {
+ displayedItem = craftResultItem.clone();
+ }
+
+ // edit displayed item
+ ItemMeta meta = displayedItem.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§e" + CasedStringUtil.snakeToUpperSpacedCase(recipe.toString()) + " §fCustom recipe");
+ meta.addItemFlags(ItemFlag.values());
+
+ meta.setLore(getRecipeLore(recipe));
+
+ displayedItem.setItemMeta(meta);
+ return displayedItem;
+ }
+
+ private static @NotNull ArrayList getRecipeLore(AnvilCustomRecipe recipe) {
+ boolean shouldWork = recipe.validate();
+
+ ArrayList lore = new ArrayList<>();
+ lore.add("§7Is valid: §" + (shouldWork ? "aYes" : "cNo"));
+ lore.add("§7Exact count: §" + (recipe.getExactCount() ? "aYes" : "cNo"));
+ lore.add("§7Recipe Level Cost: §e" + recipe.getLevelCostPerCraft());
+ lore.add("§7Recipe Linear Xp Cost: §e" + recipe.getXpCostPerCraft());
+ if (recipe.getXpCostPerCraft() != 0) {
+ lore.add("§7Exact Linear xp remove: §" + (recipe.getRemoveExactLinearXp() ? "aYes" : "cNo"));
+ }
+ return lore;
+ }
+
+ @Override
+ protected LazyElement newInstanceOfGui(AnvilCustomRecipe generic, GuiItem item) {
+ return new LazyElement<>(item, () -> new CustomRecipeSubSettingGui(this, generic));
+ }
+
+ @Override
+ protected String genericDisplayedName() {
+ return "custom recipe";
+ }
+
+ @Override
+ protected AnvilCustomRecipe createAndSaveNewEmptyGeneric(String name) {
+ // Create new empty conflict and display it to the admin
+ AnvilCustomRecipe recipe = new AnvilCustomRecipe(
+ name,
+ AnvilCustomRecipe.DEFAULT_EXACT_COUNT_CONFIG,
+
+ AnvilCustomRecipe.DEFAULT_XP_LEVEL_COST_CONFIG,
+ AnvilCustomRecipe.DEFAULT_LINEAR_XP_COST_CONFIG,
+ AnvilCustomRecipe.DEFAULT_REMOVE_EXACT_XP_CONFIG,
+
+ AnvilCustomRecipe.Companion.getDEFAULT_LEFT_ITEM_CONFIG(),
+ AnvilCustomRecipe.Companion.getDEFAULT_RIGHT_ITEM_CONFIG(),
+ AnvilCustomRecipe.Companion.getDEFAULT_RESULT_ITEM_CONFIG());
+
+ ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().cleanAddNew(recipe);
+
+ // Save recipe to file
+ recipe.saveToFile(GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE, GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+
+ return recipe;
+ }
+
+
+ @Override
+ protected Collection getEveryDisplayableInstanceOfGeneric() {
+ return ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager().getRecipeList();
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantConflictGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantConflictGui.java
new file mode 100644
index 0000000..912e6cb
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantConflictGui.java
@@ -0,0 +1,104 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.group.EnchantConflictGroup;
+import xyz.alexcrea.cuanvil.group.IncludeGroup;
+import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.elements.EnchantConflictSubSettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+public class EnchantConflictGui extends MappedGuiListConfigGui> {
+
+ private static EnchantConflictGui INSTANCE;
+
+ @Nullable
+ public static EnchantConflictGui getCurrentInstance(){
+ return INSTANCE;
+ }
+
+ @NotNull
+ public static EnchantConflictGui getInstance(){
+ if(INSTANCE == null) INSTANCE = new EnchantConflictGui();
+
+ return INSTANCE;
+ }
+
+
+ private EnchantConflictGui() {
+ super( "Conflict Config");
+
+ init();
+ }
+
+ @Override
+ protected EnchantConflictGroup createAndSaveNewEmptyGeneric(String name){
+ // Create new empty conflict and display it to the admin
+ EnchantConflictGroup conflict = new EnchantConflictGroup(
+ name,
+ new IncludeGroup("new_group"),
+ 0);
+
+ ConfigHolder.CONFLICT_HOLDER.getConflictManager().addConflict(conflict);
+
+ // save empty conflict in config
+ String[] emptyStringArray = new String[0];
+
+ FileConfiguration config = ConfigHolder.CONFLICT_HOLDER.getConfig();
+ config.set(name + ".enchantments", emptyStringArray);
+ config.set(name + ".notAffectedGroups", emptyStringArray);
+ config.set(name + ".maxEnchantmentBeforeConflict", 0);
+
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return conflict;
+ }
+
+ @Override
+ public ItemStack createItemForGeneric(EnchantConflictGroup conflict) {
+ ItemStack item = new ItemStack(conflict.getRepresentativeMaterial());
+
+ ItemMeta meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.addItemFlags(ItemFlag.values());
+ meta.setDisplayName("§e" + CasedStringUtil.snakeToUpperSpacedCase(conflict.toString()) + " §fConflict");
+ meta.setLore(Arrays.asList(
+ "§7Enchantment count: §e" + conflict.getEnchants().size(),
+ "§7Group count: §e" + conflict.getCantConflictGroup().getGroups().size(),
+ "§7Min enchantments count: §e" + conflict.getMinBeforeBlock()
+ ));
+
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ @Override
+ protected LazyElement newInstanceOfGui(EnchantConflictGroup conflict, GuiItem item) {
+ return new LazyElement<>(item, () -> new EnchantConflictSubSettingGui(this, conflict));
+ }
+
+ @Override
+ protected String genericDisplayedName() {
+ return "conflict";
+ }
+
+ @Override
+ protected Collection getEveryDisplayableInstanceOfGeneric() {
+ return ConfigHolder.CONFLICT_HOLDER.getConflictManager().getConflictList();
+ }
+
+}
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
new file mode 100644
index 0000000..a614536
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java
@@ -0,0 +1,91 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentProperties;
+import xyz.alexcrea.cuanvil.enchant.EnchantmentRarity;
+import xyz.alexcrea.cuanvil.gui.config.settings.EnchantCostSettingsGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Global Config gui for enchantment cost settings.
+ */
+public class EnchantCostConfigGui extends AbstractEnchantConfigGui {
+
+ private static final String SECTION_NAME = "enchant_values";
+
+ private static EnchantCostConfigGui INSTANCE = null;
+
+ @Nullable
+ public static EnchantCostConfigGui getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Constructor of this Global gui for enchantment cost settings.
+ */
+ public EnchantCostConfigGui() {
+ super("§8Enchantment Level Cost");
+ if(INSTANCE == null) INSTANCE = this;
+
+ init();
+ }
+
+ @Override
+ public EnchantCostSettingsGui.EnchantCostSettingFactory createFactory(CAEnchantment enchant) {
+ String key = enchant.getKey().toString().toLowerCase(Locale.ENGLISH);
+ String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key.replace(":", "_"));
+
+ return new EnchantCostSettingsGui.EnchantCostSettingFactory(prettyKey + " Cost", this,
+ SECTION_NAME + '.' + key, ConfigHolder.DEFAULT_CONFIG,
+ Arrays.asList(
+ "§7How many level should " + prettyKey,
+ "§7cost when applied by book or by another item."
+ ),
+ enchant, 0, 255,
+ 1, 10, 50);
+ }
+
+ @Override
+ public GuiItem itemFromFactory(CAEnchantment enchantment, EnchantCostSettingsGui.EnchantCostSettingFactory factory) {
+ // Get item properties
+ int itemCost = factory.getConfiguredValue();
+ int bookCost = factory.getConfiguredBookValue();
+ String itemName = "§a" + factory.getTitle();
+ // Create item
+ ItemStack item = new ItemStack(Material.ENCHANTED_BOOK);
+ ItemMeta itemMeta = item.getItemMeta();
+ assert itemMeta != null;
+
+ // Prepare lore
+ List lore = new ArrayList<>();
+ lore.add("§7Item Cost: §e" + itemCost);
+ lore.add("§7Book Cost: §e" + bookCost);
+
+ List displayLore = factory.getDisplayLore();
+ if(!displayLore.isEmpty()){
+ lore.add("");
+ lore.addAll(displayLore);
+ }
+
+ // Edit name and lore
+ itemMeta.setDisplayName(itemName);
+ itemMeta.setLore(lore);
+
+ item.setItemMeta(itemMeta);
+
+ return GuiGlobalItems.openSettingGuiItem(item, factory);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java
new file mode 100644
index 0000000..e9edbeb
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantLimitConfigGui.java
@@ -0,0 +1,83 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import io.delilaheve.util.ConfigOptions;
+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.Collections;
+import java.util.Locale;
+
+/**
+ * Global Config gui for enchantment level limit settings.
+ */
+public class EnchantLimitConfigGui extends AbstractEnchantConfigGui {
+
+ private static final String SECTION_NAME = ConfigOptions.ENCHANT_LIMIT_ROOT;
+
+ private static EnchantLimitConfigGui INSTANCE = null;
+
+ @Nullable
+ public static EnchantLimitConfigGui getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Constructor of this Global gui for enchantment level limit settings.
+ */
+ public EnchantLimitConfigGui() {
+ super("§8Enchantment Level Limit");
+ if(INSTANCE == null) INSTANCE = this;
+
+ init();
+ }
+
+ @Override
+ public IntSettingsGui.IntSettingFactory createFactory(CAEnchantment enchant) {
+ String key = enchant.getKey().toString().toLowerCase(Locale.ROOT);
+ String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key.replace(":", "_"));
+
+ var defaultValue = enchant.defaultMaxLevel();
+
+ return new IntSettingsGui.IntSettingFactory(prettyKey + " Limit", this,
+ SECTION_NAME + '.' + key, ConfigHolder.DEFAULT_CONFIG,
+ Collections.singletonList(
+ "§7Maximum applied level of " + prettyKey
+ ),
+ -1, 255, -1,
+ 1, 5, 10, 50, 100){
+
+ @Override
+ public int getConfiguredValue() {
+ var value = ConfigOptions.INSTANCE.rawEnchantLimit(enchant);
+ return Math.min(value, ConfigOptions.ENCHANT_LIMIT);
+ }
+
+ @Override
+ public String valueDisplayName(IntSettingsGui.ValueDisplayType type, int value) {
+
+ if(value < 0) {
+ return switch (type) {
+ case CURRENT -> "Default (" + defaultValue + ")";
+ case RESET -> String.valueOf(defaultValue);
+ default -> "Default";
+ };
+
+ }
+ else return super.valueDisplayName(type, value);
+ }
+ };
+ }
+
+ @Override
+ public GuiItem itemFromFactory(CAEnchantment enchantment, IntSettingsGui.IntSettingFactory inventoryFactory) {
+ return inventoryFactory.getItem(
+ Material.ENCHANTED_BOOK,
+ inventoryFactory.getTitle());
+ }
+
+}
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..0d391e7
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantMergeLimitConfigGui.java
@@ -0,0 +1,67 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import io.delilaheve.util.ConfigOptions;
+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().toString().toLowerCase(Locale.ROOT);
+ String prettyKey = CasedStringUtil.snakeToUpperSpacedCase(key.replace(":", "_"));
+
+ 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 int getConfiguredValue() {
+ return ConfigOptions.INSTANCE.maxBeforeMergeDisabled(enchant);
+ }
+ };
+ }
+
+ @Override
+ public GuiItem itemFromFactory(CAEnchantment enchantment, IntSettingsGui.IntSettingFactory inventoryFactory) {
+ return inventoryFactory.getItem(
+ Material.ENCHANTED_BOOK,
+ inventoryFactory.getTitle());
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/GroupConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/GroupConfigGui.java
new file mode 100644
index 0000000..8e20751
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/GroupConfigGui.java
@@ -0,0 +1,97 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup;
+import xyz.alexcrea.cuanvil.group.GroupType;
+import xyz.alexcrea.cuanvil.group.IncludeGroup;
+import xyz.alexcrea.cuanvil.group.ItemGroupManager;
+import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.elements.GroupConfigSubSettingGui;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+import xyz.alexcrea.cuanvil.util.LazyValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class GroupConfigGui extends MappedGuiListConfigGui> {
+
+ private static GroupConfigGui INSTANCE;
+
+ @Nullable
+ public static GroupConfigGui getCurrentInstance(){
+ return INSTANCE;
+ }
+
+ @NotNull
+ public static GroupConfigGui getInstance(){
+ if(INSTANCE == null) INSTANCE = new GroupConfigGui();
+
+ return INSTANCE;
+ }
+
+ public GroupConfigGui() {
+ super("Group Config");
+
+ init();
+ }
+
+ @Override
+ protected ItemStack createItemForGeneric(IncludeGroup group) {
+ ItemStack item = new ItemStack(group.getRepresentativeMaterial());
+ ItemMeta meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.addItemFlags(ItemFlag.values());
+ meta.setDisplayName("§e" + CasedStringUtil.snakeToUpperSpacedCase(group.getName())+ " §fGroup");
+ meta.setLore(Arrays.asList(
+ "§7Number of selected groups : " + group.getGroups().size(),
+ "§7Number of included material : " + group.getNonGroupInheritedMaterials().size(),
+ "",
+ "§7Total number of included material "+group.getMaterials().size()));
+
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ @Override
+ protected Collection getEveryDisplayableInstanceOfGeneric() {
+ ArrayList includeGroups = new ArrayList<>();
+
+ for (AbstractMaterialGroup group : ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values()) {
+ if(group instanceof IncludeGroup){
+ includeGroups.add((IncludeGroup) group);
+ }
+ }
+ return includeGroups;
+ }
+
+ @Override
+ protected LazyElement newInstanceOfGui(IncludeGroup group, GuiItem item) {
+ return new LazyElement<>(item, () -> new GroupConfigSubSettingGui(this, group));
+ }
+
+ @Override
+ protected String genericDisplayedName() {
+ return "material group";
+ }
+
+ @Override
+ protected IncludeGroup createAndSaveNewEmptyGeneric(String name) {
+ ItemGroupManager manager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager();
+ if(manager.getGroupMap().containsKey(name)) return null;
+
+ ConfigurationSection config = ConfigHolder.ITEM_GROUP_HOLDER.getConfig();
+ config.set(name+"."+ItemGroupManager.GROUP_TYPE_PATH, GroupType.INCLUDE.getGroupID());
+
+ return (IncludeGroup) manager.createGroup(config, name);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/UnitRepairConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/UnitRepairConfigGui.java
new file mode 100644
index 0000000..0e366ae
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/UnitRepairConfigGui.java
@@ -0,0 +1,148 @@
+package xyz.alexcrea.cuanvil.gui.config.global;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.gui.config.ask.SelectItemTypeGui;
+import xyz.alexcrea.cuanvil.gui.config.list.MappedGuiListConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.UnitRepairElementListGui;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class UnitRepairConfigGui extends
+ MappedGuiListConfigGui> {
+
+ private static UnitRepairConfigGui INSTANCE;
+
+ @Nullable
+ public static UnitRepairConfigGui getCurrentInstance(){
+ return INSTANCE;
+ }
+
+ @NotNull
+ public static UnitRepairConfigGui getInstance(){
+ if(INSTANCE == null) INSTANCE = new UnitRepairConfigGui();
+
+ return INSTANCE;
+ }
+
+ private UnitRepairConfigGui() {
+ super("Unit Repair Config");
+
+ init();
+ }
+
+ @Override
+ protected LazyElement newInstanceOfGui(Material material, GuiItem item) {
+ return new LazyElement<>(item, () -> {
+ UnitRepairElementListGui element = new UnitRepairElementListGui(material, this);
+ element.init();
+ return element;
+ });
+ }
+
+ @Override
+ protected ItemStack createItemForGeneric(Material material) {
+ ConfigurationSection materialSection = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().getConfigurationSection(material.name().toLowerCase());
+ String materialName = CasedStringUtil.snakeToUpperSpacedCase(material.name().toLowerCase());
+
+ if(material.isAir()){
+ material = Material.BARRIER;
+ }
+
+ int reparableItemCount = materialSection == null ? 0 : materialSection.getKeys(false).size(); // Probably an expensive call but... why not
+
+ ItemStack item = new ItemStack(material);
+ ItemMeta meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§eRepaired by " +materialName);
+ meta.setLore(Arrays.asList(
+ "§7There is currently §e" +reparableItemCount+ " §7reparable item with "+materialName,
+ "§7Click here to open the menu to edit reparable item by " + materialName
+ ));
+
+ item.setItemMeta(meta);
+
+ return item;
+ }
+
+ @Override
+ protected Collection getEveryDisplayableInstanceOfGeneric() {
+ ArrayList materials = new ArrayList<>();
+
+ for (String matName : ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().getKeys(false)) {
+ Material mat = Material.getMaterial(matName.toUpperCase());
+ if(mat != null){
+ materials.add(mat);
+ }
+ }
+ return materials;
+ }
+
+ @Override
+ protected GuiItem prepareCreateNewItem() {
+ // Create new conflict item
+ ItemStack createItem = new ItemStack(Material.PAPER);
+ ItemMeta createMeta = createItem.getItemMeta();
+ assert createMeta != null;
+
+ createMeta.setDisplayName("§aSelect a new unit material");
+ createMeta.setLore(Arrays.asList(
+ "§7Select a new unit material to be used.",
+ "§7You will be asked the material to use."
+ ));
+
+ createItem.setItemMeta(createMeta);
+
+ return new GuiItem(createItem, clickEvent -> {
+ clickEvent.setCancelled(true);
+
+ new SelectItemTypeGui(
+ "Select unit repair item.",
+ "§7Click here with an item to set the item\n" +
+ "§7You like to be an unit repair item",
+ this,
+ (itemStack, player) -> {
+ Material type = itemStack.getType();
+ // Add new material
+ updateValueForGeneric(type, true);
+
+ // Display material edit setting
+ this.elementGuiMap.get(type).get().getMappedGui().show(player);
+ },
+ true
+ ).show(clickEvent.getWhoClicked());
+ }, CustomAnvil.instance);
+ }
+
+ @NotNull
+ public LazyElement getInstanceOrCreate(Material mat){
+ LazyElement element = this.elementGuiMap.get(mat);
+ if(element == null){
+ updateValueForGeneric(mat, false);
+
+ element = this.elementGuiMap.get(mat);
+ }
+
+ return element;
+ }
+
+ @Override // Not used in this implementation.
+ protected String genericDisplayedName() {
+ return "this function Should not be used.";
+ }
+ @Override // Not used in this implementation.
+ protected Material createAndSaveNewEmptyGeneric(String name) {
+ return null;
+ }
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/ElementListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/ElementListConfigGui.java
new file mode 100644
index 0000000..f3cc0b4
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/ElementListConfigGui.java
@@ -0,0 +1,318 @@
+package xyz.alexcrea.cuanvil.gui.config.list;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import com.github.stefvanschie.inventoryframework.pane.Orientable;
+import com.github.stefvanschie.inventoryframework.pane.OutlinePane;
+import com.github.stefvanschie.inventoryframework.pane.Pane;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.UUID;
+
+public abstract class ElementListConfigGui< T > extends ChestGui implements ValueUpdatableGui {
+
+ private final String namePrefix;
+
+ protected PatternPane backgroundPane;
+
+ public static final int LIST_FILLER_START_X = 1;
+ public static final int LIST_FILLER_START_Y = 1;
+ public static final int LIST_FILLER_LENGTH = 7;
+ public static final int LIST_FILLER_HEIGHT = 4;
+
+ protected ElementListConfigGui(@NotNull String title, Gui parent) {
+ super(6, title, CustomAnvil.instance);
+ this.namePrefix = title;
+
+ // Back item panel
+ Pattern pattern = getBackgroundPattern();
+ this.backgroundPane = new PatternPane(0, 0, 9, 6, Pane.Priority.LOW, pattern);
+ GuiGlobalItems.addBackItem(this.backgroundPane, parent);
+
+ }
+
+ protected Pattern getBackgroundPattern(){
+ return new Pattern(
+ GuiSharedConstant.UPPER_FILLER_FULL_PLANE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ GuiSharedConstant.EMPTY_FILLER_FULL_LINE,
+ "B11L1R11C"
+ );
+ }
+
+ protected OutlinePane firstPage;
+ protected ArrayList pages;
+ protected HashMap pageMap;
+
+ public void init() { // Why I'm using an init function ? //TODO determine why is it using a init function and not used on constructor.
+ GuiGlobalItems.addBackgroundItem(this.backgroundPane);
+ this.backgroundPane.bindItem('1', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM);
+ addPane(this.backgroundPane);
+
+ // Page init
+ this.pages = new ArrayList<>();
+ this.pageMap = new HashMap<>();
+
+ // enchant item panel
+ this.firstPage = createEmptyPage();
+ this.pages.add(this.firstPage);
+
+ prepareStaticValues();
+ reloadValues();
+ }
+
+ protected GuiItem goLeftItem;
+ protected GuiItem goRightItem;
+
+ protected void prepareStaticValues(){
+ // Left item creation for consumer & bind
+ this.goLeftItem = new GuiItem(new ItemStack(Material.RED_STAINED_GLASS_PANE), event -> {
+ HumanEntity viewer = event.getWhoClicked();
+ UUID playerUUID = viewer.getUniqueId();
+ int page = this.pageMap.getOrDefault(playerUUID, 0);
+ this.pageMap.put(playerUUID, page - 1);
+
+ ItemStack cursor = viewer.getItemOnCursor();
+ viewer.setItemOnCursor(new ItemStack(Material.AIR));
+
+ show(viewer);
+
+ viewer.setItemOnCursor(cursor);
+ }, CustomAnvil.instance);
+
+ // Right item creation for consumer & bind
+ this.goRightItem = new GuiItem(new ItemStack(Material.LIME_STAINED_GLASS_PANE), event -> {
+ HumanEntity viewer = event.getWhoClicked();
+ UUID playerUUID = viewer.getUniqueId();
+ int page = pageMap.getOrDefault(playerUUID, 0);
+ this.pageMap.put(playerUUID, page + 1);
+
+ ItemStack cursor = viewer.getItemOnCursor();
+ viewer.setItemOnCursor(new ItemStack(Material.AIR));
+
+ show(viewer);
+
+ viewer.setItemOnCursor(cursor);
+ }, CustomAnvil.instance);
+
+ GuiItem createNew = prepareCreateNewItem();
+ if(createNew != null){
+ this.backgroundPane.bindItem('C', createNew);
+ }
+ }
+ protected void reloadValues(){
+ this.firstPage.clear();
+ this.pages.clear();
+ this.pages.add(this.firstPage);
+
+ for (T generic : getEveryDisplayableInstanceOfGeneric()) {
+ updateValueForGeneric(generic, false);
+ }
+
+ update();
+ }
+
+ protected abstract GuiItem prepareCreateNewItem();
+
+ protected OutlinePane createEmptyPage() {
+ OutlinePane page = new OutlinePane(LIST_FILLER_START_X, LIST_FILLER_START_Y, LIST_FILLER_LENGTH, LIST_FILLER_HEIGHT);
+ page.align(OutlinePane.Alignment.BEGIN);
+ page.setOrientation(Orientable.Orientation.HORIZONTAL);
+
+ return page;
+ }
+
+ public int getPlayerPageID(UUID uuid) {
+ int pageId = this.pageMap.getOrDefault(uuid, 0);
+ if (pageId >= this.pages.size()) {
+ pageId = this.pages.size() - 1;
+ }
+ return pageId;
+ }
+
+ protected void addToPage(GuiItem guiItem) {
+ // Get first available page or create one
+ OutlinePane page = this.pages.get(this.pages.size() - 1);
+ if (page.getItems().size() >= LIST_FILLER_LENGTH * LIST_FILLER_HEIGHT) {
+ page = createEmptyPage();
+ this.pages.add(page);
+ }
+
+ page.addItem(guiItem);
+ }
+
+ private void removeFromPage(GuiItem guiItem) {
+ // get item page
+ OutlinePane page = null;
+ int pageID = 0;
+ while (pageID < this.pages.size()) {
+ OutlinePane tempPage = this.pages.get(pageID);
+ if (tempPage.getItems().contains(guiItem)) {
+ page = tempPage;
+ break;
+ }
+ pageID++;
+ }
+
+ if (page == null) {// Why...
+ return;
+ }
+ removeFromPage(page, pageID, guiItem);
+ }
+
+ private void removeFromPage(OutlinePane page, int pageID, GuiItem guiItem) {
+ page.removeItem(guiItem);
+
+ // There is now a slot available, let fill it if possible
+ if (pageID < (this.pages.size() - 1)) {
+ OutlinePane newPage = this.pages.get(pageID + 1);
+ GuiItem nextPageItem = newPage.getItems().get(0);
+
+ removeFromPage(newPage, pageID + 1, nextPageItem);
+
+ OutlinePane thisPage = this.pages.get(pageID);
+ thisPage.addItem(nextPageItem);
+ } else if (pageID > 0 && page.getItems().isEmpty()) {
+ this.pages.remove(pageID);
+ }
+ }
+
+ public void placeArrow(int page, boolean customise) {
+
+ // Place left arrow
+ addPane(this.backgroundPane);
+ if (page > 0) {
+ if (customise) {
+ ItemStack leftItem = this.goLeftItem.getItem();
+ ItemMeta leftMeta = leftItem.getItemMeta();
+
+ leftMeta.setDisplayName("§eReturn to page " + (page));
+
+ leftItem.setItemMeta(leftMeta);
+ this.goLeftItem.setItem(leftItem);
+ }
+
+ this.backgroundPane.bindItem('L', this.goLeftItem);
+ } else {
+ this.backgroundPane.bindItem('L', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM);
+ }
+
+ // Place right arrow
+ if (page < pages.size() - 1) {
+ if (customise) {
+ ItemStack rightItem = this.goRightItem.getItem();
+ ItemMeta rightMeta = rightItem.getItemMeta();
+
+ rightMeta.setDisplayName("§eGo to page " + (page + 2));
+
+ rightItem.setItemMeta(rightMeta);
+ this.goRightItem.setItem(rightItem);
+ }
+
+ this.backgroundPane.bindItem('R', this.goRightItem);
+ } else {
+ this.backgroundPane.bindItem('R', GuiSharedConstant.SECONDARY_BACKGROUND_ITEM);
+ }
+ }
+
+ @Override // assume will not be called in multiple thread
+ public void show(@NotNull HumanEntity humanEntity) {
+ int pageID = getPlayerPageID(humanEntity.getUniqueId());
+ OutlinePane page = this.pages.get(pageID);
+
+ getPanes().clear();
+
+ // display the page arrow pane
+ placeArrow(pageID, true);
+ // and add actual page
+ addPane(page);
+
+ // set title
+ StringBuilder title = new StringBuilder(this.namePrefix);
+ int pagesSize = this.pages.size();
+ if(pagesSize > 1){
+ title.append(" (").append(pageID + 1).append('/').append(pagesSize).append(')');
+ }
+ setTitle(title.toString());
+
+ super.show(humanEntity);
+
+ }
+
+ @Override // assume will not be called in multiple thread
+ public void click(@NotNull InventoryClickEvent event) {
+ int pageID = getPlayerPageID(event.getWhoClicked().getUniqueId());
+ OutlinePane page = this.pages.get(pageID);
+
+ getPanes().clear();
+
+ // set the page arrow pane
+ placeArrow(pageID, false);
+ // and add actual page
+ addPane(page);
+
+ super.click(event);
+ }
+
+ // -------------------------
+ // Methods using generic T
+ // -------------------------
+
+ public void updateValueForGeneric(T generic, boolean shouldUpdate) {
+ ItemStack item = createItemForGeneric(generic);
+
+ updateGeneric(generic, item);
+
+ if (shouldUpdate) {
+ update();
+ }
+
+ }
+
+ public void removeGeneric(T generic) {
+ GuiItem item = findGuiItemForRemoval(generic);
+ if(item == null) return;
+ removeFromPage(item);
+
+ update();
+ }
+
+ protected abstract GuiItem findGuiItemForRemoval(T generic);
+
+ protected abstract ItemStack createItemForGeneric(T generic);
+
+ protected abstract void updateGeneric(T generic, ItemStack usedItem);
+
+ protected abstract Collection getEveryDisplayableInstanceOfGeneric();
+
+ @Override
+ public void updateGuiValues() {
+ // Not the optimised way to update this gui
+ // TODO maybe rework ValueUpdatableGui and it's dependency to allow a 1 item reload every time.
+
+ reloadValues();
+ }
+
+ @Override
+ public Gui getConnectedGui() {
+ return this;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedElementListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedElementListConfigGui.java
new file mode 100644
index 0000000..31ab718
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedElementListConfigGui.java
@@ -0,0 +1,106 @@
+package xyz.alexcrea.cuanvil.gui.config.list;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.config.MainConfigGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.function.Consumer;
+
+public abstract class MappedElementListConfigGui< T, S > extends ElementListConfigGui< T > {
+
+ protected final HashMap elementGuiMap;
+ protected MappedElementListConfigGui(@NotNull String title) {
+ super(title, MainConfigGui.getInstance());
+ this.elementGuiMap = new HashMap<>();
+
+ }
+
+ @Override
+ protected GuiItem prepareCreateNewItem(){
+ // Create new conflict item
+ ItemStack createItem = new ItemStack(Material.PAPER);
+ ItemMeta createMeta = createItem.getItemMeta();
+ assert createMeta != null;
+
+ createMeta.setDisplayName("§aCreate new "+genericDisplayedName());
+ createMeta.setLore(Arrays.asList(
+ "§7Create a new "+genericDisplayedName()+".",
+ "§7You will be asked to name the "+genericDisplayedName()+" in chat.",
+ "§7Then, you should edit the "+genericDisplayedName()+" config as you need"
+ ));
+
+ createItem.setItemMeta(createMeta);
+
+ return new GuiItem(createItem, clickEvent -> {
+ clickEvent.setCancelled(true);
+ HumanEntity player = clickEvent.getWhoClicked();
+
+ // check permission
+ if (!player.hasPermission(CustomAnvil.editConfigPermission)) {
+ player.closeInventory();
+ player.sendMessage(GuiGlobalActions.NO_EDIT_PERM);
+ return;
+ }
+ player.closeInventory();
+
+ player.sendMessage("§eWrite the "+genericDisplayedName()+" name you want to create in the chat.\n" +
+ "§eOr write §ccancel §eto go back to "+genericDisplayedName()+" config menu");
+
+ CustomAnvil.Companion.getChatListener().setListenedCallback(player, prepareCreateItemConsumer(player));
+
+ }, CustomAnvil.instance);
+ }
+
+ @Override
+ protected void updateGeneric(T generic, ItemStack usedItem) {
+ S element = this.elementGuiMap.get(generic);
+
+ GuiItem guiItem;
+ if (element == null) {
+ // Create new sub setting element
+ guiItem = new GuiItem(usedItem, CustomAnvil.instance);
+
+ element = newElementRequested(generic, guiItem);
+
+ this.elementGuiMap.put(generic, element);
+
+ addToPage(guiItem);
+ } else {
+ // Replace item with the updated one
+ guiItem = findItemFromElement(generic, element);
+ guiItem.setItem(usedItem);
+ }
+ updateElement(generic, element);
+
+ }
+
+ protected abstract void updateElement(T generic, S element);
+
+ protected abstract S newElementRequested(T generic, GuiItem newItem);
+
+ protected abstract GuiItem findItemFromElement(T generic, S element);
+
+ @Override
+ protected GuiItem findGuiItemForRemoval(T generic) {
+ S element = this.elementGuiMap.get(generic);
+ if (element == null) return null;
+
+ this.elementGuiMap.remove(generic);
+ return findGuiItemForRemoval(generic, element);
+ }
+
+ protected abstract GuiItem findGuiItemForRemoval(T generic, S element);
+
+ protected abstract Consumer prepareCreateItemConsumer(HumanEntity player);
+
+ protected abstract String genericDisplayedName();
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedGuiListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedGuiListConfigGui.java
new file mode 100644
index 0000000..3aa18e0
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/MappedGuiListConfigGui.java
@@ -0,0 +1,140 @@
+package xyz.alexcrea.cuanvil.gui.config.list;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.config.list.elements.ElementMappedToListGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.util.LazyValue;
+
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public abstract class MappedGuiListConfigGui< T, S extends MappedGuiListConfigGui.LazyElement>>
+ extends MappedElementListConfigGui< T, S > {
+
+ protected MappedGuiListConfigGui(@NotNull String title) {
+ super(title);
+
+ }
+
+ @Override
+ public void reloadValues() {
+ this.elementGuiMap.forEach((conflict, element) -> {
+ ElementMappedToListGui gui = element.getStored();
+ if(gui != null) gui.cleanAndBeUnusable();
+ });
+ this.elementGuiMap.clear();
+
+ super.reloadValues();
+ }
+
+ @Override
+ protected S newElementRequested(T generic, GuiItem newItem) {
+ S element = newInstanceOfGui(generic, newItem);
+
+ newItem.setAction(element.openAction());
+ return element;
+ }
+
+ @Override
+ protected GuiItem findItemFromElement(T generic, S element) {
+ return element.getParentItem();
+ }
+
+ @Override
+ protected void updateElement(T generic, S element) {
+ ElementMappedToListGui gui = element.getStored();
+ if(gui != null) gui.updateLocal();
+ }
+
+ @Override
+ protected GuiItem findGuiItemForRemoval(T generic, S element) {
+ return element.getParentItem();
+ }
+
+ @Override
+ protected Consumer prepareCreateItemConsumer(HumanEntity player){
+ AtomicReference> selfRef = new AtomicReference<>();
+ Consumer selfCallback = (message) -> {
+ if (message == null) return;
+
+ // check permission
+ if (!player.hasPermission(CustomAnvil.editConfigPermission)) {
+ player.sendMessage(GuiGlobalActions.NO_EDIT_PERM);
+ return;
+ }
+
+ message = message.toLowerCase(Locale.ROOT);
+ if ("cancel".equalsIgnoreCase(message)) {
+ player.sendMessage(genericDisplayedName()+" creation cancelled...");
+ show(player);
+ return;
+ }
+
+ message = message.replace(' ', '_');
+
+ // Try to find if it already exists in a for loop
+ // Not the most efficient on large number of conflict, but it should not run often.
+ for (T generic : getEveryDisplayableInstanceOfGeneric()) {
+ if (generic.toString().equalsIgnoreCase(message)) {
+ player.sendMessage("§cPlease enter a "+genericDisplayedName()+" name that do not already exist...");
+ // wait next message.
+ CustomAnvil.Companion.getChatListener().setListenedCallback(player, selfRef.get());
+ return;
+ }
+ }
+
+ T generic = createAndSaveNewEmptyGeneric(message);
+ if(generic == null) {// we don't know what to do. so we back up by opening this gui.
+ this.show(player);
+ return;
+ }
+
+ updateValueForGeneric(generic, true);
+
+ // show the new conflict config to the player
+ this.elementGuiMap.get(generic).get().getMappedGui().show(player);
+
+ update();
+ };
+
+ selfRef.set(selfCallback);
+ return selfCallback;
+ }
+
+ protected abstract S newInstanceOfGui(T generic, GuiItem item);
+
+ protected abstract String genericDisplayedName();
+
+ protected abstract T createAndSaveNewEmptyGeneric(String name);
+
+ public static class LazyElement extends LazyValue {
+
+ private final GuiItem parentItem;
+ private final LazyValue> lazyOpenConsumer;
+ public LazyElement(GuiItem parentItem, Supplier valueSupplier) {
+ super(valueSupplier);
+ this.parentItem = parentItem;
+
+ this.lazyOpenConsumer = new LazyValue<>(() ->
+ GuiGlobalActions.openGuiAction(this.get().getMappedGui()))
+ ;
+ }
+
+ public GuiItem getParentItem() {
+ return parentItem;
+ }
+
+ @NotNull
+ public Consumer openAction(){
+ return event -> lazyOpenConsumer.get().accept(event);
+ }
+
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/SettingGuiListConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/SettingGuiListConfigGui.java
new file mode 100644
index 0000000..f0e7cb0
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/SettingGuiListConfigGui.java
@@ -0,0 +1,103 @@
+package xyz.alexcrea.cuanvil.gui.config.list;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.config.MainConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.SettingGui;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+
+public abstract class SettingGuiListConfigGui< T, S extends SettingGui.SettingGuiFactory> extends ElementListConfigGui< T >{
+
+ protected HashMap guiItemMap;
+ protected HashMap factoryMap;
+ protected SettingGuiListConfigGui(@NotNull String title, Gui parent) {
+ super(title, parent);
+ this.guiItemMap = new HashMap<>();
+ this.factoryMap = new HashMap<>();
+ }
+
+ protected SettingGuiListConfigGui(@NotNull String title) {
+ this(title, MainConfigGui.getInstance());
+ }
+
+ @Override
+ protected GuiItem prepareCreateNewItem() {
+ ItemStack createItem = new ItemStack(Material.PAPER);
+ ItemMeta createMeta = createItem.getItemMeta();
+ assert createMeta != null;
+
+ createMeta.setDisplayName(createItemName());
+ createMeta.setLore(getCreateItemLore());
+
+ createItem.setItemMeta(createMeta);
+ return new GuiItem(createItem, getCreateClickConsumer(), CustomAnvil.instance);
+ }
+
+ @Override
+ public void updateValueForGeneric(T generic, boolean shouldUpdate) {
+ if(!this.factoryMap.containsKey(generic)){
+ // Create new item & factory
+ S factory = createFactory(generic);
+ GuiItem newItem = itemFromFactory(generic, factory);
+
+ addToPage(newItem);
+ this.guiItemMap.put(generic, newItem);
+ this.factoryMap.put(generic, factory);
+ }else{
+ S factory = this.factoryMap.get(generic);
+ // Update old item
+ GuiItem oldItem = this.guiItemMap.get(generic);
+
+ GuiItem newItem = itemFromFactory(generic, factory);
+ updateGuiItem(oldItem, newItem);
+ }
+
+ if(shouldUpdate){
+ update();
+ }
+ }
+
+ @Override
+ protected void reloadValues() {
+ this.guiItemMap.clear();
+ this.factoryMap.clear();
+
+ super.reloadValues();
+ }
+
+ private void updateGuiItem(GuiItem oldITem, GuiItem newItem){
+ oldITem.setItem(newItem.getItem());
+ oldITem.setProperties(newItem.getProperties());
+ oldITem.setVisible(newItem.isVisible());
+ }
+
+ @Override
+ protected GuiItem findGuiItemForRemoval(T generic) {
+ return this.guiItemMap.get(generic);
+ }
+
+ @Override // Not used
+ protected void updateGeneric(T generic, ItemStack usedItem) {}
+ @Override // Not used
+ protected ItemStack createItemForGeneric(T generic) {
+ return null;
+ }
+
+ protected abstract List getCreateItemLore();
+ protected abstract Consumer getCreateClickConsumer();
+ protected abstract String createItemName();
+
+ protected abstract S createFactory(T generic);
+ protected abstract GuiItem itemFromFactory(T generic, S factory);
+
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/UnitRepairElementListGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/UnitRepairElementListGui.java
new file mode 100644
index 0000000..35f8ebb
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/UnitRepairElementListGui.java
@@ -0,0 +1,195 @@
+package xyz.alexcrea.cuanvil.gui.config.list;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.meta.Damageable;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.gui.config.ask.SelectItemTypeGui;
+import xyz.alexcrea.cuanvil.gui.config.global.UnitRepairConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.list.elements.ElementMappedToListGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.DoubleSettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class UnitRepairElementListGui extends SettingGuiListConfigGui implements ElementMappedToListGui {
+
+ private final Material parentMaterial;
+ private final UnitRepairConfigGui parentGui;
+ private final String materialName;
+
+ private boolean shouldWork = true;
+ public UnitRepairElementListGui(@NotNull Material parentMaterial,
+ @NotNull UnitRepairConfigGui parentGui) {
+ super("§e" + CasedStringUtil.snakeToUpperSpacedCase(parentMaterial.name().toLowerCase()) + " §rUnit repair");
+ this.parentMaterial = parentMaterial;
+ this.parentGui = parentGui;
+ this.materialName = CasedStringUtil.snakeToUpperSpacedCase(parentMaterial.name().toLowerCase());
+
+ GuiGlobalItems.addBackItem(this.backgroundPane, parentGui);
+ }
+
+ // SettingGuiListConfigGui methods
+ @Override
+ protected List getCreateItemLore() {
+ return Arrays.asList(
+ "§7Select a new item to be repairable.",
+ "§7You will be asked the material to use."
+ );
+ }
+
+ @Override
+ protected Consumer getCreateClickConsumer() {
+ return event -> {
+ event.setCancelled(true);
+ if(!this.shouldWork){
+ return;
+ }
+ event.setCancelled(true);
+
+ new SelectItemTypeGui(
+ "Select item to be repaired.",
+ "§7Click here with an item to set the item\n" +
+ "§7You like to be repaired by " + this.materialName,
+ this,
+ (itemStack, player) -> {
+ ItemMeta meta = itemStack.getItemMeta();
+ Material type = itemStack.getType();
+
+ if(!(meta instanceof Damageable) || (type.getMaxDurability() <= 0)) {
+ player.sendMessage("§cThis item can't be damaged, so it can't be repaired.");
+ return;
+ }
+ if(type == this.parentMaterial){
+ player.sendMessage("§cItem can't repair something of the same type.");
+ return;
+ }
+
+ String materialName = type.name().toLowerCase();
+
+ // Add new material
+ ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().set(parentMaterial.name().toLowerCase() + "." + materialName,0.25);
+
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ ConfigHolder.UNIT_REPAIR_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ // Update gui
+ updateValueForGeneric(materialName, true);
+ this.parentGui.updateValueForGeneric(this.parentMaterial, true);
+
+
+ // Display material edit setting
+ this.factoryMap.get(materialName).create().show(player);
+ },
+ true
+ ).show(event.getWhoClicked());
+
+ };
+ }
+
+ @Override
+ protected String createItemName() {
+ return "§aAdd a new item reparable by " + this.materialName;
+ }
+
+ @Override
+ protected DoubleSettingGui.DoubleSettingFactory createFactory(String materialName) {
+ String materialDisplayName = CasedStringUtil.snakeToUpperSpacedCase(materialName);
+
+ return new DoubleSettingGui.DoubleSettingFactory(
+ "§0%§8" + materialDisplayName +" Repair",
+ this,
+ ConfigHolder.UNIT_REPAIR_HOLDER,
+ this.parentMaterial.name().toLowerCase()+"."+materialName,
+ Arrays.asList(
+ "§7Click here to change how many §e% §7of §a" + materialDisplayName,
+ "§7Should get repaired by §e"+this.materialName
+ ),
+ 2,
+ true, true,
+ 0,
+ 1,
+ 0.25,
+ 0.01, 0.05, 0.25
+ );
+ }
+
+ @Override
+ protected GuiItem itemFromFactory(String materialName, DoubleSettingGui.DoubleSettingFactory factory) {
+ return factory.getItem(materialFromName(materialName),
+ "§7%§a" + CasedStringUtil.snakeToUpperSpacedCase(materialName)+ " §erepaired by §a" + this.materialName);
+ }
+
+ @Override
+ protected Collection getEveryDisplayableInstanceOfGeneric() {
+ ArrayList keys = new ArrayList<>();
+ if(!this.shouldWork){
+ return keys;
+ }
+
+ ConfigurationSection materialSection = ConfigHolder.UNIT_REPAIR_HOLDER.getConfig().getConfigurationSection(parentMaterial.name().toLowerCase());
+ if(materialSection == null){
+ return keys;
+ }
+ keys.addAll(materialSection.getKeys(false));
+ return keys;
+ }
+
+ private Material materialFromName(String materialName){
+ Material mat = Material.getMaterial(materialName.toUpperCase());
+ if(mat == null || mat.isAir()) return Material.BARRIER;
+ return mat;
+ }
+
+ @Override
+ public void updateGuiValues() {
+ super.updateGuiValues();
+ this.parentGui.updateValueForGeneric(this.parentMaterial, true);
+ }
+
+ // ElementMappedToListGui methods
+
+ @Override // Not used in this implementation
+ public void updateLocal() {}
+
+ @Override
+ public void cleanAndBeUnusable() {
+ this.shouldWork = false;
+ this.backgroundPane.bindItem('S', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE));
+ this.backgroundPane.bindItem('L', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE));
+ this.backgroundPane.bindItem('R', GuiGlobalItems.backgroundItem(Material.BLACK_STAINED_GLASS_PANE));
+
+ for (HumanEntity viewer : getViewers()) {
+ viewer.sendMessage("This config do not exist anymore");
+ this.parentGui.show(viewer);
+ }
+ }
+
+ @Override
+ public Gui getMappedGui() {
+ return this;
+ }
+
+ @Override
+ public void show(@NotNull HumanEntity humanEntity) {
+ if(!this.shouldWork){
+ humanEntity.closeInventory();
+ return;
+ }
+ super.show(humanEntity);
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/CustomRecipeSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/CustomRecipeSubSettingGui.java
new file mode 100644
index 0000000..9db6d62
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/CustomRecipeSubSettingGui.java
@@ -0,0 +1,238 @@
+package xyz.alexcrea.cuanvil.gui.config.list.elements;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import kotlin.ranges.IntRange;
+import org.bukkit.Material;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui;
+import xyz.alexcrea.cuanvil.gui.config.global.CustomRecipeConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.BoolSettingsGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.ItemSettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe;
+import xyz.alexcrea.cuanvil.recipe.CustomAnvilRecipeManager;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.Collections;
+import java.util.function.Supplier;
+
+public class CustomRecipeSubSettingGui extends MappedToListSubSettingGui {
+
+ private final CustomRecipeConfigGui parent;
+ private final AnvilCustomRecipe anvilRecipe;
+ private final PatternPane pane;
+ private boolean shouldWork = true;
+
+ public CustomRecipeSubSettingGui(
+ @NotNull CustomRecipeConfigGui parent,
+ @NotNull AnvilCustomRecipe anvilRecipe) {
+ super(4, "§e" + CasedStringUtil.snakeToUpperSpacedCase(anvilRecipe.toString()) + " §8Config");
+ this.parent = parent;
+ this.anvilRecipe = anvilRecipe;
+
+ Pattern pattern = new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "01203450D",
+ "0ab000000",
+ "B00000000"
+ );
+ this.pane = new PatternPane(0, 0, 9, 4, pattern);
+ addPane(this.pane);
+
+ prepareStaticValues();
+ }
+
+ private BoolSettingsGui.BoolSettingFactory exactCountFactory;
+ private BoolSettingsGui.BoolSettingFactory removeExactLinearXpFactory;
+ private GuiItem noRemoveExactLinearXp;
+
+ private IntSettingsGui.IntSettingFactory levelCostFactory;
+ private IntSettingsGui.IntSettingFactory linearXpCostFactory;
+
+ private ItemSettingGui.ItemSettingFactory leftItemFactory;
+ private ItemSettingGui.ItemSettingFactory rightItemFactory;
+ private ItemSettingGui.ItemSettingFactory resultItemFactory;
+
+ private void prepareStaticValues() {
+
+ GuiGlobalItems.addBackItem(this.pane, this.parent);
+ GuiGlobalItems.addBackgroundItem(this.pane);
+
+ // Delete item
+ ItemStack deleteItem = new ItemStack(Material.RED_TERRACOTTA);
+ ItemMeta deleteMeta = deleteItem.getItemMeta();
+ assert deleteMeta != null;
+
+ deleteMeta.setDisplayName("§4DELETE RECIPE");
+ deleteMeta.setLore(Collections.singletonList("§cCaution with this button !"));
+
+ deleteItem.setItemMeta(deleteMeta);
+ this.pane.bindItem('D', new GuiItem(deleteItem, GuiGlobalActions.openGuiAction(createDeleteGui()), CustomAnvil.instance));
+
+ // Displayed item will be updated later
+ IntRange costRange = AnvilCustomRecipe.Companion.getXP_COST_CONFIG_RANGE();
+ this.exactCountFactory = new BoolSettingsGui.BoolSettingFactory("§8Exact count ?", this,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ this.anvilRecipe + "." + AnvilCustomRecipe.EXACT_COUNT_CONFIG, AnvilCustomRecipe.DEFAULT_EXACT_COUNT_CONFIG);
+
+ this.removeExactLinearXpFactory = new BoolSettingsGui.BoolSettingFactory("§8Remove exact linear xp ?", this,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ this.anvilRecipe + "." + AnvilCustomRecipe.REMOVE_EXACT_XP_CONFIG, AnvilCustomRecipe.DEFAULT_REMOVE_EXACT_XP_CONFIG);
+
+ ItemStack item = new ItemStack(Material.BARRIER);
+ ItemMeta meta = item.getItemMeta();
+ assert meta != null;
+
+ meta.setDisplayName("§cRemove exact linear xp ?");
+ meta.setLore(Collections.singletonList("§7Not usable if linear cost is 0"));
+ item.setItemMeta(meta);
+ this.noRemoveExactLinearXp = new GuiItem(item, GuiGlobalActions.stayInPlace, CustomAnvil.instance);
+
+ this.levelCostFactory = new IntSettingsGui.IntSettingFactory("§8Recipe Level Cost", this,
+ this.anvilRecipe + "." + AnvilCustomRecipe.XP_LEVEL_COST_CONFIG,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ null,
+ costRange.getFirst(), costRange.getLast(), AnvilCustomRecipe.DEFAULT_XP_LEVEL_COST_CONFIG, 1, 5, 10);
+
+ this.linearXpCostFactory = new IntSettingsGui.IntSettingFactory("§8Recipe Linear Xp Cost", this,
+ this.anvilRecipe + "." + AnvilCustomRecipe.LINEAR_XP_COST_CONFIG,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ null,
+ 0, Integer.MAX_VALUE, AnvilCustomRecipe.DEFAULT_LINEAR_XP_COST_CONFIG, 1, 10, 100, 1000, 10000);
+
+
+ // Right part of the gui
+ this.leftItemFactory = new ItemSettingGui.ItemSettingFactory("§eRecipe Left §8Item", this,
+ this.anvilRecipe + "." + AnvilCustomRecipe.LEFT_ITEM_CONFIG,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ AnvilCustomRecipe.Companion.getDEFAULT_LEFT_ITEM_CONFIG(),
+ "§7Set the left item of the custom craft",
+ "§7\u25A0 + \u25A1 = \u25A1");
+
+ this.rightItemFactory = new ItemSettingGui.ItemSettingFactory("§eRecipe Right §8Item", this,
+ this.anvilRecipe + "." + AnvilCustomRecipe.RIGHT_ITEM_CONFIG,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ AnvilCustomRecipe.Companion.getDEFAULT_RIGHT_ITEM_CONFIG(),
+ "§7Set the right item of the custom craft",
+ "§7\u25A1 + \u25A0 = \u25A1");
+
+ this.resultItemFactory = new ItemSettingGui.ItemSettingFactory("§aRecipe Result §8Item", this,
+ this.anvilRecipe + "." + AnvilCustomRecipe.RESULT_ITEM_CONFIG,
+ ConfigHolder.CUSTOM_RECIPE_HOLDER,
+ AnvilCustomRecipe.Companion.getDEFAULT_RESULT_ITEM_CONFIG(),
+ "§7Set the result item of the custom craft",
+ "§7\u25A1 + \u25A1 = \u25A0");
+
+ // Now we update the items
+ updateLocal();
+ }
+
+ private ConfirmActionGui createDeleteGui() {
+ Supplier deleteSupplier = () -> {
+ CustomAnvilRecipeManager manager = ConfigHolder.CUSTOM_RECIPE_HOLDER.getRecipeManager();
+
+ // Remove from manager
+ manager.cleanRemove(this.anvilRecipe);
+
+ // Remove from parent
+ this.parent.removeGeneric(this.anvilRecipe);
+
+ // Remove self
+ cleanAndBeUnusable();
+
+ // Update config file storage
+ ConfigHolder.CUSTOM_RECIPE_HOLDER.delete(this.anvilRecipe.toString());
+
+ // Save
+ boolean success = true;
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ success = ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return success;
+ };
+
+ return new ConfirmActionGui("§cDelete §e" + CasedStringUtil.snakeToUpperSpacedCase(this.anvilRecipe.toString()) + "§c?",
+ "§7Confirm that you want to delete this conflict.",
+ this, this.parent, deleteSupplier
+ );
+ }
+
+ @Override
+ public void updateGuiValues() {
+ // update value from config to conflict
+ this.anvilRecipe.updateFromFile();
+
+ // Parent should call updateLocal with this call
+ this.parent.updateValueForGeneric(this.anvilRecipe, true);
+ }
+
+ public void updateLocal() {
+ if (!this.shouldWork) return;
+
+ GuiItem exactCountItem = this.exactCountFactory.getItem();
+ this.pane.bindItem('1', exactCountItem);
+
+ if (anvilRecipe.getXpCostPerCraft() == 0) {
+ this.pane.bindItem('a', noRemoveExactLinearXp);
+ } else {
+ this.pane.bindItem('a', removeExactLinearXpFactory.getItem());
+ }
+
+ GuiItem levelCostItem = this.levelCostFactory.getItem(Material.EXPERIENCE_BOTTLE);
+ this.pane.bindItem('2', levelCostItem);
+
+
+ GuiItem xpCostItem = this.linearXpCostFactory.getItem(Material.EXPERIENCE_BOTTLE);
+ this.pane.bindItem('b', xpCostItem);
+
+ GuiItem leftGuiItem = this.leftItemFactory.getItem();
+ this.pane.bindItem('3', leftGuiItem);
+
+ GuiItem rightGuiItem = this.rightItemFactory.getItem();
+ this.pane.bindItem('4', rightGuiItem);
+
+ GuiItem resultGuiItem = this.resultItemFactory.getItem();
+ this.pane.bindItem('5', resultGuiItem);
+
+ update();
+ }
+
+ public void cleanAndBeUnusable() {
+ for (HumanEntity viewer : getViewers()) {
+ this.parent.show(viewer);
+ }
+ this.shouldWork = false;
+
+ // Just in case something is extremely wrong
+ GuiItem background = GuiGlobalItems.backgroundItem();
+ this.pane.bindItem('1', background);
+ this.pane.bindItem('2', background);
+ this.pane.bindItem('3', background);
+ this.pane.bindItem('4', background);
+ this.pane.bindItem('5', background);
+
+ this.pane.bindItem('D', background);
+ }
+
+ @Override
+ public void show(@NotNull HumanEntity humanEntity) {
+ if (this.shouldWork) {
+ super.show(humanEntity);
+ } else {
+ this.parent.show(humanEntity);
+ }
+ }
+
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/ElementMappedToListGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/ElementMappedToListGui.java
new file mode 100644
index 0000000..637f72a
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/ElementMappedToListGui.java
@@ -0,0 +1,13 @@
+package xyz.alexcrea.cuanvil.gui.config.list.elements;
+
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+
+public interface ElementMappedToListGui {
+
+ void updateLocal();
+
+ void cleanAndBeUnusable();
+
+ Gui getMappedGui();
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/EnchantConflictSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/EnchantConflictSubSettingGui.java
new file mode 100644
index 0000000..97bdfcb
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/EnchantConflictSubSettingGui.java
@@ -0,0 +1,330 @@
+package xyz.alexcrea.cuanvil.gui.config.list.elements;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.enchant.CAEnchantment;
+import xyz.alexcrea.cuanvil.group.AbstractMaterialGroup;
+import xyz.alexcrea.cuanvil.group.EnchantConflictGroup;
+import xyz.alexcrea.cuanvil.group.EnchantConflictManager;
+import xyz.alexcrea.cuanvil.gui.config.SelectEnchantmentContainer;
+import xyz.alexcrea.cuanvil.gui.config.SelectGroupContainer;
+import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui;
+import xyz.alexcrea.cuanvil.gui.config.global.EnchantConflictGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.EnchantSelectSettingGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.GroupSelectSettingGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.IntSettingsGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+import xyz.alexcrea.cuanvil.util.MetricsUtil;
+
+import java.util.*;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+
+public class EnchantConflictSubSettingGui extends MappedToListSubSettingGui implements SelectEnchantmentContainer, SelectGroupContainer {
+
+ private final EnchantConflictGui parent;
+ private final EnchantConflictGroup enchantConflict;
+ private final PatternPane pane;
+ private boolean shouldWork = true;
+
+ public EnchantConflictSubSettingGui(
+ @NotNull EnchantConflictGui parent,
+ @NotNull EnchantConflictGroup enchantConflict) {
+ super(3,
+ "§e" + CasedStringUtil.snakeToUpperSpacedCase(enchantConflict.toString()) + " §8Config");
+ this.parent = parent;
+ this.enchantConflict = enchantConflict;
+
+ Pattern pattern = new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "00EGM000D",
+ "B00000000"
+ );
+ this.pane = new PatternPane(0, 0, 9, 3, pattern);
+ addPane(this.pane);
+
+ prepareStaticValues();
+ }
+
+ private GuiItem enchantSettingItem;
+ private GuiItem groupSettingItem;
+ private IntSettingsGui.IntSettingFactory minBeforeActiveSettingFactory;
+
+ private void prepareStaticValues() {
+
+ GuiGlobalItems.addBackItem(this.pane, this.parent);
+ GuiGlobalItems.addBackgroundItem(this.pane);
+
+ // Delete item
+ ItemStack deleteItem = new ItemStack(Material.RED_TERRACOTTA);
+ ItemMeta deleteMeta = deleteItem.getItemMeta();
+ assert deleteMeta != null;
+
+ deleteMeta.setDisplayName("§4DELETE CONFLICT");
+ deleteMeta.setLore(Collections.singletonList("§cCaution with this button !"));
+
+ deleteItem.setItemMeta(deleteMeta);
+ this.pane.bindItem('D', new GuiItem(deleteItem, GuiGlobalActions.openGuiAction(createDeleteGui()), CustomAnvil.instance));
+
+ // Displayed item will be updated later
+ this.enchantSettingItem = new GuiItem(new ItemStack(Material.ENCHANTED_BOOK), event -> {
+ event.setCancelled(true);
+ EnchantSelectSettingGui enchantGui = new EnchantSelectSettingGui(
+ "§e" + CasedStringUtil.snakeToUpperSpacedCase(enchantConflict.toString()) + "§5",
+ this, this);
+ enchantGui.show(event.getWhoClicked());
+ }, CustomAnvil.instance);
+
+ this.groupSettingItem = new GuiItem(new ItemStack(Material.PAPER), event -> {
+ event.setCancelled(true);
+ GroupSelectSettingGui enchantGui = new GroupSelectSettingGui(
+ "§e" + CasedStringUtil.snakeToUpperSpacedCase(this.enchantConflict.toString()) + " §3Groups",
+ this, this, 0);
+ enchantGui.show(event.getWhoClicked());
+ }, CustomAnvil.instance);
+
+ this.minBeforeActiveSettingFactory = new IntSettingsGui.IntSettingFactory(
+ "§8Minimum enchantment count",
+ this, this.enchantConflict + ".maxEnchantmentBeforeConflict", ConfigHolder.CONFLICT_HOLDER,
+ Arrays.asList(
+ "§7Minimum enchantment count set to X mean only X enchantment can be put",
+ "§7on an item before the conflict is active."
+ ),
+ 0, 255, 0, 1
+ );
+
+ this.pane.bindItem('E', this.enchantSettingItem);
+ this.pane.bindItem('G', this.groupSettingItem);
+
+ // Now we update the items
+ updateLocal();
+
+ }
+
+ private ConfirmActionGui createDeleteGui() {
+ Supplier deleteSupplier = () -> {
+ EnchantConflictManager manager = ConfigHolder.CONFLICT_HOLDER.getConflictManager();
+
+ // Remove from enchantment
+ manager.removeConflict(this.enchantConflict);
+
+ // Remove from parent
+ this.parent.removeGeneric(this.enchantConflict);
+
+ // Remove self
+ cleanAndBeUnusable();
+
+ // Update config file storage
+ ConfigHolder.CONFLICT_HOLDER.delete(this.enchantConflict.toString());
+
+ // Save
+ boolean success = true;
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ success = ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return success;
+ };
+
+ return new ConfirmActionGui("§cDelete §e" + CasedStringUtil.snakeToUpperSpacedCase(this.enchantConflict.toString()) + "§c?",
+ "§7Confirm that you want to delete this conflict.",
+ this, this.parent, deleteSupplier
+ );
+ }
+
+ @Override
+ public void updateGuiValues() {
+ // update value from config to conflict
+ int minBeforeBlock = ConfigHolder.CONFLICT_HOLDER.getConfig().getInt(this.enchantConflict.toString()+'.'+EnchantConflictManager.ENCH_MAX_PATH, 0);
+ this.enchantConflict.setMinBeforeBlock(minBeforeBlock);
+
+ // Parent should call updateLocal with this call
+ this.parent.updateValueForGeneric(this.enchantConflict, true);
+ }
+
+ @Override
+ public void updateLocal() {
+ if (!this.shouldWork) return;
+
+ // Prepare enchantment lore
+ ArrayList enchantLore = new ArrayList<>();
+ enchantLore.add("§7Allow you to select a list of §5Enchantments §7that this conflict should include");
+ Set enchants = getSelectedEnchantments();
+ if (enchants.isEmpty()) {
+ enchantLore.add("§7There is no included enchantment for this conflict.");
+ } else {
+ enchantLore.add("§7List of included enchantment for this conflict:");
+ Iterator enchantIterator = enchants.iterator();
+
+ boolean greaterThanMax = enchants.size() > 5;
+ int maxindex = (greaterThanMax ? 4 : enchants.size());
+ for (int i = 0; i < maxindex; i++) {
+ // format string like "- Fire Protection"
+ String formattedName = CasedStringUtil.snakeToUpperSpacedCase(enchantIterator.next().getKey().getKey());
+ enchantLore.add("§7- §5" + formattedName);
+ }
+ if (greaterThanMax) {
+ enchantLore.add("§7And " + (enchants.size() - 4) + " more...");
+ }
+
+ }
+
+ // Prepare group lore
+ List groupLore = SelectGroupContainer.getGroupLore(this, "conflict", "exclude");
+
+ // Configure enchant setting item
+ ItemStack enchantItem = this.enchantSettingItem.getItem();
+ ItemMeta enchantMeta = enchantItem.getItemMeta();
+ assert enchantMeta != null;
+
+ enchantMeta.setDisplayName("§aSelect included §5Enchantments §aSettings");
+ enchantMeta.setLore(enchantLore);
+
+ enchantItem.setItemMeta(enchantMeta);
+
+ this.enchantSettingItem.setItem(enchantItem); // Just in case
+
+ // Configure group setting item
+ ItemStack groupItem = this.groupSettingItem.getItem();
+ ItemMeta groupMeta = groupItem.getItemMeta();
+ assert groupMeta != null;
+
+ groupMeta.setDisplayName("§aSelect Excluded §3Groups §aSettings");
+ groupMeta.setLore(groupLore);
+
+ groupItem.setItemMeta(groupMeta);
+
+ this.groupSettingItem.setItem(groupItem); // Just in case
+
+ this.pane.bindItem('M', this.minBeforeActiveSettingFactory.getItem(Material.COMMAND_BLOCK,
+ "Minimum Enchantment Count"));
+ update();
+ }
+
+ @Override
+ public void cleanAndBeUnusable() {
+ for (HumanEntity viewer : getViewers()) {
+ this.parent.show(viewer);
+ }
+ this.shouldWork = false;
+
+ // Just in case something is extremely wrong
+ GuiItem background = GuiGlobalItems.backgroundItem();
+ this.pane.bindItem('E', background);
+ this.pane.bindItem('G', background);
+ this.pane.bindItem('M', background);
+ this.pane.bindItem('D', background);
+ }
+
+ @Override
+ public void show(@NotNull HumanEntity humanEntity) {
+ if (this.shouldWork) {
+ super.show(humanEntity);
+ } else {
+ this.parent.show(humanEntity);
+ }
+ }
+
+ // Select enchantment container methods
+
+ @Override
+ public Set getSelectedEnchantments() {
+ return this.enchantConflict.getEnchants();
+ }
+
+ @Override
+ public boolean setSelectedEnchantments(Set enchantments) {
+ if (!this.shouldWork) {
+ CustomAnvil.instance.getLogger().info("Trying to save " + enchantConflict + " enchants but sub config is destroyed");
+ return false;
+ }
+
+ // Set live configuration
+ this.enchantConflict.setEnchants(enchantments);
+
+ // Save on file configuration
+ String[] enchantKeys = new String[enchantments.size()];
+ int index = 0;
+ for (CAEnchantment enchantment : enchantments) {
+ enchantKeys[index++] = enchantment.getKey().toString();
+ }
+ ConfigHolder.CONFLICT_HOLDER.getConfig().set(enchantConflict + ".enchantments", enchantKeys);
+
+ try {
+ updateGuiValues();
+ } catch (Exception e) {
+ CustomAnvil.instance.getLogger().log(Level.WARNING, "An error occurred while updating enchants for " + this.enchantConflict, e);
+ MetricsUtil.INSTANCE.trackError(e);
+ }
+
+ // Save file configuration to disk
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ return ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return true;
+ }
+
+ @Override
+ public Set illegalEnchantments() {
+ return Collections.emptySet();
+ }
+
+ // Select group container methods
+
+ @Override
+ public Set getSelectedGroups() {
+ return this.enchantConflict.getCantConflictGroup().getGroups();
+ }
+
+ @Override
+ public boolean setSelectedGroups(Set groups) {
+ if (!this.shouldWork) {
+ CustomAnvil.instance.getLogger().info("Trying to save " + enchantConflict.toString() + " groups but sub config is destroyed");
+ return false;
+ }
+
+ // Set live configuration
+ this.enchantConflict.getCantConflictGroup().setGroups(groups);
+
+ // Save on file configuration
+ String[] groupsNames = new String[groups.size()];
+ int index = 0;
+ for (AbstractMaterialGroup group : groups) {
+ groupsNames[index++] = group.getName();
+ }
+ ConfigHolder.CONFLICT_HOLDER.getConfig().set(this.enchantConflict + ".notAffectedGroups", groupsNames);
+
+ try {
+ updateGuiValues();
+ } catch (Exception e) {
+ CustomAnvil.instance.getLogger().log(Level.WARNING, "An error occurred while updating group for " + this.enchantConflict, e);
+ MetricsUtil.INSTANCE.trackError(e);
+ }
+
+ // Save file configuration to disk
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ return ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return true;
+ }
+
+ @Override
+ public Set illegalGroups() {
+
+ return Collections.emptySet();
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/GroupConfigSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/GroupConfigSubSettingGui.java
new file mode 100644
index 0000000..3d05674
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/GroupConfigSubSettingGui.java
@@ -0,0 +1,412 @@
+package xyz.alexcrea.cuanvil.gui.config.list.elements;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.group.*;
+import xyz.alexcrea.cuanvil.gui.config.SelectGroupContainer;
+import xyz.alexcrea.cuanvil.gui.config.SelectMaterialContainer;
+import xyz.alexcrea.cuanvil.gui.config.ask.ConfirmActionGui;
+import xyz.alexcrea.cuanvil.gui.config.global.GroupConfigGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.GroupSelectSettingGui;
+import xyz.alexcrea.cuanvil.gui.config.settings.MaterialSelectSettingGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant;
+import xyz.alexcrea.cuanvil.util.CasedStringUtil;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class GroupConfigSubSettingGui extends MappedToListSubSettingGui implements SelectGroupContainer, SelectMaterialContainer {
+
+ private final GroupConfigGui parent;
+ private final IncludeGroup group;
+ private final PatternPane pane;
+ private boolean usable = true;
+
+ public GroupConfigSubSettingGui(
+ @NotNull GroupConfigGui parent,
+ @NotNull IncludeGroup group) {
+ super(3,
+ "§e" + CasedStringUtil.snakeToUpperSpacedCase(group.getName()) + " §rConfig");
+ this.parent = parent;
+ this.group = group;
+
+ Pattern pattern = new Pattern(
+ GuiSharedConstant.EMPTY_GUI_FULL_LINE,
+ "00102000D",
+ "B00000000"
+ );
+ this.pane = new PatternPane(0, 0, 9, 3, pattern);
+ addPane(this.pane);
+
+ prepareStaticValues();
+ }
+
+ private GuiItem materialSelection;
+ private GuiItem groupSelection;
+ private void prepareStaticValues() {
+ GuiGlobalItems.addBackItem(this.pane, this.parent);
+ GuiGlobalItems.addBackgroundItem(this.pane);
+
+ // Delete item
+ ItemStack deleteItem = new ItemStack(Material.RED_TERRACOTTA);
+ ItemMeta deleteMeta = deleteItem.getItemMeta();
+
+ deleteMeta.setDisplayName("§4DELETE GROUP");
+ deleteMeta.setLore(Collections.singletonList("§cCaution with this button !"));
+
+ deleteItem.setItemMeta(deleteMeta);
+ this.pane.bindItem('D', new GuiItem(deleteItem, openGuiAndCheckAction(), CustomAnvil.instance));
+
+ // Displayed item will be updated later
+ String materialSelectionName = "§e" + CasedStringUtil.snakeToUpperSpacedCase(group.getName()) + " §rMaterials";
+ ItemStack selectItem = new ItemStack(Material.DIAMOND_SWORD);
+ ItemMeta selectItemMeta = selectItem.getItemMeta();
+ selectItemMeta.setDisplayName(materialSelectionName);
+
+ selectItem.setItemMeta(selectItemMeta);
+ this.materialSelection = new GuiItem(selectItem, (event) -> {
+ event.setCancelled(true);
+ MaterialSelectSettingGui selectGui = new MaterialSelectSettingGui(this,
+ materialSelectionName
+ , this);
+ selectGui.show(event.getWhoClicked());
+
+ }, CustomAnvil.instance);
+
+ String selectGroupName = "§e" + CasedStringUtil.snakeToUpperSpacedCase(this.group.getName()) + " §rGroups";
+ ItemStack selectGroup = new ItemStack(Material.CHEST);
+ ItemMeta selectGroupMeta = selectGroup.getItemMeta();
+ selectGroupMeta.setDisplayName(selectGroupName);
+
+ selectGroup.setItemMeta(selectGroupMeta);
+ this.groupSelection = new GuiItem(selectGroup, (event) -> {
+ event.setCancelled(true);
+ GroupSelectSettingGui enchantGui = new GroupSelectSettingGui(
+ selectGroupName,
+ this, this, 0);
+ enchantGui.show(event.getWhoClicked());
+ }, CustomAnvil.instance);
+
+ this.pane.bindItem('1', this.materialSelection);
+ this.pane.bindItem('2', this.groupSelection);
+ }
+
+ private @NotNull Consumer openGuiAndCheckAction() {
+ ConfirmActionGui deleteGui = createDeleteGui();
+ return event -> {
+ event.setCancelled(true);
+ HumanEntity player = event.getWhoClicked();
+ // Do not allow to open inventory if player do not have edit configuration permission
+ if (!player.hasPermission(CustomAnvil.editConfigPermission)) {
+ player.closeInventory();
+ player.sendMessage(GuiGlobalActions.NO_EDIT_PERM);
+ return;
+ }
+ // test if group is used & cancel & warn user if so
+ if(testAndWarnIfUsed(player)) return;
+
+ deleteGui.show(player);
+ };
+ }
+
+ private @NotNull ConfirmActionGui createDeleteGui() {
+ Supplier deleteSupplier = () -> {
+ // test if group is used & cancel if so
+ if(!getUsedLocations(this.group).isEmpty()) return false;
+
+ ItemGroupManager manager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager();
+
+ // Remove from manager
+ manager.getGroupMap().remove(this.group.getName());
+
+ // Remove from parent
+ this.parent.removeGeneric(this.group);
+
+ // Remove self
+ cleanAndBeUnusable();
+
+ // Update config file storage
+ ConfigHolder.CUSTOM_RECIPE_HOLDER.delete(this.group.getName());
+
+ // Save
+ boolean success = true;
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ success = ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return success;
+ };
+
+ return new ConfirmActionGui("§cDelete §e" + CasedStringUtil.snakeToUpperSpacedCase(this.group.toString()) + "§c?",
+ "§7Confirm that you want to delete this group.",
+ this, this.parent, deleteSupplier
+ );
+ }
+
+ public boolean testAndWarnIfUsed(HumanEntity player){
+ List usedLoc = getUsedLocations(this.group);
+ if(usedLoc.isEmpty()){
+ return false;
+ }
+ StringBuilder stb = new StringBuilder("§cCan't delete group " +this.group.getName()+
+ "\n§eUsed by:");
+ int maxIndex = usedLoc.size();
+ int nbMore = 0;
+ if(maxIndex > 10){
+ nbMore = maxIndex - 9;
+ maxIndex = 9;
+ }
+ for (int i = 0; i < maxIndex; i++) {
+ stb.append("\n§r-§e ").append(usedLoc.get(i));
+ }
+ if(nbMore > 0){
+ stb.append("§cAnd ").append(nbMore).append(" More...");
+ }
+
+ player.sendMessage(stb.toString());
+ return true;
+ }
+
+ // return a string containing every instance of where this group is used
+ public static List getUsedLocations(AbstractMaterialGroup group){
+ ArrayList usageList = new ArrayList<>();
+
+ // Test used by another group
+ ItemGroupManager groupManager = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager();
+ for (AbstractMaterialGroup otherGroup : groupManager.getGroupMap().values()) {
+ if(otherGroup.getGroups().contains(group)) {
+ usageList.add("group " + otherGroup.getName());
+ }
+ }
+
+ // Test if used for conflict
+ EnchantConflictManager conflictManager = ConfigHolder.CONFLICT_HOLDER.getConflictManager();
+ for (EnchantConflictGroup conflict : conflictManager.getConflictList()) {
+ if(conflict.getCantConflictGroup().getGroups().contains(group)) {
+ usageList.add("conflict " + conflict);
+ }
+ }
+
+ return usageList;
+ }
+
+ @Override
+ public void updateGuiValues() {
+ if(!this.usable) return;
+ // Parent should call updateLocal with this call
+ this.parent.updateValueForGeneric(this.group, true);
+
+ }
+
+ @Override
+ public void updateLocal() {
+ if(!this.usable) return;
+ // Prepare material lore
+ List matLore = SelectMaterialContainer.getMaterialLore(this, "group", "include");
+
+ // Prepare group lore
+ List groupLore = SelectGroupContainer.getGroupLore(this, "group", "include");
+
+ // Configure included material setting item
+ ItemStack matSelectItem = this.materialSelection.getItem();
+ ItemMeta matSelectMeta = matSelectItem.getItemMeta();
+
+ matSelectMeta.setDisplayName("§aSelect included §eMaterials §aSettings");
+ matSelectMeta.setLore(matLore);
+ matSelectMeta.addItemFlags(ItemFlag.values());
+
+ matSelectItem.setItemMeta(matSelectMeta);
+
+ this.materialSelection.setItem(matSelectItem); // Just in case
+
+ // Configure enchant setting item
+ ItemStack groupSelectItem = this.groupSelection.getItem();
+ ItemMeta groupSelectMeta = groupSelectItem.getItemMeta();
+
+ groupSelectMeta.setDisplayName("§aSelect included §3Groups §aSettings");
+ groupSelectMeta.setLore(groupLore);
+
+ groupSelectItem.setItemMeta(groupSelectMeta);
+
+ this.groupSelection.setItem(groupSelectItem); // Just in case
+ }
+
+ @Override
+ public void cleanAndBeUnusable() {
+ this.usable = false;
+ this.pane.bindItem('1', GuiGlobalItems.backgroundItem());
+ this.pane.bindItem('2', GuiGlobalItems.backgroundItem());
+ this.pane.bindItem('D', GuiGlobalItems.backgroundItem());
+
+ }
+
+ @Override
+ public void show(@NotNull HumanEntity player) {
+ if(!this.usable) {
+ this.parent.show(player);
+ return;
+ }
+ super.show(player);
+ }
+
+ // ----------------------------
+ // SelectGroupContainer related methods
+ // ----------------------------
+
+ @Override
+ public Set getSelectedGroups() {
+ return this.group.getGroups();
+ }
+
+ @Override
+ public boolean setSelectedGroups(Set groups) {
+ // update group and referencing groups
+ updateGroup(this.group, groups);
+
+ // Save file configuration to disk
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ return ConfigHolder.CONFLICT_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+
+ return true;
+ }
+
+ private void updateGroup(@NotNull AbstractMaterialGroup group, Set groups){
+ // Set live configuration
+ group.setGroups(groups);
+
+ // Write to file configuration
+ groups = group.getGroups(); // Maybe some group may have been rejected
+ String[] groupNames = new String[groups.size()];
+ int index = 0;
+ for (AbstractMaterialGroup otherGroup : groups) {
+ groupNames[index++] = otherGroup.getName();
+ }
+
+ ConfigHolder.ITEM_GROUP_HOLDER.getConfig().set(group.getName()+"."+ItemGroupManager.GROUP_LIST_PATH, groupNames);
+
+ // Try to update referencing group. kind of expensive operation in some case.
+ updateDirectReferencingGroups(group);
+
+ // We assume a backup & save call will be done soon after
+ }
+
+ @Override
+ public Set illegalGroups() {
+ Set illegal = new HashSet<>();
+
+ for (AbstractMaterialGroup otherGroup : ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values()) {
+ if(otherGroup.isReferencing(this.group)){
+ illegal.add(otherGroup);
+ }
+ }
+ illegal.add(this.group);
+
+ return illegal;
+ }
+
+ // ----------------------------
+ // End of SelectGroupContainer related methods
+ // ----------------------------
+ // SelectMaterialContainer related methods
+ // ----------------------------
+
+ @Override
+ public Set getSelectedMaterials() {
+ return this.group.getNonGroupInheritedMaterials();
+ }
+
+ @Override
+ public boolean setSelectedMaterials(Set materials) {
+ this.group.setNonGroupInheritedMaterials(materials);
+
+ // Write to file configuration
+ String[] groupNames = new String[materials.size()];
+ int index = 0;
+ for (NamespacedKey otherGroup : materials) {
+ groupNames[index++] = otherGroup.getKey().toLowerCase();
+ }
+
+ ConfigHolder.ITEM_GROUP_HOLDER.getConfig().set(this.group.getName()+"."+ItemGroupManager.MATERIAL_LIST_PATH, groupNames);
+
+ // update referencing groups
+ updateDirectReferencingGroups(this.group);
+
+ // Save file configuration to disk
+ if (GuiSharedConstant.TEMPORARY_DO_SAVE_TO_DISK_EVERY_CHANGE) {
+ return ConfigHolder.ITEM_GROUP_HOLDER.saveToDisk(GuiSharedConstant.TEMPORARY_DO_BACKUP_EVERY_SAVE);
+ }
+ return true;
+ }
+
+ @Override
+ public Set illegalMaterials() {
+ return Set.of(Material.AIR.getKey());
+ }
+
+ // ----------------------------
+ // End of SelectMaterialContainer related methods
+ // ----------------------------
+
+ private void updateDirectReferencingGroups(AbstractMaterialGroup referenceTo){
+ Collection everyStoredGroups = ConfigHolder.ITEM_GROUP_HOLDER.getItemGroupsManager().getGroupMap().values();
+ List everyConflicts = ConfigHolder.CONFLICT_HOLDER.getConflictManager().getConflictList();
+
+ HashSet toUpdate = new HashSet<>();
+ HashSet updateFuture = new HashSet<>();
+ HashSet conflictGroupPlanned = new HashSet<>();
+
+ updateFuture.add(referenceTo);
+ while (!updateFuture.isEmpty()){
+ HashSet temp = updateFuture;
+ updateFuture = toUpdate;
+ updateFuture.clear();
+ toUpdate = temp;
+
+ for (AbstractMaterialGroup testGroup : toUpdate) {
+ // Update other stored group
+ for (AbstractMaterialGroup otherGroup : everyStoredGroups) {
+ if(otherGroup.getGroups().contains(testGroup)){
+ otherGroup.updateMaterials();
+ updateFuture.add(otherGroup);
+ }
+ }
+
+ // plan update for conflict groups
+ for (EnchantConflictGroup everyConflict : everyConflicts) {
+ AbstractMaterialGroup conflictGroup = everyConflict.getCantConflictGroup();
+ if(conflictGroup.getGroups().contains(testGroup)){
+ conflictGroupPlanned.add(conflictGroup);
+ }
+ }
+
+ // Update parent & local by extension
+ if(testGroup instanceof IncludeGroup){
+ this.parent.updateValueForGeneric((IncludeGroup) testGroup, false);
+ }
+ }
+ }
+ this.parent.update();
+
+ // Update conflict group
+ for (AbstractMaterialGroup conflictGroup : conflictGroupPlanned) {
+ conflictGroup.updateMaterials();
+ }
+
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/MappedToListSubSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/MappedToListSubSettingGui.java
new file mode 100644
index 0000000..1d86781
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/list/elements/MappedToListSubSettingGui.java
@@ -0,0 +1,28 @@
+package xyz.alexcrea.cuanvil.gui.config.list.elements;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
+import io.delilaheve.CustomAnvil;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui;
+
+public abstract class MappedToListSubSettingGui extends ChestGui implements ValueUpdatableGui, ElementMappedToListGui {
+
+ protected MappedToListSubSettingGui(
+ int rows,
+ @NotNull String title) {
+ super(rows, title, CustomAnvil.instance);
+ }
+
+ @Override
+ public Gui getMappedGui() {
+ return this;
+ }
+
+ @Override
+ public Gui getConnectedGui() {
+ return this;
+ }
+
+}
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/AbstractSettingGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/AbstractSettingGui.java
new file mode 100644
index 0000000..b07e7c1
--- /dev/null
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/settings/AbstractSettingGui.java
@@ -0,0 +1,136 @@
+package xyz.alexcrea.cuanvil.gui.config.settings;
+
+import com.github.stefvanschie.inventoryframework.adventuresupport.StringHolder;
+import com.github.stefvanschie.inventoryframework.adventuresupport.TextHolder;
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.pane.PatternPane;
+import com.github.stefvanschie.inventoryframework.pane.util.Pattern;
+import io.delilaheve.CustomAnvil;
+import org.jetbrains.annotations.NotNull;
+import xyz.alexcrea.cuanvil.config.ConfigHolder;
+import xyz.alexcrea.cuanvil.gui.ValueUpdatableGui;
+import xyz.alexcrea.cuanvil.gui.util.GuiGlobalItems;
+
+/**
+ * An instance gui used to edit a setting.
+ */
+public abstract class AbstractSettingGui extends ChestGui implements SettingGui {
+
+ public static final String CLICK_LORE = "§7Click Here to change the value";
+
+ private PatternPane pane;
+
+ /**
+ * Prepare necessary object for a setting gui.
+ *
+ * @param rows Number of row for this gui.
+ * @param title Title of this gui.
+ * @param parent Parent gui to go back when completed.
+ */
+ protected AbstractSettingGui(int rows, @NotNull TextHolder title, ValueUpdatableGui parent) {
+ super(rows, title, CustomAnvil.instance);
+ initBase(parent);
+ }
+
+ /**
+ * Prepare necessary object for a setting gui.
+ *
+ * @param rows Number of row for this gui.
+ * @param title Title of this gui.
+ * @param parent Parent gui to go back when completed.
+ */
+ protected AbstractSettingGui(int rows, @NotNull String title, ValueUpdatableGui parent) {
+ this(rows, StringHolder.of(title), parent);
+ }
+
+ protected GuiItem saveItem;
+
+ /**
+ * Initialise and prepare value for this gui.
+ *
+ * @param parent Parent gui to go back when completed.
+ */
+ protected void initBase(ValueUpdatableGui parent) {
+ Pattern pattern = getGuiPattern();
+ pane = new PatternPane(0, 0, pattern.getLength(), pattern.getHeight(), pattern);
+ addPane(pane);
+
+ GuiGlobalItems.addBackItem(pane, parent.getConnectedGui());
+ GuiGlobalItems.addBackgroundItem(pane);
+
+ saveItem = GuiGlobalItems.saveItem(this, parent);
+
+ pane.bindItem('S', GuiGlobalItems.noChangeItem());
+
+ }
+
+ @Override
+ public void update() {
+ pane.bindItem('S', hadChange() ? saveItem : GuiGlobalItems.noChangeItem());
+ super.update();
+ }
+
+ /**
+ * Get main pane for this setting gui.
+ *
+ * @return Main pattern pain of this gui.
+ */
+ protected PatternPane getPane() {
+ return pane;
+ }
+
+ /**
+ * Used to get the gui pattern.
+ * Reserved character are:
+ *