Skip to content

feat: replace server-data slash commands with #info embeds - #59

Merged
HandyS11 merged 18 commits into
developfrom
feat/info-embeds
Jul 20, 2026
Merged

feat: replace server-data slash commands with #info embeds#59
HandyS11 merged 18 commits into
developfrom
feat/info-embeds

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

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 !command equivalents are kept untouched (Discord isn't visible from inside Rust). The "switch active player" select moves off the embed into a new /server player command.

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:

  • 🟢 Server — endpoint, connection status, active player, players / 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.
  • ⚡ Events — cargo ship, patrol heli, chinook, and both oil rigs with lifecycle phase (was /small + /large). Reads in-process marker/rig state, so it costs no Rust+ calls.
  • 👥 Team — one line per member: leader crown, presence glyph (online / AFK / dead / offline), grid reference, survival time — folding in /team, /online, /offline and /alive. Sorted online-alive → AFK → dead → offline.

How it refreshes

A new ServerInfoRefreshHostedService tracks connected servers off the connection-status event and re-renders their #info embeds every Workspace: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

  • The three renderers live in the projects that own their data (Server → Workspace, Events → Events, Team → Players); the IMessageRenderer contracts became public to allow that. In-channel order is fixed by declaration order in ServerWorkspaceSpecProvider.
  • DurationFormat and a new Daylight helper moved to Abstractions (they sit below all three renderer homes); ServerResolver moved to Features.Connections so /server player can reference WorkspaceComponentIds.
  • Every embed renders an explicit "not connected" state rather than an empty payload, so a disconnected server never shows cleared in-process state as a confident live report.

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 QueryHandlersTests are 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 !commands still answer, and /server player swaps the active credential.

🤖 Generated with Claude Code

HandyS11 and others added 16 commits July 20, 2026 14:39
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>
Copilot AI review requested due to automatic review settings July 20, 2026 16:10
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>

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

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; move ServerResolver into Features.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.

Comment on lines +10 to +12
/// 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.

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.

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).

Comment on lines +107 to +111
await foreach (var evt in eventBus.SubscribeAsync<ConnectionStatusChangedEvent>(cancellationToken)
.ConfigureAwait(false))
{
_connected.Set(evt.GuildId, evt.ServerId, evt.IsConnected);
}

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

Comment thread src/RustPlusBot.Localization/Strings.resx
Comment thread src/RustPlusBot.Localization/Strings.fr.resx
- 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>
@HandyS11
HandyS11 merged commit 47b342e into develop Jul 20, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/info-embeds branch July 20, 2026 16:47
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