commit e53f9cc88c932f7317f1013de188dd1560181d47 Author: DelilahEve Date: Sat Aug 20 21:14:21 2022 -0400 Initial commit diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1374616 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + kotlin("jvm") version "1.6.21" + java +} + +group = "io.delilaheve" +version = "1.0.0" + +repositories { + mavenCentral() + maven(url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/") +} + +dependencies { + + implementation(kotlin("stdlib")) + + compileOnly("org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0") + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +// Fat-jar builder +val fatJar = tasks.register("fatJar") { + manifest { + attributes.apply { put("Main-Class", "io.delilaheve.UnsafeEnchants") } + } + archiveFileName.set("${rootProject.name}-${version}.jar") + exclude("META-INF/*.RSA", "META-INF/*.SF", "META-INF/*.DSA") + duplicatesStrategy = DuplicatesStrategy.WARN + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + with(tasks.jar.get() as CopySpec) +} + +// Ensure fatJar and copyJar are run +tasks.getByName("build") { + dependsOn(fatJar) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..6ffcb1a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "UnsafeEnchants" + diff --git a/src/main/kotlin/io/delilaheve/AnvilEventListener.kt b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt new file mode 100644 index 0000000..77d2aeb --- /dev/null +++ b/src/main/kotlin/io/delilaheve/AnvilEventListener.kt @@ -0,0 +1,88 @@ +package io.delilaheve + +import io.delilaheve.util.ConfigOptions +import io.delilaheve.util.EnchantmentUtil.calculateValue +import io.delilaheve.util.EnchantmentUtil.combineWith +import io.delilaheve.util.ItemUtil.canMergeWith +import io.delilaheve.util.ItemUtil.findEnchantments +import io.delilaheve.util.ItemUtil.isBook +import io.delilaheve.util.ItemUtil.setEnchantmentsUnsafe +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.InventoryClickEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.inventory.AnvilInventory +import org.bukkit.inventory.InventoryView +import org.bukkit.permissions.Permission +import kotlin.math.min + +/** + * Listener for anvil events + */ +class AnvilEventListener : Listener { + + companion object { + // Vanilla repair cost limit + private const val VANILLA_REPAIR_LIMIT = 40 + // Anvil's output slot + private const val ANVIL_OUTPUT_SLOT = 2 + } + + // Permission node required for the plugin to take over enchantment combination + private val requirePermission: Permission + get() = Permission(UnsafeEnchants.unsafePermission) + + /** + * Event handler logic for when an anvil contains items to be combined + */ + @EventHandler + fun anvilCombineCheck(event: PrepareAnvilEvent) { + val inventory = event.inventory + val first = inventory.getItem(0) ?: return + val second = inventory.getItem(1) ?: return + if (first.canMergeWith(second)) { + val firstEnchants = first.findEnchantments().toMutableMap() + val secondEnchants = second.findEnchantments().toMutableMap() + if (ConfigOptions.removeRepairLimit) { + inventory.maximumRepairCost = Int.MAX_VALUE + } + val newEnchants = firstEnchants.combineWith(secondEnchants) + val enchantsString = newEnchants.map { "${it.key.key} ${it.value}" }.joinToString(", ") + UnsafeEnchants.log("New enchants for this item: $enchantsString") + val resultItem = first.clone() + resultItem.setEnchantmentsUnsafe(newEnchants) + val firstValue = firstEnchants.calculateValue(first.isBook()) + val secondValue = secondEnchants.calculateValue(second.isBook()) + var repairCost = firstValue + secondValue + if (first.isBook() && second.isBook()) { + repairCost = firstEnchants.values.sum() + secondEnchants.values.sum() + } + if (ConfigOptions.limitRepairCost) { + repairCost = min(repairCost, VANILLA_REPAIR_LIMIT) + } + inventory.repairCost = repairCost + event.view.setProperty(InventoryView.Property.REPAIR_COST, repairCost) + event.result = resultItem + } + } + + /** + * 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(requirePermission)) { + val inventory = event.inventory as? AnvilInventory ?: return + if (event.rawSlot != ANVIL_OUTPUT_SLOT) { return } + val output = inventory.getItem(2) ?: return + if (output.type == Material.AIR) { return } + if (player.level < inventory.repairCost) { return } + event.result = Event.Result.ALLOW + } + } + +} diff --git a/src/main/kotlin/io/delilaheve/UnsafeEnchants.kt b/src/main/kotlin/io/delilaheve/UnsafeEnchants.kt new file mode 100644 index 0000000..9fd73b4 --- /dev/null +++ b/src/main/kotlin/io/delilaheve/UnsafeEnchants.kt @@ -0,0 +1,40 @@ +package io.delilaheve + +import io.delilaheve.util.ConfigOptions +import org.bukkit.plugin.java.JavaPlugin + +/** + * Bukkit/Spigot/Paper plugin to alter enchantment max + * levels and allow unsafe enchantment combinations + */ +class UnsafeEnchants : JavaPlugin() { + + companion object { + // Permission string required to use the plugin's features + const val unsafePermission = "ue.unsafe" + // Current plugin instance + lateinit var instance: UnsafeEnchants + + /** + * Logging handler + */ + fun log(message: String) { + if (ConfigOptions.debugLog) { + instance.logger.info(message) + } + } + } + + /** + * Setup plugin for use + */ + override fun onEnable() { + instance = this + saveDefaultConfig() + server.pluginManager.registerEvents( + AnvilEventListener(), + this + ) + } + +} diff --git a/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt new file mode 100644 index 0000000..5f5bf12 --- /dev/null +++ b/src/main/kotlin/io/delilaheve/util/ConfigOptions.kt @@ -0,0 +1,124 @@ +package io.delilaheve.util + +import io.delilaheve.UnsafeEnchants +import io.delilaheve.util.EnchantmentUtil.enchantmentName +import org.bukkit.enchantments.Enchantment + +/** + * Config option accessors + */ +object ConfigOptions { + + // Path for default enchantment limits + private const val DEFAULT_LIMIT_PATH = "default_limit" + // Path for allowing unsafe enchants + private const val ALLOW_UNSAFE_PATH = "allow_unsafe" + // Path for limiting repair cost + private const val LIMIT_REPAIR_COST = "limit_repair_cost" + // Path for removing repair cost limits + private const val REMOVE_REPAIR_LIMIT = "remove_repair_limit" + // Root path for enchantment limits + private const val ENCHANT_LIMIT_ROOT = "enchant_limits" + // Root path for enchantment values + private const val ENCHANT_VALUES_ROOT = "enchant_values" + // Keys for specific enchantment values + private const val KEY_BOOK = "book" + private const val KEY_ITEM = "item" + // Debug logging toggle path + private const val DEBUG_LOGGING = "debug_log" + + // Default value for enchantment limits + private const val DEFAULT_ENCHANT_LIMIT = 10 + // Default value for allowing unsafe enchantments + private const val DEFAULT_ALLOW_UNSAFE = true + // Default value for limiting repair cost + private const val DEFAULT_LIMIT_REPAIR = true + // Default for removing repair cost limits + private const val DEFAULT_REMOVE_LIMIT = false + // Valid range for an enchantment limit + private val ENCHANT_LIMIT_RANGE = 1..255 + // Default value for an enchantment multiplier + private const val DEFAULT_ENCHANT_VALUE = 0 + // Default value for debug logging + private const val DEFAULT_DEBUG_LOG = false + + /** + * Default enchantment limit + */ + private val defaultEnchantLimit: Int + get() { + return UnsafeEnchants.instance + .config + .getInt(DEFAULT_LIMIT_PATH, DEFAULT_ENCHANT_LIMIT) + } + + /** + * Whether to allow unsafe enchantments + */ + val allowUnsafe: Boolean + get() { + return UnsafeEnchants.instance + .config + .getBoolean(ALLOW_UNSAFE_PATH, DEFAULT_ALLOW_UNSAFE) + } + + /** + * Whether to limit repair costs to the vanilla limit + */ + val limitRepairCost: Boolean + get() { + return UnsafeEnchants.instance + .config + .getBoolean(LIMIT_REPAIR_COST, DEFAULT_LIMIT_REPAIR) + } + + /** + * Whether to remove repair cost limit + */ + val removeRepairLimit: Boolean + get() { + return UnsafeEnchants.instance + .config + .getBoolean(REMOVE_REPAIR_LIMIT, DEFAULT_REMOVE_LIMIT) + } + + /** + * Whether to show debug logging + */ + val debugLog: Boolean + get() { + return UnsafeEnchants.instance + .config + .getBoolean(DEBUG_LOGGING, DEFAULT_DEBUG_LOG) + } + + /** + * Get the given [enchantment]'s limit + */ + fun enchantLimit(enchantment: Enchantment): Int { + val path = "${ENCHANT_LIMIT_ROOT}.${enchantment.enchantmentName}" + return UnsafeEnchants.instance + .config + .getInt(path, defaultEnchantLimit) + .takeIf { it in ENCHANT_LIMIT_RANGE } + ?: defaultEnchantLimit + } + + /** + * Get the appropriate [enchantment]'s value dependent on whether + * it's source [isFromBook] + */ + fun enchantmentValue( + enchantment: Enchantment, + isFromBook: Boolean + ): Int { + val typeKey = if (isFromBook) KEY_BOOK else KEY_ITEM + val path = "${ENCHANT_VALUES_ROOT}.${enchantment.enchantmentName}.$typeKey" + return UnsafeEnchants.instance + .config + .getInt(path, DEFAULT_ENCHANT_VALUE) + .takeIf { it >= DEFAULT_ENCHANT_VALUE } + ?: DEFAULT_ENCHANT_VALUE + } + +} diff --git a/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt new file mode 100644 index 0000000..6404621 --- /dev/null +++ b/src/main/kotlin/io/delilaheve/util/EnchantmentUtil.kt @@ -0,0 +1,64 @@ +package io.delilaheve.util + +import io.delilaheve.UnsafeEnchants +import org.bukkit.enchantments.Enchantment +import kotlin.math.max +import kotlin.math.min + +/** + * Enchantment manipulation utilities + */ +object EnchantmentUtil { + + val Enchantment.enchantmentName: String + get() = key.key + /** + * Combine 2 sets of enchantments according to our configuration + */ + fun MutableMap.combineWith( + other: MutableMap + ) = mutableMapOf().apply { + putAll(this@combineWith) + other.forEach { (enchantment, level) -> + when { + // Enchantment not yet in result list + !containsKey(enchantment) -> { + // Add the enchantment if it doesn't have conflicts, or, if we're allowing unsafe enchantments + if (!keys.any { enchantment.conflictsWith(it) } || ConfigOptions.allowUnsafe) { + this[enchantment] = level + } + } + // Enchantment already in result list... + else -> when { + // ... and they're not the same level + this[enchantment] != other[enchantment] -> { + val newLevel = max(this[enchantment] ?: 0, other[enchantment] ?: 0) + // apply the greater of the two if non-zero + if (newLevel > 0) { this[enchantment] = newLevel } + } + // ... and they're the same level + else -> { + // try to increase the enchantment level by 1 + var newLevel = this[enchantment]?.plus(1) ?: 0 + val maxLevel = ConfigOptions.enchantLimit(enchantment) + newLevel = min(newLevel, maxLevel) + if (newLevel > 0) { this[enchantment] = newLevel } + } + } + } + } + } + + /** + * Calculate the value of a set of enchantments + */ + fun Map.calculateValue( + fromBook: Boolean + ) = entries.sumOf { (enchantment, level) -> + val enchantmentMultiplier = ConfigOptions.enchantmentValue(enchantment, fromBook) + val value = level * enchantmentMultiplier + UnsafeEnchants.log("Value for ${enchantment.enchantmentName} is $value") + value + } + +} diff --git a/src/main/kotlin/io/delilaheve/util/ItemUtil.kt b/src/main/kotlin/io/delilaheve/util/ItemUtil.kt new file mode 100644 index 0000000..72cecbb --- /dev/null +++ b/src/main/kotlin/io/delilaheve/util/ItemUtil.kt @@ -0,0 +1,83 @@ +package io.delilaheve.util + +import io.delilaheve.UnsafeEnchants +import org.bukkit.Material.BOOK +import org.bukkit.Material.ENCHANTED_BOOK +import org.bukkit.enchantments.Enchantment +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.EnchantmentStorageMeta + +/** + * Item manipulation utilities + */ +object ItemUtil { + + /** + * Check if this [ItemStack] is a [BOOK] or [ENCHANTED_BOOK] + */ + fun ItemStack.isBook() = type in listOf(BOOK, ENCHANTED_BOOK) + + /** + * Check if this [ItemStack] is an [ENCHANTED_BOOK] + */ + private fun ItemStack.isEnchantedBook() = type == ENCHANTED_BOOK + + /** + * Determine if this [ItemStack] can hold enchants, this should be sufficient for + * detecting if an item is a tool/armour/etc... and not a carrot/potato/etc... + */ + private fun ItemStack.canHoldEnchants() = Enchantment.values() + .any { it.canEnchantItem(this) } + + /** + * Find the enchantment map for this [ItemStack] and return it as a [MutableMap] + */ + fun ItemStack.findEnchantments() = if (isBook()) { + (itemMeta as? EnchantmentStorageMeta)?.storedEnchants ?: emptyMap() + } else { + itemMeta?.enchants ?: emptyMap() + } + + /** + * Apply an [enchantments] map to this [ItemStack] + */ + fun ItemStack.setEnchantmentsUnsafe(enchantments: Map) { + if (isBook()) { + /* For some god-forsaken reason, item meta is not mutable + * so, we have to get the instance, modify it, then set it + * back to the item... #BecauseMinecraft */ + val bookMeta = (itemMeta as? EnchantmentStorageMeta) + bookMeta?.replaceEnchants(enchantments) + itemMeta = bookMeta + } else { + itemMeta?.enchants?.forEach { (enchant, _) -> + removeEnchantment(enchant) + } + addUnsafeEnchantments(enchantments) + } + } + + /** + * Apply an [enchantments] map to this book + */ + private fun EnchantmentStorageMeta.replaceEnchants( + enchantments: Map + ) { + storedEnchants.forEach { (enchant, _) -> + removeStoredEnchant(enchant) + } + enchantments.forEach { (enchant, level) -> + val added = addStoredEnchant(enchant, level, true) + UnsafeEnchants.log("${enchant.key} added to item? $added") + } + } + + /** + * Check that this [ItemStack] can merge with the [other] + * + * The two items should either be the same type, or, the [other] is a book + */ + fun ItemStack.canMergeWith( + other: ItemStack + ) = type == other.type || (canHoldEnchants() && other.isEnchantedBook()) +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..00b208c --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,202 @@ +# Default limit to apply to any enchants missing from override_limits +# +# Valid range of 1 - 255 +default_limit: 10 + +# Whether enchants should be combined without regard for conflicts by default +# +# This setting will override permissions, if a player has ue.unsafe but this is false +# they will be unable to combine conflicting enchantments +# +# i.e. Protection and Blast Protection can be on the same piece of armour +allow_unsafe: true + +# Whether all anvil actions should be capped to the vanilla repair limit (40 levels) +limit_repair_cost: true + +# Whether the anvil's repair limit should be removed entirely +# +# The anvil will still visually display "too expensive" however the action will be completable +remove_repair_limit: false + +# Override limits for specific enchants +# +# Enchantments not listed here will use the value of default_limit +# +# Overrides provided default to 1 in vanilla and won't change effect with extra levels +# with exceptions to this rule having their own comment +# +# 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 +# bane_of_arthropods: 1 +# blast_protection: 1 +# efficiency: 1 +# feather_falling: 1 +# fire_aspect: 1 +# fire_protection: 1 +# fortune: 1 +# frost_walker: 1 +# impaling: 1 +# knockback: 1 +# looting: 1 +# loyalty: 1 +# luck_of_the_sea: 1 +# lure: 1 +# piercing: 1 +# power: 1 +# projectile_protection: 1 +# protection: 1 +# punch: 1 +# quick_charge: 1 +# respiration: 1 +# riptide: 1 +# sharpness: 1 +# smite: 1 +# soul_speed: 1 +# sweeping: 1 +# swift_sneak: 1 +# thorns: 1 +# unbreaking: 1 + +# Multipliers used to calculate the enchantment's value in repair/combining +# +# Values here are pulled from the fandom wiki: +# https://minecraft.fandom.com/wiki/Anvil_mechanics#Costs_for_combining_enchantments +# +# If an enchantment is missing values here, or is less than 0, it will default to 0 +# +# Calculated as: [Enchantment lvl] * [multiplier] +# +# 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: + item: 4 + book: 2 + bane_of_arthropods: + item: 2 + book: 1 + binding_curse: + item: 8 + book: 4 + blast_protection: + item: 4 + book: 2 + channeling: + item: 8 + book: 4 + depth_strider: + item: 4 + book: 2 + efficiency: + item: 1 + book: 1 + flame: + item: 4 + book: 2 + feather_falling: + item: 2 + book: 1 + fire_aspect: + item: 4 + book: 2 + fire_protection: + item: 2 + book: 1 + fortune: + item: 4 + book: 2 + frost_walker: + item: 4 + book: 2 + impaling: + item: 4 + book: 2 + infinity: + item: 8 + book: 4 + knockback: + item: 2 + book: 1 + looting: + item: 4 + book: 2 + loyalty: + item: 1 + book: 1 + luck_of_the_sea: + item: 4 + book: 2 + lure: + item: 4 + book: 2 + mending: + item: 4 + book: 2 + multishot: + item: 4 + book: 2 + piercing: + item: 1 + book: 1 + power: + item: 1 + book: 1 + projectile_protection: + item: 2 + book: 1 + protection: + item: 1 + book: 1 + punch: + item: 4 + book: 2 + quick_charge: + item: 2 + book: 1 + respiration: + item: 4 + book: 2 + riptide: + item: 4 + book: 2 + silk_touch: + item: 8 + book: 4 + sharpness: + item: 1 + book: 1 + smite: + item: 2 + book: 1 + soul_speed: + item: 8 + book: 4 + swift_sneak: + item: 8 + book: 4 + sweeping: + item: 4 + book: 2 + thorns: + item: 8 + book: 4 + unbreaking: + item: 2 + book: 1 + vanishing_curse: + item: 8 + book: 4 + +# Whether to show debug logging +debug_log: false diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..096bf55 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,11 @@ +main: io.delilaheve.UnsafeEnchants +name: UnsafeEnchants +version: 1.0.0 +description: Allow all enchants to be combined +api-version: 1.18 +load: POSTWORLD +author: DelilahEve +permissions: + ue.unsafe: + default: true + description: Allow player to combine "unsafe" enchants