From 2bd0920394130f8b6c010c2ade599078b1f16844 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:51:39 +0000 Subject: [PATCH 1/2] fix: keep ArmourShop skins when a mage weapon is rebuilt A refresh or socket rewrite rebuilt the weapon from its MMOItems template and the part model scheme, which dropped any skin the player had applied and wasted the skin scroll. Players hit this after socketing a rune: the next hotbar switch refreshed the staff and reset its model. GearSkin puts the old weapon's skin back through TLibs' ItemSkinPreserver. A weapon whose amodel tag no longer matches its model already lost its skin, so it keeps the part model. Co-Authored-By: Claude Opus 5.5 --- pom.xml | 7 ++ .../magic/gear/GearItemBuilder.java | 2 +- .../tfminecraft/magic/gear/GearRefresher.java | 2 +- .../net/tfminecraft/magic/gear/GearSkin.java | 42 ++++++++++++ .../magic/GearItemCoverageTest.java | 2 + .../magic/GearRefresherCoverageTest.java | 4 ++ .../net/tfminecraft/magic/GearSkinTest.java | 66 +++++++++++++++++++ 7 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/tfminecraft/magic/gear/GearSkin.java create mode 100644 src/test/java/net/tfminecraft/magic/GearSkinTest.java diff --git a/pom.xml b/pom.xml index b0143e8..1441572 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,13 @@ + + + local + item-nbt-api-plugin + 2.16.1-tfmc-c571502e7686 + provided + local MMOItems diff --git a/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java b/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java index 7c23500..5675a4c 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java @@ -46,7 +46,7 @@ public static ItemStack rewriteSockets(ItemStack stack, int band) { List parts = GearProvenance.resolveParts(stack); List colours = SocketLayout.colours(archetype, parts, band); ItemStack rewritten = applyMmoData(stack, colours, parts); - rewritten = GearModelResolver.apply(rewritten, type, parts); + rewritten = GearSkin.keep(stack, GearModelResolver.apply(rewritten, type, parts)); copyGearPdc(stack, rewritten); GearProvenance.lockSockets(rewritten); WeaponRequirement.fromItem(stack).persist(rewritten); diff --git a/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java b/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java index 7ba3765..eaaca55 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java @@ -134,7 +134,7 @@ private static ItemStack rewrite(ItemStack stack, List colours, List i.getArgument(1)); var lore = mockStatic(WeaponLore.class)) { models .when(() -> GearModelResolver.apply(any(), any(), any())) .thenAnswer(i -> i.getArgument(0)); lore.when(() -> WeaponLore.updateItem(any())).thenAnswer(i -> i.getArgument(0)); assertSame(stack, GearItemBuilder.rewriteSockets(stack, 1)); + skin.verify(() -> GearSkin.keep(same(stack), same(stack))); assertTrue(GearProvenance.socketsLocked(stack)); assertEquals("core:1", GearProvenance.partsRaw(stack)); } diff --git a/src/test/java/net/tfminecraft/magic/GearRefresherCoverageTest.java b/src/test/java/net/tfminecraft/magic/GearRefresherCoverageTest.java index 314bb9a..f853f7e 100644 --- a/src/test/java/net/tfminecraft/magic/GearRefresherCoverageTest.java +++ b/src/test/java/net/tfminecraft/magic/GearRefresherCoverageTest.java @@ -101,6 +101,7 @@ void rebuildPreservesGearPdcAmountsAndExistingGems() { try (var nbt = mockStatic(NBTItem.class); var stats = mockStatic(GearStatApplicator.class); var model = mockStatic(GearModelResolver.class); + var skin = mockStatic(GearSkin.class, i -> i.getArgument(1)); var lore = mockStatic(WeaponLore.class); var items = mockConstruction( @@ -130,6 +131,7 @@ void rebuildPreservesGearPdcAmountsAndExistingGems() { var captured = org.mockito.ArgumentCaptor.forClass(GemSocketsData.class); verify(items.constructed().getFirst()).setData(eq(ItemStats.GEM_SOCKETS), captured.capture()); assertEquals(List.of(good), captured.getValue().getGems()); + skin.verify(() -> GearSkin.keep(same(item), same(built))); } } @@ -143,6 +145,7 @@ void outdatedRunesRebuildCurrentGearAndStampTheirRevisions() { try (var nbt = mockStatic(NBTItem.class); var stats = mockStatic(GearStatApplicator.class); var model = mockStatic(GearModelResolver.class); + var skin = mockStatic(GearSkin.class, i -> i.getArgument(1)); var lore = mockStatic(WeaponLore.class); var runes = mockStatic(RuneRefresher.class); var items = @@ -183,6 +186,7 @@ void incompatibleGemsAreHeldAndFailuresNeverReplaceOriginals() { try (var nbt = mockStatic(NBTItem.class); var stats = mockStatic(GearStatApplicator.class); var model = mockStatic(GearModelResolver.class); + var skin = mockStatic(GearSkin.class, i -> i.getArgument(1)); var lore = mockStatic(WeaponLore.class); var broken = mockStatic(GearBrokenMarker.class); var items = diff --git a/src/test/java/net/tfminecraft/magic/GearSkinTest.java b/src/test/java/net/tfminecraft/magic/GearSkinTest.java new file mode 100644 index 0000000..a5b2cf4 --- /dev/null +++ b/src/test/java/net/tfminecraft/magic/GearSkinTest.java @@ -0,0 +1,66 @@ +package net.tfminecraft.magic; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.lumine.mythic.lib.api.item.NBTItem; +import net.tfminecraft.magic.gear.GearSkin; +import net.tfminecraft.magic.util.LegacyModelData; +import net.tfminecraft.tlibs.objects.api.subapi.ItemSkinPreserver; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.*; +import org.mockito.MockedStatic; + +class GearSkinTest { + MockedStatic preserver; + MockedStatic nbt; + MockedStatic models; + + @BeforeEach + void mockStatics() { + preserver = mockStatic(ItemSkinPreserver.class); + nbt = mockStatic(NBTItem.class); + models = mockStatic(LegacyModelData.class); + } + + @AfterEach + void closeStatics() { + preserver.close(); + nbt.close(); + models.close(); + } + + /** A weapon with skin data, an {@code amodel} tag and an optional current model. */ + ItemStack weapon(boolean skinData, String amodel, Integer model, boolean hasMeta) { + var stack = mock(ItemStack.class); + var meta = hasMeta ? mock(ItemMeta.class) : null; + when(stack.getItemMeta()).thenReturn(meta); + preserver.when(() -> ItemSkinPreserver.hasSkinData(same(stack))).thenReturn(skinData); + var tags = mock(NBTItem.class); + when(tags.getString("amodel")).thenReturn(amodel); + nbt.when(() -> NBTItem.get(same(stack))).thenReturn(tags); + if (meta != null) { + models.when(() -> LegacyModelData.has(same(meta))).thenReturn(model != null); + if (model != null) models.when(() -> LegacyModelData.get(same(meta))).thenReturn(model); + } + return stack; + } + + @Test + void rebuildsKeepAWornSkinAndDropAStaleTag() { + var rebuilt = mock(ItemStack.class); + var dressed = mock(ItemStack.class); + preserver.when(() -> ItemSkinPreserver.apply(any(), same(rebuilt))).thenReturn(dressed); + + assertSame(dressed, GearSkin.keep(weapon(true, "66", 66, true), rebuilt)); + assertSame(dressed, GearSkin.keep(weapon(true, null, null, true), rebuilt)); + assertSame(dressed, GearSkin.keep(weapon(true, "", null, true), rebuilt)); + + assertSame(rebuilt, GearSkin.keep(weapon(true, "66", 2, true), rebuilt)); + assertSame(rebuilt, GearSkin.keep(weapon(true, "66", null, true), rebuilt)); + assertSame(rebuilt, GearSkin.keep(weapon(true, "66", null, false), rebuilt)); + assertSame(rebuilt, GearSkin.keep(weapon(false, "66", 66, true), rebuilt)); + assertNull(GearSkin.keep(weapon(true, "66", 66, true), null)); + } +} From b32774bdab98ef72675c436f8402d4f368cea00f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:55:50 +0000 Subject: [PATCH 2/2] build: install the pinned NBTAPI jar in CI Co-Authored-By: Claude Opus 5.5 --- .github/dependencies.sha256 | 1 + .github/scripts/install-local-dependencies.sh | 3 +++ .github/scripts/prepare-release.sh | 1 + 3 files changed, 5 insertions(+) diff --git a/.github/dependencies.sha256 b/.github/dependencies.sha256 index 1888689..e878548 100644 --- a/.github/dependencies.sha256 +++ b/.github/dependencies.sha256 @@ -4,3 +4,4 @@ a37f7789fcdcd11c9e5890a2fac742aa4501749588af23182991953e06ee1d17 libs/MMOItems- 225aa7f75d4e6e6ae6b3ad87a7ca678c025c027f00374ab1062275d24f3a73c9 libs/MythicLib-1.7.1-SNAPSHOT.jar 81d511d0830951f988615b6533cc93e82d00a83fb31e8ccbb36441ae3e8157ef libs/MMOCore-1.13.1-SNAPSHOT.jar 5a01b37bd7442fb0f2c809cadac82ba70f75173956e04efac3336595cc070368 libs/ItemsAdder-4.0.18.jar +c571502e768640c7cff10c49b3eef904ccf3fd95bd81803913abfe43cfc09746 libs/NBTAPI-2.16.1.jar diff --git a/.github/scripts/install-local-dependencies.sh b/.github/scripts/install-local-dependencies.sh index 8ee4873..23f32f8 100644 --- a/.github/scripts/install-local-dependencies.sh +++ b/.github/scripts/install-local-dependencies.sh @@ -22,3 +22,6 @@ mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1. mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \ -Dfile="libs/ItemsAdder-4.0.18.jar" -DgroupId="local" -DartifactId="ItemsAdder" \ -Dversion="4.0.18-tfmc-5a01b37bd744" -Dpackaging=jar -DgeneratePom=true "$@" +mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \ + -Dfile="libs/NBTAPI-2.16.1.jar" -DgroupId="local" -DartifactId="item-nbt-api-plugin" \ + -Dversion="2.16.1-tfmc-c571502e7686" -Dpackaging=jar -DgeneratePom=true "$@" diff --git a/.github/scripts/prepare-release.sh b/.github/scripts/prepare-release.sh index 1dd1106..89d025c 100644 --- a/.github/scripts/prepare-release.sh +++ b/.github/scripts/prepare-release.sh @@ -9,4 +9,5 @@ curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/225aa7f75d4e/MythicLib-1.7.1-SNAPSHOT.jar?ref=$ref" > "libs/MythicLib-1.7.1-SNAPSHOT.jar" curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/81d511d08309/MMOCore-1.13.1-SNAPSHOT.jar?ref=$ref" > "libs/MMOCore-1.13.1-SNAPSHOT.jar" curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/5a01b37bd744/ItemsAdder-4.0.18.jar?ref=$ref" > "libs/ItemsAdder-4.0.18.jar" +curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/c571502e7686/NBTAPI-2.16.1.jar?ref=$ref" > "libs/NBTAPI-2.16.1.jar" bash .github/scripts/install-local-dependencies.sh "$@"