diff --git a/plan/README.md b/plan/README.md index a8d3f49..d7056aa 100644 --- a/plan/README.md +++ b/plan/README.md @@ -91,6 +91,7 @@ The exit criterion is met and something is still open in the file. |---|---|---| | [match-records](match-records.md) | A finished match is player-owned, checkable data | blocked | | [match-lifecycle](match-lifecycle.md) | A match starts, survives, and ends properly | open | +| [match-control](match-control.md) | In-match commands run through us, not MegaMek's chat box | open | | [session-security](session-security.md) | A session is only as alive as the grant behind it | open | | [managed-store](managed-store.md) | Losing the box does not lose login grants | open | | [proxy-split](proxy-split.md) | Deploying the API does not drop every live match | open | diff --git a/plan/match-control.md b/plan/match-control.md new file mode 100644 index 0000000..8b6ac1c --- /dev/null +++ b/plan/match-control.md @@ -0,0 +1,185 @@ +--- +id: match-control +title: In-match commands run through us, not MegaMek's chat box +status: open +repos: [headquarters, arena, infra] +dependsOn: [lobby] +exitCriterion: > + A player surrenders from our own UI, an admin force-defeats a player through + the same entry point, and MegaMek's chat box is gone from the client. +--- + +# match-control + +Everything a player or an operator can do to a running match that is not a +move, a shot or a Done button. Today all of it goes through one place: the +chat overlay MegaMek draws on the board. `/defeat` is how a player surrenders, +`/victory` is how the other side accepts, and every operator action from +`/kick` to `/endgame` is a line of text typed into the same box. + +That box is also the only in-match chat, which is the other half of this: the +lobby has a chat channel of our own already, and it stops at the lobby door. + +The order matters more than usual. Our own control path for everything the +chat commands do comes first; the chat interface is replaced only once every +command in the ledger below is covered, and the box is removed last. A +half-replaced chat box is worse than either end state — a player who cannot +find `/defeat` and has no Surrender button either has no way out of a match. + +**One entry point, two actors.** A player pressing Surrender and an admin +running a CLI that force-defeats somebody reach the same operation in arena. +What differs is the actor and the authorization in front of it, not the +wiring underneath. + +## What is already there + +Verified against MegaMek 0.51.0, the version arena's `versions.env` pins, and +against the Suramadu bundle inside a built arena image. + +**No setting can turn chat off.** `GUIPreferences` has four chat keys, and all +four are cosmetic: background colour, transparency, auto-slide, sound. +`ChatterBoxOverlay` implements `IDisplayable`, an interface with no visibility +method at all, and `BoardView` draws every overlay unconditionally. Hiding it +is a patch. arena carries nine already, so that is a known cost rather than a +new one. + +**Commands do not have to be typed.** `Server.processCommand` uses `args[0]` +only to look the command up; `Server.getCommand(String)` and +`ServerCommand.run(int connId, String[] args)` are both public, and +`ClientServerCommand`'s argument parser starts at `args[1]`. Nothing anywhere +reads the slash. + +**The important ones do not need the command objects either.** +`TWGameManager.forceVictory(victor, endImmediately, ignorePlayerVotes)` is +public, and `ignorePlayerVotes` marks every player as admitting defeat, so +`PlayerAgreedVictory` resolves without the `/defeat` → `/victory` → `/defeat` +round trip. `DefeatCommand` itself is two lines: a chat announcement and +`player.setAdmitsDefeat(true)`. The ceremony is the chat. + +**MatchHost already holds the server.** It constructs `Server` and +`TWGameManager` in-process and already drives the game by player id — that is +what the synthesised `PLAYER_READY` packets in the ready-up path are. + +**MatchWatcher is already an observer client**, in its own JVM, subscribed by +`GameListener`. Broadcast chat and game events arrive with no patch at all. +Per-connection replies — `sendServerChat(connId, …)`, which is how a command +answers the player who ran it — do not, so the control layer emits its own +results rather than trying to observe MegaMek's. + +**The browser half is proven.** `suramadu/web/flare.js` is already HTML over +the match canvas. Suramadu registers its key and mouse listeners on +`document` but forwards nothing whose target is not its canvas, an aria +element or its own hidden input, so an HTML input cannot leak keystrokes into +MegaMek and cannot lose focus to it. The CSP in `suramadu/web/index.html` +already allows `wss:` to any host. + +**The chat itself already exists here.** `matches::lobby` holds a per-match +broadcast channel with `CHAT_HISTORY` lines of backlog, `LobbyEvent::Chat` +out and `LobbyRequest::Chat` in, attributed to the speaker's own session DID. +One line stops it carrying into the match: `lobby_live` answers 409 the +moment `status != "lobby"`. + +## The ledger is the exit condition + +`TWGameManager.getCommandList` registers 48 commands. Each one is replaced, +made admin-only, or dropped with a reason. The chat box comes out when every +row is decided and the first two groups are built. + +**Replaced** — `defeat`/`victory`, `who`, `roll`, `checkbv`/`checkbvteam`, +`listentities`, `showentity`, `showtile`, `showvalidtargets`, +`team`/`changeteam`/`allowteamchange`, `traitor`, `exportlist`. + +**Admin only, never player-reachable** — `kick`, `reset`, `endgame`, +`gamemaster`/`allowgamemaster`, `skip`, `seeall`, `singleblind`, `kill`, +`fixelevation`, `changeownership`, and the game-master effects: `nuke`, +`nuclearstrike` and its custom variant, `orbitalbombardment`, `disaster`, +`firestarter`, `firestorm`, `firefight`, `nofires`, `removesmoke`, `rescue`, +`changeweather`. + +**Dropped, one documented line each** — `help` and `bothelp` print into a chat +box that will not exist; `save`, `load`, `listsaves`, `localsave` and +`localload` have no meaning in a container that hosts one match and exits; +`addbot`; `ruler`, which is already a menu item. The convention to follow is +arena's `megamek/prune-data.sh`: one line per thing removed and why nothing +can reach it. + +The client-side commands in `megamek/client/commands` are a separate set that +runs inside the Swing client rather than on the server, so no API outside that +JVM can reach them. They go with the box, and every one of them duplicates a +button or a menu item. + +## Stages + +Each is a PR or a short stack. arena leads. The two repos cannot be one `atgc` +stack, so this is two chains with an ordering between them. + +- [ ] **The control layer, no transport** (arena). `MatchControl` beside + `MatchHost`: typed operations over the `Server` and `TWGameManager` it + already holds, each taking an actor — a seat, or an admin. Calling + `forceVictory` directly skips MegaMek's own + `canRunRestrictedCommand`, so the authorization it was doing has to be + written here instead. Surrender first, because it is the one with a + rules question inside it. Every call logged with actor, operation and + outcome. +- [ ] **The rest of the first two groups** (arena). Player-facing, then + admin. Probably two PRs; this is the bulk of the work and it is what + decides when the ledger closes. +- [ ] **A loopback control socket** (arena, then infra). `MatchHost` listens + on `127.0.0.1` with a token from the launch manifest, and an + `arena.sh` subcommand drives it over `docker exec` — the admin CLI + working end to end against a real container with headquarters not + involved yet. Reaching it from outside the task is the infra half: + `modules/match-cluster` allows exactly one port from the proxy's + security group, so a second one is a variable and an ingress rule. + Riding the existing 8080 instead means Suramadu's Jetty owns the route, + which is a worse trade than one rule. +- [ ] **Events outward** (arena). `MatchControl` results and the game's own + chat, published where a browser can read them. +- [ ] **The control route** (headquarters). Session DID → seat → the + container's socket, over the task address the proxy already resolves. + The admin path gets its own authorization and the same downstream call; + a test that asserts the two converge is what keeps the one-entry-point + property true rather than aspirational. +- [ ] **The live channel past the lobby** (headquarters). Lift the 409 in + `lobby_live`, and decide how long the channel and its backlog live once + a match is running. Post-match is the same channel a third time. +- [ ] **The overlay, alongside MegaMek's chat** (arena). Chat, plus the + commands this actor is allowed to run, in `suramadu/web/` next to + `flare.js`. Both chat surfaces live at once on purpose: this is where + the ledger gets proven, with the old path still present to compare + against. +- [ ] **Remove the chat box** (arena). Three edits in one patch: the overlay + registration in `ClientGUI` (construct, `addKeyListener`, `addOverlay`); + the `TOGGLE_CHAT` and `TOGGLE_CHAT_CMD` bindings in `BoardView`, without + which Enter still sets `chatterBoxActive` and silently kills every + keybind until the next phase change; and the lounge Done button, which + `ClientGUI` reparents *into* the chat panel before setting that panel as + the lounge's bottom component. Last, and only with the ledger closed. + +## Decisions not yet made + +- [ ] **What conceding means with more than two seats.** `/defeat` marks one + player as admitting defeat and waits; `forceVictory` needs a victor to + name. Neither is right on its own for one player conceding a 2v2, and + the answer is a rules decision before it is a code one. +- [ ] **Where the admin CLI points in production.** `docker exec` covers + local development. A Fargate task means ECS exec or going through + headquarters. Making headquarters the only door gives one authorization + story and works without exec; the loopback socket stays for local use. +- [ ] **Whether in-match chat keeps the lobby's identity model.** A lobby lets + anyone connected talk, because a lobby is where seats get arranged. In a + match the seats are known, so "who may talk" can be narrower, and + spectators are a question the lobby never had to answer. + +## Not part of this + +Moderation of what gets said — rate limit, mute, report path — is +[user-safety](user-safety.md), and an in-match chat of our own inherits that +gap rather than creating it. The lobby channel itself is [lobby](lobby.md). +The empty lobby players are dumped into after victory, and the operator-only +kill script, are [match-lifecycle](match-lifecycle.md) — though the admin +surface this epic builds is the obvious place for that script to end up. + +## Done + +Nothing yet. The research above is the first deliverable.