feat: GM/admin commands (spawn, tp, kill, resurrect) #31

Merged
marco merged 3 commits from feat/gm-commands into main 2026-07-17 16:38:30 +02:00
Owner

Summary

Game-master commands, run from the chat box with a / prefix (e.g. /spawn rat). Server-authoritative and gated on a signed JWT admin claim — a client can't self-grant.

  • Admin identity: new AppUser.IsAdmin DB column (dual-provider migration) is the source of truth; Auth stamps an admin claim into the token at login. Bootstrap the first admin with Admin:Usernames in Auth config (persisted to the DB on next login). Dev config seeds test.
  • Command hierarchy (your design): BaseCommandGmCommand (implements CanExecute = admin check) → SpawnCommand / TeleportCommand / KillCommand / ResurrectCommand, dispatched by CommandRegistry. Authorization lives in the command (CanExecute), not scattered in the dispatcher.
  • v1 commands: /spawn <rat>, /tp <x> <y>, /kill (nearest creature → corpse + respawn), /resurrect (/res).
  • World stays a thin facade: death/corpse/resurrect logic lives in CombatSystem (extracted Slay/ScheduleRespawn/ForceResurrect), reused by both combat and the GM paths; World only orchestrates. Effects surface through the normal tick/AoI reconcile; feedback returns as a SystemMessage.
  • Security: pinned token validation to HS256 (rejects alg:none/confusion) on top of the existing signature + issuer + audience + lifetime checks.
  • Client: /-prefixed chat → GmCommandRequest; SystemMessage shown over the player + logged.

Docs

docs/gm-commands.md is the canonical command list; a new CLAUDE.md rule requires updating it in the same change that adds/changes a command.

Tampering — why it's safe

The admin claim is inside an HMAC-SHA256-signed token; the GameServer validates the signature against the shared secret. Editing the payload breaks the signature → rejected. The residual risk is key secrecy — Jwt:Key must be a strong secret in prod (dev key is a placeholder).

How it was tested

  • Auth: token carries the admin claim iff IsAdmin (unit).
  • GameServer: non-admin command refused with no effect; admin spawn adds a creature; tp moves the player; kill slays the nearest creature leaving a corpse; unknown command reported (unit, via CommandRegistry + World).
  • Protocol round-trips for GmCommandRequest / SystemMessage.
  • 196 tests green, zero warnings, CSharpier clean.

Needs your eyes (GUI)

As the test user (auto-admin in dev): open chat, /spawn rat, /kill, /tp 60 60, and (after dying) /res — feedback shows over your character. A non-admin gets "You are not allowed to do that."

Checklist

  • just lint clean · [x] just test green (196) · [x] whole solution builds, 0 warnings · [x] multi-platform preserved · [x] tests added · [x] EF migrations for both providers
## Summary Game-master commands, run from the chat box with a `/` prefix (e.g. `/spawn rat`). Server-authoritative and gated on a **signed JWT `admin` claim** — a client can't self-grant. - **Admin identity:** new `AppUser.IsAdmin` DB column (dual-provider migration) is the source of truth; Auth stamps an `admin` claim into the token at login. Bootstrap the first admin with `Admin:Usernames` in Auth config (persisted to the DB on next login). Dev config seeds `test`. - **Command hierarchy (your design):** `BaseCommand` → `GmCommand` (implements `CanExecute` = admin check) → `SpawnCommand` / `TeleportCommand` / `KillCommand` / `ResurrectCommand`, dispatched by `CommandRegistry`. Authorization lives *in the command* (`CanExecute`), not scattered in the dispatcher. - **v1 commands:** `/spawn <rat>`, `/tp <x> <y>`, `/kill` (nearest creature → corpse + respawn), `/resurrect` (`/res`). - **World stays a thin facade:** death/corpse/resurrect logic lives in `CombatSystem` (extracted `Slay`/`ScheduleRespawn`/`ForceResurrect`), reused by both combat and the GM paths; World only orchestrates. Effects surface through the normal tick/AoI reconcile; feedback returns as a `SystemMessage`. - **Security:** pinned token validation to `HS256` (rejects `alg:none`/confusion) on top of the existing signature + issuer + audience + lifetime checks. - **Client:** `/`-prefixed chat → `GmCommandRequest`; `SystemMessage` shown over the player + logged. ## Docs `docs/gm-commands.md` is the canonical command list; a new **CLAUDE.md** rule requires updating it in the same change that adds/changes a command. ## Tampering — why it's safe The `admin` claim is inside an HMAC-SHA256-signed token; the GameServer validates the signature against the shared secret. Editing the payload breaks the signature → rejected. The residual risk is key secrecy — `Jwt:Key` must be a strong secret in prod (dev key is a placeholder). ## How it was tested - Auth: token carries the `admin` claim iff `IsAdmin` (unit). - GameServer: non-admin command **refused with no effect**; admin `spawn` adds a creature; `tp` moves the player; `kill` slays the nearest creature leaving a corpse; unknown command reported (unit, via `CommandRegistry` + `World`). - Protocol round-trips for `GmCommandRequest` / `SystemMessage`. - **196 tests green**, zero warnings, CSharpier clean. ## Needs your eyes (GUI) As the `test` user (auto-admin in dev): open chat, `/spawn rat`, `/kill`, `/tp 60 60`, and (after dying) `/res` — feedback shows over your character. A non-admin gets "You are not allowed to do that." ## Checklist - [x] `just lint` clean · [x] `just test` green (196) · [x] whole solution builds, 0 warnings · [x] multi-platform preserved · [x] tests added · [x] EF migrations for both providers
feat(gm): admin commands via chat (spawn, tp, kill, resurrect)
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m35s
db01aae213
GMs run chat-prefixed commands ("/spawn rat") authorized server-side by a signed
JWT admin claim — never a client-sent flag. Admin identity is AppUser.IsAdmin (new
Auth DB column, dual-provider migration), stamped into the token at login and
bootstrapped from an Admin:Usernames config list.

Commands are objects: BaseCommand -> GmCommand (CanExecute = admin check) ->
SpawnCommand/TeleportCommand/KillCommand/ResurrectCommand, dispatched by
CommandRegistry. Authorization lives in CanExecute; every effect goes through the
World facade (World stays a thin facade — the death/corpse/resurrect logic lives in
CombatSystem, reused by the GM slay/resurrect paths) and surfaces via the normal
tick/AoI reconcile; feedback returns as a SystemMessage. Client sends "/" lines as
GmCommandRequest and shows SystemMessage feedback.

Hardened token validation to pin HS256. Canonical command list in docs/gm-commands.md
(kept in sync per a new CLAUDE.md rule).
refactor(gm): one command per file; discover spawnable creatures dynamically
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m31s
84adeb23cb
Split the command types into one file each (BaseCommand, GmCommand, CommandContext,
and the four commands) and adopt "one type per file" as a project rule in CLAUDE.md.
Replace the hand-maintained spawn dictionary with a reflection-based CreatureRegistry
that indexes every concrete BaseCreature by type name — a new creature is spawnable
with no command change.
fix(gm): /kill targets your selected creature, not the nearest
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m34s
889d01d1c6
/kill now slays the creature you currently have targeted (the highlighted one):
the client fills the target id from the selection, and the command requires it —
no more killing a random nearby rat. No selection returns a hint instead.
marco merged commit cb606d79b3 into main 2026-07-17 16:38:30 +02:00
marco deleted branch feat/gm-commands 2026-07-17 16:38:30 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!31
No description provided.