@@ -334,7 +334,10 @@ public void onUpdate()
334334 || e .getValue ().untilMs () < now );
335335 offlineStasisLabels .entrySet ()
336336 .removeIf (e -> seenPearls .contains (e .getKey ())
337- || e .getValue () == null || e .getValue ().untilMs () < now );
337+ || e .getValue () == null || e .getValue ().untilMs () < now
338+ || isPositionOccupiedByVisiblePearl (
339+ e .getValue () != null ? e .getValue ().pos () : null ,
340+ seenPearls ));
338341
339342 // Disconnect inference: pearls that disappeared since last frame
340343 if (inferOwnership .isChecked ())
@@ -343,31 +346,36 @@ public void onUpdate()
343346 vanished .removeAll (seenPearls );
344347 for (UUID vanishedUuid : vanished )
345348 {
349+ Vec3 vanishedPos = lastPearlPositions .get (vanishedUuid );
350+ if (vanishedPos == null )
351+ continue ;
352+
353+ // Skip if a currently visible pearl is at the same position
354+ // (UUID change due to server re-creating the entity)
355+ if (isPositionOccupiedByVisiblePearl (vanishedPos , seenPearls ))
356+ continue ;
357+
346358 UUID existingOwner = pearlOwnerUuids .get (vanishedUuid );
347359 if (existingOwner != null )
348360 {
349- Vec3 pos = lastPearlPositions . get ( vanishedUuid );
350- if ( pos != null )
351- {
352- String inferred = getOwnerUuidLabel ( existingOwner );
353- if ( inferred != null )
354- offlineStasisLabels . put ( vanishedUuid ,
355- new StasisLabel (
356- formatOfflineLabelText ( vanishedUuid ,
357- inferred ) ,
358- pos , now + OFFLINE_STASIS_STICKY_MS ));
359- }
361+ // Never create offline labels for your own pearls. You
362+ // already know where they are and "Owner: You" is noise.
363+ if ( MC . player != null
364+ && existingOwner . equals ( MC . player . getUUID ()))
365+ continue ;
366+
367+ String inferred = getOwnerUuidLabel ( existingOwner );
368+ if ( inferred != null )
369+ putOfflineStasisLabel ( vanishedUuid ,
370+ formatOfflineLabelText ( vanishedUuid , inferred ),
371+ vanishedPos , now );
360372 continue ;
361373 }
362374
363375 String inferred = inferOwnerFromLeaveTiming (vanishedUuid );
364376 if (inferred != null )
365- {
366- Vec3 pos = lastPearlPositions .get (vanishedUuid );
367- if (pos != null )
368- offlineStasisLabels .put (vanishedUuid , new StasisLabel (
369- inferred , pos , now + OFFLINE_STASIS_STICKY_MS ));
370- }
377+ putOfflineStasisLabel (vanishedUuid , inferred , vanishedPos ,
378+ now );
371379 }
372380 }
373381 previousPearlUuids = seenPearls ;
@@ -1314,6 +1322,10 @@ private String inferOwnerFromLeaveTiming(UUID pearlUuid)
13141322 if (candidateCount != 1 || bestCandidate == null )
13151323 return null ;
13161324
1325+ // Never infer yourself — you know you didn't leave
1326+ if (MC .player != null && bestCandidate .equals (MC .player .getUUID ()))
1327+ return null ;
1328+
13171329 String label = getOwnerUuidLabel (bestCandidate );
13181330 if (label == null )
13191331 return null ;
@@ -1338,6 +1350,12 @@ private void inferOwnersFromPlayerLeave(UUID playerUuid, long now)
13381350 {
13391351 if (playerUuid == null )
13401352 return ;
1353+
1354+ // Ignore REMOVE_PLAYER info refreshes where the player is still
1355+ // visible in the world (common on servers that send remove+add
1356+ // cycles for tab-list/latency updates).
1357+ if (isPlayerVisibleInWorld (playerUuid ))
1358+ return ;
13411359
13421360 Integer playerEntityId = null ;
13431361 PlayerIdentity identity = playerIdentities .get (playerUuid );
@@ -1389,9 +1407,8 @@ private void inferOwnersFromPlayerLeave(UUID playerUuid, long now)
13891407
13901408 Vec3 pos = lastPearlPositions .get (pearlUuid );
13911409 if (pos != null )
1392- offlineStasisLabels .put (pearlUuid ,
1393- new StasisLabel (formatOfflineLabelText (pearlUuid , label ),
1394- pos , now + OFFLINE_STASIS_STICKY_MS ));
1410+ putOfflineStasisLabel (pearlUuid ,
1411+ formatOfflineLabelText (pearlUuid , label ), pos , now );
13951412
13961413 inferredCount ++;
13971414 }
@@ -1523,6 +1540,73 @@ private OwnerConfidence getPearlConfidence(UUID pearlUuid)
15231540 return pid != null ? pid .confidence () : null ;
15241541 }
15251542
1543+ /**
1544+ * Checks whether any currently visible pearl is at approximately the
1545+ * same position as the given vanished position. This prevents creating
1546+ * duplicate offline labels when the server re-creates a pearl entity
1547+ * with a new UUID (common in bubble-column stasis chambers).
1548+ */
1549+ private boolean isPositionOccupiedByVisiblePearl (Vec3 vanishedPos ,
1550+ HashSet <UUID > seenPearls )
1551+ {
1552+ if (vanishedPos == null )
1553+ return false ;
1554+ double threshold = 0.5 ;
1555+ for (UUID seenUuid : seenPearls )
1556+ {
1557+ Vec3 seenPos = lastPearlPositions .get (seenUuid );
1558+ if (seenPos != null
1559+ && seenPos .distanceToSqr (vanishedPos ) < threshold * threshold )
1560+ return true ;
1561+ }
1562+ return false ;
1563+ }
1564+
1565+ /**
1566+ * Checks whether a player with the given UUID is currently visible
1567+ * among the entities being rendered in the world.
1568+ */
1569+ private boolean isPlayerVisibleInWorld (UUID playerUuid )
1570+ {
1571+ if (playerUuid == null || MC .level == null )
1572+ return false ;
1573+ for (Entity entity : MC .level .entitiesForRendering ())
1574+ {
1575+ if (entity instanceof Player && playerUuid .equals (entity .getUUID ()))
1576+ return true ;
1577+ }
1578+ return false ;
1579+ }
1580+
1581+ /**
1582+ * Adds or refreshes an offline stasis label, de-duplicating by position.
1583+ * If an existing offline label exists at approximately the same position,
1584+ * its timestamp is refreshed instead of creating a duplicate.
1585+ */
1586+ private void putOfflineStasisLabel (UUID vanishedUuid , String text , Vec3 pos ,
1587+ long now )
1588+ {
1589+ double threshold = 0.5 ;
1590+ long untilMs = now + OFFLINE_STASIS_STICKY_MS ;
1591+
1592+ // Check for an existing offline label at the same position
1593+ for (Map .Entry <UUID , StasisLabel > entry : offlineStasisLabels .entrySet ())
1594+ {
1595+ StasisLabel existing = entry .getValue ();
1596+ if (existing != null && existing .pos () != null
1597+ && existing .pos ().distanceToSqr (pos ) < threshold * threshold )
1598+ {
1599+ // Refresh the existing entry instead of adding a duplicate
1600+ offlineStasisLabels .put (entry .getKey (),
1601+ new StasisLabel (existing .text (), existing .pos (), untilMs ));
1602+ return ;
1603+ }
1604+ }
1605+
1606+ offlineStasisLabels .put (vanishedUuid ,
1607+ new StasisLabel (text , pos , untilMs ));
1608+ }
1609+
15261610 private float computeDistanceScale (Vec3 labelPos , float baseScale )
15271611 {
15281612 if (MC .player == null )
0 commit comments