# v1.0.1 Voice chat integration, smarter update cycles, and mount reliability improvements. --- ## Added ### Simple Voice Chat Integration Sign now optionally integrates with [Simple Voice Chat](https://modrinth.com/plugin/simple-voice-chat) to display a player's voice status on their nametag. A new `{voice}` built-in placeholder resolves to a configurable icon based on four states: | State | Default Icon | Description | |---|---|---| | Speaking | 🔊 | Player is actively transmitting audio | | Idle | 🔈 | Connected to voice chat, not speaking | | Deafened | 🔇 | Voice chat is disabled or player is deafened | | Disconnected | 🔌 | Not connected to the voice chat server | Speaking detection works by registering as a Simple Voice Chat addon and tracking `MicrophonePacketEvent` packets with a 750ms timeout — if no audio packet is received within that window, the player transitions from Speaking to Idle. **Disabled by default.** Enable it in `config.yml`: ```yaml nametags: display: lines: - "{voice} <gray>{player}" - "<red>❤ <white>{health}" voice-chat: enabled: true icons: speaking: "🔊" idle: "🔈" deafened: "🔇" disconnected: "🔌" ``` Every icon value supports full MiniMessage formatting. Server owners using a resource pack can swap the emoji defaults for custom font characters: ```yaml icons: speaking: "<font:myfont:icons>\uE001</font>" idle: "<font:myfont:icons>\uE002</font>" deafened: "<font:myfont:icons>\uE003</font>" disconnected: "<font:myfont:icons>\uE004</font>" ``` The `{voice}` placeholder works everywhere regular placeholders work — config lines, global overrides, and per-viewer overrides. If Simple Voice Chat is not installed or `voice-chat.enabled` is `false`, the `{voice}` placeholder resolves to an empty string so it can safely be left in line templates. --- ### Amplifier Integration When the [Amplifier](https://modrinth.com/plugin/amplifier) voice management plugin is installed alongside Simple Voice Chat, Sign hooks into Amplifier's API for enhanced deafen detection. Amplifier manages its own deafen state (`IVoicePlayer.isDeafened()`) independently from Simple Voice Chat's native disabled state. With this hook, the `{voice}` placeholder correctly shows the deafened icon when a player is deafened through Amplifier — for example, via `/amplifier deafen true` or through another plugin using Amplifier's API. The priority chain: | Check | Source | Result | |---|---|---| | No SVC connection | Simple Voice Chat | `disconnected` | | `IVoicePlayer.isDeafened()` | Amplifier API | `deafened` | | `VoicechatConnection.isDisabled()` | Simple Voice Chat | `deafened` | | Recent mic packet (< 750ms) | Simple Voice Chat addon | `speaking` | | Fallthrough | — | `idle` | No additional configuration is needed — if Amplifier is present and `voice-chat.enabled` is `true`, the hook activates automatically and logs `Hooked into Amplifier!` on startup. --- ### Per-Viewer Dirty Caching The update loop now caches resolved text components per viewer, not just globally. On each tick, the scheduler compares each viewer's resolved output against their cached version and only sends metadata packets when that viewer's text actually changed. Previously, any viewer with an active override (`setLines(Player, List)`) was always marked dirty on every update tick, causing redundant packet sends even when the resolved text hadn't changed. This is especially impactful for servers using per-viewer overrides at scale — for example, showing faction tags that only change on faction join/leave. --- ### `/sign config` Voice Chat Section The `/sign config` command now includes a Voice Chat section showing whether the integration is enabled and, if so, the configured icons (visible on hover). --- ## Fixed ### Nametag Mount Reliability on Teleport and World Change Nametags are now hidden immediately on teleport and world change, then re-mounted after a 20-tick delay. Previously, the mount packet could arrive before the client had finished processing the teleport, causing the nametag to appear detached or invisible until the next full update cycle. The same 20-tick delay is applied on profile refresh (e.g. when a nickname plugin changes a player's game profile), where the previous 5-tick delay was too short for some clients. ### See-Through Toggle on Crouch When `support-crouching` is enabled, toggling crouch now properly sets `see-through` to `false` while sneaking and restores it to the config value when standing. Previously, the see-through flag was not updated during the crouch transition. ### Text Change Updates Use `update()` Instead of `hide()`/`show()` When nametag text changes for a viewer who is already seeing the nametag, Sign now sends a metadata update and remount packet (`update()`) instead of despawning and respawning the entire entity (`hide()` then `show()`). This eliminates a brief flicker that was visible on every text change. ### Config Comment Wording Fixed the `see-through` config comment to accurately describe the option. --- ## Changed - **Profile refresh re-mount delay** increased from 5 ticks to 20 ticks for better client compatibility. - **bStats metrics** added (plugin ID `30001`) for anonymous usage statistics. - **`{prefix}` removed** from `MessageHelper` — it was unused after the v1.0.0 command rewrite. --- ## Developer Notes ### Voice State from the API The voice chat hooks are internal to Sign and not exposed through Sign-API. If you need to read a player's voice state from your own plugin, depend on the Simple Voice Chat API or Amplifier API directly. However, the `{voice}` placeholder is fully supported in override lines set through the API: ```java INametag nametag = SignAPI.getNametagManager().get(player); nametag.setLines(List.of("{voice} <yellow>Custom Tag")); ``` The placeholder resolves at display time, so the icon updates live without needing to re-call `setLines()`. ### Per-Viewer Override Performance With the new per-viewer dirty caching, calling `setLines(viewer, lines)` with the same lines repeatedly no longer causes packet spam. The cache comparison happens before any packets are built, so the cost of a no-op update is negligible. ```java // Safe to call every tick — packets only sent when resolved output changes nametag.setLines(viewer, List.of("<gray>{player}", "<gold>" + getDynamicValue(player))); ```