Rabbit Logo

Galaxy Server — Architecture Overview

"And now, watch our Assistant pull a rabbit out of the hat..."

Core Principle: Split “Thinking” from “Facts”

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.


Zones, Sectors, and Federation

Two distinct spatial concepts govern how the simulation is organized and deployed. These terms have precise meanings and should not be used interchangeably.

Zones

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.

Sectors

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.

Federation

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:

  1. The originating Sector Server sends a pre-arrival notification to the receiving Sector Server, including the ship’s full authoritative state.
  2. The receiving server acknowledges and prepares a slot.
  3. At the boundary-crossing tick, authority transfers: the originating server removes the ship; the receiving server adds it.
  4. The ship’s client is notified of the new server address and reconnects.

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 the Galaxy class across instances and building the Galaxy Router — is a future milestone planned for when the single-Sector feature set is complete.

The Captain/XO Model

This is the central organizing concept for how human and AI captains share the same underlying control protocol.

NPC Captains (headless clients)

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.

PC Captains (GUI clients)

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.

Character model alignment

The Character type captures this distinction:

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.


Randomness and the Character Sheet

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.

Dice rolls are server-side and authoritative

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).

Character stats as modifiers

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:

StatWhat it modifies
commandSkillCrew coordination, morale events, fleet maneuvers, recruited droid reliability
engineeringSkillRepair success, crewdroid maintenance tasks, refit quality, drive charge efficiency
shipAffinityShip-handling outcomes, docking, Artifact Drive engagement reliability
evaTrainingEVA 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.

Recording randomness in the command record

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.

Universe-level random events

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.


The Artifact Drive and Inertial Dampener

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.

Artifact Drive

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:

Inertial Dampener

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:

Codebase

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.


Economy and Credits

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

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.

Income: extraction operations

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.

Income: cargo hauling and contracts

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.

Spending

Codebase

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.


Crewdroids

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.

Roles

RolePrimary activities
MaintenanceRoutine repair, diagnostic cycles, hull patching
EVAExternal hull work, resource extraction, wreck salvage, inter-ship transfers
SecurityInternal defense against boarding parties
GeneralUnspecialized; 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.

Energy and work

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.

Dice-modified outcomes

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.

Group mechanics: defense and boarding

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.

Registry

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.


Conquest and Salvage

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.

Cargo salvage

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.

Droid recruitment

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.

Module salvage (future milestone)

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.

Codebase

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

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.

What it does

Spawn and respawn

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.

Command integration

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.


Server Owns the Clock

The server runs an autonomous tick loop. Clients do not trigger simulation steps.

Current status: The tick loop is implemented as a server-driven Vibe.d background fiber (SectorServer.tickFiber()). The tick interval is configurable via tick_interval_ms in galaxy.cfg. There is no client-driven tick endpoint.

Simulation Lifecycle (GM API)

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.

EndpointEffect
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>}.

Role enforcement

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 scope and known limitation

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.

Baseline capture

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.


Persistence and Recovery

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.

What is snapshotted

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.

Snapshot frequency and storage

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.

Startup sequence

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.

Graceful shutdown

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.

Multi-server sector isolation

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.

Codebase

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.


Transport Layer

REST (HTTP)

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).

WebSocket (planned)

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.

Command Protocol

What clients send

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.

What the server returns

Each command produces a CommandResult with a status ("accepted", "rejected", "partial", "error"), a details payload, and where applicable the dice roll record from resolution.

Routing

CommandRequestCommandApiCommandDispatcherCommandHandler (one per kind) → CommandResult. Adding a new command type means implementing a CommandHandler subclass and registering it; the dispatcher and API are unchanged.


Server-Side Invariant Enforcement


NPC Scaling

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.


Sensor Visibility and Information Limits

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.


Division of Work (Target)

LayerResponsibilitiesApprox. 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%

Implementation Map

ConceptModule(s)Status
Authoritative simulation corecore.controller.SimulationControllerExists
Galaxy (Zone manager for one Sector)core.galaxyExists
Zones (orbital regions, formerly Sectors)core.zoneExists
Entities and registrycore.entity.Entity, core.entity.EntityManagerExists
Vesselscore.vessel.VesselExists
Ship systems (engines, shields)core.system.ISystem, EngineSystem, ShieldSystemExists
Artifact Drive systemcore.system.ArtifactDriveSystemExists
Inertial Dampener systemcore.system.InertialDampenerSystemExists
Locations and movement graphcore.location.LocationGraphExists
Crew members and registrycore.crew.CrewMember, core.crew.CrewRegistryExists
Crewdroidscharacters.crewdroid.CrewdroidExists; registry planned
Resources and consumptioncore.resource.ResourceRegistry, ResourceStateExists (including driveCharge and cargo fields)
The Artifact (ship fabrication/repair)core.artifact.Artifact, ShipBlueprintExists
Wreck entitiescore.wreck.Wreck, core.wreck.WreckRegistryExists
Ship modules (future)core.module.ShipModule, ModuleRegistryDeferred milestone
Station / Market entitiescore.station.Station, core.station.StationRegistryExists
Credits (on Character)characters.characterTypes.Character.creditsExists
Typed ID safetycore.ids.TypedIdExists
Dice rolls and randomnessutilities.dice.DiceExists
Command protocolcommands.request.CommandRequest, commands.model.Command/CommandResultExists
Command routingcommands.dispatcher.CommandDispatcher, commands.handler.CommandHandlerExists
HTTP entry pointcommands.api.CommandApiExists
Scenario wiringcore.composition.buildScenarioExists
Character types, stats, serializationcharacters.characterTypes.CharacterExists
Character persistence contractcore.db.ICharacterStoreExists
Game state snapshot and recoverydb.gameStateRepo.GameSnapshot, IGameStateStore, MongoGameStateStoreExists
HTTP server and route handlersserver.SectorServerExists
Simulation lifecycle (pause / resume / reset)SimulationController.pause(), resume(), reset(), captureInitialSnapshot()Exists
GM-only route enforcementSectorServer.checkGmAuth()Exists
Galaxy Router / Federation layerTBDFuture milestone
Configuration (server)utilities.config.ConfigExists
Configuration (manager client)config.managerConfig.ManagerConfig in galaxyManager/Exists
String utilitiesutilities.stringutilsExists