From 155195ef0625e29705e7766f1c0387878abc682a Mon Sep 17 00:00:00 2001 From: SchattenundLicht Date: Mon, 17 Aug 2026 18:15:55 +0200 Subject: [PATCH] plan.md aktualisiert --- plan.md | 656 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 463 insertions(+), 193 deletions(-) diff --git a/plan.md b/plan.md index d0e5688..104bcc3 100644 --- a/plan.md +++ b/plan.md @@ -1,12 +1,12 @@ --- name: Voice Architecture Plan -overview: "Development-ready SDK/ServerLib plan (Option B): custom Conan Voice Server on TeamSpeak 3 ServerLib + custom ClientLib app, with server-side whisper-list routing. Includes concrete SDK function names, init/shutdown order, data-flow, threading, and per-module API mapping. Isolated on branch ts3-Testing." +overview: "Development-ready English plan for Option B: custom Conan Voice Server (ServerLib) + custom ClientLib app, with function-level APIs, call order, data packing, VoiceRouter algorithm, threading, and phases. Branch ts3-Testing. See also vergleichs.md." todos: - id: create-branch content: "Create branch ts3-Testing off current branch; do not touch tsmain / V8 plugin" status: pending - id: sdk-license-gate - content: "Phase 0: get TS3 SDK (ClientLib+ServerLib) + evaluation license; build official examples; confirm the API names/signatures flagged VERIFY in this plan against serverlib.h/clientlib.h" + content: "Phase 0: get TS3 SDK (ClientLib+ServerLib) + evaluation license; build official examples; freeze every [VERIFY] name against serverlib.h/clientlib.h" status: pending - id: serverlib-bootstrap content: "Phase 1: conan-voice-server bootstrap (initServerLib -> createVirtualServer -> loop -> stopVirtualServer -> destroyServerLib)" @@ -15,16 +15,16 @@ todos: content: "Phase 2: custom ClientLib app connects (initClientLib -> spawnNewServerConnectionHandler -> createIdentity -> open devices -> startConnection)" status: pending - id: metadata-transport - content: "Phase 2/3: game-data transport via CLIENT_META_DATA (+ flushClientSelfUpdates); confirm server read + change event" + content: "Phase 2/3: game-data via CLIENT_META_DATA (+ flushClientSelfUpdates); confirm server read + change event" status: pending - id: routing-poc - content: "Phase 3: server whisper-list routing (ts3server_setClientWhisperList): prove A hears B, not C" + content: "Phase 3: ts3server_setClientWhisperList proves A hears B, not C" status: pending - id: threed-voice - content: "Phase 4/5: 3D via ts3client 3D listener/channel attributes fed by server positions" + content: "Phase 4/5: 3D via ClientLib listener/channel attributes fed by server positions" status: pending - id: write-en-doc - content: "Mirror this plan into docs/ as the living design doc once Phase 0 confirms API names" + content: "Keep vergleichs.md + this plan in sync; mirror into docs/ after Phase 0 freezes APIs" status: pending - id: keep-v8-untouched content: "Do not modify REWRITE_PLAN_V8.md / REWRITE_PLAN.md or the V8 plugin on tsmain" @@ -32,296 +32,566 @@ todos: isProject: false --- -# Conan Voice on TeamSpeak 3 SDK (ClientLib + ServerLib) — Development-Ready Plan +# Conan Voice — TeamSpeak 3 SDK / ServerLib Plan (Option B) -New product, new branch `ts3-Testing`. The V8 retail plugin on `tsmain` stays intact and is reused for *logic* only. +**Audience:** developers who need enough detail to start coding after Phase 0. +**Companion doc:** [`vergleichs.md`](../../vergleichs.md) (Live V8 Plugin SDK vs this plan). +**Branch:** `ts3-Testing` only. Leave `tsmain` / V8 DLL alone. -> Items marked **[VERIFY]** are exact names/signatures to confirm against `serverlib.h` / `clientlib.h` and the bundled API reference during Phase 0. Everything unmarked is confirmed from TeamSpeak docs/examples. +> **[VERIFY]** = confirm exact name/signature against `serverlib.h` / `clientlib.h` during Phase 0. Unmarked items are confirmed from TeamSpeak docs/examples. --- -## 0. The two SDKs (why this is a real switch, not a rename) +## 0. What we are building (and what we are not) -| | Client Plugin SDK (today, V8) | TeamSpeak 3 SDK (this plan) | +### Building + +A **standalone voice product**: + +```text +Conan Voice Client = our app linking TeamSpeak ClientLib +Conan Voice Server = our process hosting TeamSpeak ServerLib +``` + +Users run **our client** (like Mumble). They do **not** run retail TeamSpeak + a plugin. + +### Not building (explicit) + +- No retail TeamSpeak plugin path for this product +- No REST/WebSocket/cloud API between client and voice server +- No second transport provider until Option B is proven +- No Conan code before whisper-routing PoC succeeds + +### Why this is not "the same SDK as V8" + +| | Live V8 | This plan | |---|---|---| -| Header(s) | `ts3_functions.h` (`TS3Functions`) | `clientlib.h` + `serverlib.h` | -| Function family | `ts3plugin_*` callbacks | `ts3client_*` and `ts3server_*` | -| Who hosts voice | retail `ts3server` | **our** `conan-voice-server` process | -| Who runs the client | retail TeamSpeak app loads our DLL | **our** app embeds ClientLib | -| Can decide who hears whom | no (edit local PCM only) | **yes** (`ts3server_setClientWhisperList`) | -| License | free | paid; free evaluation for dev | -| Interop | plugin only talks to retail TS | SDK client only talks to SDK server (`0x0705` otherwise) | +| Product | Client **Plugin** SDK | TeamSpeak 3 SDK (ClientLib + ServerLib) | +| Prefixes | `ts3plugin_*`, `TS3Functions.*` | `ts3client_*`, `ts3server_*` | +| Process | Guest DLL inside retail TS | We own client app + server process | +| Hearability | Local PCM mute/gain | Server `ts3server_setClientWhisperList` | -Proof today is Plugin SDK: `sdk/README.md` line 1 = "TeamSpeak 3 Client Plugin SDK"; repo has `ts3plugin_*`, no `ts3client_*`/`ts3server_*`. - -**Consequence:** the "Conan Voice Client" is a standalone ClientLib app (users run it like the Mumble client), not retail TeamSpeak + plugin. +Proof V8 is Plugin SDK: `sdk/README.md` line 1. Repo has no `ts3client_*` / `ts3server_*`. --- -## 1. Component overview +## 1. System diagram ```mermaid -flowchart LR - subgraph ClientHost [Player PC] - game[Conan Exiles] - prov[IGameDataProvider] - cstate[VoiceClientState] - cadapt[TeamSpeakClientAdapter] - clib[SDK ClientLib] - game --> prov --> cstate --> cadapt --> clib - clib -->|"onEditPlaybackVoiceDataEvent, 3D"| cadapt --> hud[HUD] +flowchart TB + subgraph PlayerPC [Player PC] + Game[Conan Exiles] + Provider[IGameDataProvider] + CState[VoiceClientState] + CAdapt[TeamSpeakClientAdapter] + CLib[SDK ClientLib] + HUD[HUD] + Effects[AudioEffects PCM] + Game --> Provider --> CState --> CAdapt --> CLib + CLib --> Effects + CLib --> HUD end - subgraph ServerHost [Voice server] - slib[SDK ServerLib] - sadapt[TeamSpeakServerAdapter] - pm[PlayerManager] - prox[ProximityEngine] - router[VoiceRouter] - slib --> sadapt --> pm --> prox --> router --> sadapt --> slib + + subgraph VoiceHost [Voice server host] + SLib[SDK ServerLib] + SAdapt[TeamSpeakServerAdapter] + PM[PlayerManager] + Prox[ProximityEngine] + Router[VoiceRouter] + Auth[Auth] + Radio[RadioManager] + Whisper[WhisperManager] + SLib --> SAdapt + SAdapt --> PM + PM --> Prox + Prox --> Router + Auth --> PM + Radio --> Router + Whisper --> Router + Router --> SAdapt end - clib <-->|"SDK voice + CLIENT_META_DATA"| slib + + CLib <-->|"voice packets + CLIENT_META_DATA"| SLib ``` +**Ownership rules** + +- Only `TeamSpeakClientAdapter` calls `ts3client_*`. +- Only `TeamSpeakServerAdapter` calls `ts3server_*`. +- Game logic never includes `clientlib.h` / `serverlib.h` directly. + --- -## 2. Server process — exact lifecycle +## 2. Server lifecycle — function-by-function -File: `server/src/main.c` (+ `server/src/ts_server_adapter.c`). +**Files:** `server/src/main.c`, `server/src/ts_server_adapter.c` -Startup order (each step checks the return code with `ts3server_getGlobalErrorMessage` on failure): +### 2.1 Startup (strict order) ```text -1. load config (server/config/*.ini) -> our Config module -2. start logger -> our Logging module -3. fill ServerLibFunctions (memset 0, assign callbacks) -4. ts3server_initServerLib(&funcs, LogType_FILE|LogType_CONSOLE, logDir) -5. ts3server_createServerKeyPair(...) OR load stored keypair [VERIFY exact name] -6. ts3server_createVirtualServer(port, "0.0.0.0", name, keyPair, maxClients, &serverID) -7. init Voice backend (PlayerManager, ProximityEngine, VoiceRouter) -8. run event loop (routing tick; SDK callbacks fire on SDK threads) -9. shutdown: ts3server_stopVirtualServer(serverID) - ts3server_destroyServerLib() +main() + │ + ├─ Config_load("server/config/*.ini") + ├─ Log_start(...) + │ + ├─ struct ServerLibFunctions funcs; + │ memset(&funcs, 0, sizeof(funcs)); + │ funcs.onClientConnected = on_client_connected; + │ funcs.onClientDisconnected = on_client_disconnected; + │ funcs.onClientMoved = on_client_moved; + │ funcs.onServerTextMessageEvent = on_server_text; // fallback transport + │ funcs.onClientStartTalkingEvent = on_talk_start; + │ funcs.onClientStopTalkingEvent = on_talk_stop; + │ funcs.onUserLoggingMessageEvent = on_sdk_log; + │ funcs.onAccountingErrorEvent = on_accounting_error; + │ + ├─ err = ts3server_initServerLib(&funcs, LogType_FILE|LogType_CONSOLE, logDir); + │ if (err != ERROR_ok) -> ts3server_getGlobalErrorMessage + abort + │ + ├─ keyPair = load_or_create_keypair() [VERIFY exact create API name] + │ + ├─ err = ts3server_createVirtualServer( + │ port, // e.g. 9987 + │ "0.0.0.0", + │ "Conan Voice", + │ keyPair, + │ maxClients, // from license / config + │ &serverID); + │ + ├─ PlayerManager_init() + ├─ ProximityEngine_init(cellSize, ranges) + ├─ VoiceRouter_init(tickHz=5..10) + │ + └─ run_loop(): + wait SDK events + every 100-200 ms: VoiceRouter_tick() ``` -Confirmed signatures: +### 2.2 Confirmed ServerLib call signatures -- `unsigned int ts3server_initServerLib(const struct ServerLibFunctions* funcs, int usedLogTypes, const char* logFileFolder);` -- `unsigned int ts3server_createVirtualServer(unsigned int serverPort, const char* serverIp, const char* serverName, const char* serverKeyPair, unsigned int serverMaxClients, uint64* result);` -- `unsigned int ts3server_stopVirtualServer(uint64 serverID);` -- `unsigned int ts3server_destroyServerLib(void);` -- Routing: `unsigned int ts3server_setClientWhisperList(uint64 serverID, anyID clientID, const uint64* channelIDs, const anyID* clientIDs);` -- Enumerate: `ts3server_getClientList(serverID, anyID** result)`, `ts3server_getChannelList(serverID, uint64** result)`, free with `ts3server_freeMemory(ptr)`. +```c +unsigned int ts3server_initServerLib( + const struct ServerLibFunctions* functionPointers, + int usedLogTypes, + const char* logFileFolder); -Callbacks we register in `ServerLibFunctions` (assign only what we use): +unsigned int ts3server_createVirtualServer( + unsigned int serverPort, + const char* serverIp, + const char* serverName, + const char* serverKeyPair, + unsigned int serverMaxClients, + uint64* result); -- `onClientConnected(serverID, clientID, channelID, ...)` **[VERIFY arg list]** -> PlayerManager.add -- `onClientDisconnected(serverID, clientID, channelID, ...)` -> PlayerManager.remove + clear whisper list -- `onClientMoved(serverID, clientID, oldCh, newCh, ...)` -- `onServerTextMessageEvent` / `onChannelTextMessageEvent` -> fallback game-data channel (see 5) -- `onClientStartTalkingEvent` / `onClientStopTalkingEvent` -> HUD "who speaks" hints -- `onUserLoggingMessageEvent` -> our logger -- `onAccountingErrorEvent` -> license/slot errors (must handle for eval license) +unsigned int ts3server_stopVirtualServer(uint64 serverID); +unsigned int ts3server_destroyServerLib(void); ---- +unsigned int ts3server_setClientWhisperList( + uint64 serverID, + anyID clientID, + const uint64* channelIDs, // 0-terminated or NULL + const anyID* clientIDs); // 0-terminated or NULL -## 3. Client app — exact lifecycle +unsigned int ts3server_getClientList(uint64 serverID, anyID** result); +unsigned int ts3server_getChannelList(uint64 serverID, uint64** result); +void ts3server_freeMemory(void* pointer); +``` -File: `client/src/main.c` (+ `client/src/ts_client_adapter.c`, `client/src/game_provider_*.c`). +### 2.3 Shutdown order ```text -1. fill ClientUIFunctions (callbacks) -2. ts3client_initClientLib(&clientFuncs, NULL, LogType_FILE, NULL, resourcesPath) [VERIFY arg list] -3. ts3client_spawnNewServerConnectionHandler(0, &scHandlerID) -4. identity: load stored, else ts3client_createIdentity(&id) then persist -5. ts3client_openPlaybackDevice(scHandlerID, "", "") // default device - ts3client_openCaptureDevice(scHandlerID, "", "") -6. ts3client_startConnection(scHandlerID, identity, host, port, - nickname, NULL/*default channel*/, "", serverPassword) -7. wait for onConnectStatusChangeEvent == STATUS_CONNECTION_ESTABLISHED -8. loop: push game data (see 5), set 3D (see 6), render HUD -9. shutdown: ts3client_stopConnection(scHandlerID, "leaving") - ts3client_destroyServerConnectionHandler(scHandlerID) - ts3client_destroyClientLib() +1. VoiceRouter_stop() // stop tick, clear lists optional +2. ts3server_stopVirtualServer(serverID) +3. PlayerManager_clear() +4. ts3server_destroyServerLib() +5. Log_stop() ``` -Confirmed: +### 2.4 Callback → module wiring -- `ts3client_startConnection(scHandlerID, identity, ip, port, nickname, defaultChannelArray, defaultChannelPassword, serverPassword)` -- `ts3client_stopConnection(scHandlerID, quitMessage)` -- `ts3client_getConnectionStatus(scHandlerID, int* result)` -- Callback `onConnectStatusChangeEvent(scHandlerID, newStatus, errorNumber)`; `onServerStopEvent(scHandlerID, shutdownMessage)`. +| SDK callback | Immediate action | Downstream | +|---|---|---| +| `onClientConnected` | enqueue Connect | `PlayerManager.add(tsClientId)` | +| `onClientDisconnected` | enqueue Disconnect | remove player; `setClientWhisperList(..., NULL, NULL)` | +| `onClientMoved` | update channel id | optional lobby vs ingame policy | +| `onAccountingErrorEvent` | log + fail closed | refuse new sessions / shut down if fatal | +| text message (fallback) | parse discrete cmds | radio join/leave, whisper start/stop | -ClientLib callbacks we implement: `onConnectStatusChangeEvent`, `onNewChannelEvent`, `onClientMoveEvent`, `onTalkStatusChangeEvent`, `onServerStopEvent`, `onEditPlaybackVoiceDataEvent` (local audio effects: reverb/lowpass reuse from V8), `onCustom3dRolloffCalculationClientEvent` (custom distance curve). **[VERIFY exact callback names in clientlib.h]** +Keep callbacks short: queue work; do not run O(N) proximity inside the callback. --- -## 4. Data flow: connect -> position -> routing -> voice +## 3. Client lifecycle — function-by-function + +**Files:** `client/src/main.c`, `client/src/ts_client_adapter.c`, `client/src/game_provider_*.c` + +### 3.1 Startup (strict order) + +```text +main() + │ + ├─ fill ClientUIFunctions (callbacks) [VERIFY struct name] + ├─ ts3client_initClientLib(...) [VERIFY full arg list] + ├─ ts3client_spawnNewServerConnectionHandler(0, &scHandlerID) + ├─ identity = load_identity() or ts3client_createIdentity(&identity); persist + ├─ ts3client_openPlaybackDevice(scHandlerID, "", "") + ├─ ts3client_openCaptureDevice(scHandlerID, "", "") + ├─ ts3client_startConnection( + │ scHandlerID, identity, host, port, + │ nickname, NULL /* default channel */, "", serverPassword) + │ + ├─ wait until onConnectStatusChangeEvent == STATUS_CONNECTION_ESTABLISHED + │ (or fail on errorNumber != ERROR_ok) + │ + └─ run_loop(): + GameProvider_poll() ~20-30 Hz + publish CLIENT_META_DATA ~10-20 Hz (hysteresis) + apply 3D listener + speakers ~20 Hz + render HUD +``` + +### 3.2 Confirmed / expected ClientLib calls + +```c +// Connection +ts3client_startConnection(scHandlerID, identity, ip, port, + nickname, defaultChannelArray, + defaultChannelPassword, serverPassword); +ts3client_stopConnection(scHandlerID, quitMessage); +ts3client_getConnectionStatus(scHandlerID, &status); + +// Game state transport +ts3client_setClientSelfVariableAsString(scHandlerID, CLIENT_META_DATA, packed); +ts3client_flushClientSelfUpdates(scHandlerID, NULL); + +// 3D [VERIFY exact 3D names if they differ slightly] +ts3client_systemset3DSettings(scHandlerID, distanceFactor, rolloffScale); +ts3client_systemset3DListenerAttributes(scHandlerID, &pos, &forward, &up); +ts3client_channelset3DAttributes(scHandlerID, remoteClientID, &pos); +``` + +### 3.3 Client callbacks we implement + +| Callback | Responsibility | +|---|---| +| `onConnectStatusChangeEvent` | connected / disconnected / fail | +| `onServerStopEvent` | fail closed + HUD | +| `onClientMoveEvent` | channel visibility | +| `onTalkStatusChangeEvent` | HUD speaker highlight | +| `onEditPlaybackVoiceDataEvent` | local reverb/lowpass only (V8 effect patterns) | +| `onCustom3dRolloffCalculationClientEvent` | Conan voice-range curve | +| `onNewChannelEvent` | channel list bookkeeping | + +### 3.4 Shutdown order + +```text +1. stop game provider thread +2. ts3client_stopConnection(scHandlerID, "leaving") +3. ts3client_destroyServerConnectionHandler(scHandlerID) +4. ts3client_destroyClientLib() +``` + +--- + +## 4. End-to-end sequence (connect → route → hear) ```mermaid sequenceDiagram - participant G as Conan (game) - participant C as Client app (ClientLib) - participant S as Server (ServerLib) + participant G as Conan + participant C as ClientLib app + participant S as ServerLib participant R as VoiceRouter C->>S: ts3client_startConnection S-->>C: onConnectStatusChangeEvent(ESTABLISHED) - S->>S: onClientConnected -> PlayerManager.add(clientID) + S->>S: onClientConnected -> PlayerManager.add - loop ~20 Hz - G->>C: IGameDataProvider.getPlayerState(pos,rot,ids) - C->>C: ts3client_setClientSelfVariableAsString(CLIENT_META_DATA, packed) - C->>S: ts3client_flushClientSelfUpdates - S->>S: metadata change -> PlayerManager.updatePos(clientID,pos) + loop position ~20 Hz + G->>C: getPlayerState(pos,rot,ids,mode) + C->>C: pack CLIENT_META_DATA + C->>S: set + flushClientSelfUpdates + S->>S: PlayerManager.updatePos end - loop ~5-10 Hz (routing tick) - R->>R: spatial hash -> audible set per client - R->>S: ts3server_setClientWhisperList(serverID, A, {}, {B,C}) - R->>S: ts3server_setClientWhisperList(serverID, D, {}, {}) // hears no one + loop routing 5-10 Hz + R->>R: spatial hash + overlays + R->>S: setClientWhisperList(A, {B,C}) + R->>S: setClientWhisperList(D, empty/clear) end - Note over C,S: A speaks -> ClientLib routes A's voice only to B,C (whisper list) - S-->>C: voice packets to B,C only - C->>C: onEditPlaybackVoiceDataEvent -> distance gain/pan/reverb - C->>C: 3D attributes -> spatialization + Note over C,S: A talks - packets only to whisper targets + S-->>C: voice to B and C only + C->>C: 3D attributes + optional PCM effects ``` -Whisper-list routing detail (from TeamSpeak Whisper Lists example): a client with a whisper list bypasses normal channel voice and sends **only** to the listed clients/channels. So the router expresses the full audible matrix as per-speaker client-ID lists. Clearing = `ts3server_setClientWhisperList(serverID, client, NULL, NULL)`. +--- -Caveat to design around: while a client has an active whisper list, its **normal channel talk is disabled**. Therefore ALL proximity/radio/whisper voice must be expressed through whisper lists (do not mix with channel voice for routed players). +## 5. Game-data transport (no external API) + +### 5.1 Primary: `CLIENT_META_DATA` + +| Step | Who | API | +|---|---|---| +| Write | Client | `ts3client_setClientSelfVariableAsString(sc, CLIENT_META_DATA, packed)` | +| Push | Client | `ts3client_flushClientSelfUpdates(sc, NULL)` | +| Read | Server | `ts3server_getClientVariableAsString(...)` **[VERIFY]** | +| Notify | Server | change event if exists; else poll each routing tick **[VERIFY]** | + +Limit: **4096 bytes** string. Keep payload tiny. + +### 5.2 Proposed packed payload (rewrite of CEPOS idea) + +Fixed binary then base64 (example layout — freeze in Phase 2): + +```text +offset size field +0 4 magic 'CV01' +4 4 seq (uint32) +8 8 conanPlayerId +16 8 characterId +24 4 x float +28 4 y float +32 4 z float +36 4 yaw float +40 1 voiceMode (whisper/normal/shout) +41 1 flags (ptt, muted, ...) +42 2 reserved += 44 bytes raw -> ~60 bytes base64 +``` + +Reject stale `seq`, reject identity mismatch after AUTH. + +### 5.3 Discrete events (optional fallback) + +If metadata is too slow for rare events, use server text messages: + +```text +RADIO_JOIN +RADIO_LEAVE +WHISPER_START +WHISPER_STOP +AUTH +PING / PONG +``` + +Do **not** send position as text. --- -## 5. Game-data transport (the previously-missing piece) +## 6. VoiceRouter algorithm (the make-or-break module) -Requirement: client must tell the server its position ~20 Hz **without an external HTTP/REST/WebSocket API** (user constraint). Two in-SDK mechanisms: +### 6.1 Inputs per player -- **Primary: `CLIENT_META_DATA`** (string, up to 4096 bytes, Read/Write). - - Client: `ts3client_setClientSelfVariableAsString(scHandlerID, CLIENT_META_DATA, packedState)` then `ts3client_flushClientSelfUpdates(scHandlerID, NULL)`. - - Server: read with `ts3server_getClientVariableAsString(serverID, clientID, CLIENT_META_DATA, &out)` **[VERIFY server-side getter + whether a change event fires; if no push event, poll on the routing tick]**. - - Payload = compact base64/binary: `playerId,charId,x,y,z,yaw,voiceMode,flags,seq` (reuse V8 CEPOS packing idea, rewritten). -- **Fallback: text messages** `TextMessageTarget_SERVER` -> server `onServerTextMessageEvent`. Heavier; use only for discrete events (RADIO_JOIN, WHISPER_START) if metadata proves too slow. +```text +VoicePlayer { + tsClientId + conanPlayerId + position, yaw + voiceMode + radioGroupId // 0 = none + whisperTargets[] + isGlobalSpeaker + permissions + lastAudibleVersion + lastWhisperList[] // cache for diff +} +``` -Small control protocol (carried over the chosen mechanism): `HELLO, AUTH, AUTH_RESULT, POSITION, VOICE_STATE, RADIO_JOIN/LEAVE, WHISPER_START/STOP, CONFIG, PING/PONG` (from v2 draft, kept). +### 6.2 Tick (5–10 Hz) + +```text +VoiceRouter_tick(): + 1. snapshot all players (no SDK calls while computing) + 2. assign each player to spatial cell (grid) + 3. for each speaker S: + candidates = players in S.cell and 8 neighbors + audible = {} + for each T in candidates: + if distance_ok(S,T) and zone_ok(S,T) and permission_ok(S,T): + audible.add(T) + add radio group peers of S + add whisper targets of S + if S.isGlobalSpeaker: audible = all authenticated + remove S from audible + 4. for each speaker S: + if audible == S.lastWhisperList: continue // hysteresis / cache hit + ITeamSpeakServer.setWhisperList(S.tsClientId, audible) + S.lastWhisperList = audible +``` + +### 6.3 Mapping to SDK + +```c +// Hear B and C only: +anyID targets[] = { B, C, 0 }; +ts3server_setClientWhisperList(serverID, A, NULL, targets); + +// Clear (no custom routing / hear nobody via whisper): +ts3server_setClientWhisperList(serverID, A, NULL, NULL); +``` + +### 6.4 Hard rule + +While a whisper list is active, **channel voice is disabled** for that client. Therefore: + +- Every proximity / radio / whisper / global path must be represented in the whisper list. +- Do not half-use channel voice for some modes and whisper for others. + +### 6.5 Distance bands (config) + +```text +0-10 m full +10-25 m reduced (client may still apply gain/3D) +25-50 m quiet +>50 m not in audible set (no packets) +``` + +Server decides membership; client may still shape loudness with 3D/PCM for members that *do* receive packets. --- -## 6. 3D voice +## 7. 3D voice (client-side) -Server feeds authoritative positions; **client** applies 3D locally via ClientLib 3D API **[VERIFY exact names]**: +Server stores positions; **client** applies spatialization: -- Listener (self): `ts3client_systemset3DListenerAttributes(scHandlerID, &posSelf, &forward, &up)` -- Per remote speaker: `ts3client_channelset3DAttributes(scHandlerID, remoteClientID, &posRemote)` (or `ts3client_set3DWaveAttributes`) -- Custom rolloff: implement `onCustom3dRolloffCalculationClientEvent` to match Conan voice ranges. -- Vectors use `TS3_VECTOR {float x,y,z}`. +```text +each frame / 20 Hz: + ts3client_systemset3DListenerAttributes(selfPos, forward, up) + for each remote client we currently receive: + ts3client_channelset3DAttributes(remoteId, theirPos) + onCustom3dRolloffCalculationClientEvent -> Conan curve +``` -We do NOT build a voice engine; TeamSpeak does encode/decode/transport/spatialize. +Local effects (cave reverb/lowpass): `onEditPlaybackVoiceDataEvent`, driven by server env flag in metadata or config. Reuse V8 DSP ideas; rewrite into client module. --- -## 7. Module map (server + client) with the APIs each owns +## 8. Module API contracts -Server (`server/src/`): -- `ts_server_adapter` — the ONLY file calling `ts3server_*`. Exposes `ITeamSpeakServer` (connectClient/disconnect/getClientState/setWhisperList/getClientMeta/moveClient). -- `player_manager` — `VoicePlayer{tsClientId, conanPlayerId, characterId, pos, voiceMode, authenticated}`; add/remove on connect/disconnect; updatePos from metadata. -- `proximity_engine` — spatial grid (cells), neighbor query, distance -> audible; reuse V8 `proximity_math`/`zone_resolve` logic (rewrite). -- `voice_router` — merges proximity + radio + whisper + global + permissions -> per-client target list -> `ITeamSpeakServer.setWhisperList`; routing cache + hysteresis. -- `auth` — SDK identity -> Conan player mapping via short-lived token. -- `radio_manager`, `whisper_manager`, `permissions` — logical groups. -- `config`, `logging`. +### Server adapter (`ITeamSpeakServer`) -Client (`client/src/`): -- `ts_client_adapter` — the ONLY file calling `ts3client_*`. Exposes `ITeamSpeakClient`. -- `game_provider` — implements `IGameDataProvider` (file/IPC, ported from Mumble/V8 `pos_file`). -- `voice_client_state`, `audio_effects` (reuse V8 reverb/lowpass in `onEditPlaybackVoiceDataEvent`), `hud`, `config`, `logging`. +```text +setWhisperList(clientId, clientIds[]) +clearWhisperList(clientId) +getClientMeta(clientId) -> string +getClientList() -> ids +moveClient(clientId, channelId) // optional lobby +``` -Shared (`shared/`): `protocol/` (message + meta-data packing), `types/` (Vector3, PlayerId, VoiceMode), `config/`. +Implementation = only place with `#include` of ServerLib headers. + +### Client adapter (`ITeamSpeakClient`) + +```text +connect(host, port, nick, password) +disconnect() +publishMeta(packedState) +setListener3D(pos, forward, up) +setSpeaker3D(clientId, pos) +``` + +### Game provider (`IGameDataProvider`) + +```text +bool getPlayerState(PlayerState* out); +// out: playerId, characterId, position, rotation, voiceMode, ptt +``` + +First implementation: file/IPC like V8 `pos_file.c` / Mumble path. Swap later without touching adapters. --- -## 8. Threading model +## 9. Threading -- **SDK callback threads:** `ts3server_*` / `ts3client_*` callbacks fire on SDK-owned threads. Keep them short; hand off to our queues. (Same discipline as V8's callback-thread rule, but now applied to SDK callbacks.) -- **Routing tick thread (server):** 5-10 Hz; reads player snapshots, computes audible sets, calls `setClientWhisperList`. Never blocks on game I/O. -- **Game provider thread (client):** ~20-30 Hz reads Conan position; publishes snapshot; the flush to metadata happens on a controlled cadence. -- **Audio (client):** `onEditPlaybackVoiceDataEvent` — no locks, snapshot read only (reuse V8 PCM ownership pattern). +| Thread | Rate | Work | Forbidden | +|---|---|---|---| +| SDK server callbacks | event | enqueue only | proximity O(N), disk | +| Routing tick | 5–10 Hz | audible sets + `setClientWhisperList` | game file I/O | +| SDK client callbacks | event | status / PCM / 3D hooks | blocking | +| Game provider | 20–30 Hz | read Conan, snapshot | raw `ts3client_*` (use adapter) | +| PCM callback | audio | effects from snapshot | locks, SDK calls | + +Fail closed: if server unreachable → mute proximity, show disconnected HUD; never "everyone hears everyone". --- -## 9. Repository layout (branch ts3-Testing) +## 10. Repository layout (`ts3-Testing`) ```text conan-voice/ -├── client/ { src/ include/ CMakeLists.txt } -├── server/ { src/ include/ config/ CMakeLists.txt } -├── shared/ { protocol/ types/ config/ } -├── sdk/ { include/ lib/ bin/ examples/ } # examples untouched -├── tests/ { unit/ integration/ load/ } +├── client/ # ClientLib app +├── server/ # ServerLib host +├── shared/ # protocol pack, types, config schemas +├── sdk/ # vendored TS3 SDK (examples untouched) +├── tests/ +│ ├── unit/ +│ ├── integration/ # 2-client routing tests +│ └── load/ ├── docs/ -└── CMakeLists.txt # SDK path configurable, no C:\Users\... hardcodes +└── CMakeLists.txt # configurable SDK path; no hardcoded user paths ``` -Kept isolated from `src/` so the V8 DLL build is never disturbed and `tsmain` merges stay clean. +Isolated from existing `src/` so V8 DLL builds stay untouched. --- -## 10. Phases (with concrete API deliverables) +## 11. Phases with concrete exits -**Phase 0 — SDK + license + API verification.** Get SDK (ClientLib+ServerLib) + evaluation license; build official ServerLib + ClientLib examples; confirm every **[VERIFY]** name against `serverlib.h`/`clientlib.h`. Exit: examples run; API list frozen. +| Phase | Deliverable | Exit criteria | +|---|---|---| +| **0** SDK + license | Eval license; official examples build | Every **[VERIFY]** frozen against headers | +| **1** Server bootstrap | `conan-voice-server` start/stop/restart | Virtual server accepts a connection | +| **2** Client + meta | Custom client talks; metadata round-trip | Server sees positions updating | +| **3** Routing PoC | Whisper lists | **A hears B, not C**; then chain `A→B, B→C, C↛A` | +| **4** Conan | `IGameDataProvider` live | Real pos/ids/mode in metadata | +| **5** Proximity + 3D | Grid + ranges + 3D | Distance hearability + spatial audio | +| **6** Features | Radio, whisper groups, global, HUD, reverb | Feature checklist green | +| **7** Perf | 10…250 clients | CPU/RAM/latency documented | +| **8** Production | systemd, monitoring, commercial license | v1.0 DoD | -**Phase 1 — server bootstrap (Milestone 1).** `main.c` runs steps 1-9 of section 2; start/stop/restart clean. No Conan, no routing. - -**Phase 2 — client PoC + metadata.** ClientLib app connects (section 3); publishes `CLIENT_META_DATA`; server reads it. Two clients talk in one channel. - -**Phase 3 — routing PoC (make-or-break).** `voice_router` sets whisper lists: `A -> {B}` only; then chained `A->B, B->C, C-/->A`. Proves server-authoritative "who hears whom". - -**Phase 4 — Conan integration.** `IGameDataProvider` feeds real position/rotation/ids/voice-state (ported from Mumble/V8). - -**Phase 5 — proximity + 3D.** spatial grid, cache, hysteresis in `proximity_engine`; 3D via section 6. - -**Phase 6 — features.** radio, whisper groups, global, cave reverb (server env state -> client effect), HUD, voice modes, root-channel config. - -**Phase 7 — performance.** 10/25/50/100/250 clients; CPU, RAM, network, routing latency, voice latency, reconnects. - -**Phase 8 — production.** Linux/systemd, auto-restart, logging, monitoring, config validation, crash recovery, security review, **commercial license finalized**. +**Kill gate:** if Phase 1 or 3 fails, stop. No Conan, no radio, no DB. --- -## 11. Definition of done +## 12. Definition of done -- **Milestone 1:** ServerLib up, virtual server created, SDK client connects, two clients pass voice, server changes whisper routing, clean shutdown + restart. -- **v0.1:** Conan position -> proximity voice -> 3D voice; player mapping; voice range; reconnect; basic HUD. -- **v0.2:** radio, whisper, global, root-channel config, cave reverb, HUD colors, permissions. -- **v1.0:** production server+client, 250+ tested, perf documented, monitoring, crash recovery, security review, license cleared, full docs. +**Milestone 1:** ServerLib up; virtual server created; two SDK clients pass voice; server changes whisper routing; clean shutdown + restart. + +**v0.1:** Conan position → proximity → 3D; player mapping; voice range; reconnect; basic HUD. + +**v0.2:** radio, whisper, global, root-channel config, cave reverb, HUD colors, permissions. + +**v1.0:** production client+server; 250+ tested; monitoring; crash recovery; security review; **license cleared**; docs complete. --- -## 12. Hard gates / risks +## 13. Risks and open questions -- **License:** paid, negotiated; evaluation only for dev. ([licensing](https://business.teamspeak.com/licensing/)) -- **EOL:** TS3 + TS3 SDK end 2027-12-31 — keep `IVoiceTransport`/`ITeamSpeakServer` seams so transport can be swapped; do not leak SDK calls outside the two adapter files. -- **API-name risk:** some 3D/keypair/metadata-getter names are **[VERIFY]** until Phase 0. -- **Whisper-only routing:** routed clients lose normal channel voice — design accepts this. -- **Position trust:** v1 client-reported (spoofable); v2 game-server-fed. +| Risk | Mitigation | +|---|---| +| Paid license / eval limits | Phase 0 first; contact bizdev early | +| SDK EOL 2027-12-31 | Keep `ITeamSpeakServer` / `ITeamSpeakClient` seams | +| API name drift | Phase 0 freezes headers | +| Whisper disables channel voice | Design all modes as whisper lists | +| Position spoofing | Document in v1; game-server feed in v2 | +| Metadata push vs poll | Measure in Phase 2 | ---- +Open until PoC: -## 13. Open questions (resolved in Phase 0/PoC) - -1. Exact `ServerLibFunctions` / `ClientUIFunctions` callback sets and arg lists. -2. Server-side getter + change event for `CLIENT_META_DATA` (push vs poll). -3. Exact 3D + keypair function signatures. -4. Whisper-list update rate that stays artifact-free at 5-10 Hz. -5. Real client cap under the negotiated license. -6. Position authority (client vs game server) and id binding (Conan/Steam/SDK identity). -7. EOL-2027 successor/exit path. +1. Exact callback arg lists in `ServerLibFunctions` / client UI funcs +2. Server-side `CLIENT_META_DATA` getter + change event +3. Exact keypair + 3D signatures +4. Safe whisper-list update rate +5. Real slot cap under license +6. Identity bind: Conan / Steam / SDK identity +7. Post-2027 exit path --- ## Guardrails -- Work only on `ts3-Testing`. Do not touch `REWRITE_PLAN_V8.md` / `REWRITE_PLAN.md` or the V8 plugin. -- Rewrite from V8/Mumble references; never blind-copy. -- Every change gets a `doku/` entry (project rule). -- No code, no branch, no commit until you approve. +- Work only on `ts3-Testing` +- Do not edit `REWRITE_PLAN_V8.md`, `REWRITE_PLAN.md`, or V8 plugin code for this workstream +- Rewrite from V8/Mumble references — never blind-copy +- Document each code change under `doku/` when implementation starts +- No branch/code/commit until you explicitly approve execution