Voice chat runs on its own encrypted channel, relayed by the game server
Status: Accepted (2026-11-06), not implemented yet
Context
Voice is a platform service, not a sound or audio stream that a game mode plays. Voice media needs low latency, protection against eavesdropping on the path, backpressure independent of gameplay, and recipients chosen by the server. None of the existing channels fits. Gameplay UDP is not encrypted, and its reliable lane retransmits and orders events. The planned player-bound TLS control channel carries bounded requests and replies, not continuous media.
Decision
- A separate voice plane. Each client opens one encrypted QUIC connection to the same game server process. Opus media travels as QUIC DATAGRAMs without transport retransmission; one reliable stream carries only the handshake, configuration and errors. Neither gameplay UDP nor the control channel carries media.
- Bound to the game session. After verifying the server identity, the client obtains a short-lived, single-use
voice_ticketbound to the server identity, the current game session and itsplayer_token(ServerHello). The ticket is issued over the player-bound control channel, so voice depends on that channel. Disconnect, kick or reconnect revokes the old voice connection; a game session has at most one active voice connection. - Relayed, not mixed. The server checks each envelope and forwards the original compressed Opus frame to the chosen recipients. It does not decode, mix or log media. In the outgoing envelope the server itself sets the speaker's id and
player_token; the sender cannot choose them. Encryption is client-to-server, not end-to-end: the server can technically see the Opus payload, which is why there is no built-in recording and no Lua access to media. - Authority. The client owns the input device, push-to-talk, local volume and per-player mute. The server owns the right to transmit and the choice of recipients, based on player lifecycle, world scope, position and the game mode's policy. Lua sets only long-lived policy: no Lua callback runs per media frame, and no resource builds a recipient list per frame.
- First scope: proximity push-to-talk. Living, spawned players the server allows may talk; only players in the same world and interior within a server-side radius hear them, with hysteresis at the edge so a listener on the boundary does not flicker in and out. The client fades and positions each speaker in 3D. Open mic, radio and team channels, occlusion by walls, recording, transcription and media moderation APIs are outside this scope.
- Codec. Opus, mono, 48 kHz, with a base frame of 20 ms.
- Player consent. Neither the server nor a resource can open the microphone or start transmission. Push-to-talk is a core key binding (ADR 0037). The client shows when the player is transmitting and lets the player choose devices, set the overall voice volume and mute individual players. Resource pages keep having no microphone access (ADR 0049).
- Overload and untrusted input. One Opus frame fits one datagram without fragmentation. The server validates the voice session, its generation, sequence and timestamp, flags, size, the negotiated codec profile and the rate; stale, duplicate, oversized and flooding frames are dropped. Queues are bounded: when a receiver falls behind, old frames are lost instead of latency and memory growing. Discontinuous transmission, forward error correction and bitrate adapt to the measured state of the connection.
Consequences
- Before the libraries are fixed, a proof of concept must show interoperability between the client's QUIC stack and Quinn on the server: ALPN, key pinning and trust on first use, bidirectional DATAGRAMs, loss and reconnect. If the proof fails, the QUIC implementation changes, not the transport boundary.
- The voice wire gets a public specification of its own; the protocol reference stays the format of gameplay UDP.
- The server publishes an immutable, latest-wins routing snapshot to the voice worker; I/O callbacks never enter Lua and never block the server tick. Resource-owned radio and team channels are designed separately after proximity voice.
- Metrics cover only the connection, bytes and packets, loss, late and dropped frames and latency; payload and decoded audio are never logged.
- The new port, certificate and limits appear in
server.tomlonly together with the working implementation.
Alternatives considered
- Gameplay UDP or its reliable lane. No protection against an observer on the path; a shared queue and retransmission tie media to gameplay. Rejected.
- The TLS control channel. TCP brings head-of-line blocking and mixes bounded request/reply with continuous media. Rejected; the channel only issues the ticket.
- Peer-to-peer mesh. Exposes players' addresses, requires NAT traversal and multiplies the speaker's upload. Rejected.
- Decoding and mixing on the server. Costs CPU, removes per-speaker spatialization and mute, and widens the server's access to content. Rejected until a real consumer needs it.
- An external voice service or a separate microservice. Adds an account, a deployment and someone else's lifecycle. Rejected; voice lives as a separate module and listener in the same server process.
- Full WebRTC with a selective forwarding unit. Provides ready media mechanisms but brings ICE/STUN/TURN, SDP and a large stack without a browser or peer-to-peer consumer. Rejected for the first scope; a separate audio-processing module may come later for open mic.
