@@ -76,10 +76,13 @@ public final class DroppedItemDisplay extends VisualizerRunnableDisplay implemen
7676 public static final EntryKey KEY = new EntryKey ("item" );
7777 private static final int DEFAULT_VIEW_DISTANCE = 64 ;
7878 private static final int VIEW_DISTANCE_HYSTERESIS = 16 ;
79+ private static final int DEFAULT_VISIBILITY_BUCKET_SIZE = 128 ;
80+ private static final int DEFAULT_VISIBILITY_REFILL = 32 ;
7981
8082 private final Map <UUID , Item > trackedItems = new HashMap <>();
8183 private final Map <UUID , TextDisplay > labels = new HashMap <>();
8284 private final Set <UUID > eligibleViewers = new HashSet <>();
85+ private final Map <UUID , VisibilityState > visibilityStates = new HashMap <>();
8386
8487 private String regularFormatting ;
8588 private String singularFormatting ;
@@ -93,6 +96,8 @@ public final class DroppedItemDisplay extends VisualizerRunnableDisplay implemen
9396 private int ticksUntilUpdate ;
9497 private int despawnTicks = 6000 ;
9598 private int viewDistance = DEFAULT_VIEW_DISTANCE ;
99+ private int visibilityBucketSize = DEFAULT_VISIBILITY_BUCKET_SIZE ;
100+ private int visibilityRefill = DEFAULT_VISIBILITY_REFILL ;
96101 private boolean stripColorBlacklist ;
97102 private DroppedItemBlacklist blacklist = DroppedItemBlacklist .compile (List .of (), DroppedItemDisplay ::warn );
98103
@@ -118,6 +123,16 @@ public void onReload(InteractionVisualizerReloadEvent event) {
118123 viewDistance = configuredViewDistance > 0
119124 ? Math .max (8 , Math .min (512 , configuredViewDistance ))
120125 : DEFAULT_VIEW_DISTANCE ;
126+ int configuredBucketSize = InteractionVisualizer .plugin .getConfiguration ()
127+ .getInt ("Entities.Item.Options.VisibilityRateLimit.BucketSize" );
128+ visibilityBucketSize = configuredBucketSize > 0
129+ ? configuredBucketSize
130+ : DEFAULT_VISIBILITY_BUCKET_SIZE ;
131+ int configuredRefill = InteractionVisualizer .plugin .getConfiguration ()
132+ .getInt ("Entities.Item.Options.VisibilityRateLimit.RestorePerTick" );
133+ visibilityRefill = configuredRefill > 0
134+ ? configuredRefill
135+ : DEFAULT_VISIBILITY_REFILL ;
121136 int configuredDespawnTicks = InteractionVisualizer .plugin .getConfiguration ().getInt ("Entities.Item.Options.DespawnTicks" );
122137 despawnTicks = configuredDespawnTicks > 0 ? configuredDespawnTicks : 6000 ;
123138 stripColorBlacklist = InteractionVisualizer .plugin .getConfiguration ()
@@ -158,6 +173,7 @@ public ScheduledTask run() {
158173 return new ScheduledRunnable () {
159174 @ Override
160175 public void run () {
176+ drainVisibilityQueues ();
161177 if (--ticksUntilUpdate <= 0 ) {
162178 ticksUntilUpdate = updateRate ;
163179 tickAll ();
@@ -201,7 +217,10 @@ public void onEntityRemove(EntityRemoveEvent event) {
201217 if (label != null ) {
202218 // EntityRemoveEvent is monitoring-only. Defer entity mutation
203219 // until Paper has finished removing the item's passengers.
204- Scheduler .runTask (InteractionVisualizer .plugin , () -> removeLabel (label ));
220+ Scheduler .runTask (InteractionVisualizer .plugin , () -> {
221+ forgetLabelVisibility (itemId , label );
222+ removeLabel (label );
223+ });
205224 }
206225 }
207226 }
@@ -247,6 +266,7 @@ private void tickAll() {
247266 for (TrackedItem tracked : validItems ) {
248267 update (tracked , itemIndex , viewerIndex );
249268 }
269+ reconcileLabelVisibility (viewers , validItems );
250270 }
251271
252272 private void update (TrackedItem tracked , DroppedItemSpatialIndex itemIndex ,
@@ -281,12 +301,10 @@ private void update(TrackedItem tracked, DroppedItemSpatialIndex itemIndex,
281301 }
282302
283303 Component text = format (stack , ticksLeft );
284- boolean created = false ;
285304 if (label == null || !label .isValid () || !label .getWorld ().equals (item .getWorld ())) {
286305 removeLabel (item .getUniqueId ());
287306 label = spawnLabel (item );
288307 labels .put (item .getUniqueId (), label );
289- created = true ;
290308 }
291309 if (!text .equals (label .text ())) {
292310 label .text (text );
@@ -316,11 +334,6 @@ private void update(TrackedItem tracked, DroppedItemSpatialIndex itemIndex,
316334 }
317335 label .teleport (labelLocation (item ));
318336 }
319- if (created ) {
320- // Mount and configure the final render height before revealing the
321- // label so the first tracking bundle cannot flash at item height.
322- showToEligibleViewers (label );
323- }
324337 }
325338
326339 private TextDisplay spawnLabel (Item item ) {
@@ -381,36 +394,100 @@ private Collection<Player> reconcileEligibleViewers() {
381394 for (UUID uuid : new HashSet <>(eligibleViewers )) {
382395 if (!desired .containsKey (uuid )) {
383396 Player player = Bukkit .getPlayer (uuid );
384- if (player != null ) {
385- for (TextDisplay label : labels .values ()) {
386- if (label .isValid ()) {
387- player .hideEntity (InteractionVisualizer .plugin , label );
388- }
389- }
390- }
397+ removeVisibilityState (uuid , player );
391398 eligibleViewers .remove (uuid );
392399 }
393400 }
394401 for (Map .Entry <UUID , Player > entry : desired .entrySet ()) {
395- if (eligibleViewers .add (entry .getKey ())) {
396- Player player = entry .getValue ();
397- for (TextDisplay label : labels .values ()) {
398- if (label .isValid ()) {
399- player .showEntity (InteractionVisualizer .plugin , label );
402+ eligibleViewers .add (entry .getKey ());
403+ }
404+ return desired .values ();
405+ }
406+
407+ private void reconcileLabelVisibility (Collection <Player > viewers , List <TrackedItem > validItems ) {
408+ for (Player player : viewers ) {
409+ UUID playerId = player .getUniqueId ();
410+ VisibilityState state = visibilityStates .computeIfAbsent (playerId ,
411+ ignored -> new VisibilityState (visibilityBucketSize ));
412+ Set <UUID > desired = new HashSet <>();
413+ Location playerLocation = player .getLocation ();
414+ int trackingDistance = InteractionVisualizer .playerTrackingRange
415+ .getOrDefault (player .getWorld (), DEFAULT_VIEW_DISTANCE );
416+ double range = Math .min (viewDistance , trackingDistance );
417+ double rangeSquared = range * range ;
418+
419+ for (TrackedItem tracked : validItems ) {
420+ TextDisplay label = labels .get (tracked .itemId ());
421+ if (label == null || !label .isValid () || !tracked .item ().getWorld ().equals (player .getWorld ())) {
422+ continue ;
423+ }
424+ Location location = tracked .location ();
425+ double deltaX = location .getX () - playerLocation .getX ();
426+ double deltaY = location .getY () - playerLocation .getY ();
427+ double deltaZ = location .getZ () - playerLocation .getZ ();
428+ if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= rangeSquared ) {
429+ desired .add (tracked .itemId ());
430+ }
431+ }
432+
433+ for (UUID itemId : new HashSet <>(state .shown )) {
434+ if (!desired .contains (itemId )) {
435+ TextDisplay label = labels .get (itemId );
436+ if (label != null && label .isValid ()) {
437+ player .hideEntity (InteractionVisualizer .plugin , label );
400438 }
439+ state .shown .remove (itemId );
440+ }
441+ }
442+ for (UUID itemId : state .desired ) {
443+ if (!desired .contains (itemId )) {
444+ state .pending .cancel (itemId );
445+ }
446+ }
447+ state .desired = desired ;
448+ for (UUID itemId : desired ) {
449+ if (!state .shown .contains (itemId )) {
450+ state .pending .request (itemId );
451+ }
452+ }
453+ }
454+ }
455+
456+ private void drainVisibilityQueues () {
457+ for (Map .Entry <UUID , VisibilityState > entry : visibilityStates .entrySet ()) {
458+ Player player = Bukkit .getPlayer (entry .getKey ());
459+ VisibilityState state = entry .getValue ();
460+ if (player == null || !player .isOnline () || !eligibleViewers .contains (entry .getKey ())) {
461+ continue ;
462+ }
463+ for (UUID itemId : state .pending .drain (visibilityBucketSize , visibilityRefill , id -> {
464+ TextDisplay label = labels .get (id );
465+ return state .desired .contains (id ) && !state .shown .contains (id )
466+ && label != null && label .isValid ();
467+ })) {
468+ TextDisplay label = labels .get (itemId );
469+ if (label != null && label .isValid ()) {
470+ player .showEntity (InteractionVisualizer .plugin , label );
471+ state .shown .add (itemId );
401472 }
402473 }
403474 }
404- return desired .values ();
405475 }
406476
407- private void showToEligibleViewers (TextDisplay label ) {
408- for (UUID uuid : eligibleViewers ) {
409- Player player = Bukkit .getPlayer (uuid );
410- if (player != null ) {
411- player .showEntity (InteractionVisualizer .plugin , label );
477+ private void removeVisibilityState (UUID playerId , Player player ) {
478+ VisibilityState state = visibilityStates .remove (playerId );
479+ if (state == null ) {
480+ return ;
481+ }
482+ if (player != null ) {
483+ for (UUID itemId : state .shown ) {
484+ TextDisplay label = labels .get (itemId );
485+ if (label != null && label .isValid ()) {
486+ player .hideEntity (InteractionVisualizer .plugin , label );
487+ }
412488 }
413489 }
490+ state .pending .clear ();
414491 }
415492
416493 private Component format (ItemStack stack , int ticksLeft ) {
@@ -460,17 +537,27 @@ private void remove(UUID itemId) {
460537 }
461538
462539 private void removeLabel (UUID itemId ) {
463- removeLabel (labels .remove (itemId ));
540+ TextDisplay label = labels .remove (itemId );
541+ forgetLabelVisibility (itemId , label );
542+ removeLabel (label );
464543 }
465544
466- private void removeLabel (TextDisplay label ) {
467- if (label != null && label .isValid ()) {
468- for (UUID uuid : eligibleViewers ) {
469- Player player = Bukkit .getPlayer (uuid );
545+ private void forgetLabelVisibility (UUID itemId , TextDisplay label ) {
546+ for (Map .Entry <UUID , VisibilityState > entry : visibilityStates .entrySet ()) {
547+ VisibilityState state = entry .getValue ();
548+ state .desired .remove (itemId );
549+ state .pending .cancel (itemId );
550+ if (state .shown .remove (itemId ) && label != null && label .isValid ()) {
551+ Player player = Bukkit .getPlayer (entry .getKey ());
470552 if (player != null ) {
471553 player .hideEntity (InteractionVisualizer .plugin , label );
472554 }
473555 }
556+ }
557+ }
558+
559+ private void removeLabel (TextDisplay label ) {
560+ if (label != null && label .isValid ()) {
474561 label .remove ();
475562 }
476563 }
@@ -485,4 +572,15 @@ private static NamespacedKey ownerKey() {
485572
486573 private record TrackedItem (UUID itemId , Item item , Location location ) {
487574 }
575+
576+ private static final class VisibilityState {
577+
578+ private Set <UUID > desired = new HashSet <>();
579+ private final Set <UUID > shown = new HashSet <>();
580+ private final VisibilityTokenBucket <UUID > pending ;
581+
582+ private VisibilityState (int initialTokens ) {
583+ this .pending = new VisibilityTokenBucket <>(initialTokens );
584+ }
585+ }
488586}
0 commit comments