3232import net .kyori .adventure .text .serializer .plain .PlainTextComponentSerializer ;
3333import org .bukkit .Bukkit ;
3434import org .bukkit .Color ;
35+ import org .bukkit .Location ;
3536import org .bukkit .Material ;
3637import org .bukkit .NamespacedKey ;
3738import org .bukkit .World ;
5152import org .bukkit .inventory .meta .Damageable ;
5253import org .bukkit .inventory .meta .ItemMeta ;
5354import org .bukkit .persistence .PersistentDataType ;
55+ import org .bukkit .util .Transformation ;
56+ import org .joml .Quaternionf ;
57+ import org .joml .Vector3f ;
5458
5559import java .util .HashMap ;
5660import java .util .HashSet ;
@@ -79,6 +83,7 @@ public final class DroppedItemDisplay extends VisualizerRunnableDisplay implemen
7983 private String mediumColor = "" ;
8084 private String lowColor = "" ;
8185 private int cramp = 6 ;
86+ private double labelYOffset = 0.8D ;
8287 private int updateRate = 20 ;
8388 private int ticksUntilUpdate ;
8489 private int despawnTicks = 6000 ;
@@ -98,6 +103,9 @@ public void onReload(InteractionVisualizerReloadEvent event) {
98103 mediumColor = configString ("Entities.Item.Options.Color.Medium" );
99104 lowColor = configString ("Entities.Item.Options.Color.Low" );
100105 cramp = InteractionVisualizer .plugin .getConfiguration ().getInt ("Entities.Item.Options.Cramping" );
106+ double configuredLabelYOffset = InteractionVisualizer .plugin .getConfiguration ()
107+ .getDouble ("Entities.Item.Options.LabelYOffset" );
108+ labelYOffset = Double .isFinite (configuredLabelYOffset ) ? configuredLabelYOffset : 0.8D ;
101109 updateRate = Math .max (1 , InteractionVisualizer .plugin .getConfiguration ().getInt ("Entities.Item.Options.UpdateRate" ));
102110 int configuredDespawnTicks = InteractionVisualizer .plugin .getConfiguration ().getInt ("Entities.Item.Options.DespawnTicks" );
103111 despawnTicks = configuredDespawnTicks > 0 ? configuredDespawnTicks : 6000 ;
@@ -229,14 +237,10 @@ private void update(Item item) {
229237 label .text (text );
230238 }
231239 boolean mounted = item .equals (label .getVehicle ()) || item .addPassenger (label );
232- if (created ) {
233- // Mount before revealing the label so Paper can pair both entities
234- // with their passenger relationship in the initial tracking bundle.
235- showToEligibleViewers (label );
236- }
237240 if (mounted ) {
238241 // A mounted display follows the item on every client render frame.
239242 // Text refreshes stay low-frequency without sampling item positions.
243+ setLabelVerticalTranslation (label , mountedLabelTranslation (labelYOffset , item .getHeight ()));
240244 if (label .getInterpolationDuration () != 0 ) {
241245 label .setInterpolationDuration (0 );
242246 }
@@ -246,16 +250,22 @@ private void update(Item item) {
246250 } else {
247251 // Preserve a safe fallback if another plugin cancels the mount or
248252 // changes either entity during the update.
253+ setLabelVerticalTranslation (label , 0.0F );
249254 int transitionTicks = Math .min (59 , updateRate );
250255 if (label .getTeleportDuration () != transitionTicks ) {
251256 label .setTeleportDuration (transitionTicks );
252257 }
253- label .teleport (item .getLocation ().add (0.0 , item .getHeight () * 1.7 , 0.0 ));
258+ label .teleport (labelLocation (item ));
259+ }
260+ if (created ) {
261+ // Mount and configure the final render height before revealing the
262+ // label so the first tracking bundle cannot flash at item height.
263+ showToEligibleViewers (label );
254264 }
255265 }
256266
257267 private TextDisplay spawnLabel (Item item ) {
258- return item .getWorld ().spawn (item . getLocation (). add ( 0.0 , item . getHeight () * 1.7 , 0.0 ),
268+ return item .getWorld ().spawn (labelLocation ( item ),
259269 TextDisplay .class , display -> {
260270 display .setPersistent (false );
261271 display .setVisibleByDefault (false );
@@ -277,6 +287,27 @@ private TextDisplay spawnLabel(Item item) {
277287 });
278288 }
279289
290+ private Location labelLocation (Item item ) {
291+ return item .getLocation ().add (0.0 , labelYOffset , 0.0 );
292+ }
293+
294+ static float mountedLabelTranslation (double yOffset , double itemHeight ) {
295+ return (float ) (yOffset - itemHeight );
296+ }
297+
298+ private static void setLabelVerticalTranslation (TextDisplay label , float targetY ) {
299+ Transformation current = label .getTransformation ();
300+ Vector3f translation = current .getTranslation ();
301+ if (Math .abs (translation .y - targetY ) <= 1.0E-4F ) {
302+ return ;
303+ }
304+ label .setTransformation (new Transformation (
305+ new Vector3f (translation .x , targetY , translation .z ),
306+ new Quaternionf (current .getLeftRotation ()),
307+ new Vector3f (current .getScale ()),
308+ new Quaternionf (current .getRightRotation ())));
309+ }
310+
280311 private void reconcileEligibleViewers () {
281312 Map <UUID , Player > desired = new HashMap <>();
282313 for (Player player : InteractionVisualizerAPI .getPlayerModuleList (Modules .HOLOGRAM , KEY )) {
0 commit comments