Skip to content

Commit

Permalink
feat: max_damage and item_name components support (#249)
Browse files Browse the repository at this point in the history
* Set item name in item_name component

* Use Paper API for item names instead of NMS

* Change custom durability mechanic to vanilla

* Fix damage conversion

* Check max damage on item damage

* Refactor new durability mechanic
  • Loading branch information
Sylfare committed Jun 30, 2024
1 parent 93809b5 commit 8bbea3c
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public static ItemStack newStack(final CustomItem customItem) {
*/
public static ItemStack newStack(final CustomItem customItem, final int amount) {
final var itemStack = new ItemStack(customItem.baseMaterial(), amount);
itemStack.editMeta(meta -> meta.displayName(customItem.displayName()));
itemStack.editMeta(meta -> meta.itemName(customItem.displayName()));
return CustomItemHelper.updateItemStack(customItem, itemStack);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package org.oddlama.vane.core.item;

import java.util.ArrayList;

import org.bukkit.NamespacedKey;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.inventory.PrepareAnvilEvent;
import org.bukkit.event.player.PlayerItemDamageEvent;
import org.bukkit.event.player.PlayerItemMendEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.oddlama.vane.core.Core;
import org.oddlama.vane.core.Listener;
Expand All @@ -20,6 +17,9 @@

import net.kyori.adventure.text.Component;


// TODO: what about inventory based item repair?

public class DurabilityManager extends Listener<Core> {
public static final NamespacedKey ITEM_DURABILITY_MAX = StorageUtil.namespaced_key("vane", "durability.max");
public static final NamespacedKey ITEM_DURABILITY_DAMAGE = StorageUtil.namespaced_key("vane", "durability.damage");
Expand Down Expand Up @@ -52,68 +52,6 @@ private static void remove_lore(final ItemStack item_stack) {
}
}

/**
* Returns the remaining uses for a given item, if it has custom durability.
* Returns -1 otherwise.
*/
private static int remaining_uses(final ItemStack item_stack) {
if (item_stack == null || !item_stack.hasItemMeta()) {
return -1;
}
final var data = item_stack.getItemMeta().getPersistentDataContainer();
final var max = data.getOrDefault(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER, -1);
if (max == -1) {
return -1;
}
final var damage = data.getOrDefault(ITEM_DURABILITY_DAMAGE, PersistentDataType.INTEGER, 0);
return Math.max(0, Math.min(max - damage, max));
}

/**
* Updates lore on an item. Both persistent data field should exist,
* otherwise result is unspecified.
*/
private static void update_lore(final CustomItem custom_item, final ItemStack item_stack) {
var lore = item_stack.lore();
if (lore == null) {
lore = new ArrayList<Component>();
}

// Remove old component
lore.removeIf(DurabilityManager::is_durability_lore);

// Add new component only if requested
final var lore_component = custom_item.durabilityLore();
if (lore_component != null) {
final var max = item_stack.getItemMeta().getPersistentDataContainer().getOrDefault(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER, custom_item.durability());
final var remaining_uses = remaining_uses(item_stack);
lore.add(ItemUtil.add_sentinel(lore_component.args(Component.text(remaining_uses), Component.text(max)), SENTINEL));
}

item_stack.lore(lore);
}

/**
* Calculates visual damage value given the visual max durability,
* the actual damage and actual maximum damage. The actual damage value
* will be automatically clamped to reasonable values.
*/
private static int visual_damage(final int actual_damage, final int actual_max, final short visual_max) {
if (actual_damage <= 0) {
return 0;
} else if (actual_damage == actual_max - 1) {
// This forces item's that have 1 durability left to also show 1 durability left
// in their visual damage, which allows client mods to swap items if necessary.
return visual_max - 1;
} else if (actual_damage >= actual_max) {
return visual_max;
} else {
final var damage_percentage = (double)actual_damage / actual_max;
// Never allow the calculation to show the item as 0 visual durability,
return Math.min(visual_max - 1, (int)(damage_percentage * visual_max));
}
}

/**
* Sets the item's damage regarding our custom durability. The durability will get
* clamped to plausible values. Damage values >= max will result in item breakage.
Expand All @@ -125,32 +63,11 @@ private static void set_damage_and_update_item(final CustomItem custom_item, fin
if (ro_meta.isUnbreakable()) {
damage = 0;
}

// Clamp values below or above defined damage values.
final var actual_max = ro_meta.getPersistentDataContainer().getOrDefault(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER, custom_item.durability());
final var actual_damage = Math.min(actual_max, Math.max(0, damage));

// Store new damage values
item_stack.editMeta(meta -> {
final var data = meta.getPersistentDataContainer();
data.set(ITEM_DURABILITY_DAMAGE, PersistentDataType.INTEGER, actual_damage);
if (!data.has(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER)) {
data.set(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER, actual_max);
}
});

// Update lore
update_lore(custom_item, item_stack);

// Update visual damage if base item is damageable.
item_stack.editMeta(Damageable.class, damage_meta -> {
final var visual_max = item_stack.getType().getMaxDurability();
damage_meta.setDamage(visual_damage(actual_damage, actual_max, visual_max));
});
set_damage_and_max_damage(custom_item, item_stack, damage);
}

/**
* Initialized damage tags on the item, or removes them if custom durability
* Initializes damage on the item, or removes them if custom durability
* is disabled for the given custom item.
*/
public static boolean initialize_or_update_max(final CustomItem custom_item, final ItemStack item_stack) {
Expand Down Expand Up @@ -188,91 +105,71 @@ public static boolean initialize_or_update_max(final CustomItem custom_item, fin
}

set_damage_and_update_item(custom_item, item_stack, actual_damage);

return true;
}

/**
* Damages item by given amount regarding our custom durability.
* Negative amounts repair the item.
*/
private static void damage_and_update_item(final CustomItem custom_item, final ItemStack item_stack, final int amount) {
if (!item_stack.getItemMeta().getPersistentDataContainer().has(ITEM_DURABILITY_DAMAGE, PersistentDataType.INTEGER)) {
if (!initialize_or_update_max(custom_item, item_stack)) {
return;
}
}

final var damage = item_stack.getItemMeta().getPersistentDataContainer().getOrDefault(ITEM_DURABILITY_DAMAGE, PersistentDataType.INTEGER, 0);
set_damage_and_update_item(custom_item, item_stack, damage + amount);
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void on_item_damage(final PlayerItemDamageEvent event) {
final var item = event.getItem();
final var custom_item = get_module().item_registry().get(item);

// Ignore normal items or custom items with standard durability.
if (custom_item == null || custom_item.durability() <= 0) {
// Ignore normal items
if (custom_item == null) {
return;
}

damage_and_update_item(custom_item, item, event.getDamage());

// Wow this is hacky but the only workaround to prevent recusivly
// calling this event. We always increase the visual durability by 1
// and let the server implementation decrease it again to
// allow the item to break.
item.editMeta(Damageable.class, damage_meta -> damage_meta.setDamage(damage_meta.getDamage() - 1));
event.setDamage(1);
update_damage(custom_item, item);
}


@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void on_item_mend(final PlayerItemMendEvent event) {
// TODO: Durability Overridden items can not yet support mending effectively.
// see: https://github.com/PaperMC/Paper/issues/7313
final var item = event.getItem();
final var custom_item = get_module().item_registry().get(item);

// Ignore normal items or custom items with standard durability.
if (custom_item == null || custom_item.durability() <= 0) {
return;
/**
* Update existing max damage to match the configuration
*/
static public void update_damage(CustomItem custom_item, ItemStack item_stack) {
if (!(item_stack.getItemMeta() instanceof Damageable meta))
return; // everything should be damageable now

boolean updated = false;
PersistentDataContainer data = meta.getPersistentDataContainer();

final int new_max_damage = custom_item.durability() == 0 ? item_stack.getType().getMaxDurability() : custom_item.durability();

int old_damage;
int old_max_damage;
// if the item has damage in their data, get the value and remove it from PDC
if(data.has(ITEM_DURABILITY_DAMAGE) && data.has(ITEM_DURABILITY_MAX)) {
old_damage = data.get(ITEM_DURABILITY_DAMAGE, PersistentDataType.INTEGER);
old_max_damage = data.get(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER);
data.remove(ITEM_DURABILITY_DAMAGE);
data.remove(ITEM_DURABILITY_MAX);
updated = true;
} else {
old_damage = meta.hasDamage() ? meta.getDamage() : 0;
old_max_damage = meta.hasMaxDamage() ? meta.getMaxDamage() : item_stack.getType().getMaxDurability();
}

remove_lore(item_stack);

final var mend_amount = 2 * event.getExperienceOrb().getExperience();
damage_and_update_item(custom_item, item, -mend_amount);

// Never let the server do any repairing, our durability bar is our percentage indicator.
event.setCancelled(true);
if(!updated) updated = old_max_damage != new_max_damage; // only update if there was old data or a different max durability
if(!updated) return; // and do nothing if nothing changed
final int new_damage = scale_damage(old_damage, old_max_damage, new_max_damage);
set_damage_and_max_damage(custom_item, item_stack, new_damage);
}

// TODO: what about inventory based item repair?
// Update durability on result items.
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void on_prepare_anvil(final PrepareAnvilEvent event) {
if (event.getResult() == null) {
return;
}

final var r = event.getResult();
final var custom_item_r = get_module().item_registry().get(r);
if (custom_item_r == null) {
return;
}

if (custom_item_r.durability() <= 0) {
// Remove leftover components
damage_and_update_item(custom_item_r, r, 0);
} else {
final var uses_a = remaining_uses(event.getInventory().getFirstItem());
final var uses_b = remaining_uses(event.getInventory().getSecondItem());
static public int scale_damage(int old_damage, int old_max_damage, int new_max_damage) {
return old_max_damage == new_max_damage ? old_damage : (int)(new_max_damage * ((float) old_damage / (float) old_max_damage));
}

final var max = r.getItemMeta().getPersistentDataContainer().getOrDefault(ITEM_DURABILITY_MAX, PersistentDataType.INTEGER, custom_item_r.durability());
if (uses_a >= 0 && uses_b >= 0) {
set_damage_and_update_item(custom_item_r, r, max - (uses_a + uses_b));
static public boolean set_damage_and_max_damage(CustomItem custom_item, ItemStack item, int damage) {
return item.editMeta(Damageable.class, meta -> {
if(custom_item.durability() != 0) {
meta.setMaxDamage(custom_item.durability());
} else {
meta.setMaxDamage((int) item.getType().getMaxDurability());
}
}

event.setResult(r);
meta.setDamage(damage);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.inventory.meta.Damageable;
import org.jetbrains.annotations.NotNull;
import org.oddlama.vane.core.Core;
import org.oddlama.vane.core.Listener;
Expand Down Expand Up @@ -80,7 +80,7 @@ private void process_inventory(@NotNull Inventory inventory) {
}

contents[i] = convert_to_custom_item.convertExistingStack(is);
contents[i].editMeta(meta -> meta.displayName(convert_to_custom_item.displayName()));
contents[i].editMeta(meta -> meta.itemName(convert_to_custom_item.displayName()));
get_module().enchantment_manager.update_enchanted_item(contents[i]);
get_module().log.info("Converted legacy item to " + convert_to_custom_item.key());
++changed;
Expand Down Expand Up @@ -110,10 +110,14 @@ private void process_inventory(@NotNull Inventory inventory) {
}

// Update maximum durability on existing items if changed.
if (meta.getPersistentDataContainer().getOrDefault(DurabilityManager.ITEM_DURABILITY_MAX, PersistentDataType.INTEGER, 0) != custom_item.durability()) {
Damageable damageableMeta = (Damageable) contents[i].getItemMeta();
int max_damage = damageableMeta.hasMaxDamage() ? damageableMeta.getMaxDamage() : contents[i].getType().getMaxDurability();
int correct_max_damage = custom_item.durability() == 0 ? contents[i].getType().getMaxDurability() : custom_item.durability();
if (max_damage != correct_max_damage || meta.getPersistentDataContainer().has(DurabilityManager.ITEM_DURABILITY_DAMAGE)) {
get_module().log.info("Updated item durability " + custom_item.key());
DurabilityManager.initialize_or_update_max(custom_item, contents[i]);
DurabilityManager.update_damage(custom_item, contents[i]);
++changed;
continue;
}
}

Expand Down
24 changes: 13 additions & 11 deletions vane-core/src/main/java/org/oddlama/vane/util/ItemUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@
import java.util.UUID;
import java.util.stream.Collectors;

import com.destroystokyo.paper.profile.ProfileProperty;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;

import org.apache.commons.lang3.tuple.Pair;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.OfflinePlayer;
import org.bukkit.craftbukkit.enchantments.CraftEnchantment;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.SkullMeta;
Expand All @@ -32,6 +29,10 @@
import org.oddlama.vane.core.Core;
import org.oddlama.vane.core.material.ExtendedMaterial;

import com.destroystokyo.paper.profile.ProfileProperty;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
Expand All @@ -40,8 +41,6 @@
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.item.ItemParser;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.Item;
Expand All @@ -55,9 +54,11 @@ public class ItemUtil {
public static final UUID MODIFIER_UUID_GENERIC_ATTACK_SPEED = UUID.fromString(
"FA233E1C-4180-4865-B01B-BCCE9785ACA3");

private static RandomSource randomSource = RandomSource.create();

public static void damage_item(final Player player, final ItemStack item_stack, final int amount) {
if (player.getGameMode() == GameMode.CREATIVE) { // don't damage the tool if the player is in creative
return;
}

if (amount <= 0) {
return;
}
Expand All @@ -66,8 +67,9 @@ public static void damage_item(final Player player, final ItemStack item_stack,
if (handle == null) {
return;
}

handle.hurtAndBreak(amount, randomSource, player_handle(player), () -> {});
RandomSource random = Nms.world_handle(player.getWorld()).getRandom();

handle.hurtAndBreak(amount, random, player_handle(player), () -> { player.broadcastSlotBreak(EquipmentSlot.HAND); item_stack.subtract(); });
}

public static String name_of(final ItemStack item) {
Expand Down

0 comments on commit 8bbea3c

Please sign in to comment.