Below is a first-pass design document for the small commands layer, written for humans, not the compiler. It is intentionally concrete but language-agnostic enough that we can still tweak it before coding.
The commands layer sits between:
SimulationController, its event queue, and domain
registries.[1][2][3][4]Its responsibilities:
SimEvents via
SimulationController.enqueue*.[1]The commands layer does not:
SimEvent mechanism.CrewRegistry,
LocationGraph, etc., except through
SimulationController or carefully defined service
methods.[2][3][4][5][1]Commands can originate from:
actorId and possibly a
connection/session.All of these flow through the same commands layer; the origin is recorded in metadata and can influence permissions but not core structure.
For each incoming JSON command request:
JSONValue to the
commands layer.CommandRequest
object with a well-defined schema.SimulationController queries), compute:
SimEvent).SimEvents scheduled.SimEvents via SimulationController.enqueue*.
These events will be processed on the next tick as
usual.[1]CommandResult with:
CommandRequest.CommandResult and dice rolls.SimEvents), persist the queued command as
well.A CommandRequest is the normalized, in-memory
representation of what the caller asked for.
Key fields (conceptual):
string id:
string originType:
"pc", "npc", "gm",
"system", etc.string originId:
"pc" and "npc", usually an
actorId."gm", a GM user id or tool id.string kind:
"moveActor",
"setThrottle", "moveVessel",
"assignStation".double requestTime (simulation time):
double desiredExecuteTime (simulation time, optional):
JSONValue payload:
{ "toLocationId": "...", "speed": "walk" }{ "vesselId": "...", "systemId": "...", "value": 0.5 }Constraints:
CommandRequest should be trivially
serializable back to JSON for logging and persistence.version field to
allow changes to command format over time.We may distinguish:
CommandRequest: what the client asked for.Command: a derived object created after static
validation and rule evaluation.A Command object could be:
id: same as request.kind.actorId (normalized, even for GM commands that target
an actor).executeAt (simulation time actually chosen by
rules).payload (possibly augmented by rules: resolved location
ids, computed path, etc.).metadata:
JSON persistence of Command remains straightforward;
this supports crash recovery and debugging.
Represents what the server decided about a command and is sent back to callers.
Fields:
string commandId.string status:
"accepted", "rejected",
"partial", "error".double requestTime.double effectiveStartTime (sim time when first effect
happens).double effectiveEndTime (if known, e.g. duration of a
macro).JSONValue details:
reason for rejection.dice details (rolls, modifiers).events summary (e.g., which SimEvent kinds
were scheduled, at what times).resourceChanges predictions or actual deltas.This object is also kept JSON-friendly so it can be persisted in Mongo for audit and replay.[6]
Define a polymorphic handler abstraction, but keep it small:
CommandHandler responsibilities:
CommandRequest for that kind.Command (internal).SimEvents via
SimulationController.enqueue* as needed.[1]CommandResult.Handlers do not:
SimulationController.elapsedTime and its queues.[1]SimulationController methods.A central CommandDispatcher:
kind → CommandHandler.CommandRequest, does:
kind.CommandResult.This ensures extensibility:
No need to modify every caller.
In the “small layer” design, we do not create a new clock or independent loop. We keep a minimal CommandQueue that:
Command objects that:
SimEvents).Key design ideas:
SimEvent queue:
SimEvent is low-level, fully deterministic, and
executed by SimulationController.tick.[1]Command is higher-level, maybe multi-step, maybe
depends on dice and rules.enqueue* to schedule
SimEvents.Command remains in the queue.Conceptual fields per queued item:
Command (internal form).string state:
"pending", "inProgress",
"completed", "cancelled".double nextActionTime:
JSONValue macroState:
The queue itself is:
nextActionTime).commandsQueue).app.d main
loop) can:
nextActionTime <= currentSimTime and
state = "pending" or "inProgress".SimEvents or mark them completed.This is incremental: we can start without macros and only add the
macroState aspects once needed.
Macros are commands that expand into sequences or trees of other commands.
Examples:
LocationGraph.findPath.moveActorStep events or
one high-level moveActor event.[3]Design decisions:
SimEvents.macroState in the queue if the
macro is long-lived.Important constraint: macros should still have a single
CommandResult visible to the caller, summarizing
success/failure and key timings.
We already have Mongo infrastructure. The commands layer should use it to persist:[6]
commands_requests
CommandRequest as received.commandId, originType,
originId, kind, requestTime,
desiredExecuteTime, payload,
version.commands_results
CommandResult.commandId, status,
requestTime, effectiveStartTime,
effectiveEndTime, details.commands_queue (optional at first)
commandId, kind, state,
nextActionTime, macroState,
lastUpdated.The galaxy state (entities, vessels, resources, crew, sectors) and the simulation time are persisted via your core layer’s mechanisms as you already plan.[7][8][9][2][3][4][1]
On restart:
SimulationController.elapsedTime via your usual
persistence.[1]SimEvents or rely on the
persisted commands_queue:
state != completed:
macroState
and current time, to re-emit any missing SimEvents.This design assumes that SimEvents themselves are
either:
Key integration points:
SimulationController.galaxyView, crewView,
and other query APIs.[2][1]SimulationController (e.g., getters for
LocationGraph).[3][1]SimulationController.enqueueSetThrottle,
enqueueMoveActor, enqueueMoveVessel,
etc.[1]enqueue* methods as more command kinds
appear.CrewRegistry, LocationGraph,
ResourceRegistry, SystemRegistry
directly.SimulationController.[5][4][2][3][1]This keeps OO boundaries clear:
core: deterministic state + events.commands: intent-level rules + dice + mapping to core
events.app/server: IO, networking, and orchestration.Before we move to code, I’d like your preferences on a few points:
SimEvents?SimulationController (and serialized occasionally)?app.d polls and updates
each tick?payload
blob?Once we settle these, we can:
commands/request.d, commands/handler.d,
commands/persistence.d) and start implementing.Sources [1] controller.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/30070de6-50df-460f-b02c-2a9cd68e6c9a/controller.d [2] crew.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/794663ff-d875-49a3-a1c1-ce265991ed36/crew.d [3] location.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/80ddf50c-7272-434d-bd62-ef1664b18fda/location.d [4] resource.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/4fccda02-b426-497f-a2a7-e2bc96f6880e/resource.d [5] system.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/61f75032-a0aa-4045-a886-95657f8543fa/system.d [6] mongoClient.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/28aa99b4-3c99-4894-ad79-71638b111e61/mongoClient.d [7] entity.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/8ed0db1a-fa9b-43b9-aa4f-a2ec4f274a42/entity.d [8] vessel.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/1ad364aa-3c6e-4e56-bcc1-fccc120f24f0/vessel.d [9] sector.d https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/15197330/78c63c35-c826-4c97-83c6-64e677f23f9b/sector.d [10] singleShipAgent.md https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_06cc074f-93d4-4c2f-adcf-a637e908c6ce/4b377787-43a1-44a0-b4e2-2749e13f8588/singleShipAgent.md [11] dub.selections.json https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_06cc074f-93d4-4c2f-adcf-a637e908c6ce/2c693b58-f401-467f-a3ce-6f059f6d6234/dub.selections.json [12] dub.json https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/collection_06cc074f-93d4-4c2f-adcf-a637e908c6ce/085c0316-6f7d-44f9-aa11-4cab94022ee6/dub.json