Skip to content

Subsystem 3b: in-game !command framework (thin slice) - #9

Merged
HandyS11 merged 16 commits into
developfrom
feat/command-framework
Jun 16, 2026
Merged

Subsystem 3b: in-game !command framework (thin slice)#9
HandyS11 merged 16 commits into
developfrom
feat/command-framework

Conversation

@HandyS11

@HandyS11 HandyS11 commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the in-game !command framework — the engine that turns a !-prefixed line in in-game team chat into a parsed, authorized, rate-limited command whose localized reply is sent back to team chat — proven with a thin slice of six commands. New RustPlusBot.Features.Commands project (mirrors Pairing/Connections/Chat), consuming the existing TeamMessageReceivedEvent from the bus alongside the chat relay.

Commands shipped: !mute, !unmute, !uptime, !pop, !time, !wipe.

Behavior

  • !mute silences all bot→game output — command replies and the Discord→game relay — persisted per (guild, server); !mute/!unmute stay runnable while muted.
  • Per-(server, command) in-memory cooldown; EN/FR replies localized by guild culture.
  • The command prefix is stored per (guild, server) (entity ServerCommandSettings.Prefix, default !) and read by the dispatcher via IMuteStore.GetPrefixAsync. This slice is read-only: there is no runtime setter/UI to change the prefix yet — a #settings control to edit it is deferred to a later slice. So today every server uses the ! default.
  • Any teammate may run any command (no role gating — that's a later subsystem).

Two new public seams

  • IRustServerQuery (read side of the live socket, implemented by ConnectionSupervisor, returning our own ServerInfoSnapshot/ServerTimeSnapshot DTOs so RustPlusApi types don't leak).
  • IMuteStore + ServerCommandSettings entity (FK→RustServer cascade, CommandSettings migration), shared with Features.Chat, which gates its relay on mute.

Deferred: team-intel commands (!online/!team/!afk/…, → 3b-ii), history + !leader (need a team-event tracker), Discord surfaces (#commands, /help, /uptime, → 3c), and the prefix-edit settings surface (the store + column exist; the write path is later).

Test Plan

  • Full suite green — 202 tests across 7 assemblies (Commands +43 new), 0 failures
  • dotnet build -warnaserror → 0 warnings / 0 errors
  • ReSharper jb cleanupcode --profile=ReformatAndReorder clean (pre-push gate passes)
  • No EF model drift (has-pending-model-changes)
  • DI scope validation (validateScopes: true) — no captive dependency (singleton hosted service + inbound processor open per-event/-message scopes for scoped stores)
  • CommandCooldown.TryConsume atomic under concurrency (lock-free CAS + parallel test)
  • Manual: pair a server, type !pop/!time/!wipe/!uptime in-game and confirm replies; !mute then confirm both command replies and Discord→game relay go silent; !unmute restores both

🤖 Generated with Claude Code

HandyS11 and others added 15 commits June 16, 2026 19:24
Create RustPlusBot.Features.Commands (refs Abstractions, Persistence, Connections) and its test project; implement CommandOptions with DefaultPrefix and Cooldown defaults; wire both projects into the solution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ttings migration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 8: MuteCommandHandler and UnmuteCommandHandler (internal sealed,
primary-constructor), each calling IMuteStore.SetMutedAsync and returning a
localized confirmation. Tests written first (TDD); 2 new + 19 prior = 21 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….server key

Final-review cleanup: DefaultPrefix was bound/validated but never read (the
per-server prefix lives in ServerCommandSettings with a "!" fallback in the
store), and command.uptime.ok.server was an unused localization key.
Copilot AI review requested due to automatic review settings June 16, 2026 19:23

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

Adds a new in-game !command framework that consumes TeamMessageReceivedEvent and dispatches parsed commands with localized replies, plus new persistence for per-server mute/prefix settings and a new live-query seam for server info/time.

Changes:

  • Introduces RustPlusBot.Features.Commands (dispatcher, cooldown, hosted service, localization, and 6 thin-slice commands).
  • Adds IRustServerQuery + DTO snapshots and implements the query seam in ConnectionSupervisor.
  • Persists per-server command settings (mute + prefix) via EF (ServerCommandSettings, migration, IMuteStore/MuteStore) and gates Discord→game relay on mute.

Reviewed changes

Copilot reviewed 56 out of 57 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/RustPlusBot.Persistence.Tests/Commands/ServerCommandSettingsSchemaTests.cs Verifies EF schema + cascade behavior for command settings.
tests/RustPlusBot.Persistence.Tests/Commands/MuteStoreTests.cs Covers MuteStore defaults and mute persistence behavior.
tests/RustPlusBot.Features.Connections.Tests/ServerQueryTests.cs Exercises new IRustServerQuery behavior via ConnectionSupervisor harness.
tests/RustPlusBot.Features.Connections.Tests/Fakes/FakeRustSocketSource.cs Extends fake connection to return server info/time snapshots.
tests/RustPlusBot.Features.Commands.Tests/RustPlusBot.Features.Commands.Tests.csproj Adds new Commands test project wiring.
tests/RustPlusBot.Features.Commands.Tests/Localization/CommandLocalizerTests.cs Tests localization fallback/formatting/normalization.
tests/RustPlusBot.Features.Commands.Tests/Hosting/BotUptimeTests.cs Tests uptime baseline behavior.
tests/RustPlusBot.Features.Commands.Tests/Handlers/QueryHandlersTests.cs Tests handlers using IRustServerQuery (pop/time/wipe/uptime).
tests/RustPlusBot.Features.Commands.Tests/Handlers/MuteHandlersTests.cs Tests mute/unmute handlers + localization.
tests/RustPlusBot.Features.Commands.Tests/Formatting/DurationFormatTests.cs Tests compact duration formatting.
tests/RustPlusBot.Features.Commands.Tests/Dispatching/CommandLineTests.cs Tests command line parsing and custom prefix behavior.
tests/RustPlusBot.Features.Commands.Tests/Dispatching/CommandDispatcherTests.cs Tests dispatcher behavior incl. mute gate + cooldown + prefix.
tests/RustPlusBot.Features.Commands.Tests/Dispatching/CommandCooldownTests.cs Tests cooldown window semantics.
tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs Ensures DI registrations resolve and expected handlers are present.
tests/RustPlusBot.Features.Commands.Tests/CommandOptionsTests.cs Tests default command options values.
tests/RustPlusBot.Features.Chat.Tests/TeamChatInboundProcessorTests.cs Updates inbound relay tests to include mute gate via scoped IMuteStore.
tests/RustPlusBot.Features.Chat.Tests/ChatRegistrationTests.cs Registers scoped IMuteStore in test DI to validate scope correctness.
src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs Registers IMuteStore in persistence DI.
src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs Updates model snapshot for ServerCommandSettings.
src/RustPlusBot.Persistence/Migrations/20260616175850_CommandSettings.Designer.cs Adds migration designer for command settings table.
src/RustPlusBot.Persistence/Migrations/20260616175850_CommandSettings.cs Adds migration creating ServerCommandSettings table.
src/RustPlusBot.Persistence/Configurations/ServerCommandSettingsConfiguration.cs EF configuration for command settings entity + cascade FK.
src/RustPlusBot.Persistence/Commands/MuteStore.cs Implements EF-backed mute/prefix reads and mute persistence.
src/RustPlusBot.Persistence/Commands/IMuteStore.cs Defines persistence seam for mute/prefix reads and mute writes.
src/RustPlusBot.Persistence/BotDbContext.cs Adds DbSet + configuration application for command settings.
src/RustPlusBot.Host/RustPlusBot.Host.csproj References Commands feature from the host.
src/RustPlusBot.Host/Program.cs Binds/validates CommandOptions and registers Commands feature.
src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs Implements IRustServerQuery to expose live server info/time snapshots.
src/RustPlusBot.Features.Connections/Listening/ServerTimeSnapshot.cs Introduces time snapshot DTO decoupled from RustPlusApi types.
src/RustPlusBot.Features.Connections/Listening/ServerInfoSnapshot.cs Introduces server info snapshot DTO decoupled from RustPlusApi types.
src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs Implements new query calls against RustPlusApi and maps to snapshots.
src/RustPlusBot.Features.Connections/Listening/IRustServerQuery.cs Adds new read-side live query interface.
src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs Extends socket connection seam with query methods.
src/RustPlusBot.Features.Connections/ConnectionServiceCollectionExtensions.cs Registers IRustServerQuery backed by ConnectionSupervisor.
src/RustPlusBot.Features.Commands/RustPlusBot.Features.Commands.csproj Adds Commands feature project with test visibility.
src/RustPlusBot.Features.Commands/Localization/ICommandLocalizer.cs Adds commands localizer abstraction.
src/RustPlusBot.Features.Commands/Localization/CommandLocalizer.cs Implements command localization with EN fallback + normalization.
src/RustPlusBot.Features.Commands/Localization/CommandLocalizationCatalog.cs Adds built-in EN/FR string catalog.
src/RustPlusBot.Features.Commands/Hosting/CommandsHostedService.cs Adds hosted service consuming team-message events and dispatching commands.
src/RustPlusBot.Features.Commands/Hosting/BotUptime.cs Adds uptime baseline singleton for !uptime.
src/RustPlusBot.Features.Commands/Handlers/WipeCommandHandler.cs Adds !wipe handler.
src/RustPlusBot.Features.Commands/Handlers/UptimeCommandHandler.cs Adds !uptime handler.
src/RustPlusBot.Features.Commands/Handlers/UnmuteCommandHandler.cs Adds !unmute handler.
src/RustPlusBot.Features.Commands/Handlers/TimeCommandHandler.cs Adds !time handler.
src/RustPlusBot.Features.Commands/Handlers/PopCommandHandler.cs Adds !pop handler.
src/RustPlusBot.Features.Commands/Handlers/MuteCommandHandler.cs Adds !mute handler.
src/RustPlusBot.Features.Commands/Formatting/DurationFormat.cs Adds compact duration formatter used by replies.
src/RustPlusBot.Features.Commands/Dispatching/ICommandHandler.cs Defines handler contract used by dispatcher.
src/RustPlusBot.Features.Commands/Dispatching/CommandLine.cs Adds command parsing for prefix + args.
src/RustPlusBot.Features.Commands/Dispatching/CommandDispatcher.cs Adds dispatcher (parse/lookup/mute gate/cooldown/reply).
src/RustPlusBot.Features.Commands/Dispatching/CommandCooldown.cs Adds per-(server,command) in-memory cooldown.
src/RustPlusBot.Features.Commands/Dispatching/CommandContext.cs Adds invocation context passed to handlers.
src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs Registers Commands DI graph (handlers, dispatcher, hosted service, etc.).
src/RustPlusBot.Features.Commands/CommandOptions.cs Adds configurable cooldown options.
src/RustPlusBot.Features.Chat/Inbound/TeamChatInboundProcessor.cs Gates Discord→game relay on persisted mute state via scoped store.
src/RustPlusBot.Domain/Commands/ServerCommandSettings.cs Adds domain entity for persisted command settings.
RustPlusBot.slnx Adds Commands feature and test project to solution.
Files not reviewed (1)
  • src/RustPlusBot.Persistence/Migrations/20260616175850_CommandSettings.Designer.cs: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +38
public bool TryConsume(Guid serverId, string name)
{
var now = clock.UtcNow;
var key = (serverId, name);
var allowed = true;
_last.AddOrUpdate(
key,
now,
(_, previous) =>
{
if (now - previous < _cooldown)
{
allowed = false;
return previous;
}

return now;
});
return allowed;
}
}

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.

Good catch — fixed in 0615e22. Replaced the AddOrUpdate + closure-flag pattern with a lock-free TryGetValue + TryAdd/TryUpdate compare-and-swap, so exactly one caller wins the write at window-expiry. Added a parallel test (ConcurrentCallers_ForSameKey_LetExactlyOneThrough, 64 threads) that asserts a single true.

Comment on lines +21 to +25
/// <summary>Gets the command prefix ("!" when unset).</summary>
/// <param name="guildId">Owning Discord guild snowflake.</param>
/// <param name="serverId">The Rust server id.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The trigger prefix, or "!" when unset.</returns>

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 — this slice ships the prefix as read-only: it's stored per (guild, server) on ServerCommandSettings.Prefix (default !) and read by the dispatcher, but there's no runtime setter/UI to change it yet. That write surface (a #settings control) is deferred to a later slice. I've updated the PR description to say so explicitly rather than imply it's runtime-configurable today.

The closure-captured 'allowed' flag inside ConcurrentDictionary.AddOrUpdate's
updateFactory was not an atomic gate: the factory can run concurrently/retry, so
two threads racing at window-expiry could both observe the same 'previous' and
both return true. Replace with a lock-free TryGetValue + TryAdd/TryUpdate CAS so
exactly one caller wins. Addresses Copilot review on PR #9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HandyS11
HandyS11 merged commit 240a349 into develop Jun 16, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/command-framework branch June 16, 2026 19:36
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