Skip to content

Commit 47b342e

Browse files
HandyS11claude
andauthored
feat: replace server-data slash commands with #info embeds (#59)
* docs: add #info server-data embeds design spec 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> * docs: add #info server-data embeds implementation plan 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> * refactor: move DurationFormat to Abstractions and extract Daylight helper 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> * refactor: make the workspace message-renderer contracts public 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> * feat: add players, time and wipe to the #info server embed 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> * feat: add the #info events embed 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> * docs: correct build/test gates in the info-embeds plan 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> * feat: add the #info team embed 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> * Pin the online-alive survival-time tiebreak in ServerTeamMessageRendererTests 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> * feat: declare the events and team message specs in #info Declaration order fixes the on-screen order: map, status, events, team. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: add a render-gated in-place refresh for #info 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> * fix: propagate cancellation and strengthen gate-invalidation test in 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> * feat: refresh #info on a configurable interval 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> * refactor: delete the nine server-data slash commands 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> * feat: add /server player to replace the #info swap select 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> * Fix EditMessageAsync silently no-op'ing on a missing Discord channel 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> * docs: update README for #info embeds and /server player 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> * fix: address Copilot review on #info refresh (PR #59) - 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> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent db2591d commit 47b342e

61 files changed

Lines changed: 4951 additions & 560 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ recycle/craft/research/decay/upkeep calculators are all shipped. Cameras are nex
2525
server's channels.
2626
- **Credential pool per server** — multiple accounts can pair with the same server;
2727
the bot keeps one live socket per `(guild, server)` with **hot-swap** of the
28-
active player (select menu on `#info`) and **auto-failover** to a standby
28+
active player (`/server player`) and **auto-failover** to a standby
2929
credential when one goes invalid (owner is DM'd).
3030
- **Lifecycle controls** — "Remove server" (on `#info`) and "Disconnect account"
3131
(on `#setup`), both Manage-Server-gated with a confirmation step; cascade
3232
cleanup of channels, credentials, and live state.
33-
- **Live status**`#info` shows connection status, player count, and a team
34-
summary (online/total + leader), refreshed on connection-state changes.
33+
- **Live `#info` dashboard** — three auto-refreshing embeds below the map image:
34+
**Server** (status, players/queue, in-game time, wipe age), **Events** (cargo /
35+
heli / chinook / both oil rigs), and **Team** (per-member presence, grid, and
36+
survival time). Refreshed on connection-state changes and on a configurable
37+
interval (`Workspace:InfoRefreshInterval`, default 1 min).
3538
- **Bilingual** — every provisioned surface renders in **English or French**,
3639
switchable from a select menu in `#settings`.
3740

@@ -51,12 +54,13 @@ configurable per-server prefix (default `!`) and per-command cooldowns:
5154
- **Items**`!item`, `!recycle`, `!craft`, `!research`, `!decay`, `!upkeep` (name or id)
5255
- **Control**`!mute` / `!unmute` (gate all bot→game output)
5356

54-
### Slash commands
57+
Live server data (population, in-game time, wipe, team, oil rigs) is no longer a
58+
set of ephemeral commands — it renders continuously in the `#info` dashboard
59+
above, and stays available in-game via the `!commands`.
5560

56-
- **Server data** (ephemeral, with a `server` selector when several are paired) —
57-
`/pop`, `/time`, `/wipe`, `/online`, `/offline`, `/team`, `/alive`, `/small`, `/large`
5861
- **Items** (ephemeral) — `/item`, `/recycle`, `/craft`, `/research`, `/decay`, `/upkeep` (name or id)
5962
- **Utility**`/help`, `/uptime`, `/leader` (Manage Server — transfer in-game team leadership)
63+
- **Server**`/server player` (Manage Server — hot-swap the active paired account)
6064
- **Admin**`/setup`, `/workspace reset`, `/workspace simulate-server`
6165

6266
### Live map events

docs/superpowers/plans/2026-07-20-info-embeds-server-data.md

Lines changed: 2772 additions & 0 deletions
Large diffs are not rendered by default.

docs/superpowers/specs/2026-07-20-info-embeds-server-data-design.md

Lines changed: 374 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Globalization;
2+
3+
namespace RustPlusBot.Abstractions.Connections;
4+
5+
/// <summary>
6+
/// Day/night reasoning over a <see cref="ServerTimeSnapshot" />. Shared by the in-game !time
7+
/// handler and the #info server embed so the two can never disagree. All intervals are in
8+
/// <em>in-game</em> hours: Rust's day length is server-configurable and the API never reports
9+
/// it, so these values must not be presented as real-world minutes.
10+
/// </summary>
11+
public static class Daylight
12+
{
13+
private const float HoursPerDay = 24f;
14+
15+
/// <summary>True when the in-game clock sits between sunrise (inclusive) and sunset (exclusive).</summary>
16+
/// <param name="snapshot">The in-game time snapshot.</param>
17+
/// <returns>True during daylight; false at night.</returns>
18+
public static bool IsDay(ServerTimeSnapshot snapshot)
19+
{
20+
ArgumentNullException.ThrowIfNull(snapshot);
21+
return snapshot.TimeOfDay >= snapshot.Sunrise && snapshot.TimeOfDay < snapshot.Sunset;
22+
}
23+
24+
/// <summary>Renders the in-game clock as zero-padded "HH:mm".</summary>
25+
/// <param name="snapshot">The in-game time snapshot.</param>
26+
/// <returns>The clock text, e.g. "14:32".</returns>
27+
public static string Clock(ServerTimeSnapshot snapshot)
28+
{
29+
ArgumentNullException.ThrowIfNull(snapshot);
30+
var hours = (int)snapshot.TimeOfDay;
31+
var minutes = (int)((snapshot.TimeOfDay - hours) * 60f);
32+
return string.Create(CultureInfo.InvariantCulture, $"{hours:00}:{minutes:00}");
33+
}
34+
35+
/// <summary>In-game hours until the next sunrise or sunset, wrapping past midnight.</summary>
36+
/// <param name="snapshot">The in-game time snapshot.</param>
37+
/// <returns>The interval in in-game hours; never negative.</returns>
38+
public static float HoursUntilTransition(ServerTimeSnapshot snapshot)
39+
{
40+
ArgumentNullException.ThrowIfNull(snapshot);
41+
var target = IsDay(snapshot) ? snapshot.Sunset : snapshot.Sunrise;
42+
var delta = target - snapshot.TimeOfDay;
43+
return delta >= 0f ? delta : delta + HoursPerDay;
44+
}
45+
46+
/// <summary>The same interval as <see cref="HoursUntilTransition" />, as a <see cref="TimeSpan" />.</summary>
47+
/// <param name="snapshot">The in-game time snapshot.</param>
48+
/// <returns>The interval in in-game hours expressed as a TimeSpan.</returns>
49+
public static TimeSpan UntilTransition(ServerTimeSnapshot snapshot)
50+
=> TimeSpan.FromHours(HoursUntilTransition(snapshot));
51+
}

src/RustPlusBot.Features.Commands/Formatting/DurationFormat.cs renamed to src/RustPlusBot.Abstractions/Formatting/DurationFormat.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System.Globalization;
22

3-
namespace RustPlusBot.Features.Commands.Formatting;
3+
namespace RustPlusBot.Abstractions.Formatting;
44

5-
/// <summary>Formats durations compactly for in-game replies.</summary>
6-
internal static class DurationFormat
5+
/// <summary>Formats durations compactly for in-game replies and Discord embeds.</summary>
6+
public static class DurationFormat
77
{
88
/// <summary>Renders a duration as "Xd Yh" / "Yh Zm" / "Zm".</summary>
99
/// <param name="span">The duration to render.</param>

src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using RustPlusBot.Features.Commands.Help;
66
using RustPlusBot.Features.Commands.Hosting;
77
using RustPlusBot.Features.Commands.Leader;
8-
using RustPlusBot.Features.Commands.Servers;
98
using RustPlusBot.Features.ItemData;
109
using RustPlusBot.Localization;
1110

@@ -62,8 +61,6 @@ public static IServiceCollection AddCommands(this IServiceCollection services)
6261
// Discord surfaces (3c): help renderer, leader service, and the interaction modules.
6362
services.AddScoped<HelpEmbedRenderer>();
6463
services.AddScoped<LeaderService>();
65-
services.AddScoped<ServerResolver>();
66-
services.AddScoped<ServerQueryService>();
6764
services.AddSingleton(new InteractionModuleAssembly(typeof(CommandServiceCollectionExtensions).Assembly));
6865

6966
return services;

src/RustPlusBot.Features.Commands/Formatting/DecayLine.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using RustPlusBot.Abstractions.Formatting;
23
using RustPlusBot.Features.ItemData.Data;
34

45
namespace RustPlusBot.Features.Commands.Formatting;

src/RustPlusBot.Features.Commands/Formatting/DurabilityLine.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using RustPlusBot.Abstractions.Formatting;
23
using RustPlusBot.Features.ItemData.Data;
34
using RustPlusBot.Features.ItemData.Naming;
45

src/RustPlusBot.Features.Commands/Formatting/ItemLine.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using RustPlusBot.Abstractions.Formatting;
23
using RustPlusBot.Features.ItemData.Data;
34

45
namespace RustPlusBot.Features.Commands.Formatting;

src/RustPlusBot.Features.Commands/Formatting/SmeltLine.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using RustPlusBot.Abstractions.Formatting;
23
using RustPlusBot.Features.ItemData.Data;
34
using RustPlusBot.Features.ItemData.Naming;
45

0 commit comments

Comments
 (0)