diff --git a/src/main/java/net/tfminecraft/magic/listener/ArtifactListener.java b/src/main/java/net/tfminecraft/magic/listener/ArtifactListener.java index 198ac32..7961e59 100644 --- a/src/main/java/net/tfminecraft/magic/listener/ArtifactListener.java +++ b/src/main/java/net/tfminecraft/magic/listener/ArtifactListener.java @@ -1,5 +1,9 @@ package net.tfminecraft.magic.listener; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -184,12 +188,28 @@ public void onChunkLoad(ChunkLoadEvent event) { ArtifactDisplayIndex.ensureChunk(event.getChunk()); } + /** Players with a lore refresh already queued for the next tick. */ + private final Set pendingLore = new HashSet<>(); + + /** + * Refreshes the inventory's aura and charge lore on the next tick, once per player however + * many items they picked up this tick. One refresh is the same as several: ArtifactLore.apply + * and ChargeLore.apply strip their own lore block and rebuild it from the item's data, so a + * second run in the same tick gives the same item. The queued mark is cleared by its own task + * even when SessionGuard skips the refresh (player gone or character switched), so the next + * pickup always queues again. + */ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPickup(EntityPickupItemEvent event) { - if (!(event.getEntity() instanceof Player player)) { + if (!(event.getEntity() instanceof Player player) || Magic.plugin == null) { + return; + } + UUID id = player.getUniqueId(); + if (!pendingLore.add(id)) { return; } SessionGuard.runLater(player, () -> VesselLore.updateInventory(player)); + Magic.plugin.getServer().getScheduler().runTask(Magic.plugin, () -> pendingLore.remove(id)); } @EventHandler diff --git a/src/test/java/net/tfminecraft/magic/ArtifactListenerTest.java b/src/test/java/net/tfminecraft/magic/ArtifactListenerTest.java index 541f632..902fdc1 100644 --- a/src/test/java/net/tfminecraft/magic/ArtifactListenerTest.java +++ b/src/test/java/net/tfminecraft/magic/ArtifactListenerTest.java @@ -348,6 +348,58 @@ void pickupLoreUpdateRunsForAPlayerWithoutACharacter() { } } + @Test + void manyPickupsInOneTickRefreshTheLoreOnceAndALaterPickupRefreshesAgain() { + var listener = new ArtifactListener(); + var player = server.addPlayer(); + var other = server.addPlayer(); + try (var lore = mockStatic(VesselLore.class)) { + for (int i = 0; i < 5; i++) { + listener.onPickup(new EntityPickupItemEvent(player, mock(Item.class), 0)); + } + listener.onPickup(new EntityPickupItemEvent(other, mock(Item.class), 0)); + lore.verifyNoInteractions(); + server.getScheduler().performOneTick(); + lore.verify(() -> VesselLore.updateInventory(player)); + lore.verify(() -> VesselLore.updateInventory(other)); + listener.onPickup(new EntityPickupItemEvent(player, mock(Item.class), 0)); + server.getScheduler().performOneTick(); + lore.verify(() -> VesselLore.updateInventory(player), times(2)); + lore.verify(() -> VesselLore.updateInventory(other)); + } + } + + @Test + void aSkippedRefreshStillLetsTheNextPickupQueueOne() { + var listener = new ArtifactListener(); + var player = server.addPlayer(); + var provider = new CharacterTestSupport(); + provider.start(player, "Alice"); + try (var lore = mockStatic(VesselLore.class)) { + listener.onPickup(new EntityPickupItemEvent(player, mock(Item.class), 0)); + provider.start(player, "Bob"); + server.getScheduler().performOneTick(); + lore.verifyNoInteractions(); + listener.onPickup(new EntityPickupItemEvent(player, mock(Item.class), 0)); + server.getScheduler().performOneTick(); + lore.verify(() -> VesselLore.updateInventory(player)); + } finally { + provider.unregister(); + } + } + + @Test + void pickupsQueueNothingWhileThePluginIsNotRunning() { + var listener = new ArtifactListener(); + var player = server.addPlayer(); + try (var lore = mockStatic(VesselLore.class)) { + Magic.plugin = null; + listener.onPickup(new EntityPickupItemEvent(player, mock(Item.class), 0)); + server.getScheduler().performOneTick(); + lore.verifyNoInteractions(); + } + } + @Test void deferredShrineWorkKeepsTheWorldItemButDropsAnOfflineOrStaleActor() { var listener = new ArtifactListener();