Files
md-show/plan.md
T
2026-08-17 18:15:55 +02:00

598 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: Voice Architecture Plan
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; 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)"
status: pending
- id: sdk-client-poc
content: "Phase 2: custom ClientLib app connects (initClientLib -> spawnNewServerConnectionHandler -> createIdentity -> open devices -> startConnection)"
status: pending
- id: metadata-transport
content: "Phase 2/3: game-data via CLIENT_META_DATA (+ flushClientSelfUpdates); confirm server read + change event"
status: pending
- id: routing-poc
content: "Phase 3: ts3server_setClientWhisperList proves A hears B, not C"
status: pending
- id: threed-voice
content: "Phase 4/5: 3D via ClientLib listener/channel attributes fed by server positions"
status: pending
- id: write-en-doc
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"
status: pending
isProject: false
---
# Conan Voice — TeamSpeak 3 SDK / ServerLib Plan (Option B)
**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.
> **[VERIFY]** = confirm exact name/signature against `serverlib.h` / `clientlib.h` during Phase 0. Unmarked items are confirmed from TeamSpeak docs/examples.
---
## 0. What we are building (and what we are not)
### 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 |
|---|---|---|
| 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 V8 is Plugin SDK: `sdk/README.md` line 1. Repo has no `ts3client_*` / `ts3server_*`.
---
## 1. System diagram
```mermaid
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 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 <-->|"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 lifecycle — function-by-function
**Files:** `server/src/main.c`, `server/src/ts_server_adapter.c`
### 2.1 Startup (strict order)
```text
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()
```
### 2.2 Confirmed ServerLib call signatures
```c
unsigned int ts3server_initServerLib(
const struct ServerLibFunctions* functionPointers,
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);
unsigned int ts3server_setClientWhisperList(
uint64 serverID,
anyID clientID,
const uint64* channelIDs, // 0-terminated or NULL
const anyID* clientIDs); // 0-terminated or NULL
unsigned int ts3server_getClientList(uint64 serverID, anyID** result);
unsigned int ts3server_getChannelList(uint64 serverID, uint64** result);
void ts3server_freeMemory(void* pointer);
```
### 2.3 Shutdown order
```text
1. VoiceRouter_stop() // stop tick, clear lists optional
2. ts3server_stopVirtualServer(serverID)
3. PlayerManager_clear()
4. ts3server_destroyServerLib()
5. Log_stop()
```
### 2.4 Callback → module wiring
| 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 |
Keep callbacks short: queue work; do not run O(N) proximity inside the callback.
---
## 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
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
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 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 talks - packets only to whisper targets
S-->>C: voice to B and C only
C->>C: 3D attributes + optional PCM effects
```
---
## 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.
---
## 6. VoiceRouter algorithm (the make-or-break module)
### 6.1 Inputs per player
```text
VoicePlayer {
tsClientId
conanPlayerId
position, yaw
voiceMode
radioGroupId // 0 = none
whisperTargets[]
isGlobalSpeaker
permissions
lastAudibleVersion
lastWhisperList[] // cache for diff
}
```
### 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.
---
## 7. 3D voice (client-side)
Server stores positions; **client** applies spatialization:
```text
each frame / 20 Hz:
ts3client_systemset3DListenerAttributes(selfPos, forward, up)
for each remote client we currently receive:
ts3client_channelset3DAttributes(remoteId, theirPos)
onCustom3dRolloffCalculationClientEvent -> Conan curve
```
Local effects (cave reverb/lowpass): `onEditPlaybackVoiceDataEvent`, driven by server env flag in metadata or config. Reuse V8 DSP ideas; rewrite into client module.
---
## 8. Module API contracts
### Server adapter (`ITeamSpeakServer`)
```text
setWhisperList(clientId, clientIds[])
clearWhisperList(clientId)
getClientMeta(clientId) -> string
getClientList() -> ids
moveClient(clientId, channelId) // optional lobby
```
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.
---
## 9. Threading
| 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".
---
## 10. Repository layout (`ts3-Testing`)
```text
conan-voice/
├── 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 # configurable SDK path; no hardcoded user paths
```
Isolated from existing `src/` so V8 DLL builds stay untouched.
---
## 11. Phases with concrete exits
| 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 |
**Kill gate:** if Phase 1 or 3 fails, stop. No Conan, no radio, no DB.
---
## 12. Definition of done
**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.
---
## 13. Risks and open questions
| 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:
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 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