Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/net/tfminecraft/tlibs/TLibs.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import net.tfminecraft.tlibs.armour.ArmorEquipEvent;
import net.tfminecraft.tlibs.armour.CosmeticHelmetListener;
import net.tfminecraft.tlibs.armour.FalseBreakRepair;
import net.tfminecraft.tlibs.armour.equipment.EquipmentAssetListener;
import net.tfminecraft.tlibs.armour.equipment.EquipmentAssets;
import net.tfminecraft.tlibs.armour.equipment.ItemsAdderEquipmentListener;
Expand Down Expand Up @@ -43,6 +44,7 @@ public void onEnable() {
reloadPluginConfig();
initializeAPIs();
ItemScanService.start(this);
ItemScanService.get().subscribe(new FalseBreakRepair());
GemApplyPrimer.init(this);
ArmorEquipEvent.registerListener(this);
Bukkit.getPluginManager().registerEvents(new CosmeticHelmetListener(), this);
Expand Down
78 changes: 78 additions & 0 deletions src/main/java/net/tfminecraft/tlibs/armour/CosmeticHelmetFix.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static void afterAppearanceChange(ItemStack item, Material previousType,
Integer previousMaxDamage, List<SavedTag> tags) {
MmoItemTagPreserver.restoreMissing(item, tags);
if (!shouldMaintain(item)) {
rescaleVanillaDamage(item, previousType, tags);
return;
}
int componentMax = previousMaxDamage == null ? 0 : previousMaxDamage;
Expand Down Expand Up @@ -68,6 +69,83 @@ public static void syncVanillaDamage(ItemStack item) {
MmoItemTagPreserver.restoreMissing(item, tags);
}

/**
* A skin that changes the material keeps the old vanilla damage, but the bar it counts against is the new
* material's. A crafted piece leaving a hat look (bar sized to MMOItems, e.g. 187 of 1003) for leather (55)
* then reads as broken. Drops the bar a hat look added, then maps the MMOItems durability onto the new bar the
* way MMOItems does, so pieces without MMOItems durability (mage weapons wear the vanilla bar) are left alone.
*/
static boolean rescaleVanillaDamage(ItemStack item, Material previousType, List<SavedTag> tags) {
if (item == null || item.getType().isAir()) {
return false;
}
int customMax = MmoItemTagPreserver.customMaxDurability(item);
if (customMax <= 0 || !(item.getItemMeta() instanceof Damageable damageable)) {
return false;
}
boolean changed = false;
if (SkinnedArmorDurability.isCosmeticHelmetMaterial(previousType) && damageable.hasMaxDamage()
&& damageable.getMaxDamage() == customMax) {
damageable.setMaxDamage(null);
changed = true;
}
changed |= alignDamage(item, damageable, 1);
if (!changed) {
return false;
}
item.setItemMeta(damageable);
MmoItemTagPreserver.restoreMissing(item, tags);
return true;
}

/** An MMOItems piece whose vanilla bar reads broken (at or past its end) while MMOItems durability is left. */
public static boolean showsFalseBreak(ItemStack item) {
if (item == null || item.getType().isAir()) {
return false;
}
int customMax = MmoItemTagPreserver.customMaxDurability(item);
if (customMax <= 0 || currentDurability(item, customMax) <= 0
|| !(item.getItemMeta() instanceof Damageable damageable)) {
return false;
}
int max = vanillaMax(item, damageable);
return max > 0 && damageable.getDamage() >= max;
}

/** Puts the vanilla bar of a {@link #showsFalseBreak} piece back in line with its MMOItems durability. */
public static boolean repairFalseBreak(ItemStack item) {
if (!showsFalseBreak(item)) {
return false;
}
List<SavedTag> tags = MmoItemTagPreserver.snapshot(item);
Damageable damageable = (Damageable) item.getItemMeta();
alignDamage(item, damageable, 0);
item.setItemMeta(damageable);
MmoItemTagPreserver.restoreMissing(item, tags);
return true;
}

private static boolean alignDamage(ItemStack item, Damageable damageable, int tolerance) {
int max = vanillaMax(item, damageable);
if (max <= 0) {
return false;
}
int expected = Math.min(expectedDamage(item, max, 0), max - 1);
if (Math.abs(damageable.getDamage() - expected) <= tolerance) {
return false;
}
damageable.setDamage(expected);
return true;
}

private static int vanillaMax(ItemStack item, Damageable damageable) {
return damageable.hasMaxDamage() ? damageable.getMaxDamage() : item.getType().getMaxDurability();
}

private static int currentDurability(ItemStack item, int customMax) {
return MmoItemTagPreserver.hasDurabilityValue(item) ? MmoItemTagPreserver.customDurability(item) : customMax;
}

private static boolean applyProfile(ItemStack item, int componentMax, int materialMax, int fallbackDamage,
List<SavedTag> tags) {
int customMax = MmoItemTagPreserver.customMaxDurability(item);
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/net/tfminecraft/tlibs/armour/FalseBreakRepair.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.tfminecraft.tlibs.armour;

import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;

import net.tfminecraft.tlibs.itemscan.ItemScanHandler;

/**
* Repairs the vanilla bar of crafted pieces that a skin left reading broken while their MMOItems durability is
* fine, such as a helmet moved from a hat look (bar of 1003) to a leather armour skin (bar of 55).
*/
public final class FalseBreakRepair implements ItemScanHandler {

@Override
public boolean matches(ItemStack stack) {
return CosmeticHelmetFix.showsFalseBreak(stack);
}

@Override
public void update(Player player, Inventory inventory, int slot, ItemStack stack) {
if (CosmeticHelmetFix.repairFalseBreak(stack) && inventory != null) {
inventory.setItem(slot, stack);
}
}
}
132 changes: 132 additions & 0 deletions src/test/java/net/tfminecraft/tlibs/armour/CosmeticHelmetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,138 @@ void itemDamageSynchronizesEveryArmorPiece() {
assertEquals(50, meta(helmet).getDamage());
}

@Test
void leavingAHatLookForLeatherDropsTheHatBarAndRescalesTheDamage() {
// Koda's Stygmetal helmet: 816/1003 under a hat look (bar 1003, damage 187), then a leather skin.
ItemStack item = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(item).setMaxDamage(1003);
meta(item).setDamage(187);
CosmeticHelmetFix.afterAppearanceChange(item, Material.CARVED_PUMPKIN, 187, 1003, List.of());
assertFalse(meta(item).hasMaxDamage());
assertEquals(10, meta(item).getDamage(), "816/1003 is 81% left, so 10 damage on leather's 55");
clearInvocations(item);
CosmeticHelmetFix.afterAppearanceChange(item, Material.LEATHER_HELMET, 10, null, List.of());
verify(item, never()).setItemMeta(any());
}

@Test
void reskinningAPieceThatAlreadyReadsBrokenRescalesIt() {
ItemStack item = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(item).setDamage(185);
CosmeticHelmetFix.afterAppearanceChange(item, Material.LEATHER_HELMET, 185, null, List.of());
assertEquals(10, meta(item).getDamage());
ItemStack full = crafted(Material.LEATHER_HELMET, 1000, 1000);
meta(full).setDamage(30);
CosmeticHelmetFix.afterAppearanceChange(full, Material.CARVED_PUMPKIN, 30, null, List.of());
assertEquals(0, meta(full).getDamage());
assertEquals(1000, pieces.get(full).tags.get("MMOITEMS_DURABILITY"), "MMOItems durability is untouched");
}

@Test
void rescaleKeepsBarsTheHatLookDidNotAddAndLeavesOtherItemsAlone() {
ItemStack ownBar = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(ownBar).setMaxDamage(400);
meta(ownBar).setDamage(300);
CosmeticHelmetFix.afterAppearanceChange(ownBar, Material.CARVED_PUMPKIN, 300, 400, List.of());
assertEquals(400, meta(ownBar).getMaxDamage());
assertEquals(74, meta(ownBar).getDamage());
ItemStack fromHelmet = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(fromHelmet).setMaxDamage(1003);
meta(fromHelmet).setDamage(187);
CosmeticHelmetFix.afterAppearanceChange(fromHelmet, Material.DIAMOND_HELMET, 187, 1003, List.of());
assertEquals(1003, meta(fromHelmet).getMaxDamage(), "only a hat look's bar is dropped");
ItemStack untagged = item(Material.LEATHER_HELMET);
meta(untagged).setDamage(40);
CosmeticHelmetFix.afterAppearanceChange(untagged, Material.CARVED_PUMPKIN, 40, null, List.of());
assertEquals(40, meta(untagged).getDamage());
ItemStack undamageable = crafted(Material.PAPER, 1003, 816);
CosmeticHelmetFix.afterAppearanceChange(undamageable, Material.LEATHER_HELMET, 0, null, List.of());
verify(undamageable, never()).setItemMeta(any());
ItemStack plainMeta = crafted(Material.LEATHER_HELMET, 1003, 816);
ItemMeta ordinaryMeta = mock(ItemMeta.class);
when(ordinaryMeta.clone()).thenReturn(ordinaryMeta);
pieces.get(plainMeta).meta = ordinaryMeta;
CosmeticHelmetFix.afterAppearanceChange(plainMeta, Material.CARVED_PUMPKIN, 0, null, List.of());
verify(plainMeta, never()).setItemMeta(any());
assertFalse(CosmeticHelmetFix.rescaleVanillaDamage(null, Material.CARVED_PUMPKIN, List.of()));
assertFalse(CosmeticHelmetFix.rescaleVanillaDamage(item(Material.AIR), Material.CARVED_PUMPKIN, List.of()));
}

@Test
void falseBreaksAreFoundOnlyWhileMmoItemsDurabilityIsLeft() {
ItemStack broken = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(broken).setDamage(185);
assertTrue(CosmeticHelmetFix.showsFalseBreak(broken));
meta(broken).setDamage(54);
assertFalse(CosmeticHelmetFix.showsFalseBreak(broken));
ItemStack reallyBroken = crafted(Material.LEATHER_HELMET, 1003, 0);
meta(reallyBroken).setDamage(55);
assertFalse(CosmeticHelmetFix.showsFalseBreak(reallyBroken));
ItemStack noValue = item(Material.LEATHER_HELMET);
pieces.get(noValue).tags.put("MMOITEMS_MAX_DURABILITY", 1003);
meta(noValue).setDamage(60);
assertTrue(CosmeticHelmetFix.showsFalseBreak(noValue), "no durability tag means a full bar");
ItemStack untagged = item(Material.LEATHER_HELMET);
meta(untagged).setDamage(60);
assertFalse(CosmeticHelmetFix.showsFalseBreak(untagged));
ItemStack paper = crafted(Material.PAPER, 1003, 816);
meta(paper).setDamage(60);
assertFalse(CosmeticHelmetFix.showsFalseBreak(paper));
ItemStack plainMeta = crafted(Material.LEATHER_HELMET, 1003, 816);
ItemMeta ordinaryMeta = mock(ItemMeta.class);
when(ordinaryMeta.clone()).thenReturn(ordinaryMeta);
pieces.get(plainMeta).meta = ordinaryMeta;
assertFalse(CosmeticHelmetFix.showsFalseBreak(plainMeta));
assertFalse(CosmeticHelmetFix.showsFalseBreak(null));
assertFalse(CosmeticHelmetFix.showsFalseBreak(item(Material.AIR)));
}

@Test
void repairPutsTheBarBackEvenOnePointPastItsEnd() {
ItemStack broken = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(broken).setDamage(185);
assertTrue(CosmeticHelmetFix.repairFalseBreak(broken));
assertEquals(10, meta(broken).getDamage());
assertFalse(CosmeticHelmetFix.repairFalseBreak(broken));
// 1/100 left maps to 54 of 55: one point from 55, which the reskin tolerance would leave broken.
ItemStack edge = crafted(Material.LEATHER_HELMET, 100, 1);
meta(edge).setDamage(55);
assertTrue(CosmeticHelmetFix.repairFalseBreak(edge));
assertEquals(54, meta(edge).getDamage());
ItemStack hat = crafted(Material.CARVED_PUMPKIN, 100, 50);
meta(hat).setMaxDamage(100);
meta(hat).setDamage(100);
assertTrue(CosmeticHelmetFix.repairFalseBreak(hat));
assertEquals(50, meta(hat).getDamage());
assertEquals(100, meta(hat).getMaxDamage());
}

@Test
void scanHandlerRepairsFalseBreaksInInventoriesAndPickups() {
FalseBreakRepair repair = new FalseBreakRepair();
ItemStack broken = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(broken).setDamage(185);
assertTrue(repair.matches(broken));
org.bukkit.inventory.Inventory inventory = mock(org.bukkit.inventory.Inventory.class);
repair.update(null, inventory, 20, broken);
assertEquals(10, meta(broken).getDamage());
verify(inventory).setItem(20, broken);
assertFalse(repair.matches(broken));
repair.update(null, inventory, 20, broken);
verify(inventory, times(1)).setItem(anyInt(), any());
ItemStack picked = crafted(Material.LEATHER_HELMET, 1003, 816);
meta(picked).setDamage(185);
repair.update(null, null, -1, picked);
assertEquals(10, meta(picked).getDamage());
}

private ItemStack crafted(Material material, int max, int current) {
ItemStack item = item(material);
pieces.get(item).tags.putAll(Map.of("MMOITEMS_ITEM_TYPE", "HELMETS", "MMOITEMS_MAX_DURABILITY", max,
"MMOITEMS_DURABILITY", current));
return item;
}

private ItemStack protectedHelmet() {
ItemStack item = item(Material.CARVED_PUMPKIN);
pieces.get(item).tags.put("MMOITEMS_MAX_DURABILITY", 100);
Expand Down