Skip to content

The operator console is a typed, socket-free dispatcher over the server facade ​

Status: Accepted (2026-07-07)

Context ​

The operator console had grown into one large async function: every command parsed its own arguments, sent its own packets on the socket and logged its own outcome. Parsing, the domain action and socket I/O were mixed, so no command could be tested without a socket, and a second way into the console (a TCP or web admin adapter) would have meant rewriting every command.

The console needs enough structure that future adapters and resource-owned commands fit without changing the contract, but no code for consumers that do not exist yet. It builds on the server facade (ADR 0009).

Decision ​

  • The dispatcher is synchronous and adapter-agnostic. operator::dispatch(line, &mut ServerApi) returns Option<OperatorOutcome>: a typed reply (status and text) plus the Sends the command produced; None is a blank line. It does no socket I/O. The adapter (stdin today, the only one) delivers the sends and logs the reply. The facade borrow ends before any .await, as everywhere else in the server.

  • Commands are a table, not an enum. A built-in command is a BuiltinCommand { name, handler } in a static table; the dispatcher looks the name up and calls the handler, which parses its own arguments and owns its usage string. Each command is testable on its own, and a second lookup stage (resource-owned commands) adds to the table lookup instead of rewriting it.

  • A handler sees only ServerApi. No handler touches the wire plane, the roster, the transport or protocol types, so the console is not a second mutation path around the facade.

  • The module lives in the server binary (src/operator/), not the library. The console is a concern of the binary; the facade is the library's.

  • Outcomes are typed. Success, Usage (bad arguments, nothing changed), Failed (the facade refused, nothing changed) and Unknown (no such command). A remote adapter can map them to request results; the stdin adapter maps Success to an info log line and the rest to warnings.

  • Ownership of console commands. The core keeps only what is the server's own business; gameplay, world and admin actions belong to resources:

    • resource list | start | stop | reload manages the resource host (ADR 0025) and is handled in main.rs, because it mutates the host rather than the facade;
    • help, status, uptime, version (sver) and shutdown [reason] need server-loop context the facade deliberately lacks (ports, uptime, the resource host, the stop signal) and live in src/console.rs. shutdown (like Ctrl-C or SIGTERM) stops every resource, each after the ones that depend on it, then exits;
    • a resource adds console commands with dlmp.add_console_command(name, fn) (one resource per name, removed with the resource's stop); the shipped demo resource provides ban, unban and bans this way;
    • the operator table holds players (a read-only listing, including connections still waiting for login) and password set <text> | clear (the server password, as in the config).

    The console tries these planes in that order. A password line never reaches resource commands, because it carries the password.

Consequences ​

  • Each command is a small function tested without a socket.
  • A command's reply is text with a typed status rather than structured log fields; argument names and order are kept stable.
  • Remote adapters, a channel feeding the dispatcher, access control and permission metadata on commands are not built: they have no consumer. A remote adapter would sit on the same dispatch -> (reply, sends) seam; a remote-console protocol, if ever needed, would be an alias over this plane, not a second path into the server state.
  • The client and the wire protocol are not involved; the console is server-side only.

Alternatives considered ​

  • An OperatorCommand enum parsed first and executed in a central match. Rejected: two matches per command, and it cannot take dynamic, resource-owned commands without moving to boxed handlers later.
  • Putting the dispatcher in the library. Rejected: it would pull console-only data and concerns into the public domain crate and blur the console/domain boundary.
  • A boolean success flag or raw log levels as the outcome. Rejected: future adapters need a shared, typed meaning of the result.
  • Keeping gameplay commands (weapons, wanted level, kick, teleport, time, weather) as core built-ins. Rejected: they are gamemode policy and belong to resources, which can expose them through dlmp.add_console_command.

Documentation licensed under CC BY 4.0. DarqlineMP is not affiliated with Rockstar Games or Take-Two Interactive. Grand Theft Auto is a trademark of Take-Two Interactive Software, Inc.