# v1.1.0
Complete rewrite of the nametag spawn/mount system. Nametags are now driven by actual client-side entity packets instead of guessing with fixed tick delays, with major performance improvements for high-player-count servers.
---
## Added
### Packet-Driven Nametag Spawning
The entire nametag lifecycle is now driven by intercepting `SPAWN_ENTITY` and `DESTROY_ENTITIES` packets rather than relying on fixed tick delays after events.
**Before:** When a player teleported, changed worlds, or joined, the plugin would hide their nametag and blindly wait 20 ticks before respawning it — hoping the client had processed the entity by then. Too early and the mount failed silently; too late and there was a visible gap with no nametag.
**After:** The plugin intercepts the actual `SPAWN_ENTITY` packet that the server sends when a player entity appears on a viewer's client. The nametag is spawned and mounted 2 ticks after the entity packet, guaranteeing the client has the player entity before the mount arrives.
| Event | Old Behavior | New Behavior |
|---|---|---|
| Player joins | 20-tick delay + 10-tick re-mount | Immediate on `PlayerClientLoadedWorldEvent` |
| Teleport (cross-range) | `hideForAll()` + 20-tick `scheduleRefresh` | `DESTROY_ENTITIES` → hide, `SPAWN_ENTITY` → show |
| Teleport (in-range) | `hideForAll()` + 20-tick `scheduleRefresh` | No action needed — mount persists naturally |
| World change | `hideForAll()` + 20-tick `scheduleRefresh` | `DESTROY_ENTITIES` → hide, `SPAWN_ENTITY` → show |
| Respawn | 5-tick delay | `showToEligible()` after 2 ticks |
| Re-enter tracking range | 20-tick delay from `DESTROY_ENTITIES` handler | `SPAWN_ENTITY` → show after 2 ticks |
The `PlayerTeleportEvent` and `PlayerChangedWorldEvent` handlers have been removed entirely — the packet layer handles both cases with deterministic timing.
---
### GSit / Player Riding Compatibility
Nametags now survive when a player is mounted as a passenger on another entity — such as sitting, laying, or crawling via [GSit](https://modrinth.com/plugin/gsit), or riding another player.
When the server sends a `SET_PASSENGERS` packet that lists a nametag's player as a **passenger** (not the vehicle), Sign re-sends the nametag mount packet after a 2-tick delay. This forces the client to re-attach the display entities that it silently drops when processing the new passenger relationship.
A new `remount(Player viewer)` method on `Nametag` handles this efficiently — it only re-sends the `SET_PASSENGERS` packet without respawning or updating metadata, so there is no visual flicker.
---
### `showToEligible()` Method
A new method that shows the nametag to all eligible players who don't already see it. Unlike the old approach of calling `updateVisibilityForAll()` (which would spawn nametags even when the player entity might not exist on the viewer's client), `showToEligible()` is only called from contexts where the entity is guaranteed to exist:
- After initial nametag creation (`NametagManager.create()`)
- After respawn (entity was never destroyed for other viewers)
- After leaving spectator mode (entity re-enters tracking)
---
## Fixed
### Scheduler No Longer Pre-Spawns Nametags
The periodic scheduler (`updateVisibilityForAll()`) previously called `show()` for any player where `shouldSee()` returned true, even when the player entity hadn't been sent to the viewer's client yet. This caused a race condition:
1. Scheduler sees player in range → calls `show()` → spawns display + sends mount referencing a player entity ID that doesn't exist on the client → mount fails silently
2. `SPAWN_ENTITY` packet fires later → handler calls `show()` → blocked by the duplicate viewer guard → nametag stays unmounted
The scheduler now **only** updates text and hides out-of-range viewers. It never spawns new nametags. The `show()` method also has a guard preventing double-spawning if both the packet handler and another code path attempt to show the same viewer.
### Duplicate Viewer Guard in `show()`
`show()` now returns early if the viewer is already in the viewers set. This prevents double-spawning from concurrent show paths (scheduler, packet handler, event handler) without visual side effects — the vanilla nametag is still hidden even when the guard triggers.
---
## Changed
### Scheduler Performance Optimization
The scheduler's `updateVisibilityForAll()` has been significantly optimized for high-player-count servers:
**String-level dirty checking.** Text resolution is now split into two phases:
1. `resolveToStrings()` — placeholder substitution and condition evaluation (cheap string operations)
2. `parseComponents()` — MiniMessage deserialization (expensive, only called when strings actually changed)
Previously, every scheduler tick resolved placeholders **and** parsed MiniMessage for every nametag, then compared the resulting `Component` objects. With 100 players, that was 100+ full MiniMessage parse cycles per tick — **~11% of the server thread** according to profiler data. Now, when text hasn't changed (the common case), only cheap string comparisons run.
**Early exit for unwatched nametags.** If a nametag has zero viewers, the scheduler skips all resolution and parsing work entirely.
**No more all-player iteration.** The scheduler now only iterates the existing `viewers` set instead of `Bukkit.getOnlinePlayers()`. With 100 players spread across the map where each nametag has ~10 viewers, this reduces per-nametag iteration from 100 to 10.
**`sendMetadataUpdate()`.** When updating non-override viewers, the scheduler sends the current display state directly without re-resolving text. The displays already have the correct text from the dirty check phase. Override viewers still use `update()` which resolves their specific text (rare path).
**Static `DecimalFormat`.** The `new DecimalFormat("#.##")` allocation that ran per line per nametag per tick is now a static constant.
### Removed `inRange` Tracking Set
The `inRange` set that tracked players within visibility distance (separate from `viewers`) has been removed. It was used for a "re-entered range" respawn path that is now handled deterministically by `DESTROY_ENTITIES` → `SPAWN_ENTITY` packets.
### Removed Debounce Infrastructure
The `pendingRefreshes`, `pendingTeleportPlayers`, `scheduleRefresh()`, `cancelPendingRefresh()`, and `hasPendingRefresh()` systems in `PlayerListener` have been removed. The packet-driven approach eliminates the need for debounced delayed tasks — each packet event is self-contained.
---
## Developer Notes
### Spawn Timing Guarantees
Nametag spawning is now tied to the actual `SPAWN_ENTITY` packet the server sends to each viewer. If you're calling `show()` from your own code, ensure the player entity exists on the target viewer's client — otherwise the mount will fail silently. Use `showToEligible()` when you know the entity exists for nearby viewers (e.g., after creating a nametag for a player who is already in the world).
### `updateVisibilityForAll()` Behavior Change
This method no longer spawns nametags for new viewers. It only:
- Sends text/metadata updates to existing viewers when dirty
- Hides viewers who are no longer eligible (dead, invisible, spectator, out of range, different world)
If you were relying on `updateVisibilityForAll()` to show nametags to new viewers, use `showToEligible()` instead.
### `remount(Player viewer)`
A new public method on `Nametag` (not in `INametag` API) that re-sends the mount packet without respawning display entities. Useful if you know a viewer's client has dropped the passenger relationship:
```java
Nametag nametag = (Nametag) SignAPI.getNametagManager().get(player);
nametag.remount(viewer);
```
---
## Links
- [Modrinth](https://modrinth.com/plugin/sign)
- [Documentation](https://lode.gg/sign)