From 42fe0dfc4d4a727aa5353bda979857d37b10fd30 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:50:48 +0100 Subject: [PATCH] perf: refresh pickup lore once per player per tick Every item a player picked up queued its own next-tick refresh of the whole inventory's aura and charge lore, and each refresh re-reads the lore of every item. Picking up a pile queued one full refresh per item entity. The listener now queues one refresh per player per tick: further pickups before it runs ride on it, since it reads the whole inventory then. That is the same result as several refreshes, because ArtifactLore.apply and ChargeLore.apply strip their own block and rebuild it from the item's data. The queued mark is cleared by its own task, also when SessionGuard skips the refresh (player gone or character switched), so the next pickup always queues again. Coin pickups never reach this (DenarEconomy cancels them). Co-Authored-By: Claude Opus 5.5 --- .../magic/listener/ArtifactListener.java | 22 +++++++- .../magic/ArtifactListenerTest.java | 52 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) 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();