refactor: remove legacy tauri stack and update iced docs

This commit is contained in:
ReTeamSpeak
2026-05-13 13:35:22 +09:00
parent 4a54f6667c
commit abf380476f
94 changed files with 789 additions and 13393 deletions
+33 -718
View File
@@ -1,729 +1,44 @@
# Software Design Document (SDD)
# ReTeamSpeak - Cross-Platform TeamSpeak Client
# Software Design Document
**Version**: 1.0.0
**Date**: 2026-05-12
**Status**: Based on actual implementation
## iced-app
---
### `main.rs`
## 1. Detailed Design
- Defines the full app state and message enum
- Uses `SyncConnection` / `SyncConnectionHandle` from `tsclientlib`
- Maintains local UI mirrors for:
- bookmarks
- server metadata
- channels
- clients
- chat messages
- device selection
- mute / AFK state
### 1.1 `shared` Module Design
### `audio.rs`
#### 1.1.1 Core Type System
- Output playback through `cpal`
- Input capture through `cpal`
- Opus encode for outgoing voice
- Simple VAD for continuous talk mode
```
┌─────────────────────────────────────────────────────────────┐
│ Type Hierarchy │
├─────────────────────────────────────────────────────────────┤
│ Identifier Types (Newtype Pattern) │
│ ├── ClientId(u16) // In-session client ID │
│ ├── ChannelId(u64) // Channel identifier │
│ ├── ServerGroupId(u64) // Server group │
│ ├── ChannelGroupId(u64) // Channel group │
│ ├── ClientDbId(u64) // Database client ID │
│ ├── Uid(String) // Unique identity (base64) │
│ ├── PermissionId(u32) // Permission ID │
│ └── IconId(i32) // Icon identifier │
│ │
│ Enumerations │
│ ├── Codec { SpeexNB, SpeexWB, SpeexUWB, Celt, OpusVoice, │
│ │ OpusMusic } │
│ ├── ChannelType { Permanent, SemiPermanent, Temporary } │
│ ├── ClientType { Normal, Query { admin } } │
│ ├── ConnectionState { Disconnected, Connecting, ... } │
│ ├── Reason { None, Moved, LostConnection, KickChannel, ... }│
│ ├── CodecEncryptionMode { PerChannel, ForcedOff, ForcedOn }│
│ ├── HostMessageMode { None, Log, Modal, Modalquit } │
│ ├── GroupType { Template, Regular, Query } │
│ └── GroupNamingMode { None, Before, After } │
│ │
│ Data Structures │
│ ├── ServerInfo { id, name, platform, version, max_clients, │
│ │ clients_online, ... } │
│ ├── ChannelInfo { id, parent_id, name, codec, max_clients, │
│ │ channel_type, ... } │
│ ├── ClientInfo { id, channel_id, uid, name, muted, ... } │
│ ├── ChatMessage { id, timestamp, invoker, target, message }│
│ └── AppConfig { nickname, audio, hotkeys, theme, ... } │
└─────────────────────────────────────────────────────────────┘
```
### `noise_cancel.rs`
#### 1.1.2 Event System Design
- Runtime-selected noise cancellation backend
- Current backends:
- `None`
- `nnnoiseless`
- `sonora`
```
AppEvent
├── Connection(ConnectionEvent)
│ ├── Connecting { address }
│ ├── Connected { server, own_client }
│ ├── StateChanged { state }
│ ├── DisconnectedTemporarily { reason }
│ ├── Disconnected { reason }
│ └── ConnectionFailed { error }
├── Client(ClientEvent)
│ ├── EnteredView { client, reason }
│ ├── LeftView { client_id, reason, reason_message }
│ ├── Updated { client_id, changes: ClientChanges }
│ ├── Moved { client_id, from_channel, to_channel, reason }
│ ├── StartedTalking { client_id }
│ ├── StoppedTalking { client_id }
│ ├── ServerGroupChanged { client_id, group_id, added }
│ └── ChannelGroupChanged { client_id, group_id }
├── Channel(ChannelEvent)
│ ├── Created { channel }
│ ├── Deleted { channel_id }
│ ├── Updated { channel_id, changes: ChannelChanges }
│ ├── Moved { channel_id, new_parent, new_order }
│ ├── PasswordChanged { channel_id }
│ ├── DescriptionChanged { channel_id }
│ └── Subscribed { channel_id, subscribed }
├── Server(ServerEvent)
│ ├── Updated { changes: ServerChanges }
│ ├── ServerGroupList { groups }
│ └── ChannelGroupList { groups }
├── Message(MessageEvent)
│ ├── Received { message }
│ ├── Sent { message }
│ ├── Read { message_id }
│ └── UnreadCountChanged { count }
├── Audio(AudioEvent)
│ ├── InputDeviceChanged { device }
│ ├── OutputDeviceChanged { device }
│ ├── InputVolumeChanged { volume }
│ ├── OutputVolumeChanged { volume }
│ ├── InputMutedChanged { muted }
│ ├── OutputMutedChanged { muted }
│ ├── DeviceList { input_devices, output_devices }
│ ├── InputLevel { level }
│ └── OutputLevel { level }
├── FileTransfer(FileTransferEvent)
│ ├── Started { transfer_id, file_name, file_size, is_upload }
│ ├── Progress { transfer_id, progress }
│ ├── Completed { transfer_id }
│ ├── Failed { transfer_id, error }
│ └── Cancelled { transfer_id }
└── Error(ErrorEvent)
├── Protocol { code, message }
├── Network { message }
├── Audio { message }
├── Database { message }
└── Other { message }
```
### `identity.rs`
---
- Locates TeamSpeak config database in common Linux paths
- Extracts `identity_secret_key`
- Parses it into `tsclientlib::Identity`
### 1.2 `tscore` Module Design
## Known Design Limitations
#### 1.2.1 Packet Processing Pipeline
```
SEND RECEIVE
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Command String │ │ UDP Packet │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Command::serialize() │ │ InPacket::parse() │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ QuickLZ compress │ │ AES-EAX decrypt │
│ (if Command/CommandLow) │ │ (or fake decrypt) │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Fragment (if > 500 bytes) │ │ QuickLZ decompress │
│ │ │ │ (if COMPRESSED flag) │
│ ▼ │ │ │ │
│ AES-EAX encrypt │ │ ▼ │
│ (or fake encrypt) │ │ Defragment │
│ │ │ │ (if FRAGMENTED flag) │
│ ▼ │ │ │ │
│ Assign Packet ID │ │ ▼ │
│ │ │ │ Command::parse() │
│ ▼ │ │ │ │
│ UDP Send │ │ ▼ │
└──────────────────────────────┘ │ Application Layer │
└──────────────────────────────┘
```
#### 1.2.2 Encryption Key Derivation
```
Input: packet_type, direction, generation_id, shared_iv[64]
┌───────────────────────────────────────────────────────┐
│ temp[0] = direction_byte (0x30=S2C, 0x31=C2S) │
│ temp[1] = packet_type.u8() │
│ temp[2..6] = generation_id.to_be_bytes() │
│ temp[6..70] = shared_iv[0..64] │
│ │
│ key_nonce = SHA-256(temp) │
│ key = key_nonce[0..16] │
│ nonce = key_nonce[16..32] │
│ │
│ key[0] ^= (packet_id >> 8) as u8 │
│ key[1] ^= (packet_id & 0xFF) as u8 │
└───────────────────────────────────────────────────────┘
Output: key[16], nonce[16] → AES-128-EAX
```
#### 1.2.3 Connection State Machine
```
┌──────────────┐
│ Disconnected │
└──────┬───────┘
│ start_handshake()
┌──────────────┐
┌──────│ Connecting │◄─────────────────┐
│ └──────┬───────┘ │
│ │ Init1 received │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ IdentityLevelIncreasing│ │
│ └──────┬───────────────┘ │
│ │ Init3 received │
│ ▼ │
│ ┌──────────────┐ │
│ │ Connected │──────────────────┤
│ └──────┬───────┘ │
│ │ channellistfinished │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ ChannelListFinished │ │
│ └──────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
└─────>│ DisconnectedTemporarily │──────┘
└──────┬───────────────────┘
│ timeout / manual
┌──────────────┐
│ Error │
└──────┬───────┘
┌──────────────┐
│ Disconnected │
└──────────────┘
```
#### 1.2.4 RSA Puzzle Solver
```rust
/// Solves y = x^(2^level) mod n
///
/// Algorithm:
/// y = x
/// for i in 0..level:
/// y = (y * y) mod n
///
/// Time complexity: O(level * M(n)) where M(n) is multiplication cost
/// Space complexity: O(n) for big integer storage
fn solve_rsa_puzzle(x: &[u8; 64], n: &[u8; 64], level: u32) -> [u8; 64] {
let x_big = BigUint::from_bytes_be(x);
let n_big = BigUint::from_bytes_be(n);
let mut y = x_big;
for _ in 0..level {
y = (y.clone() * y) % &n_big;
}
// Convert back to 64-byte array (big-endian, zero-padded)
}
```
#### 1.2.5 Retransmission System
```
┌─────────────────────────────────────────────────────────┐
│ ResendManager │
├─────────────────────────────────────────────────────────┤
│ pending: BTreeMap<PacketId, SentPacket> │
│ max_retries: u32 (default: 10) │
│ connection_timeout: Duration (default: 30s) │
├─────────────────────────────────────────────────────────┤
│ add_sent(id, data) → Add to pending queue │
│ ack(id) → bool → Remove from pending │
│ get_retransmissions() → Vec<(id, data)> to resend │
│ is_connection_timeout()→ Check for dead connection │
├─────────────────────────────────────────────────────────┤
│ │
│ SentPacket: │
│ data: Vec<u8> │
│ sent_at: Instant │
│ retry_count: u32 │
│ timeout: Duration (starts at 500ms, doubles) │
│ │
│ RttEstimator: │
│ srtt: Duration (smoothed RTT) │
│ rtt_var: Duration (RTT variance) │
│ rto: Duration (retransmission timeout) │
│ update(measured_rtt) → recalculate SRTT, RTO │
└─────────────────────────────────────────────────────────┘
```
---
### 1.3 `tsaudio` Module Design
#### 1.3.1 Audio Pipeline
```
CAPTURE PIPELINE:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ cpal │───>│ VAD │───>│ Opus │───>│ Packet │
│ capture │ │ detect │ │ encode │ │ output │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
PLAYBACK PIPELINE:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Packet │───>│ Jitter │───>│ Opus │───>│ cpal │
│ input │ │ buffer │ │ decode │ │ playback │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
#### 1.3.2 Voice Activity Detection
```rust
pub struct VadDetector {
threshold: f32,
state: VadState,
}
impl VadDetector {
pub fn detect(&mut self, samples: &[f32]) -> VadState {
let energy = samples.iter().map(|s| s * s).sum::<f32>()
/ samples.len() as f32;
if energy > self.threshold {
self.state = VadState::Speaking;
} else {
self.state = VadState::Silent;
}
self.state
}
}
```
#### 1.3.3 Jitter Buffer
```
┌─────────────────────────────────────────────────────────┐
│ JitterBuffer │
├─────────────────────────────────────────────────────────┤
│ buffer: Vec<Option<AudioFrame>> (ring buffer) │
│ head: usize │
│ tail: usize │
│ size: usize │
│ capacity: usize │
├─────────────────────────────────────────────────────────┤
│ push(frame) → Result<()> // Add frame │
│ pop() → Option<AudioFrame> // Get next frame │
│ len() → usize // Current buffer size │
│ is_empty() → bool │
│ is_full() → bool │
│ clear() // Reset buffer │
└─────────────────────────────────────────────────────────┘
```
---
### 1.4 `tsdb` Module Design
#### 1.4.1 Database Schema
```sql
-- Identity storage
CREATE TABLE identities (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL, -- Display name
private_key TEXT NOT NULL, -- Base64 encoded ECC private key
counter INTEGER DEFAULT 0, -- Hash Cash counter
max_counter INTEGER DEFAULT 0, -- Maximum counter tried
created_at TEXT NOT NULL, -- ISO 8601 timestamp
updated_at TEXT NOT NULL -- ISO 8601 timestamp
);
-- Server bookmarks
CREATE TABLE bookmarks (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL, -- Display name
address TEXT NOT NULL, -- Server address
port INTEGER DEFAULT 9987, -- Server port
nickname TEXT, -- Preferred nickname
server_password TEXT, -- Encrypted server password
channel TEXT, -- Default channel
channel_password TEXT, -- Encrypted channel password
default_token TEXT, -- Permission token
auto_connect INTEGER DEFAULT 0,-- Auto-connect on startup
last_connected TEXT, -- Last connection timestamp
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Message history
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_address TEXT NOT NULL, -- Server address
invoker_id INTEGER NOT NULL, -- Client ID
invoker_name TEXT NOT NULL, -- Display name
invoker_uid TEXT NOT NULL, -- Unique ID
target_type TEXT NOT NULL, -- "server", "channel", "client"
target_id INTEGER, -- Target ID
message TEXT NOT NULL, -- Message content
is_read INTEGER DEFAULT 0, -- Read status
timestamp TEXT NOT NULL -- ISO 8601 timestamp
);
-- Key-value settings
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
```
#### 1.4.2 CRUD Operations
```rust
impl DatabaseManager {
// Identity operations
fn create_identity(&self, name, private_key) -> DatabaseResult<Identity>;
fn get_identity(&self, id) -> DatabaseResult<Identity>;
fn get_all_identities(&self) -> DatabaseResult<Vec<Identity>>;
fn update_identity(&self, id, name?, counter?) -> DatabaseResult<()>;
fn delete_identity(&self, id) -> DatabaseResult<()>;
// Bookmark operations
fn create_bookmark(&self, name, address, port, nickname?) -> DatabaseResult<Bookmark>;
fn get_bookmark(&self, id) -> DatabaseResult<Bookmark>;
fn get_all_bookmarks(&self) -> DatabaseResult<Vec<Bookmark>>;
fn update_bookmark(&self, id, name?, address?, port?, nickname?) -> DatabaseResult<()>;
fn delete_bookmark(&self, id) -> DatabaseResult<()>;
// Message operations
fn create_message(&self, server_address, invoker_id, invoker_name,
invoker_uid, target_type, target_id?, message) -> DatabaseResult<Message>;
fn get_server_messages(&self, server_address, limit, offset) -> DatabaseResult<Vec<Message>>;
fn mark_message_read(&self, id) -> DatabaseResult<()>;
// Settings operations
fn get_setting(&self, key) -> DatabaseResult<Option<String>>;
fn set_setting(&self, key, value) -> DatabaseResult<()>;
}
```
---
### 1.5 `tauri-app` Design
#### 1.5.1 Tauri Command Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ invoke("get_bookmarks") → Promise<BookmarkInfo[]> │ │
│ │ invoke("connect", {addr, port, nick, pass}) │ │
│ │ invoke("send_message", {target, message}) │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────────┬─────────────────────────────────┘
│ Tauri IPC
┌─────────────────────────────────────────────────────────────┐
│ Tauri Shell (Rust) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ #[tauri::command] │ │
│ │ async fn get_bookmarks(state: State<AppState>) │ │
│ │ -> Result<Vec<BookmarkInfo>, String> │ │
│ │ { │ │
│ │ state.db.get_all_bookmarks() │ │
│ │ .map(|b| b.into_iter().map(Into::into)) │ │
│ │ .map_err(|e| e.to_string()) │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ AppState { │ │
│ │ db: DatabaseManager, │ │
│ │ connection_state: Mutex<ConnectionState>, │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
#### 1.5.2 React Component Structure
```
App
├── Header
│ ├── Logo
│ ├── ConnectionStatus
│ └── SettingsButton
├── Sidebar
│ ├── BookmarkList
│ │ └── BookmarkItem (clickable)
│ └── RecentServers
├── MainContent
│ ├── ConnectForm (when no connection)
│ │ ├── AddressInput
│ │ ├── NicknameInput
│ │ ├── PasswordInput
│ │ └── ConnectButton
│ │
│ └── ChatView (when connected)
│ ├── ChannelTree
│ ├── ClientList
│ ├── MessageList
│ └── MessageInput
└── StatusBar
├── ConnectionInfo
├── AudioStatus
└── LatencyDisplay
```
---
## 2. Algorithm Specifications
### 2.1 Shared Secret Computation (Old Protocol <3.1)
```
Input: alpha[10], beta[10], shared_data[32]
Output: SharedIV[64], SharedMac[8]
1. SharedIV[0..20] = SHA-1(shared_data)
2. SharedIV[0..10] ^= alpha[0..10]
3. SharedIV[10..20] ^= beta[0..10]
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
```
### 2.2 Shared Secret Computation (New Protocol ≥3.1)
```
Input: alpha[10], beta[54], shared_data[32]
Output: SharedIV[64], SharedMac[8]
1. SharedIV[0..64] = SHA-512(shared_data)
2. SharedIV[0..10] ^= alpha[0..10]
3. SharedIV[10..64] ^= beta[0..54]
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
```
### 2.3 Hash Cash Level Computation
```
Input: omega (public key string), offset (u64)
Output: level (u8)
1. data = SHA-1(omega + offset.to_string())
2. level = 0
3. for byte in data:
4. if byte == 0:
5. level += 8
6. else:
7. level += trailing_zeros(byte)
8. break
9. return level
```
### 2.4 UID Computation
```
Input: publicKey (ASN.1-DER encoded)
Output: uid (base64 string)
1. hash = SHA-1(publicKey)
2. uid = base64(hash)
```
---
## 3. Interface Specifications
### 3.1 Tauri IPC Interface
```typescript
// TypeScript interface for Tauri commands
interface ITauriCommands {
// Identity management
get_identities(): Promise<IdentityInfo[]>;
create_identity(name: string): Promise<IdentityInfo>;
delete_identity(id: string): Promise<void>;
// Bookmark management
get_bookmarks(): Promise<BookmarkInfo[]>;
create_bookmark(name: string, address: string, port: number,
nickname?: string): Promise<BookmarkInfo>;
delete_bookmark(id: string): Promise<void>;
// Connection
connect(address: string, port: number, nickname: string,
password?: string): Promise<void>;
disconnect(): Promise<void>;
// Messaging
send_message(target: string, message: string): Promise<void>;
get_messages(server_address: string, limit: number,
offset: number): Promise<MessageInfo[]>;
}
interface IdentityInfo {
id: string;
name: string;
counter: number;
max_counter: number;
}
interface BookmarkInfo {
id: string;
name: string;
address: string;
port: number;
nickname: string | null;
auto_connect: boolean;
last_connected: string | null;
}
interface MessageInfo {
id: number;
invoker_name: string;
message: string;
timestamp: string;
is_read: boolean;
}
```
### 3.2 Internal Rust Interfaces
```rust
// Protocol layer
pub trait PacketProcessor {
fn encode(&self, packet: OutPacket) -> Result<Vec<OutUdpPacket>>;
fn decode(&self, data: &[u8]) -> Result<InPacket>;
}
// Audio layer
pub trait AudioCapture {
async fn start(&mut self) -> AudioResult<()>;
async fn stop(&mut self) -> AudioResult<()>;
async fn capture(&mut self) -> AudioResult<AudioFrame>;
}
pub trait AudioPlayback {
async fn start(&mut self) -> AudioResult<()>;
async fn stop(&mut self) -> AudioResult<()>;
async fn play(&mut self, frame: AudioFrame) -> AudioResult<()>;
}
// Database layer
pub trait IdentityStore {
fn create(&self, name: &str, key: &str) -> DatabaseResult<Identity>;
fn get(&self, id: &str) -> DatabaseResult<Identity>;
fn list(&self) -> DatabaseResult<Vec<Identity>>;
fn update(&self, id: &str, updates: IdentityUpdates) -> DatabaseResult<()>;
fn delete(&self, id: &str) -> DatabaseResult<()>;
}
```
---
## 4. Data Dictionary
### 4.1 Protocol Fields
| Field | Type | Size | Description |
|-------|------|------|-------------|
| MAC | [u8; 8] | 8 bytes | EAX message authentication code |
| PId | u16 | 2 bytes | Packet sequence ID |
| CId | u16 | 2 bytes | Client ID (C2S only) |
| PT | u8 | 1 byte | Packet type + flags |
| VId | u16 | 2 bytes | Voice packet ID |
| Codec | u8 | 1 byte | Audio codec type |
### 4.2 Flag Bits
| Bit | Name | Mask | Description |
|-----|------|------|-------------|
| 7 | UE | 0x80 | Unencrypted |
| 6 | CP | 0x40 | Compressed (QuickLZ) |
| 5 | NP | 0x20 | New protocol |
| 4 | FR | 0x10 | Fragmented |
| 3-0 | Type | 0x0F | Packet type (0-8) |
### 4.3 Error Codes
| Code | Name | Description |
|------|------|-------------|
| 0x0000 | ok | Success |
| 0x0200 | client_invalid_id | Invalid client ID |
| 0x0201 | client_nickname_inuse | Nickname already in use |
| 0x0208 | client_invalid_password | Wrong password |
| 0x0300 | channel_invalid_id | Invalid channel ID |
| 0x0400 | server_invalid_id | Invalid server ID |
| 0x0403 | server_maxclients_reached | Server full |
| 0x0701 | connection_lost | Connection lost |
---
## 5. Test Design
### 5.1 Test Cases (32 total)
#### Protocol Tests (14)
1. `test_packet_type_conversion` - PacketType enum conversion
2. `test_flags` - Flag bit manipulation
3. `test_header_c2s` - C2S header parsing
4. `test_header_s2c` - S2C header parsing
5. `test_in_packet_parse` - Input packet parsing
6. `test_out_packet` - Output packet creation
7. `test_command_parse` - Command string parsing
8. `test_command_serialize` - Command serialization
9. `test_command_builder` - CommandBuilder pattern
10. `test_escape_sequences` - Escape/unescape
11. `test_init_packet_parse` - Init packet parsing
12. `test_init_packet_serialize` - Init packet serialization
13. `test_ack_packet` - Acknowledgement packet
14. `test_packet_type_properties` - Type property queries
#### Crypto Tests (12)
1. `test_sha1` - SHA-1 hash
2. `test_sha256` - SHA-256 hash
3. `test_sha512` - SHA-512 hash
4. `test_hash_password` - Password hashing
5. `test_create_key_nonce` - Key derivation
6. `test_create_encryption_key` - Packet-specific key
7. `test_shared_secret_old` - Old protocol shared secret
8. `test_shared_secret_new` - New protocol shared secret
9. `test_key_cache` - Key caching
10. `test_eax_encrypt_decrypt` - EAX encryption/decryption
11. `test_fake_encrypt_decrypt` - Fake encryption
12. `test_hash_cash_level` - Hash Cash computation
13. `test_compute_uid` - UID computation
#### Connection Tests (6)
1. `test_encode_version` - Version encoding
2. `test_rsa_puzzle` - RSA puzzle solver
3. `test_resend_manager` - Retransmission manager
4. `test_rtt_estimator` - RTT estimation
5. `test_sent_packet_retry` - Packet retry logic
---
## 6. References
1. TS3 Protocol Paper: `refercence/tsdeclarations/ts3protocol.md`
2. Packet Definitions: `refercence/tsdeclarations/Packets.txt`
3. Message Definitions: `refercence/tsdeclarations/Messages.toml`
4. Source Code: `src/` (4780 lines, 39 Rust files)
- `main.rs` is still monolithic and should be split later
- Bookmarks are not persisted
- ServerQuery is not wired to a real backend yet
- Audio settings apply to runtime state, but broader config persistence is not implemented