diff --git a/src/main/java/net/tfminecraft/geminfusion/GemSocketRebuildListener.java b/src/main/java/net/tfminecraft/geminfusion/GemSocketRebuildListener.java index de9f268..40e39cd 100644 --- a/src/main/java/net/tfminecraft/geminfusion/GemSocketRebuildListener.java +++ b/src/main/java/net/tfminecraft/geminfusion/GemSocketRebuildListener.java @@ -75,15 +75,14 @@ private void handleGemUnsocket(MMOItemRebuildEvent event, ItemStack oldItem, Ite } SocketRarityStore.remove(fixed, removedUuid); - if (rarityId != null) { - Gemstone gem = findGemFromSocketed(oldItem, removedUuid); - if (gem != null) { - String capturedRarity = rarityId; - Gemstone capturedGem = gem; - var beforeInventory = UnsocketInventorySnapshot.poll(player); - Bukkit.getScheduler().runTask(InfusionMain.plugin, () -> UnsocketedGemRestorer.restore(player, - capturedRarity, capturedGem, beforeInventory)); - } + // Gems socketed before rarities were stored have none; restore them without one. + Gemstone gem = findGemFromSocketed(oldItem, removedUuid); + if (gem != null) { + String capturedRarity = rarityId; + Gemstone capturedGem = gem; + var beforeInventory = UnsocketInventorySnapshot.poll(player); + Bukkit.getScheduler().runTask(InfusionMain.plugin, () -> UnsocketedGemRestorer.restore(player, + capturedRarity, capturedGem, beforeInventory)); } } diff --git a/src/main/java/net/tfminecraft/geminfusion/InfusedGemBuilder.java b/src/main/java/net/tfminecraft/geminfusion/InfusedGemBuilder.java index 878e2ee..1adb19c 100644 --- a/src/main/java/net/tfminecraft/geminfusion/InfusedGemBuilder.java +++ b/src/main/java/net/tfminecraft/geminfusion/InfusedGemBuilder.java @@ -22,6 +22,7 @@ import net.Indyuce.mmoitems.stat.type.ItemStat; import net.Indyuce.mmoitems.stat.type.NameData; import net.Indyuce.mmoitems.stat.type.StatHistory; +import net.tfminecraft.geminfusion.goldsmith.InfusedGemValidator; public final class InfusedGemBuilder { private InfusedGemBuilder() { @@ -57,12 +58,13 @@ public static void rollStats(MMOItem mmo, Gemstone gem, GemRarity rarity, int in mmo.setData(ItemStats.SUCCESS_RATE, new DoubleData(Math.floor(Math.random() * maxChance) + 40)); } + /** A null rarity (unknown after a reset) leaves the rarity out of the name and lore. */ // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") public static void applyCosmetics(MMOItem mmo, Gemstone gem, GemRarity rarity) { mmo.setData(ItemStats.DISPLAYED_TYPE, new StringData("Infused Gemstone")); - String infusedName = rarity.getName() + " Infused " + gem.getName(); + String infusedName = (rarity == null ? "" : rarity.getName() + " ") + "Infused " + gem.getName(); StringData itemName = (StringData) mmo.getData(ItemStats.NAME); if (itemName == null) { itemName = new StringData(infusedName); @@ -81,17 +83,37 @@ public static void applyCosmetics(MMOItem mmo, Gemstone gem, GemRarity rarity) { List loreList = new ArrayList<>(); loreList.add(ChatColor.GRAY + "Gemstone Type: " + gem.getSocketNameColour() + gem.getSocketColour()); - loreList.add(ChatColor.GRAY + "Rarity: " + rarity.getName()); + if (rarity != null) { + loreList.add(ChatColor.GRAY + "Rarity: " + rarity.getName()); + } mmo.setData(ItemStats.LORE, new StringListData(loreList)); } public static ItemStack applyCosmeticsToItem(ItemStack item, Gemstone gem, GemRarity rarity) { - if (item == null || item.getType().isAir() || gem == null || rarity == null) { + if (item == null || item.getType().isAir() || gem == null) { return item; } LiveMMOItem mmo = new LiveMMOItem(NBTItem.get(item)); applyCosmetics(mmo, gem, rarity); - return finalizeItem(mmo.newBuilder().build(), rarity.getId()); + ItemStack rebuilt = finalizeItem(mmo.newBuilder().build(), rarity == null ? null : rarity.getId()); + if (rebuilt != null) { + rebuilt.setAmount(item.getAmount()); + } + return rebuilt; + } + + /** + * Gives a reset infused gem (see {@link InfusedGemValidator#isReset}) its infused look back, + * keeping its rolled stats. Its rarity is kept only when the item still records it. + */ + public static ItemStack restoreReset(ItemStack item) { + if (!InfusedGemValidator.isReset(item)) { + return item; + } + NBTItem nbt = NBTItem.get(item); + Gemstone gem = ConfigLoader.findGemByMmoItem(nbt.getType(), nbt.getString("MMOITEMS_ITEM_ID")); + GemRarity rarity = ConfigLoader.findRarityById(GemRarityPdc.read(item)); + return applyCosmeticsToItem(item, gem, rarity); } public static ItemStack buildInfusedGem(Gemstone gem, GemRarity rarity, int infusionAmount, Player player) { diff --git a/src/main/java/net/tfminecraft/geminfusion/InfusionEvents.java b/src/main/java/net/tfminecraft/geminfusion/InfusionEvents.java index 4e64dec..7af7df1 100644 --- a/src/main/java/net/tfminecraft/geminfusion/InfusionEvents.java +++ b/src/main/java/net/tfminecraft/geminfusion/InfusionEvents.java @@ -20,6 +20,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.EquipmentSlot; import net.tfminecraft.geminfusion.goldsmith.GoldsmithLog; +import net.tfminecraft.geminfusion.goldsmith.InfusedGemValidator; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.util.Vector; @@ -61,6 +62,14 @@ public void addGemEvent(PlayerInteractEvent e) { NBTItem nbt = NBTItem.get(item); if(nbt.hasType() == false) return; if(!nbt.getString("MMOITEMS_DISPLAYED_TYPE").equalsIgnoreCase("Blank Gemstone")) return; + if(InfusedGemValidator.isReset(item)) { + // Infusing again would wipe the roll; give the gem its infused look back instead. + ItemStack restored = InfusedGemBuilder.restoreReset(item); + if(restored == null) return; + p.getInventory().setItemInMainHand(restored); + p.sendMessage(ChatColor.YELLOW + "This gem already holds an infusion. Its glow returns."); + return; + } for(Gemstone gem : ConfigLoader.loadedGems) { if(nbt.getType().equalsIgnoreCase(gem.getMMOItemString().split("\\.")[0]) && nbt.getString("MMOITEMS_ITEM_ID").equalsIgnoreCase(gem.getMMOItem().getId())) { if(gem.isLocationSpecific() == true) { diff --git a/src/main/java/net/tfminecraft/geminfusion/UnsocketedGemRestorer.java b/src/main/java/net/tfminecraft/geminfusion/UnsocketedGemRestorer.java index 41cf594..8bbbc9e 100644 --- a/src/main/java/net/tfminecraft/geminfusion/UnsocketedGemRestorer.java +++ b/src/main/java/net/tfminecraft/geminfusion/UnsocketedGemRestorer.java @@ -9,20 +9,18 @@ import org.bukkit.inventory.PlayerInventory; import io.lumine.mythic.lib.api.item.NBTItem; -import net.Indyuce.mmoitems.ItemStats; +import net.tfminecraft.geminfusion.goldsmith.InfusedGemValidator; public final class UnsocketedGemRestorer { private UnsocketedGemRestorer() { } + /** A null or unknown rarityId restores the gem without a rarity. */ public static void restore(Player player, String rarityId, Gemstone gem, Map beforeInventory) { - if (player == null || rarityId == null || gem == null) { + if (player == null || gem == null) { return; } GemRarity rarity = ConfigLoader.findRarityById(rarityId); - if (rarity == null) { - return; - } Integer slot = findTargetSlot(player, gem, beforeInventory); if (slot == null) { @@ -30,8 +28,10 @@ public static void restore(Player player, String rarityId, Gemstone gem, Map beforeInventory) { @@ -86,13 +86,6 @@ private static boolean matchesGem(ItemStack item, String gemType, String gemId) } private static boolean needsRestore(ItemStack item) { - NBTItem nbt = NBTItem.get(item); - if (nbt.hasTag(ItemStats.DISPLAYED_TYPE.getNBTPath())) { - String displayed = nbt.getString(ItemStats.DISPLAYED_TYPE.getNBTPath()); - if (displayed != null && !displayed.isBlank() && !displayed.equalsIgnoreCase("Blank Gemstone")) { - return false; - } - } - return true; + return InfusedGemValidator.isReset(item); } } diff --git a/src/main/java/net/tfminecraft/geminfusion/goldsmith/InfusedGemValidator.java b/src/main/java/net/tfminecraft/geminfusion/goldsmith/InfusedGemValidator.java index 84057bf..bd17e30 100644 --- a/src/main/java/net/tfminecraft/geminfusion/goldsmith/InfusedGemValidator.java +++ b/src/main/java/net/tfminecraft/geminfusion/goldsmith/InfusedGemValidator.java @@ -3,7 +3,10 @@ import org.bukkit.inventory.ItemStack; import io.lumine.mythic.lib.api.item.NBTItem; +import net.Indyuce.mmoitems.MMOItems; +import net.Indyuce.mmoitems.stat.type.ItemStat; import net.tfminecraft.geminfusion.ConfigLoader; +import net.tfminecraft.geminfusion.GemStat; import net.tfminecraft.geminfusion.Gemstone; public final class InfusedGemValidator { @@ -18,9 +21,24 @@ public static boolean isInfused(ItemStack item) { if (item == null || item.getType().isAir()) return false; NBTItem nbt = NBTItem.get(item); if (!nbt.hasType()) return false; - if (!DISPLAY_INFUSED.equalsIgnoreCase(nbt.getString("MMOITEMS_DISPLAYED_TYPE"))) return false; Gemstone gem = ConfigLoader.findGemByMmoItem(nbt.getType(), nbt.getString("MMOITEMS_ITEM_ID")); - return gem != null; + if (gem == null) return false; + String displayed = nbt.getString("MMOITEMS_DISPLAYED_TYPE"); + if (DISPLAY_INFUSED.equalsIgnoreCase(displayed)) return true; + return DISPLAY_BLANK.equalsIgnoreCase(displayed) && hasInfusionStat(nbt, gem); + } + + /** + * An infused gem that an MMOItems rebuild (e.g. unsocketing) turned back into a + * "Blank Gemstone": it keeps the rolled infusion stat, which blank templates never have. + */ + public static boolean isReset(ItemStack item) { + if (item == null || item.getType().isAir()) return false; + NBTItem nbt = NBTItem.get(item); + if (!nbt.hasType()) return false; + if (!DISPLAY_BLANK.equalsIgnoreCase(nbt.getString("MMOITEMS_DISPLAYED_TYPE"))) return false; + Gemstone gem = ConfigLoader.findGemByMmoItem(nbt.getType(), nbt.getString("MMOITEMS_ITEM_ID")); + return gem != null && hasInfusionStat(nbt, gem); } public static boolean isGemstoneCandidate(ItemStack item) { @@ -33,4 +51,14 @@ public static boolean isGemstoneCandidate(ItemStack item) { } return ConfigLoader.findGemByMmoItem(nbt.getType(), nbt.getString("MMOITEMS_ITEM_ID")) != null; } + + private static boolean hasInfusionStat(NBTItem nbt, Gemstone gem) { + for (GemStat block : gem.getStats()) { + ItemStat stat = MMOItems.plugin.getStats().get(block.getStatId().toUpperCase()); + if (stat == null) continue; + String path = stat.getNBTPath(); + if (nbt.hasTag(path) && nbt.getDouble(path) != 0) return true; + } + return false; + } } diff --git a/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java b/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java index f52cb64..917ddf7 100644 --- a/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java +++ b/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java @@ -151,6 +151,13 @@ void cosmeticsUpdateNameHistoryLoreAndRarityWithoutChangingStats() { assertEquals("Rare Infused Ruby", original.getString()); verify(output).setStatHistory(ItemStats.NAME, history); verify(output, times(2)).setData(eq(ItemStats.LORE), any(StringListData.class)); + MMOItem unknown = mock(MMOItem.class); + InfusedGemBuilder.applyCosmetics(unknown, gem, null); + verify(unknown).replaceData(eq(ItemStats.NAME), name.capture()); + assertEquals("Infused Ruby", name.getValue().getString()); + var lore = ArgumentCaptor.forClass(StringListData.class); + verify(unknown).setData(eq(ItemStats.LORE), lore.capture()); + assertEquals(1, lore.getValue().getList().size()); } @Test @@ -166,13 +173,18 @@ void buildsAndRebuildsGemWithCosmeticsGlintAndPdc() { assertSame(result, InfusedGemBuilder.buildInfusedGem(template, rarity, 3, player)); assertEquals("rare", GemRarityPdc.read(result)); assertTrue(result.getItemMeta().hasItemFlag(ItemFlag.HIDE_ENCHANTS)); + blank.setAmount(2); assertSame(result, InfusedGemBuilder.applyCosmeticsToItem(blank, gem, rarity)); + assertEquals(2, result.getAmount()); + assertSame(result, InfusedGemBuilder.applyCosmeticsToItem(blank, gem, null)); + } + try (var constructed = live(m -> when(m.newBuilder().build()).thenReturn(null))) { + assertNull(InfusedGemBuilder.applyCosmeticsToItem(blank, gem, rarity)); } assertNull(InfusedGemBuilder.applyCosmeticsToItem(null, gem, rarity)); ItemStack air = new ItemStack(Material.AIR); assertSame(air, InfusedGemBuilder.applyCosmeticsToItem(air, gem, rarity)); assertSame(blank, InfusedGemBuilder.applyCosmeticsToItem(blank, null, rarity)); - assertSame(blank, InfusedGemBuilder.applyCosmeticsToItem(blank, gem, null)); assertNull(InfusedGemBuilder.finalizeItem(null, "rare")); assertSame(air, InfusedGemBuilder.finalizeItem(air, "rare")); } @@ -202,6 +214,64 @@ void infusedValidatorRecognizesOnlyConfiguredInfusedGemsAndCandidates() { assertTrue(InfusedGemValidator.isGemstoneCandidate(item)); } + @Test + void blankGemKeepingItsInfusionStatCountsAsInfusedAndReset() throws Exception { + ItemStack item = tagged(Material.DIAMOND); + String path = ItemStats.ATTACK_DAMAGE.getNBTPath(); + gem.addStat(stat("common", "MISSING", 1)); + gem.addStat(stat("rare", "ATTACK_DAMAGE", 2)); + when(MMOItems.plugin.getStats().get("MISSING")).thenReturn(null); + when(nbt.getString("MMOITEMS_DISPLAYED_TYPE")).thenReturn("Blank Gemstone"); + assertFalse(InfusedGemValidator.isInfused(item)); + assertFalse(InfusedGemValidator.isReset(item)); + when(nbt.hasTag(path)).thenReturn(true); + assertFalse(InfusedGemValidator.isReset(item)); + when(nbt.getDouble(path)).thenReturn(4.1); + assertTrue(InfusedGemValidator.isInfused(item)); + assertTrue(InfusedGemValidator.isReset(item)); + when(nbt.getString("MMOITEMS_DISPLAYED_TYPE")).thenReturn("Sword"); + assertFalse(InfusedGemValidator.isInfused(item)); + assertFalse(InfusedGemValidator.isReset(item)); + when(nbt.getString("MMOITEMS_DISPLAYED_TYPE")).thenReturn("Infused Gemstone"); + assertFalse(InfusedGemValidator.isReset(item)); + when(nbt.getString("MMOITEMS_DISPLAYED_TYPE")).thenReturn("Blank Gemstone"); + when(nbt.getString("MMOITEMS_ITEM_ID")).thenReturn("OTHER"); + assertFalse(InfusedGemValidator.isReset(item)); + when(nbt.hasType()).thenReturn(false); + assertFalse(InfusedGemValidator.isReset(item)); + assertFalse(InfusedGemValidator.isReset(null)); + assertFalse(InfusedGemValidator.isReset(new ItemStack(Material.AIR))); + } + + @Test + void restoreResetRebuildsOnlyResetGemsKeepingKnownRarityAndAmount() throws Exception { + ItemStack item = tagged(Material.DIAMOND), withRarity = tagged(Material.DIAMOND); + String path = ItemStats.ATTACK_DAMAGE.getNBTPath(); + gem.addStat(stat("rare", "ATTACK_DAMAGE", 2)); + when(nbt.getString("MMOITEMS_DISPLAYED_TYPE")).thenReturn("Blank Gemstone"); + assertSame(item, InfusedGemBuilder.restoreReset(item)); + when(nbt.hasTag(path)).thenReturn(true); + when(nbt.getDouble(path)).thenReturn(4.1); + ConfigLoader.loadedRarities.add(rarity); + item.setAmount(3); + try (var constructed = live(m -> when(m.newBuilder().build()).thenReturn(withRarity))) { + GemRarityPdc.write(item, "rare"); + assertSame(withRarity, InfusedGemBuilder.restoreReset(item)); + assertEquals("rare", GemRarityPdc.read(withRarity)); + assertEquals(3, withRarity.getAmount()); + verify(constructed.constructed().get(0)) + .replaceData(eq(ItemStats.NAME), argThat(d -> "Rare Infused Ruby".equals(((StringData) d).getString()))); + } + ItemStack unknown = tagged(Material.DIAMOND), withoutRarity = tagged(Material.DIAMOND); + try (var constructed = live(m -> when(m.newBuilder().build()).thenReturn(withoutRarity))) { + assertSame(withoutRarity, InfusedGemBuilder.restoreReset(unknown)); + assertNull(GemRarityPdc.read(withoutRarity)); + verify(constructed.constructed().get(0)) + .replaceData(eq(ItemStats.NAME), argThat(d -> "Infused Ruby".equals(((StringData) d).getString()))); + } + ConfigLoader.loadedRarities.clear(); + } + @Test void jewelryRejectsMissingInputsInvalidGemsAndUnreadableStats() { GoldsmithStation station = mock(GoldsmithStation.class); diff --git a/src/test/java/net/tfminecraft/geminfusion/InfusionEventsTest.java b/src/test/java/net/tfminecraft/geminfusion/InfusionEventsTest.java index 563fe06..694bb74 100644 --- a/src/test/java/net/tfminecraft/geminfusion/InfusionEventsTest.java +++ b/src/test/java/net/tfminecraft/geminfusion/InfusionEventsTest.java @@ -6,6 +6,7 @@ import io.lumine.mythic.lib.api.item.NBTItem; import java.util.*; import net.Indyuce.mmoitems.api.item.mmoitem.MMOItem; +import net.tfminecraft.geminfusion.goldsmith.InfusedGemValidator; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.entity.*; @@ -46,6 +47,7 @@ void setup() { when(block.getLocation()).thenReturn(location); when(block.getType()).thenReturn(Material.ENCHANTING_TABLE); when(hand.getAmount()).thenReturn(5); + when(hand.getType()).thenReturn(Material.AMETHYST_SHARD); when(nbt.hasType()).thenReturn(true); when(nbt.getType()).thenReturn("GEM_STONE"); when(nbt.getString("MMOITEMS_DISPLAYED_TYPE")).thenReturn("Blank Gemstone"); @@ -144,6 +146,32 @@ void configuredStationAndGemLocationsMustMatch() { location.clone().add(.5, 1, .5), events.currentStations.getFirst().getParticleLocation()); } + @Test + void resetInfusedGemIsRestoredInHandInsteadOfInfusedAgain() { + ItemStack restored = mock(ItemStack.class); + try (var valid = mockStatic(InfusedGemValidator.class); + var builder = mockStatic(InfusedGemBuilder.class)) { + valid.when(() -> InfusedGemValidator.isReset(hand)).thenReturn(true); + builder.when(() -> InfusedGemBuilder.restoreReset(hand)).thenReturn(restored); + PlayerInteractEvent click = event(Action.RIGHT_CLICK_BLOCK, EquipmentSlot.HAND); + events.addGemEvent(click); + assertTrue(click.isCancelled()); + } + verify(inventory).setItemInMainHand(restored); + verify(player).sendMessage(contains("already holds an infusion")); + assertTrue(events.currentStations.isEmpty()); + // A failed rebuild keeps the original gem and does not infuse it. + try (var valid = mockStatic(InfusedGemValidator.class); + var builder = mockStatic(InfusedGemBuilder.class)) { + valid.when(() -> InfusedGemValidator.isReset(hand)).thenReturn(true); + events.addGemEvent(event(Action.RIGHT_CLICK_BLOCK, EquipmentSlot.HAND)); + } + verify(inventory, times(1)).setItemInMainHand(any()); + verify(player, times(1)).sendMessage(contains("already holds an infusion")); + assertTrue(events.currentStations.isEmpty()); + verify(hand, never()).setAmount(anyInt()); + } + @Test void startedAndFullStationsRejectFurtherGems() { InfusionBlock s = station(location, 1, 1); diff --git a/src/test/java/net/tfminecraft/geminfusion/SocketIntegrationTest.java b/src/test/java/net/tfminecraft/geminfusion/SocketIntegrationTest.java index 7e8b62c..913d4ba 100644 --- a/src/test/java/net/tfminecraft/geminfusion/SocketIntegrationTest.java +++ b/src/test/java/net/tfminecraft/geminfusion/SocketIntegrationTest.java @@ -8,6 +8,7 @@ import net.Indyuce.mmoitems.ItemStats; import net.Indyuce.mmoitems.api.event.item.UnsocketGemStoneEvent; import net.Indyuce.mmoitems.stat.data.*; +import net.tfminecraft.geminfusion.goldsmith.InfusedGemValidator; import net.tfminecraft.tlibs.event.MMOItemRebuildEvent; import net.tfminecraft.tlibs.event.MMOItemRebuildEvent.RebuildReason; import net.tfminecraft.tlibs.socket.GemSocketsNbtEditor; @@ -221,8 +222,13 @@ void unsocketRemovesRarityAndSchedulesRestorationFromSnapshot() { sockets.when(() -> GemSocketsNbtEditor.getSockets(same(old))).thenReturn(data); when(data.getGems()).thenReturn(List.of(unrelated)); new GemSocketRebuildListener().onRebuild(event); + when(data.getGems()).thenReturn(List.of(unrelated, entry)); SocketRarityStore.write(old, Map.of()); new GemSocketRebuildListener().onRebuild(event); + verify(scheduler, times(2)).runTask(same(InfusionMain.plugin), task.capture()); + task.getValue().run(); + restorer.verify( + () -> UnsocketedGemRestorer.restore(eq(player), isNull(), same(gem), anyMap())); sockets.when(() -> GemSocketsNbtEditor.getGemstoneUuids(same(old))).thenReturn(Set.of()); new GemSocketRebuildListener().onRebuild(event); } @@ -248,8 +254,10 @@ void restorerTargetsNewOrIncreasedStackWithoutTouchingPreexistingCandidates() { when(nbt.getType()).thenReturn("GEM_STONE"); when(nbt.getString("MMOITEMS_ITEM_ID")).thenReturn("RUBY"); try (var api = mockStatic(NBTItem.class); - var builder = mockStatic(InfusedGemBuilder.class)) { + var builder = mockStatic(InfusedGemBuilder.class); + var valid = mockStatic(InfusedGemValidator.class)) { api.when(() -> NBTItem.get(any(ItemStack.class))).thenReturn(nbt); + valid.when(() -> InfusedGemValidator.isReset(any())).thenReturn(true); builder .when(() -> InfusedGemBuilder.applyCosmeticsToItem(any(), same(gem), same(rarity))) .thenReturn(fixed); @@ -273,11 +281,27 @@ void restorerTargetsNewOrIncreasedStackWithoutTouchingPreexistingCandidates() { verify(inventory, times(3)).setItem(1, fixed); UnsocketedGemRestorer.restore(actor, "rare", gem, Map.of(0, first, 1, second)); verify(inventory, times(3)).setItem(1, fixed); - for (String display : Arrays.asList(null, " ", "Blank Gemstone", "Infused Gemstone")) { - when(nbt.hasTag(ItemStats.DISPLAYED_TYPE.getNBTPath())).thenReturn(true); - when(nbt.getString(ItemStats.DISPLAYED_TYPE.getNBTPath())).thenReturn(display); - UnsocketedGemRestorer.restore(actor, "rare", gem, Map.of()); - } + // A genuinely blank gem (no infusion stat) is never turned into an infused one. + valid.when(() -> InfusedGemValidator.isReset(any())).thenReturn(false); + UnsocketedGemRestorer.restore(actor, "rare", gem, Map.of()); + verify(inventory, times(3)).setItem(1, fixed); + valid.when(() -> InfusedGemValidator.isReset(any())).thenReturn(true); + // Without a stored rarity the gem is still restored, just without one. + ItemStack noRarity = tagged(Material.GOLD_INGOT); + builder + .when(() -> InfusedGemBuilder.applyCosmeticsToItem(any(), same(gem), isNull())) + .thenReturn(noRarity); + when(inventory.getItem(1)).thenReturn(null); + UnsocketedGemRestorer.restore(actor, null, gem, Map.of()); + UnsocketedGemRestorer.restore(actor, "missing", gem, Map.of()); + verify(inventory, times(2)).setItem(0, noRarity); + // A failed rebuild leaves the gem in place. + builder + .when(() -> InfusedGemBuilder.applyCosmeticsToItem(any(), same(gem), isNull())) + .thenReturn(null); + UnsocketedGemRestorer.restore(actor, null, gem, Map.of()); + verify(inventory, never()).setItem(eq(0), isNull()); + when(inventory.getItem(1)).thenReturn(second); when(inventory.getItem(0)).thenReturn(new ItemStack(Material.AIR)); when(inventory.getItem(1)).thenReturn(null); UnsocketedGemRestorer.restore(actor, "rare", gem, Map.of());