Subsystem 4a: Smart Switch pairing & control (#switches channel, ON/OFF/Strobe/Rename)#18
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cepunch ServerId Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…and component ids
…-to-end) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements “Subsystem 4a” Smart Switch pairing and control end-to-end: ingame Smart Switch pairings flow through FCM, get resolved to a Discord-managed server, require Discord validation, and then render/manage per-switch embeds with ON/OFF/Strobe/Rename controls in a per-server #switches channel, kept in sync with live socket state.
Changes:
- Add Smart Switch persistence (entity, EF configuration, migrations, store) and DI registration.
- Add Switches feature project (localization, embed rendering, pairing coordinator, state relay, Discord posting, interaction module, hosted service) plus host/workspace wiring.
- Extend pairing + live socket layers to support entity pairing attribution via Facepunch
ServerIdGUID and smart-switch read/control + trigger events.
Reviewed changes
Copilot reviewed 61 out of 63 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Persistence.Tests/Switches/SwitchStoreTests.cs | Adds unit tests for the EF-backed switch store. |
| tests/RustPlusBot.Persistence.Tests/Switches/SmartSwitchSchemaTests.cs | Verifies SmartSwitch EF schema, cascade delete, and uniqueness. |
| tests/RustPlusBot.Persistence.Tests/Servers/ServerServiceTests.cs | Adds tests for endpoint lookup and FacepunchServerId lookup/backfill. |
| tests/RustPlusBot.Persistence.Tests/PersistenceRegistrationTests.cs | Ensures ISwitchStore is registered by persistence DI. |
| tests/RustPlusBot.Features.Workspace.Tests/Locating/SwitchChannelLocatorTests.cs | Tests #switches channel lookup + TTL cache behavior. |
| tests/RustPlusBot.Features.Switches.Tests/SwitchStateRelayTests.cs | Tests relay behavior for state changes and unreachable connections. |
| tests/RustPlusBot.Features.Switches.Tests/SwitchRegistrationTests.cs | Verifies DI registration for switches feature services. |
| tests/RustPlusBot.Features.Switches.Tests/SwitchPairingCoordinatorTests.cs | Tests pairing prompt, ignore-if-managed, and accept behavior. |
| tests/RustPlusBot.Features.Switches.Tests/SwitchLocalizationCatalogTests.cs | Validates localization catalog completeness + fallback. |
| tests/RustPlusBot.Features.Switches.Tests/SwitchEmbedRendererTests.cs | Tests embed rendering and component enable/disable logic. |
| tests/RustPlusBot.Features.Switches.Tests/RustPlusBot.Features.Switches.Tests.csproj | Introduces test project for the switches feature. |
| tests/RustPlusBot.Features.Pairing.Tests/PairingHandlerTests.cs | Updates pairing tests for FacepunchServerId + entity pairing path. |
| tests/RustPlusBot.Features.Connections.Tests/SwitchQueryTests.cs | Tests live socket smart-switch query/control + priming/trigger publish. |
| tests/RustPlusBot.Features.Connections.Tests/Fakes/FakeRustSocketSource.cs | Adds smart-switch read/control + trigger simulation to the fake socket. |
| tests/RustPlusBot.Abstractions.Tests/SwitchEventsTests.cs | Adds tests for new switch-related event records. |
| src/RustPlusBot.Persistence/Switches/SwitchStore.cs | Implements EF-backed ISwitchStore. |
| src/RustPlusBot.Persistence/Switches/ISwitchStore.cs | Defines switch persistence store contract. |
| src/RustPlusBot.Persistence/Servers/ServerService.cs | Adds server lookup/backfill APIs for endpoint and FacepunchServerId. |
| src/RustPlusBot.Persistence/Servers/IServerService.cs | Extends server service interface with new lookup/backfill methods. |
| src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs | Registers ISwitchStore in persistence DI. |
| src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs | Updates EF model snapshot for SmartSwitches + FacepunchServerId. |
| src/RustPlusBot.Persistence/Migrations/20260619122323_FacepunchServerId.Designer.cs | EF migration designer for FacepunchServerId. |
| src/RustPlusBot.Persistence/Migrations/20260619122323_FacepunchServerId.cs | EF migration adding FacepunchServerId + index. |
| src/RustPlusBot.Persistence/Migrations/20260619111443_SmartSwitches.Designer.cs | EF migration designer for SmartSwitches table. |
| src/RustPlusBot.Persistence/Migrations/20260619111443_SmartSwitches.cs | EF migration creating SmartSwitches table + indexes/FK. |
| src/RustPlusBot.Persistence/Configurations/SmartSwitchConfiguration.cs | EF configuration for SmartSwitch entity + cascade delete + uniqueness. |
| src/RustPlusBot.Persistence/Configurations/RustServerConfiguration.cs | Adds index configuration for FacepunchServerId. |
| src/RustPlusBot.Persistence/BotDbContext.cs | Adds DbSet for SmartSwitch and applies configuration. |
| src/RustPlusBot.Host/RustPlusBot.Host.csproj | Adds reference to switches feature project. |
| src/RustPlusBot.Host/Program.cs | Registers switches feature in host startup. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | Registers ISwitchChannelLocator in workspace DI. |
| src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs | Adds workspace channel key for per-server switches channel. |
| src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs | Adds per-server #switches channel spec. |
| src/RustPlusBot.Features.Workspace/Locating/SwitchChannelLocator.cs | Implements cached locator for per-server #switches channels. |
| src/RustPlusBot.Features.Workspace/Locating/ISwitchChannelLocator.cs | Defines #switches channel locator interface. |
| src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs | Adds localized channel name key for switches channel. |
| src/RustPlusBot.Features.Switches/SwitchServiceCollectionExtensions.cs | Adds switches feature DI registrations + interaction module assembly registration. |
| src/RustPlusBot.Features.Switches/RustPlusBot.Features.Switches.csproj | Introduces switches feature project and dependencies. |
| src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizer.cs | Adds EN-fallback localizer for switch UI strings. |
| src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizationCatalog.cs | Adds EN/FR switch localization string catalog. |
| src/RustPlusBot.Features.Switches/Rendering/SwitchEmbedRenderer.cs | Renders switch embed + controls and pairing prompt embed. |
| src/RustPlusBot.Features.Switches/Rendering/SwitchComponentIds.cs | Defines component/custom-id scheme for switch interactions. |
| src/RustPlusBot.Features.Switches/Rendering/ISwitchLocalizer.cs | Defines switch-localization interface. |
| src/RustPlusBot.Features.Switches/Relaying/SwitchStateRelay.cs | Keeps switch embeds updated based on state/connection events. |
| src/RustPlusBot.Features.Switches/Posting/ISwitchChannelPoster.cs | Defines abstraction for posting/editing switch embeds. |
| src/RustPlusBot.Features.Switches/Posting/DiscordSwitchChannelPoster.cs | Discord.Net implementation for embed upsert/self-heal. |
| src/RustPlusBot.Features.Switches/Pairing/SwitchPairingCoordinator.cs | Handles pending pairings, validation prompt, and accept persistence + initial render. |
| src/RustPlusBot.Features.Switches/Modules/SwitchRenameModal.cs | Defines rename modal payload for rename interaction. |
| src/RustPlusBot.Features.Switches/Modules/SwitchComponentModule.cs | Handles switch buttons + modal submit interactions. |
| src/RustPlusBot.Features.Switches/Hosting/SwitchesHostedService.cs | Subscribes to switch pairing/state/connection events and dispatches handlers. |
| src/RustPlusBot.Features.Pairing/Pairing/PairingHandler.cs | Adds entity-pairing handling and server GUID backfill. |
| src/RustPlusBot.Features.Pairing/Listening/RustPlusFcmPairingSource.cs | Subscribes to SmartSwitch pairing event and dispatches entity pairing notifications. |
| src/RustPlusBot.Features.Pairing/Listening/PairingNotification.cs | Extends pairing notification with FacepunchServerId + EntityId. |
| src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs | Adds smart-switch query/control APIs, trigger subscription, and connect-time priming. |
| src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs | Implements smart-switch calls and trigger forwarding via RustPlusApi socket. |
| src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs | Extends connection interface with smart-switch operations + trigger event. |
| src/RustPlusBot.Domain/Switches/SmartSwitch.cs | Adds persisted SmartSwitch domain entity. |
| src/RustPlusBot.Domain/Servers/RustServer.cs | Adds nullable FacepunchServerId to RustServer domain entity. |
| src/RustPlusBot.Abstractions/Events/SwitchStateChangedEvent.cs | Adds event raised when switch state is observed. |
| src/RustPlusBot.Abstractions/Events/SwitchPairedEvent.cs | Adds event raised when an ingame switch pairing is resolved to a server. |
| src/RustPlusBot.Abstractions/Connections/IRustServerQuery.cs | Extends query abstraction with smart-switch state/control operations. |
| RustPlusBot.slnx | Adds switches feature + tests projects to solution. |
| Directory.Packages.props | Bumps RustPlusApi and RustPlusApi.Fcm to 2.0.0-beta.2. |
Files not reviewed (2)
- src/RustPlusBot.Persistence/Migrations/20260619111443_SmartSwitches.Designer.cs: Generated file
- src/RustPlusBot.Persistence/Migrations/20260619122323_FacepunchServerId.Designer.cs: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Best-effort: remove the transient prompt message. | ||
| await DeferAsync(ephemeral: true).ConfigureAwait(false); | ||
| await DeleteOriginalResponseSafeAsync().ConfigureAwait(false); | ||
| } |
There was a problem hiding this comment.
Fixed in 35626cd. DismissAsync now deletes the component interaction's source message (IComponentInteraction.Message) — the actual prompt — instead of DeleteOriginalResponseAsync() which only removed the ephemeral defer. The interaction is acknowledged with an ephemeral "Dismissed." response.
| await servers.SetFacepunchServerIdAsync(server.Id, notification.FacepunchServerId, cancellationToken) | ||
| .ConfigureAwait(false); |
There was a problem hiding this comment.
Fixed in 35626cd. The backfill now runs only when notification.FacepunchServerId != Guid.Empty, so servers paired without a real Facepunch GUID no longer collide on Guid.Empty. Hardened the resolution side too: GetByFacepunchServerIdAsync treats Guid.Empty as no-match and uses FirstOrDefaultAsync (not Single) so it can't throw even if legacy rows ever shared an id.
| context.SmartSwitches.Add(entity); | ||
| await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); | ||
| return entity; |
There was a problem hiding this comment.
Fixed in 35626cd. AddAsync now catches the DbUpdateException from the unique-index violation, detaches the failed insert, and returns the row the winner persisted — so a concurrent double-accept is idempotent instead of surfacing as an interaction failure. Added a SwitchStoreTests case (Add_is_idempotent_when_switch_already_exists) covering it.
…tent AddAsync, delete real dismiss prompt - PairingHandler: only backfill FacepunchServerId when non-empty, so servers paired without a Facepunch GUID don't all collide on Guid.Empty (which would break entity attribution / throw on resolution). - ServerService.GetByFacepunchServerIdAsync: treat Guid.Empty as no-match and use FirstOrDefault so resolution can never throw on duplicate legacy ids. - SwitchStore.AddAsync: recover idempotently from the unique-index violation when two users accept the same pending pairing concurrently (detach + return the winner row) instead of surfacing a DbUpdateException as an interaction failure. + test. - SwitchComponentModule.DismissAsync: delete the component's source prompt message (IComponentInteraction.Message) rather than the ephemeral interaction response, which left the prompt lingering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Subsystem 4a — Smart Switch pairing & control
First slice of subsystem 4 (smart devices). Pair a Smart Switch in-game → validate it in Discord → get a per-switch embed in a per-server
#switcheschannel with ON / OFF / Strobe / Rename controls whose status stays in sync with the real in-game state.Builds the deferred FCM entity-pairing path (ignored since 1b-i), the live-socket entity read/control seam, and Smart Switches end-to-end.
What's included
RustPlusBot.Features.Switchesproject — pairing coordinator, embed renderer, state relay, channel poster, interaction module (+ rename modal), localizer (EN/FR), hosted service, DI.OnSmartSwitchPairing→SwitchPairedEvent; user validation ("Add it?") required before a switch becomes managed.IRustServerConnection/IRustServerQuerysmart-switch read/control +OnSmartSwitchTriggered; connect-time priming (re-subscribe + seed state) and trigger forwarding →SwitchStateChangedEvent.SmartSwitchentity +ISwitchStore(accepted switches persist; pending pairings stay in-memory). Cascade-delete withRustServer.#switcheschannel (Interactive) + locator.Package bump
RustPlusApi/RustPlusApi.Fcm→ 2.0.0-beta.2 (entity-id types areulong?, aligning FCM ids with the socket'sulong).Design change vs. the original spec
The spec planned to attribute entity pairings to a server by endpoint (Ip/Port). In beta.2 the typed
OnSmartSwitchPairingevent carries onlyNotification<ulong?>(entity id, PlayerId, PlayerToken, and the FacepunchServerIdGUID) — no Ip/Port, andOnEntityPairingdoesn't carry them either. So entity pairings are now resolved by the FacepunchServerIdGUID: a nullableFacepunchServerIdwas added toRustServer, backfilled on server pairing, and entity pairings resolve by it (unknown GUID → log + drop, never create a server). This is the spec's stated "future hardening", brought forward.Out of scope (later slices)
Smart alarms (4b), storage monitors (4c), switch groups, a dedicated unreachable-devices channel, cameras (subsystem 5), and
!//command equivalents.Migrations
Two intended:
SmartSwitches,FacepunchServerId. No other model drift.Verification
dotnet build RustPlusBot.slnx -warnaserror→ 0/0dotnet jb cleanupcode --profile=ReformatAndReorderappliedef migrations has-pending-model-changes→ cleanKnown follow-ups (non-blocking, from review)
ServerService.GetByEndpointAsyncis now unused by 4a (superseded by GUID lookup) — keep as utility or drop in cleanup.PairingKind.Entitydoc comment is stale ("ignored in 1b-i").FacepunchServerId == null, so its first entity pairing drops until a re-pair/credential refresh backfills it (correct + self-healing; undocumented).GetByFacepunchServerIdAsyncusesSingleOrDefaultAsyncon a non-unique index (would only throw if two servers shared a non-null GUID — unreachable in practice).[ModalInteraction]rename flow builds and is registered correctly; its runtime dispatch warrants a manual smoke test of the Rename button.🤖 Generated with Claude Code