19 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Voice Architecture Plan | 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. |
|
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 (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.hduring 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:
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
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
TeamSpeakClientAdaptercallsts3client_*. - Only
TeamSpeakServerAdaptercallsts3server_*. - Game logic never includes
clientlib.h/serverlib.hdirectly.
2. Server lifecycle — function-by-function
Files: server/src/main.c, server/src/ts_server_adapter.c
2.1 Startup (strict order)
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
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
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)
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
// 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
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)
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):
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:
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
VoicePlayer {
tsClientId
conanPlayerId
position, yaw
voiceMode
radioGroupId // 0 = none
whisperTargets[]
isGlobalSpeaker
permissions
lastAudibleVersion
lastWhisperList[] // cache for diff
}
6.2 Tick (5–10 Hz)
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
// 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)
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:
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)
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)
connect(host, port, nick, password)
disconnect()
publishMeta(packedState)
setListener3D(pos, forward, up)
setSpeaker3D(clientId, pos)
Game provider (IGameDataProvider)
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)
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:
- Exact callback arg lists in
ServerLibFunctions/ client UI funcs - Server-side
CLIENT_META_DATAgetter + change event - Exact keypair + 3D signatures
- Safe whisper-list update rate
- Real slot cap under license
- Identity bind: Conan / Steam / SDK identity
- 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