# Why Sign over DisplayTags
Sign is a maintained continuation of the DisplayTags plugin, rebuilt from the ground up with a new architecture, a developer API, voice chat integration, and dozens of reliability fixes. This document breaks down every meaningful difference.
---
## Architecture
### DisplayTags
- Single-module project. Plugin code, entity logic, commands, and config are all in one flat Gradle module.
- Package: `me.itsskeptical.displaytags`
- No API. Other plugins have zero way to interact with nametags programmatically.
- PacketEvents is a **required external dependency** — server owners must download and install it separately.
### Sign
- **Multi-module architecture**: `Sign-API` (lightweight interface jar) and `Sign-Paper` (implementation).
- Package: `gg.lode.sign`
- Full developer API published to JitPack. Other plugins can depend on `Sign-API` without touching internals.
- PacketEvents is **shaded and relocated** into the plugin jar. No external dependency needed — drag and drop.
- CommandAPI is also shaded and relocated, replacing the custom command framework.
- bStats integration for anonymous usage metrics.
**Why it matters:** Server owners install one jar instead of two. Developers get a stable API contract that won't break when internals change. Shading prevents version conflicts when other plugins use different PacketEvents versions.
---
## Developer API
### DisplayTags
No API exists. There is no way for another plugin to:
- Read a player's nametag state
- Override nametag lines
- React to config reloads
- Control visibility
### Sign
Full programmatic control via `Sign-API`:
```java
// Get a player's nametag
INametag nametag = SignAPI.getNametagManager().get(player);
// Override for all viewers
nametag.setLines(List.of("<red>WANTED", "<gray>Bounty: 1000g"));
// Override for a specific viewer
nametag.setLines(viewer, List.of("<green>Ally", "<gray>{health} HP"));
// Check and release overrides
if (nametag.hasOverride(viewer)) {
nametag.release(viewer);
}
```
Override priority:
| Priority | Source |
|---|---|
| 1 (highest) | Per-viewer override — `setLines(Player, List)` |
| 2 | Global override — `setLines(List)` |
| 3 (lowest) | Config lines |
All built-in placeholders (`{player}`, `{health}`, `{voice}`), PlaceholderAPI, MiniMessage, and `<condition>` tags work inside override lines. They resolve at display time, so a line like `{voice} <gray>{player}` updates live without re-calling `setLines()`.
`SignReloadEvent` fires after `/sign reload`, letting plugins reapply overrides that were cleared:
```java
@EventHandler
public void onSignReload(SignReloadEvent event) {
reapplyAllOverrides();
}
```
**Why it matters:** Plugins like rank systems, factions, vanish, and minigame engines can control nametags without packets or NMS. Per-viewer overrides enable use cases DisplayTags cannot touch — showing different information to allies vs enemies, hiding nametags from specific players, or displaying viewer-relative data.
---
## Voice Chat Integration
### DisplayTags
No voice chat support. No awareness of Simple Voice Chat or any voice plugin.
### Sign
Optional integration with **Simple Voice Chat** and **Amplifier**.
The `{voice}` placeholder displays a live icon based on the player's voice state:
| State | Default | When |
|---|---|---|
| Speaking | 🔊 | Player is transmitting audio (mic packet within 750ms) |
| Idle | 🔈 | Connected, not speaking |
| Deafened | 🔇 | SVC disabled or Amplifier deafened |
| Disconnected | 🔌 | Not connected to the voice server |
```yaml
nametags:
display:
lines:
- "{voice} <gray>{player}"
voice-chat:
enabled: true
icons:
speaking: "🔊"
idle: "🔈"
deafened: "🔇"
disconnected: "🔌"
```
Every icon supports MiniMessage, so server owners can use custom resource pack fonts:
```yaml
icons:
speaking: "<font:myfont:icons>\uE001</font>"
```
When Amplifier is installed, Sign uses its API for deafen detection (`IVoicePlayer.isDeafened()`), which catches Amplifier-managed deafens that SVC's native `isDisabled()` doesn't cover.
Disabled by default. If SVC is not installed, `{voice}` resolves to an empty string — safe to leave in templates.
**Why it matters:** Voice chat is a core part of modern multiplayer servers. Seeing who's speaking, muted, or disconnected at a glance — directly on the nametag — is information that was previously only available in a separate voice chat HUD overlay.
---
## Packet System
### DisplayTags
- Packets are sent individually. Spawn, metadata, and mount packets go out as separate calls.
- No packet interception. Other plugins that modify entity passengers (vehicles, seats) can silently unmount the nametag display entities.
- No profile refresh handling. When a nickname plugin changes a player's game profile, the nametag detaches and never re-mounts until the next join.
### Sign
- **Packet bundling**: All packets for a single nametag operation (spawn + metadata + mount) are sent together via `sendPacketSilently` in a single bundle. Reduces the window for client-side race conditions.
- **SET_PASSENGERS interception**: Sign intercepts outgoing passenger packets and merges nametag display entity IDs into the passenger list. Other plugins cannot accidentally unmount nametags.
- **DESTROY_ENTITIES interception**: When a player entity is destroyed for a viewer (respawn, re-tracking), Sign schedules a re-mount after 20 ticks to ensure the nametag survives the entity lifecycle.
- **Profile refresh detection**: Nametags automatically re-mount when a player's profile is refreshed (e.g. nicking plugins, skin changes).
**Why it matters:** DisplayTags nametags frequently detach — after teleports, respawns, world changes, vehicle mounts, or profile refreshes. Sign's packet interception and delayed re-mount system makes nametags survive every scenario we've tested.
---
## Update System
### DisplayTags
- Every viewer with a visible nametag receives metadata packets on every update tick, regardless of whether the text changed.
- No per-viewer caching. The same text is re-resolved and re-sent every cycle.
- No sneak state caching. Crouching triggers a full text re-send even if opacity is the only thing that changed.
### Sign
- **Global dirty checking**: Text and sneak state are cached. If nothing changed since the last tick, no packets are sent to any viewer.
- **Per-viewer dirty caching**: Each viewer's resolved text is cached separately. Viewers with per-viewer overrides only receive packets when *their specific* resolved output changes — not when another viewer's changes.
- **`update()` instead of `hide()`/`show()`**: When text changes for an already-visible nametag, Sign sends a metadata update + remount instead of despawning and respawning the entity. This eliminates the brief flicker visible in DisplayTags on every text update.
For a server with 100 players and a 1-second update interval, DisplayTags sends ~10,000 metadata packets per second (100 players × 100 viewers). Sign sends packets only when text actually changes — typically near zero for static nametags and proportional to actual changes for dynamic ones.
**Why it matters:** Less bandwidth, less CPU, no visual flicker. The difference is especially noticeable on large servers or when using fast update intervals.
---
## Crouching Support
### DisplayTags
- No crouching support. Nametags remain fully opaque and visible when a player is sneaking.
### Sign
- When `support-crouching` is enabled (default `true`):
- Sneaking lowers text opacity from full (-1) to semi-transparent (64)
- See-through is forced to `false` while crouching, then restored to the config value when standing
- The transition happens immediately on the `PlayerToggleSneakEvent` — no waiting for the next update tick
**Why it matters:** Vanilla Minecraft makes player names semi-transparent when crouching. DisplayTags breaks this expectation entirely. Sign restores it.
---
## Visibility and Mount Handling
### DisplayTags
- No delayed mount on teleport or world change. The mount packet often arrives before the client processes the teleport, causing the nametag to detach.
- No re-mount on death/respawn beyond a basic hide/show.
- No protection against other plugins removing the nametag from the passenger list.
### Sign
- **Teleport/world change**: Nametag is hidden immediately, then re-shown after a 20-tick delay to ensure the client has processed the position change.
- **Death/respawn**: Hidden on death, re-shown with a 5-tick delay after respawn.
- **Profile refresh**: Re-mounted after a 20-tick delay.
- **Passenger packet protection**: The `PacketListener` intercepts `SET_PASSENGERS` packets and merges nametag entity IDs back in if another plugin tries to set passengers on a player who has a visible nametag.
- **Entity re-tracking**: When the server sends a `DESTROY_ENTITIES` packet for a player (e.g. going out of view range and coming back), Sign schedules a re-mount to ensure the nametag reappears.
**Why it matters:** Every edge case that causes DisplayTags nametags to vanish — teleporting, dying, changing worlds, mounting a horse, having another plugin modify passengers — is handled in Sign.
---
## Conditional Lines
### DisplayTags
- Lines are static. If a placeholder resolves to empty, the line remains as a blank gap in the nametag.
### Sign
- `<condition:'value'>text</condition>` syntax. When `value` is `true`, the text is shown. When `false`, the entire line is removed.
- Lines that resolve to blank (after stripping color codes) are automatically hidden with no vertical gap. The remaining lines stack with consistent spacing.
- Dynamic display count: if an API override has more lines than the config, Sign creates additional `ClientTextDisplay` entities on the fly.
```yaml
lines:
- "<gray>{player}"
- "<condition:'%player_is_op%'><gold>OP</condition>"
- "<red>❤ <white>{health}"
```
Non-OP players see two lines (name and health). OP players see three. No blank line in between.
**Why it matters:** Servers that show rank, guild, or status tags only to certain players no longer need to waste a line slot on empty space.
---
## Commands
### DisplayTags
- Custom command framework with `CommandGroup` and `Subcommand` abstract classes.
- `/displaytags` root command with `help`, `reload`, and `config` subcommands.
- Tab completion handled manually.
### Sign
- Uses **CommandAPI** (shaded and relocated). Proper argument parsing, tab completion, and permission handling out of the box.
- `/sign` root command with `version`, `reload`, and `config` subcommands.
- `/sign config` now includes voice chat settings with hover tooltips for icon preview.
- Permission: `lodestone.sign.admin`
**Why it matters:** CommandAPI handles edge cases (async tab complete, permission filtering, argument validation) that the custom framework didn't. Less code to maintain, fewer bugs.
---
## Config
### DisplayTags
```yaml
# config.yml (DisplayTags 1.1.4)
nametags:
enabled: true
show-self: true
update-interval: 20
visibility-distance: 32
display:
lines: [...]
text-shadow: true
see-through: false
text-alignment: "center"
background: "default"
billboard: "center"
scale: { x: 1, y: 1, z: 1 }
```
### Sign
```yaml
# config.yml (Sign 1.0.1)
version: 1
nametags:
enabled: true
show-self: true
update-interval: 20
visibility-distance: 32
display:
lines: [...]
text-shadow: true
see-through: false
support-crouching: true # New
condense-holograms: false # New
text-alignment: "center"
background: "default"
billboard: "center"
scale: { x: 1, y: 1, z: 1 }
voice-chat: # New
enabled: false
icons:
speaking: "🔊"
idle: "🔈"
deafened: "🔇"
disconnected: "🔌"
```
New options:
| Field | Default | Description |
|---|---|---|
| `version` | `1` | Config version for automatic migration |
| `support-crouching` | `true` | Lowers opacity and disables see-through when sneaking |
| `condense-holograms` | `false` | Single text display vs one-per-line |
| `voice-chat.enabled` | `false` | Enable the `{voice}` placeholder |
| `voice-chat.icons.*` | Emoji | Configurable icons per voice state |
**Config migration**: Sign automatically migrates v0 configs to v1 on startup (converts update-interval from seconds to ticks, adds new options). DisplayTags has no migration system.
---
## Dependencies
### DisplayTags
- **PacketEvents** — required, must be installed separately by the server owner
- **PlaceholderAPI** — optional
- **TAB** — optional
### Sign
- **No external dependencies** — PacketEvents, CommandAPI, and bStats are all shaded into the jar
- **PlaceholderAPI** — optional
- **TAB** — optional
- **Simple Voice Chat** — optional (for `{voice}` placeholder)
- **Amplifier** — optional (enhances voice chat deafen detection)
**Why it matters:** One jar. No dependency hell. No version mismatches. Server owners don't need to know what PacketEvents is.
---
## Summary
| Feature | DisplayTags | Sign |
|---|---|---|
| Architecture | Single module | Multi-module (API + Paper) |
| Developer API | None | Full (overrides, events, per-viewer) |
| External dependencies | PacketEvents required | Zero (all shaded) |
| Voice chat support | None | SVC + Amplifier |
| Packet bundling | No | Yes |
| Passenger protection | No | Intercepts SET_PASSENGERS |
| Profile refresh mount | No | 20-tick delayed re-mount |
| Teleport/world mount | No delay | 20-tick delayed re-mount |
| Crouching support | No | Opacity + see-through toggle |
| Conditional lines | No | `<condition>` tags + blank line hiding |
| Per-viewer overrides | No | Yes |
| Dirty caching | No | Global + per-viewer |
| Text update method | hide/show (flicker) | metadata update (no flicker) |
| Config migration | No | Automatic v0 → v1 |
| Command framework | Custom | CommandAPI (shaded) |
| bStats | No | Yes |
| Update checker | Modrinth API | Lodestone API + OP notification |