diff --git a/.gitignore b/.gitignore index e55eefe..39bc171 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ .DS_Store Thumbs.db -# Apple / Windows client build artifacts (added in M4) +# Apple / Windows client build artifacts clients/apple/**/build/ clients/apple/**/*.xcodeproj/xcuserdata/ clients/apple/**/*.xcodeproj/project.xcworkspace/ diff --git a/CMakePresets.json b/CMakePresets.json index 6499bf9..65377c6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -15,7 +15,7 @@ { "name": "dev", "displayName": "Dev (full real-deps build, vcpkg)", - "description": "Day-to-day development preset. Real protocol, crypto, voice, server — everything from M1 onward. Builds server + tools + tests (21 tests). Auto-triplet: x64-mingw-static on Windows, x64-linux on Linux, arm64-osx on Apple Silicon. Requires VCPKG_ROOT.", + "description": "Day-to-day development preset. Real protocol, crypto, voice, server. Builds server + tools + tests. Auto-triplet: x64-mingw-static on Windows, x64-linux on Linux, arm64-osx on Apple Silicon. Requires VCPKG_ROOT.", "inherits": "vcpkg-common", "binaryDir": "${sourceDir}/build/dev", "cacheVariables": { @@ -52,7 +52,7 @@ }, { "name": "windows-client", - "displayName": "Windows client (voicecat.dll for C# WinForms, M4)", + "displayName": "Windows client (voicecat.dll for C# WinForms)", "description": "Produces a redistributable Release voicecat.dll with no MinGW runtime DLL dependencies (see core/CMakeLists.txt's static-runtime link flags and clients/windows/README.md). Server/tools/tests are off — this preset exists only to build the DLL. Windows only.", "inherits": "vcpkg-common", "binaryDir": "${sourceDir}/build/windows-client", diff --git a/clients/apple/Sources/VoiceCatCore/Enums.swift b/clients/apple/Sources/VoiceCatCore/Enums.swift index 6dbb83e..91975fb 100644 --- a/clients/apple/Sources/VoiceCatCore/Enums.swift +++ b/clients/apple/Sources/VoiceCatCore/Enums.swift @@ -61,7 +61,7 @@ public enum VoiceCatConnectionState: UInt32, Sendable, Equatable { case tlsHandshake = 2 case authenticating = 3 case connected = 4 - /// M4: handshake succeeded, waiting on `confirmServerIdentity()`. + /// Handshake succeeded, waiting on `confirmServerIdentity()`. case verifyingIdentity = 5 public init(_ cValue: vc_connection_state) { @@ -125,13 +125,13 @@ public enum VoiceCatEventType: UInt32, Sendable, Equatable { case talkState = 9 case error = 10 case disconnected = 11 - /// M4: reply to `joinChannel()` — see `VoiceCatEvent.result` / `.channelId`. + /// Reply to `joinChannel()` — see `VoiceCatEvent.result` / `.channelId`. case joinResult = 12 - /// M4: the TOFU server-identity gate — see `VoiceCatEvent.tofuStatus` / `.text`. + /// The TOFU server-identity gate — see `VoiceCatEvent.tofuStatus` / `.text`. case serverIdentity = 13 - /// M5: async result for moderation/admin/channel operations. + /// Async result for moderation/admin/channel operations. case genericResult = 14 - /// M5: reply to `requestAccountList()` — call `listAccounts()` to read. + /// Reply to `requestAccountList()` — call `listAccounts()` to read. case accountList = 15 /// Voice-plane subscription state. `u32a` = 1 (subscribed) or 0 (unsubscribed). case voiceState = 16 diff --git a/clients/apple/Sources/VoiceCatCore/Models.swift b/clients/apple/Sources/VoiceCatCore/Models.swift index 6aa3803..853bc25 100644 --- a/clients/apple/Sources/VoiceCatCore/Models.swift +++ b/clients/apple/Sources/VoiceCatCore/Models.swift @@ -75,7 +75,7 @@ public struct User: Sendable, Equatable, Identifiable { } } -/// Permission bitset — mirrors `vc_permissions` (M5). +/// Permission bitset — mirrors `vc_permissions`. public struct Permissions: Sendable, Equatable { public let canCreateTempChannel: Bool public let canKick: Bool @@ -91,7 +91,7 @@ public struct Permissions: Sendable, Equatable { } } -/// Account entry — mirrors `vc_account` (M5, reply to `listAccounts()`). +/// Account entry — mirrors `vc_account` (reply to `listAccounts()`). public struct Account: Sendable, Equatable { public let username: String public let isAdmin: Bool diff --git a/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift b/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift index 8a38621..eb32a64 100644 --- a/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift +++ b/clients/apple/Sources/VoiceCatCore/VoiceCatClient.swift @@ -219,7 +219,7 @@ public final class VoiceCatClient { VoiceCatResult(vc_authenticate_user(handle, username, password)) } - // MARK: - TOFU server-identity gate (M4) + // MARK: - TOFU server-identity gate /// Accept or reject the pending server-identity check. Call after a `.serverIdentity` /// event. `accept=true` on firstConnect/mismatch updates the pin file and proceeds; @@ -413,7 +413,7 @@ public final class VoiceCatClient { /// Global playback volume applied after mixing all remote streams. gain 0.0 = silent, /// 1.0 = unity (default), >1.0 amplifies. Always LOCAL — no protocol traffic. Mirrors the - /// Windows client's `SetOutputVolume` and the C ABI `vc_set_output_volume` added in M5. + /// Windows client's `SetOutputVolume` and the C ABI `vc_set_output_volume`. @discardableResult public func setOutputVolume(_ gain: Float) -> VoiceCatResult { VoiceCatResult(vc_set_output_volume(handle, gain < 0 ? 0 : gain)) @@ -500,7 +500,7 @@ public final class VoiceCatClient { return Marshaling.devices(&native) } - // MARK: - M5: Moderation + // MARK: - Moderation @discardableResult public func kickUser(_ userId: UInt32, reason: String? = nil) -> VoiceCatResult { @@ -535,7 +535,7 @@ public final class VoiceCatClient { VoiceCatResult(vc_move_user(handle, userId, channelId)) } - // MARK: - M5: Channel admin + // MARK: - Channel admin @discardableResult public func createChannel(_ info: ChannelEdit) -> VoiceCatResult { @@ -558,7 +558,7 @@ public final class VoiceCatClient { VoiceCatResult(vc_delete_channel(handle, channelId)) } - // MARK: - M5: Account admin + // MARK: - Account admin @discardableResult public func createAccount(_ username: String, password: String) -> VoiceCatResult { diff --git a/clients/apple/Tests/VoiceCatCoreTests/VoiceCatClientSmokeTests.swift b/clients/apple/Tests/VoiceCatCoreTests/VoiceCatClientSmokeTests.swift index 21deeca..5a67530 100644 --- a/clients/apple/Tests/VoiceCatCoreTests/VoiceCatClientSmokeTests.swift +++ b/clients/apple/Tests/VoiceCatCoreTests/VoiceCatClientSmokeTests.swift @@ -64,7 +64,7 @@ private final class ServerHarness { } self.port = port - // Provision a known admin account for moderation/admin tests (M5). + // Provision a known admin account for moderation/admin tests. let adminURL = URL(fileURLWithPath: repoRoot) .appendingPathComponent("build/dev/bin/voicecat-admin") guard FileManager.default.isExecutableFile(atPath: adminURL.path) else { @@ -252,12 +252,12 @@ final class VoiceCatClientSmokeTests: XCTestCase { XCTAssertTrue(channels.contains { $0.id == 1 && $0.name == "Lobby" }, "expected Lobby (channel 1) in \(channels.map { $0.name })") - // M5: permissions getter round-trip. + // Permissions getter round-trip. let perms = client.getPermissions() XCTAssertFalse(perms.isAdmin) XCTAssertFalse(perms.canKick) - // M5: guest ListAccounts is rejected by the server with a GenericResult — proves the + // Guest ListAccounts is rejected by the server with a GenericResult — proves the // moderation wrapper path works end-to-end through the Swift interop layer. events.removeAll() XCTAssertEqual(client.requestAccountList(), .ok) diff --git a/clients/apple/iOS/VoiceCatiOS/AppState.swift b/clients/apple/iOS/VoiceCatiOS/AppState.swift index 3801f20..7620828 100644 --- a/clients/apple/iOS/VoiceCatiOS/AppState.swift +++ b/clients/apple/iOS/VoiceCatiOS/AppState.swift @@ -116,16 +116,6 @@ final class AppState { ServerListStore.shared.save(servers) } - // MARK: - Teardown - - /// `AppState` is the @Observable app root owned by the SwiftUI `App`; it lives for the - /// whole app process and is torn down only on process exit, at which point OS cleanup - /// suffices. Reconnect state (the in-flight `Task` and the `NWPathMonitor`) is cancelled - /// via `cancelReconnect()` driven by `disconnect()`/`cancelConnect()` and on successful - /// restore — those run on user-initiated teardown, which is the only path that matters. - /// (The `Task` captures `[weak self]` and guards on `nil`/`userInitiatedDisconnect`, so a - /// stray task left running when AppState is gone is a no-op; the monitor similarly guards.) - // MARK: - Connect flow /// Public connect entry. Always starts a fresh session (no restore). @@ -245,6 +235,13 @@ final class AppState { /// Cancel any in-flight reconnect task and stop the path monitor. Safe to call when nothing /// is armed (no-op). Does NOT touch `userInitiatedDisconnect` or `lastSession` — callers set /// those as needed (disconnect/cancelConnect clear them; scheduleReconnect keeps them). + /// + /// `AppState` is the @Observable app root owned by the SwiftUI `App`; it lives for the + /// whole app process and is torn down only on process exit, at which point OS cleanup + /// suffices. This method is driven by `disconnect()`/`cancelConnect()` and on successful + /// restore — those run on user-initiated teardown, which is the only path that matters. + /// (The reconnect `Task` captures `[weak self]` and guards on `nil`/`userInitiatedDisconnect`, + /// so a stray task left running when AppState is gone is a no-op; the monitor similarly guards.) private func cancelReconnect() { reconnectTask?.cancel() reconnectTask = nil @@ -310,15 +307,12 @@ final class AppState { if prevSig == nil { return } if self.session != nil { - // Connected — tear down + reconnect on a meaningful path change. - // `.unsatisfied` (all radios off) OR a primary-interface change (Wi-Fi↔cellular) - // almost always breaks the live TCP connection; reconnecting proactively beats - // waiting for the C core's keepalive/reaper timeout. + // See this method's doc comment for why these conditions trigger a + // proactive reconnect. if path.status != .satisfied || sig != prevSig { self.proactiveReconnect() } } else if self.lastSession != nil { - // Mid-reconnect — a path is available again; fast-fresh the next attempt. if path.status == .satisfied { self.reconnectAttempt = 0 self.scheduleReconnect() diff --git a/clients/apple/iOS/VoiceCatiOS/IOSAudioRouter.swift b/clients/apple/iOS/VoiceCatiOS/IOSAudioRouter.swift index 03e6d4c..440e6a0 100644 --- a/clients/apple/iOS/VoiceCatiOS/IOSAudioRouter.swift +++ b/clients/apple/iOS/VoiceCatiOS/IOSAudioRouter.swift @@ -163,13 +163,8 @@ final class IOSAudioRouter: ObservableObject { /// burns CPU and cycles the audio session on/off (the "glitching" bug). private var isApplyingConfiguration = false - /// Last `overrideOutputAudioPort` value we successfully applied (`.none` or `.speaker`), - /// so `applyA2dpSpeakerFallback` can skip a redundant `overrideOutputAudioPort` call. - /// That call fires a `.override` route-change notification on every invocation, and on - /// an AirPods disconnect the fallback is invoked once per `recoverAudio()` — which - /// itself fires on every non-skipped route change — so without this guard the override - /// call and the route-change handler ping-pong: the AirPods-disconnect reinitialize - /// loop (each iteration also rebuilds the AVAudioEngine via reconfigure()). + /// Last `overrideOutputAudioPort` value we successfully applied (`.none` or `.speaker`). + /// See `applyA2dpSpeakerFallback`'s doc comment for why this cache exists. /// `nil` = "unknown / assume not applied" — reset at the top of `applyConfiguration()` /// because `setCategory` can reset the override out from under us, and on first run. private var lastAppliedOutputOverride: AVAudioSession.PortOverride? @@ -382,13 +377,8 @@ final class IOSAudioRouter: ObservableObject { // 3. Input & mic-capsule configuration. if captureChannels == .stereo { - // Stereo: enable the built-in mic's .stereo polar pattern AND anchor the input - // route explicitly via setPreferredInput + setInputDataSource. With HFP disabled - // the system routes input to the built-in mic, but without the explicit - // preferred-input anchor the route can collapse during the mode switch - // (.voiceChat → .default) and the output dies. The channel count is carried by the - // engine's mic tap + vc_set_capture_channels, NOT via - // setPreferredInputNumberOfChannels(2) — that call collapses the A2DP output route. + // See configureStereoCapture's doc comment for the full stereo-capture recipe + // and why each step is necessary. configureStereoCapture(session: session) } else if let portId = selectedInputPortId, !portId.isEmpty, let port = session.availableInputs?.first(where: { $0.uid == portId }) { @@ -434,8 +424,6 @@ final class IOSAudioRouter: ObservableObject { do { try builtIn.setPreferredDataSource(stereoSource) try stereoSource.setPreferredPolarPattern(.stereo) - // Anchor the input route explicitly; without it the route can collapse during - // the mode switch (.voiceChat → .default) and the A2DP output dies. try session.setPreferredInput(builtIn) // Commit the data source at the session level. setPreferredDataSource alone only // sets the port-level preference; setInputDataSource makes it the active source. @@ -697,8 +685,6 @@ final class IOSAudioRouter: ObservableObject { } let desired: AVAudioSession.PortOverride = hasExternalOutput ? .none : .speaker if desired == lastAppliedOutputOverride { - // Already in the desired state — calling overrideOutputAudioPort again would just - // fire a redundant `.override` route-change notification (the loop driver). logger.debug("A2DP fallback — desired=\(self.overrideLabel(desired)) already applied, skipping") return } diff --git a/clients/apple/iOS/VoiceCatiOS/SessionState.swift b/clients/apple/iOS/VoiceCatiOS/SessionState.swift index 3690f9b..2b0066d 100644 --- a/clients/apple/iOS/VoiceCatiOS/SessionState.swift +++ b/clients/apple/iOS/VoiceCatiOS/SessionState.swift @@ -54,11 +54,11 @@ final class SessionState { var accounts: [Account] = [] var devices: [Device] = [] - /// Back-reference to the app state. Once `SessionState.init` overwrites `client.onEvent` - /// (`SessionState.swift:87`), `AppState.handleConnectEvent` no longer receives per-session - /// events — so the `.disconnected` event for a LIVE session arrives here in `handleEvent`, - /// not in AppState. This weak ref lets us hand the disconnect back to AppState (which owns - /// the reconnect state machine) so the auto-reconnect fires. Set by AppState on auth success. + /// Back-reference to the app state. Once `SessionState.init` overwrites `client.onEvent`, + /// `AppState.handleConnectEvent` no longer receives per-session events — so the + /// `.disconnected` event for a LIVE session arrives here in `handleEvent`, not in AppState. + /// This weak ref lets us hand the disconnect back to AppState (which owns the reconnect + /// state machine) so the auto-reconnect fires. Set by AppState on auth success. weak var appState: AppState? // MARK: - Reconnect restore state @@ -260,17 +260,18 @@ final class SessionState { // MARK: - Self-channel / server-mute sync /// Sync currentChannelId from the self user's channelId in the user list. Mirrors macOS - /// MainWindowController.swift:461,491,522. The server auto-places every authed user into - /// the Lobby (channel 1) on connect, but without this sync currentChannelId stays 0 and - /// the mic button (gated on currentChannelId == 0) stays permanently dimmed. + /// MainWindowController's bootstrap/event-handling sync. The server auto-places every + /// authed user into the Lobby (channel 1) on connect, but without this sync + /// currentChannelId stays 0 and the mic button (gated on currentChannelId == 0) stays + /// permanently dimmed. private func syncSelfChannel() { if let me = users.first(where: { $0.id == selfUserId }) { currentChannelId = me.channelId } } - /// Apply server-side mute/deafen state — mirrors macOS MainWindowController.swift:693-700. - /// iOS was previously ignoring server mute/deafen entirely. + /// Apply server-side mute/deafen state — mirrors macOS MainWindowController's handling + /// of UserEvent.UPDATED for the self user. private func applyServerMuteState(muted: Bool, deafened: Bool) { if muted && !voiceState.serverMuted { addActivity("You have been server-muted") } if deafened && !voiceState.serverDeafened { addActivity("You have been server-deafened") } diff --git a/clients/apple/iOS/VoiceCatiOS/Views/PerUserTuningView.swift b/clients/apple/iOS/VoiceCatiOS/Views/PerUserTuningView.swift index 114d46a..8ef4e26 100644 --- a/clients/apple/iOS/VoiceCatiOS/Views/PerUserTuningView.swift +++ b/clients/apple/iOS/VoiceCatiOS/Views/PerUserTuningView.swift @@ -48,7 +48,6 @@ struct PerUserTuningView: View { } } .onAppear { - // Load from first stream if available let streams = streamsForUser if let first = streams.first { let (_, state) = session.client.getRemoteStream(userId: user.id, streamId: first.id) diff --git a/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift b/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift index ee0541d..c21af1e 100644 --- a/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift +++ b/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift @@ -44,7 +44,6 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate { private func buildUI() { guard let contentView = window?.contentView else { return } - // Server list let col = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("server")) col.title = "Saved Servers" serverTableView.addTableColumn(col) @@ -61,7 +60,6 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate { serverScrollView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(serverScrollView) - // Buttons row configureButton(addButton, title: "Add…", action: #selector(addClicked)) configureButton(editButton, title: "Edit…", action: #selector(editClicked)) configureButton(removeButton, title: "Remove", action: #selector(removeClicked)) @@ -72,13 +70,11 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate { buttonStack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(buttonStack) - // Status statusLabel.translatesAutoresizingMaskIntoConstraints = false statusLabel.textColor = .secondaryLabelColor statusLabel.setAccessibilityLabel("Connection status") contentView.addSubview(statusLabel) - // Connect button connectButton.title = "Connect" connectButton.bezelStyle = .rounded connectButton.keyEquivalent = "\r" @@ -376,7 +372,3 @@ extension ConnectWindowController: NSTableViewDataSource, NSTableViewDelegate { return cell } } - -// MARK: - Helper - - diff --git a/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift b/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift index 9612370..4104293 100644 --- a/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift +++ b/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift @@ -185,9 +185,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { required init?(coder: NSCoder) { fatalError() } - deinit { - NSLog("[VoiceCatMac] MainWindowController deinit — client and event handlers are gone") - } + deinit {} // MARK: - UI construction @@ -455,10 +453,6 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { } ownPermissions = client.getPermissions() - NSLog("[VoiceCatMac] bootstrap: channels=%d users=%d perms{admin=%d kick=%d} currentChannelId=%u", - channels.count, allUsers.count, - ownPermissions.isAdmin, ownPermissions.canKick, currentChannelId) - // Apply initial output volume (default 80% — matches Windows client) client.setOutputVolume(0.8) @@ -477,9 +471,6 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { // MARK: - Event handling private func handleEvent(_ event: VoiceCatEvent) { - NSLog("[VoiceCatMac] event type=%d result=%d userId=%u channelId=%u streamId=%u text=%@", - event.type.rawValue, event.result.rawValue, event.userId, event.channelId, - event.streamId, event.text ?? "(nil)") switch event.type { case .channelList: channels = client.listChannels() @@ -1067,7 +1058,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { composeField.stringValue = "" } - // MARK: - M5: Moderation helpers + // MARK: - Moderation helpers private func moveUser(_ user: User) { let sheet = MoveUserSheet(channels: channels, currentChannelId: user.channelId) @@ -1241,7 +1232,6 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { presentSheet(sheet) } - /// Open a PM window from the user context menu. private func openPmWindow(_ user: User) { getOrOpenPmWindow(user.id) } @@ -1288,17 +1278,13 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { func windowWillClose(_ notification: Notification) { if let mon = pttMonitor { NSEvent.removeMonitor(mon) } NotificationCenter.default.removeObserver(self) - // Close all PM windows for (_, pmWin) in pmWindows { pmWin.close() } pmWindows.removeAll() - // Close settings window settingsWindowController?.close() settingsWindowController = nil - // Remove app menus we added if let item = voiceMenuItem { NSApp.mainMenu?.removeItem(item) } if let item = messagesMenuItem { NSApp.mainMenu?.removeItem(item) } if let item = adminMenuItem { NSApp.mainMenu?.removeItem(item) } - // Remove Settings menu item + separator from app menu if let appMenu = NSApp.mainMenu?.item(at: 0)?.submenu { if let item = settingsMenuItem { appMenu.removeItem(item) } // Remove the separator we inserted before Quit diff --git a/clients/windows/VoiceCat.App/Audio/ProcessAudioMixer.cs b/clients/windows/VoiceCat.App/Audio/ProcessAudioMixer.cs index 5fa9f4a..3049872 100644 --- a/clients/windows/VoiceCat.App/Audio/ProcessAudioMixer.cs +++ b/clients/windows/VoiceCat.App/Audio/ProcessAudioMixer.cs @@ -105,7 +105,6 @@ public sealed class ProcessAudioMixer : IDisposable for (int i = 0; i < frameLen; i++) { int sum = mix[i] + frame[i]; - // Saturating clamp mix[i] = (short)Math.Clamp(sum, short.MinValue, short.MaxValue); } } diff --git a/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs b/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs index 395251f..cb14eae 100644 --- a/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs +++ b/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs @@ -92,13 +92,8 @@ public partial class ConnectDialog : Form private void BtnConnect_Click(object? sender, EventArgs e) { - Console.WriteLine("[ConnectDialog] BtnConnect_Click fired"); if (lstServers.SelectedItem is not SavedServer server) - { - Console.WriteLine("[ConnectDialog] no SavedServer selected — ignoring click"); return; - } - Console.WriteLine($"[ConnectDialog] selected server: Host={server.Host} Port={server.Port} AuthMode={server.AuthMode}"); try { StartConnect(server); @@ -121,21 +116,15 @@ public partial class ConnectDialog : Form : server.DisplayName; string tofuDir = Path.GetDirectoryName(ServerListStore.TofuStorePath)!; - Console.WriteLine($"[ConnectDialog] tofu store dir: {tofuDir}"); Directory.CreateDirectory(tofuDir); - Console.WriteLine("[ConnectDialog] creating VoiceCatClient..."); _client = new VoiceCatClient("VoiceCat-Windows", VoiceCatClient.VersionString, VcLogLevel.Info, ServerListStore.TofuStorePath); - Console.WriteLine("[ConnectDialog] VoiceCatClient created OK"); _client.EventReceived += OnEvent; _identityDialogShown = false; _pumpTimer.Start(); - Console.WriteLine($"[ConnectDialog] pump timer started, Enabled={_pumpTimer.Enabled}, Interval={_pumpTimer.Interval}"); - Console.WriteLine($"[ConnectDialog] calling Connect({server.Host}, {server.Port})..."); var connectResult = _client.Connect(server.Host, server.Port); - Console.WriteLine($"[ConnectDialog] Connect() returned {connectResult}"); if (connectResult != VcResult.Ok) { lblStatus.Text = $"Connect failed: {connectResult}"; @@ -146,9 +135,7 @@ public partial class ConnectDialog : Form if (server.AuthMode == AuthMode.Guest) { Nickname = string.IsNullOrWhiteSpace(server.LastNickname) ? Environment.UserName : server.LastNickname; - Console.WriteLine($"[ConnectDialog] calling AuthenticateGuest({Nickname})..."); - var authResult = _client.AuthenticateGuest(Nickname); - Console.WriteLine($"[ConnectDialog] AuthenticateGuest() returned {authResult}"); + _client.AuthenticateGuest(Nickname); } else { @@ -169,15 +156,12 @@ public partial class ConnectDialog : Form password = pwDlg.Password; } Nickname = server.SavedUsername ?? ""; - Console.WriteLine($"[ConnectDialog] calling AuthenticateUser({Nickname})..."); - var authResult = _client.AuthenticateUser(server.SavedUsername ?? "", password); - Console.WriteLine($"[ConnectDialog] AuthenticateUser() returned {authResult}"); + _client.AuthenticateUser(server.SavedUsername ?? "", password); } } private void OnEvent(VoiceCatEvent ev) { - Console.WriteLine($"[ConnectDialog] event: {ev}"); switch (ev.Type) { case VcEventType.ConnectionState: @@ -225,24 +209,18 @@ public partial class ConnectDialog : Form private void HandleServerIdentity(VcTofuStatus status, string certFingerprintHex) { - Console.WriteLine($"[ConnectDialog] HandleServerIdentity status={status} fp={certFingerprintHex} alreadyShown={_identityDialogShown}"); if (_identityDialogShown) return; // one decision per connect attempt if (status == VcTofuStatus.Matched) { // Silent success path — no dialog. See ServerIdentityDialog's doc comment. - Console.WriteLine("[ConnectDialog] status=Matched -> auto-confirming, no dialog"); _client!.ConfirmServerIdentity(true); return; } _identityDialogShown = true; - Console.WriteLine("[ConnectDialog] showing ServerIdentityDialog..."); using var dlg = new ServerIdentityDialog(status, certFingerprintHex, _client!.GetServerIdentityDisplay()); - var dlgResult = dlg.ShowDialog(this); - Console.WriteLine($"[ConnectDialog] ServerIdentityDialog closed with {dlgResult}"); - bool accept = dlgResult == DialogResult.OK; - var confirmResult = _client.ConfirmServerIdentity(accept); - Console.WriteLine($"[ConnectDialog] ConfirmServerIdentity({accept}) returned {confirmResult}"); + bool accept = dlg.ShowDialog(this) == DialogResult.OK; + _client.ConfirmServerIdentity(accept); if (!accept) lblStatus.Text = "Server identity rejected."; } diff --git a/clients/windows/VoiceCat.App/Forms/MainForm.cs b/clients/windows/VoiceCat.App/Forms/MainForm.cs index 24fabcc..8bdfc95 100644 --- a/clients/windows/VoiceCat.App/Forms/MainForm.cs +++ b/clients/windows/VoiceCat.App/Forms/MainForm.cs @@ -1114,7 +1114,7 @@ public partial class MainForm : Form OpenPmWindow(dlg.SelectedUserId); } - // ── M5: Moderation helpers ──────────────────────────────────────────────── + // ── Moderation helpers ───────────────────────────────────────────────────── private void UpdateSelfServerMuteState(bool muted, bool deafened) { diff --git a/clients/windows/VoiceCat.App/Forms/PasswordPromptDialog.cs b/clients/windows/VoiceCat.App/Forms/PasswordPromptDialog.cs index abfef83..d1aa56e 100644 --- a/clients/windows/VoiceCat.App/Forms/PasswordPromptDialog.cs +++ b/clients/windows/VoiceCat.App/Forms/PasswordPromptDialog.cs @@ -1,7 +1,7 @@ namespace VoiceCat.App.Forms; /// Small modal for "type a password right now" — used when a saved server's -/// password wasn't remembered, and (Phase E) for password-protected channel joins. +/// password wasn't remembered, and for password-protected channel joins. public partial class PasswordPromptDialog : Form { public string Password => txtPassword.Text; diff --git a/clients/windows/VoiceCat.App/Forms/ServerIdentityDialog.cs b/clients/windows/VoiceCat.App/Forms/ServerIdentityDialog.cs index 153e6b5..f135a07 100644 --- a/clients/windows/VoiceCat.App/Forms/ServerIdentityDialog.cs +++ b/clients/windows/VoiceCat.App/Forms/ServerIdentityDialog.cs @@ -3,7 +3,7 @@ using VoiceCat.Interop; namespace VoiceCat.App.Forms; /// -/// TOFU server-identity confirmation (M4). Shown only for VcTofuStatus.FirstConnect/Mismatch +/// TOFU server-identity confirmation. Shown only for VcTofuStatus.FirstConnect/Mismatch /// — never Matched (that's the silent-success "subsequent connects verify the pin" path /// docs/security.md describes; showing a dialog on every routine reconnect would be exactly /// the "overly chatty" experience this project avoids elsewhere too). diff --git a/clients/windows/VoiceCat.App/Notifications/SoundEvent.cs b/clients/windows/VoiceCat.App/Notifications/SoundEvent.cs index 5643b16..69ba5f1 100644 --- a/clients/windows/VoiceCat.App/Notifications/SoundEvent.cs +++ b/clients/windows/VoiceCat.App/Notifications/SoundEvent.cs @@ -7,17 +7,17 @@ namespace VoiceCat.App.Notifications; /// public enum SoundEvent { - ChannelJoin, // another user joined my channel - ChannelLeave, // another user left my channel + ChannelJoin, + ChannelLeave, ChannelRecv, // channel text message from someone else ChannelSent, // channel text message I sent - PmRecv, // private message received - PmSent, // private message I sent - Login, // connected / authenticated - Logout, // clean disconnect - ConnectionLost, // unexpected disconnect - VoiceOn, // my microphone stream started - VoiceOff, // my microphone stream stopped + PmRecv, + PmSent, + Login, + Logout, + ConnectionLost, + VoiceOn, + VoiceOff, VaStart, // my voice-activity began (off by default) VaStop, // my voice-activity ended (off by default) Ptt, // push-to-talk engaged (off by default) diff --git a/clients/windows/VoiceCat.App/Program.cs b/clients/windows/VoiceCat.App/Program.cs index 19ccacb..47d2af3 100644 --- a/clients/windows/VoiceCat.App/Program.cs +++ b/clients/windows/VoiceCat.App/Program.cs @@ -7,30 +7,21 @@ internal static class Program [STAThread] private static void Main() { - // Diagnostic-logging-only for now (manual debugging session) — every exception that - // would otherwise be silently caught by WinForms' default message-loop handling (or - // crash with no visible cause) gets printed to stdout/stderr first. + // Surface exceptions that WinForms' default message-loop handling would otherwise + // swallow silently (or crash with no visible cause). Application.ThreadException += (_, e) => Console.Error.WriteLine($"[UNHANDLED ThreadException] {e.Exception}"); AppDomain.CurrentDomain.UnhandledException += (_, e) => Console.Error.WriteLine($"[UNHANDLED AppDomain exception] {e.ExceptionObject}"); - Console.WriteLine("VoiceCat.App starting..."); ApplicationConfiguration.Initialize(); using var connectDialog = new ConnectDialog(); - Console.WriteLine("Showing ConnectDialog..."); var result = connectDialog.ShowDialog(); - Console.WriteLine($"ConnectDialog closed with DialogResult={result}, ConnectedClient={(connectDialog.ConnectedClient is null ? "null" : "set")}"); if (result != DialogResult.OK || connectDialog.ConnectedClient is null) - { - Console.WriteLine("Exiting (cancelled or no connected client)."); return; - } - Console.WriteLine("Launching MainForm..."); Application.Run(new MainForm(connectDialog.ConnectedClient, connectDialog.SelfUserId, connectDialog.Nickname, connectDialog.ServerName)); - Console.WriteLine("MainForm closed. Exiting."); } } diff --git a/clients/windows/VoiceCat.Interop.Tests/VoiceCatClientSmokeTests.cs b/clients/windows/VoiceCat.Interop.Tests/VoiceCatClientSmokeTests.cs index 9403dd6..f7067c5 100644 --- a/clients/windows/VoiceCat.Interop.Tests/VoiceCatClientSmokeTests.cs +++ b/clients/windows/VoiceCat.Interop.Tests/VoiceCatClientSmokeTests.cs @@ -23,10 +23,10 @@ public sealed class VoiceCatClientSmokeTests : IDisposable _tempDir = Path.Combine(Path.GetTempPath(), "vc_csharp_smoke_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); - string serverExe = Path.Combine(FindRepoRoot(), "build", "m1-dev", "bin", "voicecat-server.exe"); + string serverExe = Path.Combine(FindRepoRoot(), "build", "dev", "bin", "voicecat-server.exe"); Assert.True(File.Exists(serverExe), - $"voicecat-server.exe not found at '{serverExe}' — build the m1-dev preset first " + - "(cmake --preset m1-dev && cmake --build --preset m1-dev)."); + $"voicecat-server.exe not found at '{serverExe}' — build the dev preset first " + + "(cmake --preset dev && cmake --build --preset dev)."); var psi = new ProcessStartInfo(serverExe) { @@ -56,9 +56,9 @@ public sealed class VoiceCatClientSmokeTests : IDisposable Assert.True(port is not null, "voicecat-server.exe did not report a bound TCP port within 10s."); _port = port!.Value; - // M5: provision a known admin account so we can exercise moderation wrappers end-to-end. - string adminExe = Path.Combine(FindRepoRoot(), "build", "m1-dev", "bin", "voicecat-admin.exe"); - Assert.True(File.Exists(adminExe), "voicecat-admin.exe not found — build the m1-dev preset."); + // Provision a known admin account so we can exercise moderation wrappers end-to-end. + string adminExe = Path.Combine(FindRepoRoot(), "build", "dev", "bin", "voicecat-admin.exe"); + Assert.True(File.Exists(adminExe), "voicecat-admin.exe not found — build the dev preset."); var adminPsi = new ProcessStartInfo(adminExe) { Arguments = $"--data-dir \"{_tempDir}\" account add admin2 --admin --password testpassword123", @@ -140,14 +140,14 @@ public sealed class VoiceCatClientSmokeTests : IDisposable var channels = client.ListChannels(); Assert.Contains(channels, c => c.Id == 1 && c.Name == "Lobby"); - // M5: permissions getter round-trip. + // Permissions getter round-trip. var perms = client.GetPermissions(); Assert.False(perms.IsAdmin); Assert.False(perms.CanKick); - // M5: moderation request wrappers queue without error. As a guest, account listing + // Moderation request wrappers queue without error. As a guest, account listing // is rejected by the server with a GenericResult, which proves the wrapper path works - // end-to-end and that the new event type is delivered through P/Invoke. + // end-to-end and that the event type is delivered through P/Invoke. Assert.Equal(VcResult.Ok, client.RequestAccountList()); Assert.True(PumpUntil(client, () => events.Any(e => e.Type == VcEventType.GenericResult), 3000), diff --git a/clients/windows/VoiceCat.Interop/Enums.cs b/clients/windows/VoiceCat.Interop/Enums.cs index 8fc9562..e9817e2 100644 --- a/clients/windows/VoiceCat.Interop/Enums.cs +++ b/clients/windows/VoiceCat.Interop/Enums.cs @@ -37,7 +37,7 @@ public enum VcConnectionState TlsHandshake = 2, Authenticating = 3, Connected = 4, - /// M4: handshake succeeded, waiting on vc_confirm_server_identity(). + /// Handshake succeeded, waiting on vc_confirm_server_identity(). VerifyingIdentity = 5, } @@ -84,13 +84,13 @@ public enum VcEventType TalkState = 9, Error = 10, Disconnected = 11, - /// M4: reply to VoiceCatClient.JoinChannelAsync's underlying vc_join_channel. + /// Reply to VoiceCatClient.JoinChannelAsync's underlying vc_join_channel. JoinResult = 12, - /// M4: the TOFU server-identity gate — see VcTofuStatus. + /// The TOFU server-identity gate — see VcTofuStatus. ServerIdentity = 13, - /// M5: async result for moderation/admin/channel operations. + /// Async result for moderation/admin/channel operations. GenericResult = 14, - /// M5: reply to VoiceCatClient.RequestAccountList — call ListAccounts() to read. + /// Reply to VoiceCatClient.RequestAccountList — call ListAccounts() to read. AccountList = 15, /// Voice-plane subscription state. u32a = 1 (subscribed) or 0 (unsubscribed). VoiceState = 16, diff --git a/clients/windows/VoiceCat.Interop/NativeMethods.cs b/clients/windows/VoiceCat.Interop/NativeMethods.cs index 757abce..96438bf 100644 --- a/clients/windows/VoiceCat.Interop/NativeMethods.cs +++ b/clients/windows/VoiceCat.Interop/NativeMethods.cs @@ -148,7 +148,7 @@ internal static partial class NativeMethods [LibraryImport(LibName)] internal static partial void vc_free_device_list(ref VcDeviceListNative list); - // ── M4: channel / user / stream snapshot getters ──────────────────────────────────────── + // ── Channel / user / stream snapshot getters ──────────────────────────────────────────── [LibraryImport(LibName)] internal static partial VcResult vc_list_channels(nint c, out VcChannelListNative outList); @@ -168,7 +168,7 @@ internal static partial class NativeMethods [LibraryImport(LibName)] internal static partial void vc_free_stream_summary_list(ref VcStreamSummaryListNative list); - // ── M4: TOFU server-identity gate ─────────────────────────────────────────────────────── + // ── TOFU server-identity gate ─────────────────────────────────────────────────────────── [LibraryImport(LibName)] internal static partial VcResult vc_confirm_server_identity(nint c, int accept); @@ -176,7 +176,7 @@ internal static partial class NativeMethods internal static partial VcResult vc_get_server_identity_display(nint c, nint outBuf, nuint bufCap, out nuint outLen); - // ── M5: Moderation & admin ───────────────────────────────────────────────────────────── + // ── Moderation & admin ─────────────────────────────────────────────────────────────────── [LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)] internal static partial VcResult vc_kick_user(nint c, uint userId, string? reason); diff --git a/clients/windows/VoiceCat.Interop/VoiceCatClient.cs b/clients/windows/VoiceCat.Interop/VoiceCatClient.cs index c3fa02d..b9071d7 100644 --- a/clients/windows/VoiceCat.Interop/VoiceCatClient.cs +++ b/clients/windows/VoiceCat.Interop/VoiceCatClient.cs @@ -95,10 +95,6 @@ public sealed class VoiceCatClient : IDisposable internal void EnqueueEvent(VoiceCatEvent ev) { - // Temporary diagnostic (manual debugging session) — confirms the native callback - // chain (UnmanagedCallersOnly -> GCHandle resolve -> here) actually fires, independent - // of whether the UI-thread drain (PumpEvents) ever sees it. - Console.WriteLine($"[VoiceCatClient] EnqueueEvent (native thread): {ev}"); _events.Writer.TryWrite(ev); } internal void EnqueueLevel(uint streamId, float rms) => _latestLevels[streamId] = rms; @@ -116,7 +112,7 @@ public sealed class VoiceCatClient : IDisposable public VcResult AuthenticateUser(string username, string password) => NativeMethods.vc_authenticate_user(_handle.DangerousGetHandle(), username, password); - // ── TOFU server-identity gate (M4) ────────────────────────────────────────────────────── + // ── TOFU server-identity gate ─────────────────────────────────────────────────────────── public VcResult ConfirmServerIdentity(bool accept) => NativeMethods.vc_confirm_server_identity(_handle.DangerousGetHandle(), accept ? 1 : 0); @@ -143,9 +139,7 @@ public sealed class VoiceCatClient : IDisposable // ── Channels ───────────────────────────────────────────────────────────────────────── /// Result arrives as a VcEventType.JoinResult event, not via this return value - /// (which only reflects "request queued" — see voicecat.h's vc_join_channel doc comment). - /// NOTE: no in-tree channel has a server-side password to check yet (M5+ feature) — this - /// path is wired but not yet exercisable end-to-end. + /// (which only reflects "request queued" — see voicecat.h's vc_join_channel doc comment). public VcResult JoinChannel(uint channelId, string? password = null) => NativeMethods.vc_join_channel(_handle.DangerousGetHandle(), channelId, password); @@ -290,7 +284,7 @@ public sealed class VoiceCatClient : IDisposable public VcResult SetPcmSink(nint cb, IntPtr user) => NativeMethods.vc_set_pcm_sink(_handle.DangerousGetHandle(), cb, user); - // ── M5: Moderation & admin ─────────────────────────────────────────────────────────── + // ── Moderation & admin ─────────────────────────────────────────────────────────────── public VcResult KickUser(uint userId, string? reason = null) => NativeMethods.vc_kick_user(_handle.DangerousGetHandle(), userId, reason); diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7d3d500..49c3d60 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -6,7 +6,7 @@ file(GLOB_RECURSE VOICECAT_SOURCES CONFIGURE_DEPENDS if(VOICECAT_BUILD_SHARED) add_library(voicecat SHARED ${VOICECAT_SOURCES}) if(WIN32 AND MINGW) - # M4: the C# client only ships voicecat.dll itself — no MinGW runtime DLLs alongside + # The C# client only ships voicecat.dll itself — no MinGW runtime DLLs alongside # it. x64-mingw-static only statically links vcpkg's OWN library deps (protobuf, # sodium, mbedTLS, ...); the GCC/MinGW runtime stays dynamic by default # (libgcc_s_seh-1.dll/libwinpthread-1.dll/libstdc++-6.dll — confirmed via `objdump -p` diff --git a/core/include/voicecat.h b/core/include/voicecat.h index 5f93c6e..b9f4d69 100644 --- a/core/include/voicecat.h +++ b/core/include/voicecat.h @@ -8,9 +8,8 @@ * Design: docs/architecture.md §4. Everything here is async + event-driven — calls return * immediately and results/state changes arrive via the vc_callbacks.on_event callback. * - * STATUS: real. Control plane, voice, multi-stream, device enumeration, VAD/PTT, and stereo - * playback all work via core/src/core/client.cpp. webrtc AEC/NS/AGC remains an inert passthrough - * (no Windows/MSVC port upstream — docs/voice.md §8/§11, PROGRESS.md). + * webrtc AEC/NS/AGC remains an inert passthrough (no Windows/MSVC port upstream — see + * docs/voice.md §8/§11). */ #ifndef VOICECAT_H #define VOICECAT_H @@ -81,7 +80,7 @@ typedef enum vc_connection_state { VC_STATE_TLS_HANDSHAKE = 2, VC_STATE_AUTHENTICATING = 3, VC_STATE_CONNECTED = 4, - /* M4: between TLS_HANDSHAKE and AUTHENTICATING — the handshake succeeded and the core is + /* Between TLS_HANDSHAKE and AUTHENTICATING — the handshake succeeded and the core is * waiting for vc_confirm_server_identity() (see VC_EVENT_SERVER_IDENTITY below). Appended * at the end (not inserted) to keep existing enum values stable — additive-only ABI. */ VC_STATE_VERIFYING_IDENTITY = 5, @@ -125,7 +124,7 @@ typedef enum vc_event_type { VC_EVENT_TALK_STATE = 9, /* user_id, stream_id, u32a = talking(0/1) */ VC_EVENT_ERROR = 10, /* result, text */ VC_EVENT_DISCONNECTED = 11, /* result, text = reason */ - /* M4 additions — appended, not inserted, to keep existing enum values stable. */ + /* Appended, not inserted, to keep existing enum values stable. */ VC_EVENT_JOIN_RESULT = 12, /* result (VC_OK/VC_ERR_*), channel_id, text = error on failure. Reply to vc_join_channel(). */ VC_EVENT_SERVER_IDENTITY = 13, /* u32a = vc_tofu_status, text = hex-encoded TLS leaf-cert @@ -134,7 +133,7 @@ typedef enum vc_event_type { attempt, right after the TLS handshake succeeds. The connection is held open until vc_confirm_server_identity() is called. */ - /* M5 additions — appended, not inserted. */ + /* Appended, not inserted. */ VC_EVENT_GENERIC_RESULT = 14, /* result, u32a = server error code, text = message. Reply to vc_kick_user/vc_ban_user/vc_set_permission/ vc_move_user/vc_create_channel/vc_edit_channel/ @@ -147,7 +146,7 @@ typedef enum vc_event_type { (vc_user) carries per-user voice_subscribed. */ } vc_event_type; -/* TOFU server-identity classification (M4) — see VC_EVENT_SERVER_IDENTITY and +/* TOFU server-identity classification — see VC_EVENT_SERVER_IDENTITY and * vc_confirm_server_identity. Pins the TLS leaf certificate's own SHA-256 fingerprint * (verifiable directly from the handshake), NOT the declared Ed25519 * server_identity_fingerprint from ServerHello — the TLS cert and the server's Ed25519 @@ -194,7 +193,7 @@ typedef struct vc_config { const char* client_name; /* e.g. "VoiceCat-macOS" */ const char* client_version; /* e.g. "0.0.1" */ vc_log_level log_level; - /* M4, optional (added at the end — existing brace-initialized callers default this to + /* Optional (added at the end — existing brace-initialized callers default this to * NULL, no source change needed). Path to the TOFU pin file (see VC_EVENT_SERVER_IDENTITY/ * vc_confirm_server_identity). NULL = a built-in relative default * ("./voicecat_tofu_pins.txt") so existing tests need no real persistence. A real app @@ -232,7 +231,7 @@ typedef struct vc_audio_config { int dred; /* bool — Deep REDundancy (Opus 1.6), off by default */ } vc_audio_config; -/* M5: permission bitset (mirrors protocol Permissions). */ +/* Permission bitset (mirrors protocol Permissions). */ typedef struct vc_permissions { int can_create_temp_channel; /* bool */ int can_kick; /* bool */ @@ -242,7 +241,7 @@ typedef struct vc_permissions { int is_admin; /* bool */ } vc_permissions; -/* M5: account entry (reply to vc_list_accounts / vc_get_account_list). */ +/* Account entry (reply to vc_list_accounts / vc_get_account_list). */ typedef struct vc_account { const char* username; int is_admin; /* bool */ @@ -255,7 +254,7 @@ typedef struct vc_account_list { size_t count; } vc_account_list; -/* M5: channel creation/edition descriptor. */ +/* Channel creation/edition descriptor. */ typedef struct vc_channel_info { uint32_t id; /* 0 = new channel for create */ uint32_t parent_id; /* 0 = root */ @@ -280,7 +279,7 @@ typedef struct vc_device_list { size_t count; } vc_device_list; -/* ── Channel / user / stream snapshots (M4 — for the channel-tree/user-list UI) ─────────── +/* ── Channel / user / stream snapshots (for the channel-tree/user-list UI) ──────────────── * Pull-based: re-call after VC_EVENT_CHANNEL_LIST / VC_EVENT_USER_JOINED / _LEFT / _UPDATED to * refresh — there is no push variant; those events just mean "go look". Same ownership * contract as vc_device/vc_device_list above: core-allocated, caller frees with the matching @@ -363,10 +362,8 @@ VC_API vc_result vc_authenticate_user(vc_client* c, const char* username, /* ── Channels ─────────────────────────────────────────────────────────────── */ /* Result arrives as VC_EVENT_JOIN_RESULT, not a return value beyond "request queued". `password` - * is forwarded to the server's JoinChannelRequest.password for channels with - * vc_channel.password_protected set; NOTE (M4): no in-tree channel currently has a server-side - * password to check against — channel creation/passwords are a future (M5+) feature, so this - * path is wired but not yet exercisable end-to-end. */ + * is forwarded to the server's JoinChannelRequest.password and checked against the channel's + * stored password for channels with vc_channel.password_protected set. */ VC_API vc_result vc_join_channel(vc_client* c, uint32_t channel_id, const char* password /* nullable */); VC_API vc_result vc_leave_channel(vc_client* c); @@ -468,7 +465,7 @@ VC_API vc_result vc_set_capture_channels(vc_client* c, uint32_t stream_id, uint3 * music / relay), soundboards, DAW integration. Works for any stream kind (MIC / * SCREEN_AUDIO / AUX_DEVICE). Thread-safe; may be called from any thread. * - * Replaces vc_test_inject_capture (deprecated alias, see below). */ + * Replaces vc_test_inject_capture (deprecated alias, see above). */ VC_API vc_result vc_stream_feed_pcm(vc_client* c, uint32_t stream_id, const int16_t* pcm, size_t samples_per_channel, uint32_t channels); @@ -538,7 +535,7 @@ VC_API vc_result vc_send_text(vc_client* c, vc_text_scope scope, uint32_t target VC_API vc_result vc_list_devices(vc_client* c, vc_device_kind kind, vc_device_list* out); VC_API void vc_free_device_list(vc_device_list* list); -/* ── Channel / user / stream enumeration (M4; mirrors vc_list_devices above) ─────────────── */ +/* ── Channel / user / stream enumeration (mirrors vc_list_devices above) ──────────────────── */ VC_API vc_result vc_list_channels(vc_client* c, vc_channel_list* out); VC_API void vc_free_channel_list(vc_channel_list* list); @@ -551,7 +548,7 @@ VC_API vc_result vc_list_user_streams(vc_client* c, uint32_t user_id, vc_stream_summary_list* out); VC_API void vc_free_stream_summary_list(vc_stream_summary_list* list); -/* ── TOFU server-identity confirmation (M4) — see VC_EVENT_SERVER_IDENTITY/vc_tofu_status ── */ +/* ── TOFU server-identity confirmation — see VC_EVENT_SERVER_IDENTITY/vc_tofu_status ──────── */ /* Accept or reject the pending server-identity check for the in-progress connect(). Must be * called after a VC_EVENT_SERVER_IDENTITY event; the io_thread_ holds the connection open * (ClientHello/auth deferred) until this is called, up to a generous internal timeout (after @@ -570,7 +567,7 @@ VC_API vc_result vc_confirm_server_identity(vc_client* c, int accept /* bool */) VC_API vc_result vc_get_server_identity_display(vc_client* c, char* out_buf, size_t buf_cap, size_t* out_len); -/* ── M5: Moderation & admin ───────────────────────────────────────────────── +/* ── Moderation & admin ─────────────────────────────────────────────────────── * All calls are async; the result arrives as VC_EVENT_GENERIC_RESULT (or * VC_EVENT_ACCOUNT_LIST for vc_list_accounts). They require VC_STATE_CONNECTED and, * on the server side, the appropriate permission. */ diff --git a/core/proto/voicecat.proto b/core/proto/voicecat.proto index ce5c47d..27b97fc 100644 --- a/core/proto/voicecat.proto +++ b/core/proto/voicecat.proto @@ -122,7 +122,7 @@ message User { bool self_deafened = 6; bool server_muted = 7; repeated StreamInfo streams = 8; - bool server_deafened = 9; // M5: server-imposed deafen + bool server_deafened = 9; // server-imposed deafen bool voice_subscribed = 10; // true when the user is on the voice plane (hears + can send) } @@ -194,7 +194,7 @@ message UserEvent { Kind kind = 1; User user = 2; uint32 left_id = 3; - string reason = 4; // M5: kick/ban reason for LEFT events + string reason = 4; // kick/ban reason for LEFT events } message SubscribeRequest { repeated uint32 channel_ids = 1; bool presence = 2; } diff --git a/core/src/audio/audio_engine.h b/core/src/audio/audio_engine.h index 069a8ca..34f27d9 100644 --- a/core/src/audio/audio_engine.h +++ b/core/src/audio/audio_engine.h @@ -7,7 +7,7 @@ * * REAL-TIME RULE: audio-callback threads never allocate, lock, or block (architecture.md §3). * The JitterBuffer and per-stream maps are accessed only under a try_lock; a failed lock - * causes PLC for that period (acceptable for M2; lock-free ring buffer is the M3 upgrade). + * causes PLC for that period (acceptable today; a lock-free ring buffer would remove even that). */ #ifndef VOICECAT_AUDIO_AUDIO_ENGINE_H #define VOICECAT_AUDIO_AUDIO_ENGINE_H @@ -134,11 +134,11 @@ class AudioEngine { // Callback type for encoded capture frames ready to be sent. `kind` identifies which // local stream this PCM belongs to (a vc_stream_kind value; 0 = MIC for the real capture // device, which is always the "primary" tap). `channels` is the channel count of the PCM - // buffer (1 = mono, 2 = stereo interleaved) — the mic capture device is mono in v1, but + // buffer (1 = mono, 2 = stereo interleaved) — the mic capture device is mono by default, but // the WASAPI loopback path (SCREEN_AUDIO) captures in the channel's mode when stereo, so - // the encoder sees real interleaved L/R PCM rather than a mono upmix. M3: multiple - // concurrent local streams are possible (e.g. MIC + SCREEN_AUDIO), each fed via its own - // injection tap (see inject_capture) since there is only one real hardware capture device. + // the encoder sees real interleaved L/R PCM rather than a mono upmix. Multiple concurrent + // local streams are possible (e.g. MIC + SCREEN_AUDIO), each fed via its own injection tap + // (see inject_capture) since there is only one real hardware capture device. using CaptureCallback = std::function; @@ -459,7 +459,7 @@ class AudioEngine { // never called. See on_playback's decode loop. int64_t plc_samples_since_real = 0; - // M3: listener-chosen, local-only noise reduction (docs/voice.md §10). Lazily + // Listener-chosen, local-only noise reduction (docs/voice.md §10). Lazily // created only when enabled — bounded by how many remote streams this listener // subscribes to, so no separate instance cap is needed. bool noise_reduction_enabled = false; @@ -485,7 +485,7 @@ class AudioEngine { uint32_t user_id = 0; uint32_t stream_id = 0; - // M3: talk-indicator edge detection (docs/voice.md §7) — updated by push_recv_frame + // Talk-indicator edge detection (docs/voice.md §7) — updated by push_recv_frame // (already off the real-time audio thread), polled by poll_talk_transitions(). std::atomic last_voice_ms{0}; bool talking = false; diff --git a/core/src/core/client.cpp b/core/src/core/client.cpp index be9840e..8cad54f 100644 --- a/core/src/core/client.cpp +++ b/core/src/core/client.cpp @@ -40,10 +40,10 @@ std::vector make_frame(const voicecat::v1::Envelope& env) { } // namespace -// ── vc_client M1 implementation ─────────────────────────────────────────────── +// ── vc_client implementation ─────────────────────────────────────────────────── vc_client::vc_client(const vc_config& cfg, vc_callbacks cb) : cfg_(cfg), cb_(cb) { - // M4 TOFU: NULL/empty tofu_store_path falls back to a relative default so existing tests + // TOFU: NULL/empty tofu_store_path falls back to a relative default so existing tests // (which never set this field) keep working without real per-user persistence. std::filesystem::path tofu_path = (cfg.tofu_store_path && cfg.tofu_store_path[0]) ? std::filesystem::path(cfg.tofu_store_path) @@ -228,7 +228,7 @@ void vc_client::run_io(std::string host, uint16_t port) { } } - // ── TOFU server-identity gate (M4) ────────────────────────────────────── + // ── TOFU server-identity gate ──────────────────────────────────────────── // Pins the TLS leaf cert's own fingerprint (real, verifiable right here from the // handshake) — NOT the declared Ed25519 server_identity_fingerprint from ServerHello, // which hasn't even arrived yet at this point (it's sent *inside* this now-established @@ -542,7 +542,7 @@ void vc_client::handle_envelope(const voicecat::v1::Envelope& env) { void vc_client::handle_server_hello(const voicecat::v1::ServerHello& msg, uint64_t /*req_id*/) { server_udp_port_ = static_cast(msg.udp_port()); - // M4: stash the declared Ed25519 fingerprint for vc_get_server_identity_display() — + // Stash the declared Ed25519 fingerprint for vc_get_server_identity_display() — // display-only, not the TOFU-pinned value (that's the TLS cert fingerprint, gated before // ClientHello was even sent — see the TOFU block above in run_io()). { @@ -687,7 +687,6 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) { ev.user_id = user.id(); ev.channel_id = user.channel_id(); - static const char* nick_buf_ptr = nullptr; std::string nick = user.nickname(); switch (ue.kind()) { @@ -714,7 +713,7 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) { case voicecat::v1::UserEvent::UPDATED: ev.type = VC_EVENT_USER_UPDATED; emit(ev); - // M5: if this is an update to our own user, reflect server-mute/deafen locally. + // If this is an update to our own user, reflect server-mute/deafen locally. if (user.id() == self_user_id_) { server_muted_.store(user.server_muted(), std::memory_order_release); server_deafened_.store(user.server_deafened(), std::memory_order_release); @@ -731,7 +730,6 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) { default: break; } - (void)nick_buf_ptr; } void vc_client::handle_text_message(const voicecat::v1::TextMessage& msg) { @@ -799,9 +797,7 @@ vc_result vc_client::join_channel(uint32_t channel_id, const char* password) { req.set_request_id(next_req_id_++); auto* jc = req.mutable_join_channel(); jc->set_channel_id(channel_id); - // See voicecat.h's vc_join_channel doc comment: wired through to the wire message, but no - // in-tree channel has a server-side password to check yet (no channel-creation feature - // exists — M5+). + // See voicecat.h's vc_join_channel doc comment. if (password) jc->set_password(password); queue_envelope(req); return VC_OK; @@ -828,7 +824,7 @@ vc_result vc_client::send_text(vc_text_scope scope, uint32_t target_id, const ch return VC_OK; } -// ── M2: UDP binding ─────────────────────────────────────────────────────────── +// ── UDP binding ──────────────────────────────────────────────────────────────── void vc_client::start_udp_binding() { voicecat::v1::Envelope req; @@ -977,8 +973,8 @@ int64_t client_now_ms() { } // Builds an OpusParams from a wire AudioConfig, applying the same field-by-field mapping on -// both the send (local-stream encoder) and receive (remote-stream decoder) paths — fixes the -// M2 gap where mode/dtx/complexity/application were silently dropped. +// both the send (local-stream encoder) and receive (remote-stream decoder) paths — fixes a +// gap where mode/dtx/complexity/application were silently dropped. voicecat::codec::OpusParams opus_params_from_audio_config(const voicecat::v1::AudioConfig& a) { voicecat::codec::OpusParams p; // Opus always runs at 48 kHz internally (docs/voice.md §3): the whole AudioEngine clock is @@ -1038,8 +1034,8 @@ void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples, int auto it = local_streams_.find(kind); if (it == local_streams_.end() || !it->second.active.load(std::memory_order_acquire)) return; // "Mic muted" only gates the MIC stream — a concurrently-running SCREEN_AUDIO share keeps - // playing while the user's mic is muted (docs §M3 scope decision). - // M5: server-mute is also a hard gate on MIC transmission. + // playing while the user's mic is muted (scope decision — see docs/voice.md). + // Server-mute is also a hard gate on MIC transmission. if (kind == static_cast(VC_STREAM_MIC) && (self_mic_muted_.load(std::memory_order_acquire) || server_muted_.load(std::memory_order_acquire))) return; @@ -1308,7 +1304,7 @@ void vc_client::sync_remote_streams(const voicecat::v1::User& user) { } } -// ── M2: stream / device control ────────────────────────────────────────────── +// ── Stream / device control ──────────────────────────────────────────────────── vc_result vc_client::stream_start(const vc_stream_desc& desc, uint32_t* out_stream_id) { if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED; @@ -1884,7 +1880,7 @@ vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) { return VC_OK; } -// ── M4: channel/user/stream snapshot getters ───────────────────────────────── +// ── Channel/user/stream snapshot getters ────────────────────────────────────── vc_result vc_client::list_channels(vc_channel_list* out) { std::lock_guard lk(session_model_mu_); @@ -1962,7 +1958,7 @@ vc_result vc_client::list_user_streams(uint32_t user_id, vc_stream_summary_list* return VC_OK; } -// ── M4: TOFU server-identity gate ───────────────────────────────────────────── +// ── TOFU server-identity gate ─────────────────────────────────────────────────── vc_result vc_client::confirm_server_identity(bool accept) { std::lock_guard lk(tofu_mu_); @@ -1987,7 +1983,7 @@ vc_result vc_client::get_server_identity_display(char* out_buf, size_t buf_cap, return VC_OK; } -// ── M5: Moderation & admin ─────────────────────────────────────────────────── +// ── Moderation & admin ───────────────────────────────────────────────────────── vc_result vc_client::kick_user(uint32_t user_id, const char* reason) { if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED; diff --git a/core/src/core/client.h b/core/src/core/client.h index a6ce288..d37aab7 100644 --- a/core/src/core/client.h +++ b/core/src/core/client.h @@ -68,16 +68,16 @@ struct vc_client { vc_result list_devices(vc_device_kind kind, vc_device_list* out); - // M4: channel/user/stream snapshot getters (read session_model_; see voicecat.h). + // Channel/user/stream snapshot getters (read session_model_; see voicecat.h). vc_result list_channels(vc_channel_list* out); vc_result list_users(vc_user_list* out); vc_result list_user_streams(uint32_t user_id, vc_stream_summary_list* out); - // M4: TOFU server-identity gate (see voicecat.h's VC_EVENT_SERVER_IDENTITY doc comment). + // TOFU server-identity gate (see voicecat.h's VC_EVENT_SERVER_IDENTITY doc comment). vc_result confirm_server_identity(bool accept); vc_result get_server_identity_display(char* out_buf, size_t buf_cap, size_t* out_len); - // M3: effective Opus config for a (user_id, stream_id) — our own pending/active local + // Effective Opus config for a (user_id, stream_id) — our own pending/active local // streams, or any peer's broadcast StreamInfo.audio. vc_result get_stream_audio_config(uint32_t user_id, uint32_t stream_id, vc_audio_config* out); @@ -97,7 +97,7 @@ struct vc_client { // TEST-ONLY (see voicecat.h) — deprecated alias for stream_feed_pcm(..., channels=1). vc_result test_inject_capture(uint32_t stream_id, const int16_t* pcm, size_t samples); - // M5: moderation & admin. + // Moderation & admin. vc_result kick_user(uint32_t user_id, const char* reason); vc_result ban_user(uint32_t user_id, const char* reason, uint64_t expires_unix_ms); vc_result set_permission(uint32_t user_id, const vc_permissions* perms); @@ -123,7 +123,7 @@ struct vc_client { vc_config cfg_{}; vc_callbacks cb_{}; - // ── M1: TCP/TLS control channel ───────────────────────────────────────────── + // ── TCP/TLS control channel ─────────────────────────────────────────────────── std::atomic state_net_{VC_STATE_DISCONNECTED}; // Blocking I/O thread (one per vc_client lifetime) @@ -187,17 +187,17 @@ struct vc_client { std::atomic last_udp_keepalive_ms_{0}; // Client-side session model. Mutated only on io_thread_ (handle_server_state/ - // handle_user_event/handle_channel_event), but read from any thread via the M4 + // handle_user_event/handle_channel_event), but read from any thread via the // list_channels/list_users/list_user_streams getters — session_model_mu_ guards both. voicecat::session::SessionModel session_model_; mutable std::mutex session_model_mu_; - // M5: last ListAccountsResult snapshot, populated on io_thread_ when + // Last ListAccountsResult snapshot, populated on io_thread_ when // VC_EVENT_ACCOUNT_LIST fires and read by vc_get_account_list on caller threads. std::vector last_account_list_; mutable std::mutex account_list_mu_; - // ── M4: TOFU server-identity gate ─────────────────────────────────────────── + // ── TOFU server-identity gate ───────────────────────────────────────────────── std::unique_ptr tofu_store_; // owns the pin file std::mutex tofu_mu_; std::condition_variable tofu_cv_; @@ -205,7 +205,7 @@ struct vc_client { bool tofu_accept_{false}; std::string pending_identity_fp_hex_; // ServerHello's Ed25519 fp, display-only - // ── M2: UDP / media plane ──────────────────────────────────────────────────── + // ── UDP / media plane ────────────────────────────────────────────────────────── std::array udp_token_{}; uint16_t server_udp_port_{0}; std::string udp_host_; @@ -220,9 +220,8 @@ struct vc_client { voicecat::audio::AudioEngine audio_engine_; - // M3: one LocalStream per concurrently-active stream kind (MIC/SCREEN_AUDIO/AUX_DEVICE - // are each singletons for a given client — see docs/roadmap.md §M3). Replaces the M2 - // single-stream fields (local_encoder_/local_stream_active_/etc). + // One LocalStream per concurrently-active stream kind (MIC/SCREEN_AUDIO/AUX_DEVICE + // are each singletons for a given client). struct LocalStream { voicecat::codec::OpusEncoder encoder; std::atomic active{false}; @@ -291,7 +290,7 @@ struct vc_client { mutable std::mutex remote_streams_mu_; std::unordered_map> remote_streams_; - // M3: talk-indicator polling thread (separate from udp_thread_ / the miniaudio callback + // Talk-indicator polling thread (separate from udp_thread_ / the miniaudio callback // thread — see architecture.md §3 real-time rule). std::thread talk_timer_thread_; std::atomic talk_timer_stop_{false}; @@ -307,10 +306,10 @@ struct vc_client { std::atomic server_muted_{false}; std::atomic server_deafened_{false}; - // M5: permissions from last AuthResult. + // Permissions from last AuthResult. vc_permissions own_permissions_{}; - // Follow-up to M3: send-side input gate (docs/voice.md §11). MIC-only — SCREEN_AUDIO/ + // Send-side input gate (docs/voice.md §11). MIC-only — SCREEN_AUDIO/ // AUX_DEVICE are never gated (see PROGRESS.md for the rationale). Pure local state, no // protocol traffic. mic_vad_ is constructed once the MIC stream's StreamAnnounceResult // lands (handle_stream_announce_result, on io_thread_ — not the RT capture callback). @@ -366,7 +365,7 @@ struct vc_client { // Stop every active local stream (used on voice leave — emits STREAM_STOPPED for each). void stop_all_local_streams(); - // ── M2: UDP / media helpers ────────────────────────────────────────────────── + // ── UDP / media helpers ──────────────────────────────────────────────────────── // Kicks off TCP UdpBinding request; called once after a successful AuthResult. void start_udp_binding(); // Opens the UDP socket, sends the plaintext bootstrap packet, starts udp_thread_. @@ -378,7 +377,7 @@ struct vc_client { // (docs/voice.md §6). Plaintext — no AEAD — to avoid racing the audio thread's seal(). void send_udp_keepalive(); // capture_cb passed to audio_engine_.start(): encode + seal + send one frame for the - // given local stream `kind` (M3: multiple concurrent local streams are possible). + // given local stream `kind` (multiple concurrent local streams are possible). void on_capture_frame(int kind, const int16_t* pcm, int samples, int channels); // Encode one frame of exactly ls.frame_samples samples-per-channel (upmixing mono→stereo // for a stereo channel as needed), seal it, and send it over UDP, advancing ls.timestamp. @@ -394,7 +393,7 @@ struct vc_client { // Joins udp_thread_, stops audio_engine_, clears media crypto/remote-stream state. // Safe to call multiple times. Called both from run_io()'s cleanup and disconnect(). void teardown_voice(); - // talk_timer_thread_ entry point (M3): polls audio_engine_ for remote talk-state edges + // talk_timer_thread_ entry point: polls audio_engine_ for remote talk-state edges // and local capture activity, emitting VC_EVENT_TALK_STATE. Never the audio RT thread. void run_talk_timer(); // Find a LocalStream by its client-assigned stream_id (held under local_streams_mu_ by diff --git a/core/src/crypto/crypto.cpp b/core/src/crypto/crypto.cpp index 1fa071d..c94ab3e 100644 --- a/core/src/crypto/crypto.cpp +++ b/core/src/crypto/crypto.cpp @@ -95,14 +95,12 @@ ServerCert ServerCert::generate(const std::string& server_name) { strlen(pers)), "ctr_drbg_seed"); - // Generate ECDSA-P256 key throw_if(mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)), "pk_setup"); throw_if(mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(key), mbedtls_ctr_drbg_random, &ctr_drbg), "ecp_gen_key"); - // Build self-signed cert mbedtls_x509write_crt_set_version(&cert, MBEDTLS_X509_CRT_VERSION_3); mbedtls_x509write_crt_set_md_alg(&cert, MBEDTLS_MD_SHA256); mbedtls_x509write_crt_set_subject_key(&cert, &key); @@ -112,7 +110,6 @@ ServerCert ServerCert::generate(const std::string& server_name) { throw_if(mbedtls_x509write_crt_set_subject_name(&cert, dn.c_str()), "set_subject"); throw_if(mbedtls_x509write_crt_set_issuer_name(&cert, dn.c_str()), "set_issuer"); - // Serial = 0x01 (1 byte, value 1) uint8_t serial_raw[] = {0x01}; throw_if(mbedtls_x509write_crt_set_serial_raw(&cert, serial_raw, sizeof(serial_raw)), "set_serial"); @@ -124,13 +121,11 @@ ServerCert ServerCert::generate(const std::string& server_name) { throw_if(mbedtls_x509write_crt_set_basic_constraints(&cert, 0, -1), "set_basic_constraints"); - // Write PEM cert unsigned char cert_buf[4096] = {}; throw_if(mbedtls_x509write_crt_pem(&cert, cert_buf, sizeof(cert_buf), mbedtls_ctr_drbg_random, &ctr_drbg), "write_cert_pem"); - // Write PEM key unsigned char key_buf[4096] = {}; throw_if(mbedtls_pk_write_key_pem(&key, key_buf, sizeof(key_buf)), "write_key_pem"); diff --git a/core/src/crypto/crypto.h b/core/src/crypto/crypto.h index a739216..8970622 100644 --- a/core/src/crypto/crypto.h +++ b/core/src/crypto/crypto.h @@ -59,7 +59,7 @@ struct ServerCert { // ── TLS 1.3 context ─────────────────────────────────────────────────────────── // Wraps mbedTLS for one TLS connection (server or client side). -// All public methods except close() must be called from a single thread at a time. +// All public methods must be called from a single thread at a time. class TlsContext { public: enum class Role { Server, Client }; @@ -86,7 +86,7 @@ class TlsContext { bool export_keying_material(const char* label, const uint8_t* ctx, size_t ctx_len, uint8_t* out, size_t out_len); - // M4 TOFU: SHA-256 of the peer's leaf X.509 certificate (DER), valid only after a + // TOFU: SHA-256 of the peer's leaf X.509 certificate (DER), valid only after a // successful Role::Client handshake(). This is the value vc_client pins — see // voicecat.h's vc_tofu_status doc comment for why the cert fingerprint is pinned instead // of the declared Ed25519 server_identity_fingerprint. Returns false if no peer cert is @@ -119,7 +119,7 @@ class TlsContext { mbedtls_net_context net_ctx_{}; }; -// ── Media AEAD (M2) ─────────────────────────────────────────────────────────── +// ── Media AEAD ───────────────────────────────────────────────────────────────── // Per-frame voice encryption. Abstracted so the backend is swappable. class MediaCrypto { public: diff --git a/core/src/crypto/tofu_store.h b/core/src/crypto/tofu_store.h index 9a923f5..cf5187b 100644 --- a/core/src/crypto/tofu_store.h +++ b/core/src/crypto/tofu_store.h @@ -27,7 +27,7 @@ class TofuStore { // Check the fingerprint for host:port. Stores on first connect. // Thread-safe (single-writer lock). - // NOTE: kept for compatibility; the M4 gated-confirmation flow (vc_client) uses peek() + // NOTE: kept for compatibility; vc_client's gated-confirmation flow uses peek() // + pin() instead, since check_and_pin's unconditional first-connect write is wrong for a // flow where the application must approve the fingerprint before it's trusted/persisted. TofuResult check_and_pin(const std::string& host, uint16_t port, diff --git a/core/src/net/transport.h b/core/src/net/transport.h index e2f9690..a0da996 100644 --- a/core/src/net/transport.h +++ b/core/src/net/transport.h @@ -3,9 +3,6 @@ * * Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing). * Implementation uses standalone Asio for sockets and timers. - * - * Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing). - * Implementation uses standalone Asio for sockets and timers. */ #ifndef VOICECAT_NET_TRANSPORT_H #define VOICECAT_NET_TRANSPORT_H @@ -180,7 +177,7 @@ class TcpAcceptor { std::vector> conns_; }; -// ── UDP media channel (M2) ─────────────────────────────────────────────────── +// ── UDP media channel ──────────────────────────────────────────────────────── // Thin async UDP socket. send_to() is thread-safe. Recv callbacks fire on the // io_context's thread (same thread that runs the io_context::run() loop). class UdpMediaChannel { diff --git a/core/src/protocol/protocol.h b/core/src/protocol/protocol.h index 79dc712..2018bb5 100644 --- a/core/src/protocol/protocol.h +++ b/core/src/protocol/protocol.h @@ -6,10 +6,9 @@ * request_id ↔ response, and dispatches to handlers. Media frames do NOT come through here * (they use the fixed binary header in voice.md §2). * - * STATUS: real. Protobuf codegen is on (core/CMakeLists.txt) for VOICECAT_HAS_NET builds - * (`dev`/`release`/`server-release`); FrameCodec below is fully implemented and used by both the - * client (net/transport.h) and the server (conn_session.cpp). See protocol/envelope.h for the - * Envelope-level encode/decode that sits on top of this. + * FrameCodec below is used by both the client (net/transport.h) and the server + * (conn_session.cpp). See protocol/envelope.h for the Envelope-level encode/decode that sits + * on top of this. */ #ifndef VOICECAT_PROTOCOL_PROTOCOL_H #define VOICECAT_PROTOCOL_PROTOCOL_H diff --git a/core/src/session/session.cpp b/core/src/session/session.cpp index b33c59d..aeddde3 100644 --- a/core/src/session/session.cpp +++ b/core/src/session/session.cpp @@ -148,9 +148,7 @@ void SessionModel::apply_channel_event(const voicecat::v1::ChannelEvent& ev) { } else if (ev.kind() == Kind::DELETED) { // deleted_id, not channel().id() — the proto leaves `channel` unset for deletes - // (docs/protocol.md, core/proto/voicecat.proto's ChannelEvent). Pre-existing bug, dead - // code until something actually emits ChannelEvent (no channel CRUD exists yet — M5+), - // fixed here while touching this function for the M4 field-population fix. + // (docs/protocol.md, core/proto/voicecat.proto's ChannelEvent). uint32_t cid = ev.deleted_id(); channels_.erase(std::remove_if(channels_.begin(), channels_.end(), [cid](const Channel& x) { return x.id == cid; }), diff --git a/core/src/session/session.h b/core/src/session/session.h index d3c9b70..b26656d 100644 --- a/core/src/session/session.h +++ b/core/src/session/session.h @@ -46,7 +46,7 @@ struct Stream { int kind{0}; std::string label; // Full effective AudioConfig (docs/protocol.md §5), as broadcast by the server in - // StreamInfo.audio — mirrors voicecat::v1::AudioConfig field-for-field so M3 per-channel + // StreamInfo.audio — mirrors voicecat::v1::AudioConfig field-for-field so per-channel // tuning (mono/stereo, bitrate, FEC/DTX, application) is observable client-side, not just // sample_rate/frame_ms. uint32_t sample_rate{48000}; diff --git a/core/src/voicecat.cpp b/core/src/voicecat.cpp index 179f0f0..b5c3bc0 100644 --- a/core/src/voicecat.cpp +++ b/core/src/voicecat.cpp @@ -1,11 +1,8 @@ /* * voicecat.cpp — C ABI implementation. * - * Lifecycle (create/destroy) and trivial accessors are always real. Everything else below - * just delegates to vc_client (core/src/core/client.cpp): under VOICECAT_HAS_NET - * (`dev`/`release`/`server-release` — see docs/building.md) that's the real M1–M3 implementation; - * under the no-deps `skeleton` preset, client.cpp's `#else` branch returns VC_ERR_NOT_IMPLEMENTED - * for all of it, to keep that skeleton build green. + * Lifecycle (create/destroy) and trivial accessors are handled directly here; everything else + * delegates to vc_client (core/src/core/client.cpp). */ #include "voicecat.h" diff --git a/server/src/conn_session.cpp b/server/src/conn_session.cpp index b147889..9c85b54 100644 --- a/server/src/conn_session.cpp +++ b/server/src/conn_session.cpp @@ -88,9 +88,8 @@ void ConnSession::on_frame(std::vector frame) { handle_ping(env.ping()); break; case voicecat::v1::Envelope::kDisconnect: - // Client-initiated graceful disconnect (code=0). Falls through to close() which - // broadcasts UserEvent::LEFT — same as a TCP drop, but immediate (no reaper/EOF - // wait). Gated on Authenticated so a pre-auth stray Disconnect can't skip cleanup. + // See handle_client_disconnect for the close()/idempotency rationale. Gated on + // Authenticated so a pre-auth stray Disconnect can't skip cleanup. if (st == State::Authenticated) handle_client_disconnect(env.disconnect()); break; case voicecat::v1::Envelope::kLeaveChannel: @@ -118,7 +117,7 @@ void ConnSession::on_frame(std::vector frame) { handle_unsubscribe_voice(env.request_id()); break; - // ── M5 moderation / admin ───────────────────────────────────────────── + // ── Moderation / admin ────────────────────────────────────────────────── case voicecat::v1::Envelope::kKick: if (st == State::Authenticated) handle_kick_request(env.request_id(), env.kick()); break; @@ -203,7 +202,7 @@ void ConnSession::close() { if (close_fn_) close_fn_(); } -// ── M2: media crypto ───────────────────────────────────────────────────────── +// ── Media crypto ─────────────────────────────────────────────────────────────── void ConnSession::set_media_crypto( std::unique_ptr send, @@ -223,7 +222,7 @@ voicecat::crypto::SodiumMediaCrypto* ConnSession::recv_crypto() { return recv_crypto_.get(); } -// ── M2: UDP endpoint ───────────────────────────────────────────────────────── +// ── UDP endpoint ───────────────────────────────────────────────────────────── void ConnSession::set_udp_endpoint(asio::ip::udp::endpoint ep) { { @@ -335,7 +334,7 @@ void ConnSession::finish_password_auth(const std::string& username, // Argon2id runs on the worker pool (deliberately slow). auto self = shared_from_this(); workers_->post([self, username, password, req_id] { - // M5: check username bans before verifying password. + // Check username bans before verifying password. if (self->db_->ban_check("username", username)) { auto env = make_env(req_id); env.mutable_auth_result()->set_ok(false); @@ -669,7 +668,7 @@ void ConnSession::handle_unsubscribe_voice(uint64_t req_id) { } } -// ── M5 handlers ────────────────────────────────────────────────────────────── +// ── Moderation & admin handlers ───────────────────────────────────────────────── void ConnSession::handle_kick_request(uint64_t req_id, const voicecat::v1::KickRequest& msg) { if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_kick)) { diff --git a/server/src/conn_session.h b/server/src/conn_session.h index d0aeacc..1103174 100644 --- a/server/src/conn_session.h +++ b/server/src/conn_session.h @@ -60,20 +60,20 @@ class ConnSession : public std::enable_shared_from_this { // close a session without making it a friend class. void send_disconnect_and_close(uint32_t code, const std::string& reason); - // ── M2: media key injection (called from on_tls_ready) ─────────────────── + // ── Media key injection (called from on_tls_ready) ──────────────────────── void set_media_crypto(std::unique_ptr send, std::unique_ptr recv); - // ── M2: UDP endpoint (set by MediaRelay on UdpBinding) ──────────────────── + // ── UDP endpoint (set by MediaRelay on UdpBinding) ──────────────────────── void set_udp_endpoint(asio::ip::udp::endpoint ep); asio::ip::udp::endpoint udp_endpoint() const; bool has_udp_endpoint() const { return has_udp_ep_.load(); } - // ── M2: media crypto access (for SFU relay) ────────────────────────────── + // ── Media crypto access (for SFU relay) ─────────────────────────────────── voicecat::crypto::SodiumMediaCrypto* send_crypto(); voicecat::crypto::SodiumMediaCrypto* recv_crypto(); - // ── M2: UDP token (for binding) ─────────────────────────────────────────── + // ── UDP token (for binding) ──────────────────────────────────────────────── const std::array& udp_token() const { return udp_token_; } // ── Accessors ────────────────────────────────────────────────────────────── @@ -107,7 +107,7 @@ class ConnSession : public std::enable_shared_from_this { void handle_subscribe_voice(uint64_t req_id); void handle_unsubscribe_voice(uint64_t req_id); - // M5 handlers + // Moderation & admin handlers void handle_kick_request(uint64_t req_id, const voicecat::v1::KickRequest& msg); void handle_ban_request(uint64_t req_id, const voicecat::v1::BanRequest& msg); void handle_set_permission(uint64_t req_id, const voicecat::v1::SetPermissionRequest& msg); @@ -155,7 +155,7 @@ class ConnSession : public std::enable_shared_from_this { // The reaper (server.cpp) drops sessions whose last_seen is older than 45s. std::atomic last_seen_ms_{0}; - // M2 UDP / media + // UDP / media std::array udp_token_{}; mutable std::mutex udp_ep_mu_; asio::ip::udp::endpoint udp_ep_; @@ -165,13 +165,13 @@ class ConnSession : public std::enable_shared_from_this { std::unique_ptr send_crypto_; std::unique_ptr recv_crypto_; - // M2/M3: locally-announced streams. The server assigns the stream_id (unique per + // Locally-announced streams. The server assigns the stream_id (unique per // session), so a per-session counter + the set of currently-active ids is enough to // support multiple concurrent streams (MIC + SCREEN_AUDIO + AUX_DEVICE) per user. uint32_t next_stream_id_{1}; std::vector announced_stream_ids_; - // M5: permissions granted at auth time (server-side authority). + // Permissions granted at auth time (server-side authority). voicecat::v1::Permissions permissions_; // Voice-plane subscription. When false, the SFU relay excludes this session from the diff --git a/server/src/db.cpp b/server/src/db.cpp index e8795b1..b71d971 100644 --- a/server/src/db.cpp +++ b/server/src/db.cpp @@ -89,7 +89,6 @@ bool Database::open(std::string& error) { } bool Database::migrate(std::string& error) { - // Ensure server_meta row exists. if (!exec("INSERT OR IGNORE INTO server_meta (key, value) VALUES ('schema_version', '1')", error)) return false; @@ -241,7 +240,6 @@ std::optional Database::authenticate(const std::string& username, if (crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) != 0) return std::nullopt; - // Update last_login int64_t now = now_unix(); sqlite3_stmt* upd = nullptr; sqlite3_prepare_v2(db_, "UPDATE accounts SET last_login=? WHERE id=?", -1, &upd, nullptr); diff --git a/server/src/db.h b/server/src/db.h index 2f999b9..ca96c50 100644 --- a/server/src/db.h +++ b/server/src/db.h @@ -44,7 +44,7 @@ struct ChannelRecord { // Persistent ban record. struct BanRecord { int64_t id{0}; - std::string subject_type; // "user_id" or "ip" + std::string subject_type; // "user_id", "username", or "ip" std::string subject; // the banned value std::string reason; int64_t expires_at{0}; // 0 = permanent diff --git a/server/src/main.cpp b/server/src/main.cpp index 4d46f13..a5f932f 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -68,10 +68,15 @@ int main(int argc, char** argv) { } } + if (print_config_only) { + std::printf("server_name = %s\n", cfg.server_name.c_str()); + std::printf("data_dir = %s\n", cfg.data_dir.c_str()); + std::printf("bind_port = %u\n", cfg.bind_port); + std::printf("allow_guests = %s\n", cfg.allow_guests ? "true" : "false"); + return 0; + } + std::printf("[voicecat-server %s] starting\n", vc_version_string()); voicecat::server::Server server(cfg); - if (print_config_only) { - // run() currently just prints config; in M1 split this into a pure config dump. - } return server.run(); } diff --git a/server/src/media_relay.h b/server/src/media_relay.h index 381ff75..799d763 100644 --- a/server/src/media_relay.h +++ b/server/src/media_relay.h @@ -1,5 +1,5 @@ /* - * server/media_relay.h — UDP SFU relay for M2 voice. + * server/media_relay.h — UDP SFU relay for voice. * * Design: docs/architecture.md §5, docs/voice.md §2. * Receives encrypted UDP voice frames from clients, decrypts+authenticates them, diff --git a/server/src/server.cpp b/server/src/server.cpp index f4d34f7..e77b829 100644 --- a/server/src/server.cpp +++ b/server/src/server.cpp @@ -65,7 +65,7 @@ int Server::run() { // ── Asio io_context ────────────────────────────────────────────────────── asio::io_context io; - // ── UDP media relay (M2) ───────────────────────────────────────────────── + // ── UDP media relay ──────────────────────────────────────────────────────── auto media_relay = std::make_shared(io, registry); // Control and media share one port number on TCP+UDP (docs/deployment.md): when media_port // is left at 0, follow bind_port so a single forward rule covers both. If bind_port is also 0 @@ -116,7 +116,8 @@ int Server::run() { auto tcp = std::make_shared( std::move(sock), std::move(cbs), std::move(tls)); - // Give session its send/close capability (weak_ptr avoids cycle) + // Give session its send/close capability (weak_ptr — see the shared_ptr/cycle + // note above). std::weak_ptr weak_tcp = tcp; session->set_io( [weak_tcp](std::vector frame) { diff --git a/server/src/server.h b/server/src/server.h index 8aa3a86..2aa3f55 100644 --- a/server/src/server.h +++ b/server/src/server.h @@ -18,7 +18,7 @@ struct Config { std::string server_name = "VoiceCat Server"; std::string data_dir = "voicecat-data"; uint16_t bind_port = 8384; // 0 = let OS pick (useful for tests) - uint16_t media_port = 0; // M2 UDP media; 0 = follow bind_port (or OS-pick if that's 0) + uint16_t media_port = 0; // UDP media port; 0 = follow bind_port (or OS-pick if that's 0) bool allow_guests = true; // Called with the actual bound TCP port once the acceptor is ready. std::function on_ready; diff --git a/server/src/session_registry.cpp b/server/src/session_registry.cpp index 47ebe0c..1e08ce4 100644 --- a/server/src/session_registry.cpp +++ b/server/src/session_registry.cpp @@ -274,7 +274,7 @@ namespace { ue->set_kind(voicecat::v1::UserEvent::LEFT); ue->mutable_user()->set_id(user_id); ue->set_left_id(user_id); - ue->set_reason(reason); // M5 additive field + ue->set_reason(reason); return env; } } @@ -447,7 +447,7 @@ bool SessionRegistry::check_channel_password(uint32_t channel_id, return db_->check_channel_password(channel_id, password); } -// ── M2: UDP / media ─────────────────────────────────────────────────────────── +// ── UDP / media ──────────────────────────────────────────────────────────────── void SessionRegistry::register_udp_token(const std::array& token, uint64_t session_id) { diff --git a/server/src/session_registry.h b/server/src/session_registry.h index 03a9ef5..dfb0d73 100644 --- a/server/src/session_registry.h +++ b/server/src/session_registry.h @@ -2,7 +2,7 @@ * server/session_registry.h — In-memory session, channel, and user registry. * * Tracks all authenticated sessions, the channel tree, user<→>channel assignments, - * UDP endpoint bindings (M2), and SSRC<→>session mappings (M2). + * UDP endpoint bindings, and SSRC<→>session mappings. * Protected by a shared_mutex (many readers, few writers). All methods are thread-safe. */ #ifndef VOICECAT_SERVER_SESSION_REGISTRY_H @@ -144,7 +144,7 @@ class SessionRegistry { // Check a channel password. bool check_channel_password(uint32_t channel_id, const std::string& password) const; - // ── M2: UDP / media ──────────────────────────────────────────────────────── + // ── UDP / media ──────────────────────────────────────────────────────────── // Register a session's UDP token (called at auth success). void register_udp_token(const std::array& token, uint64_t session_id); @@ -177,7 +177,7 @@ class SessionRegistry { // Return the channel_id of a user (0 if not found). uint32_t user_channel(uint32_t user_id) const; - // Return a channel's authoritative AudioConfig (M3 per-channel Opus tuning), or nullopt + // Return a channel's authoritative AudioConfig (per-channel Opus tuning), or nullopt // if the channel doesn't exist. There is no per-id Channel getter today otherwise — // channel_snapshot() copies every channel, which callers needing just one config should // avoid. @@ -199,7 +199,7 @@ class SessionRegistry { std::unordered_map channels_; std::unordered_map session_permissions_; - // M2: token → session_id (populated at auth, cleared on disconnect) + // Token → session_id (populated at auth, cleared on disconnect) struct TokenHash { size_t operator()(const std::array& t) const { // FNV-1a over 16 bytes @@ -210,10 +210,10 @@ class SessionRegistry { }; std::unordered_map, uint64_t, TokenHash> udp_tokens_; - // M2: UDP endpoint → session_id (populated after UDP binding packet arrives) + // UDP endpoint → session_id (populated after UDP binding packet arrives) std::unordered_map udp_endpoints_; - // M2: ssrc → session_id (populated when StreamAnnounce is processed) + // ssrc → session_id (populated when StreamAnnounce is processed) std::unordered_map ssrc_to_session_; std::atomic next_ssrc_{1}; }; diff --git a/tools/vccli/src/main.cpp b/tools/vccli/src/main.cpp index daaf2f2..e247bce 100644 --- a/tools/vccli/src/main.cpp +++ b/tools/vccli/src/main.cpp @@ -1,9 +1,8 @@ /* * vccli — headless test client. * - * This is the primary way the protocol is exercised and verified from M1 onward (see - * AGENTS.md). Each milestone's exit criterion is demonstrated by driving two vccli - * instances against a real voicecat-server. + * This is the primary way the protocol is exercised and verified (see AGENTS.md): driving + * two vccli instances against a real voicecat-server. */ #include #include @@ -26,11 +25,11 @@ struct Stats { std::atomic auth_done{false}; std::atomic auth_ok{false}; // Set right after vc_client_create, before vc_connect — lets on_event auto-confirm the - // M4 TOFU gate (VC_EVENT_SERVER_IDENTITY below). vccli has no interactive prompt, so it + // TOFU gate (VC_EVENT_SERVER_IDENTITY below). vccli has no interactive prompt, so it // trusts-on-first-connect unconditionally (prints the fingerprint for visibility). vc_client* client{nullptr}; - // M5 async result tracking. Generic results are used by every moderation/admin/channel + // Async result tracking. Generic results are used by every moderation/admin/channel // request; account-list is its own event. We count generic results so callers can wait // for a new one even if several arrived earlier. std::atomic generic_result_count{0}; @@ -125,7 +124,6 @@ bool wait_until(std::atomic& flag, int timeout_ms) { return true; } -// Wait for a new generic result to arrive. Returns the vc_result from that result. vc_result wait_generic_result(Stats& st, int baseline_count, int timeout_ms) { auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); while (st.generic_result_count.load() <= baseline_count) { @@ -200,7 +198,7 @@ void print_usage() { " --username U authenticate as registered user U\n" " --password P password for --username\n" " --channel ID channel to join after auth (default 1, Lobby)\n" - " --wait-ms N timeout for M5 async result events (default 5000)\n" + " --wait-ms N timeout for async result events (default 5000)\n" "\n" "Voice / devices:\n" " --voice start a MIC stream and stay connected until Ctrl+C\n" @@ -278,7 +276,7 @@ void run_stdin_commands(vc_client* c, std::atomic& stop) { } } -// Issue an M5 request that produces VC_EVENT_GENERIC_RESULT and wait for it. +// Issue a request that produces VC_EVENT_GENERIC_RESULT and wait for it. // Returns the result code from the event. using RequestFn = std::function; @@ -536,107 +534,107 @@ int main(int argc, char** argv) { std::this_thread::sleep_for(std::chrono::milliseconds(300)); // let the relay land } - // M5 moderation / admin requests (executed in a sensible order if multiple are given). - bool m5_error = false; + // Moderation / admin requests (executed in a sensible order if multiple are given). + bool mod_request_error = false; if (self_mute || self_deafen) { r = vc_set_self_mute(c, self_mute ? 1 : 0, self_deafen ? 1 : 0); std::printf("vc_set_self_mute -> %d (%s)\n", r, vc_result_string(r)); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_kick) { + if (!mod_request_error && do_kick) { r = run_generic_request(st, wait_ms, [&]() { return vc_kick_user(c, kick_user_id, kick_reason.c_str()); }, "vc_kick_user"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_ban) { + if (!mod_request_error && do_ban) { r = run_generic_request(st, wait_ms, [&]() { return vc_ban_user(c, ban_user_id, ban_reason.c_str(), ban_expires_ms); }, "vc_ban_user"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_move) { + if (!mod_request_error && do_move) { r = run_generic_request(st, wait_ms, [&]() { return vc_move_user(c, move_user_id, move_channel_id); }, "vc_move_user"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_server_mute_request) { + if (!mod_request_error && do_server_mute_request) { r = run_generic_request(st, wait_ms, [&]() { return vc_set_server_mute(c, server_mute_user_id, server_mute_muted, server_mute_deafened); }, "vc_set_server_mute"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_set_permission) { + if (!mod_request_error && do_set_permission) { r = run_generic_request(st, wait_ms, [&]() { return vc_set_permission(c, perm_user_id, &perms); }, "vc_set_permission"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_create_channel) { + if (!mod_request_error && do_create_channel) { r = run_generic_request(st, wait_ms, [&]() { return vc_create_channel(c, &channel_info); }, "vc_create_channel"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_edit_channel) { + if (!mod_request_error && do_edit_channel) { r = run_generic_request(st, wait_ms, [&]() { return vc_edit_channel(c, &channel_info); }, "vc_edit_channel"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_delete_channel) { + if (!mod_request_error && do_delete_channel) { r = run_generic_request(st, wait_ms, [&]() { return vc_delete_channel(c, delete_channel_id); }, "vc_delete_channel"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_create_account) { + if (!mod_request_error && do_create_account) { r = run_generic_request(st, wait_ms, [&]() { return vc_create_account(c, acct_user.c_str(), acct_pass.c_str()); }, "vc_create_account"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_reset_password) { + if (!mod_request_error && do_reset_password) { r = run_generic_request(st, wait_ms, [&]() { return vc_reset_password(c, acct_user.c_str(), acct_pass.c_str()); }, "vc_reset_password"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_delete_account) { + if (!mod_request_error && do_delete_account) { r = run_generic_request(st, wait_ms, [&]() { return vc_delete_account(c, acct_user.c_str()); }, "vc_delete_account"); - if (r != VC_OK) m5_error = true; + if (r != VC_OK) mod_request_error = true; } - if (!m5_error && do_list_accounts) { + if (!mod_request_error && do_list_accounts) { st.account_list_received.store(false); r = vc_list_accounts(c); std::printf("vc_list_accounts -> %d (%s)\n", r, vc_result_string(r)); if (r != VC_OK) { - m5_error = true; + mod_request_error = true; } else { if (!wait_until(st.account_list_received, wait_ms)) { std::fprintf(stderr, "vc_list_accounts: timed out waiting for list event\n"); - m5_error = true; + mod_request_error = true; } } } - if (m5_error && !voice_mode) { + if (mod_request_error && !voice_mode) { vc_disconnect(c); vc_client_destroy(c); return 1; @@ -705,5 +703,5 @@ int main(int argc, char** argv) { vc_disconnect(c); vc_client_destroy(c); std::printf("ok\n"); - return m5_error ? 1 : 0; + return mod_request_error ? 1 : 0; } diff --git a/tools/voicecat-admin/src/main.cpp b/tools/voicecat-admin/src/main.cpp index e419964..ba75dfd 100644 --- a/tools/voicecat-admin/src/main.cpp +++ b/tools/voicecat-admin/src/main.cpp @@ -39,7 +39,6 @@ int main(int argc, char** argv) { std::string data_dir = "voicecat-data"; int i = 1; - // Parse --data-dir if (i < argc && std::strcmp(argv[i], "--data-dir") == 0) { if (++i >= argc) { std::fprintf(stderr, "Missing argument to --data-dir\n"); return 1; } data_dir = argv[i++]; @@ -48,7 +47,7 @@ int main(int argc, char** argv) { if (i >= argc || std::strcmp(argv[i], "account") != 0) { print_usage(argv[0]); return 1; } - ++i; // skip "account" + ++i; if (i >= argc) { print_usage(argv[0]); return 1; } std::string subcmd = argv[i++]; diff --git a/vcpkg.json b/vcpkg.json index 5b88a38..eae8c6d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -11,8 +11,8 @@ { "name": "asio", "$why": "TCP/UDP/timers reactor — docs/architecture.md" }, { "name": "sqlite3", "$why": "server accounts/state — docs/security.md" }, { "name": "spdlog", "$why": "logging — docs/tech-stack.md" }, - { "name": "opus", "$why": "voice codec (libopus) — docs/voice.md — M2" }, - { "name": "miniaudio", "$why": "cross-platform capture/playback — docs/tech-stack.md — M2" } + { "name": "opus", "$why": "voice codec (libopus) — docs/voice.md" }, + { "name": "miniaudio", "$why": "cross-platform capture/playback — docs/tech-stack.md" } ], "$license-note": "All of the above are permissive (BSD/MIT/ISC/Apache-2.0/public-domain). No GPL/LGPL — see docs/tech-stack.md §5.", "$vendored-note": "RNNoise (noise suppression) is NOT a vcpkg dep — its port is !windows !arm — so it is vendored in third_party/rnnoise/ (BSD-3 + CC0). See third_party/README.md and docs/voice.md §10."