Lot of internal change and monetary cost (#116)

Internal changes this big was not intentional but had to do it for
monetary cost

excluding that: 
- add monetary cost, with dependency on rename
dialogue
- also change some a bit rename dialog
This commit is contained in:
alexcrea 2026-06-10 15:35:41 +02:00 committed by GitHub
commit 380b0de92f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1702 additions and 717 deletions

View file

@ -40,11 +40,14 @@ repositories {
// ItemsAdder
maven(url = "https://maven.devs.beer/")
// for fast stats
// For fast stats
maven {
name = "thenextlvlReleases"
url = uri("https://repo.thenextlvl.net/releases")
}
// For vault unlocked
maven { url = uri("https://repo.codemc.io/repository/creatorfromhell/") }
}
val reobfNMS = providers.gradleProperty("subprojects.reobfnms")
@ -103,6 +106,9 @@ dependencies {
// ItemsAdder API
compileOnly("dev.lone:api-itemsadder:4.0.10")
// Vault api
compileOnly("net.milkbowl.vault:VaultUnlockedAPI:2.16")
// Include nms
implementation(project(":nms:nms-common"))
implementation(project(":nms:nms-paper"))

View file

@ -430,6 +430,33 @@ lore_edit:
allow_hexadecimal_color: false
allow_minimessage: true
# Allow to replace the xp cost by a monetary cost
# If enabled it will not be bound to the experience level limits
#
# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher)
# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost
#
# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird"
# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect
#
# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later
monetary_cost:
enabled: false
# If using vault unlocked this allow to specify what currency should be used for anvil usage
# default being the default currency
currency: default
# multiply the anvil cost by a value to allow to have price a big bigger than like 40
multipliers:
# global multipliers. all usage type will be multiplied by this value
global: 1.0
# usage specific type. it will only apply for specific xp "reason"
enchantment: 1.0 # related to enchantments level
repair: 1.0 # for repairing via unit repair (per unit)
rename: 1.0 # for renaming the item
lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled)
work_penalty: 1.0 # for work penalty (aka use penalty)
recipe: 1.0 # for custom anvil recipe cost
# Whether to show debug logging
debug_log: false

View file

@ -450,6 +450,33 @@ lore_edit:
allow_hexadecimal_color: false
allow_minimessage: true
# Allow to replace the xp cost by a monetary cost
# If enabled it will not be bound to the experience level limits
#
# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher)
# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost
#
# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird"
# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect
#
# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later
monetary_cost:
enabled: false
# If using vault unlocked this allow to specify what currency should be used for anvil usage
# default being the default currency
currency: default
# multiply the anvil cost by a value to allow to have price a big bigger than like 40
multipliers:
# global multipliers. all usage type will be multiplied by this value
global: 1.0
# usage specific type. it will only apply for specific xp "reason"
enchantment: 1.0 # related to enchantments level
repair: 1.0 # for repairing via unit repair (per unit)
rename: 1.0 # for renaming the item
lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled)
work_penalty: 1.0 # for work penalty (aka use penalty)
recipe: 1.0 # for custom anvil recipe cost
# Whether to show debug logging
debug_log: false

View file

@ -442,6 +442,33 @@ lore_edit:
allow_hexadecimal_color: false
allow_minimessage: true
# Allow to replace the xp cost by a monetary cost
# If enabled it will not be bound to the experience level limits
#
# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher)
# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost
#
# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird"
# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect
#
# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later
monetary_cost:
enabled: false
# If using vault unlocked this allow to specify what currency should be used for anvil usage
# default being the default currency
currency: default
# multiply the anvil cost by a value to allow to have price a big bigger than like 40
multipliers:
# global multipliers. all usage type will be multiplied by this value
global: 1.0
# usage specific type. it will only apply for specific xp "reason"
enchantment: 1.0 # related to enchantments level
repair: 1.0 # for repairing via unit repair (per unit)
rename: 1.0 # for renaming the item
lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled)
work_penalty: 1.0 # for work penalty (aka use penalty)
recipe: 1.0 # for custom anvil recipe cost
# Whether to show debug logging
debug_log: false

View file

@ -430,6 +430,33 @@ lore_edit:
allow_hexadecimal_color: false
allow_minimessage: true
# Allow to replace the xp cost by a monetary cost
# If enabled it will not be bound to the experience level limits
#
# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher)
# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost
#
# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird"
# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect
#
# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later
monetary_cost:
enabled: false
# If using vault unlocked this allow to specify what currency should be used for anvil usage
# default being the default currency
currency: default
# multiply the anvil cost by a value to allow to have price a big bigger than like 40
multipliers:
# global multipliers. all usage type will be multiplied by this value
global: 1.0
# usage specific type. it will only apply for specific xp "reason"
enchantment: 1.0 # related to enchantments level
repair: 1.0 # for repairing via unit repair (per unit)
rename: 1.0 # for renaming the item
lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled)
work_penalty: 1.0 # for work penalty (aka use penalty)
recipe: 1.0 # for custom anvil recipe cost
# Whether to show debug logging
debug_log: false

View file

@ -18,4 +18,6 @@ interface AnvilRenameDialog {
fun currentText(player: HumanEntity): String?
fun isOpenFor(player: HumanEntity): Boolean
}

View file

