5656import org .joml .Quaternionf ;
5757import org .joml .Vector3f ;
5858
59+ import java .util .ArrayList ;
60+ import java .util .Collection ;
5961import java .util .HashMap ;
6062import java .util .HashSet ;
63+ import java .util .Iterator ;
6164import java .util .List ;
6265import java .util .Map ;
6366import java .util .Set ;
7174public final class DroppedItemDisplay extends VisualizerRunnableDisplay implements Listener {
7275
7376 public static final EntryKey KEY = new EntryKey ("item" );
77+ private static final int DEFAULT_VIEW_DISTANCE = 64 ;
78+ private static final int VIEW_DISTANCE_HYSTERESIS = 16 ;
7479
7580 private final Map <UUID , Item > trackedItems = new HashMap <>();
7681 private final Map <UUID , TextDisplay > labels = new HashMap <>();
@@ -87,6 +92,7 @@ public final class DroppedItemDisplay extends VisualizerRunnableDisplay implemen
8792 private int updateRate = 20 ;
8893 private int ticksUntilUpdate ;
8994 private int despawnTicks = 6000 ;
95+ private int viewDistance = DEFAULT_VIEW_DISTANCE ;
9096 private boolean stripColorBlacklist ;
9197 private DroppedItemBlacklist blacklist = DroppedItemBlacklist .compile (List .of (), DroppedItemDisplay ::warn );
9298
@@ -107,6 +113,11 @@ public void onReload(InteractionVisualizerReloadEvent event) {
107113 .getDouble ("Entities.Item.Options.LabelYOffset" );
108114 labelYOffset = Double .isFinite (configuredLabelYOffset ) ? configuredLabelYOffset : 0.8D ;
109115 updateRate = Math .max (1 , InteractionVisualizer .plugin .getConfiguration ().getInt ("Entities.Item.Options.UpdateRate" ));
116+ int configuredViewDistance = InteractionVisualizer .plugin .getConfiguration ()
117+ .getInt ("Entities.Item.Options.ViewDistance" );
118+ viewDistance = configuredViewDistance > 0
119+ ? Math .max (8 , Math .min (512 , configuredViewDistance ))
120+ : DEFAULT_VIEW_DISTANCE ;
110121 int configuredDespawnTicks = InteractionVisualizer .plugin .getConfiguration ().getInt ("Entities.Item.Options.DespawnTicks" );
111122 despawnTicks = configuredDespawnTicks > 0 ? configuredDespawnTicks : 6000 ;
112123 stripColorBlacklist = InteractionVisualizer .plugin .getConfiguration ()
@@ -200,32 +211,76 @@ private void track(Item item) {
200211 }
201212
202213 private void tickAll () {
203- reconcileEligibleViewers ();
204- for (Map .Entry <UUID , Item > entry : new HashMap <>(trackedItems ).entrySet ()) {
214+ Collection <Player > viewers = reconcileEligibleViewers ();
215+ DroppedItemSpatialIndex .ViewerIndex viewerIndex = new DroppedItemSpatialIndex .ViewerIndex ();
216+ for (Player viewer : viewers ) {
217+ Location location = viewer .getLocation ();
218+ viewerIndex .addViewer (viewer .getWorld ().getUID (), location .getX (), location .getY (), location .getZ ());
219+ }
220+
221+ List <TrackedItem > validItems = new ArrayList <>(trackedItems .size ());
222+ Iterator <Map .Entry <UUID , Item >> iterator = trackedItems .entrySet ().iterator ();
223+ while (iterator .hasNext ()) {
224+ Map .Entry <UUID , Item > entry = iterator .next ();
205225 Item item = entry .getValue ();
206226 if (!item .isValid () || item .isDead ()) {
207- remove (entry .getKey ());
227+ iterator .remove ();
228+ removeLabel (entry .getKey ());
208229 continue ;
209230 }
210- update (item );
231+ validItems .add (new TrackedItem (entry .getKey (), item , item .getLocation ()));
232+ }
233+
234+ if (viewerIndex .isEmpty ()) {
235+ for (UUID itemId : new HashSet <>(labels .keySet ())) {
236+ removeLabel (itemId );
237+ }
238+ return ;
239+ }
240+
241+ DroppedItemSpatialIndex itemIndex = new DroppedItemSpatialIndex ();
242+ for (TrackedItem tracked : validItems ) {
243+ Location location = tracked .location ();
244+ itemIndex .addItem (tracked .item ().getWorld ().getUID (),
245+ location .getX (), location .getY (), location .getZ ());
246+ }
247+ for (TrackedItem tracked : validItems ) {
248+ update (tracked , itemIndex , viewerIndex );
211249 }
212250 }
213251
214- private void update (Item item ) {
252+ private void update (TrackedItem tracked , DroppedItemSpatialIndex itemIndex ,
253+ DroppedItemSpatialIndex .ViewerIndex viewerIndex ) {
254+ Item item = tracked .item ();
255+ Location itemLocation = tracked .location ();
256+ TextDisplay label = labels .get (tracked .itemId ());
257+ int trackingDistance = InteractionVisualizer .playerTrackingRange
258+ .getOrDefault (item .getWorld (), DEFAULT_VIEW_DISTANCE );
259+ int effectiveViewDistance = Math .min (viewDistance , trackingDistance );
260+ int cullingDistance = label == null
261+ ? effectiveViewDistance
262+ : effectiveViewDistance + VIEW_DISTANCE_HYSTERESIS ;
263+ if (!viewerIndex .hasViewerWithin (item .getWorld ().getUID (),
264+ itemLocation .getX (), itemLocation .getY (), itemLocation .getZ (), cullingDistance )) {
265+ removeLabel (tracked .itemId ());
266+ return ;
267+ }
268+
215269 ItemStack stack = item .getItemStack ();
216270 String matchingName = matchingName (stack );
217271 NamespacedKey customItemId = blacklist .requiresCustomItemId ()
218272 ? CustomContentManager .customItemId (stack ).orElse (null )
219273 : null ;
220274 int ticksLeft = despawnTicks - item .getTicksLived ();
221275 if (stack .isEmpty () || blacklist .matches (matchingName , stack .getType (), customItemId )
222- || item .getPickupDelay () >= Short .MAX_VALUE || ticksLeft <= 0 || isCramping (item )) {
276+ || item .getPickupDelay () >= Short .MAX_VALUE || ticksLeft <= 0
277+ || (cramp > 0 && itemIndex .exceedsItemLimit (item .getWorld ().getUID (),
278+ itemLocation .getX (), itemLocation .getY (), itemLocation .getZ (), cramp ))) {
223279 removeLabel (item .getUniqueId ());
224280 return ;
225281 }
226282
227283 Component text = format (stack , ticksLeft );
228- TextDisplay label = labels .get (item .getUniqueId ());
229284 boolean created = false ;
230285 if (label == null || !label .isValid () || !label .getWorld ().equals (item .getWorld ())) {
231286 removeLabel (item .getUniqueId ());
@@ -236,6 +291,10 @@ private void update(Item item) {
236291 if (!text .equals (label .text ())) {
237292 label .text (text );
238293 }
294+ float targetViewRange = labelViewRange ();
295+ if (Math .abs (label .getViewRange () - targetViewRange ) > 1.0E-4F ) {
296+ label .setViewRange (targetViewRange );
297+ }
239298 boolean mounted = item .equals (label .getVehicle ()) || item .addPassenger (label );
240299 if (mounted ) {
241300 // A mounted display follows the item on every client render frame.
@@ -274,7 +333,7 @@ private TextDisplay spawnLabel(Item item) {
274333 display .setSilent (true );
275334 display .setNoPhysics (true );
276335 display .setBillboard (Display .Billboard .CENTER );
277- display .setViewRange (1.0F );
336+ display .setViewRange (labelViewRange () );
278337 display .setInterpolationDuration (0 );
279338 display .setTeleportDuration (0 );
280339 display .setShadowed (true );
@@ -287,6 +346,10 @@ private TextDisplay spawnLabel(Item item) {
287346 });
288347 }
289348
349+ private float labelViewRange () {
350+ return (float ) Math .max (0.125D , Math .min (8.0D , viewDistance / 64.0D ));
351+ }
352+
290353 private Location labelLocation (Item item ) {
291354 return item .getLocation ().add (0.0 , labelYOffset , 0.0 );
292355 }
@@ -308,7 +371,7 @@ private static void setLabelVerticalTranslation(TextDisplay label, float targetY
308371 new Quaternionf (current .getRightRotation ())));
309372 }
310373
311- private void reconcileEligibleViewers () {
374+ private Collection < Player > reconcileEligibleViewers () {
312375 Map <UUID , Player > desired = new HashMap <>();
313376 for (Player player : InteractionVisualizerAPI .getPlayerModuleList (Modules .HOLOGRAM , KEY )) {
314377 if (player .isOnline ()) {
@@ -338,6 +401,7 @@ private void reconcileEligibleViewers() {
338401 }
339402 }
340403 }
404+ return desired .values ();
341405 }
342406
343407 private void showToEligibleViewers (TextDisplay label ) {
@@ -390,15 +454,6 @@ private String matchingName(ItemStack stack) {
390454 return stripColorBlacklist ? ChatColorUtils .stripColor (plain ) : plain ;
391455 }
392456
393- private boolean isCramping (Item item ) {
394- return cramp > 0 && item .getWorld ()
395- .getNearbyEntitiesByType (Item .class , item .getLocation (), 0.5 , 0.5 , 0.5 )
396- .stream ()
397- .filter (nearby -> !isOwned (nearby ))
398- .limit (cramp + 1L )
399- .count () > cramp ;
400- }
401-
402457 private void remove (UUID itemId ) {
403458 trackedItems .remove (itemId );
404459 removeLabel (itemId );
@@ -427,4 +482,7 @@ private static boolean isOwned(Entity entity) {
427482 private static NamespacedKey ownerKey () {
428483 return new NamespacedKey (InteractionVisualizer .plugin , "visual_entity" );
429484 }
485+
486+ private record TrackedItem (UUID itemId , Item item , Location location ) {
487+ }
430488}
0 commit comments