plan.md aktualisiert
This commit is contained in:
@@ -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 <groupId>
|
||||
RADIO_LEAVE <groupId>
|
||||
WHISPER_START <targetId>
|
||||
WHISPER_STOP
|
||||
AUTH <token>
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user