2424import com .loohp .interactionvisualizer .utils .ItemNameUtils ;
2525import com .loohp .interactionvisualizer .scheduler .ScheduledRunnable ;
2626import com .loohp .interactionvisualizer .scheduler .ScheduledTask ;
27+ import com .loohp .interactionvisualizer .scheduler .Scheduler ;
2728import net .kyori .adventure .text .Component ;
2829import net .kyori .adventure .text .TextReplacementConfig ;
2930import net .kyori .adventure .text .format .NamedTextColor ;
4041import org .bukkit .entity .Player ;
4142import org .bukkit .entity .TextDisplay ;
4243import org .bukkit .event .EventHandler ;
44+ import org .bukkit .event .EventPriority ;
4345import org .bukkit .event .Listener ;
46+ import org .bukkit .event .entity .EntityRemoveEvent ;
4447import org .bukkit .event .entity .ItemSpawnEvent ;
4548import org .bukkit .event .world .EntitiesLoadEvent ;
4649import org .bukkit .event .world .EntitiesUnloadEvent ;
@@ -170,6 +173,20 @@ public void onEntitiesUnload(EntitiesUnloadEvent event) {
170173 }
171174 }
172175
176+ @ EventHandler (priority = EventPriority .MONITOR )
177+ public void onEntityRemove (EntityRemoveEvent event ) {
178+ if (event .getEntity () instanceof Item item ) {
179+ UUID itemId = item .getUniqueId ();
180+ trackedItems .remove (itemId );
181+ TextDisplay label = labels .remove (itemId );
182+ if (label != null ) {
183+ // EntityRemoveEvent is monitoring-only. Defer entity mutation
184+ // until Paper has finished removing the item's passengers.
185+ Scheduler .runTask (InteractionVisualizer .plugin , () -> removeLabel (label ));
186+ }
187+ }
188+ }
189+
173190 private void track (Item item ) {
174191 trackedItems .put (item .getUniqueId (), item );
175192 }
@@ -211,17 +228,30 @@ private void update(Item item) {
211228 if (!text .equals (label .text ())) {
212229 label .text (text );
213230 }
214- int transitionTicks = Math .min (59 , updateRate );
215- if (label .getInterpolationDuration () != transitionTicks ) {
216- label .setInterpolationDuration (transitionTicks );
217- }
218- if (label .getTeleportDuration () != transitionTicks ) {
219- label .setTeleportDuration (transitionTicks );
220- }
221- label .teleport (item .getLocation ().add (0.0 , item .getHeight () * 1.7 , 0.0 ));
231+ boolean mounted = item .equals (label .getVehicle ()) || item .addPassenger (label );
222232 if (created ) {
233+ // Mount before revealing the label so Paper can pair both entities
234+ // with their passenger relationship in the initial tracking bundle.
223235 showToEligibleViewers (label );
224236 }
237+ if (mounted ) {
238+ // A mounted display follows the item on every client render frame.
239+ // Text refreshes stay low-frequency without sampling item positions.
240+ if (label .getInterpolationDuration () != 0 ) {
241+ label .setInterpolationDuration (0 );
242+ }
243+ if (label .getTeleportDuration () != 0 ) {
244+ label .setTeleportDuration (0 );
245+ }
246+ } else {
247+ // Preserve a safe fallback if another plugin cancels the mount or
248+ // changes either entity during the update.
249+ int transitionTicks = Math .min (59 , updateRate );
250+ if (label .getTeleportDuration () != transitionTicks ) {
251+ label .setTeleportDuration (transitionTicks );
252+ }
253+ label .teleport (item .getLocation ().add (0.0 , item .getHeight () * 1.7 , 0.0 ));
254+ }
225255 }
226256
227257 private TextDisplay spawnLabel (Item item ) {
@@ -235,8 +265,8 @@ private TextDisplay spawnLabel(Item item) {
235265 display .setNoPhysics (true );
236266 display .setBillboard (Display .Billboard .CENTER );
237267 display .setViewRange (1.0F );
238- display .setInterpolationDuration (Math . min ( 59 , updateRate ) );
239- display .setTeleportDuration (Math . min ( 59 , updateRate ) );
268+ display .setInterpolationDuration (0 );
269+ display .setTeleportDuration (0 );
240270 display .setShadowed (true );
241271 display .setSeeThrough (false );
242272 display .setDefaultBackground (false );
@@ -344,7 +374,10 @@ private void remove(UUID itemId) {
344374 }
345375
346376 private void removeLabel (UUID itemId ) {
347- TextDisplay label = labels .remove (itemId );
377+ removeLabel (labels .remove (itemId ));
378+ }
379+
380+ private void removeLabel (TextDisplay label ) {
348381 if (label != null && label .isValid ()) {
349382 for (UUID uuid : eligibleViewers ) {
350383 Player player = Bukkit .getPlayer (uuid );
0 commit comments