Skip to content

Commit 2ec8387

Browse files
HandyS11claude
andauthored
Subsystem 3c: command Discord surfaces (/help, /uptime, /leader, #info team summary) (#11)
* docs(3c): spec for command Discord surfaces (/help, /uptime, /leader, #info team summary) Subsystem 3c — third slice of subsystem 3. Surfaces the in-game command framework into Discord: /help (in-game + slash command listing), /uptime (bot process), /leader (member-select promote), and a team summary field on the per-server #info embed. Slash modules + help catalog live in the existing Features.Commands project (Approach A); the #info edit is local to Workspace's ServerInfoMessageRenderer. The #commands channel relay is deferred to 3c-ii. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(3c): implementation plan for command Discord surfaces 7-task subagent-driven TDD plan: PromoteToLeaderAsync seam, CommandHelpCatalog + drift guard, HelpEmbedRenderer, LeaderService, the /help+/uptime+/leader modules, and the #info team summary field. Verified signatures against the codebase (IRustServerQuery, ServerInfoMessageRenderer, FakeRustSocketSource, RustPlusApi PromoteToLeaderAsync) and the Discord project/package ref gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(3c): add PromoteToLeaderAsync to the connection query seam Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(3c): add command help catalog + help/leader/uptime localization keys Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(3c): add HelpEmbedRenderer Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(3c): add LeaderService (fetch members + promote) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(3c): add /help, /uptime, /leader slash + component modules Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(3c): add team summary field to #info embed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(3c): tighten #info null-team test + document IRustServerQuery DI dependency Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(3c): apply jb ReformatAndReorder Cosmetic only — multi-line parameter/call-chain wrapping in the 3c files that Roslynator does not flag (the repo's format gate). Build 0/0, full suite 249 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: stop tracking superpowers spec/plan docs (kept local only) docs/superpowers/ is gitignored; these spec/plan files were force-added past the ignore. Untrack them (3c here + the 3b-ii docs carried from PR #10) so they stay local working docs and never ship in a PR. Files remain on disk. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(3c): address Copilot review on PR #11 - LeaderComponentModule.OnMemberSelectedAsync now surfaces the GetMembersAsync error (e.g. not-connected if the socket dropped between select and click) instead of falling through to a generic promote failure. - LeaderComponentModule guards Context.Guild == null on both component handlers (forged/DM payloads), matching the repo pattern (SettingsComponentModule etc.). - ServerInfoMessageRenderer #info team summary treats an empty/whitespace leader display name as missing and falls back to the SteamId (the API can report a member with no name); +1 renderer test. Build 0/0, full suite 250 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 70a92c7 commit 2ec8387

29 files changed

Lines changed: 1096 additions & 1526 deletions

docs/superpowers/plans/2026-06-17-rustplusbot-3b-ii-team-intel.md

Lines changed: 0 additions & 1345 deletions
This file was deleted.

docs/superpowers/specs/2026-06-17-rustplusbot-3b-ii-team-intel-design.md

Lines changed: 0 additions & 176 deletions
This file was deleted.

src/RustPlusBot.Features.Connections/Listening/IRustServerQuery.cs renamed to src/RustPlusBot.Abstractions/Connections/IRustServerQuery.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,12 @@ public interface IRustServerQuery
2323
/// <param name="cancellationToken">A cancellation token.</param>
2424
/// <returns>A team snapshot, or null when there is no live socket.</returns>
2525
Task<TeamInfoSnapshot?> GetTeamInfoAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken);
26+
27+
/// <summary>Promotes a team member to team leader; returns false when there is no live socket or the API fails.</summary>
28+
/// <param name="guildId">The owning guild snowflake.</param>
29+
/// <param name="serverId">The target server id.</param>
30+
/// <param name="steamId">Steam64 id of the member to promote.</param>
31+
/// <param name="cancellationToken">A cancellation token.</param>
32+
/// <returns>True if promoted; false when no live socket or the API fails.</returns>
33+
Task<bool> PromoteToLeaderAsync(ulong guildId, Guid serverId, ulong steamId, CancellationToken cancellationToken);
2634
}

src/RustPlusBot.Features.Connections/Listening/ServerInfoSnapshot.cs renamed to src/RustPlusBot.Abstractions/Connections/ServerInfoSnapshot.cs

File renamed without changes.

src/RustPlusBot.Features.Connections/Listening/ServerTimeSnapshot.cs renamed to src/RustPlusBot.Abstractions/Connections/ServerTimeSnapshot.cs

File renamed without changes.

src/RustPlusBot.Features.Connections/Listening/TeamInfoSnapshot.cs renamed to src/RustPlusBot.Abstractions/Connections/TeamInfoSnapshot.cs

File renamed without changes.

src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using Microsoft.Extensions.DependencyInjection;
2+
using RustPlusBot.Discord;
23
using RustPlusBot.Features.Commands.Dispatching;
34
using RustPlusBot.Features.Commands.Handlers;
5+
using RustPlusBot.Features.Commands.Help;
46
using RustPlusBot.Features.Commands.Hosting;
7+
using RustPlusBot.Features.Commands.Leader;
58
using RustPlusBot.Features.Commands.Localization;
69

710
namespace RustPlusBot.Features.Commands;
@@ -38,6 +41,11 @@ public static IServiceCollection AddCommands(this IServiceCollection services)
3841
services.AddScoped<CommandDispatcher>();
3942
services.AddHostedService<CommandsHostedService>();
4043

