feat: replace server-data slash commands with #info embeds - #59
Conversation
Replaces the nine server-data slash commands (/pop, /time, /wipe, /online, /offline, /team, /alive, /small, /large) with three auto-refreshing embeds in the per-server #info channel: Server, Events and Team, below the existing map image embed. The in-game !command surface is untouched. The "Switch active player" select moves off the embed into a new /server player command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ten TDD tasks: move DurationFormat/Daylight into Abstractions, open the renderer contracts, build the three embeds in the projects that own their data, declare the specs, add a render-gated refresh path plus its hosted service, delete the nine slash commands, and add /server player. Also corrects the spec: the Team renderer lands in Features.Players (not Connections, which cannot see GridReference), ServerResolver moves rather than staying put, and offline members carry no fabricated duration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lper Both are needed by the #info renderers, which sit below Features.Commands in the project graph. Behaviour is unchanged; TimeCommandHandler now shares its day/night math with the forthcoming server embed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lets Features.Events and Features.Players contribute renderers for the new #info embeds without moving the state interfaces into Abstractions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renders the MaxPlayers/QueuedPlayers/WipeTimeUtc already carried by ServerInfoSnapshot but never shown, replacing /pop, /time and /wipe. Drops the swap select ahead of /server player replacing it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cargo, patrol heli, chinook and both oil rigs in one embed, replacing /small and /large. Reads in-process marker and rig state only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-flight against the previous feature's ledger surfaced four gate violations in the plan as written: - 28 invocations used `dtk build X`; dtk wraps dotnet, so the form is `dtk dotnet build X`, and `-maxcpucount:1` is mandatory (parallel MSBuild races ConfigureGitHooks on .git/config, silently dropping a whole test assembly that then reports 0 tests and looks green). - The jb cleanupcode invocation lacked --no-build --verbosity=ERROR. - Global Constraints omitted -warnaserror, the CA1305/1307/1310 rules, the no-FluentAssertions rule, the global `using Xunit`, the commit trailer, and the 18-assembly baseline of 1028+1. - Two steps told the implementer to create a test project and add an InternalsVisibleTo grant that both already exist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One line per member with presence glyph, grid reference and state age, replacing /team, /online, /offline and /alive. Sorted online-alive, AFK, dead, offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rerTests Online_alive_sort_before_afk_dead_and_offline puts every member in a distinct RankOf bucket, so the renderer's ThenByDescending(SurvivedFor) tiebreak was never exercised. Add a dedicated test with two online-alive, non-AFK members at different survival times to pin the longest-surviving-first ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Declaration order fixes the on-screen order: map, status, events, team. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Skips the provisioning lock and channel walk that ReconcileServerAsync performs, and suppresses PATCHes for unchanged renders. Any edit failure invalidates the gate and escalates to a full reconcile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ServerInfoRefresher Finding 1: EditMessageAsync's broad catch swallowed OperationCanceledException on shutdown, invalidating the gate and reconciling with an already-cancelled token instead of letting cancellation propagate. Finding 2: the failed-edit test's ShouldSend probe used a dummy literal that passed regardless of whether the gate was correctly invalidated or wrongly committed; it now probes with the exact attempted canonical, verified by mutation to fail under a commit-before-edit bug and pass under the correct Invalidate call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tracks connected servers off the connection status event and re-renders their #info embeds every Workspace:InfoRefreshInterval (default 1m), floored at one second so a misconfiguration cannot spin the loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Their data now lives in the #info embeds. The in-game !pop/!time/!wipe/ !online/!offline/!team/!alive/!small/!large handlers are untouched. ServerResolver moves to Features.Connections, which /server player needs because Features.Commands cannot see WorkspaceComponentIds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replies ephemerally with the same select and custom id the embed used to carry, so ConnectionComponentModule handles it unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DiscordWorkspaceGateway.EditMessageAsync returned silently when the channel was absent from the socket cache, while PostMessageAsync threw for the identical condition. ServerInfoRefresher commits a render to the RenderGate right after EditMessageAsync returns, so the silent no-op let the gate record a render that never landed, leaving the rarely-changing #info Events/Team embeds stale until content next changed. Throwing symmetrically lets the refresher's existing catch -> invalidate -> reconcile self-heal path handle it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The nine server-data slash commands are gone; their data now renders in the auto-refreshing #info dashboard (Server / Events / Team embeds) and stays available in-game via !commands. Active-player hot-swap moved from the #info select menu to the new /server player command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR replaces a set of read-only server-data Discord slash commands with a per-server #info “dashboard” composed of auto-refreshing embeds, and introduces a new /server player command to replace the “active player” select that previously lived on the #info embed. It also refactors shared formatting/time helpers into Abstractions and adjusts DI registrations and tests accordingly.
Changes:
- Add three #info embeds (server status, events, team) contributed by feature-owned renderers, with spec-ordered declaration.
- Add a periodic refresh hosted service + render-gated in-place edit path for #info messages.
- Remove the nine server-data slash commands; add
/server player; moveServerResolverintoFeatures.Connections.
Reviewed changes
Copilot reviewed 60 out of 61 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs | Updates expected localization catalog key count for new strings. |
| tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs | Adds tests to lock #info message spec ordering and validate channel targets. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ServerInfoRefresherTests.cs | New tests for render-gated #info refresh behavior and escalation paths. |
| tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs | Updates server info renderer tests for new fields and removal of swap select. |
| tests/RustPlusBot.Features.Workspace.Tests/Hosting/ServerInfoRefreshHostedServiceTests.cs | New tests for refresh interval clamping and connected-server tracking. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceGateway.cs | Aligns fake gateway behavior with production by throwing on missing channel edits. |
| tests/RustPlusBot.Features.Players.Tests/Messages/ServerTeamMessageRendererTests.cs | Adds coverage for the new team embed renderer (ordering, AFK/dead/offline handling). |
| tests/RustPlusBot.Features.Events.Tests/Messages/ServerEventsMessageRendererTests.cs | Adds coverage for the new events embed renderer (markers/rig phases/disconnect). |
| tests/RustPlusBot.Features.Connections.Tests/Servers/ServerResolverTests.cs | Updates namespaces after moving ServerResolver to Connections. |
| tests/RustPlusBot.Features.Connections.Tests/ConnectionRegistrationTests.cs | Ensures ServerResolver resolves from the Connections service container. |
| tests/RustPlusBot.Features.Commands.Tests/Servers/ServerQueryServiceTests.cs | Removes tests for deleted ServerQueryService. |
| tests/RustPlusBot.Features.Commands.Tests/Formatting/DurationFormatTests.cs | Updates tests to use the moved DurationFormat from Abstractions. |
| tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs | Removes DI assertions for moved/deleted services (resolver/query service). |
| tests/RustPlusBot.Abstractions.Tests/Connections/DaylightTests.cs | Adds tests for the new shared Daylight helper. |
| src/RustPlusBot.Localization/Strings.resx | Adds new localization keys for #info embeds and /server player, plus help text. |
| src/RustPlusBot.Localization/Strings.fr.resx | French counterparts for added localization keys. |
| src/RustPlusBot.Host/appsettings.json | Adds Workspace:InfoRefreshInterval default configuration. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | Registers IServerInfoRefresher and the refresh hosted service. |
| src/RustPlusBot.Features.Workspace/WorkspaceOptions.cs | Introduces InfoRefreshInterval option with XML documentation. |
| src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs | Adds message keys for new #info embeds (server.events, server.team). |
| src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs | Declares message specs in fixed #info render order (map → status → events → team). |
| src/RustPlusBot.Features.Workspace/Registry/MessageRenderContext.cs | Makes render context public for cross-project renderers. |
| src/RustPlusBot.Features.Workspace/Registry/IMessageRenderer.cs | Makes renderer contract public for cross-project renderers. |
| src/RustPlusBot.Features.Workspace/Reconciler/ServerInfoRefresher.cs | New render-gated in-place refresh implementation for #info embeds. |
| src/RustPlusBot.Features.Workspace/Reconciler/IServerInfoRefresher.cs | New internal contract for #info refresh. |
| src/RustPlusBot.Features.Workspace/Messages/ServerInfoMessageRenderer.cs | Updates server status embed to include players/time/wipe and removes swap select UI. |
| src/RustPlusBot.Features.Workspace/Hosting/ServerInfoRefreshHostedService.cs | New periodic refresher tracking connected servers from connection status events. |
| src/RustPlusBot.Features.Workspace/Gateway/MessagePayload.cs | Makes payload type public for cross-project renderers. |
| src/RustPlusBot.Features.Workspace/Gateway/IWorkspaceGateway.cs | Documents EditMessageAsync throwing for missing channel. |
| src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs | Changes edit behavior to throw when the channel is missing (symmetry with post). |
| src/RustPlusBot.Features.Players/PlayerEventServiceCollectionExtensions.cs | Registers ServerTeamMessageRenderer as an IMessageRenderer. |
| src/RustPlusBot.Features.Players/Messages/ServerTeamMessageRenderer.cs | New renderer for the team #info embed (presence, grid, durations, sort). |
| src/RustPlusBot.Features.Events/Messages/ServerEventsMessageRenderer.cs | New renderer for the events #info embed (markers + rig phases). |
| src/RustPlusBot.Features.Events/EventServiceCollectionExtensions.cs | Registers ServerEventsMessageRenderer as an IMessageRenderer. |
| src/RustPlusBot.Features.Connections/Servers/ServerResolver.cs | Moves resolver into Connections and updates namespace. |
| src/RustPlusBot.Features.Connections/Servers/ServerResolution.cs | Moves resolution record into Connections and updates namespace. |
| src/RustPlusBot.Features.Connections/Servers/ServerAutocompleteHandler.cs | Moves autocomplete handler into Connections and updates namespace. |
| src/RustPlusBot.Features.Connections/Modules/ServerPlayerModule.cs | Adds /server player command to present the credential switch select ephemerally. |
| src/RustPlusBot.Features.Connections/ConnectionServiceCollectionExtensions.cs | Adds localization + registers ServerResolver in Connections DI. |
| src/RustPlusBot.Features.Commands/Servers/ServerQueryService.cs | Removes obsolete slash-command wrapper service. |
| src/RustPlusBot.Features.Commands/Modules/ServerCommandModule.cs | Removes obsolete server-data slash commands module. |
| src/RustPlusBot.Features.Commands/Modules/DiagnosticsModule.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Modules/CommandSurfaceModule.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Help/CommandHelpCatalog.cs | Adds help catalog entry for /server player. |
| src/RustPlusBot.Features.Commands/Handlers/WipeCommandHandler.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Handlers/UptimeCommandHandler.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Handlers/TimeCommandHandler.cs | Reuses shared Daylight helper for clock/day-night formatting. |
| src/RustPlusBot.Features.Commands/Handlers/RigReply.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Handlers/AliveCommandHandler.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Handlers/AfkCommandHandler.cs | Updates DurationFormat namespace usage after move to Abstractions. |
| src/RustPlusBot.Features.Commands/Formatting/SmeltLine.cs | Adds Abstractions DurationFormat import for compact time formatting. |
| src/RustPlusBot.Features.Commands/Formatting/ItemLine.cs | Adds Abstractions DurationFormat import for compact time formatting. |
| src/RustPlusBot.Features.Commands/Formatting/DurabilityLine.cs | Adds Abstractions DurationFormat import for compact time formatting. |
| src/RustPlusBot.Features.Commands/Formatting/DecayLine.cs | Adds Abstractions DurationFormat import for compact time formatting. |
| src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs | Removes DI registrations for moved/deleted server-data command services. |
| src/RustPlusBot.Abstractions/Formatting/DurationFormat.cs | Moves DurationFormat into Abstractions and makes it public. |
| src/RustPlusBot.Abstractions/Connections/Daylight.cs | Adds shared day/night and in-game clock formatting helper. |
| docs/superpowers/specs/2026-07-20-info-embeds-server-data-design.md | Design spec describing the embed replacement architecture and edge cases. |
| docs/superpowers/plans/2026-07-20-info-embeds-server-data.md | Implementation plan capturing step-by-step tasks and constraints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// How often every connected server's #info embeds are re-rendered. Unchanged renders are | ||
| /// suppressed by the render gate, so the steady-state cost is three Rust+ calls per server | ||
| /// per tick and at most three Discord edits. |
There was a problem hiding this comment.
Correct — the Team renderer also calls GetMapDimensionsAsync, so it's four live-socket queries per tick (server info + time + team + map dimensions), not three. Fixed the doc in 43ec691, and noted that a disconnected server costs no round-trips (the live queries return null immediately).
| await foreach (var evt in eventBus.SubscribeAsync<ConnectionStatusChangedEvent>(cancellationToken) | ||
| .ConfigureAwait(false)) | ||
| { | ||
| _connected.Set(evt.GuildId, evt.ServerId, evt.IsConnected); | ||
| } |
There was a problem hiding this comment.
Fixed in 43ec691. Switched to the same source-of-truth-each-tick pattern InfoMapHostedService uses: the tick now enumerates IConnectionStore.ListConnectableServersAsync and refreshes each, so it no longer depends on having observed the connect event. Seeding an event-driven set from the store wouldn't have worked — ListConnectableServersAsync returns connectable servers (a superset of connected), and the event-only design relied on the disconnect event to remove entries, so a never-connected server would have refreshed forever. Removed ConnectedServerSet and the event loop; a disconnected server now renders cheaply (null live queries) and a transient store failure no longer kills the loop.
- ServerInfoRefreshHostedService: enumerate connectable servers from the store each tick instead of tracking them from ConnectionStatusChangedEvent. The connection can publish "connected" before this service subscribes on startup, so an event-only set missed it and that server's #info embeds never refreshed. Mirrors InfoMapHostedService's source-of-truth-each-tick pattern; removes ConnectedServerSet and the event loop. A disconnected server renders cheaply (live queries return null with no round-trip), and a transient store-enumeration failure no longer kills the tick loop. - WorkspaceOptions doc: the steady-state cost is four Rust+ calls per tick (server info, time, team, map dimensions), not three. - help.slash.server.player: add the terminal period, en + fr, to match the sibling help entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Replaces the nine ephemeral server-data slash commands —
/pop,/time,/wipe,/online,/offline,/team,/alive,/small,/large— with three auto-refreshing embeds in each server's #info channel, at rustplusplus-level density. The in-game!commandequivalents are kept untouched (Discord isn't visible from inside Rust). The "switch active player" select moves off the embed into a new/server playercommand.Closes the ask: "remove these slash commands and display the same info in embeds; the #info embed should contain most of the data."
What #info now shows
Below the existing 🗺️ RustMaps image:
/max·queued (was/pop), in-game time + daylight countdown (was/time), wipe age (was/wipe). Map size/seed stay out — they're already on the map embed above./small+/large). Reads in-process marker/rig state, so it costs no Rust+ calls./team,/online,/offlineand/alive. Sorted online-alive → AFK → dead → offline.How it refreshes
A new
ServerInfoRefreshHostedServicetracks connected servers off the connection-status event and re-renders their #info embeds everyWorkspace:InfoRefreshInterval(default 1 min, floored at 1 s), on top of the existing event-driven triggers. Refreshes go through a narrow render-gated in-place edit path (ServerInfoRefresher) that skips the provisioning lock and suppresses no-op PATCHes; a missing message or failed edit self-heals via full reconcile.Architecture notes
IMessageRenderercontracts became public to allow that. In-channel order is fixed by declaration order inServerWorkspaceSpecProvider.DurationFormatand a newDaylighthelper moved toAbstractions(they sit below all three renderer homes);ServerResolvermoved toFeatures.Connectionsso/server playercan referenceWorkspaceComponentIds.Testing
Full suite green: 1068 passed, 1 skipped, 18 assemblies (baseline 1028+1). New coverage for each renderer (connected/disconnected, AFK, dead, offline, empty team, rig phases, marker age, sort tiebreak), the refresher (gate suppression, escalation, missing-channel), the hosted service (interval floor, connect/disconnect tracking), and the spec ordering. Localization parity holds at 315 keys (en+fr). The in-game
QueryHandlersTestsare untouched — the guard that!pop/!time/… still work.Not yet done
Live #info smoke on a real paired server: confirm the four messages render in order, the embeds refresh on the interval, the nine slash commands are gone from Discord's picker, the in-game
!commandsstill answer, and/server playerswaps the active credential.🤖 Generated with Claude Code