The design divides the simulation into two layers with a clear contract between them:
Clients never send authoritative state. They send intents — “what I am trying to do” — and the server determines what actually happens, including any random element.
Two distinct spatial concepts govern how the simulation is organized and deployed. These terms have precise meanings and should not be used interchangeably.
A Zone is a named orbital region within a star system — a gameplay and narrative subdivision with no server boundary between them. Examples within the solar system: Earth Orbit, Mars Belt, Mars Surface Orbit, Inner Belt, Outer Belt. Ships move between Zones freely; the server managing the Sector handles all Zones within it.
In the codebase, what was previously called a Sector (a named region with coordinates and planets) is now a Zone. This naming change is complete; the module is core.zone.
A Sector is a star-system-scale region managed by exactly one server instance. The entire solar system is one Sector. Alpha Centauri, when the simulation expands there, will be another Sector on another server instance. All Zones within a Sector are the responsibility of that Sector’s server; there is no intra-Sector server boundary.
When the simulation spans more than one Sector, a thin Galaxy Router layer connects the Sector Servers, maintaining the inter-Sector map and brokering ship handoffs at Sector boundaries. The handoff sequence when a ship crosses a Sector boundary:
The pre-arrival step is important because a ship near a boundary may be within sensor range of entities in both Sectors; the receiving server must know about the ship before it fully crosses.
Current status: A single server instance (SectorServer) manages all Zones. The Sector-scoped federation refactor — distributing theGalaxyclass across instances and building the Galaxy Router — is a future milestone planned for when the single-Sector feature set is complete.
This is the central organizing concept for how human and AI captains share the same underlying control protocol.
A headless NPC client runs an AI Captain that reads the server’s snapshot for its ship, makes decisions (navigation, combat, trade), and emits a stream of control commands back to the server. The AI Captain is fully responsible for translating strategic goals into the low-level control stream.
A PC client inserts a human between the strategic layer and the control stream, but keeps the same AI machinery underneath:
Human Player
│
│ high-level orders
│ ("attack that freighter", "plot course to Centauri", "raise shields")
▼
AI Executive Officer (XO)
│
│ continuous control stream
│ (throttle curves, heading adjustments, weapon fire timing, shield toggles)
▼
[same command protocol as NPC]
│
▼
Galaxy Server
The XO’s job is to receive a player’s high-level intent and translate it into the same low-level control commands the NPC’s AI Captain would produce autonomously. From the server’s perspective, a PC captain and an NPC captain are indistinguishable — both emit CommandRequest objects with an originType of "pc" or "npc".
The practical consequence: the NPC codebase is the foundation. The PC codebase is the NPC codebase plus a human decision-making layer at the top. Adding a GUI does not require replacing the AI; it requires surfacing the AI’s state and intent to a human who can redirect it.
The Character type captures this distinction:
FictionRole: pc vs. npc — narrative role.controlMode: humanGui, aiAgent, script, idle — how the captain is currently being driven.A PC captain with controlMode = humanGui has a human feeding orders to the XO. The same captain with controlMode = aiAgent runs fully autonomously — the XO becomes the captain. This makes it natural for a PC ship to run on autopilot when the player is offline.
The galaxy is not a deterministic machine. Outcomes are shaped by two sources of randomness: the inherent unpredictability of the universe, and the varying capabilities of the captain and crew.
All dice rolls happen on the server. This is an invariant of the same character as “no negative fuel”: if clients could roll dice and report results, the anti-cheat guarantee collapses. The server is the sole source of random outcomes.
The Dice class (utilities.dice) provides the mechanism: a die of any number of sides (2–100), seeded independently per instance, with a roll returning a value in [1, sides]. Typical use is d6 (general chance), d20 (skill checks), and d100 (percentile events).
The Character sheet carries integer skill values that act as modifiers in the D&D tradition — a higher skill shifts the effective outcome of a roll:
| Stat | What it modifies |
|---|---|
commandSkill | Crew coordination, morale events, fleet maneuvers, recruited droid reliability |
engineeringSkill | Repair success, crewdroid maintenance tasks, refit quality, drive charge efficiency |
shipAffinity | Ship-handling outcomes, docking, Artifact Drive engagement reliability |
evaTraining | EVA mission outcomes, boarding action rolls, dampener failure survival |
The resolution pattern is:
effective result = dice roll + stat modifier
outcome = compare effective result against difficulty threshold
Difficulty thresholds are set per command kind by the CommandHandler for that kind.
The Command type carries a metadata field specifically for dice results and modifiers. When a handler resolves a command with a roll, it records: which dice were rolled and their results, which character stats contributed and by how much, the difficulty threshold tested, and the narrative outcome category (success, partial, failure, critical). This record is included in CommandResult.details and returned to the client.
Not all randomness is tied to a command. The server’s tick loop can generate spontaneous events — equipment malfunctions, sensor anomalies, unexpected encounters, resource discoveries — by rolling against configured probability tables. These are injected as SimEvents into the event queue and arrive at clients as part of the normal snapshot stream, giving the universe a living, unpredictable quality that neither player nor AI fully controls.
Singleships include two propulsion-related systems whose operating principles are not understood by anyone in the solar system. Both are ISystem implementations and subject to dice-modified outcomes.
The Artifact Drive produces velocities that conventional physics does not permit. It is the reason interplanetary and eventually interstellar travel is practical within a game timeframe. Key properties:
shipAffinity and current drive integrity determines whether the drive activates on a given attempt.ResourceState, separate from conventional fuel, that replenishes slowly and can be topped up at certain stations.The Inertial Dampener is a field system that makes survival at Artifact Drive velocities and high-g combat maneuvering possible. Without it, the accelerations involved would be immediately fatal. Key properties:
evaTraining and dampener system health.ArtifactDriveSystem and InertialDampenerSystem are both implemented as ISystem subclasses in core.system. ResourceState carries a driveCharge field; charge accumulates each tick via ResourceRegistry.accumulateDriveCharge(), called in SimulationController.updateVessel after the resource drain step. The command kind for drive engagement is engageDrive, handled by EngageDriveHandler. The existing move-vessel path remains for conventional in-Zone repositioning and docking.
The simulation has a functional economy. Captains earn by extracting resources or hauling cargo; they spend on supplies, equipment, and crewdroids. The economic layer is intentionally lightweight — the point is to create meaningful tradeoffs and motivate activity, not to simulate a full market economy.
Credits are the interoperable currency of the solar system, not issued by any one faction. Credits live on the Character record, not on the ship — because a captain survives the loss of their ship (the Artifact replaces it), but their financial position carries forward. A captain who loses their ship is not broke; they are shipless.
Crewdroids can be dispatched on EVA to an asteroid, ice body, or other raw resource. The extraction command produces cargo at a dice-modified yield and quality. Higher engineeringSkill improves yield; the environment difficulty sets the base difficulty threshold.
Captains can accept cargo contracts from factions, stations, or other captains. Delivery is a standard command sequence: pick up, transit, dock, deliver. Payment is recorded in Credits against the captain’s Character.
Character has a credits field persisted in MongoDB via CharacterRepository. ResourceState has a cargo field for generic extractable/tradeable material. Station entities (with hasMarket flag) exist in core.station and are managed by StationRegistry inside SimulationController. The implemented sell command is sellCargo, handled by SellCargoHandler: it validates zone co-location and enqueues a cargo-deduction SimEvent, while the server layer applies the credit to the captain’s Character record after the command is accepted (Option C two-phase pattern). Per-station pricing and extraction commands are future work.
Every ship, including the smallest singleship, is assigned at least two crewdroids. They are not Characters — they are simulation entities (Crewdroid extends Entity) managed by the EntityManager, with an energy model representing their charge level drawn from the ship’s power bus.
| Role | Primary activities |
|---|---|
Maintenance | Routine repair, diagnostic cycles, hull patching |
EVA | External hull work, resource extraction, wreck salvage, inter-ship transfers |
Security | Internal defense against boarding parties |
General | Unspecialized; capable of all roles at reduced effectiveness |
A droid’s role can be changed at runtime (setRole), subject to current energy and any active task.
All droid activity costs energy (performWork(energyCost)). A droid that lacks sufficient charge cannot perform work. Energy is replenished from the ship’s power bus (recharge(amount)), meaning crewdroid activity competes with engines, shields, life support, and drive charge for available power — a real resource tradeoff.
performWork() is the dice hook. Beyond the energy check, the success and quality of a work outcome is determined by a server-side roll modified by the droid’s current role, the captain’s relevant stat, and environmental difficulty.
Mob defense: When a boarding party enters the ship, Security and General droids form a defensive mob. The group roll is a single die modified by the number of combat-ready droids and their aggregate energy, tested against the boarding party’s attack difficulty. Outcomes are graduated: critical success, success, partial success, failure, critical failure.
EVA boarding support: EVA-capable droids can assist in or conduct boarding operations against another ship. The attack roll is modified by squad size, energy levels, and the captain’s evaTraining stat.
Crewdroids are currently registered in the EntityManager as typed entities. A dedicated CrewdroidRegistry (analogous to CrewRegistry) is the planned next step for efficient per-ship lookup and group queries.
When a ship’s hull reaches zero, it becomes a Wreck entity in the simulation rather than being removed. The conquering captain’s crewdroids can then conduct salvage operations against it.
EVA droids dispatched to a wreck can extract cargo, supplies, and spare parts. The server tracks a true condition for every salvageable item, but the initial report to the salvaging captain is a dice-modified estimate. As droid examination time accumulates, the estimated condition converges toward the truth. A captain who rushes a salvage operation may load damaged goods without knowing it; one who takes time gets a more accurate picture — but time has costs of its own.
The conquered ship’s crewdroids, if intact, can be offered a choice. This is a dice roll modified by the recruiting captain’s commandSkill. Success adds those droids to the captain’s complement; failure leaves them inert or resistant. Recruited droids carry a small reliability penalty on critical tasks that diminishes with accumulated service time. They were made for someone else, and they seem to know it.
The intended long-term design allows crewdroids to extract discrete ship modules from a wreck — engine components, shield emitters, sensor arrays — and attach them to the salvaging ship. This requires a ShipModule type and ModuleRegistry not yet in the codebase. Cargo-only salvage is the first implementation; module extraction is explicitly deferred and documented here as the planned next layer.
A Wreck entity type is created when a Vessel’s hull reaches zero during combat resolution. New command kinds: scavengeWreck, recruitDroids. Crewdroid gains optional originalShipId and loyaltyLevel fields. The server tracks trueCondition and observedCondition per salvageable item; a WreckExaminationHandler improves the observed value toward truth with each dice-modified examination step.
The Artifact is a unique station that appeared in high Earth orbit without explanation. It is the physical anchor of the early game and the mechanical foundation of the spawn/respawn loop.
ShipBlueprint registry defines the ship classes it can produce. Initially only the singleship is registered; additional classes can be unlocked or discovered.Character.homeArtifactId (defaulting to "artifact-earth-orbit") records which Artifact a character is anchored to. When the simulation detects a captain has no living ship, it routes them to their home Artifact for fabrication. Loss of a ship is a setback that costs time and possibly credits, not existence.
The Artifact is an Entity in the simulation. Interactions (dock, request fabrication, request repair) are modeled as CommandRequests routed through the standard dispatcher with dedicated CommandHandler implementations.
The server runs an autonomous tick loop. Clients do not trigger simulation steps.
SimEvents whose scheduled time has arrived, updates resource consumption and system state for every active vessel, generates any universe-level random events, then produces outgoing snapshots for connected clients.desiredExecuteTime field on CommandRequest allows clients to schedule actions at a future simulation time, within reason.Current status: The tick loop is implemented as a server-driven Vibe.d background fiber (SectorServer.tickFiber()). The tick interval is configurable viatick_interval_msingalaxy.cfg. There is no client-driven tick endpoint.
Three GM-only HTTP endpoints let an operator control the running simulation without restarting the process. All three require an authenticated session with simRole == "gm"; sessions with role "pc" or "npc" receive 403 Forbidden.
| Endpoint | Effect |
|---|---|
POST /simulation/pause |
Sets SimulationController._paused = true. tick() becomes a no-op; wall-clock time advances but simulation time does not. HTTP handlers remain live throughout. |
POST /simulation/resume |
Clears the paused flag. Normal ticking resumes on the next fiber wake. |
POST /simulation/reset |
Calls SimulationController.reset(), which restores elapsed time (to 0), vessel resource levels, wreck loot state, and the event queue to the baseline captured at cold-start. Immediately flushes a MongoDB snapshot so the reset state survives a crash before the next periodic write. |
Each endpoint returns {"status": "paused"|"running"|"reset", "elapsedTime": <double>}.
SectorServer.checkGmAuth() is a private helper that first calls checkAuth() (returns 401 if no session) and then checks sess.get("simRole") == "gm" (returns 403 if not). Only users authenticated from a storyteller account of type "admin" receive simRole == "gm"; guest (pc) and user (npc) accounts are rejected.
reset() restores the mutable GameSnapshot layer. ISystem fields — engine throttle, shield state — are outside the snapshot contract and are therefore not restored. A future deep reset milestone will replay the initial system-control events from buildScenario() so that throttle and shield state are also consistent after a reset. See SESSION.md known issue 2 for design notes.
SectorServer calls SimulationController.captureInitialSnapshot(sectorId) once in its constructor, immediately after buildScenario() and before the MongoDB restore. This locks in the buildScenario initial values as the reset target. The baseline snapshot is stored in memory only and is never written to MongoDB.
The server maintains a periodic snapshot of all mutable simulation state so that it can survive cold starts, crashes, and planned restarts without losing more than a few seconds of progress.
The snapshot captures only the dynamic state that changes during play — the parts that buildScenario() cannot reconstruct. Structural topology (which vessels, stations, wrecks, and zones exist, and what their identities are) is always rebuilt deterministically by buildScenario() at startup. The snapshot overlays the mutable layer on top of that structural skeleton: resource levels per vessel, wreck loot state, the pending SimEvent queue, and the simulation clock. Crew members are treated as ephemeral and are not snapshotted.
A snapshot is written to MongoDB every 5 wall-clock seconds, checked at the end of each tick handler. A snapshot is also written immediately on graceful shutdown (SIGTERM or SIGINT). The snapshot for a sector is stored as a single document in the gameState collection, keyed by sector-id, and upserted on each write so there is at most one document per sector at all times.
buildScenario() always runs first to construct the structural skeleton with stable hardcoded entity IDs. The server then attempts to load a snapshot for the configured sector_id. If one exists, SimulationController.restore() overlays it and the server logs “resumed from snapshot.” If none exists (first run or after a deliberate reset), the server logs “cold start — no snapshot found” and proceeds with default initial state.
SIGTERM and SIGINT are caught by an extern(C) nothrow @nogc signal handler that sets an atomic flag (g_shutdown). The background tick fiber polls this flag after every tick: if set, it writes a final snapshot, stops the HTTP listener, and exits the event loop. Because the fiber runs continuously, a SIGTERM is handled promptly regardless of HTTP activity.
Each server instance is assigned a sector_id in its configuration file (e.g. sector_id=sol). The gameState collection is filtered by sector-id, so multiple server instances sharing a MongoDB cluster do not interfere with each other’s simulation state. The characters collection is intentionally global and unscoped — a captain’s identity and financial position belong to the simulation at large, not to any particular sector server.
db.gameStateRepo defines GameSnapshot and its component record types (ResourceStateRecord, WreckStateRecord, SimEventRecord), the IGameStateStore interface, and MongoGameStateStore. SimulationController gains snapshot(sectorId) and restore(snap) methods. SectorServer wires snapshot writes into the constructor (restore on startup) and tick handler (periodic + shutdown). app.d registers the signal handlers and passes the shutdown flag pointer to the server.
Used for low-frequency, request/response interactions: authentication, character management, high-level one-off orders, on-demand state snapshots, and admin/GM operations including simulation lifecycle management (POST /simulation/pause, /simulation/resume, /simulation/reset).
Used for the high-frequency real-time channels:
Current status: Only the REST channel exists. WebSocket support is a future milestone; Vibe.d supports it natively.
Each command is a CommandRequest with: commandId, originType ("pc", "npc", "gm", "system"), originId, kind, payload, requestTime / desiredExecuteTime, and schemaVersion. No packet directly sets position, resource values, enemy state, or dice results.
Implemented command kinds: moveActorSimple (actor repositioning within a zone), engageDrive (Artifact Drive engagement), salvageCargo (EVA cargo extraction from a wreck), sellCargo (sell all held cargo at a market station). Planned future kinds include: setThrottle, fireWeapon, dockAtArtifact, extractResource, buyItem, buyCrewdroid, scavengeWreck, recruitDroids.
Each command produces a CommandResult with a status ("accepted", "rejected", "partial", "error"), a details payload, and where applicable the dice roll record from resolution.
CommandRequest → CommandApi → CommandDispatcher → CommandHandler (one per kind) → CommandResult. Adding a new command type means implementing a CommandHandler subclass and registering it; the dispatcher and API are unchanged.
NPCs are headless clients running the same control protocol as PC clients. Scaling NPC population means adding more AI worker processes, not scaling the server core. Character.isHeadlessOnly marks NPCs that should never receive a UI snapshot.
Status: future milestone. The current implementation sends full vessel snapshots to all authenticated clients.
Each vessel will have a sensor range driven by its SensorSystem and available power. Outgoing snapshots will be filtered to include only the client’s own vessel (full detail), entities within sensor range (reduced detail), and entities with explicitly granted visibility. Entities outside sensor range are absent from the snapshot entirely. Wrecks within sensor range appear as wrecks; their salvageable contents are not visible until examination begins.
| Layer | Responsibilities | Approx. CPU share |
|---|---|---|
| Client (PC or NPC) | UI rendering, AI decision-making, XO planning, local prediction, control frame generation, economy decision-making | 70–80% |
| Server | Physics integration, command validation, dice resolution, resource update, event dispatch, crewdroid work resolution, salvage examination, economy transaction validation, snapshot generation, sensor filtering | 20–30% |
| Concept | Module(s) | Status |
|---|---|---|
| Authoritative simulation core | core.controller.SimulationController | Exists |
| Galaxy (Zone manager for one Sector) | core.galaxy | Exists |
| Zones (orbital regions, formerly Sectors) | core.zone | Exists |
| Entities and registry | core.entity.Entity, core.entity.EntityManager | Exists |
| Vessels | core.vessel.Vessel | Exists |
| Ship systems (engines, shields) | core.system.ISystem, EngineSystem, ShieldSystem | Exists |
| Artifact Drive system | core.system.ArtifactDriveSystem | Exists |
| Inertial Dampener system | core.system.InertialDampenerSystem | Exists |
| Locations and movement graph | core.location.LocationGraph | Exists |
| Crew members and registry | core.crew.CrewMember, core.crew.CrewRegistry | Exists |
| Crewdroids | characters.crewdroid.Crewdroid | Exists; registry planned |
| Resources and consumption | core.resource.ResourceRegistry, ResourceState | Exists (including driveCharge and cargo fields) |
| The Artifact (ship fabrication/repair) | core.artifact.Artifact, ShipBlueprint | Exists |
| Wreck entities | core.wreck.Wreck, core.wreck.WreckRegistry | Exists |
| Ship modules (future) | core.module.ShipModule, ModuleRegistry | Deferred milestone |
| Station / Market entities | core.station.Station, core.station.StationRegistry | Exists |
| Credits (on Character) | characters.characterTypes.Character.credits | Exists |
| Typed ID safety | core.ids.TypedId | Exists |
| Dice rolls and randomness | utilities.dice.Dice | Exists |
| Command protocol | commands.request.CommandRequest, commands.model.Command/CommandResult | Exists |
| Command routing | commands.dispatcher.CommandDispatcher, commands.handler.CommandHandler | Exists |
| HTTP entry point | commands.api.CommandApi | Exists |
| Scenario wiring | core.composition.buildScenario | Exists |
| Character types, stats, serialization | characters.characterTypes.Character | Exists |
| Character persistence contract | core.db.ICharacterStore | Exists |
| Game state snapshot and recovery | db.gameStateRepo.GameSnapshot, IGameStateStore, MongoGameStateStore | Exists |
| HTTP server and route handlers | server.SectorServer | Exists |
| Simulation lifecycle (pause / resume / reset) | SimulationController.pause(), resume(), reset(), captureInitialSnapshot() | Exists |
| GM-only route enforcement | SectorServer.checkGmAuth() | Exists |
| Galaxy Router / Federation layer | TBD | Future milestone |
| Configuration (server) | utilities.config.Config | Exists |
| Configuration (manager client) | config.managerConfig.ManagerConfig in galaxyManager/ | Exists |
| String utilities | utilities.stringutils | Exists |