@ -18,6 +18,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventoryView
import org.bukkit.craftbukkit.inventory.view.CraftAnvilView
import org.bukkit.entity.HumanEntity
import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType
import org.bukkit.plugin.Plugin
@ -32,7 +33,7 @@ class AnvilRenameDialogImpl(
val keepUserPreviousDialog: Supplier<Boolean>,
val maxLength: Supplier<Int>,
val plugin: Plugin,
): AnvilRenameDialog {
) : AnvilRenameDialog {
companion object {
private const val RENAME_TEXT_KEY = "rename"
@ -43,16 +44,28 @@ class AnvilRenameDialogImpl(
// Need to be able to translate it later !
private val USER_FACING_RENAME_TITLE = Component.text("Rename Your Item")
private val USER_FACING_WARNING = Component.text("Note that the repair text will appear blank after Confirm\n" +
"But the name will be correctly applied")
private val USER_FACING_WARNING = Component.text(
"Note that the repair text will appear blank after Confirm\n" +
"But the name will be correctly applied"
)
private val USER_FACING_CONFIRM = Component.text("Confirm").color(TextColor.fromHexString("#40FF40"))
private val USER_FACING_CANCEL = Component.text("Cancel").color(TextColor.fromHexString("#FF4040"))
fun itemDefaultName(item: ItemStack?): String? {
return PLAIN_TEXT_SERIALIZER.serializeOrNull(item?.effectiveName())
}
}
private val lastNames = HashMap<UUID, String>()
private val lastRenames = HashMap<UUID, String>()
private val lastLeftItem = HashMap<UUID, String>()
private val runTaskMap = HashMap<UUID, ScheduledTask>()
// For monetary cost
val hasUiOpen = HashSet<UUID>()
private val containerField = CraftInventoryView::class.java.getDeclaredField("container")
init {
@ -63,14 +76,15 @@ class AnvilRenameDialogImpl(
return true
}
fun makeDialog(initial: String?, callback: Consumer<String?>): Dialog {
fun makeDialog(playerID: UUID, initial: String?, callback: Consumer<String?>): Dialog {
val maxLength = this.maxLength.get()
val initialFinal = initial?.take(maxLength)
val baseBuilder = DialogBase.builder(USER_FACING_RENAME_TITLE)
.canCloseWithEscape(true)
.afterAction(DialogBase.DialogAfterAction.CLOSE)
.inputs(listOf(
.inputs(
listOf(
DialogInput.text(RENAME_TEXT_KEY, Component.text("Rename text"))
.maxLength(maxLength)
.initial(initialFinal ?: "")
@ -79,56 +93,73 @@ class AnvilRenameDialogImpl(
.build(),
),
)
baseBuilder.body(listOf(
baseBuilder.body(
listOf(
DialogBody.plainMessage(USER_FACING_WARNING, MAX_WIDTH)
))
)
)
return Dialog.create { builder -> builder.empty()
return Dialog.create { builder ->
builder.empty()
.base(baseBuilder.build())
.type(DialogType.confirmation(
.type(
DialogType.confirmation(
ActionButton.builder(USER_FACING_CONFIRM)
.action(DialogAction.customClick({ response, _ ->
hasUiOpen.remove(playerID)
val text = response.getText(RENAME_TEXT_KEY)!!
callback.accept(text)
}, ClickCallback.Options.builder().build()))
.build(),
ActionButton.builder(USER_FACING_CANCEL)
.action(DialogAction.customClick({ response, _ ->
hasUiOpen.remove(playerID)
}, ClickCallback.Options.builder().build()))
.build(),
))
)
)
}
}
private fun setResult(player: HumanEntity, view: CraftAnvilView, result: String?) {
val defaultName = PLAIN_TEXT_SERIALIZER.serializeOrNull(view.getItem(0)?.effectiveName())
if(defaultName == result) {
setName(player, view, "")
if(defaultName != null) lastNames[player.uniqueId] = defaultName
}
else setName(player, view, result)
private fun setResult(player: HumanEntity, view: InventoryView, result: String?) {
val defaultName = itemDefaultName(view.getItem(0))
if (defaultName == result) {
setName(player, view, "", null)
if (defaultName != null) lastNames[player.uniqueId] = defaultName
} else setName(player, view, result, result)
}
private fun setName(player: HumanEntity, view: CraftAnvilView, name: String?) {
private fun setName(player: HumanEntity, view: InventoryView, name: String?, rename: String?) {
val menu = (containerField.get(view) as AnvilMenu)
menu.itemName = name
val isSameName = menu.itemName == name
menu.itemName = rename
if(name == null)
if (name == null)
lastNames.remove(player.uniqueId)
else
lastNames[player.uniqueId] = name
if (rename == null)
lastRenames.remove(player.uniqueId)
else
lastRenames[player.uniqueId] = rename
if (!isSameName)
CraftEventFactory.callPrepareResultEvent(menu, 2);
}
private fun nameFromItem(player: HumanEntity, item: ItemStack?): String? {
// Already has text
if(item?.hasItemMeta() != true || !item.itemMeta.hasCustomName())
if (item?.hasItemMeta() != true || !item.itemMeta.hasCustomName())
return PLAIN_TEXT_SERIALIZER.serializeOrNull(item?.effectiveName())
if(keepUserPreviousDialog.get() && item.hasItemMeta()) {
if (keepUserPreviousDialog.get() && item.hasItemMeta()) {
val lastName = item.itemMeta.persistentDataContainer.get(
AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY,
PersistentDataType.STRING)
PersistentDataType.STRING
)
if(lastName != null) return lastName
if (lastName != null) return lastName
}
return fromFormated.apply(player, item.effectiveName())
@ -136,33 +167,37 @@ class AnvilRenameDialogImpl(
private fun tryShowDialogScheduled(player: HumanEntity, event: PrepareAnvilEvent) {
val view = event.view
if(view !is CraftAnvilView) return
if (view !is CraftAnvilView) return
val renameText = view.renameText
val leftItem = view.getItem(0)
val leftItemStr = leftItem?.toString()
val lastName = lastNames.getOrDefault(player.uniqueId, null)
if(lastLeftItem.getOrDefault(player.uniqueId, null) != leftItemStr) {
if(leftItemStr == null)
val lastName = lastNames.getOrDefault(player.uniqueId, null)
val lastRename = lastRenames.getOrDefault(player.uniqueId, null)
if (lastLeftItem.getOrDefault(player.uniqueId, null) != leftItemStr) {
if (leftItemStr == null)
lastLeftItem.remove(player.uniqueId)
else lastLeftItem[player.uniqueId] = leftItemStr
setName(player, view, nameFromItem(player, leftItem))
setName(player, view, renameText, nameFromItem(player, leftItem))
return
}
if(lastName == renameText)
if (lastName == renameText || lastRename == renameText)
return
if(renameText?.isBlank() == true) {
setName(player, view, lastNames[player.uniqueId])
if (renameText?.isBlank() == true || renameText == itemDefaultName(leftItem)) {
setName(player, view, lastName, lastRename)
return
}
val dialog = makeDialog(lastName)
val dialog = makeDialog(player.uniqueId, lastRename)
{ result -> setResult(player, view, result) }
player.showDialog(dialog)
hasUiOpen.add(player.uniqueId)
}
// We need to wait for a short time as changing item will change the name BEFORE the item change
@ -178,13 +213,14 @@ class AnvilRenameDialogImpl(
{},
2
)
if(task == null) return
if (task == null) return
runTaskMap[player.uniqueId] = task
}
override fun closeInventory(player: HumanEntity) {
lastNames.remove(player.uniqueId)
lastRenames.remove(player.uniqueId)
lastLeftItem.remove(player.uniqueId)
runTaskMap.remove(player.uniqueId)?.cancel()
}
@ -193,4 +229,8 @@ class AnvilRenameDialogImpl(
return lastNames[player.uniqueId]
}
override fun isOpenFor(player: HumanEntity): Boolean {
return hasUiOpen.contains(player.uniqueId)
}
}

View file

@ -0,0 +1,45 @@
package xyz.alexcrea.cuanvil.util
import io.papermc.paper.threadedregions.scheduler.ScheduledTask
import org.bukkit.entity.HumanEntity
import org.bukkit.inventory.InventoryView
import org.bukkit.plugin.Plugin
import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog
import java.util.HashMap
import java.util.UUID
object AnvilTitleUtil {
private val runTaskMap = HashMap<UUID, ScheduledTask>()
private fun actualRename(view: InventoryView, name: String, player: HumanEntity, anvilDialog: AnvilRenameDialog) {
runTaskMap.remove(player.uniqueId)
if (view.title == name) return
// We assume rename impl is used
if (anvilDialog.isOpenFor(player)) return
view.title = name
}
// We don't want to rename instantly it is causing issue with rename text
// especially as it can "override" current ui when it is rename ui time but rename ui also need some delay
fun rename(view: InventoryView, name: String, player: HumanEntity, anvilDialog: AnvilRenameDialog, plugin: Plugin) {
runTaskMap.remove(player.uniqueId)?.cancel()
val task = player.scheduler.runDelayed(
plugin,
{ _ ->
run { actualRename(view, name, player, anvilDialog) }
},
{
runTaskMap.remove(player.uniqueId)
},
2
)
if (task == null) return
runTaskMap[player.uniqueId] = task
}
}

View file

@ -0,0 +1,12 @@
package xyz.alexcrea.cuanvil.util
import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.view.AnvilView
object ModernPrepareAnvilCreator {
fun createPrepareAnvil(view: InventoryView, item: ItemStack?): PrepareAnvilEvent {
return PrepareAnvilEvent(view as AnvilView, item)
}
}

View file

@ -17,7 +17,7 @@ import org.jetbrains.annotations.NotNull;
* Most of the time you would likely need {@link CAPreAnvilBypassEvent} or {@link CAEarlyPreAnvilBypassEvent}
* for this event to be useful.
* <p>
* There is also {@link CATreatAnvilResultEvent} that may be better for some use case.
* There is also {@link CATreatAnvilResult2Event} that may be better for some use case.
*/
public class CAClickResultBypassEvent extends Event implements Cancellable {

View file

@ -15,7 +15,7 @@ import org.jetbrains.annotations.NotNull;
* <p>
* You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful.
* <p>
* It is also recommended that you read about {@link CAPreAnvilBypassEvent} and {@link CATreatAnvilResultEvent}
* It is also recommended that you read about {@link CAPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event}
* as your use case may be more prone to use theses.
*/
public class CAEarlyPreAnvilBypassEvent extends Event implements Cancellable {

View file

@ -18,7 +18,7 @@ import org.jetbrains.annotations.NotNull;
* <p>
* You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful.
* <p>
* It is also recommended that you read about {@link CAEarlyPreAnvilBypassEvent} and {@link CATreatAnvilResultEvent}
* It is also recommended that you read about {@link CAEarlyPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event}
* as your use case may be more prone to use theses.
*/
public class CAPreAnvilBypassEvent extends Event implements Cancellable {

View file

@ -0,0 +1,196 @@
package xyz.alexcrea.cuanvil.api.event.listener;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.alexcrea.cuanvil.anvil.AnvilCost;
import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
/**
* Called after custom anvil processed the click on the result on the anvil inventory.
* This event should be used to modify the result of an anvil use.
* <p>
* You may also want to check {@link CAClickResultBypassEvent},
* {@link CAPreAnvilBypassEvent}
* and {@link CAEarlyPreAnvilBypassEvent} for your use case
* <p>
* A null result will cancel this event
*/
@SuppressWarnings("unused")
public class CATreatAnvilResult2Event extends Event {
private static final HandlerList HANDLERS = new HandlerList();
public static HandlerList getHandlerList() {
return HANDLERS;
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
@NotNull
private final InventoryView view;
private final AnvilUseType useType;
@Nullable
private final ItemStack left;
@Nullable
private final ItemStack right;
@Nullable
private ItemStack result;
private final AnvilCost cost;
@ApiStatus.Internal
public CATreatAnvilResult2Event(
@NotNull InventoryView view,
Inventory inv,
AnvilUseType useType,
@Nullable ItemStack result,
AnvilCost cost) {
this.view = view;
this.useType = useType;
this.left = inv.getItem(0); // TODO use view here
this.right = inv.getItem(1);
this.result = result;
this.cost = cost;
}
/**
* Get the bukkit inventory view.
* <p>
* Temporarily marked as internal as it will get changed to anvil view on legacy removal
* so signature will change
*
* @return The inventory view of this event.
*/
@ApiStatus.Internal
public @NotNull InventoryView getView() {
return view;
}
/**
* Get the type of use source of the result.
*
* @return The craft use type.
*/
public AnvilUseType getUseType() {
return useType;
}
/**
* Get the left item of the anvil use
*
* @return the left item
*/
public @Nullable ItemStack getLeftItem() {
return left;
}
/**
* Get the right item of the anvil use
*
* @return the right item
*/
public @Nullable ItemStack getRightItem() {
return right;
}
/**
* Get the current result
* <p>
* note that it will not be null unless another listener previously set it to null.
*
* @return The current result.
*/
public @Nullable ItemStack getResult() {
return result;
}
/**
* Set the current result
* <p>
* note that a null result will cancel this anvil use.
*
* @param result The new result
*/
public void setResult(@Nullable ItemStack result) {
this.result = result;
}
/**
* Get the level cost displayed on the anvil.
* <h3>Important note:</h3>
* the final price are re calculated on click for the following use case:
* <ul>
* <li>Custom craft</li>
* <li>Unit repair</li>
* <li>Lore edit</li>
* </ul>
* This value will be used as final price for:
* <li>Item merge</li>
* <li>Item rename</li>
* </ul>
*
* @return The current cost.
* @deprecated use #{@link #getCost()} instead
*/
@Deprecated(forRemoval = true, since = "1.17.0")
public int getLevelCost() {
return cost.asXpCost();
}
/**
* Set the level cost displayed on the anvil.
* <h3>Important note:</h3>
* the final price are re calculated on click for the following use case:
* <ul>
* <li>Custom craft</li>
* <li>Unit repair</li>
* <li>Lore edit</li>
* </ul>
* This value will be used as final price for:
* <li>Item merge</li>
* <li>Item rename</li>
* </ul>
*
* @param levelCost The new cost.
* @deprecated use #{@link #getCost()} and set value on this instead
*/
@Deprecated(forRemoval = true, since = "1.17.0")
public void setLevelCost(int levelCost) {
cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost());
}
/**
* Allow access to the current cost of the event
* Note that modifying this object will change the event resulting cost
*
* <h3>Important note:</h3>
* the final price are re calculated on click for the following use case:
* <ul>
* <li>Custom craft</li>
* <li>Unit repair</li>
* <li>Lore edit</li>
* </ul>
* This value will be used as final price for:
* <li>Item merge</li>
* <li>Item rename</li>
*
* @return the current anvil cost
*/
public AnvilCost getCost() {
return cost;
}
}

View file

@ -6,7 +6,8 @@ import org.bukkit.event.inventory.PrepareAnvilEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.alexcrea.cuanvil.util.AnvilUseType;
import xyz.alexcrea.cuanvil.anvil.AnvilCost;
import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
/**
* Called after custom anvil processed the click on the result on the anvil inventory.
@ -17,8 +18,12 @@ import xyz.alexcrea.cuanvil.util.AnvilUseType;
* and {@link CAEarlyPreAnvilBypassEvent} for your use case
* <p>
* A null result will cancel this pre anvil event
*
* @deprecated Prepare anvil Event cannot be provided as it can be called on result and therefore not have prepared anvil event
* use {@link CATreatAnvilResult2Event} instead
*/
@SuppressWarnings("unused")
@Deprecated(forRemoval = true, since = "1.17.0")
public class CATreatAnvilResultEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@ -40,13 +45,13 @@ public class CATreatAnvilResultEvent extends Event {
@Nullable
private ItemStack result;
private int levelCost;
private final AnvilCost cost;
public CATreatAnvilResultEvent(@NotNull PrepareAnvilEvent event, AnvilUseType useType, @Nullable ItemStack result, int levelCost) {
public CATreatAnvilResultEvent(@NotNull PrepareAnvilEvent event, AnvilUseType useType, @Nullable ItemStack result, AnvilCost cost) {
this.event = event;
this.useType = useType;
this.result = result;
this.levelCost = levelCost;
this.cost = cost;
}
/**
@ -104,9 +109,11 @@ public class CATreatAnvilResultEvent extends Event {
* </ul>
*
* @return The current cost.
* @deprecated use #{@link #getCost()} instead
*/
@Deprecated(forRemoval = true, since = "1.17.0")
public int getLevelCost() {
return levelCost;
return cost.asXpCost();
}
/**
@ -124,8 +131,32 @@ public class CATreatAnvilResultEvent extends Event {
* </ul>
*
* @param levelCost The new cost.
* @deprecated use #{@link #getCost()} and set value on this instead
*/
@Deprecated(forRemoval = true, since = "1.17.0")
public void setLevelCost(int levelCost) {
this.levelCost = levelCost;
cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost());
}
/**
* Allow access to the current cost of the event
* Note that modifying this object will change the event resulting cost
*
* <h3>Important note:</h3>
* the final price are re calculated on click for the following use case:
* <ul>
* <li>Custom craft</li>
* <li>Unit repair</li>
* <li>Lore edit</li>
* </ul>
* This value will be used as final price for:
* <li>Item merge</li>
* <li>Item rename</li>
*
* @return the current anvil cost
*/
public AnvilCost getCost() {
return cost;
}
}

View file

@ -2,7 +2,7 @@ package xyz.alexcrea.cuanvil.config;
import com.google.common.collect.ImmutableMap;
import org.jetbrains.annotations.Nullable;
import xyz.alexcrea.cuanvil.util.AnvilUseType;
import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
import java.util.EnumMap;

View file

@ -11,11 +11,11 @@ import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
import xyz.alexcrea.cuanvil.config.ConfigHolder;
import xyz.alexcrea.cuanvil.config.WorkPenaltyType;
import xyz.alexcrea.cuanvil.gui.config.global.BasicConfigGui;
import xyz.alexcrea.cuanvil.gui.util.GuiGlobalActions;
import xyz.alexcrea.cuanvil.util.AnvilUseType;
import java.util.ArrayList;
import java.util.EnumMap;

View file

@ -2,10 +2,10 @@ package xyz.alexcrea.cuanvil.update.plugin;
import io.delilaheve.util.ConfigOptions;
import org.bukkit.configuration.file.FileConfiguration;
import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
import xyz.alexcrea.cuanvil.config.ConfigHolder;
import xyz.alexcrea.cuanvil.config.WorkPenaltyType;
import xyz.alexcrea.cuanvil.gui.config.settings.WorkPenaltyTypeSettingGui;
import xyz.alexcrea.cuanvil.util.AnvilUseType;
import javax.annotation.Nonnull;
import java.util.EnumMap;

View file

@ -12,6 +12,7 @@ import xyz.alexcrea.cuanvil.command.ReloadExecutor
import xyz.alexcrea.cuanvil.config.ConfigHolder
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dependency.MinecraftVersionUtil
import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil
import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry
import xyz.alexcrea.cuanvil.gui.config.MainConfigGui
@ -259,9 +260,11 @@ open class CustomAnvil : JavaPlugin() {
MainConfigGui.getInstance().init(DependencyManager.packetManager)
GuiSharedConstant.loadConstants()
// Prepare economy if possible
EconomyManager.setupEconomy(this)
// Finally, re add default we may be missing
PluginSetDefault.reAddMissingDefault()
}
fun reloadResource(

View file

@ -3,12 +3,16 @@ package io.delilaheve.util
import io.delilaheve.CustomAnvil
import io.delilaheve.util.EnchantmentUtil.enchantmentName
import org.bukkit.NamespacedKey
import org.bukkit.entity.HumanEntity
import xyz.alexcrea.cuanvil.anvil.AnvilUseType
import xyz.alexcrea.cuanvil.config.ConfigHolder
import xyz.alexcrea.cuanvil.config.WorkPenaltyType
import xyz.alexcrea.cuanvil.config.WorkPenaltyType.WorkPenaltyPart
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager
import xyz.alexcrea.cuanvil.enchant.CAEnchantment
import xyz.alexcrea.cuanvil.util.AnvilUseType
import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil
import java.math.BigDecimal
import java.util.*
/**
@ -72,6 +76,11 @@ object ConfigOptions {
const val IMMUTABLE_ENCHANTMENT_LIST = "immutable_enchantments"
// Monetary configs
const val MONETARY_USAGE_ROOT = "monetary_cost"
const val SHOULD_USE_MONEY = "$MONETARY_USAGE_ROOT.enabled"
const val MONEY_CURRENCY = "$MONETARY_USAGE_ROOT.currency"
const val MONETARY_MULTIPLIER_ROOT = "$MONETARY_USAGE_ROOT.multipliers"
// Keys for specific enchantment values
private const val KEY_BOOK = "book"
@ -110,6 +119,11 @@ object ConfigOptions {
const val DEFAULT_PER_COLOR_CODE_PERMISSION = false
// Monetary configs
const val DEFAULT_SHOULD_USE_MONEY = false
const val DEFAULT_MONEY_CURRENCY = "default"
const val DEFAULT_MONEY_MULTIPLIER = 1.0
// Debug flag
private const val DEFAULT_DEBUG_LOG = false
private const val DEFAULT_VERBOSE_DEBUG_LOG = false
@ -164,6 +178,11 @@ object ConfigOptions {
// Default max before merge disabled (negative mean enabled)
const val DEFAULT_MAX_BEFORE_MERGE_DISABLED = -1
// -----------
// Permissions
// -----------
private const val RENAME_DIALOG_PERMISSION = "ca.rename.dialog"
// -------------
// Get methods
// -------------
@ -452,6 +471,13 @@ object ConfigOptions {
.getBoolean(DIALOG_RENAME_USE_PERMISSION, DEFAULT_DIALOG_RENAME_USE_PERMISSION)
}
fun canUseDialogRename(player: HumanEntity): Boolean {
if(!doRenameDialog || !AnvilRenameDialogUtil.anvilRenameDialog.canSendDialog()) return false
if(doRenameDialogUsePermission && !player.hasPermission(RENAME_DIALOG_PERMISSION)) return false
return true
}
/**
* Do the dialog menu require permission
*/
@ -625,4 +651,29 @@ object ConfigOptions {
return false
}
/*
* Monetary configs (only for 1.21.6+)
* Also require dialog rename
*/
fun shouldUseMoney(player: HumanEntity): Boolean {
return EconomyManager.economy?.initialized() == true &&
canUseDialogRename(player) &&
ConfigHolder.DEFAULT_CONFIG
.config
.getBoolean(SHOULD_USE_MONEY, DEFAULT_SHOULD_USE_MONEY)
}
val usedCurrency: String
get() {
return ConfigHolder.DEFAULT_CONFIG
.config
.getString(MONEY_CURRENCY, DEFAULT_MONEY_CURRENCY)!!
}
fun getMonetaryMultiplier(type: String): BigDecimal {
return BigDecimal(ConfigHolder.DEFAULT_CONFIG
.config
.getDouble("$MONETARY_MULTIPLIER_ROOT.$type", DEFAULT_MONEY_MULTIPLIER))
}
}

View file

@ -0,0 +1,55 @@
package xyz.alexcrea.cuanvil.anvil
import java.math.BigDecimal
import io.delilaheve.util.ConfigOptions.getMonetaryMultiplier as moneyMultiplier
open class AnvilCost {
private val isAlone: Boolean
var valid = true // Get set as invalid if cost can be satisfied
var isMonetary = false
var generic = 0
var enchantment = 0
var repair = 0
var rename = 0
var lore = 0
var illegalPenalty = 0
var workPenalty = 0
var recipe = 0
constructor(generic: Int) {
this.generic = generic
isAlone = true
}
constructor() {
isAlone = false
}
fun asXpCost(): Int {
return generic + enchantment + repair + rename + lore + illegalPenalty + workPenalty + recipe
}
open fun asMonetaryCost(): BigDecimal {
// multiply by per use type multipliers
return BigDecimal(generic)
.add(BigDecimal(enchantment).multiply(moneyMultiplier("enchantment")))
.add(BigDecimal(repair).multiply(moneyMultiplier("repair")))
.add(BigDecimal(rename).multiply(moneyMultiplier("rename")))
.add(BigDecimal(lore).multiply(moneyMultiplier("lore_edit")))
.add(BigDecimal(enchantment).multiply(moneyMultiplier("enchantment")))
.add(BigDecimal(illegalPenalty).multiply(moneyMultiplier("work_penalty")))
.add(BigDecimal(workPenalty).multiply(moneyMultiplier("work_penalty")))
.add(BigDecimal(recipe).multiply(moneyMultiplier("recipe")))
.multiply(moneyMultiplier("global"))
}
}
class CustomCraftCost(val rawCost: Int): AnvilCost() {
override fun asMonetaryCost(): BigDecimal {
return BigDecimal(rawCost)
.multiply(moneyMultiplier("global"))
}
}

View file

@ -0,0 +1,331 @@
package xyz.alexcrea.cuanvil.anvil
import io.delilaheve.CustomAnvil
import io.delilaheve.util.ConfigOptions
import io.delilaheve.util.EnchantmentUtil.combineWith
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.Material
import org.bukkit.entity.HumanEntity
import org.bukkit.entity.Player
import org.bukkit.inventory.AnvilInventory
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.ItemMeta
import org.bukkit.persistence.PersistentDataType
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog
import xyz.alexcrea.cuanvil.enchant.CAEnchantment
import xyz.alexcrea.cuanvil.recipe.AnvilCustomRecipe
import xyz.alexcrea.cuanvil.util.CasedStringUtil
import xyz.alexcrea.cuanvil.util.CustomRecipeUtil
import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir
import xyz.alexcrea.cuanvil.util.MiniMessageUtil
import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair
import xyz.alexcrea.cuanvil.util.anvil.AnvilColorUtil
import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.config.LoreEditType
import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil
object AnvilMergeLogic {
open class AnvilResult {
companion object {
val EMPTY = AnvilResult(null, AnvilCost())
}
val item: ItemStack?
val cost: AnvilCost
val ignoreXpRules: Boolean
constructor(item: ItemStack?, cost: AnvilCost, ignoreXpRules: Boolean = false) {
this.item = item
this.cost = cost
this.ignoreXpRules = ignoreXpRules
}
fun isEmpty(): Boolean {
return item == null
}
}
class UnitRepairResult : AnvilResult {
companion object {
val EMPTY = UnitRepairResult(null, AnvilCost(), 0)
}
val repairAmount: Int
constructor(item: ItemStack?, cost: AnvilCost, repairAmount: Int) : super(item, cost) {
this.repairAmount = repairAmount
}
}
class CustomCraftResult : AnvilResult {
companion object {
val EMPTY = CustomCraftResult(null, CustomCraftCost(0), 0, null)
}
val customCraftCost: CustomCraftCost
val amount: Int
val recipe: AnvilCustomRecipe?
constructor(
item: ItemStack?, cost: CustomCraftCost,
amount: Int, recipe: AnvilCustomRecipe?
) : super(item, cost, true) {
this.customCraftCost = cost
this.amount = amount
this.recipe = recipe
}
}
class LoreEditResult : AnvilResult {
companion object {
val EMPTY = LoreEditResult(null, AnvilCost(), LoreEditType.APPEND_PAPER)
}
val type: LoreEditType
constructor(item: ItemStack?, cost: AnvilCost, type: LoreEditType) : super(item, cost) {
this.type = type
}
}
fun doRenaming(
view: InventoryView, //TODO use anvil view
inventory: AnvilInventory,
player: Player, first: ItemStack
): AnvilResult {
val resultItem = DependencyManager.cloneItem(player, first)
val cost = AnvilCost()
cost.rename = handleRename(resultItem, inventory, player)
// Test/stop if nothing changed.
if (first == resultItem) {
CustomAnvil.log("no right item, But input is same as output")
return AnvilResult.EMPTY
}
cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY)
val result =
DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.RENAME_ONLY, cost)
return AnvilResult(result, cost)
}
private fun processDialogPCD(meta: ItemMeta, player: HumanEntity) {
val text = AnvilRenameDialogUtil.anvilRenameDialog.currentText(player)
return processPCD(meta, player, text)
}
fun processPCD(meta: ItemMeta, player: HumanEntity, text: String?) {
val keepDialog = ConfigOptions.canUseDialogRename(player) && ConfigOptions.shouldKeepRenameText
val pdc = meta.persistentDataContainer
if (!keepDialog)
pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY)
else {
if (text == null || text.isBlank())
pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY)
else pdc.set(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY, PersistentDataType.STRING, text)
}
}
private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int {
// Can be null
var renameText = ChatColor.stripColor(inventory.renameText)
var sumCost = 0
var useColor = false
if (ConfigOptions.renameColorPossible && renameText != null) {
val component = AnvilColorUtil.handleColor(
renameText,
AnvilColorUtil.renamePermission(player)
)
if (component != null) {
renameText = MiniMessageUtil.legacy_mm.serialize(component)
sumCost += ConfigOptions.useOfColorCost
useColor = true
}
}
// Rename item and add renaming cost
resultItem.itemMeta?.let {
val hasDisplayName = it.hasDisplayName()
val displayName = if (!hasDisplayName) null
else if (useColor) it.displayName
else ChatColor.stripColor(it.displayName)
if (!displayName.contentEquals(renameText) && !(displayName == null &&
renameText == "" ||
//TODO on recent paper check effective name instead
renameText == CasedStringUtil.snakeToUpperSpacedCase(resultItem.type.name.lowercase())
)
) {
it.setDisplayName(renameText)
processDialogPCD(it, player)
resultItem.itemMeta = it
sumCost += ConfigOptions.itemRenameCost
}
return sumCost
}
return 0
}
fun doMerge(
view: InventoryView, //TODO use anvil view instead
inventory: AnvilInventory,
player: Player,
first: ItemStack, second: ItemStack
): AnvilResult {
val newEnchants = first.findEnchantments()
.combineWith(second.findEnchantments(), first, player)
var hasChanged = !isIdentical(first.findEnchantments(), newEnchants)
val resultItem = DependencyManager.cloneItem(player, first)
val cost = AnvilCost()
if (hasChanged) {
resultItem.setEnchantmentsUnsafe(newEnchants)
// Calculate enchantment cost
AnvilXpUtil.getRightValues(second, resultItem, cost)
}
// 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)
cost.repair = if (repaired) ConfigOptions.itemRepairCost else 0
hasChanged = hasChanged || repaired
}
// Test/stop if nothing changed.
if (!hasChanged) {
CustomAnvil.log("Mergeable with second, But input is same as output")
return AnvilResult.EMPTY
}
// As calculatePenalty edit result, we need to calculate penalty after checking equality
cost.workPenalty = AnvilXpUtil.calculatePenalty(first, second, resultItem, AnvilUseType.MERGE)
// Calculate rename cost
cost.rename = handleRename(resultItem, inventory, player)
val result =
DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.MERGE, cost)
return AnvilResult(result, cost)
}
private fun isIdentical(
firstEnchants: MutableMap<CAEnchantment, Int>,
resultEnchants: MutableMap<CAEnchantment, Int>
): Boolean {
if (firstEnchants.size != resultEnchants.size) return false
for (entry in resultEnchants) {
if (firstEnchants.getOrDefault(entry.key, entry.value - 1) != entry.value) return false
}
return true
}
// return true if a custom recipe exist with these ingredients
fun testCustomRecipe(
view: InventoryView, //TODO use anvil view instead
inventory: AnvilInventory,
player: Player,
first: ItemStack, second: ItemStack?
): CustomCraftResult {
val recipe = CustomRecipeUtil.getCustomRecipe(first, second)
CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}")
if (recipe == null) return CustomCraftResult.EMPTY
val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second)
val resultItem: ItemStack = DependencyManager.cloneItem(player, recipe.resultItem!!)
resultItem.amount *= amount
// Maybe add an option on custom craft to ignore/not ignore penalty ??
val xpCost = recipe.determineCost(amount, first, resultItem)
val cost = CustomCraftCost(xpCost)
// This is for displayed cost
cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost)
else AnvilXpUtil.calculateLevelForXp(xpCost)
val result =
DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.CUSTOM_CRAFT, cost)
return CustomCraftResult(result, cost, amount, recipe)
}
fun testUnitRepair(
view: InventoryView, //TODO use anvil view
inventory: AnvilInventory,
player: Player,
first: ItemStack, second: ItemStack
): UnitRepairResult {
val unitRepairAmount = first.getRepair(second) ?: return UnitRepairResult.EMPTY
return testUnitRepair(view, inventory, player, first, second, unitRepairAmount)
}
fun testUnitRepair(
view: InventoryView, //TODO use anvil view instead
inventory: AnvilInventory,
player: Player,
first: ItemStack, second: ItemStack,
unitRepairAmount: Double
): UnitRepairResult {
val resultItem = DependencyManager.cloneItem(player, first)
val cost = AnvilCost()
cost.rename = handleRename(resultItem, inventory, player)
val repairAmount = resultItem.unitRepair(second.amount, unitRepairAmount)
if (repairAmount > 0)
cost.repair = repairAmount * ConfigOptions.unitRepairCost
// We do not care about right item penalty for unit repair
cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.UNIT_REPAIR)
// Test/stop if nothing changed.
if (first == resultItem) {
CustomAnvil.log("unit repair, But input is same as output")
return UnitRepairResult.EMPTY
}
val result =
DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.UNIT_REPAIR, cost)
return UnitRepairResult(result, cost, repairAmount)
}
fun testLoreEdit(
player: Player,
first: ItemStack, second: ItemStack
): LoreEditResult {
val type = second.type
val result = if (Material.WRITABLE_BOOK == type)
AnvilLoreEditUtil.tryLoreEditByBook(player, first, second)
else if (Material.PAPER == type)
AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second)
else LoreEditResult.EMPTY
if (result.isEmpty()) return result
if (result.item!!.isAir || first == result.item) {
CustomAnvil.log("lore edit, But input is same as output")
return LoreEditResult.EMPTY
}
return result
}
}

View file

@ -1,60 +1,60 @@
package xyz.alexcrea.cuanvil.util
package xyz.alexcrea.cuanvil.anvil
import org.bukkit.Material
import xyz.alexcrea.cuanvil.config.WorkPenaltyType.WorkPenaltyPart
import xyz.alexcrea.cuanvil.util.config.LoreEditType
import xyz.alexcrea.cuanvil.config.WorkPenaltyType
import xyz.alexcrea.cuanvil.util.anvil.AnvilUseTypeUtil
enum class AnvilUseType(
val typeName: String, val path: String,
val defaultPenalty: WorkPenaltyPart,
val defaultPenalty: WorkPenaltyType.WorkPenaltyPart,
val displayName: String, val displayMat: Material
) {
RENAME_ONLY(
"rename_only",
WorkPenaltyPart(false, true),
WorkPenaltyType.WorkPenaltyPart(false, true),
"Rename Only", Material.NAME_TAG
),
MERGE(
"merge",
WorkPenaltyPart(true, true),
WorkPenaltyType.WorkPenaltyPart(true, true),
"Merge", Material.ANVIL
),
UNIT_REPAIR(
"unit_repair",
WorkPenaltyPart(true, true),
WorkPenaltyType.WorkPenaltyPart(true, true),
"Unit Repair", Material.DIAMOND
),
CUSTOM_CRAFT(
"custom_craft",
WorkPenaltyPart(false, false),
WorkPenaltyType.WorkPenaltyPart(false, false),
"Custom Craft", Material.CRAFTING_TABLE
),
LORE_EDIT_BOOK_APPEND(
"lore_edit_book_append", "lore_edit.book_and_quil.append",
WorkPenaltyPart(false, false),
WorkPenaltyType.WorkPenaltyPart(false, false),
"Book Add", Material.WRITABLE_BOOK
),
LORE_EDIT_BOOK_REMOVE(
"lore_edit_book_remove", "lore_edit.book_and_quil.remove",
WorkPenaltyPart(false, false),
WorkPenaltyType.WorkPenaltyPart(false, false),
"Book Remove", Material.WRITABLE_BOOK
),
LORE_EDIT_PAPER_APPEND(
"lore_edit_paper_append", "lore_edit.paper.append_line",
WorkPenaltyPart(false, false),
WorkPenaltyType.WorkPenaltyPart(false, false),
"Paper Add", Material.WRITABLE_BOOK
),
LORE_EDIT_PAPER_REMOVE(
"lore_edit_paper_remove", "lore_edit.paper.remove_line",
WorkPenaltyPart(false, false),
WorkPenaltyType.WorkPenaltyPart(false, false),
"Paper Remove", Material.WRITABLE_BOOK
),
;
constructor(
typeName: String,
defaultPenalty: WorkPenaltyPart,
defaultPenalty: WorkPenaltyType.WorkPenaltyPart,
displayName: String, displayMat: Material
) :
this(

View file

@ -7,15 +7,19 @@ import org.bukkit.Bukkit
import org.bukkit.ChatColor
import org.bukkit.command.CommandSender
import org.bukkit.entity.HumanEntity
import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.AnvilInventory
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.anvil.AnvilUseType
import xyz.alexcrea.cuanvil.api.event.listener.CAClickResultBypassEvent
import xyz.alexcrea.cuanvil.api.event.listener.CAEarlyPreAnvilBypassEvent
import xyz.alexcrea.cuanvil.api.event.listener.CAPreAnvilBypassEvent
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResultEvent
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResult2Event
import xyz.alexcrea.cuanvil.config.ConfigHolder
import xyz.alexcrea.cuanvil.dependency.datapack.DataPackDependency
import xyz.alexcrea.cuanvil.dependency.gui.GenericExternGuiTester
@ -29,7 +33,6 @@ import xyz.alexcrea.cuanvil.dependency.scheduler.TaskScheduler
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.componentLore
import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener.Companion.ANVIL_OUTPUT_SLOT
import xyz.alexcrea.cuanvil.util.AnvilUseType
import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError
import java.util.logging.Level
@ -198,7 +201,7 @@ object DependencyManager {
}
// Return true if should bypass (either by a dependency or error)
fun tryEventPreAnvilBypass(event: PrepareAnvilEvent, player: HumanEntity): Boolean {
fun tryEventPreAnvilBypass(event: PrepareAnvilEvent, player: Player): Boolean {
try {
return unsafeTryEventPreAnvilBypass(event, player)
} catch (e: Exception) {
@ -207,7 +210,7 @@ object DependencyManager {
}
}
private fun unsafeTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: HumanEntity): Boolean {
private fun unsafeTryEventPreAnvilBypass(event: PrepareAnvilEvent, player: Player): Boolean {
// Run the event
val bypassEvent = CAPreAnvilBypassEvent(event)
Bukkit.getPluginManager().callEvent(bypassEvent)
@ -232,22 +235,24 @@ object DependencyManager {
// Return null if there was an issue
fun tryTreatAnvilResult(
event: PrepareAnvilEvent,
view: InventoryView,
inventory: Inventory, // TODO REMOVE, use view instead on legacy removal
player: HumanEntity,
result: ItemStack,
useType: AnvilUseType,
cost: Int
): CATreatAnvilResultEvent? {
val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost)
cost: AnvilCost
): ItemStack? {
val treatEvent = CATreatAnvilResult2Event(view, inventory, useType, result, cost)
try {
unsafeTryTreatAnvilResult(treatEvent)
return treatEvent;
return treatEvent.result
} catch (e: Exception) {
logExceptionAndClear(event.view.player, event.inventory, e)
logExceptionAndClear(player, inventory, e)
return null
}
}
private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResultEvent) {
private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResult2Event) {
Bukkit.getPluginManager().callEvent(event)
excellentEnchantsCompatibility?.treatAnvilResult(event)
@ -293,11 +298,11 @@ object DependencyManager {
}
// Clone item and use plugin specific clone if needed
fun cloneItem(event: PrepareAnvilEvent, item: ItemStack): ItemStack {
fun cloneItem(player: HumanEntity, item: ItemStack): ItemStack {
try {
return unsafeCloneItem(item)
} catch (e: Exception) {
logException(event.view.player, e)
logException(player, e)
return item.clone()
}
}

View file

@ -0,0 +1,32 @@
package xyz.alexcrea.cuanvil.dependency.economy
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import java.math.BigDecimal
interface EconomyManager {
companion object {
var economy: EconomyManager? = null
fun setupEconomy(plugin: Plugin) {
if (plugin.server.pluginManager.getPlugin("Vault") == null)
return
if (UnlockedEconomyManager.unlockedAvailable())
economy = UnlockedEconomyManager(plugin)
if (economy == null || !economy!!.initialized())
economy = VaultEconomyManager(plugin)
}
}
fun initialized(): Boolean
// We assume "initialized" got checked before these function get called
fun has(player: Player, money: BigDecimal): Boolean
fun remove(player: Player, money: BigDecimal): Boolean
fun format(money: BigDecimal): String;
}

View file

@ -0,0 +1,74 @@
package xyz.alexcrea.cuanvil.dependency.economy
import io.delilaheve.util.ConfigOptions
import net.milkbowl.vault2.economy.Economy
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import java.math.BigDecimal
class UnlockedEconomyManager : EconomyManager {
val plugin: String
val economy: Economy?
companion object {
fun unlockedAvailable(): Boolean {
try {
Class.forName("net.milkbowl.vault2.economy.Economy")
return true
} catch (_: ClassNotFoundException) {
return false
}
}
}
constructor(plugin: Plugin) {
this.plugin = plugin.name
val rsp = plugin.server.servicesManager.getRegistration(Economy::class.java)
economy = rsp?.getProvider()
}
override fun initialized(): Boolean {
return economy != null
}
private fun currency(): String {
val configured = ConfigOptions.usedCurrency
return if ("default".equals(configured, true))
economy!!.getDefaultCurrency(plugin)
else configured
}
override fun has(player: Player, money: BigDecimal): Boolean {
if (money.signum() <= 0) return true
return economy!!.has(
plugin,
player.uniqueId,
player.world.name,
currency(),
money
)
}
override fun remove(player: Player, money: BigDecimal): Boolean {
if (money.signum() <= 0) return true
return economy!!.withdraw(
plugin,
player.uniqueId,
player.world.name,
currency(),
money
)
.transactionSuccess()
}
override fun format(money: BigDecimal): String {
return economy!!.format(plugin, money, currency())
}
}

View file

@ -0,0 +1,37 @@
package xyz.alexcrea.cuanvil.dependency.economy
import net.milkbowl.vault.economy.Economy
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import java.math.BigDecimal
class VaultEconomyManager : EconomyManager {
val economy: Economy?
constructor(plugin: Plugin) {
val rsp = plugin.server.servicesManager.getRegistration(Economy::class.java)
economy = rsp?.getProvider()
}
override fun initialized(): Boolean {
return economy != null
}
override fun has(player: Player, money: BigDecimal): Boolean {
if (money.signum() <= 0) return true
return economy!!.has(player, money.toDouble())
}
override fun remove(player: Player, money: BigDecimal): Boolean {
if (money.signum() <= 0) return true
return economy!!.withdrawPlayer(player, money.toDouble()).transactionSuccess()
}
override fun format(money: BigDecimal): String {
return economy!!.format(money.toDouble())
}
}

View file

@ -8,15 +8,16 @@ import com.jankominek.disenchantment.events.ShatterEvent
import com.jankominek.disenchantment.listeners.DisenchantClickListener
import com.jankominek.disenchantment.listeners.ShatterClickListener
import io.delilaheve.CustomAnvil
import org.bukkit.entity.HumanEntity
import org.bukkit.entity.Player
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.ItemStack
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener
import xyz.alexcrea.cuanvil.util.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.MetricsUtil.trackError
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil
import java.util.logging.Level
import kotlin.reflect.KClass
@ -50,7 +51,7 @@ class DisenchantmentDependency {
InventoryClickEvent.getHandlerList().unregister(listener)
}
fun testPrepareAnvil(event: PrepareAnvilEvent, player: HumanEntity): Boolean {
fun testPrepareAnvil(event: PrepareAnvilEvent, player: Player): Boolean {
val previousResult = event.result
event.result = null
@ -58,14 +59,14 @@ class DisenchantmentDependency {
DisenchantEvent.onEvent(event)
if (event.result != null) {
CustomAnvil.log("Detected pre anvil item extract bypass.")
AnvilXpUtil.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost)
AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost))
return true
}
ShatterEvent.onEvent(event)
if (event.result != null) {
CustomAnvil.log("Detected pre anvil split enchant bypass.")
AnvilXpUtil.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost)
AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost))
return true
}

View file

@ -8,11 +8,12 @@ import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.RegisteredListener
import xyz.alexcrea.cuanvil.api.EnchantmentApi
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResultEvent
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResult2Event
import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEPreV5Enchantment
import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEV5Enchantment
import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEV5_4Enchantment
import xyz.alexcrea.cuanvil.enchant.wrapped.CALegacyEEEnchantment
import xyz.alexcrea.cuanvil.util.ModernPrepareAnvilCreator
import java.lang.reflect.Method
import su.nightexpress.excellentenchants.api.EnchantRegistry as V5EnchantRegistry
import su.nightexpress.excellentenchants.enchantment.impl.universal.CurseOfFragilityEnchant as LegacyCurseOfFragilityEnchant
@ -218,14 +219,20 @@ class ExcellentEnchantsDependency {
return handleRechargeMethod.invoke(this.usedAnvilListener, event, first, second) as Boolean
}
fun treatAnvilResult(event: CATreatAnvilResultEvent) {
val result = event.result
if (result == null) return
fun treatAnvilResult(event: CATreatAnvilResult2Event) {
val result = event.result ?: return
val first: ItemStack = treatInput(event.event.inventory.getItem(0))
val second: ItemStack = treatInput(event.event.inventory.getItem(1))
val first: ItemStack = treatInput(event.leftItem)
val second: ItemStack = treatInput(event.rightItem)
val fakeEvent: PrepareAnvilEvent = try {
//TODO remove this on legacy removal
PrepareAnvilEvent(event.view, result)
} catch (_: NoSuchMethodError) {
ModernPrepareAnvilCreator.createPrepareAnvil(event.view, result)
}
handleCombineMethod.invoke(this.usedAnvilListener, fakeEvent, first, second, result)
handleCombineMethod.invoke(this.usedAnvilListener, event.event, first, second, result)
event.result = fakeEvent.result
}
fun testAnvilResult(event: InventoryClickEvent): Any {

View file

@ -1,7 +1,7 @@
package xyz.alexcrea.cuanvil.dependency.plugins
import io.delilaheve.CustomAnvil
import org.bukkit.entity.HumanEntity
import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.PrepareAnvilEvent
import org.bukkit.inventory.AnvilInventory
@ -9,8 +9,9 @@ import org.bukkit.plugin.RegisteredListener
import valorless.havenbags.HavenBags
import valorless.havenbags.features.BagSkin
import valorless.havenbags.features.BagUpgrade
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.listener.PrepareAnvilListener
import xyz.alexcrea.cuanvil.util.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil
class HavenBagsDependency {
@ -45,7 +46,7 @@ class HavenBagsDependency {
}
fun testPrepareAnvil(event: PrepareAnvilEvent, player: HumanEntity): Boolean {
fun testPrepareAnvil(event: PrepareAnvilEvent, player: Player): Boolean {
val previousResult = event.result
event.result = null
@ -53,14 +54,14 @@ class HavenBagsDependency {
bagSkin.onPrepareAnvil(event)
if (event.result != null) {
CustomAnvil.log("Detected pre anvil heaven bag anvil skin.")
AnvilXpUtil.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost)
AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost))
return true
}
bagUpgrade.onPrepareAnvil(event)
if (event.result != null) {
CustomAnvil.log("Detected pre anvil heaven bag anvil upgrade.")
AnvilXpUtil.setAnvilInvXp(event.inventory, event.view, player, event.inventory.repairCost)
AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, AnvilCost(event.inventory.repairCost))
return true
}

View file

@ -3,7 +3,6 @@ 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
@ -16,22 +15,25 @@ import org.bukkit.inventory.AnvilInventory
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.BookMeta
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.AnvilResult
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.CustomCraftResult
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.LoreEditResult
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.UnitRepairResult
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentDisplayName
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.AnvilLoreEditUtil
import xyz.alexcrea.cuanvil.util.AnvilUseType
import xyz.alexcrea.cuanvil.util.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.CustomRecipeUtil
import xyz.alexcrea.cuanvil.util.MiniMessageUtil
import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair
import xyz.alexcrea.cuanvil.util.anvil.AnvilLoreEditUtil
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil
import xyz.alexcrea.cuanvil.util.config.LoreEditType
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.min
@ -50,6 +52,7 @@ class AnvilResultListener : Listener {
fun anvilExtractionCheck(event: InventoryClickEvent) {
val player = event.whoClicked as? Player ?: return
val inventory = event.inventory as? AnvilInventory ?: return
val view = event.view
if (event.rawSlot != ANVIL_OUTPUT_SLOT) {
return
@ -64,84 +67,104 @@ class AnvilResultListener : Listener {
val leftItem = inventory.getItem(ANVIL_INPUT_LEFT) ?: return
val rightItem = inventory.getItem(ANVIL_INPUT_RIGHT)
if (GameMode.CREATIVE != player.gameMode && inventory.repairCost >= inventory.maximumRepairCost) {
// Deny by default. allow if working
event.result = Event.Result.DENY
if (GameMode.CREATIVE != player.gameMode && inventory.repairCost >= inventory.maximumRepairCost) {
return
}
// Test custom recipe
val recipe = CustomRecipeUtil.getCustomRecipe(leftItem, rightItem)
if (recipe != null) {
event.result = Event.Result.ALLOW
val customRecipeResult = AnvilMergeLogic.testCustomRecipe(view, inventory, player, leftItem, rightItem)
if (!customRecipeResult.isEmpty()) {
onCustomCraft(
event, recipe, player,
leftItem, rightItem, output, inventory
event, player, inventory,
leftItem, rightItem, customRecipeResult
)
return
}
// Do not continue if there was no change
if ((output == inventory.getItem(ANVIL_INPUT_LEFT))) {
event.result = Event.Result.DENY
return
}
// Rename
if (rightItem == null) {
event.result = Event.Result.ALLOW
val result = AnvilMergeLogic.doRenaming(view, inventory, player, leftItem)
if (result.isEmpty()) return
extractAnvilResult(
event, player, inventory,
null, 0,
null, 0,
result
)
return
}
// Merge
val canMerge = leftItem.canMergeWith(rightItem)
if (canMerge) {
event.result = Event.Result.ALLOW
val result = AnvilMergeLogic.doMerge(view, inventory, player, leftItem, rightItem)
extractAnvilResult(
event, player, inventory,
null, 0,
null, 0,
result
)
return
}
// Unit repair
val unitRepairResult = leftItem.getRepair(rightItem)
if (unitRepairResult != null) {
val unitRepairResult = AnvilMergeLogic.testUnitRepair(
view, inventory, player,
leftItem, rightItem
)
if (unitRepairResult.isEmpty()) {
onUnitRepairExtract(
leftItem, rightItem, output,
unitRepairResult, event, player, inventory
rightItem, event, player, inventory,
unitRepairResult
)
return
}
// For lore edit
if (handleBookLoreEdit(event, inventory, player, leftItem, rightItem, output)) {
return
} else if (handlePaperLoreEdit(event, inventory, player, leftItem, rightItem, output)) {
val loreResult = AnvilMergeLogic.testLoreEdit(player, leftItem, rightItem)
if (!loreResult.isEmpty()) {
if (loreResult.type.isBook)
handleBookLoreEdit(event, inventory, player, leftItem, rightItem, loreResult)
else
handlePaperLoreEdit(event, inventory, player, leftItem, rightItem, loreResult)
return
}
// Else there was no working situation somehow so we deny
event.result = Event.Result.DENY
}
private fun onCustomCraft(
event: InventoryClickEvent,
recipe: AnvilCustomRecipe,
player: Player,
inventory: AnvilInventory,
leftItem: ItemStack,
rightItem: ItemStack?,
output: ItemStack,
inventory: AnvilInventory
result: CustomCraftResult,
) {
event.result = Event.Result.DENY
if (recipe.leftItem == null) return // in case it changed
val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem)
val xpCost = recipe.determineCost(amount, leftItem, output)
val recipe = result.recipe!!
val rawCost = result.customCraftCost.rawCost
val finalCost =
if (recipe.removeExactLinearXp) xpCost
else AnvilXpUtil.calculateLevelForXp(xpCost)
if (recipe.removeExactLinearXp) rawCost
else AnvilXpUtil.calculateLevelForXp(rawCost)
CustomAnvil.log(
"gamemode: ${player.gameMode != GameMode.CREATIVE}, " +
"cost: $finalCost, level: ${player.level}, " +
"result: ${player.totalExperience < finalCost} ${player.level < finalCost}"
)
CustomAnvil.log("gamemode: ${player.gameMode != GameMode.CREATIVE}, cost: $finalCost, level: ${player.level}, result: ${player.totalExperience < finalCost} ${player.level < finalCost}")
if (player.gameMode != GameMode.CREATIVE) {
if (recipe.removeExactLinearXp) {
if (ConfigOptions.shouldUseMoney(player)) {
result.cost.isMonetary = true
if (!EconomyManager.economy!!.has(player, result.cost.asMonetaryCost())) return
} else if (recipe.removeExactLinearXp) {
val levelXp = AnvilXpUtil.calculateXpForLevel(player.level)
val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp
val totalXp = levelXp + player.exp * delta
@ -158,31 +181,31 @@ class AnvilResultListener : Listener {
if (event.click != ClickType.MIDDLE &&
!handleCustomCraftClick(
event,
recipe,
inventory,
player,
leftItem,
rightItem,
amount,
finalCost,
recipe.removeExactLinearXp
result
)
) return
// Finally, we add the item to the player
if (slotDestination.type == SlotType.CURSOR) {
player.setItemOnCursor(output)
player.setItemOnCursor(result.item)
} else {// We assume SlotType == SlotType.INVENTORY
player.inventory.setItem(slotDestination.slot, output)
player.inventory.setItem(slotDestination.slot, result.item)
}
}
private fun handleCustomCraftClick(
event: InventoryClickEvent, recipe: AnvilCustomRecipe,
event: InventoryClickEvent,
inventory: AnvilInventory, player: Player,
leftItem: ItemStack, rightItem: ItemStack?,
amount: Int, xpCost: Int, linearCost: Boolean = false
result: CustomCraftResult
): Boolean {
val amount = result.amount
val recipe = result.recipe!!
// We remove what should be removed
if (rightItem != null) {
if (recipe.rightItem == null) return false// in case it changed
@ -194,25 +217,7 @@ class AnvilResultListener : Listener {
leftItem.amount -= amount * recipe.leftItem!!.amount
inventory.setItem(ANVIL_INPUT_LEFT, leftItem)
if (player.gameMode != GameMode.CREATIVE) {
if (linearCost) {
val levelXp = AnvilXpUtil.calculateXpForLevel(player.level)
val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp
var totalXp = levelXp + player.exp * delta
totalXp -= xpCost
val newLevel = AnvilXpUtil.calculateLevelForXp(totalXp.toInt())
val newLevelXp = AnvilXpUtil.calculateXpForLevel(newLevel)
val newDelta = AnvilXpUtil.calculateXpForLevel(newLevel + 1) - newLevelXp
val xp = (totalXp - newLevelXp) / newDelta
player.level = newLevel
player.exp = xp / newDelta
} else {
player.level -= xpCost
}
}
removeCustomCraftCost(player, result)
// Then we try to find the new values for the anvil
val newAmount = CustomRecipeUtil.getCustomRecipeAmount(recipe, leftItem, rightItem)
@ -236,6 +241,47 @@ class AnvilResultListener : Listener {
return true
}
private fun removeCustomCraftCost(player: Player, result: CustomCraftResult) {
if (player.gameMode == GameMode.CREATIVE) return
val rawCost = result.customCraftCost.rawCost
if (result.cost.isMonetary) {
EconomyManager.economy!!.remove(player, result.cost.asMonetaryCost())
return
}
if (result.recipe!!.removeExactLinearXp) {
val levelXp = AnvilXpUtil.calculateXpForLevel(player.level)
val delta = AnvilXpUtil.calculateXpForLevel(player.level + 1) - levelXp
var totalXp = levelXp + player.exp * delta
totalXp -= rawCost
val newLevel = AnvilXpUtil.calculateLevelForXp(totalXp.toInt())
val newLevelXp = AnvilXpUtil.calculateXpForLevel(newLevel)
val newDelta = AnvilXpUtil.calculateXpForLevel(newLevel + 1) - newLevelXp
val xp = (totalXp - newLevelXp) / newDelta
player.level = newLevel
player.exp = xp / newDelta
} else {
player.level -= AnvilXpUtil.calculateLevelForXp(rawCost)
}
}
private fun tryRemoveCost(player: Player, cost: AnvilCost): Boolean {
if (player.gameMode == GameMode.CREATIVE) return true
if (cost.isMonetary) {
val result = EconomyManager.economy!!.remove(player, cost.asMonetaryCost())
if (!result) return false
} else {
player.level -= cost.asXpCost()
}
return true
}
private fun extractAnvilResult(
event: InventoryClickEvent,
player: Player,
@ -244,15 +290,17 @@ class AnvilResultListener : Listener {
leftRemoveCount: Int,
rightItem: ItemStack?,
rightRemoveCount: Int,
output: ItemStack,
repairCost: Int,
result: AnvilResult
): Boolean {
if (result.isEmpty()) return false
// To avoid vanilla, we cancel the event
event.result = Event.Result.DENY
event.isCancelled = true
val cost = result.cost
// Assumed if player do not have enough xp then it returned MIN_VALUE
if (repairCost == Int.MIN_VALUE) return false
processCost(inventory, player, cost)
if (!cost.valid && player.gameMode != GameMode.CREATIVE) return false
// Where should we get the item
val slotDestination = getActionSlot(event, player)
@ -260,6 +308,8 @@ class AnvilResultListener : Listener {
// If not creative middle click...
if (event.click != ClickType.MIDDLE) {
if (!tryRemoveCost(player, cost)) return false
// We remove what should be removed
if (leftItem != null) leftItem.amount -= leftRemoveCount
inventory.setItem(ANVIL_INPUT_LEFT, leftItem)
@ -268,99 +318,58 @@ class AnvilResultListener : Listener {
inventory.setItem(ANVIL_INPUT_RIGHT, rightItem)
inventory.setItem(ANVIL_OUTPUT_SLOT, null)
player.level -= repairCost
}
// Finally, we add the item to the player
if (SlotType.CURSOR == slotDestination.type) {
player.setItemOnCursor(output)
player.setItemOnCursor(result.item)
} else {// We assume SlotType == SlotType.INVENTORY
player.inventory.setItem(slotDestination.slot, output)
player.inventory.setItem(slotDestination.slot, result.item)
}
// TODO probably anvil damage & sound here ??
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
)
// Get repair cost
val repairCost = getUnitRepairCost(inventory, player, leftItem, output, resultCopy, resultAmount)
// And then we give the item manually
extractAnvilResult(
event, player, inventory,
null, 0,
rightItem, resultAmount,
resultCopy, repairCost
)
}
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, AnvilUseType.UNIT_REPAIR)
repairCost += resultAmount * ConfigOptions.unitRepairCost
private fun processCost(inventory: AnvilInventory, player: Player, cost: AnvilCost) {
var sum = cost.repair
if (
!ConfigOptions.doRemoveCostLimit &&
ConfigOptions.doCapCost
) {
repairCost = min(repairCost, ConfigOptions.maxAnvilCost)
val final = min(sum, ConfigOptions.maxAnvilCost)
cost.generic += (final - sum)
sum = final
}
if ((inventory.maximumRepairCost <= repairCost)
|| (player.level < repairCost)
) return Int.MIN_VALUE
return repairCost
if (ConfigOptions.shouldUseMoney(player)) {
cost.isMonetary = true
if (!EconomyManager.economy!!.has(player, cost.asMonetaryCost()))
cost.valid = false
} else {
if ((inventory.maximumRepairCost <= sum)
|| (player.level < sum)
) cost.valid = false
}
}
private fun getFromLoreEditXpCost(
xpCost: AtomicInteger,
private fun onUnitRepairExtract(
rightItem: ItemStack,
event: InventoryClickEvent,
player: Player,
inventory: AnvilInventory,
): Int {
if (GameMode.CREATIVE == player.gameMode) return 0
val repairCost = xpCost.get()
return if ((inventory.maximumRepairCost <= repairCost)
|| (player.level < repairCost)
) Int.MIN_VALUE
else repairCost
result: UnitRepairResult,
) {
// We give the item manually
extractAnvilResult(
event, player, inventory,
null, 0,
rightItem, result.repairAmount,
result
)
}
private fun handleBookLoreEdit(
@ -369,16 +378,22 @@ class AnvilResultListener : Listener {
player: Player,
leftItem: ItemStack,
rightItem: ItemStack,
output: ItemStack,
): Boolean {
if (Material.WRITABLE_BOOK != rightItem.type) return false
val bookMeta = rightItem.itemMeta as BookMeta? ?: return false
result: LoreEditResult
) {
if (result.type.isAppend)
handleBookLoreAppend(event, inventory, player, rightItem, result)
else
handleBookLoreRemove(event, inventory, player, leftItem, rightItem, result)
}
val editType = AnvilLoreEditUtil.bookLoreEditIsAppend(leftItem, rightItem) ?: return false
val xpCost = AtomicInteger()
if (editType) {
if (output != AnvilLoreEditUtil.handleLoreAppendByBook(player, leftItem, bookMeta, xpCost)) return false
private fun handleBookLoreAppend(
event: InventoryClickEvent,
inventory: AnvilInventory,
player: Player,
rightItem: ItemStack,
result: LoreEditResult
) {
val bookMeta = rightItem.itemMeta as BookMeta? ?: return
// Remove pages to book
val clearedBook: ItemStack?
@ -390,18 +405,27 @@ class AnvilResultListener : Listener {
clearedBook.itemMeta = bookMeta
}
return extractAnvilResult(
extractAnvilResult(
event, player, inventory,
null, 0,
clearedBook, 0,
output, getFromLoreEditXpCost(xpCost, player, inventory)
result
)
} else {
if (output != AnvilLoreEditUtil.handleLoreRemoveByBook(player, leftItem, xpCost)) return false
}
private fun handleBookLoreRemove(
event: InventoryClickEvent,
inventory: AnvilInventory,
player: Player,
leftItem: ItemStack,
rightItem: ItemStack,
result: LoreEditResult
) {
val bookMeta = rightItem.itemMeta as BookMeta? ?: return
// fill book meta
val lore = DependencyManager.stripLore(leftItem)
if (lore.isEmpty()) return false
if (lore.isEmpty()) return
val rightCopy: ItemStack?
if (LoreEditType.REMOVE_BOOK.doConsume) {
@ -413,7 +437,7 @@ class AnvilResultListener : Listener {
val bookPage = StringBuilder()
lore.forEach {
if (bookPage.isNotEmpty()) bookPage.append('\n')
if(it == null) return@forEach
if (it == null) return@forEach
bookPage.append(MiniMessageUtil.plain_text_mm.serialize(it))
}
@ -426,14 +450,13 @@ class AnvilResultListener : Listener {
rightCopy.itemMeta = bookMeta
}
return extractAnvilResult(
extractAnvilResult(
event, player, inventory,
null, 0,
rightCopy, 0,
output, getFromLoreEditXpCost(xpCost, player, inventory)
result
)
}
}
private fun handlePaperLoreEdit(
event: InventoryClickEvent,
@ -441,16 +464,23 @@ class AnvilResultListener : Listener {
player: Player,
leftItem: ItemStack,
rightItem: ItemStack,
output: ItemStack,
): Boolean {
if (Material.PAPER != rightItem.type) return false
val paperMeta = rightItem.itemMeta ?: return false
result: LoreEditResult
) {
if (result.type.isAppend)
handlePaperLoreAppend(event, inventory, player, rightItem, result)
else
handlePaperLoreRemove(event, inventory, player, leftItem, rightItem, result)
}
val editTypeIsAppend = AnvilLoreEditUtil.paperLoreEditIsAppend(leftItem, rightItem) ?: return false
private fun handlePaperLoreAppend(
event: InventoryClickEvent,
inventory: AnvilInventory,
player: Player,
rightItem: ItemStack,
result: LoreEditResult
) {
val paperMeta = rightItem.itemMeta ?: return
val xpCost = AtomicInteger()
if (editTypeIsAppend) {
if (output != AnvilLoreEditUtil.handleLoreAppendByPaper(player, leftItem, rightItem, xpCost)) return false
val paperCopy: ItemStack?
if (LoreEditType.APPEND_PAPER.doConsume) {
@ -460,31 +490,43 @@ class AnvilResultListener : Listener {
paperCopy = rightItem.clone()
paperCopy.amount = 1
paperMeta.setComponentDisplayName(null)
// Remove pcd name
AnvilMergeLogic.processPCD(paperMeta, player, null)
paperCopy.itemMeta = paperMeta
}
return if (rightItem.amount > 1) {
if (rightItem.amount > 1) {
extractAnvilResult(
event, player, inventory,
paperCopy, 0,
rightItem, 1,
output, getFromLoreEditXpCost(xpCost, player, inventory)
result
)
} else {
extractAnvilResult(
event, player, inventory,
null, 0,
paperCopy, 0,
output, getFromLoreEditXpCost(xpCost, player, inventory)
result
)
}
} else {
if (output != AnvilLoreEditUtil.handleLoreRemoveByPaper(player, leftItem, xpCost)) return false
}
private fun handlePaperLoreRemove(
event: InventoryClickEvent,
inventory: AnvilInventory,
player: Player,
leftItem: ItemStack,
rightItem: ItemStack,
result: LoreEditResult
) {
val leftMeta = leftItem.itemMeta
if (leftMeta == null || !leftMeta.hasLore()) return false
if (leftMeta == null || !leftMeta.hasLore()) return
val lore = DependencyManager.stripLore(leftItem)
if (lore.isEmpty()) return false
if (lore.isEmpty()) return
// Create result item
val rightClone: ItemStack?
@ -502,30 +544,28 @@ class AnvilResultListener : Listener {
rightClone = rightItem.clone()
rightClone.amount = 1
val resultMeta = rightClone.itemMeta ?: return false
val resultMeta = rightClone.itemMeta ?: return
resultMeta.setComponentDisplayName(ref.get())
rightClone.itemMeta = resultMeta
}
return if (rightItem.amount > 1) {
if (rightItem.amount > 1) {
extractAnvilResult(
event, player, inventory,
rightClone, 0,
rightItem, 1,
output, getFromLoreEditXpCost(xpCost, player, inventory)
result
)
} else {
extractAnvilResult(
event, player, inventory,
null, 0,
rightClone, 0,
output, getFromLoreEditXpCost(xpCost, player, inventory)
result
)
}
}
}
/**
* Get the destination slot or "NO_SLOT" slot container if there is no slot available
*/

View file

@ -3,33 +3,29 @@ package xyz.alexcrea.cuanvil.listener
import com.github.stefvanschie.inventoryframework.util.InventoryViewUtil
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.Material
import org.bukkit.entity.HumanEntity
import org.bukkit.entity.Player
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.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.EnchantmentStorageMeta
import org.bukkit.inventory.meta.ItemMeta
import org.bukkit.persistence.PersistentDataType
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.AnvilResult
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.doMerge
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.doRenaming
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.testCustomRecipe
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.testLoreEdit
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.testUnitRepair
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog
import xyz.alexcrea.cuanvil.enchant.CAEnchantment
import xyz.alexcrea.cuanvil.util.*
import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir
import xyz.alexcrea.cuanvil.util.UnitRepairUtil.getRepair
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil
import java.util.concurrent.atomic.AtomicInteger
/**
* Listener for anvil events
@ -44,8 +40,6 @@ class PrepareAnvilListener : Listener {
const val ANVIL_OUTPUT_SLOT = 2
var IS_EMPTY_TEST = false
private const val RENAME_DIALOG_PERMISSION = "ca.rename.dialog"
}
/**
@ -53,23 +47,27 @@ class PrepareAnvilListener : Listener {
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
fun anvilCombineCheck(event: PrepareAnvilEvent) {
// Should find player
val player: HumanEntity = InventoryViewUtil.getInstance().getPlayer(event.view)
val view = event.view
val inventory = event.inventory
val player = InventoryViewUtil.getInstance().getPlayer(view)
if(player !is Player) return
tryRenameDialog(player, event)
// Test if custom anvil is bypassed before immutability test
if (DependencyManager.earlyTryEventPreAnvilBypass(event, player)) {
// even if we got bypassed we still want to set price
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, event.inventory.repairCost)
AnvilXpUtil.setAnvilInvCost(inventory, view, player, AnvilCost(event.inventory.repairCost))
return
}
val first = inventory.getItem(ANVIL_INPUT_LEFT) ?: return
val first = inventory.getItem(ANVIL_INPUT_LEFT)
val second = inventory.getItem(ANVIL_INPUT_RIGHT)
if(IS_EMPTY_TEST) {
event.result = null
IS_EMPTY_TEST = false
applyResult(event, player, AnvilResult.EMPTY)
return
}
@ -81,79 +79,69 @@ class PrepareAnvilListener : Listener {
if (isImmutable(first) || isImmutable(second)) {
CustomAnvil.verboseLog("Skipping anvil process as one of the two item is immutable")
event.result = null
applyResult(event, player, AnvilResult.EMPTY)
return
}
tryRenameDialog(player, event)
// Test if the event should bypass custom anvil.
if (DependencyManager.tryEventPreAnvilBypass(event, player)) {
// even if we got bypassed we still want to set price
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, event.inventory.repairCost)
AnvilXpUtil.setAnvilInvCost(inventory, view, player, AnvilCost(event.inventory.repairCost))
return
}
if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return
val result = getResult(view, inventory, player, first, second)
applyResult(event, player, result)
}
fun getResult(
view: InventoryView, //TODO use anvil view
inventory: AnvilInventory,
player: Player,
first: ItemStack?, second: ItemStack?) : AnvilResult
{
if(first == null)
return AnvilResult.EMPTY
// Test custom recipe
if (testCustomRecipe(event, inventory, player, first, second)) return
var result: AnvilResult = testCustomRecipe(view, inventory, player, first, second)
if (!result.isEmpty())
return result
// Test rename lonely item
val isAir = second.isAir
CustomAnvil.verboseLog("checking air in main logic: $isAir")
if (isAir) {
doRenaming(event, inventory, player, first)
return
}
val shouldTryRename = second.isAir
CustomAnvil.verboseLog("checking air in main logic: $shouldTryRename")
if (shouldTryRename)
return doRenaming(view, inventory, player, first)
// Test for merge
if (first.canMergeWith(second!!)) {
doMerge(event, inventory, player, first, second)
return
}
if (first.canMergeWith(second!!))
return doMerge(view, inventory, player, first, second)
// Test for unit repair
if (testUnitRepair(event, inventory, player, first, second)) return
result = testUnitRepair(view, inventory, player, first, second)
if (!result.isEmpty())
return result
// Test for lore edit
if (testLoreEdit(event, inventory, player, first, second)) return
CustomAnvil.log("no anvil fuse type found")
event.result = null
result = testLoreEdit(player, first, second)
if (!result.isEmpty())
return result
return AnvilResult.EMPTY
}
private fun tryRenameDialog(
player: HumanEntity,
event: PrepareAnvilEvent
) {
if(!canUseRenameDialog(player)) return
if(!ConfigOptions.canUseDialogRename(player)) return
AnvilRenameDialogUtil.anvilRenameDialog.tryShowDialog(player, event)
}
private fun canUseRenameDialog(player: HumanEntity): Boolean {
if(!ConfigOptions.doRenameDialog || !AnvilRenameDialogUtil.anvilRenameDialog.canSendDialog()) return false
if(ConfigOptions.doRenameDialogUsePermission && !player.hasPermission(RENAME_DIALOG_PERMISSION)) return false
return true
}
private fun processDialogPCD(it: ItemMeta, player: HumanEntity) {
val keepDialog = canUseRenameDialog(player) && ConfigOptions.shouldKeepRenameText
val pdc = it.persistentDataContainer
if(!keepDialog)
pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY)
else {
val text = AnvilRenameDialogUtil.anvilRenameDialog.currentText(player)
if(text == null || text.isBlank())
pdc.remove(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY)
else pdc.set(AnvilRenameDialog.PCD_KEEP_RENAME_TEXT_KEY, PersistentDataType.STRING, text)
}
}
private fun isImmutable(item: ItemStack?): Boolean {
if (item.isAir) return false
@ -180,221 +168,14 @@ class PrepareAnvilListener : Listener {
return false
}
// return true if a custom recipe exist with these ingredients
private fun testCustomRecipe(
event: PrepareAnvilEvent, inventory: AnvilInventory,
player: HumanEntity,
first: ItemStack, second: ItemStack?
): Boolean {
val recipe = CustomRecipeUtil.getCustomRecipe(first, second)
CustomAnvil.verboseLog("custom recipe not null? ${recipe != null}")
if (recipe == null) return false
private fun applyResult(event: PrepareAnvilEvent, player: Player, result: AnvilResult) {
event.result = result.item
val amount = CustomRecipeUtil.getCustomRecipeAmount(recipe, first, second)
val resultItem: ItemStack = DependencyManager.cloneItem(event, recipe.resultItem!!)
resultItem.amount *= amount
// Maybe add an option on custom craft to ignore/not ignore penalty ??
val xpCost = recipe.determineCost(amount, first, resultItem)
val levelCost =
if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost)
else AnvilXpUtil.calculateLevelForXp(xpCost)
val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.CUSTOM_CRAFT, levelCost)
if (finalResult == null) return false
event.result = finalResult.result
if (finalResult.result.isAir) return false
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost, true)
return true
}
private fun doRenaming(
event: PrepareAnvilEvent, inventory: AnvilInventory,
player: HumanEntity, first: ItemStack
) {
val resultItem = DependencyManager.cloneItem(event, first)
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
if(result.item == null) {
AnvilXpUtil.onNoResult(player, event.view)
return
}
anvilCost += AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY)
val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.RENAME_ONLY, anvilCost)
if (finalResult == null) return
event.result = finalResult.result
if (finalResult.result.isAir) return
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost)
AnvilXpUtil.setAnvilInvCost(event.inventory, event.view, player, result.cost, result.ignoreXpRules)
}
private fun handleRename(resultItem: ItemStack, inventory: AnvilInventory, player: HumanEntity): Int {
// Can be null
var renameText = ChatColor.stripColor(inventory.renameText)
var sumCost = 0
var useColor = false
if (ConfigOptions.renameColorPossible && renameText != null) {
val component = AnvilColorUtil.handleColor(
renameText,
AnvilColorUtil.renamePermission(player))
if (component != null) {
renameText = MiniMessageUtil.legacy_mm.serialize(component)
sumCost += ConfigOptions.useOfColorCost
useColor = true
}
}
// Rename item and add renaming cost
resultItem.itemMeta?.let {
val hasDisplayName = it.hasDisplayName()
val displayName = if (!hasDisplayName) null
else if (useColor) it.displayName
else ChatColor.stripColor(it.displayName)
if (!displayName.contentEquals(renameText) && !(displayName == null &&
renameText == "" ||
//TODO on recent paper check effective name instead
renameText == CasedStringUtil.snakeToUpperSpacedCase(resultItem.type.name.lowercase())
)) {
it.setDisplayName(renameText)
processDialogPCD(it, player)
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)
var hasChanged = !isIdentical(first.findEnchantments(), newEnchants)
val resultItem = DependencyManager.cloneItem(event, first)
var anvilCost = 0
if(hasChanged){
resultItem.setEnchantmentsUnsafe(newEnchants)
// Calculate enchantment cost
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
hasChanged = hasChanged || repaired
}
// Test/stop if nothing changed.
if (!hasChanged) {
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, AnvilUseType.MERGE)
// Calculate rename cost
anvilCost += handleRename(resultItem, inventory, player)
// Finally, we set result
val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.MERGE, anvilCost)
if (finalResult == null) return
event.result = finalResult.result
if (finalResult.result.isAir) return
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost)
}
private fun isIdentical(
firstEnchants: MutableMap<CAEnchantment, Int>,
resultEnchants: MutableMap<CAEnchantment, Int>
): Boolean {
if(firstEnchants.size != resultEnchants.size) return false
for (entry in resultEnchants) {
if(firstEnchants.getOrDefault(entry.key, entry.value-1) != entry.value) return false
}
return true
}
// return true if there is a valid unit repair with these ingredients
private fun testUnitRepair(
event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity,
first: ItemStack, second: ItemStack
): Boolean {
val unitRepairAmount = first.getRepair(second) ?: return false
val resultItem = DependencyManager.cloneItem(event, first)
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, AnvilUseType.UNIT_REPAIR)
// Test/stop if nothing changed.
if (first == resultItem) {
CustomAnvil.log("unit repair, But input is same as output")
event.result = null
return true
}
val finalResult = DependencyManager.tryTreatAnvilResult(event, resultItem, AnvilUseType.UNIT_REPAIR, anvilCost)
if (finalResult == null) return false
event.result = finalResult.result
if (finalResult.result.isAir) return false
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, finalResult.levelCost)
return true
}
private fun testLoreEdit(
event: PrepareAnvilEvent, inventory: AnvilInventory, player: HumanEntity,
first: ItemStack, second: ItemStack
): Boolean {
val type = second.type
var result: ItemStack? = null
val xpCost = AtomicInteger()
if (Material.WRITABLE_BOOK == type) {
result = AnvilLoreEditUtil.tryLoreEditByBook(player, first, second, xpCost)
} else if (Material.PAPER == type) {
result = AnvilLoreEditUtil.tryLoreEditByPaper(player, first, second, xpCost)
}
if (result.isAir || first == result) {
CustomAnvil.log("lore edit, But input is same as output")
event.result = null
return false
}
event.result = result
AnvilXpUtil.setAnvilInvXp(inventory, event.view, player, xpCost.get())
return true
}
}

View file

@ -3,11 +3,11 @@ package xyz.alexcrea.cuanvil.recipe
import io.delilaheve.CustomAnvil
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.inventory.ItemStack
import xyz.alexcrea.cuanvil.anvil.AnvilUseType
import xyz.alexcrea.cuanvil.config.ConfigHolder
import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant
import xyz.alexcrea.cuanvil.util.AnvilUseType
import xyz.alexcrea.cuanvil.util.AnvilXpUtil
import xyz.alexcrea.cuanvil.util.MaterialUtil.isAir
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil
class AnvilCustomRecipe(
val name: String,

View file

@ -1,11 +1,11 @@
package xyz.alexcrea.cuanvil.util
package xyz.alexcrea.cuanvil.util.anvil
import io.delilaheve.util.ConfigOptions
import net.kyori.adventure.text.Component
import org.bukkit.permissions.Permissible
import xyz.alexcrea.cuanvil.util.MiniMessageUtil
import java.util.regex.Matcher
import java.util.regex.Pattern
import kotlin.text.indexOf
object AnvilColorUtil {
private val HEX_PATTERN: Pattern = Pattern.compile("#[A-Fa-f0-9]{6}") // pattern to find hexadecimal string

View file

@ -1,17 +1,19 @@
package xyz.alexcrea.cuanvil.util
package xyz.alexcrea.cuanvil.util.anvil
import net.kyori.adventure.text.Component
import org.bukkit.entity.HumanEntity
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.BookMeta
import org.bukkit.permissions.Permissible
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.anvil.AnvilMergeLogic.LoreEditResult
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.componentLore
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.setComponentLore
import xyz.alexcrea.cuanvil.util.MiniMessageUtil
import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil
import xyz.alexcrea.cuanvil.util.config.LoreEditType
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
object AnvilLoreEditUtil {
@ -31,7 +33,7 @@ object AnvilLoreEditUtil {
player: Permissible,
first: ItemStack,
book: BookMeta,
xpCost: AtomicInteger
cost: AnvilCost
): ItemStack? {
if (!hasLoreEditByBookPermission(player)) return null
@ -42,8 +44,10 @@ object AnvilLoreEditUtil {
val page = book.pages[0]
val lines = ArrayList<String>(page.split("\n"))
val outLines = ArrayList<Component>(lines.size)
val colorCost = colorLines(player, LoreEditType.APPEND_BOOK,
lines, outLines)
val colorCost = colorLines(
player, LoreEditType.APPEND_BOOK,
lines, outLines
)
lore.addAll(outLines)
@ -53,14 +57,14 @@ object AnvilLoreEditUtil {
if (result == first) return null
// Handle xp
xpCost.addAndGet(colorCost) // Cost of using color
xpCost.addAndGet(outLines.size * LoreEditType.APPEND_BOOK.perLineCost) // per line cost
xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.APPEND_BOOK)) // Fixed cost and work penalty
cost.lore = colorCost // Cost of using color
cost.lore += outLines.size * LoreEditType.APPEND_BOOK.perLineCost // per line cost
baseEditLoreXpCost(cost, first, result, LoreEditType.APPEND_BOOK) // Fixed cost and work penalty
return result
}
fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, xpCost: AtomicInteger): ItemStack? {
fun handleLoreRemoveByBook(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? {
if (!hasLoreEditByBookPermission(player)) return null
// remove lore
@ -78,9 +82,9 @@ object AnvilLoreEditUtil {
if (result == first) return null
// Handle xp
xpCost.addAndGet(uncolorCost)
xpCost.addAndGet(currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost)
xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.REMOVE_BOOK))
cost.lore = uncolorCost
cost.lore += currentLore.size * LoreEditType.REMOVE_BOOK.perLineCost
baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_BOOK)
return result
}
@ -116,12 +120,17 @@ object AnvilLoreEditUtil {
return null
}
fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack, xpCost: AtomicInteger): ItemStack? {
val isAppend = bookLoreEditIsAppend(first, second) ?: return null
fun tryLoreEditByBook(player: HumanEntity, first: ItemStack, second: ItemStack): LoreEditResult {
val isAppend = bookLoreEditIsAppend(first, second) ?: return LoreEditResult.EMPTY
val type = if (isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK
val meta = second.itemMeta as BookMeta
return if (isAppend) handleLoreAppendByBook(player, first, meta, xpCost)
else handleLoreRemoveByBook(player, first, xpCost)
val cost = AnvilCost()
val item = if (isAppend)
handleLoreAppendByBook(player, first, meta, cost)
else handleLoreRemoveByBook(player, first, cost)
return LoreEditResult(item, cost, type)
}
// Return true if appended, false if removed, null if neither
@ -147,7 +156,7 @@ object AnvilLoreEditUtil {
player: Permissible,
first: ItemStack,
second: ItemStack,
xpCost: AtomicInteger
cost: AnvilCost
): ItemStack? {
if (!hasLoreEditByPaperPermission(player)) return null
@ -159,9 +168,11 @@ object AnvilLoreEditUtil {
// A bit overdone to color 1 line but hey
val outList = ArrayList<Component>(1)
val colorCost = colorLines(player, LoreEditType.APPEND_PAPER,
val colorCost = colorLines(
player, LoreEditType.APPEND_PAPER,
Collections.singletonList(second.itemMeta!!.displayName),
outList)
outList
)
val line = outList[0]
if (appendEnd)
@ -175,13 +186,13 @@ object AnvilLoreEditUtil {
if (result == first) return null
// Handle xp
xpCost.addAndGet(colorCost)
xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.APPEND_PAPER))
cost.lore = colorCost
baseEditLoreXpCost(cost, first, result, LoreEditType.APPEND_PAPER)
return result
}
fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, xpCost: AtomicInteger): ItemStack? {
fun handleLoreRemoveByPaper(player: Permissible, first: ItemStack, cost: AnvilCost): ItemStack? {
if (!hasLoreEditByPaperPermission(player)) return null
// remove lore line
@ -213,8 +224,8 @@ object AnvilLoreEditUtil {
val uncolorCost = uncolorLine(player, line, LoreEditType.REMOVE_PAPER)
// Handle other xp
xpCost.addAndGet(uncolorCost)
xpCost.addAndGet(baseEditLoreXpCost(first, result, LoreEditType.REMOVE_PAPER))
cost.lore = uncolorCost
baseEditLoreXpCost(cost, first, result, LoreEditType.REMOVE_PAPER)
return result
}
@ -222,33 +233,39 @@ object AnvilLoreEditUtil {
fun tryLoreEditByPaper(
player: HumanEntity,
first: ItemStack,
second: ItemStack,
xpCost: AtomicInteger
): ItemStack? {
val isAppend = paperLoreEditIsAppend(first, second) ?: return null
second: ItemStack
): LoreEditResult {
val isAppend = paperLoreEditIsAppend(first, second) ?: return LoreEditResult.EMPTY
val type = if (isAppend) LoreEditType.APPEND_BOOK else LoreEditType.REMOVE_BOOK
return if (isAppend) handleLoreAppendByPaper(player, first, second, xpCost)
else handleLoreRemoveByPaper(player, first, xpCost)
val cost = AnvilCost()
val item = if (isAppend)
handleLoreAppendByPaper(player, first, second, cost)
else handleLoreRemoveByPaper(player, first, cost)
return LoreEditResult(item, cost, type)
}
private fun baseEditLoreXpCost(
cost: AnvilCost,
first: ItemStack,
result: ItemStack,
editType: LoreEditType
): Int {
var xpCost = editType.fixedCost
) {
cost.lore += editType.fixedCost
xpCost += AnvilXpUtil.calculatePenalty(first, null, result, editType.useType)
return xpCost
cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, result, editType.useType)
}
fun colorPermission(player: Permissible, editType: LoreEditType): AnvilColorUtil.ColorPermissions {
return AnvilColorUtil.calculatePermissions(player,
return AnvilColorUtil.calculatePermissions(
player,
false,
editType.allowColorCode,
editType.allowHexColor,
editType.allowMinimessage,
AnvilColorUtil.ColorUseType.LORE_EDIT)
AnvilColorUtil.ColorUseType.LORE_EDIT
)
}
private fun colorLine(line: String, permission: AnvilColorUtil.ColorPermissions): Component? {
@ -258,8 +275,10 @@ object AnvilLoreEditUtil {
)
}
private fun colorLines(player: Permissible, editType: LoreEditType,
lines: List<String>, outLines: MutableList<Component>): Int {
private fun colorLines(
player: Permissible, editType: LoreEditType,
lines: List<String>, outLines: MutableList<Component>
): Int {
val permission = colorPermission(player, editType)
val colorCost = editType.useColorCost
@ -286,7 +305,7 @@ object AnvilLoreEditUtil {
// Now handle color of each lines
var hasUndidColor = false
for ((index, line) in lines.withIndex()) {
if(line == null){
if (line == null) {
lines[index] = null
continue
}
@ -330,7 +349,7 @@ object AnvilLoreEditUtil {
var hasUndidColor = false
val result: String
if(clearedLine != null){
if (clearedLine != null) {
hasUndidColor = true
result = clearedLine
} else {

View file

@ -1,4 +1,4 @@
package xyz.alexcrea.cuanvil.util
package xyz.alexcrea.cuanvil.util.anvil
import io.delilaheve.util.ConfigOptions

View file

@ -1,4 +1,4 @@
package xyz.alexcrea.cuanvil.util
package xyz.alexcrea.cuanvil.util.anvil
import io.delilaheve.CustomAnvil
import io.delilaheve.util.ConfigOptions
@ -14,25 +14,48 @@ import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.Repairable
import org.bukkit.persistence.PersistentDataType
import xyz.alexcrea.cuanvil.anvil.AnvilCost
import xyz.alexcrea.cuanvil.anvil.AnvilUseType
import xyz.alexcrea.cuanvil.config.ConfigHolder
import xyz.alexcrea.cuanvil.dependency.DependencyManager
import xyz.alexcrea.cuanvil.dependency.economy.EconomyManager
import xyz.alexcrea.cuanvil.group.ConflictType
import xyz.alexcrea.cuanvil.util.AnvilTitleUtil
import xyz.alexcrea.cuanvil.util.dialog.AnvilRenameDialogUtil
import kotlin.math.min
object AnvilXpUtil {
const val EXCLUSIVE_PENALTY_PREFIX = "repair_cost"
/**
* Display the required cost (either as xp or as )
*/
fun setAnvilInvCost(
inventory: AnvilInventory,
view: InventoryView,
player: Player,
cost: AnvilCost,
ignoreRules: Boolean = false
) {
if (ConfigOptions.shouldUseMoney(player)) {
cost.isMonetary = true
setAnvilPrice(inventory, view, player, cost)
} else
setAnvilInvXp(inventory, view, player, cost.asXpCost(), ignoreRules)
}
/**
* Display xp needed for the work on the anvil inventory
*/
fun setAnvilInvXp(
private fun setAnvilInvXp(
inventory: AnvilInventory,
view: InventoryView,
player: HumanEntity,
anvilCost: Int,
ignoreRules: Boolean = false
) {
// Test repair cost limit
val finalAnvilCost = if (
!ignoreRules &&
@ -78,7 +101,51 @@ object AnvilXpUtil {
}
player.updateInventory()
}
}
/**
* Display monetary cost needed for the work on the anvil inventory
*/
private fun setAnvilPrice(
inventory: AnvilInventory,
view: InventoryView,
player: Player,
cost: AnvilCost,
) {
val finalCost = cost.asMonetaryCost()
val has = player.gameMode == GameMode.CREATIVE ||
EconomyManager.economy!!.has(player, finalCost)
val text = "Cost: " + (if (has) "§2" else "§4") +
EconomyManager.economy!!.format(finalCost)
AnvilTitleUtil.rename(
view, text,
player,
AnvilRenameDialogUtil.anvilRenameDialog,
CustomAnvil.instance
)
clearAnvilXpCost(inventory, view, player)
}
private fun clearAnvilXpCost(
inventory: AnvilInventory,
view: InventoryView,
player: HumanEntity,
) {
// TODO for 2.x.x use anvil view & set directly there
inventory.repairCost = 0
// retry after a tick
DependencyManager.scheduler.scheduleOnEntity(
CustomAnvil.instance, player
) {
inventory.repairCost = 0
if (player !is Player) return@scheduleOnEntity
player.updateInventory()
}
}
@ -128,6 +195,16 @@ object AnvilXpUtil {
return resultSum
}
fun onNoResult(player: HumanEntity, view: InventoryView) {
if (ConfigOptions.shouldUseMoney(player))
AnvilTitleUtil.rename(
view, "Repair & Name",
player,
AnvilRenameDialogUtil.anvilRenameDialog,
CustomAnvil.instance
)
}
private fun exclusivePenaltyKey(useType: AnvilUseType): NamespacedKey {
return NamespacedKey(CustomAnvil.instance, "${EXCLUSIVE_PENALTY_PREFIX}_${useType.typeName}")
}
@ -159,10 +236,8 @@ object AnvilXpUtil {
* Function to calculate right enchantment values
* it include enchantment placed on final item and conflicting enchantment
*/
fun getRightValues(right: ItemStack, result: ItemStack): Int {
fun getRightValues(right: ItemStack, result: ItemStack, cost: AnvilCost) {
// Calculate right value and illegal enchant penalty
var illegalPenalty = 0
var rightValue = 0
val rightIsFormBook = right.isEnchantedBook()
val resultEnchs = result.findEnchantments()
@ -180,7 +255,7 @@ object AnvilXpUtil {
resultEnchsKeys.remove(enchantment.key)
if (ConflictType.ENCHANTMENT_CONFLICT == conflictType) {
illegalPenalty += ConfigOptions.sacrificeIllegalCost
cost.illegalPenalty += ConfigOptions.sacrificeIllegalCost
CustomAnvil.verboseLog("Big conflict. Adding illegal price penalty")
}
continue
@ -191,16 +266,14 @@ object AnvilXpUtil {
val enchantmentMultiplier = ConfigOptions.enchantmentValue(enchantment.key, rightIsFormBook)
val value = resultLevel * enchantmentMultiplier
CustomAnvil.log("Value for ${enchantment.key.enchantmentName} level ${enchantment.value} is $value ($resultLevel * $enchantmentMultiplier)")
rightValue += value
cost.enchantment += value
}
CustomAnvil.log(
"Calculated right values: " +
"rightValue: $rightValue, " +
"illegalPenalty: $illegalPenalty"
"rightValue: ${cost.enchantment}, " +
"illegalPenalty: ${cost.illegalPenalty}"
)
return rightValue + illegalPenalty
}
/**

View file

@ -1,6 +1,6 @@
package xyz.alexcrea.cuanvil.util.config
import xyz.alexcrea.cuanvil.util.AnvilUseType
import xyz.alexcrea.cuanvil.anvil.AnvilUseType
import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.ALLOW_COLOR_CODE
import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.ALLOW_HEX_COLOR
import xyz.alexcrea.cuanvil.util.config.LoreEditConfigUtil.ALLOW_MINIMESSAGE
@ -18,20 +18,23 @@ import xyz.alexcrea.cuanvil.config.ConfigHolder.DEFAULT_CONFIG as CONFIG
enum class LoreEditType(
val rootPath: String,
val useType: AnvilUseType,
val isBook: Boolean,
val isAppend: Boolean,
val isMultiLine: Boolean,
) {
APPEND_BOOK(AnvilUseType.LORE_EDIT_BOOK_APPEND, true, true),
REMOVE_BOOK(AnvilUseType.LORE_EDIT_BOOK_REMOVE, false, true),
APPEND_PAPER(AnvilUseType.LORE_EDIT_PAPER_APPEND, true, false),
REMOVE_PAPER(AnvilUseType.LORE_EDIT_PAPER_REMOVE, false, false),
APPEND_BOOK(AnvilUseType.LORE_EDIT_BOOK_APPEND, true, true, true),
REMOVE_BOOK(AnvilUseType.LORE_EDIT_BOOK_REMOVE, true, false, true),
APPEND_PAPER(AnvilUseType.LORE_EDIT_PAPER_APPEND, false, true, false),
REMOVE_PAPER(AnvilUseType.LORE_EDIT_PAPER_REMOVE, false, false, false),
;
constructor(
useType: AnvilUseType,
isPaper: Boolean,
isAppend: Boolean,
isMultiLine: Boolean,
) : this(useType.path, useType, isAppend, isMultiLine)
) : this(useType.path, useType,
isPaper, isAppend, isMultiLine)
/**
* If this edit type is enabled

View file

@ -8,7 +8,7 @@ import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil
import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialog
import xyz.alexcrea.cuanvil.dialog.AnvilRenameDialogImpl
import xyz.alexcrea.cuanvil.update.UpdateUtils
import xyz.alexcrea.cuanvil.util.AnvilColorUtil
import xyz.alexcrea.cuanvil.util.anvil.AnvilColorUtil
object AnvilRenameDialogUtil {
@ -47,5 +47,9 @@ object AnvilRenameDialogUtil {
return null
}
override fun isOpenFor(player: HumanEntity): Boolean {
return false
}
}
}

View file

@ -432,6 +432,34 @@ lore_edit:
allow_hexadecimal_color: false
allow_minimessage: true
# Allow to replace the xp cost by a monetary cost
# If enabled it will not be bound to the experience level limits
#
# It also requires to enable dialog rename (set "enable_dialog_rename: false" a bit higher)
# If dialog rename permission is enabled and player do not have the permission merge will fall back to vanilla xp cost
#
# If you are using custom craft I recommend using Linear Xp Cost with Exact Linear Xp as normal Xp Cost will act "weird"
# But Linear Xp will act as 1$ time global multiplier. In other word: like you expect
#
# As this feature require dialog rename, it can only be enabled starting with paper 1.21.6 and later
monetary_cost:
enabled: false
# If using vault unlocked this allow to specify what currency should be used for anvil usage
# default being the default currency
currency: default
# multiply the anvil cost by a value to allow to have price a big bigger than like 40
multipliers:
# global multipliers. all usage type will be multiplied by this value
global: 1.0
# usage specific type. it will only apply for specific xp "reason"
enchantment: 1.0 # related to enchantments level
repair: 1.0 # for repairing via unit repair (per unit)
rename: 1.0 # for renaming the item
lore_edit: 1.0 # for changing the lore of the item (only if lore edit is enabled)
illegal_penalty: 1.0 # for trying to combine illegal enchantment
work_penalty: 1.0 # for work penalty (aka use penalty)
recipe: 1.0 # for custom anvil recipe cost
# Whether to show debug logging
debug_log: false

View file

@ -195,7 +195,7 @@ public class AnvilFuseTestUtil {
simulateClick(anvil, player, data.expectedResult());
// Should have similated the click
// Should have simulated the click
assertEqual(data.leftItem(), anvil.getFirstItem());
assertEqual(data.rightItem(), anvil.getSecondItem());
assertEqual(data.resultSlotItem(), anvil.getResult());
@ -260,7 +260,7 @@ public class AnvilFuseTestUtil {
}
public static boolean isAir(@Nullable ItemStack item) {
return item == null || item.isEmpty();
return item == null || item.isEmpty() || item.getAmount() == 0;
}
public static void assertPriceEqual(Integer expectedPrice, int price) {