diff --git a/plan.md b/plan.md index 104bcc3..d191a99 100644 --- a/plan.md +++ b/plan.md @@ -1,597 +1,111 @@ --- -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." +name: Plugin Server Data Plan +overview: Stay on retail TeamSpeak + Client Plugin SDK (API 26). Improve what we send through TeamSpeak (and optionally a small authority) so less is ad-hoc client-only — without building a custom ServerLib app. todos: - - id: create-branch - content: "Create branch ts3-Testing off current branch; do not touch tsmain / V8 plugin" + - id: inventory-wire + content: Inventory current CEPOS fields + hub_parser keys (what already goes via TS) 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" + - id: protocol-design + content: Design versioned plugin-command + hub schema for extra server-side policy/data status: pending - - id: serverlib-bootstrap - content: "Phase 1: conan-voice-server bootstrap (initServerLib -> createVirtualServer -> loop -> stopVirtualServer -> destroyServerLib)" + - id: implement-one + content: Implement one additive send path (one message type or hub keys) — Golden Rule status: pending - - id: sdk-client-poc - content: "Phase 2: custom ClientLib app connects (initClientLib -> spawnNewServerConnectionHandler -> createIdentity -> open devices -> startConnection)" + - id: optional-hybrid + content: "Only if chosen: sketch hybrid authority audible-set API (still client PCM)" 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" + - id: no-serverlib + content: Do not revive ClientLib/ServerLib custom app status: pending isProject: false --- -# Conan Voice — TeamSpeak 3 SDK / ServerLib Plan (Option B) +# Better “send to server” on Plugin SDK (API 26) -**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. +**Discarded:** Option B (own ClientLib/ServerLib app). Deleted old plan file. -> **[VERIFY]** = confirm exact name/signature against `serverlib.h` / `clientlib.h` during Phase 0. Unmarked items are confirmed from TeamSpeak docs/examples. +**Stay on:** retail TeamSpeak + plugin (`PLUGIN_API_VERSION 26`), branch `tsmain` / current V8 path. ---- +## What “server” can mean here -## 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 | +| Meaning | Reality with Plugin SDK | |---|---| -| `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 | +| **A. Retail TeamSpeak server** | Relay only. Plugin commands (`sendPluginCommand`) go client→server→clients. Channel description holds hub config. **No game proximity engine on TS.** | +| **B. Optional game voice authority** | Your own small process (or Conan dedicated server). Clients upload state; it returns audible sets. Plugin still applies PCM/3D. Not TeamSpeak ServerLib. | -### 3.4 Shutdown order +This plan focuses on **A first** (more/better data over TeamSpeak), then optional **B** if you want central decisions. + +## What V8 already sends today + +- **CEPOS** via `sendPluginCommand("CEPOS:…")` — position, facing, voice distance (~56 B base64), throttled + keepalive ([`ts3_cepos.c`](src/ts/proximity/ts3_cepos.c)) +- **CEDRAIN** — wakeup tick only +- **Hub settings** — read from **root channel description** (server admin writes config; plugin parses) + +Proximity math and mute/gain stay **on each client**. + +## What Plugin API 26 cannot do + +- Run proximity inside retail `ts3server` +- True packet drop like ServerLib `ts3server_setClientWhisperList` +- Replace client PCM as the hearing mechanism without leaving Plugin SDK + +## Improvements worth planning (ranked) + +### 1. Richer / cleaner plugin-command protocol (TeamSpeak relay) + +Keep CEPOS, add typed messages (compact binary/base64), e.g.: + +- `CEPOS:` — position (as now; maybe version bump if fields added) +- `CEAUTH:` / identity bind hints (Steam/Conan id hash — still spoofable without B) +- `CEMODE:` — voice mode change events (less poll noise) +- `CEZONE:` — local zone enter/leave if useful for peers +- `CEPING:` — RTT / stale detection beyond keepalive + +Rules: still `sendPluginCommand`; throttle; bounds-check; no secrets in cleartext. + +### 2. Push more config to the TeamSpeak server (channel description / hub) + +Today hub already carries ranges/zones. Expand systematically: + +- Documented key schema (versioned) +- Radio defaults, fail-closed policy, feature flags +- Single writer (server admin / description) — clients only read + +This is the cleanest “server owns policy” path **without** a custom backend. + +### 3. Optional: client whisper lists (limited) + +`requestClientSetWhisperList` exists in Plugin SDK. Possible for **overlays** (admin whisper), not as main proximity router (permissions + “whisper disables channel talk”). Plan as experiment only after protocol work. + +### 4. Optional hybrid authority (if you want “server computes”) ```text -1. stop game provider thread -2. ts3client_stopConnection(scHandlerID, "leaving") -3. ts3client_destroyServerConnectionHandler(scHandlerID) -4. ts3client_destroyClientLib() +Conan / clients -> Voice Authority (your process) +Authority -> audible set per listener +Plugin applies mute/gain/3D (PCM) as today +TeamSpeak = voice transport + CEPOS or authority uplink ``` ---- +Fail closed if authority down. Positions still untrusted unless Conan dedicated server feeds the authority. -## 4. End-to-end sequence (connect → route → hear) +## Explicitly out of scope -```mermaid -sequenceDiagram - participant G as Conan - participant C as ClientLib app - participant S as ServerLib - participant R as VoiceRouter +- Custom TeamSpeak SDK 3.5 ClientLib/ServerLib product +- Branch `ts3-Testing` ServerLib bootstrap +- Expecting retail `ts3server` to run Conan proximity - C->>S: ts3client_startConnection - S-->>C: onConnectStatusChangeEvent(ESTABLISHED) - S->>S: onClientConnected -> PlayerManager.add +## Suggested first milestone (if you approve later) - 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 +1. Inventory current CEPOS + hub keys (freeze wire format) +2. Design versioned plugin-command + hub schema (doc only) +3. Implement one additive message type or hub keys (one function) +4. Only then consider hybrid authority or whisper-list experiment - loop routing 5-10 Hz - R->>R: spatial hash + overlays - R->>S: setClientWhisperList(A, {B,C}) - R->>S: setClientWhisperList(D, empty/clear) - end +## Docs - 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 -``` +- Keep [`vergleichs.md`](vergleichs.md) as historical Option B comparison, or add a short “discarded” banner when implementing +- New living doc under `doku/` when code starts (project rule) ---- +## Decision needed from you (before any code) -## 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. - ---- - -## 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 +Prefer **A-only** (better plugin commands + hub config on retail TS), or **A + optional hybrid authority** for central audible sets?