Skip to content

Subsystem 4a: Smart Switch pairing & control (#switches channel, ON/OFF/Strobe/Rename)#18

Merged
HandyS11 merged 18 commits into
developfrom
feat/smart-switches
Jun 19, 2026
Merged

Subsystem 4a: Smart Switch pairing & control (#switches channel, ON/OFF/Strobe/Rename)#18
HandyS11 merged 18 commits into
developfrom
feat/smart-switches

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

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 #switches channel 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

  • New RustPlusBot.Features.Switches project — pairing coordinator, embed renderer, state relay, channel poster, interaction module (+ rename modal), localizer (EN/FR), hosted service, DI.
  • Pairing: FCM OnSmartSwitchPairingSwitchPairedEvent; user validation ("Add it?") required before a switch becomes managed.
  • Connections: IRustServerConnection/IRustServerQuery smart-switch read/control + OnSmartSwitchTriggered; connect-time priming (re-subscribe + seed state) and trigger forwarding → SwitchStateChangedEvent.
  • Domain/Persistence: SmartSwitch entity + ISwitchStore (accepted switches persist; pending pairings stay in-memory). Cascade-delete with RustServer.
  • Workspace: per-server #switches channel (Interactive) + locator.
  • Unreachable/removed switches show inline (⚠️, buttons disabled) — no separate channel. Any guild member may validate/operate switches (role-gating stays in subsystem 9).

Package bump

RustPlusApi / RustPlusApi.Fcm2.0.0-beta.2 (entity-id types are ulong?, aligning FCM ids with the socket's ulong).

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 OnSmartSwitchPairing event carries only Notification<ulong?> (entity id, PlayerId, PlayerToken, and the Facepunch ServerId GUID) — no Ip/Port, and OnEntityPairing doesn't carry them either. So entity pairings are now resolved by the Facepunch ServerId GUID: a nullable FacepunchServerId was added to RustServer, 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/0
  • Full suite green (447 tests across 11 assemblies)
  • dotnet jb cleanupcode --profile=ReformatAndReorder applied
  • ef migrations has-pending-model-changes → clean

Known follow-ups (non-blocking, from review)

  • Supervisor's switch event-subscription sits just outside its try/finally (OCE-only leak window; self-heals via dispose) — tidy-up.
  • ServerService.GetByEndpointAsync is now unused by 4a (superseded by GUID lookup) — keep as utility or drop in cleanup.
  • PairingKind.Entity doc comment is stale ("ignored in 1b-i").
  • A server paired before 4a has FacepunchServerId == null, so its first entity pairing drops until a re-pair/credential refresh backfills it (correct + self-healing; undocumented).
  • GetByFacepunchServerIdAsync uses SingleOrDefaultAsync on a non-unique index (would only throw if two servers shared a non-null GUID — unreachable in practice).
  • The wildcard [ModalInteraction] rename flow builds and is registered correctly; its runtime dispatch warrants a manual smoke test of the Rename button.

🤖 Generated with Claude Code

HandyS11 and others added 16 commits June 19, 2026 13:09
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cepunch ServerId

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-to-end)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 19, 2026 13:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ServerId GUID 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.

Comment on lines +64 to +67
// Best-effort: remove the transient prompt message.
await DeferAsync(ephemeral: true).ConfigureAwait(false);
await DeleteOriginalResponseSafeAsync().ConfigureAwait(false);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +38 to +39
await servers.SetFacepunchServerIdAsync(server.Id, notification.FacepunchServerId, cancellationToken)
.ConfigureAwait(false);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +31 to +33
context.SmartSwitches.Add(entity);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return entity;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

HandyS11 and others added 2 commits June 19, 2026 15:31
…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>
@HandyS11 HandyS11 merged commit 6f1a7d1 into develop Jun 19, 2026
3 checks passed
@HandyS11 HandyS11 deleted the feat/smart-switches branch June 19, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants