Voice protocol
WebSocket audio framing, capability negotiation, PLC, spatial audio, speaker roster and soundboard wire protocol
這一頁還沒有翻譯,以下顯示英文原文。
Voice, the player roster, speaker snapshots and status between client and server all travel over one WebSocket connection. The protocol is defined in common/core and shared by the client, the server mods and the Paper plugin. Wire format changes must be verified on both ends and cover mixed old/new versions, invalid data, reconnects and rollback.
Connection and capability negotiation
After the handshake delivers the base URL and a one-time token, the client connects to <public-url>/voice?token=...&uuid=.... The WebSocket uses a 10-second ping/pong check so a dead connection is detected even when nobody is talking.
Every new capability is requested with a custom header on the HTTP WebSocket upgrade request, and is negotiated only when the server echoes the same header in the upgrade response. Without negotiation both ends keep the old behaviour, so old and new versions can be mixed.
| Header | Requires | Adds |
|---|---|---|
X-SocketSVC-Audio: 2 | — | v2 audio framing (stream ID, sequence) and PLC |
X-SocketSVC-Status: 1 | — (independent of audio v2) | Deafen state sync |
X-SocketSVC-Spatial: 1 | audio v2 | S2C kind byte and locational sources (speakers) |
X-SocketSVC-Speakers: 1 | audio v2 + spatial v1 | Named speaker roster text snapshots |
X-SocketSVC-Soundboard: 2 | audio v2 + spatial v1 | C2S kind byte and the soundboard channel |
X-SocketSVC-Groups: 2 | status v1, and groups.enabled in the server config | Voice group membership, invitations and their routing rules |
X-SocketSVC-Group-Edit: 1 | groups v2 | Renaming, passwords, persistence, kicking and handing on a group (RENAME, PASSWORD, PERSIST, KICK, OWNER) |
| (none) | status v1 | SSVC_MUTE_V1 — server mutes; no header, because nothing depends on the line arriving |
| (none) | status v1 | SSVC_RANGE_V1 / SSVC_RANGES_V1 — whisper and shout; no header, same reason |
Reverse proxies
Reverse proxies or relays in between must not strip the custom upgrade request/response headers, or both ends fall back to the older protocol.
Audio framing
The base v1/v2 layouts below remain unchanged for sockets without spatial support. The optional spatial extension wraps S2C v2 frames only after its separate capability is negotiated.
Negotiation
The HTTP WebSocket upgrade request advertises X-SocketSVC-Audio: 2. A supporting server echoes that header in the upgrade response. Without that response, the new client sends and reads v1, even though it requested v2. No additional C2S text message is sent to legacy servers (they reject text). Servers select each socket's format from its upgrade request and translate separately for each recipient.
Frame layouts
| Direction | v1 | v2 |
|---|---|---|
| Client to server | Opus | stream ID: int64, sequence: int64, Opus |
| Server to client | UUID: 16 bytes, gain: uint8, dx/dz: float32, Opus | Same 25-byte routing header, stream ID: int64, sequence: int64, Opus |
- All numbers use network byte order.
- Opus remains one 20 ms, 48 kHz mono frame, 1–1275 bytes.
- v2 C2S maximum is 1291 bytes, S2C maximum 1316 bytes. The wire decoder allows the larger v2 limit, but the application independently enforces each socket's negotiated header and the unchanged 1275-byte Opus bound.
Streams and sequences
- Stream IDs are positive random longs; sequences start at zero, increase before bounded outgoing enqueue, and must be nonnegative and less than
Long.MAX_VALUE. - A new PTT/VAD speech segment or capture restart creates a fresh stream and resets the encoder.
- The server maps it to a recipient-specific stream ID and preserves sequence values.
- Filtering a packet for range/world/spectator policy invalidates that recipient's stream, so re-entry is not treated as loss.
- Legacy speakers relayed to a v2 listener have stream/sequence
0/0, disabling sequence inference.
Packet loss concealment (PLC)
The receiving jitter queue still waits for actual packets. PLC is only requested when a later same-stream sequence proves an intervening gap in ordered WebSocket delivery:
- It emits one 960-sample frame per missing packet, at most three (60 ms), then decodes the real successor once.
- Empty input, normal speech restarts and legacy audio do not trigger PLC.
- More than three missing frames causes decoder reset and resumption with real audio, not unbounded generated speech.
- Duplicated or older same-stream sequences do not replay.
- Missing final packets without a successor cannot be detected by this scheme. It does not hide arbitrary TCP stalls, reconstruct missing words, or implement FEC.
Developer HUD
Shows v1/v2, playout sequence gaps, successful PLC frames, and discarded stale/duplicate frames. Gaps include application-side drops and playback queue overflow; they are not TCP network packet-loss measurements. Counts reset with the existing connection diagnostics. Legacy sources on a v2 connection have no gap accounting. Malicious sequence jumps cannot allocate per-missing-frame memory or cause unbounded concealment loops; the gap counter saturates.
Rollout and rollback
- Update either end first; mixed versions continue using legacy payloads on legacy sockets. Update both ends for sender sequence tracking and PLC.
- Rolling either end back restores the v1 path on reconnect. No configuration or saved-data migration is needed; do not strip the custom upgrade response header in an intermediate proxy.
- Tests cover both client negotiation paths, mixed server relays, maximum sizes, invalid metadata, route/speech restarts, duplicates, bounded PLC and native decoding after compensation. Cross-region proxy and acoustic validation still requires actual players.
Listener-state extension
X-SocketSVC-Status: 1 is requested and acknowledged independently of audio v2.
- Only after acknowledgment does the client send
SSVC_DEAFEN_V1:0or:1. An older server, including one supporting audio v2 only, receives no new text controls. - The latest local toggle is retained through backpressure and coalesced to at most one send per second.
- The server accepts only authenticated, negotiated clients and exact messages.
- No text control message disconnects a client for arriving too often. Deafen toggles and tier changes are applied whatever the rate — each is one small piece of state — and the roster republish behind them is rationed instead, to once per 250 ms per connection, with anything deferred picked up by the next flush so no change is lost. The only remaining guard is a flood guard at 60 text messages per second per socket (plus a second of credit), closing with 4008: far above what a held key can produce, and reaching it means the client is not being operated by a person.
Supporting listeners receive an extra SSVC_DEAFENED_V1 row followed by a comma-separated UUID list in each roster snapshot; ordinary UUID/name rows stay unchanged. Legacy clients receive the original roster without this row. Status changes publish a new snapshot; only the latest snapshot is queued. Disconnect and quit remove state; reconnect resends the retained local deafen setting.
The server skips audio to deafened listeners and resets their routing streams on state changes so un-deafening cannot trigger PLC for intentionally omitted audio. Audio already queued before a toggle may still arrive; client-side deafen remains immediate. No microphone state or mute payload is introduced. Both ends need the extension for remote speaker-off icons; rolling either end back safely disables that synchronization without changing audio framing.
Spatial audio extension v1
Request and echo X-SocketSVC-Spatial: 1 together with X-SocketSVC-Audio: 2. The client activates spatial framing only when both response headers match. Without this capability, the server sends ordinary proximity frames only; it never disguises a speaker channel as a player on legacy clients. C2S is unchanged.
Every S2C binary frame on a spatial socket begins with one kind byte:
| Kind | Remaining payload |
|---|---|
| 0 (player) | Existing v2 S2C frame: player UUID, gain, dx, dz, stream, sequence, Opus |
| 1 (location) | channel UUID (16), gain (1), dx (4), dz (4), stream (8), sequence (8), origin UUID (16), Opus |
| 2 (soundboard) | Requires the separate soundboard capability; see Soundboard |
Field rules
- Sizes are bytes. Network byte order applies. Maximum kind-0 size is 1317; maximum kind-1 size is 1333 (58-byte header plus 1275-byte Opus).
- A zero origin UUID means server audio; otherwise it identifies the microphone player for individual playback volume/mute and speaking status.
- Decode/jitter state is keyed by channel UUID, never origin UUID.
- Kind-1 stream must be positive, sequence must be in [0,
Long.MAX_VALUE), and dx/dz must be finite. Unknown kinds and invalid lengths/metadata are discarded. - UUIDs identify sources, not authentication claims from clients: only the authenticated server constructs S2C spatial packets.
Routing
- Gain includes channel volume and linear attenuation by 3D distance. dx/dz are relative to each listener; the client calculates stereo pan using its live yaw.
- Only same-world, in-range, permitted, non-deafened listeners receive a channel.
- Microphone relays preserve spectator restrictions and omit the origin player.
- Listener stream IDs change on range/filter re-entry, source change and restart.
- Microphone v2 sequence numbers preserve input gaps; legacy microphone packets receive a channel sequence without claiming to measure upstream loss.
Closing a channel
Closing a channel removes server queued packets and route state. There is no remote immediate-stop message: already delivered audio drains through bounded client/device buffers, and idle client decoders expire after five seconds. New source types such as playlists can use the same PCM/Opus entry points; extensions requiring new client metadata must negotiate a new capability/version. For API usage, see Spatial audio API.
Named speaker roster v1
Request/echo X-SocketSVC-Speakers: 1 in addition to audio v2 and spatial v1. This capability adds S2C text snapshots only; binary spatial audio is unchanged. Clients activate it only when all three response headers match. Unsupported clients receive no speaker snapshots and continue existing spatial/proximity audio.
Snapshot format
Each snapshot starts with SSVC_SPEAKERS_V1\n, then zero to 32 rows of:
channel-UUID SPACE base64(UTF-8-display-name) LF- Names contain 1..64 UTF-16 code units of plain text (no control, format, section-sign or unpaired surrogate characters; not blank).
- Messages are bounded to 10000 characters.
- Invalid UTF-8, duplicate IDs, invalid names/rows or excessive counts reject the whole snapshot; a valid empty snapshot clears the displayed list.
- Names never identify decoders.
Sync behaviour
- The server polls current channel definitions with roster flushing (10 ticks), filters recipient allowlists, and sends only changed snapshots when the socket has no buffered output. Intermediate changes coalesce while backpressured.
- Close/owner-disable removes channels from the next snapshot; reconnect gets a fresh snapshot.
- Client disconnect clears visible entries but retains local preferences keyed by endpoint namespace plus channel UUID.
- Range, world and deafen do not hide defined speakers from allowed recipients.
Speaker volume is a client playback multiplier independent of the wire gain. For relayed microphones it multiplies the existing origin-player preference; normal proximity voice is unaffected. Persisted IDs allow server plugins to recreate speakers after restart without losing listeners' preferences.
Soundboard
The soundboard lets a player trigger a local sound clip that is transmitted to nearby players as its own audio channel, independent of their microphone: muting the mic does not silence it, but a listener's own deafen state still blocks it, same as it blocks mic audio. It is gated server-side by a permission node (socketsvc.soundboard) so it can be blocked per player/group regardless of what the client does. For the player-side guide, see Soundboard.
Negotiation
Request/echo X-SocketSVC-Soundboard: 2 together with X-SocketSVC-Spatial: 1 and X-SocketSVC-Audio: 2 (the same gating chain the named speaker roster uses). Without a match on all three, both ends behave as before: no kind byte on C2S, and S2C spatial framing (if negotiated on its own) is unchanged. Piggybacking on the Spatial capability instead of introducing a second, independently negotiable kind-byte scheme on S2C avoids ever needing two stacked kind-byte prefixes on the same frame.
- Soundboard v2 permits up to ten concurrent stream IDs per sender.
- The server still accepts v1 clients with their original single-stream/cooldown behaviour.
- A v2 client falls back to the mic channel when connected to an older server that cannot negotiate v2.
C2S
The base v1/v2 C2S layouts never had a kind byte before. On a Soundboard-negotiated socket, every binary frame begins with one kind byte, then the existing v2 C2S layout (stream ID: int64, sequence: int64, Opus) unchanged:
| Kind | Meaning |
|---|---|
| 0 | Microphone (what every C2S frame already meant before this capability existed) |
| 1 | Soundboard |
S2C
Extends the existing spatial kind-byte enum with one new value, reusing the identical kind-0/player byte shape rather than inventing a new header:
| Kind | Remaining payload |
|---|---|
| 2 (soundboard) | Same as kind 0 (player): player UUID, gain, dx, dz, stream, sequence, Opus |
A kind-2 frame is routed and gain-computed exactly like a kind-0 player frame (world/distance/visibility/spectator/deafen; see Server routing below). The only difference is the tag, which tells the client to mix it through the listener's soundboard volume/mute for that player instead of their mic volume/mute.
Permission
- The hot-reloadable server setting
soundboard-enableddefaults totrue. When disabled, the server continues negotiating the dedicated protocol but drops every soundboard frame, preventing clients from falling back through the microphone channel. Per-player permission checks apply only when the global switch is enabled. socketsvc.soundboard,default: true, has the same shape assocketsvc.speak/socketsvc.listen(plugin.yml): a cached per-tick boolean inPlayerLocationCache(canSoundboard, populated viaplayer.hasPermission("socketsvc.soundboard")inrefreshOne, fail-open for a not-yet-ticked player), checked inVoiceServerbefore a soundboard frame is routed anywhere.- A denied trigger is dropped silently server-side (no disconnect) with a throttled action-bar notice, mirroring
notifyPermissionDenied's existing 5-second-per-reason cooldown. - Listener-side, deafen already blocks all incoming audio via the existing
canListengate reused unchanged in the soundboard routing path, so "deafen still blocks soundboard" needs no new code.
For all permission nodes, see Permissions.
Server routing
VoiceServer.onMessage reads the new kind byte first (only on Soundboard-negotiated sockets); kind 0 goes through the unchanged mic path. Kind 1 goes through a separate routing branch that clones the mic loop's gain computation (same senderLoc/otherLoc, same world/distance/visibility/spectator checks via settings.gain(...)), gated by canSoundboard(sender) instead of canSpeak, and tags outgoing frames as S2C kind 2 instead of kind 0.
Because gain is recomputed fresh from the sender's current location on every frame (exactly like mic audio), a soundboard "channel" naturally follows the triggering player around without the fixed-position bookkeeping LocationalAudioChannel (common/core/src/main/java/com/socketsvc/api/) would need. That speaker API's channels have a static SpatialPosition that doesn't auto-track a moving player and accept only one input source at a time, so soundboard audio does not ride it.
Trigger-rate guard
- Mic and soundboard have separate
AudioRateLimitinstances, so normal simultaneous mic and soundboard traffic cannot drain one shared budget. - V1 retains its 60/s frame budget and 300 ms trigger-start cooldown.
- V2 allows an aggregate budget of ten 60/s streams and tracks each active stream independently in routing and outbound queues; the client replaces its oldest session before starting an eleventh.
- The 1275-byte frame cap applies to both versions.
Groups extension v2
Request and echo X-SocketSVC-Groups: 2 in addition to X-SocketSVC-Status: 1, whose text channel these messages travel on. See Voice groups for what the feature does; this section is the wire.
Negotiation is the switch
The server echoes the header only while groups.enabled is true. That is deliberate and is the only capability here whose negotiation depends on a config value rather than on what both ends can parse: it lets a client tell "this server does not do groups" apart from "this server has not answered yet" without guessing, and show one sentence instead of a menu whose every request would be dropped.
Enforcement does not depend on the client honouring that. A SSVC_GROUP_V1: message on a connection that did not negotiate the capability — or on one that did, after groups were switched off — is closed with 1003 unsupported control message, exactly like any other unsupported text.
C2S requests
One line each, all SSVC_GROUP_V1:<VERB> ..., at most 1024 characters. A request arriving within 200 ms of the previous one from the same connection is dropped silently; unlike the deafen path, an over-eager client loses the request rather than the connection, because a create form double-clicked should not cost someone their voice chat.
| Message | Meaning |
|---|---|
SSVC_GROUP_V1:LIST | Ask for a fresh snapshot |
SSVC_GROUP_V1:CREATE <type> <persistent 0|1> <base64 name> <base64 password|-> | Create a group and join it |
SSVC_GROUP_V1:JOIN <group-UUID> <base64 password|-> | Join a group |
SSVC_GROUP_V1:LEAVE | Leave whichever group this player is in |
SSVC_GROUP_V1:REMOVE <group-UUID> | Disband a group (its creator, or a group administrator) |
SSVC_GROUP_V1:TYPE <group-UUID> <isolated|normal|open> | Change a live group's type (same two) |
SSVC_GROUP_V1:MOVE <player-UUID> <group-UUID|-> | Put another player into a group, or take them out of whichever they are in |
SSVC_GROUP_V1:INVITE <player-UUID> | Invite another connected player into the sender's group |
SSVC_GROUP_V1:ACCEPT <group-UUID> | Take up an invitation; the group's password is bypassed |
SSVC_GROUP_V1:DECLINE <group-UUID> | Turn one down |
SSVC_GROUP_V1:RENAME <group-UUID> <base64 name> | Rename a group (its owner, or a group administrator) |
SSVC_GROUP_V1:PASSWORD <group-UUID> <base64 password|-> | Set or (with -) remove its password (same two) |
SSVC_GROUP_V1:PERSIST <group-UUID> <0|1> | Whether it outlives its last member (same two); turning it off on an empty group disbands it |
SSVC_GROUP_V1:KICK <group-UUID> <player-UUID> | Take a member out of that group (same two) |
SSVC_GROUP_V1:OWNER <group-UUID> <player-UUID> | Hand the group to another of its members (same two) |
INVITE is about somebody else but is not administrator-only, because nothing happens to them until their own ACCEPT — the target must simply be connected to voice, since the invitation is delivered in their snapshot and there is nowhere else to put one. Any member may send it, not only the owner. A second invitation to the same group is refused rather than stacked, and one inbox holds at most eight.
MOVE is the only request that both acts on somebody else and acts immediately, so it requires socketsvc.groups.admin with no owner exemption — a group's creator may run their own group, not move people around the server's. It is what the moderator-facing player list in the client menu is built on.
The last five are the group-edit extension: a client sends them only after the server echoed X-SocketSVC-Group-Edit: 1. An owner may do all five to their own group, because none of them can pull anybody in. A PASSWORD whose base64 is not valid UTF-8 is refused outright rather than read as -, which would have opened the group instead of locking it.
- is the marker for an omitted optional field, so every row keeps a fixed column count; base64 that does not decode as valid UTF-8 is treated as absent rather than silently becoming a different password. Names are 1–24 characters and passwords 1–64, both excluding control, format and section-sign characters and unpaired surrogates — the same rule as speaker names. Anything that does not parse is an unsupported control message.
S2C snapshot
SSVC_GROUPS_V1
C <canUse 0|1> <canCreate 0|1> <canAdmin 0|1> <canInvite 0|1>
I <group-UUID> <from-UUID> <secondsLeft>
G <group-UUID> <type> <hasPassword 0|1> <persistent 0|1> <owner-UUID|-> <base64 name>
M <group-UUID> <member-UUID>The whole-server view, bounded to 262144 characters, pushed whenever membership changes and to a connection that has just opened. The group and member rows are encoded once per change; the C and I lines differ per connection, and have to: capabilities come from permission nodes that are the player's own, and an inbox is nobody else's business.
I rows are the invitations waiting for this player, carrying seconds remaining rather than a deadline because the reader's clock is its own. The client counts down from what it was told and stops offering the invitation when it runs out, which self-corrects a snapshot that is not re-sent before the server prunes it. An invitation being sent, answered or expiring bumps the same revision a membership change does, so it is pushed on the same path. A snapshot with no C line, an unknown row type, a member of a group not in the same message, a duplicate group or a name that fails validation is rejected whole rather than in part.
The password is never on the wire in either direction after the join that carried it: G carries only whether there is one. The server stores a salted SHA-256 hash and compares in constant time.
Snapshots follow the same "newest only, never a backlog" rule as the player roster: while a socket has buffered output the pending snapshot is replaced rather than queued, and it is flushed by the same pass.
S2C result
SSVC_GROUP_RESULT_V1 <VERB> <CODE>The outcome of one request from this connection — OK, or one of DISABLED, NO_PERMISSION, INVALID_NAME, INVALID_PASSWORD, NAME_TAKEN, TOO_MANY_GROUPS, NOT_FOUND, WRONG_PASSWORD, GROUP_FULL, ALREADY_IN_GROUP, NOT_IN_GROUP, TYPE_NOT_ALLOWED, PASSWORDS_NOT_ALLOWED, PERSISTENT_NOT_ALLOWED. A code travels rather than a sentence, because the server does not know what language the player is reading; the client translates it. A client that does not recognise a code ignores the line instead of failing, so codes can be added later.
Routing
Group membership is consulted before the distance rules, once per speaker/listener pair per frame, and decides one of three things:
| Membership | Result |
|---|---|
| Same group | Gain 255, dx/dz both zero — no distance, world, visibility or spectator rule applies |
| Different groups | Silent, whatever either type is |
| One in a group, one in none | The group's type decides the direction; if audible, the ordinary proximity rules then apply unchanged |
| Neither in a group | Exactly the pre-groups behaviour |
The zeroed dx/dz is what makes group audio non-positional: the client pans it centred rather than computing a bearing against a location the listener may not even share a world with. Deafen and the socketsvc.speak/socketsvc.listen nodes are checked as they always were; the soundboard and microphone paths share one routing method so a clip cannot outrun its player's group, and a spatial speaker carrying a player's microphone is filtered by that player's group too.
Server mute v1
No capability header of its own. SSVC_MUTE_V1 rides the listener-state extension's existing text channel, and a client that does not understand the line ignores it, exactly as it already ignores any unrecognised text message. A header would have bought nothing: every server that can send this can already negotiate the status extension, and the mute itself is enforced at the relay whether or not the line is ever read.
It exists because a mute is otherwise invisible from inside the game. The microphone still opens, the level meter still moves and the speaking indicator still lights, so a muted player cannot tell moderation from a broken microphone — which makes it unreportable for them and undiagnosable for whoever they tell.
S2C
Sent on connect and whenever the state behind it changes:
SSVC_MUTE_V1 <0 none|1 this player|2 server-wide> <seconds remaining|-> <base64 reason|->| Field | Meaning |
|---|---|
| Kind | 0 not muted, 1 an individual mute against this player, 2 the server-wide mute (mute-all) |
| Seconds remaining | - for a mute that does not lapse on its own, and for 0 and 2 |
| Reason | Base64 UTF-8, - for none. Base64 that does not decode as valid text is treated as absent rather than silently becoming different text — the same rule group passwords follow |
A player holding socketsvc.mute.exempt is sent 0 while mute-all is on, because for them it is not in force.
Why seconds, and why the state is compared rather than the line
The countdown travels as seconds remaining rather than as the wall-clock time the mute lapses, because the two machines do not share a clock: an absolute timestamp read against a client a few minutes out shows a mute that has already expired, or one that never will. The client anchors it to its own clock on arrival.
Whether to send at all is decided on the state — kind, expiry and reason — and not on the encoded line, which carries that countdown and so differs every time it is built. Keyed on the line, "has this client been told?" would answer no on every flush: twice a second, per connected client, for the whole length of every timed mute.
The same comparison is what publishes a mute lapsing on its own, so nothing has to sweep it. The state goes from "muted until t" to "not muted" the first time it is read past its expiry, and the client is told on that pass.
Routing
A mute is checked once per inbound frame per speaker, before the socketsvc.speak node, on both the microphone and soundboard paths. Everything a silenced player would have transmitted is dropped there: microphone, instrument, group audio and soundboard clips alike. Nothing about what they receive changes, and they do not read as speaking to anyone — a nameplate lighting up for audio nobody gets would say the mute is not working.
The soundboard path is checked separately and deliberately, rather than being folded into canSoundboard: it is a second stream with its own permission node, and a mute enforced only on the microphone would leave the first thing a silenced player reaches for working.
Whisper and shout
No capability header, for the same reason server mutes have none: both ride the listener-state extension's text channel, and either end that does not understand a line ignores it. A client that predates whisper and shout never sends a tier and is heard normally — which is what it was already doing.
C2S — the tier this client is speaking at
SSVC_RANGE_V1:<whisper|normal|shout>Sent when the tier changes, at most one per 100 ms per socket. Excess is dropped, not disconnected: a press and a release are two messages, and a player using a push-to-whisper key rapidly is doing something entirely ordinary.
A tier the server has switched off is accepted and recorded rather than refused — the gain rule scales a disabled tier by 1, so the speaker is heard at ordinary volume. Refusing would close the socket of a player whose only mistake is running a client one reload behind the server, and going silent would be indistinguishable from a broken microphone. A word that is not one of the three closes with 1003, like any other unsupported control message.
S2C — which tiers this server offers
SSVC_RANGES_V1 <whisper 0|1> <shout 0|1>Sent on connect and whenever the answer changes, and this half is what keeps the feature honest. Without it a player holds the whisper key on a server that has whisper off, watches their own HUD change, and is heard at completely ordinary volume — a disagreement between two machines that both think they are right, which nothing anywhere would report.
A client that has not been told assumes neither tier exists, so a tier is never offered on the strength of a line that did not arrive or did not parse.
S2C — who else is whispering or shouting
Two extra rows on the player roster, beside the existing SSVC_DEAFENED_V1:
SSVC_WHISPERING_V1 <uuid,uuid,...>
SSVC_SHOUTING_V1 <uuid,uuid,...>Separate rows rather than one row of pairs, because they are read exactly the way the deafened row already is, and an older client ignores a row it does not recognise. They exist so a client can draw another player's nameplate icon smaller for a whisper and larger for a shout, and they are published on the same path a deafen toggle takes.
A tier the server has switched off never appears on them — a client one reload ahead of the operator would otherwise draw somebody as whispering while the relay routes them at ordinary range, which is the same two-machines-disagreeing fault the offer line prevents, pointed at a third party.
Routing
The tier is a multiplier on the range already in force where the speaker is standing, applied after the per-world and per-region rules and after the two ends are compared. So:
- a shout cannot reach into a place whose range is 0 — silence scaled by anything is still silence;
- an arena that carries 16 blocks gets a shout of 32, not the server-wide 96.
It changes distance and nothing else. World, visibility, spectator and deafen rules apply exactly as they do to an ordinary frame, and a soundboard clip is always normal — a tier is how loudly somebody is talking, and a clip is not speech.