Initial commit

This commit is contained in:
DelilahEve 2022-08-20 21:14:21 -04:00
commit e53f9cc88c
14 changed files with 939 additions and 0 deletions

View file

@ -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
}
}
}

View file

@ -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
)
}
}

View file

@ -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
}
}

View file

@ -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<Enchantment, Int>.combineWith(
other: MutableMap<Enchantment, Int>
) = mutableMapOf<Enchantment, Int>().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<Enchantment, Int>.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
}
}

View file

@ -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<Enchantment, Int>) {
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<Enchantment, Int>
) {
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())
}

View file

@ -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

View file

@ -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