44+
// Discord surfaces (3c): help renderer, leader service, and the interaction modules.
45+
services.AddScoped<HelpEmbedRenderer>();
46+
services.AddScoped<LeaderService>();
47+
services.AddSingleton(new InteractionModuleAssembly(typeof(CommandServiceCollectionExtensions).Assembly));
48+
4149
return services;
4250
}
4351
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace RustPlusBot.Features.Commands.Help;
2+
3+
/// <summary>The display grouping for a command in the <c>/help</c> listing.</summary>
4+
internal enum CommandGroup
5+
{
6+
/// <summary>Bot control commands (mute/unmute).</summary>
7+
Control = 0,
8+
9+
/// <summary>Server-state commands (pop/time/wipe).</summary>
10+
Server = 1,
11+
12+
/// <summary>Team-intel commands (online/offline/team/steamid/alive/prox).</summary>
13+
TeamIntel = 2,
14+
15+
/// <summary>Bot-meta commands (uptime).</summary>
16+
Bot = 3,
17+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace RustPlusBot.Features.Commands.Help;
2+
3+
/// <summary>One entry in the help listing.</summary>
4+
/// <param name="Name">The command name without prefix (in-game) or slash command name.</param>
5+
/// <param name="Group">The display group.</param>
6+
/// <param name="DescriptionKey">The localization key for the description.</param>
7+
internal sealed record CommandHelpEntry(string Name, CommandGroup Group, string DescriptionKey);
8+
9+
/// <summary>The curated help manifest. Tested against the live handler registry to catch drift.</summary>
10+
internal static class CommandHelpCatalog
11+
{
12+
/// <summary>The in-game <c>!commands</c>, in display order.</summary>
13+
public static IReadOnlyList<CommandHelpEntry> InGame { get; } =
14+
[
15+
new("mute", CommandGroup.Control, "help.mute"),
16+
new("unmute", CommandGroup.Control, "help.unmute"),
17+
new("pop", CommandGroup.Server, "help.pop"),
18+
new("time", CommandGroup.Server, "help.time"),
19+
new("wipe", CommandGroup.Server, "help.wipe"),
20+
new("online", CommandGroup.TeamIntel, "help.online"),
21+
new("offline", CommandGroup.TeamIntel, "help.offline"),
22+
new("team", CommandGroup.TeamIntel, "help.team"),
23+
new("steamid", CommandGroup.TeamIntel, "help.steamid"),
24+
new("alive", CommandGroup.TeamIntel, "help.alive"),
25+
new("prox", CommandGroup.TeamIntel, "help.prox"),
26+
new("uptime", CommandGroup.Bot, "help.uptime"),
27+
];
28+
29+
/// <summary>The Discord slash commands, in display order.</summary>
30+
public static IReadOnlyList<CommandHelpEntry> Slash { get; } =
31+
[
32+
new("help", CommandGroup.Bot, "help.slash.help"),
33+
new("uptime", CommandGroup.Bot, "help.slash.uptime"),
34+
new("leader", CommandGroup.Bot, "help.slash.leader"),
35+
];
36+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Globalization;
2+
using Discord;
3+
using RustPlusBot.Features.Commands.Localization;
4+
5+
namespace RustPlusBot.Features.Commands.Help;
6+
7+
/// <summary>Builds the <c>/help</c> embed from the catalog, the server prefix, and the guild culture.</summary>
8+
/// <param name="localizer">Resolves the title, group headings, and per-command descriptions.</param>
9+
internal sealed class HelpEmbedRenderer(ICommandLocalizer localizer)
10+
{
11+
private static readonly (CommandGroup Group, string HeadingKey)[] GroupOrder =
12+
[
13+
(CommandGroup.Control, "help.group.control"),
14+
(CommandGroup.Server, "help.group.server"),
15+
(CommandGroup.TeamIntel, "help.group.teamintel"),
16+
(CommandGroup.Bot, "help.group.bot"),
17+
];
18+
19+
/// <summary>Renders the help embed.</summary>
20+
/// <param name="prefix">The in-game command prefix to display (e.g. "!").</param>
21+
/// <param name="culture">The guild culture ("en"/"fr").</param>
22+
/// <param name="showPrefixNote">True to add the multi-server prefix note to the description.</param>
23+
/// <returns>The built embed.</returns>
24+
public Embed Render(string prefix, string culture, bool showPrefixNote)
25+
{
26+
var embed = new EmbedBuilder()
27+
.WithTitle(localizer.Get("help.title", culture))
28+
.WithColor(Color.Blue);
29+
30+
if (showPrefixNote)
31+
{
32+
embed.WithDescription(localizer.Get("help.prefixnote", culture));
33+
}
34+
35+
foreach (var (group, headingKey) in GroupOrder)
36+
{
37+
var lines = CommandHelpCatalog.InGame
38+
.Where(e => e.Group == group)
39+
.Select(e => string.Create(CultureInfo.InvariantCulture,
40+
$"`{prefix}{e.Name}` — {localizer.Get(e.DescriptionKey, culture)}"))
41+
.ToList();
42+
if (lines.Count > 0)
43+
{
44+
embed.AddField(localizer.Get(headingKey, culture), string.Join('\n', lines));
45+
}
46+
}
47+
48+
var slashLines = CommandHelpCatalog.Slash
49+
.Select(e => string.Create(CultureInfo.InvariantCulture,
50+
$"`/{e.Name}` — {localizer.Get(e.DescriptionKey, culture)}"))
51+
.ToList();
52+
embed.AddField(localizer.Get("help.group.slash", culture), string.Join('\n', slashLines));
53+
54+
return embed.Build();
55+
}
56+
}

0 commit comments

Comments
 (0)