# 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 |