nameSortedEnchantments;
@@ -62,6 +62,8 @@ public class CAEnchantmentRegistry {
}
+ private static boolean hasWarnedRegistering = false;
+
/**
* Can be used to register new enchantment.
*
@@ -73,19 +75,25 @@ public class CAEnchantmentRegistry {
public boolean register(@NotNull CAEnchantment enchantment){
if(byKeyMap.containsKey(enchantment.getKey())){
CustomAnvil.instance.getLogger().log(Level.WARNING,
- "Duplicate registered enchantment. This should NOT happen.",
+ "Duplicate registered enchantment. This should NOT happen any time.\n" +
+ "If you are a custom anvil developer. You maybe custom anvil detected your enchantment as a bukkit enchantment. " +
+ "maybe remove enchantment with the same key before registering yours",
new IllegalStateException(enchantment.getKey()+" enchantment was already registered"));
return false;
}
- if(byNameMap.containsKey(enchantment.getName())){
+
+ if((!hasWarnedRegistering) && byNameMap.containsKey(enchantment.getName())){
+ hasWarnedRegistering = true;
+
CustomAnvil.instance.getLogger().log(Level.WARNING,
- "Duplicate registered enchantment name. There will have issue. " +
- "\nI hope this do not happen to you on a production server. If it do, there is probably a plugin trying to register an enchantment with the same name than another one",
- new IllegalStateException(enchantment.getKey()+" enchantment name was already registered"));
+ "Duplicate registered enchantment name. Please check that configuration is using namespace.");
}
byKeyMap.put(enchantment.getKey(), enchantment);
- byNameMap.put(enchantment.getName(), enchantment);
+
+ byNameMap.putIfAbsent(enchantment.getName(), new ArrayList<>());
+ byNameMap.get(enchantment.getName()).add(enchantment);
+
nameSortedEnchantments.add(enchantment);
if(!enchantment.isGetOptimised()){
@@ -112,7 +120,7 @@ public class CAEnchantmentRegistry {
public boolean unregister(@Nullable CAEnchantment enchantment){
if(enchantment == null) return false;
byKeyMap.remove(enchantment.getKey());
- byNameMap.remove(enchantment.getName());
+ byNameMap.get(enchantment.getName()).remove(enchantment);
nameSortedEnchantments.remove(enchantment);
@@ -135,10 +143,26 @@ public class CAEnchantmentRegistry {
* Gets the enchantment by the provided name.
* @param name Name to fetch.
* @return Registered enchantment. null if absent.
+ *
+ * @deprecated use {@link #getListByName(String)}
*/
+ @Deprecated(since = "1.6.3")
@Nullable
public CAEnchantment getByName(@NotNull String name){
- return byNameMap.get(name);
+ List enchantments = getListByName(name);
+ if(enchantments.isEmpty()) return null;
+
+ return enchantments.get(0);
+ }
+
+ /**
+ * Gets list of enchantment using the provided name.
+ * @param name Name to fetch.
+ * @return List of registered enchantment.
+ */
+ @NotNull
+ public List getListByName(@NotNull String name){
+ return byNameMap.getOrDefault(name, Collections.emptyList());
}
/**
diff --git a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java
index 6c44fac..a614536 100644
--- a/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java
+++ b/src/main/java/xyz/alexcrea/cuanvil/gui/config/global/EnchantCostConfigGui.java
@@ -44,24 +44,16 @@ public class EnchantCostConfigGui extends AbstractEnchantConfigGui displayLore,
- int min, int max, int defaultItemVal, int defaultBookVal,
- int... steps) {
- return new EnchantCostSettingFactory(
- title, parent,
- configPath, config,
- displayLore,
- min, max, defaultItemVal, defaultBookVal, steps);
- }
-
/**
* A factory for an enchantment cost setting gui that hold setting's information.
*/
public static class EnchantCostSettingFactory extends IntSettingsGui.IntSettingFactory {
int defaultBookVal;
+ @NotNull CAEnchantment enchantment;
/**
* Constructor for an enchantment cost setting gui factory.
*
- * @param title The title of the gui.
- * @param parent Parent gui to go back when completed.
- * @param configPath Configuration path of this setting.
- * @param config Configuration holder of this setting.
- * @param displayLore Gui display item lore.
- * @param min Minimum value of this setting.
- * @param max Maximum value of this setting.
- * @param defaultItemVal Default item value if not found on the config.
- * @param defaultBookVal Default book value if not found on the config.
- * @param steps List of step the value can increment/decrement.
- * List's size should be between 1 (included) and 3 (included).
- * it is visually preferable to have an odd number of step.
- * If step only contain 1 value, no step item should be displayed.
+ * @param title The title of the gui.
+ * @param parent Parent gui to go back when completed.
+ * @param configPath Configuration path of this setting.
+ * @param config Configuration holder of this setting.
+ * @param displayLore Gui display item lore.
+ * @param min Minimum value of this setting.
+ * @param max Maximum value of this setting.
+ * @param enchantment Enchantment to change the cost to
+ * @param steps List of step the value can increment/decrement.
+ * List's size should be between 1 (included) and 3 (included).
+ * it is visually preferable to have an odd number of step.
+ * If step only contain 1 value, no step item should be displayed.
*/
- protected EnchantCostSettingFactory(
+ public EnchantCostSettingFactory(
@NotNull String title, ValueUpdatableGui parent,
@NotNull String configPath, @NotNull ConfigHolder config,
@Nullable List displayLore,
- int min, int max, int defaultItemVal, int defaultBookVal,
- int... steps) {
+ @NotNull CAEnchantment enchantment,
+ int min, int max, int... steps) {
super(title, parent,
configPath, config,
displayLore,
- min, max, defaultItemVal, steps);
- this.defaultBookVal = defaultBookVal;
+ min, max, enchantment.defaultRarity().getItemValue(),
+ steps);
+
+ this.defaultBookVal = enchantment.defaultRarity().getBookValue();
+ this.enchantment = enchantment;
}
/**
@@ -311,14 +285,14 @@ public class EnchantCostSettingsGui extends IntSettingsGui {
*/
@Override
public int getConfiguredValue() {
- return this.config.getConfig().getInt(this.configPath + ITEM_PATH, this.defaultVal);
+ return ConfigOptions.INSTANCE.enchantmentValue(enchantment, false);
}
/**
* @return The configured value for the enchant setting book value.
*/
public int getConfiguredBookValue() {
- return this.config.getConfig().getInt(this.configPath + BOOK_PATH, this.defaultBookVal);
+ return ConfigOptions.INSTANCE.enchantmentValue(enchantment, true);
}
@Override
diff --git a/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java b/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java
index 3211497..af21989 100644
--- a/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java
+++ b/src/main/java/xyz/alexcrea/cuanvil/update/Update_1_21.java
@@ -45,13 +45,13 @@ public class Update_1_21 {
addToStringList(groupConfig, "can_unbreak.groups", "mace");
// Add new enchant conflicts
- addToStringList(conflictConfig, "restriction_density.enchantments", "density");
+ addToStringList(conflictConfig, "restriction_density.enchantments", "minecraft:density");
addToStringList(conflictConfig, "restriction_density.notAffectedGroups", "mace", "enchanted_book");
- addToStringList(conflictConfig, "restriction_breach.enchantments", "breach");
+ addToStringList(conflictConfig, "restriction_breach.enchantments", "minecraft:breach");
addToStringList(conflictConfig, "restriction_breach.notAffectedGroups", "mace", "enchanted_book");
- addToStringList(conflictConfig, "restriction_wind_burst.enchantments", "wind_burst");
+ addToStringList(conflictConfig, "restriction_wind_burst.enchantments", "minecraft:wind_burst");
addToStringList(conflictConfig, "restriction_wind_burst.notAffectedGroups", "mace", "enchanted_book");
// Add mace to conflicts
@@ -59,13 +59,14 @@ public class Update_1_21 {
addToStringList(conflictConfig, "restriction_smite.notAffectedGroups", "mace");
addToStringList(conflictConfig, "restriction_bane_of_arthropods.notAffectedGroups", "mace");
- addToStringList(conflictConfig, "mace_enchant_conflict.enchantments", "density", "breach", "smite", "bane_of_arthropods");
+ addToStringList(conflictConfig, "mace_enchant_conflict.enchantments",
+ "minecraft:density", "minecraft:breach", "minecraft:smite", "minecraft:bane_of_arthropods");
conflictConfig.set("mace_enchant_conflict.maxEnchantmentBeforeConflict", 1);
// Add level limit
- baseConfig.set("enchant_limits.density", 5);
- baseConfig.set("enchant_limits.breach", 4);
- baseConfig.set("enchant_limits.wind_burst", 3);
+ baseConfig.set("enchant_limits.minecraft:density", 5);
+ baseConfig.set("enchant_limits.minecraft:breach", 4);
+ baseConfig.set("enchant_limits.minecraft:wind_burst", 3);
// Add enchant values
baseConfig.set("enchant_values.density.item", 1);
diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt
index c6e7286..f192ff9 100644
--- a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt
+++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt
@@ -301,21 +301,28 @@ object ConfigOptions {
* Get the given [enchantment]'s limit
*/
fun enchantLimit(enchantment: CAEnchantment): Int {
- return enchantLimit(enchantment.enchantmentName)
+ // Test namespace
+ var limit = enchantLimit(enchantment.key.toString())
+ if(limit != null) return limit;
+
+ // Test legacy (name only)
+ limit = enchantLimit(enchantment.enchantmentName)
+ if(limit != null) return limit;
+
+ // get default (and test old legacy if present)
+ return getDefaultLevel(enchantment.enchantmentName)
}
/**
* Get the given [enchantmentName]'s limit
*/
- private fun enchantLimit(enchantmentName: String): Int {
- val default = getDefaultLevel(enchantmentName)
+ private fun enchantLimit(enchantmentName: String): Int? {
val path = "${ENCHANT_LIMIT_ROOT}.$enchantmentName"
return CustomAnvil.instance
.config
- .getInt(path, default)
+ .getInt(path, ENCHANT_LIMIT_RANGE.first-1)
.takeIf { it in ENCHANT_LIMIT_RANGE }
- ?: default
}
/**
@@ -324,7 +331,9 @@ object ConfigOptions {
private fun getDefaultLevel(enchantmentName: String, // compatibility with 1.20.5. TODO better update system
) : Int {
if(enchantmentName == "sweeping_edge"){
- return enchantLimit("sweeping")
+ val limit = enchantLimit("sweeping")
+ if(limit != null) return limit
+
}
return defaultEnchantLimit
}
@@ -337,7 +346,17 @@ object ConfigOptions {
enchantment: CAEnchantment,
isFromBook: Boolean
): Int {
- return enchantmentValue(enchantment.enchantmentName, isFromBook)
+ // Test namespace
+ var limit = enchantmentValue(enchantment.key.toString(), isFromBook)
+ if(limit != null) return limit;
+
+ // Test legacy (name only)
+ limit = enchantmentValue(enchantment.enchantmentName, isFromBook)
+ if(limit != null) return limit;
+
+ // get default (and test old legacy if present)
+ return getDefaultValue(enchantment, isFromBook)
+
}
/**
@@ -347,36 +366,36 @@ object ConfigOptions {
private fun enchantmentValue(
enchantmentName: String,
isFromBook: Boolean
- ): Int {
- val default = getDefaultValue(enchantmentName, isFromBook)
-
+ ): Int? {
val typeKey = if (isFromBook) KEY_BOOK else KEY_ITEM
val path = "${ENCHANT_VALUES_ROOT}.${enchantmentName}.$typeKey"
return CustomAnvil.instance
.config
- .getInt(path, default)
+ .getInt(path, DEFAULT_ENCHANT_VALUE - 1)
.takeIf { it >= DEFAULT_ENCHANT_VALUE }
- ?: DEFAULT_ENCHANT_VALUE
}
/**
* Get default value if enchantment do not exist on config
*/
- private fun getDefaultValue(enchantmentName: String, // compatibility with 1.20.5. TODO better update system
+ private fun getDefaultValue(enchantment: CAEnchantment, // compatibility with 1.20.5. TODO better update system
isFromBook: Boolean) : Int {
- if(enchantmentName == "sweeping_edge"){
- return enchantmentValue("sweeping", isFromBook)
+
+ val enchantmentName = enchantment.key.toString()
+ if(enchantmentName == "minecraft:sweeping_edge"){
+ var limit = enchantmentValue("minecraft:sweeping", isFromBook)
+ if(limit != null) return limit
+
+ // legacy name
+ limit = enchantmentValue("sweeping", isFromBook)
+ if(limit != null) return limit
}
- val enchantment = CAEnchantment.getByName(enchantmentName)
- if(enchantment != null){
- val rarity = enchantment.defaultRarity()
-
- return if(isFromBook) rarity.bookValue
- else rarity.itemValue
- }
-
- return DEFAULT_ENCHANT_VALUE
+ val rarity = enchantment.defaultRarity()
+ return if(isFromBook)
+ rarity.bookValue
+ else
+ rarity.itemValue
}
/**
@@ -384,28 +403,39 @@ object ConfigOptions {
* a negative value would mean never disabled
*/
fun maxBeforeMergeDisabled(enchantment: CAEnchantment): Int {
- return maxBeforeMergeDisabled(enchantment.enchantmentName)
+ val key = enchantment.key.toString()
+ var value = maxBeforeMergeDisabled(key)
+ if(value != null) return value
+
+ // Legacy name
+ val legacy = enchantment.enchantmentName
+ value = maxBeforeMergeDisabled(legacy)
+ if(value != null) return value
+
+ if(key == "minecraft:sweeping_edge"){
+ value = maxBeforeMergeDisabled("minecraft:sweeping")
+ if(value != null) return value
+
+ // legacy name of legacy enchantment name
+ value = maxBeforeMergeDisabled("sweeping")
+ if(value != null) return value
+ }
+
+ return DEFAULT_MAX_BEFORE_MERGE_DISABLED
}
/**
* Get the given [enchantmentName]'s level before merge is disabled
* a negative value would mean never disabled
*/
- private fun maxBeforeMergeDisabled(enchantmentName: String) : Int {
+ private fun maxBeforeMergeDisabled(enchantmentName: String) : Int? {
// find if set
val path = "${DISABLE_MERGE_OVER_ROOT}.$enchantmentName"
- val value = CustomAnvil.instance
+ return CustomAnvil.instance
.config
- .getInt(path, DEFAULT_MAX_BEFORE_MERGE_DISABLED)
+ .getInt(path, ENCHANT_LIMIT_RANGE.min() - 1)
.takeIf { it in ENCHANT_LIMIT_RANGE }
- ?: DEFAULT_MAX_BEFORE_MERGE_DISABLED;
-
- if((value == DEFAULT_MAX_BEFORE_MERGE_DISABLED) && (enchantmentName == "sweeping_edge")){
- return maxBeforeMergeDisabled("sweeping")
- }
-
- return value
}
}
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt
index c58c6bf..56e923f 100644
--- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictGroup.kt
@@ -15,6 +15,9 @@ class EnchantConflictGroup(
fun addEnchantment(enchant: CAEnchantment) {
enchantments.add(enchant)
}
+ fun addEnchantments(enchants: List) {
+ enchantments.addAll(enchants)
+ }
fun allowed(enchants: Set, mat: Material): Boolean {
if (enchantments.size < minBeforeBlock) {
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt
index abebcbc..0c0850b 100644
--- a/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/group/EnchantConflictManager.kt
@@ -30,9 +30,9 @@ class EnchantConflictManager {
const val DEFAULT_GROUP_NAME = "joinedGroup"
// 1.20.5 compatibility TODO better update system
- private val SWEEPING_EDGE_ENCHANT =
+ private val SWEEPING_EDGE_ENCHANT = Collections.singletonList(
CAEnchantment.getByKey(NamespacedKey.minecraft("sweeping_edge")) ?:
- CAEnchantment.getByKey(Enchantment.SWEEPING_EDGE.key)
+ CAEnchantment.getByKey(Enchantment.SWEEPING_EDGE.key))
}
@@ -94,14 +94,14 @@ class EnchantConflictManager {
// Read and add enchantment to conflict
val enchantList = section.getStringList(ENCH_LIST_PATH)
for (enchantName in enchantList) {
- val enchant = getEnchantByName(enchantName)
- if (enchant == null) {
+ val enchants = getEnchantByIdentifier(enchantName)
+ if (enchants.isEmpty()) {
if (!futureUse) { //TODO future use will be deprecated once the new update system is finished
CustomAnvil.instance.logger.warning("Enchantment $enchantName do not exist but was asked for conflict $conflictName")
}
continue
}
- conflict.addEnchantment(enchant)
+ conflict.addEnchantments(enchants)
}
if (conflict.getEnchants().isEmpty()) {
if (!futureUse) { //TODO future use will be deprecated once the new update system is finished
@@ -112,16 +112,23 @@ class EnchantConflictManager {
return conflict
}
- private fun getEnchantByName(enchantName: String): CAEnchantment? {
+ private fun getEnchantByIdentifier(enchantName: String): List {
+ val key = NamespacedKey.fromString(enchantName)
+ if(key != null){
+ val enchantment = CAEnchantment.getByKey(key)
+ if(enchantment != null) return Collections.singletonList(enchantment)
+
+ }
// Temporary solution for 1.20.5
when(enchantName){
- "sweeping", "sweeping_edge" -> {
+ "minecraft:sweeping", "sweeping",
+ "minecraft:sweeping_edge", "sweeping_edge" -> {
return SWEEPING_EDGE_ENCHANT
}
}
- return CAEnchantment.getByName(enchantName)
+ return CAEnchantment.getListByName(enchantName)
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 82b28a8..dd24558 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -97,46 +97,46 @@ default_limit: 5
#
# Valid range of 1 - 255 for each enchantment
enchant_limits:
- aqua_affinity: 1
- binding_curse: 1
- channeling: 1
- flame: 1
- infinity: 1
- mending: 1
- multishot: 1
- silk_touch: 1
- vanishing_curse: 1
- depth_strider: 3 # anything more than 3 is treated as 3 by the game
- protection: 4
- fire_protection: 4
- blast_protection: 4
- projectile_protection: 4
- feather_falling: 4
- thorns: 3
- respiration: 3
- sharpness: 5
- smite: 5
- bane_of_arthropods: 5
- knockback: 2
- fire_aspect: 2
- looting: 3
- sweeping: 3
- sweeping_edge: 3
- efficiency: 5
- unbreaking: 3
- fortune: 3
- power: 5
- punch: 2
- luck_of_the_sea: 3
- lure: 3
- frost_walker: 2
- impaling: 5
- riptide: 3
- loyalty: 3
- piercing: 4
- quick_charge: 3
- soul_speed: 3
- swift_sneak: 3
+ minecraft:aqua_affinity: 1
+ minecraft:binding_curse: 1
+ minecraft:channeling: 1
+ minecraft:flame: 1
+ minecraft:infinity: 1
+ minecraft:mending: 1
+ minecraft:multishot: 1
+ minecraft:silk_touch: 1
+ minecraft:vanishing_curse: 1
+ minecraft:depth_strider: 3 # anything more than 3 is treated as 3 by the game
+ minecraft:protection: 4
+ minecraft:fire_protection: 4
+ minecraft:blast_protection: 4
+ minecraft:projectile_protection: 4
+ minecraft:feather_falling: 4
+ minecraft:thorns: 3
+ minecraft:respiration: 3
+ minecraft:sharpness: 5
+ minecraft:smite: 5
+ minecraft:bane_of_arthropods: 5
+ minecraft:knockback: 2
+ minecraft:fire_aspect: 2
+ minecraft:looting: 3
+ minecraft:sweeping: 3
+ minecraft:sweeping_edge: 3
+ minecraft:efficiency: 5
+ minecraft:unbreaking: 3
+ minecraft:fortune: 3
+ minecraft:power: 5
+ minecraft:punch: 2
+ minecraft:luck_of_the_sea: 3
+ minecraft:lure: 3
+ minecraft:frost_walker: 2
+ minecraft:impaling: 5
+ minecraft:riptide: 3
+ minecraft:loyalty: 3
+ minecraft:piercing: 4
+ minecraft:quick_charge: 3
+ minecraft:soul_speed: 3
+ minecraft:swift_sneak: 3
# Multipliers used to calculate the enchantment's value in repair/combining
#
@@ -150,124 +150,124 @@ enchant_limits:
# With default values protection 4 would have a value of 4 when
# coming from either a book (4 * 1) or an item (4 * 1)
enchant_values:
- aqua_affinity:
+ minecraft:aqua_affinity:
item: 4
book: 2
- bane_of_arthropods:
+ minecraft:bane_of_arthropods:
item: 2
book: 1
- binding_curse:
+ minecraft:binding_curse:
item: 8
book: 4
- blast_protection:
+ minecraft:blast_protection:
item: 4
book: 2
- channeling:
+ minecraft:channeling:
item: 8
book: 4
- depth_strider:
+ minecraft:depth_strider:
item: 4
book: 2
- efficiency:
+ minecraft:efficiency:
item: 1
book: 1
- flame:
+ minecraft:flame:
item: 4
book: 2
- feather_falling:
+ minecraft:feather_falling:
item: 2
book: 1
- fire_aspect:
+ minecraft:fire_aspect:
item: 4
book: 2
- fire_protection:
+ minecraft:fire_protection:
item: 2
book: 1
- fortune:
+ minecraft:fortune:
item: 4
book: 2
- frost_walker:
+ minecraft:frost_walker:
item: 4
book: 2
- impaling:
+ minecraft:impaling:
item: 4
book: 2
- infinity:
+ minecraft:infinity:
item: 8
book: 4
- knockback:
+ minecraft:knockback:
item: 2
book: 1
- looting:
+ minecraft:looting:
item: 4
book: 2
- loyalty:
+ minecraft:loyalty:
item: 1
book: 1
- luck_of_the_sea:
+ minecraft:luck_of_the_sea:
item: 4
book: 2
- lure:
+ minecraft:lure:
item: 4
book: 2
- mending:
+ minecraft:mending:
item: 4
book: 2
- multishot:
+ minecraft:multishot:
item: 4
book: 2
- piercing:
+ minecraft:piercing:
item: 1
book: 1
- power:
+ minecraft:power:
item: 1
book: 1
- projectile_protection:
+ minecraft:projectile_protection:
item: 2
book: 1
- protection:
+ minecraft:protection:
item: 1
book: 1
- punch:
+ minecraft:punch:
item: 4
book: 2
- quick_charge:
+ minecraft:quick_charge:
item: 2
book: 1
- respiration:
+ minecraft:respiration:
item: 4
book: 2
- riptide:
+ minecraft:riptide:
item: 4
book: 2
- silk_touch:
+ minecraft:silk_touch:
item: 8
book: 4
- sharpness:
+ minecraft:sharpness:
item: 1
book: 1
- smite:
+ minecraft:smite:
item: 2
book: 1
- soul_speed:
+ minecraft:soul_speed:
item: 8
book: 4
- swift_sneak:
+ minecraft:swift_sneak:
item: 8
book: 4
- sweeping:
+ minecraft:sweeping:
item: 4
book: 2
- sweeping_edge:
+ minecraft:sweeping_edge:
item: 4
book: 2
- thorns:
+ minecraft:thorns:
item: 8
book: 4
- unbreaking:
+ minecraft:unbreaking:
item: 2
book: 1
- vanishing_curse:
+ minecraft:vanishing_curse:
item: 8
book: 4
@@ -278,9 +278,9 @@ enchant_values:
# -1 mean enchantment merge for this enchantment is not disabled. default to -1 if absent.
disable-merge-over:
# Sharpness is set to -1. it equivalent to it not being set to anything (and work as vanilla)
- sharpness: -1
+ minecraft:sharpness: -1
# If uncommented. 2 unbreaking II book would not give an unbreaking III book. but unbreaking III book can still be applied
- #unbreaking: 2
+ #minecraft:unbreaking: 2
# Whether to show debug logging
debug_log: false
diff --git a/src/main/resources/enchant_conflict.yml b/src/main/resources/enchant_conflict.yml
index ed154ab..0e8b3f3 100644
--- a/src/main/resources/enchant_conflict.yml
+++ b/src/main/resources/enchant_conflict.yml
@@ -17,158 +17,158 @@
# ----------------------------------------------------
restriction_aqua_affinity:
- enchantments: [ aqua_affinity ]
+ enchantments: [ minecraft:aqua_affinity ]
notAffectedGroups: [ enchanted_book, helmets ]
restriction_bane_of_arthropods:
- enchantments: [ bane_of_arthropods ]
+ enchantments: [ minecraft:bane_of_arthropods ]
notAffectedGroups: [ enchanted_book, melee_weapons ]
restriction_blast_protection:
- enchantments: [ blast_protection ]
+ enchantments: [ minecraft:blast_protection ]
notAffectedGroups: [ enchanted_book, armors ]
restriction_channeling:
- enchantments: [ channeling ]
+ enchantments: [ minecraft:channeling ]
notAffectedGroups: [ enchanted_book, trident ]
restriction_binding_curse:
- enchantments: [ binding_curse ]
+ enchantments: [ minecraft:binding_curse ]
notAffectedGroups: [ enchanted_book, wearable ]
restriction_vanishing_curse:
- enchantments: [ vanishing_curse ]
+ enchantments: [ minecraft:vanishing_curse ]
notAffectedGroups: [ enchanted_book, can_vanish ]
restriction_depth_strider:
- enchantments: [ depth_strider ]
+ enchantments: [ minecraft:depth_strider ]
notAffectedGroups: [ enchanted_book, boots ]
restriction_efficiency:
- enchantments: [ efficiency ]
+ enchantments: [ minecraft:efficiency ]
notAffectedGroups: [ enchanted_book, tools, shears ]
restriction_feather_falling:
- enchantments: [ feather_falling ]
+ enchantments: [ minecraft:feather_falling ]
notAffectedGroups: [ enchanted_book, boots ]
restriction_fire_aspect:
- enchantments: [ fire_aspect ]
+ enchantments: [ minecraft:fire_aspect ]
notAffectedGroups: [ enchanted_book, swords ]
restriction_fire_protection:
- enchantments: [ fire_protection ]
+ enchantments: [ minecraft:fire_protection ]
notAffectedGroups: [ enchanted_book, armors ]
restriction_flame:
- enchantments: [ flame ]
+ enchantments: [ minecraft:flame ]
notAffectedGroups: [ enchanted_book, bow ]
restriction_fortune:
- enchantments: [ fortune ]
+ enchantments: [ minecraft:fortune ]
notAffectedGroups: [ enchanted_book, tools ]
restriction_frost_walker:
- enchantments: [ frost_walker ]
+ enchantments: [ minecraft:frost_walker ]
notAffectedGroups: [ enchanted_book, boots ]
restriction_impaling:
- enchantments: [ impaling ]
+ enchantments: [ minecraft:impaling ]
notAffectedGroups: [ enchanted_book, trident ]
restriction_infinity:
- enchantments: [ infinity ]
+ enchantments: [ minecraft:infinity ]
notAffectedGroups: [ enchanted_book, bow ]
restriction_knockback:
- enchantments: [ knockback ]
+ enchantments: [ minecraft:knockback ]
notAffectedGroups: [ enchanted_book, swords ]
restriction_looting:
- enchantments: [ looting ]
+ enchantments: [ minecraft:looting ]
notAffectedGroups: [ enchanted_book, swords ]
restriction_loyalty:
- enchantments: [ loyalty ]
+ enchantments: [ minecraft:loyalty ]
notAffectedGroups: [ enchanted_book, trident ]
restriction_lure:
- enchantments: [ lure ]
+ enchantments: [ minecraft:lure ]
notAffectedGroups: [ enchanted_book, fishing_rod ]
restriction_mending:
- enchantments: [ mending ]
+ enchantments: [ minecraft:mending ]
notAffectedGroups: [ enchanted_book, can_unbreak ]
-restriction_multishot:
- enchantments: [ multishot ]
+restriction_minecraft_multishot:
+ enchantments: [ minecraft:multishot ]
notAffectedGroups: [ enchanted_book, crossbow ]
restriction_piercing:
- enchantments: [ piercing ]
+ enchantments: [ minecraft:piercing ]
notAffectedGroups: [ enchanted_book, crossbow ]
restriction_power:
- enchantments: [ power ]
+ enchantments: [ minecraft:power ]
notAffectedGroups: [ enchanted_book, bow ]
restriction_projectile_protection:
- enchantments: [ projectile_protection ]
+ enchantments: [ minecraft:projectile_protection ]
notAffectedGroups: [ enchanted_book, armors ]
restriction_protection:
- enchantments: [ protection ]
+ enchantments: [ minecraft:protection ]
notAffectedGroups: [ enchanted_book, armors ]
restriction_punch:
- enchantments: [ punch ]
+ enchantments: [ minecraft:punch ]
notAffectedGroups: [ enchanted_book, bow ]
restriction_quick_charge:
- enchantments: [ quick_charge ]
+ enchantments: [ minecraft:quick_charge ]
notAffectedGroups: [ enchanted_book, crossbow ]
restriction_respiration:
- enchantments: [ respiration ]
+ enchantments: [ minecraft:respiration ]
notAffectedGroups: [ enchanted_book, helmets ]
restriction_riptide:
- enchantments: [ riptide ]
+ enchantments: [ minecraft:riptide ]
notAffectedGroups: [ enchanted_book, trident ]
restriction_sharpness:
- enchantments: [ sharpness ]
+ enchantments: [ minecraft:sharpness ]
notAffectedGroups: [ enchanted_book, melee_weapons ]
-restriction_silk_touch:
- enchantments: [ silk_touch ]
+restriction__silk_touch:
+ enchantments: [ minecraft:silk_touch ]
notAffectedGroups: [ enchanted_book, tools ]
restriction_smite:
- enchantments: [ smite ]
+ enchantments: [ minecraft:smite ]
notAffectedGroups: [ enchanted_book, melee_weapons ]
restriction_soul_speed:
- enchantments: [ soul_speed ]
+ enchantments: [ minecraft:soul_speed ]
notAffectedGroups: [ enchanted_book, boots ]
restriction_sweeping_edge:
- enchantments: [ sweeping, sweeping_edge ]
+ enchantments: [ minecraft:sweeping, minecraft:sweeping_edge ]
notAffectedGroups: [ enchanted_book, swords ]
# Do not exist in 1.18, that mean useInFuture will be set to true
# useInFuture set to true also mean it will not warn if there is an issue
restriction_swift_sneak:
useInFuture: true
- enchantments: [ swift_sneak ]
+ enchantments: [ minecraft:swift_sneak ]
notAffectedGroups: [ enchanted_book, leggings ]
restriction_thorns:
- enchantments: [ thorns ]
+ enchantments: [ minecraft:thorns ]
notAffectedGroups: [ enchanted_book, armors ]
-restriction_unbreaking:
- enchantments: [ unbreaking ]
+restriction__unbreaking:
+ enchantments: [ minecraft:unbreaking ]
notAffectedGroups: [ enchanted_book, can_unbreak ]
# ----------------------------------------------------
@@ -180,60 +180,60 @@ restriction_unbreaking:
sword_enchant_conflict:
enchantments:
- - bane_of_arthropods
- - smite
- - sharpness
+ - minecraft:bane_of_arthropods
+ - minecraft:smite
+ - minecraft:sharpness
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
protection_enchant_conflict:
enchantments:
- - blast_protection
- - fire_protection
- - projectile_protection
- - protection
+ - minecraft:blast_protection
+ - minecraft:fire_protection
+ - minecraft:projectile_protection
+ - minecraft:protection
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
trident_conflict1:
enchantments:
- - channeling
- - riptide
+ - minecraft:channeling
+ - minecraft:riptide
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
trident_conflict2:
enchantments:
- - loyalty
- - riptide
+ - minecraft:loyalty
+ - minecraft:riptide
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
boot_conflict:
enchantments:
- - depth_strider
- - frost_walker
+ - minecraft:depth_strider
+ - minecraft:frost_walker
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
tool_conflict:
enchantments:
- - fortune
- - silk_touch
+ - minecraft:fortune
+ - minecraft:silk_touch
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
bow_conflict:
enchantments:
- - mending
- - infinity
+ - minecraft:mending
+ - minecraft:infinity
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
crossbow_conflict:
enchantments:
- - multishot
- - piercing
+ - minecraft:multishot
+ - minecraft:piercing
notAffectedGroups: [ ]
maxEnchantmentBeforeConflict: 1
From 7c283dc7f8407d94724dbc572e31053f6bd44ea9 Mon Sep 17 00:00:00 2001
From: alexcrea <42614139+alexcrea@users.noreply.github.com>
Date: Tue, 15 Oct 2024 06:46:05 +0200
Subject: [PATCH 068/495] Splitted main logic to be more readable
---
.../io/delilaheve/AnvilEventListener.kt | 580 ------------------
src/main/kotlin/io/delilaheve/CustomAnvil.kt | 7 +-
.../dependency/DisenchantmentDependency.kt | 6 +-
.../cuanvil/listener/AnvilCloseListener.kt | 23 +
.../cuanvil/listener/AnvilResultListener.kt | 292 +++++++++
.../cuanvil/listener/PrepareAnvilListener.kt | 206 +++++++
.../alexcrea/cuanvil/util/AnvilColorUtil.kt | 88 +++
.../alexcrea/cuanvil/util/CustomRecipeUtil.kt | 54 ++
8 files changed, 672 insertions(+), 584 deletions(-)
delete mode 100644 src/main/kotlin/io/delilaheve/AnvilEventListener.kt
create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt
create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt
create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt
create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt
create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt
diff --git a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt
deleted file mode 100644
index bdb1ac9..0000000
--- a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt
+++ /dev/null
@@ -1,580 +0,0 @@
-package io.delilaheve
-
-import io.delilaheve.util.ConfigOptions
-import io.delilaheve.util.EnchantmentUtil.combineWith
-import io.delilaheve.util.ItemUtil.canMergeWith
-import io.delilaheve.util.ItemUtil.findEnchantments
-import io.delilaheve.util.ItemUtil.isEnchantedBook
-import io.delilaheve.util.ItemUtil.repairFrom
-import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe
-import io.delilaheve.util.ItemUtil.unitRepair
-import org.bukkit.ChatColor
-import org.bukkit.GameMode
-import org.bukkit.Material
-import org.bukkit.entity.HumanEntity
-import org.bukkit.entity.Player
-import org.bukkit.event.Event
-import org.bukkit.event.EventHandler
-import org.bukkit.event.EventPriority.HIGHEST
-import org.bukkit.event.Listener
-import org.bukkit.event.inventory.ClickType
-import org.bukkit.event.inventory.InventoryClickEvent
-import org.bukkit.event.inventory.InventoryCloseEvent
-import org.bukkit.event.inventory.PrepareAnvilEvent
-import org.bukkit.inventory.AnvilInventory
-import org.bukkit.inventory.InventoryView.Property.REPAIR_COST
-import org.bukkit.inventory.ItemStack
-import xyz.alexcrea.cuanvil.config.ConfigHolder
-import xyz.alexcrea.cuanvil.dependency.DependencyManager
-import xyz.alexcrea.cuanvil.dependency.packet.PacketManager
-import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe
-import xyz.alexcrea.cuanvil.util.AnvilXpUtil.calculatePenalty
-import xyz.alexcrea.cuanvil.util.AnvilXpUtil.getRightValues
-import xyz.alexcrea.cuanvil.util.AnvilXpUtil.setAnvilInvXp
-import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair
-import java.util.regex.Matcher
-import java.util.regex.Pattern
-import kotlin.math.min
-
-
-/**
- * Listener for anvil events
- */
-class AnvilEventListener(private val packetManager: PacketManager) : Listener {
-
- companion object {
- // Anvil's output slot
- const val ANVIL_INPUT_LEFT = 0
- const val ANVIL_INPUT_RIGHT = 1
- const val ANVIL_OUTPUT_SLOT = 2
-
- // static slot container
- private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0)
- private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0)
- }
-
- /**
- * Event handler logic for when an anvil contains items to be combined
- */
- @EventHandler(priority = HIGHEST, ignoreCancelled = true)
- fun anvilCombineCheck(event: PrepareAnvilEvent) {
- // Test if the event should bypass custom anvil.
- if(DependencyManager.tryEventPreAnvilBypass(event)) return
-
- val inventory = event.inventory
- val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return
- val second = inventory.getItem(ANVIL_INPUT_RIGHT)
-
- // Should find player
- val player = event.view.player
- if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return
-
- // Test custom recipe
- val recipe = getCustomRecipe(first, second)
- CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}")
- if(recipe != null){
- val amount = getCustomRecipeAmount(recipe, first, second)
-
- val resultItem: ItemStack = recipe.resultItem!!.clone()
- resultItem.amount *= amount
-
- event.result = resultItem
- setAnvilInvXp(inventory, event.view, recipe.xpCostPerCraft * amount, true)
-
- return
- }
-
- // Test rename lonely item
- if (second == null) {
- val resultItem = first.clone()
- var anvilCost = handleRename(resultItem, inventory, player)
-
- // Test/stop if nothing changed.
- if (first == resultItem) {
- CustomAnvil.log("no right item, But input is same as output")
- event.result = null
- return
- }
-
- event.result = resultItem
-
- anvilCost += calculatePenalty(first, null, resultItem)
-
- setAnvilInvXp(inventory, event.view, anvilCost)
- return
- }
-
- // Test for merge
- if (first.canMergeWith(second)) {
- val newEnchants = first.findEnchantments()
- .combineWith(second.findEnchantments(), first, player)
- val resultItem = first.clone()
- resultItem.setEnchantmentsUnsafe(newEnchants)
-
- // Calculate enchantment cost
- var anvilCost = getRightValues(second, resultItem)
- // Calculate repair cost
- if (!first.isEnchantedBook() && !second.isEnchantedBook()) {
- // we only need to be concerned with repair when neither item is a book
- val repaired = resultItem.repairFrom(first, second)
- anvilCost += if (repaired) ConfigOptions.itemRepairCost else 0
- }
-
- // Test/stop if nothing changed.
- if (first == resultItem) {
- CustomAnvil.log("Mergable with second, But input is same as output")
- event.result = null
- return
- }
- // As calculatePenalty edit result, we need to calculate penalty after checking equality
- anvilCost += calculatePenalty(first, second, resultItem)
- // Calculate rename cost
- anvilCost += handleRename(resultItem, inventory, player)
-
- // Finally, we set result
- event.result = resultItem
-
- setAnvilInvXp(inventory, event.view, anvilCost)
- return
- }
-
- // Test for unit repair
- val unitRepairAmount = first.getRepair(second)
- if (unitRepairAmount != null) {
- val resultItem = first.clone()
- var anvilCost = handleRename(resultItem, inventory, player)
-
- val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount)
- if (repairAmount > 0) {
- anvilCost += repairAmount * ConfigOptions.unitRepairCost
- }
- // We do not care about right item penalty for unit repair
- anvilCost += calculatePenalty(first, null, resultItem, true)
-
- // Test/stop if nothing changed.
- if (first == resultItem) {
- CustomAnvil.log("unit repair, But input is same as output")
- event.result = null
- return
- }
- event.result = resultItem
-
- setAnvilInvXp(inventory, event.view, anvilCost)
- } else {
- CustomAnvil.log("no anvil fuse type found")
- event.result = null
- }
-
- }
-
- private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int {
- // Rename item and add renaming cost
- resultItem.itemMeta?.let {
- val displayName = ChatColor.stripColor(it.displayName)
- var inventoryName = ChatColor.stripColor(inventory.renameText)
-
- var sumCost = 0
-
- var useColor = false
- if(ConfigOptions.renameColorPossible){
- val resultString = StringBuilder(inventoryName)
-
- useColor = handleRenamingColor(resultString, player)
-
- if(useColor) {
- inventoryName = resultString.toString()
-
- sumCost+= ConfigOptions.useOfColorCost
- }
- }
-
- if ((!useColor && (!displayName.contentEquals(inventoryName))) || (useColor && !(it.displayName).contentEquals(inventoryName))) {
- it.setDisplayName(inventoryName)
- resultItem.itemMeta = it
-
- sumCost+= ConfigOptions.itemRenameCost
- }
-
- return sumCost
- }
- return 0
- }
-
- private fun handleRenamingColor(textToColor: StringBuilder, player: HumanEntity): Boolean {
- val usePermission = ConfigOptions.permissionNeededForColor
- val canUseColorCode = ConfigOptions.allowColorCode && (!usePermission || player.hasPermission("ca.color.code"))
- val canUseHexColor = ConfigOptions.allowHexadecimalColor && (!usePermission || player.hasPermission("ca.color.hex"))
-
- if((!canUseColorCode) && (!canUseHexColor)) return false
-
- var useColor = false
- // Handle color code
- if(canUseColorCode){
- var nbReplacement = replaceAll(textToColor, "&", "§", 2)
- nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2)
-
- if(nbReplacement > 0) useColor = true
- }
-
- if(canUseHexColor){
- val nbReplacement = replaceHexToColor(textToColor, 7)
-
- if(nbReplacement > 0) useColor = true
- }
-
- return useColor
- }
-
- /**
- * Replace every instance of "from" to "to".
- * @param builder The builder to replace the string from.
- * @param from The source that should be replaced.
- * @param to The string that should replace.
- * @param endOffset Amount of character that should be ignored at the end.
- * @return The number of replacement was that was done.
- */
- private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int {
- var index = builder.indexOf(from)
- var numberOfChanges = 0
-
- while (index != -1 && index < builder.length - endOffset) {
- builder.replace(index, index + from.length, to)
- index += to.length
- index = builder.indexOf(from, index)
-
- numberOfChanges+=1
- }
-
- return numberOfChanges
- }
-
- val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string
- /**
- * Replace every hex color formatted like #000000 to the minecraft format
- * @param builder The builder to replace the hex color from.
- * @param endOffset Amount of character that should be ignored at the end.
- * @return The number of replacement was that was done.
- */
- private fun replaceHexToColor(builder: StringBuilder, endOffset: Int): Int {
- val matcher: Matcher = HEX_PATTERN.matcher(builder)
-
- var numberOfChanges = 0
- var startIndex = 0
-
- while(matcher.find(startIndex)){
- startIndex = matcher.start()
- if(startIndex >= builder.length - endOffset) break
-
- builder.replace(startIndex, startIndex + 1, "§x")
- startIndex+=2
- for (i in 0..5) {
- builder.insert(startIndex, '§')
- startIndex+=2
- }
-
- numberOfChanges+=1
- }
-
- return numberOfChanges
- }
-
- /**
- * Event handler logic for when a player is trying to pull an item out of the anvil
- */
- @EventHandler(ignoreCancelled = true)
- fun anvilExtractionCheck(event: InventoryClickEvent) {
- val player = event.whoClicked as? Player ?: return
- if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return
- val inventory = event.inventory as? AnvilInventory ?: return
-
- if (event.rawSlot != ANVIL_OUTPUT_SLOT) {
- return
- }
- // Test if the event should bypass custom anvil.
- if(DependencyManager.tryClickAnvilResultBypass(event, inventory)) return
-
- val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return
- val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return
- val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT)
-
- // Test custom recipe
- val recipe = getCustomRecipe(leftItem, rightItem)
- if(recipe != null){
- event.result = Event.Result.ALLOW
- onCustomCraft(
- event, recipe, player,
- leftItem, rightItem, output, inventory)
- return
- }
-
- val canMerge = leftItem.canMergeWith(rightItem)
- val unitRepairResult = leftItem.getRepair(rightItem)
- val allowed = (rightItem == null)
- || (canMerge)
- || (unitRepairResult != null)
-
- // True if there was no change or not allowed
- if ((output == inventory.getItem(ANVIL_INPUT_LEFT))
- || !allowed
- ) {
- event.result = Event.Result.DENY
- return
- }
- if (rightItem == null) {
- event.result = Event.Result.ALLOW
- return
- }
- if (canMerge) {
- event.result = Event.Result.ALLOW
- } else if (unitRepairResult != null) {
- onUnitRepairExtract(
- leftItem, rightItem, output,
- unitRepairResult, event, player, inventory
- )
-
- return
- }
- }
-
- private fun onCustomCraft(event: InventoryClickEvent,
- recipe: AnvilCustomRecipe,
- player: Player,
- leftItem: ItemStack,
- rightItem: ItemStack?,
- output: ItemStack,
- inventory: AnvilInventory) {
- event.result = Event.Result.DENY
-
- if(recipe.leftItem == null) return // in case it changed
-
- val amount = getCustomRecipeAmount(recipe, leftItem, rightItem)
- val xpCost = amount * recipe.xpCostPerCraft
-
- if ((player.gameMode != GameMode.CREATIVE) && (player.level < xpCost)) return
-
- // We give the item manually
- // But first we check if we should give the item
- val slotDestination = getActionSlot(event, player)
- if (slotDestination.type == SlotType.NO_SLOT) return
-
- // If not creative middle click...
- if (event.click != ClickType.MIDDLE) {
- // We remove what should be removed
- leftItem.amount -= amount * recipe.leftItem!!.amount
- inventory.setItem(ANVIL_INPUT_LEFT, leftItem)
-
- if(rightItem != null){
- if(recipe.rightItem == null) return // in case it changed
-
- rightItem.amount -= amount * recipe.rightItem!!.amount
- inventory.setItem(ANVIL_INPUT_RIGHT, rightItem)
- }
-
- if(player.gameMode != GameMode.CREATIVE){
- player.level -= xpCost
- }
-
- // Then we try to find the new values for the anvil
- val newAmount = getCustomRecipeAmount(recipe, leftItem, rightItem)
-
- CustomAnvil.verboseLog("new amount is $newAmount")
- if(newAmount <= 0 || recipe.exactCount){
- inventory.setItem(ANVIL_OUTPUT_SLOT, null)
- }else{
- val resultItem: ItemStack = recipe.resultItem!!.clone()
- resultItem.amount *= newAmount
-
- val newXp = newAmount * newAmount
-
- inventory.repairCost = newXp
- event.view.setProperty(REPAIR_COST, newXp)
-
- inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem)
-
- player.updateInventory()
- }
- }
-
- // Finally, we add the item to the player
- if (slotDestination.type == SlotType.CURSOR) {
- player.setItemOnCursor(output)
- } else {// We assume SlotType == SlotType.INVENTORY
- player.inventory.setItem(slotDestination.slot, output)
- }
-
-
- }
-
- private fun onUnitRepairExtract(
- leftItem: ItemStack,
- rightItem: ItemStack,
- output: ItemStack,
- unitRepairResult: Double,
- event: InventoryClickEvent,
- player: Player,
- inventory: AnvilInventory
- ) {
- val resultCopy = leftItem.clone()
- val resultAmount = resultCopy.unitRepair(
- rightItem.amount, unitRepairResult
- )
-
- // To avoid vanilla, we cancel the event for unit repair
- event.result = Event.Result.DENY
- event.isCancelled = true
- // And we give the item manually
- // But first we check if we should give the item
- val slotDestination = getActionSlot(event, player)
- if (slotDestination.type == SlotType.NO_SLOT) return
-
- // Test repair cost
- var repairCost = 0
- if (player.gameMode != GameMode.CREATIVE) {
- // Get repairCost
- leftItem.itemMeta?.let { leftMeta ->
- val leftName = leftMeta.displayName
- output.itemMeta?.let {
- // Rename cost
- if (!leftName.contentEquals(it.displayName)) {
- repairCost += ConfigOptions.itemRenameCost
-
- // Color cost
- if(it.displayName.contains('§')){
- repairCost += ConfigOptions.useOfColorCost
- }
- }
- }
- }
-
- repairCost += calculatePenalty(leftItem, null, resultCopy)
- repairCost += resultAmount * ConfigOptions.unitRepairCost
-
- if (
- !ConfigOptions.doRemoveCostLimit &&
- ConfigOptions.doCapCost) {
-
- repairCost = min(repairCost, ConfigOptions.maxAnvilCost)
- }
-
- if ((inventory.maximumRepairCost <= repairCost)
- || (player.level < repairCost)
- ) return
- }
- // If not creative middle click...
- if (event.click != ClickType.MIDDLE) {
- // We remove what should be removed
- inventory.setItem(ANVIL_INPUT_LEFT, null)
- rightItem.amount -= resultAmount
- inventory.setItem(ANVIL_INPUT_RIGHT, rightItem)
- inventory.setItem(ANVIL_OUTPUT_SLOT, null)
- player.level -= repairCost
- }
-
- // Finally, we add the item to the player
- if (slotDestination.type == SlotType.CURSOR) {
- player.setItemOnCursor(output)
- } else {// We assume SlotType == SlotType.INVENTORY
- player.inventory.setItem(slotDestination.slot, output)
- }
- }
-
- /**
- * Get the destination slot or "NO_SLOT" slot container if there is no slot available
- */
- private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer {
- if (event.isShiftClick) {
- val inventory = player.inventory
- val firstEmpty = inventory.firstEmpty()
- if (firstEmpty == -1) {
- return NO_SLOT
- }
- //check hotbare full
- var slotIndex = 8
- while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) {
- slotIndex--
- }
- if (slotIndex >= 0) {
- return SlotContainer(SlotType.INVENTORY, slotIndex)
- }
- slotIndex = 35 //4*9 - 1 (max of player inventory)
- while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) {
- slotIndex--
- }
- if (slotIndex < 9) {
- return NO_SLOT
- }
- return SlotContainer(SlotType.INVENTORY, slotIndex)
- } else {
- if (player.itemOnCursor.type != Material.AIR) {
- return NO_SLOT
- }
- return CURSOR_SLOT
- }
- }
-
- private fun getCustomRecipe (
- leftItem: ItemStack,
- rightItem: ItemStack?) : AnvilCustomRecipe? {
-
- val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null
-
- CustomAnvil.verboseLog("Testing " + recipeList.size+" recipe...")
- for (recipe in recipeList) {
- if(recipe.testItem(leftItem, rightItem)){
- return recipe
- }
- }
-
- return null
- }
-
- private fun getCustomRecipeAmount(
- recipe: AnvilCustomRecipe,
- leftItem: ItemStack,
- rightItem: ItemStack?
- ): Int{
- return if(recipe.exactCount) {
- if(leftItem.amount != recipe.leftItem!!.amount){
- 0
- }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){
- 0
- }else{
- 1
- }
- }
- else {
- // test amount
- val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us
- val maxResultAmount = resultItem.type.maxStackSize/resultItem.amount
- val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount
- val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount }
-
- CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount")
-
- min(min(maxResultAmount, maxLeftAmount), maxRightAmount)
- }
- }
-
-
- @EventHandler
- fun onAnvilClose(event: InventoryCloseEvent){
- val player = event.player
- if(event.inventory !is AnvilInventory) return
- if(player is Player && GameMode.CREATIVE != player.gameMode){
- packetManager.setInstantBuild(player, false)
- }
-
- }
-
-}
-
-
-
-
-private class SlotContainer(val type: SlotType, val slot: Int)
-private enum class SlotType {
- CURSOR,
- INVENTORY,
- NO_SLOT
-
-}
diff --git a/src/main/kotlin/io/delilaheve/CustomAnvil.kt b/src/main/kotlin/io/delilaheve/CustomAnvil.kt
index cbeb5cd..285e1c5 100644
--- a/src/main/kotlin/io/delilaheve/CustomAnvil.kt
+++ b/src/main/kotlin/io/delilaheve/CustomAnvil.kt
@@ -13,7 +13,10 @@ import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry
import xyz.alexcrea.cuanvil.gui.config.MainConfigGui
import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant
+import xyz.alexcrea.cuanvil.listener.AnvilCloseListener
+import xyz.alexcrea.cuanvil.listener.AnvilResultListener
import xyz.alexcrea.cuanvil.listener.ChatEventListener
+import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener
import xyz.alexcrea.cuanvil.update.PluginSetDefault
import xyz.alexcrea.cuanvil.update.Update_1_21
import xyz.alexcrea.cuanvil.update.plugin.PluginUpdates
@@ -110,7 +113,9 @@ class CustomAnvil : JavaPlugin() {
DependencyManager.loadDependency()
// Register anvil events
- server.pluginManager.registerEvents(AnvilEventListener(DependencyManager.packetManager), this)
+ server.pluginManager.registerEvents(PrepareAnvilListener(), this)
+ server.pluginManager.registerEvents(AnvilResultListener(), this)
+ server.pluginManager.registerEvents(AnvilCloseListener(DependencyManager.packetManager), this)
// Load metrics
Metrics(this, bstatsPluginId)
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt
index be27c05..92ee146 100644
--- a/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/dependency/DisenchantmentDependency.kt
@@ -4,13 +4,13 @@ import cz.kominekjan.disenchantment.events.DisenchantClickEvent
import cz.kominekjan.disenchantment.events.DisenchantEvent
import cz.kominekjan.disenchantment.events.ShatterClickEvent
import cz.kominekjan.disenchantment.events.ShatterEvent
-import io.delilaheve.AnvilEventListener
import io.delilaheve.CustomAnvil
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.AnvilInventory
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.RegisteredListener
+import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener
import xyz.alexcrea.cuanvil.util.AnvilXpUtil
class DisenchantmentDependency {
@@ -96,7 +96,7 @@ class DisenchantmentDependency {
}
fun testAnvilResult(event: InventoryClickEvent, inventory: AnvilInventory): Boolean {
- val previousResultSlot = inventory.getItem(AnvilEventListener.ANVIL_OUTPUT_SLOT)?.clone()
+ val previousResultSlot = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT)?.clone()
// Test event if change the result
itemClickEvent.onDisenchantmentClickEvent(event)
@@ -115,7 +115,7 @@ class DisenchantmentDependency {
}
private fun testAnvilInventoryChange(inventory: AnvilInventory, previous: ItemStack?): Boolean {
- val currentResult = inventory.getItem(AnvilEventListener.ANVIL_OUTPUT_SLOT)
+ val currentResult = inventory.getItem(PrepareAnvilListener.ANVIL_OUTPUT_SLOT)
return currentResult == previous
}
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt
new file mode 100644
index 0000000..60a0339
--- /dev/null
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilCloseListener.kt
@@ -0,0 +1,23 @@
+package xyz.alexcrea.cuanvil.listener
+
+import org.bukkit.GameMode
+import org.bukkit.entity.Player
+import org.bukkit.event.EventHandler
+import org.bukkit.event.Listener
+import org.bukkit.event.inventory.InventoryCloseEvent
+import org.bukkit.inventory.AnvilInventory
+import xyz.alexcrea.cuanvil.dependency.packet.PacketManager
+
+class AnvilCloseListener(private val packetManager: PacketManager) : Listener {
+
+ @EventHandler
+ fun onAnvilClose(event: InventoryCloseEvent){
+ val player = event.player
+ if(event.inventory !is AnvilInventory) return
+ if(player is Player && GameMode.CREATIVE != player.gameMode){
+ packetManager.setInstantBuild(player, false)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt
new file mode 100644
index 0000000..fbc603d
--- /dev/null
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/AnvilResultListener.kt
@@ -0,0 +1,292 @@
+package xyz.alexcrea.cuanvil.listener
+
+import io.delilaheve.CustomAnvil
+import io.delilaheve.util.ConfigOptions
+import io.delilaheve.util.ItemUtil.canMergeWith
+import io.delilaheve.util.ItemUtil.unitRepair
+import org.bukkit.GameMode
+import org.bukkit.Material
+import org.bukkit.entity.Player
+import org.bukkit.event.Event
+import org.bukkit.event.EventHandler
+import org.bukkit.event.Listener
+import org.bukkit.event.inventory.ClickType
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.AnvilInventory
+import org.bukkit.inventory.InventoryView
+import org.bukkit.inventory.ItemStack
+import xyz.alexcrea.cuanvil.dependency.DependencyManager
+import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_LEFT
+import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_INPUT_RIGHT
+import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT_SLOT
+import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe
+import xyz.alexcrea.cuanvil.util.AnvilXpUtil
+import xyz.alexcrea.cuanvil.util.CustomRecipeUtil
+import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair
+import kotlin.math.min
+
+class AnvilResultListener: Listener {
+
+ companion object {
+ // static slot container
+ private val NO_SLOT = SlotContainer(SlotType.NO_SLOT, 0)
+ private val CURSOR_SLOT = SlotContainer(SlotType.CURSOR, 0)
+ }
+
+ /**
+ * Event handler logic for when a player is trying to pull an item out of the anvil
+ */
+ @EventHandler(ignoreCancelled = true)
+ fun anvilExtractionCheck(event: InventoryClickEvent) {
+ val player = event.whoClicked as? Player ?: return
+ if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return
+ val inventory = event.inventory as? AnvilInventory ?: return
+
+ if (event.rawSlot != ANVIL_OUTPUT_SLOT) {
+ return
+ }
+ // Test if the event should bypass custom anvil.
+ if(DependencyManager.tryClickAnvilResultBypass(event, inventory)) return
+
+ val output = inventory.getItem(ANVIL_OUTPUT_SLOT) ?: return
+ val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return
+ val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT)
+
+ // Test custom recipe
+ val recipe = CustomRecipeUtil.getCustomRecipe(leftItem, rightItem)
+ if(recipe != null){
+ event.result = Event.Result.ALLOW
+ onCustomCraft(
+ event, recipe, player,
+ leftItem, rightItem, output, inventory)
+ return
+ }
+
+ val canMerge = leftItem.canMergeWith(rightItem)
+ val unitRepairResult = leftItem.getRepair(rightItem)
+ val allowed = (rightItem == null)
+ || (canMerge)
+ || (unitRepairResult != null)
+
+ // True if there was no change or not allowed
+ if ((output == inventory.getItem(ANVIL_INPUT_LEFT))
+ || !allowed
+ ) {
+ event.result = Event.Result.DENY
+ return
+ }
+ if (rightItem == null) {
+ event.result = Event.Result.ALLOW
+ return
+ }
+ if (canMerge) {
+ event.result = Event.Result.ALLOW
+ } else if (unitRepairResult != null) {
+ onUnitRepairExtract(
+ leftItem, rightItem, output,
+ unitRepairResult, event, player, inventory
+ )
+
+ return
+ }
+ }
+
+
+ private fun onCustomCraft(event: InventoryClickEvent,
+ recipe: AnvilCustomRecipe,
+ player: Player,
+ leftItem: ItemStack,
+ rightItem: ItemStack?,
+ output: ItemStack,
+ inventory: AnvilInventory
+ ) {
+ event.result = Event.Result.DENY
+
+ if(recipe.leftItem == null) return // in case it changed
+
+ val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem)
+ val xpCost = amount * recipe.xpCostPerCraft
+
+ if ((player.gameMode != GameMode.CREATIVE) && (player.level < xpCost)) return
+
+ // We give the item manually
+ // But first we check if we should give the item
+ val slotDestination = getActionSlot(event, player)
+ if (slotDestination.type == SlotType.NO_SLOT) return
+
+ // Handle not creative middle click...
+ if (event.click != ClickType.MIDDLE &&
+ !handleCustomCraftClick(event, recipe, inventory, player, leftItem, rightItem, amount, xpCost)) return;
+
+ // Finally, we add the item to the player
+ if (slotDestination.type == SlotType.CURSOR) {
+ player.setItemOnCursor(output)
+ } else {// We assume SlotType == SlotType.INVENTORY
+ player.inventory.setItem(slotDestination.slot, output)
+ }
+ }
+
+ private fun handleCustomCraftClick(event: InventoryClickEvent, recipe: AnvilCustomRecipe,
+ inventory: AnvilInventory, player: Player,
+ leftItem: ItemStack, rightItem: ItemStack?,
+ amount: Int, xpCost: Int): Boolean {
+ // We remove what should be removed
+ if(rightItem != null){
+ if(recipe.rightItem == null) return false// in case it changed
+
+ rightItem.amount -= amount * recipe.rightItem!!.amount
+ inventory.setItem(ANVIL_INPUT_RIGHT, rightItem)
+ }
+
+ leftItem.amount -= amount * recipe.leftItem!!.amount
+ inventory.setItem(ANVIL_INPUT_LEFT, leftItem)
+
+ if(player.gameMode != GameMode.CREATIVE){
+ player.level -= xpCost
+ }
+
+ // Then we try to find the new values for the anvil
+ val newAmount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem)
+
+ CustomAnvil.verboseLog("new amount is $newAmount")
+ if(newAmount <= 0 || recipe.exactCount){
+ inventory.setItem(ANVIL_OUTPUT_SLOT, null)
+ }else{
+ val resultItem: ItemStack = recipe.resultItem!!.clone()
+ resultItem.amount *= newAmount
+
+ val newXp = newAmount * newAmount
+
+ inventory.repairCost = newXp
+ event.view.setProperty(InventoryView.Property.REPAIR_COST, newXp)
+
+ inventory.setItem(ANVIL_OUTPUT_SLOT, resultItem)
+
+ player.updateInventory()
+ }
+ return true
+ }
+
+ private fun onUnitRepairExtract(
+ leftItem: ItemStack,
+ rightItem: ItemStack,
+ output: ItemStack,
+ unitRepairResult: Double,
+ event: InventoryClickEvent,
+ player: Player,
+ inventory: AnvilInventory
+ ) {
+ val resultCopy = leftItem.clone()
+ val resultAmount = resultCopy.unitRepair(
+ rightItem.amount, unitRepairResult
+ )
+
+ // To avoid vanilla, we cancel the event for unit repair
+ event.result = Event.Result.DENY
+ event.isCancelled = true
+ // And we give the item manually
+ // But first we check if we should give the item
+ val slotDestination = getActionSlot(event, player)
+ if (slotDestination.type == SlotType.NO_SLOT) return
+
+ // Test repair cost
+ val repairCost = getUnitRepairCost(inventory, player, leftItem, output, resultCopy, resultAmount)
+ if(repairCost == Int.MIN_VALUE) return
+
+ // If not creative middle click...
+ if (event.click != ClickType.MIDDLE) {
+ // We remove what should be removed
+ inventory.setItem(ANVIL_INPUT_LEFT, null)
+ rightItem.amount -= resultAmount
+ inventory.setItem(ANVIL_INPUT_RIGHT, rightItem)
+ inventory.setItem(ANVIL_OUTPUT_SLOT, null)
+ player.level -= repairCost
+ }
+
+ // Finally, we add the item to the player
+ if (slotDestination.type == SlotType.CURSOR) {
+ player.setItemOnCursor(output)
+ } else {// We assume SlotType == SlotType.INVENTORY
+ player.inventory.setItem(slotDestination.slot, output)
+ }
+ }
+
+ private fun getUnitRepairCost(inventory: AnvilInventory, player: Player,
+ leftItem: ItemStack, output: ItemStack,
+ resultCopy: ItemStack, resultAmount: Int): Int {
+ if (player.gameMode == GameMode.CREATIVE) return 0
+
+ var repairCost = 0;
+ // Get repairCost
+ leftItem.itemMeta?.let { leftMeta ->
+ val leftName = leftMeta.displayName
+ output.itemMeta?.let {
+ // Rename cost
+ if (!leftName.contentEquals(it.displayName)) {
+ repairCost += ConfigOptions.itemRenameCost
+
+ // Color cost
+ if(it.displayName.contains('§')){
+ repairCost += ConfigOptions.useOfColorCost
+ }
+ }
+ }
+ }
+
+ repairCost += AnvilXpUtil.calculatePenalty(leftItem, null, resultCopy)
+ repairCost += resultAmount * ConfigOptions.unitRepairCost
+
+ if (
+ !ConfigOptions.doRemoveCostLimit &&
+ ConfigOptions.doCapCost
+ ) {
+ repairCost = min(repairCost, ConfigOptions.maxAnvilCost)
+ }
+
+ if ((inventory.maximumRepairCost <= repairCost)
+ || (player.level < repairCost)
+ ) return Int.MIN_VALUE
+
+ return repairCost
+ }
+
+ /**
+ * Get the destination slot or "NO_SLOT" slot container if there is no slot available
+ */
+ private fun getActionSlot(event: InventoryClickEvent, player: Player): SlotContainer {
+ if (event.isShiftClick) {
+ val inventory = player.inventory
+ val firstEmpty = inventory.firstEmpty()
+ if (firstEmpty == -1) {
+ return NO_SLOT
+ }
+ //check hotbare full
+ var slotIndex = 8
+ while (slotIndex >= 0 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) {
+ slotIndex--
+ }
+ if (slotIndex >= 0) {
+ return SlotContainer(SlotType.INVENTORY, slotIndex)
+ }
+ slotIndex = 35 //4*9 - 1 (max of player inventory)
+ while (slotIndex >= 9 && ((inventory.getItem(slotIndex)?.type ?: Material.AIR) != Material.AIR)) {
+ slotIndex--
+ }
+ if (slotIndex < 9) {
+ return NO_SLOT
+ }
+ return SlotContainer(SlotType.INVENTORY, slotIndex)
+ }
+ else if (player.itemOnCursor.type != Material.AIR) return NO_SLOT
+ return CURSOR_SLOT
+ }
+
+ private class SlotContainer(val type: SlotType, val slot: Int)
+ private enum class SlotType {
+ CURSOR,
+ INVENTORY,
+ NO_SLOT
+
+ }
+
+}
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt
new file mode 100644
index 0000000..57de82f
--- /dev/null
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/listener/PrepareAnvilListener.kt
@@ -0,0 +1,206 @@
+package xyz.alexcrea.cuanvil.listener
+
+import io.delilaheve.CustomAnvil
+import io.delilaheve.util.ConfigOptions
+import io.delilaheve.util.EnchantmentUtil.combineWith
+import io.delilaheve.util.ItemUtil.canMergeWith
+import io.delilaheve.util.ItemUtil.findEnchantments
+import io.delilaheve.util.ItemUtil.isEnchantedBook
+import io.delilaheve.util.ItemUtil.repairFrom
+import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe
+import io.delilaheve.util.ItemUtil.unitRepair
+import org.bukkit.ChatColor
+import org.bukkit.entity.HumanEntity
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.inventory.PrepareAnvilEvent
+import org.bukkit.inventory.AnvilInventory
+import org.bukkit.inventory.ItemStack
+import xyz.alexcrea.cuanvil.dependency.DependencyManager
+import xyz.alexcrea.cuanvil.util.AnvilColorUtil
+import xyz.alexcrea.cuanvil.util.AnvilXpUtil
+import xyz.alexcrea.cuanvil.util.CustomRecipeUtil
+import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair
+
+/**
+ * Listener for anvil events
+ */
+class PrepareAnvilListener : Listener {
+
+ companion object {
+
+ // Anvil's output slot
+ const val ANVIL_INPUT_LEFT = 0
+ const val ANVIL_INPUT_RIGHT = 1
+ const val ANVIL_OUTPUT_SLOT = 2
+ }
+
+ /**
+ * Event handler logic for when an anvil contains items to be combined
+ */
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ fun anvilCombineCheck(event: PrepareAnvilEvent) {
+ // Test if the event should bypass custom anvil.
+ if(DependencyManager.tryEventPreAnvilBypass(event)) return
+
+ val inventory = event.inventory
+ val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return
+ val second = inventory.getItem(ANVIL_INPUT_RIGHT)
+
+ // Should find player
+ val player = event.view.player
+ if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return
+
+ // Test custom recipe
+ if(testCustomRecipe(event, inventory, first, second)) return
+
+ // Test rename lonely item
+ if(second == null) {
+ doRenaming(event, inventory, player, first)
+ return
+ }
+
+ // Test for merge
+ if (first.canMergeWith(second)) {
+ doMerge(event, inventory, player, first, second)
+ return
+ }
+
+ // Test for unit repair
+ if(testUnitRepair(event, inventory, player, first, second)) return
+
+ CustomAnvil.log("no anvil fuse type found")
+ event.result = null
+
+ }
+
+ private fun testCustomRecipe(event: PrepareAnvilEvent, inventory: AnvilInventory, first: ItemStack, second: ItemStack?): Boolean {
+ val recipe = CustomRecipeUtil.getCustomRecipe(first, second)
+ CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}")
+ if(recipe == null) return false
+
+ val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second)
+
+ val resultItem: ItemStack = recipe.resultItem!!.clone()
+ resultItem.amount *= amount
+
+ event.result = resultItem
+ AnvilXpUtil.setAnvilInvXp(inventory, event.view, recipe.xpCostPerCraft * amount, true)
+
+ return true
+ }
+
+ private fun doRenaming(event: PrepareAnvilEvent, inventory: AnvilInventory,
+ player: HumanEntity, first: ItemStack) {
+ val resultItem = first.clone()
+ var anvilCost = handleRename(resultItem, inventory, player)
+
+ // Test/stop if nothing changed.
+ if (first == resultItem) {
+ CustomAnvil.log("no right item, But input is same as output")
+ event.result = null
+ return
+ }
+
+ event.result = resultItem
+
+ anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem)
+
+ AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost)
+ }
+
+ private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int {
+ // Rename item and add renaming cost
+ resultItem.itemMeta?.let {
+ val displayName = ChatColor.stripColor(it.displayName)
+ var inventoryName = ChatColor.stripColor(inventory.renameText)
+
+ var sumCost = 0
+
+ var useColor = false
+ if(ConfigOptions.renameColorPossible){
+ val resultString = StringBuilder(inventoryName)
+
+ useColor = AnvilColorUtil.handleRenamingColor(resultString, player)
+
+ if(useColor) {
+ inventoryName = resultString.toString()
+
+ sumCost+= ConfigOptions.useOfColorCost
+ }
+ }
+
+ if ((!useColor && (!displayName.contentEquals(inventoryName))) || (useColor && !(it.displayName).contentEquals(inventoryName))) {
+ it.setDisplayName(inventoryName)
+ resultItem.itemMeta = it
+
+ sumCost+= ConfigOptions.itemRenameCost
+ }
+
+ return sumCost
+ }
+ return 0
+ }
+
+ private fun doMerge(event: PrepareAnvilEvent, inventory: AnvilInventory,
+ player: HumanEntity,
+ first: ItemStack, second: ItemStack) {
+ val newEnchants = first.findEnchantments()
+ .combineWith(second.findEnchantments(), first, player)
+ val resultItem = first.clone()
+ resultItem.setEnchantmentsUnsafe(newEnchants)
+
+ // Calculate enchantment cost
+ var anvilCost = AnvilXpUtil.getRightValues(second, resultItem)
+ // Calculate repair cost
+ if (!first.isEnchantedBook() && !second.isEnchantedBook()) {
+ // we only need to be concerned with repair when neither item is a book
+ val repaired = resultItem.repairFrom(first, second)
+ anvilCost += if (repaired) ConfigOptions.itemRepairCost else 0
+ }
+
+ // Test/stop if nothing changed.
+ if (first == resultItem) {
+ CustomAnvil.log("Mergable with second, But input is same as output")
+ event.result = null
+ return
+ }
+ // As calculatePenalty edit result, we need to calculate penalty after checking equality
+ anvilCost += AnvilXpUtil.calculatePenalty(first, second, resultItem)
+ // Calculate rename cost
+ anvilCost += handleRename(resultItem, inventory, player)
+
+ // Finally, we set result
+ event.result = resultItem
+
+ AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost)
+ }
+
+ private fun testUnitRepair(event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity,
+ first: ItemStack, second: ItemStack): Boolean {
+ val unitRepairAmount = first.getRepair(second) ?: return false
+
+ val resultItem = first.clone()
+ var anvilCost = handleRename(resultItem, inventory, player)
+
+ val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount)
+ if (repairAmount > 0) {
+ anvilCost += repairAmount * ConfigOptions.unitRepairCost
+ }
+ // We do not care about right item penalty for unit repair
+ anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, true)
+
+ // Test/stop if nothing changed.
+ if (first == resultItem) {
+ CustomAnvil.log("unit repair, But input is same as output")
+ event.result = null
+ return true
+ }
+ event.result = resultItem
+
+ AnvilXpUtil.setAnvilInvXp(inventory, event.view, anvilCost)
+ return true
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt
new file mode 100644
index 0000000..0e216e8
--- /dev/null
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/AnvilColorUtil.kt
@@ -0,0 +1,88 @@
+package xyz.alexcrea.cuanvil.util
+
+import io.delilaheve.util.ConfigOptions
+import org.bukkit.entity.HumanEntity
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+object AnvilColorUtil {
+ private val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string
+
+ fun handleRenamingColor(textToColor: StringBuilder, player: HumanEntity): Boolean {
+ val usePermission = ConfigOptions.permissionNeededForColor
+ val canUseColorCode = ConfigOptions.allowColorCode && (!usePermission || player.hasPermission("ca.color.code"))
+ val canUseHexColor = ConfigOptions.allowHexadecimalColor && (!usePermission || player.hasPermission("ca.color.hex"))
+
+ if((!canUseColorCode) && (!canUseHexColor)) return false
+
+ var useColor = false
+ // Handle color code
+ if(canUseColorCode){
+ var nbReplacement = replaceAll(textToColor, "&", "§", 2)
+ nbReplacement -= 2 * replaceAll(textToColor, "§§", "&", 2)
+
+ if(nbReplacement > 0) useColor = true
+ }
+
+ if(canUseHexColor){
+ val nbReplacement = replaceHexToColor(textToColor, 7)
+
+ if(nbReplacement > 0) useColor = true
+ }
+
+ return useColor
+ }
+
+ /**
+ * Replace every instance of "from" to "to".
+ * @param builder The builder to replace the string from.
+ * @param from The source that should be replaced.
+ * @param to The string that should replace.
+ * @param endOffset Amount of character that should be ignored at the end.
+ * @return The number of replacement was that was done.
+ */
+ private fun replaceAll(builder: java.lang.StringBuilder, from: String, to: String, endOffset: Int): Int {
+ var index = builder.indexOf(from)
+ var numberOfChanges = 0
+
+ while (index != -1 && index < builder.length - endOffset) {
+ builder.replace(index, index + from.length, to)
+ index += to.length
+ index = builder.indexOf(from, index)
+
+ numberOfChanges+=1
+ }
+
+ return numberOfChanges
+ }
+
+ /**
+ * Replace every hex color formatted like #000000 to the minecraft format
+ * @param builder The builder to replace the hex color from.
+ * @param endOffset Amount of character that should be ignored at the end.
+ * @return The number of replacement was that was done.
+ */
+ private fun replaceHexToColor(builder: StringBuilder, endOffset: Int): Int {
+ val matcher: Matcher = HEX_PATTERN.matcher(builder)
+
+ var numberOfChanges = 0
+ var startIndex = 0
+
+ while(matcher.find(startIndex)){
+ startIndex = matcher.start()
+ if(startIndex >= builder.length - endOffset) break
+
+ builder.replace(startIndex, startIndex + 1, "§x")
+ startIndex+=2
+ for (i in 0..5) {
+ builder.insert(startIndex, '§')
+ startIndex+=2
+ }
+
+ numberOfChanges+=1
+ }
+
+ return numberOfChanges
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt
new file mode 100644
index 0000000..3ec5e71
--- /dev/null
+++ b/src/main/kotlin/xyz/alexcrea/cuanvil/util/CustomRecipeUtil.kt
@@ -0,0 +1,54 @@
+package xyz.alexcrea.cuanvil.util
+
+import io.delilaheve.CustomAnvil
+import org.bukkit.inventory.ItemStack
+import xyz.alexcrea.cuanvil.config.ConfigHolder
+import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe
+import kotlin.math.min
+
+object CustomRecipeUtil {
+
+ fun getCustomRecipe (
+ leftItem: ItemStack,
+ rightItem: ItemStack?) : AnvilCustomRecipe? {
+
+ val recipeList = ConfigHolder.CUSTOM_RECIPE_HOLDER.recipeManager.recipeByMat[leftItem.type] ?: return null
+
+ CustomAnvil.verboseLog("Testing " + recipeList.size + " recipe...")
+ for (recipe in recipeList) {
+ if(recipe.testItem(leftItem, rightItem)){
+ return recipe
+ }
+ }
+
+ return null
+ }
+
+ fun getCustomRecipeAmount(
+ recipe: AnvilCustomRecipe,
+ leftItem: ItemStack,
+ rightItem: ItemStack?
+ ): Int{
+ return if(recipe.exactCount) {
+ if(leftItem.amount != recipe.leftItem!!.amount){
+ 0
+ }else if(rightItem != null && rightItem.amount != recipe.rightItem!!.amount){
+ 0
+ }else{
+ 1
+ }
+ }
+ else {
+ // test amount
+ val resultItem = recipe.resultItem!! // we know exist as the recipe was returned to us
+ val maxResultAmount = resultItem.type.maxStackSize/resultItem.amount
+ val maxLeftAmount = leftItem.amount/recipe.leftItem!!.amount
+ val maxRightAmount = if(rightItem == null){ maxLeftAmount } else{ rightItem.amount/recipe.rightItem!!.amount }
+
+ CustomAnvil.verboseLog("resultItem: $resultItem, maxResultAmount: $maxResultAmount, maxLeftAmount: $maxLeftAmount, maxRightAmount: $maxRightAmount")
+
+ min(min(maxResultAmount, maxLeftAmount), maxRightAmount)
+ }
+ }
+
+}
\ No newline at end of file
From 13b7e73d8c6aa9a1f48ce4c4fa1a33983ca32174 Mon Sep 17 00:00:00 2001
From: alexcrea <42614139+alexcrea@users.noreply.github.com>
Date: Tue, 15 Oct 2024 08:27:26 +0200
Subject: [PATCH 069/495] Excellent enchants compatibility (#34)
Add compatibility with
[ExcellentEnchants](https://www.spigotmc.org/resources/excellentenchants-%E2%AD%90-75-vanilla-like-enchantments.61693/)
---
build.gradle.kts | 4 +
libs/ExcellentEnchants-4.2.2.jar | Bin 0 -> 424362 bytes
libs/nightcore-2.6.4.jar | Bin 0 -> 446550 bytes
.../enchant/wrapped/CAEEEnchantment.java | 46 ++++++++
.../cuanvil/dependency/DependencyManager.kt | 16 +++
.../dependency/DisenchantmentDependency.kt | 1 -
.../dependency/ExcellentEnchantsDependency.kt | 107 ++++++++++++++++++
src/main/resources/plugin.yml | 7 +-
8 files changed, 177 insertions(+), 4 deletions(-)
create mode 100644 libs/ExcellentEnchants-4.2.2.jar
create mode 100644 libs/nightcore-2.6.4.jar
create mode 100644 src/main/java/xyz/alexcrea/cuanvil/enchant/wrapped/CAEEEnchantment.java
create mode 100644 src/main/kotlin/xyz/alexcrea/cuanvil/dependency/ExcellentEnchantsDependency.kt
diff --git a/build.gradle.kts b/build.gradle.kts
index 28cb586..507b783 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -36,6 +36,10 @@ dependencies {
compileOnly("com.willfp:EcoEnchants:12.5.1")
compileOnly("com.willfp:eco:6.70.1")
+ // ExcellentEnchants
+ compileOnly(files("libs/nightcore-2.6.4.jar"))
+ compileOnly(files("libs/ExcellentEnchants-4.2.2.jar"))
+
// Disenchantment
compileOnly("cz.kominekjan:Disenchantment:v5.4.0")
diff --git a/libs/ExcellentEnchants-4.2.2.jar b/libs/ExcellentEnchants-4.2.2.jar
new file mode 100644
index 0000000000000000000000000000000000000000..23d9630877624f38bf9eec367f3b50c8c1b7be2f
GIT binary patch
literal 424362
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