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.

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. For the narrative perspective on how captains experience command through the XO, see The Captain’s Interface. For the technical architecture of the three-layer client model, see Client Architecture.

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.

XO safety: collision prevention

The XO enforces a safety constraint on all navigation commands: it prevents maneuvers that would result in collision with celestial bodies, stations, or other vessels. A navigation order that would place the ship on a collision trajectory is rejected by the XO before it reaches the server’s command pipeline. This applies equally to PC and NPC captains — the XO does not distinguish between a human order and an AI decision when evaluating collision risk.

Deliberate ramming as a tactical action is not currently supported. If introduced in a future combat system revision, it would require an explicit override command that bypasses the XO’s collision avoidance, with appropriate risk and consequence modeling.

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.

Character struct fields

The canonical Character struct (characters/characterTypes.d) carries:

CategoryFields
Identityid, name, callsign, species, gender, age, homeworld, faction
NarrativefictionRole (pc/npc), controlMode, importance, status (active/missing/dead/retired), concept, centralMotivation, coreConflict, loyalty, threatLevel, tags, groups
Auth / adminstoryteller_id, captainId, clearanceLevel, isHeadlessOnly
Ship / crewassignedVesselId, homeArtifactId, crewComplement (maxDroidSlots, currentDroidCount)
SkillsshipAffinity, engineeringSkill, commandSkill, evaTraining, tacticalSkill
Economycredits — Credits (CR); belongs to the captain, survives ship loss

Removed fields (v0.2): role, rank, and simulationRole were vestigial narrative metadata that carried no gameplay weight. Under the singleship model the captain’s role is implicit (they are the captain) and rank is meaningless in a solo command. These fields are silently ignored when loading legacy MongoDB documents.

CrewComplement was simplified from four fields (baseCrewSlots, baseDroidSlots, currentCrewCount, currentDroidCount) to two: maxDroidSlots and currentDroidCount. Under the singleship model there are no organic crew — only the captain (fixed in the lifepod) and crewdroids. Legacy documents with the old field names are read via fallback in fromJSON().


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 their crewdroids.

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
commandSkillCrewdroid 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
evaTrainingDirecting crewdroid EVA operations, boarding action rolls, dampener failure survival
tacticalSkillWeapon to-hit modifier in combat (see Combat section below)

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.


Combat and Weapon Resolution

Weapon fire is resolved per-tick inside step 4.5 of SimulationController.updateVessel(). Each WeaponSystem that has a target assigned and whose fire-cycle timer has elapsed fires once per tick, subject to the following sequential checks:

  1. Power check. The weapon consumes powerDraw × powerEfficiency units of vessel power. If power is insufficient, the shot does not fire and the cycle timer is not reset.
  2. To-hit roll. The server rolls a d20 and applies two modifiers:

    Full formula (with crewdroid at bridge):

      roll = d20() + tacticalSkill/10 + (accuracy − 50)/10
      hit  = (roll ≥ WEAPON_HIT_THRESHOLD)   // threshold = 12

    Full formula (no crewdroid at bridge):

      roll = d20() + (accuracy − 50)/10
      hit  = (roll ≥ WEAPON_HIT_THRESHOLD)   // threshold = 12

    At baseline with crewdroid (tacticalSkill = 50, accuracy = 50) the modifier is +5, so rolls of 7 or higher hit (70% hit rate). Without crewdroid (accuracy = 50), the modifier is +0, so rolls of 12 or higher hit (45% hit rate). A highly accurate weapon (accuracy 100) with a skilled captain (tacticalSkill 100) and crewdroid at bridge adds +10 total, making any d20 roll a hit.

  3. Damage application. On a hit, applyWeaponDamage(targetId, damage) is called. The damage field on WeaponSystem is in hull-fraction units [0, 1] (e.g. 0.08 = 8% per hit). If the target’s shields are raised, damage depletes shield integrity directly (shieldIntegrity −= damage) and the hull is untouched. Otherwise, damage reduces hull integrity directly (hullIntegrity −= damage). A vessel whose hull integrity reaches zero or below is queued for destruction at end-of-tick.
  4. Weapon wear. Each shot (hit or miss) degrades weaponIntegrity by a small fixed amount. A weapon at zero integrity continues to fire but may malfunction in future revisions.
  5. Cycle reset. The fire-cycle timer is reset to fireCycle seconds, establishing the delay before the weapon can fire again.

Weapon stats and blueprints

Each WeaponSystem carries a set of stats rolled at instantiation time from a WeaponBlueprint. The blueprint defines ranges; the factory rolls within those ranges adjusted by quality tier. The rolled stats that influence combat are:

StatRole in combat
damagePoints of damage applied on a hit (before shield/hull split)
accuracyShifts the to-hit roll; 50 is neutral, range is 0–100
fireCycleSeconds between shots; lower is faster
powerDrawRaw power consumed per shot
powerEfficiencyMultiplier on powerDraw; 1.0 is nominal, lower is more efficient
malfunctionChanceProbability of a malfunction event (future use)
requiresSparePartsWhether firing consumes spare parts (future use)
weaponIntegrityStructural health; depleted by wear per shot

All rolled stats and current weaponIntegrity are persisted to MongoDB as part of the GameSnapshot in a dedicated weaponInstances array, separate from the systemHealth array used by other systems.


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:

Transit lifecycle

The full lifecycle of an Artifact Drive transit is a multi-phase process. Each phase has concrete mechanics, formulas, and failure modes.

  1. Charge. The drive accumulates charge at chargeRate (default 0.03/s) toward the 1.0 maximum. Charge accumulates regardless of throttle or shield state and has no power cost. Partial charge engagement: the captain does not need full charge to transit. The required charge is proportional to distance: requiredCharge = distance / MAX_RANGE where MAX_RANGE = 250. A short 50-unit hop needs only 0.20 charge (~7s), while the longest Sol route (~183 units) needs 0.73 charge (~24s). Full charge (33s) covers any destination up to 250 coordinate units.
  2. Engagement roll (see Engagement Dice Roll below). The captain issues engageDrive. The server rolls a d20 with modifiers to determine whether the drive fires. On failure, the drive does not fire, charge is preserved, and a 15-second cooldown is imposed before retry.
  3. Fuel check. If the engagement roll succeeds, the server computes the fuel cost for the requested destination (see Fuel Cost Formula) and checks that the vessel has sufficient fuel. If fuel is insufficient, the engagement is rejected with status "rejected" and reason "insufficient_fuel". Charge is consumed (the drive fired) but the transit does not proceed. This is a costly mistake — the captain must wait 50 seconds to recharge.
  4. Enter transit. Proportional drive charge is consumed (requiredCharge = distance / MAX_RANGE); remaining charge is preserved for future short hops. Fuel is deducted. The vessel enters the in-transit state (see In-Transit State). The drive is marked as engaged. A SimEvent of kind "arriveZone" is enqueued at currentTime + transitTime.
  5. Travel. The vessel remains in transit for the computed duration (see Transit Time Formula). During this phase, drive wear accumulates each tick. No other resource drain occurs except life support.
  6. Accuracy roll (see Transit Accuracy below). When the arriveZone event fires, the server rolls for landing accuracy. The result determines whether the vessel arrives at the intended zone, an adjacent zone, or a random zone with damage.
  7. Arrive. The vessel exits transit, is placed in the determined arrival zone, and the drive is disengaged. Normal operations resume.

Fuel cost formula

Transit fuel cost is proportional to the Euclidean distance between origin and destination zone coordinates:

    fuelCost = FUEL_COST_PER_UNIT × distance
    FUEL_COST_PER_UNIT = 0.5

    distance = ZoneCoords.distanceTo(origin, destination)
             = sqrt((x&sub2;−x&sub1;)² + (y&sub2;−y&sub1;)² + (z&sub2;−z&sub1;)²)

The fuel check is performed after the engagement roll succeeds. If the captain does not have enough fuel, the drive fires but the transit aborts — proportional charge is consumed (the drive fired at the required level for the distance). This makes pre-flight fuel calculation important. The XO safety layer provides fuel-for-distance advisories to prevent this situation (see NPC Distance Awareness).

Transit time formula

Transit time is proportional to distance:

    transitTime = TIME_PER_UNIT × distance
    TIME_PER_UNIT = 0.35  (seconds per coordinate unit)

Representative transit table

The default Sol sector (5 zones) produces the following transit costs from common routes:

RouteDistanceFuel CostReq. ChargeCharge TimeTransit TimeTotal Cycle
earth-orbit → mars-transit28.914.50.124s10s14s
earth-orbit → mars-orbit53.726.90.217s19s26s
earth-orbit → inner-belt101.150.60.4013s35s48s
earth-orbit → outer-belt182.891.40.7324s64s88s
inner-belt → outer-belt82.041.00.3311s29s40s
mars-transit → mars-orbit25.112.60.103s9s12s

With partial charge engagement and tuned transit speed, short hops are snappy: mars-transit is a 14-second cycle. The outer-belt round trip takes ~176s. With 500 starting fuel and a 20-unit XO reserve, operational fuel is 480 — approximately 5 round trips to outer-belt or 16 to mars-transit before refueling. Leftover charge from a short hop carries over to the next jump, further reducing subsequent wait times.

In-transit state

While in transit, the vessel is in a special state with the following properties:

Transit metadata is stored on the Vessel object: transitDestination (target zone ID), transitArrivalTime (simulation time when the arriveZone event fires). These fields enable client-side progress display (ETA countdown) and server-side validation.

Engagement dice roll

When a captain issues an engageDrive command, the server rolls for engagement success before any transit begins:

    roll = d20() + floor(shipAffinity / 10) + floor(driveIntegrity × 5) + lowChargePenalty
    lowChargePenalty = (driveCharge < 0.5) ? −2 : 0
    success = (roll ≥ ENGAGEMENT_THRESHOLD)
    ENGAGEMENT_THRESHOLD = 8

Low-charge penalty: When engaging the drive below 50% charge (emergency short-range jumps), the engagement roll takes a −2 penalty. This makes partial-charge escape jumps riskier but still possible for skilled captains with healthy drives.

Modifier breakdown:

ConditionshipAffinity moddriveIntegrity modTotal modMin d20 to succeedSuccess rate
Healthy ship, skilled captain (aff=50, int=1.0)+5+5+10≤0 (auto)100%
Healthy ship, unskilled captain (aff=20, int=1.0)+2+5+71100%
Damaged drive, skilled captain (aff=50, int=0.3)+5+1+6295%
Badly damaged (aff=20, int=0.1)+2+0+2675%
Nearly wrecked (aff=10, int=0.05)+1+0+1770%

Design intent: a healthy ship with a competent captain should transit reliably. Engagement failure is a consequence of damage, not routine. Pushing a damaged ship is risky; losing drive integrity through wear or combat makes each subsequent transit less certain.

Failed engagement: The drive does not fire. Drive charge is preserved (the drive failed to activate, not expended and missed). A 15-second cooldown is imposed before the captain can attempt engagement again. The CommandResult includes the roll details so clients can display the failure reason.

Compromised drive: If driveIntegrity ≤ 0 (isCompromised() == true), the engagement automatically fails with no roll. The drive is non-functional until repaired.

Transit accuracy

When the arriveZone event fires at the end of transit, the server rolls for landing accuracy:

    accuracyRoll = d20() + floor(shipAffinity / 10) + longDistancePenalty + lowChargePenalty
    longDistancePenalty = (distance > 100) ? −2 : 0
    lowChargePenalty    = (chargeAtEngagement < 0.5) ? −2 : 0

Low-charge accuracy penalty: When the drive was engaged below 50% charge, accuracy also takes a −2 penalty. Emergency partial-charge jumps are less precise — the captain trades navigational accuracy for speed of escape. This penalty stacks with the long-distance penalty for a maximum of −4.

Roll resultOutcomeEffect
≥ 12Precise arrivalVessel arrives at the intended destination zone.
6 – 11DriftVessel arrives at the nearest zone to the intended destination (by coordinate distance, excluding origin zone). If the intended destination is the nearest zone, this counts as precise arrival.
2 – 5Significant driftVessel arrives at a random zone (uniform selection, excluding origin zone).
Natural 1 (before modifiers)Critical failureVessel arrives at a random zone (excluding origin) and takes 10% hull damage (hullIntegrity −= 0.10) and 20% drive integrity damage (driveIntegrity −= 0.20). The inertial dampener failed during transit; the captain was slammed around the lifepod.

At baseline (shipAffinity 50, modifier +5, distance ≤ 100): rolls of 7+ give precise arrival (70%). At long range (distance > 100, modifier +3): needs 9+ for precise arrival (60%), drift on 3–8 (30%).

A natural 1 on the d20 is always a critical failure regardless of modifiers (5% base rate). This represents the fundamental unpredictability of the Artifact Drive — even the best captains cannot fully control it.

Drift resolution: The “nearest zone” for drift outcomes is computed by coordinate distance from the intended destination, not from the origin. This means a drift on a long haul might still land the captain fairly close to where they wanted to go. The “random zone” for significant drift and critical failure is a uniform random selection from all zones in the sector excluding the origin zone.

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:

Drive wear and integrity

ArtifactDriveSystem carries a driveIntegrity field [0–1] alongside its existing chargeRate and engagement state. Wear is deterministic and tick-driven; stochastic outcomes (engagement success/failure rolls) happen at command time in EngageDriveHandler and at arrival in the arriveZone event dispatcher.

Two sources of wear are applied each tick by SimulationController.updateVessel:

Because the drive now stays engaged for the full transit duration, self-wear is a meaningful mechanic. Representative wear per transit:

RouteTransit TimeSelf-Wear (0.001/s)Transits to 50% integrity
earth → mars-transit10s0.010 (1.0%)~50
earth → inner-belt35s0.035 (3.5%)~14
earth → outer-belt64s0.064 (6.4%)~8
inner-belt → outer-belt29s0.029 (2.9%)~17

A captain making long hauls will need to factor drive maintenance into their planning. Short hops are nearly free on drive integrity; a captain running the inner-belt/outer-belt cargo loop will need drive repair approximately every 6 round trips.

When driveIntegrity reaches 0, isCompromised() returns true. Engagement automatically fails with no roll.

Repair paths: Artifact station docking and crewdroid field repair using spareParts. Neither is implemented; driveIntegrity is in place to receive them.

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.

Vessel gains two transit fields: transitDestination (string, target zone ID; empty when not in transit) and transitArrivalTime (double, simulation time of arrival; 0 when not in transit). A helper isInTransit() returns transitDestination != "". These fields are included in Vessel.toJSON() and persisted in vessel documents.

ZoneCoords (in core.zone) gains a distanceTo(ZoneCoords other) method returning the Euclidean distance. Constants FUEL_COST_PER_UNIT (0.5) and TIME_PER_UNIT (0.35) are defined in core.controller or commands.engageDriveHandler.

The engageDrive event dispatch path in SimulationController is expanded: compute distance, roll engagement, check fuel, deduct fuel, enter transit, enqueue arriveZone event. A new event kind "arriveZone" is dispatched at the scheduled arrival time: roll accuracy, place vessel in determined zone, disengage drive.

Both driveIntegrity (on ArtifactDriveSystem) and integrity (on InertialDampenerSystem) are persisted to MongoDB via the SystemHealthRecord struct added to db.gameStateRepo. GameSnapshot carries a systemHealth array (one record per system-id); snapshot() and restore() in SimulationController read and write it. The systemHealth key is absent in snapshots written before this feature was added; GameSnapshot.fromJSON defaults to an empty array for backward compatibility. The in-memory reset path (captureSystemEvents) also captures and replays driveIntegrity via a "setDriveIntegrity" SystemControlEvent.


Weapon Systems and Combat

WeaponSystem as ISystem

WeaponSystem is a concrete subclass of ISystem, living in core.system alongside EngineSystem, ShieldSystem, ArtifactDriveSystem, and InertialDampenerSystem. Like all systems it is owned by the SystemRegistry inside SimulationController and keyed by a string system ID. The ISystem contract (“what vessel do you belong to?”, “are you compromised?”) is the only interface the combat loop needs; the rest of the field set is WeaponSystem-specific and only touched by the combat subsystem and the two targeting commands.

Standard issue: the Pulse Lance

Each singleship has exactly one weapon mount point. The Artifact issues every new singleship with a Pulse Lance — a directed-energy weapon integrated at fabrication. The Pulse Lance is the sole Artifact weapon template currently in use. The blueprint system supports multiple templates (random selection from the pool of Artifact blueprints weighted by quality tier), but only one template is registered at present; additional Artifact weapon designs are a future expansion hook.

Weapon acquisition and swapping

Human-manufactured weapons are purchasable at market stations. Because each singleship has a single mount point, acquiring a new weapon requires selling or discarding the currently installed weapon and installing the replacement — a swap, not an addition. The transaction happens at a market station and is modeled as a future command kind (swapWeapon): validate that the vessel is docked at a station with a market, verify the captain has sufficient Credits, remove the old weapon instance, deduct Credits, and instantiate the new weapon from its blueprint.

A captain who swaps their Artifact-issued Pulse Lance for a human-manufactured weapon cannot recover it. The Artifact replaces the weapon only when it replaces the entire ship (respawn after destruction). This makes the swap decision strategically significant: human weapons offer variety and repairability at the cost of the Pulse Lance’s reliability and efficiency.

Blueprints — reference data, not simulation state

WeaponBlueprint is a read-only template struct stored in the weaponBlueprints MongoDB collection. The server loads all blueprints at startup into an in-memory BlueprintRegistry; the running simulation never writes back to that collection. A blueprint carries:

Blueprints are managed by db.blueprintRepo.BlueprintRepository : IBlueprintStore (MongoDB backend) and core.blueprint.BlueprintRegistry (in-memory lookup). The repository uses the weaponBlueprints collection with upsert semantics; the registry provides get(id), byCategory(cat), and artifactBlueprintIds() helpers.

Instantiation — what gets rolled, what stays fixed

The factory function instantiateWeapon(blueprint, vesselId, dice) (in core.weapon) is called by buildScenario() when arming a vessel. It is the sole site where randomness is introduced into weapon state. The lifecycle of each field is:

FieldSourcePersisted?Notes
blueprintId, isArtifactWeaponCopied from blueprintNo (structural)Part of the structural skeleton rebuilt by buildScenario() on every cold start.
qualityTierWeighted random draw from blueprint.qualityTiersNo (structural)Determines the statMultiplier applied to all rolled stats.
damage, accuracy, fireCycle, powerDrawRandom in [min, max] × tier multiplierNo (structural)Rolled once at instantiation; fixed for the weapon’s lifetime. Re-rolled on cold start if the dice seed is not deterministic.
malfunctionChance, powerEfficiency, requiresSparePartsCopied from blueprint (penalty fields)No (structural)Constant for the weapon’s class; not modified by quality tier or wear.
weaponIntegrity1.0 at instantiationYes — via SystemHealthRecord in GameSnapshotDegrades each firing cycle. Persisted alongside drive and dampener integrity so crash recovery preserves weapon wear.
targetId"" (idle) at instantiationNoSet/cleared by engageTarget / ceaseTarget commands. Reset to idle on any server restart — a restarting server never resumes mid-engagement.
cycleTimer0.0 (ready) at instantiationNoCountdown to next allowed shot. Lost on restart; weapon fires immediately on its first eligible tick after resumption.
Known gap: The rolled combat stats (damage, accuracy, fireCycle, powerDraw, qualityTier) are part of the structural skeleton and are not in the GameSnapshot. If the server’s Dice instance is not seeded deterministically, a cold start produces different rolled stats than the previous run. Adding a seeded or recorded roll to the snapshot is a deferred milestone.

Combat loop — what happens each tick

Step 4.5 of SimulationController.updateVessel(vesselId, dt), inserted between the per-system applyTick pass and the final ResourceRegistry.setState call, runs the weapon fire cycle:

  1. For each WeaponSystem on the vessel: skip if isCompromised or targetId is empty.
  2. Decrement cycleTimer by dt. Skip if still positive (weapon is still on cooldown).
  3. Compute powerCost = powerDraw × powerEfficiency. Skip if the vessel’s current power is insufficient.
  4. Deduct powerCost from the local ResourceState being accumulated for this tick.
  5. Check for an on-duty crewdroid at the bridge (the ship’s tactical command location). If present, compute roll = d20.roll() + captainTacticalSkill / 10 + (accuracy − 50) / 10. If no crewdroid is on duty at the bridge, compute roll = d20.roll() + (accuracy − 50) / 10 (no skill bonus).
  6. If roll ≥ WEAPON_HIT_THRESHOLD (12), call applyWeaponDamage(targetId, weapon.damage).
  7. Degrade weaponIntegrity by HUMAN_WEAPON_WEAR_PER_SHOT (0.01) per firing cycle regardless of hit or miss. (Artifact weapon wear rates are differentiated via the blueprint’s field values; the 0.01 constant applies to human weapons. Artifact weapons have negligible wear by design.)
  8. Reset cycleTimer = fireCycle.

Because the tick fiber and HTTP handlers share one OS thread under Vibe.d’s cooperative scheduler, no mutex is needed. Resource changes within step 4.5 are written to the resources local copy and committed together with all other per-tick changes in the single setState call at step 5, making each tick’s resource accounting atomic.

Damage model

applyWeaponDamage(targetVesselId, damage) is a private method on SimulationController. The damage value on WeaponSystem is in hull-fraction units [0, 1] (e.g. 0.08 means 8% of full integrity per hit). Damage is applied directly without division. Resolution proceeds in two phases:

Deferred destruction and the tick loop

Vessel destruction cannot happen synchronously inside the foreach (vesselId, _; byVessel) iteration in tick() because removing a vessel from ResourceRegistry during iteration mutates the associative array being iterated. Instead, applyWeaponDamage pushes the ID into a private string[] _destroyedThisTick scratch buffer (with a canFind dedup guard to handle multiple weapons targeting the same vessel in the same tick). After the per-vessel loop completes, tick() iterates _destroyedThisTick and calls destroyVessel(id) for each entry.

destroyVessel(vesselId): creates a Wreck entity from the vessel’s zone, class, hull integrity, and current cargo; adds it to WreckRegistry; clears every other vessel’s WeaponSystem.targetId that references the destroyed vessel; removes the vessel from EntityManager and its resource state from ResourceRegistry.

Targeting commands

Two command kinds manage WeaponSystem.targetId directly without enqueueing SimEvents — targeting is a pure system-state mutation with immediate effect, consistent with the architecture’s pattern of “direct mutation for system state, SimEvents for deferred domain changes”:

KindHandlerEffect
engageTarget EngageTargetHandler (commands/engageTargetHandler.d) Sets wpn.targetId = targetVesselId. Validates: weapon exists and belongs to the declaring vessel, weapon is not compromised, target vessel exists, no self-targeting.
ceaseTarget CeaseTargetHandler (commands/ceaseTargetHandler.d) Clears wpn.targetId = "". Idempotent (already-idle weapon accepted). Reports previousTarget in the response details.

Engine System and Consumption Scaling

The EngineSystem carries a maxPower field (default 1000 for the standard singleship) that acts as a consumption scalar. All engine-related fuel and power drain is multiplied by maxPower / 1000, allowing future ship classes with larger or smaller engines to consume proportionally more or fewer resources without changing the base rate constants.

    fuelUse  = (maxPower / 1000) × baseFuelRate × throttle × dt
    powerUse = (maxPower / 1000) × basePowerRate × throttle × dt

    baseFuelRate  = 1.0  (units per second at full throttle, standard engine)
    basePowerRate = 1.0  (units per second at full throttle, standard engine)

At maxPower = 1000 (standard singleship), the multiplier is 1.0 and behavior is identical to the hardcoded rates used prior to this change. A future heavy cruiser with maxPower = 2000 would consume fuel and power at double the rate; a scout with maxPower = 500 would consume at half the rate.

Implementation note: The existing Vessel.update() hardcodes rates of 1.0. The change is to read maxPower from the EngineSystem in the systems array and apply the multiplier. The ResourceRegistry.consumeForEngines() helper already accepts configurable rate parameters and can be used as an alternative call site.


Power Balance

Power is the critical operational bottleneck for a singleship in combat. It is consumed by engines, shields, shield regeneration, and weapon fire. The balance between starting power, shield maintenance cost, and combat drain rates determines how long a captain can sustain combat operations before being forced to disengage or face systems failure.

Starting power: 350

The standard singleship blueprint provides 350 starting power (increased from 200). This change reflects the design intent that a singleship should be able to sustain a meaningful combat engagement without power being exhausted before tactical decisions can play out.

Shield maintenance: 0.5 power/sec

Shield maintenance rate remains at 0.5 power/sec while raised. This is a significant ongoing cost that forces a real tradeoff between defensive posture and operational endurance. Shields are not free — running them continuously drains operational reserves.

Combat endurance calculations

ScenarioPower drain/secEndurance (350 power)Previous (200 power)
Full throttle + shields (typical combat)1.5233s (3m 53s)133s (2m 13s)
Half throttle + shields (cruising)1.0350s (5m 50s)200s (3m 20s)
Shields only, idle (defensive posture)0.5700s (11m 40s)400s (6m 40s)
Full throttle, no shields (evasion)1.0350s (5m 50s)200s (3m 20s)

The 233-second combat window at full engagement is long enough for multi-round weapon exchanges and tactical maneuvering, while still creating pressure to manage power reserves. A captain who enters combat with depleted power from extended shield use or engine running is at a real disadvantage.

Refueling economics at 350 power

At Earth (cheapest station): full power refill = 350 × 12 CR/unit = 4,200 CR. At outer-belt (most expensive): 350 × 25 = 8,750 CR. The emergency stake (1,000 CR) buys approximately 83 power units at Earth prices — enough to restore meaningful operational capability but not a full tank.

Blueprint change

ShipBlueprint.standardSingleship() in core/artifact.d is updated: resources.power changes from 200.0 to 350.0. The audit document’s refueling costs (Section 4.4) should be recalculated with the new value. Existing vessel documents in MongoDB will retain whatever power level they had at their last save; only newly fabricated ships receive the new default.


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

Station commerce: buying supplies

The buySupplies command lets a captain spend Credits to purchase fuel, power, life support, or spare parts at a market station. The command payload specifies the vessel, station, resource type, and amount. The server validates that the vessel is in a zone with the specified market station, checks the captain’s CR balance against the total cost, deducts Credits, and adds the purchased resource amount to the vessel’s ResourceState.

Per-resource pricing is defined on Station with one price field per purchasable resource: fuelPricePerUnit, powerPricePerUnit, lifeSupportPricePerUnit, sparePartsPricePerUnit. Each has a sensible default so that stations without explicit pricing still function. The existing creditsPerCargoUnit on SellCargoHandler provides the model for this pattern.

Faction-based price variation and dynamic supply/demand pricing are noted as future enhancements. The initial implementation uses flat per-unit prices.

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 Credits to the captain’s Character record after the command is accepted (Option C two-phase pattern). The buySupplies command follows the same two-phase CR-deduction 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 registered in the EntityManager as typed entities, with a dedicated CrewdroidRegistry providing per-ship lookup and group queries (artifact-issued vs. recruited). The existing CrewRegistry and its Actor/CrewMember hierarchy are being converged into the crewdroid model: the activity-state FSM from CrewMember will be incorporated into the unified Crewdroid type so that all shipboard agents share a single entity class with energy, role, EVA capability, and activity tracking.


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 a ship is destroyed, the captain’s lifepod ejects and returns autonomously to the home Artifact for re-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 simulation uses a dual-path persistence model. Two complementary stores capture different aspects of the world:

Sector infrastructure — zones, stations, and wrecks — is persisted in three per-sector MongoDB collections (zones, stations, wrecks), each document carrying a sectorId field for multi-sector filtering. On startup, the server loads sector infrastructure from these collections via ISectorStore and passes the loaded data to buildScenarioFromData(), which constructs the SimulationController with zones registered by named string key via Galaxy.addZone(). If the collections are empty for the configured sector_id, the server auto-seeds the default Sol scenario (two zones, two stations, two wrecks) before proceeding. A GM endpoint (POST /sector/seed) can re-seed at any time.

Characters are persisted independently in the global characters collection (see Persistent Entities).

Snapshot frequency and storage

A GameSnapshot 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.

Vessel documents are written to the vessels collection at lifecycle boundaries only (not on the 5-second tick). Each vessel document carries a lastSaved ISO-8601 timestamp.

Startup sequence

The startup sequence reconstructs the world in layers:

  1. Load sector infrastructure (zones, stations, wrecks) from MongoDB for the configured sector_id via ISectorStore. If no data exists, auto-seed the default Sol scenario (two zones, two stations, two wrecks) via seedDefaultSector(). Pass the loaded data to buildScenarioFromData(), which constructs the SimulationController with zones registered by named string key. No vessels or characters are created. On a cold start with an empty MongoDB, the galaxy contains no ships.
  2. Load all vessel documents from the vessels collection. For each document, reconstruct the in-memory Vessel, its systems, crewdroids, and LocationGraph.
  3. Load all character documents from the characters collection. Bind each character to its assignedVesselId if present.
  4. Attempt to load a GameSnapshot for the configured sector_id. If one exists and its capturedAt timestamp is newer than a vessel document’s lastSaved, overlay the snapshot’s resource/system/wreck state onto the in-memory objects (the snapshot wins for crash recovery). If no snapshot exists, proceed with the state loaded from vessel documents.
  5. Log “resumed with N vessels, M characters” or “cold start — empty galaxy” as appropriate.

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. db.sectorRepo defines ISectorStore and MongoSectorStore for sector-scoped CRUD on the zones, stations, and wrecks collections. SimulationController has 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.


Persistent Entities: Characters and Vessels

Characters and vessels are MongoDB-first entities. They exist as durable documents in their own collections, independent of buildScenario(), and survive server restarts without any snapshot overlay. This is the foundation of the persistent world model: a fresh MongoDB means an empty galaxy with no ships; the first captain to connect gets a ship manufactured by the Artifact.

buildScenario() scope

buildScenario() is world-only. It creates zones, planets, stations, wrecks, and celestial bodies — the structural geography that exists regardless of whether anyone is flying in it. It does not create vessels, crewdroids, or characters. Those are player/NPC-owned data loaded from MongoDB or created through the onboarding flow (see Captain Lifecycle).

MongoDB collections

The persistent world uses four MongoDB collections, three of which are shared across all galaxy server instances:

CollectionScopeKeyed byPurpose
characters Global (cross-server) character-id Captain identity, skills, Credits (CR), assignedVesselId, homeArtifactId, controlMode. Enhanced from the existing collection.
vessels Global (cross-server) vessel-id Full vessel state: zone, ship class, hull integrity, resources, system definitions and health, weapon instance data, crewdroid roster, LocationGraph edges. One document per vessel.
activeSessions Global (cross-server) _id = username Currently logged-in accounts. Fields: serverId, loginTime, lastHeartbeat, vesselId. Used for single-logon enforcement and disconnect detection.
gameState Per-sector sector-id Monolithic snapshot: simulation clock, pending SimEvent queue, per-vessel resource/system overlays. Crash-recovery safety net (see Persistence and Recovery).
zones Per-sector zoneId + sectorId Zone definitions: coordinates, planet list, display name. Loaded at startup; registered by named string key via Galaxy.addZone().
stations Per-sector stationId + sectorId Station definitions: zone placement, market flag, Artifact affiliation, per-resource pricing.
wrecks Per-sector wreckId + sectorId Wreck structural definitions: zone placement, ship class, true cargo units. Dynamic state (loot flags) is in the gameState snapshot.

Vessel document schema

Each vessel document captures everything needed to reconstruct a fully functional vessel in memory without buildScenario():

Conflict resolution: snapshot vs. vessel document

Because the GameSnapshot is written every 5 seconds and vessel documents are written only at lifecycle boundaries, a crash may leave the snapshot more recent than the vessel documents. On startup, the server applies a simple conflict rule: load vessel documents first (they are the canonical store), then overlay the GameSnapshot if its capturedAt timestamp is newer than a vessel document’s lastSaved. This ensures that crash recovery never loses more than 5 seconds of progress while keeping vessel documents as the long-term source of truth.

Artifact blueprint system

The Artifact station fabricates new singleships from a ShipBlueprint (already stubbed in core/artifact.d). The standard singleship blueprint defines:

The blueprint is used by the onboarding endpoint to fabricate a new vessel when a captain has no assigned ship. Additional blueprints (larger ships, specialised variants) can be registered in the Artifact’s BlueprintRegistry as a future expansion.

Codebase implications

core/composition.d: buildScenario() is refactored to world-only. Vessel, crewdroid, and system creation code moves to a new fabricateVessel(blueprint, captainId, zoneId) function used by the onboarding endpoint. db/gameStateRepo.d: gains VesselDocument struct with toJSON()/fromJSON(), IVesselStore interface, and MongoVesselStore implementation. characters/characterLoad.d: the existing ICharacterStore is enhanced with lookup-by-username and vessel-binding fields.


Captain Lifecycle: Onboarding, Session, and Disconnect

A captain’s relationship with the galaxy server follows a defined lifecycle: authenticate, onboard (create or reconnect), operate, and disconnect (graceful or detected). This section specifies each phase and the mechanisms that enforce session integrity across the fleet.

Single active logon

Only one active session per username is permitted at a time, across all galaxy server instances. This prevents a captain from being “in two places at once” and avoids the race conditions that would arise from duplicate command streams for the same vessel.

Onboarding: POST /onboard

The onboarding endpoint is the single entry point for a captain joining the simulation. It handles both first-time captains and returning captains.

ScenarioServer action
No Character document exists for this user Create a new Character with default stats, starting Credits (CR), homeArtifactId = "artifact-earth-orbit", and controlMode derived from session role. Fabricate a new vessel from the Artifact’s standard singleship ShipBlueprint, placed in the zone of the captain’s home Artifact station (Earth Orbit). Persist both documents. Return isNewCaptain: true.
Character exists but has no assignedVesselId (post-destruction, awaiting respawn) Fabricate a new vessel from the blueprint. Update the character’s assignedVesselId. Persist. Return isNewCaptain: false.
Character exists with a valid assignedVesselId Load the existing vessel. If it was in stasis, deactivate the stasis shield and apply the reconnect recovery bonus. Update controlMode. Return isNewCaptain: false.

Response payload: { "characterId": "...", "vesselId": "...", "zoneId": "...", "isNewCaptain": true|false }.

All clients (PC and NPC) call POST /onboard after authentication. The endpoint is idempotent for returning captains.

Disconnect detection and the stasis shield

When a captain disconnects — whether by closing the client, losing network, or crashing — the server activates a stasis shield around their vessel. This is an Artifact-origin protective mechanism: the lifepod detects that the captain has gone dormant and the ship’s Artifact systems autonomously project a defensive field that suspends all operations.

Detection

The server tracks lastActivity per authenticated session. Every API call refreshes this timestamp. A background check, piggybacked on the tick loop (every ~5 seconds), scans for sessions whose lastActivity exceeds the configurable disconnect_timeout_s (default: 60 seconds). Under future WebSocket transport, a closed connection triggers immediate disconnect handling.

Stasis effects

Tactical implications

The stasis shield offers complete protection but no tactical awareness. A captain who disconnects in the open — in a contested zone, near a station under dispute, or in proximity to hostile vessels — will return to find the universe moved on around them. Enemies can position themselves around a stasis-shielded vessel, waiting for the captain to reconnect and the shield to drop. Wise captains who anticipate going dormant will first move to a safe location: deep in friendly territory, docked at a station, or in an uncontested corner of an empty zone. Going into stasis without taking precautions is a calculated risk. The Artifact protects the ship; it does not protect the captain’s position.

Duration

There is no time limit on stasis. A vessel can remain in stasis indefinitely until its captain reconnects. The persistent world holds a ship’s place regardless of how long its captain is away.

Reconnect and recovery

When a captain reconnects (authenticates and calls POST /onboard), the stasis shield drops and normal operations resume. The Artifact’s restorative properties provide a reconnect recovery bonus: a modest boost to hull integrity and resource levels (configurable; approximately +5% hull, +10 fuel, +10 power, +10 life support), representing the beneficial effects of the stasis field’s dormancy period. controlMode is restored to the appropriate active mode ("humanGui" or "aiAgent").

HTTP route access control

Every HTTP route has an explicit access level. Routes not listed as public require an authenticated session cookie; routes marked GM-only additionally require simRole == "gm".

MethodPathAccessDescription
GET*PublicServe static files from public/ with CORS headers.
POST/loginPublicAuthenticate; establishes server-side session cookie. Enforces single active logon.
POST/onboardAuthenticatedCaptain onboarding: create or reconnect character + vessel.
GET/characters/meAuthenticatedLoad the authenticated captain’s own Character document.
GET/charactersGM onlyLoad all characters from MongoDB.
POST/charactersGM onlyUpsert a character to MongoDB (full admin edit).
GET/game/stateAuthenticatedFull galaxy + vessel snapshot as JSON. Filtered-by-vessel endpoint is a Chunk 15+ concern.
GET/game/state/sizeAuthenticatedCharacter count of the JSON state (diagnostic).
POST/commands/oneAuthenticatedSubmit a single CommandRequest for dispatch.
POST/crew/spawnGM onlySpawn a new crewdroid onto a vessel (admin override).
POST/simulation/pauseGM onlyPause the simulation tick loop.
POST/simulation/resumeGM onlyResume the simulation tick loop.
POST/simulation/resetGM onlyReset simulation to baseline snapshot.
POST/sector/seedGM onlyRe-seed sector infrastructure (zones, stations, wrecks) from default data. Overwrites existing sector collections for the configured sector_id.

Implementation: SectorServer.checkGmAuth() (existing) handles the GM check. A new checkAuth() wrapper is applied to all authenticated routes. The activeSessions heartbeat update is applied as middleware on every authenticated API call.


Transport Layer

See also: Transport and Server Communication in the Client Architecture document for the client-side transport abstraction and server API evolution plan.

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: moveCrewdroid (captain directive to reposition a crewdroid within the vessel — crewdroids are autonomous enough to position themselves for routine tasks, but the captain can explicitly order a droid to a specific location for tactical reasons such as damage control, EVA staging, or weapons station manning), engageDrive (Artifact Drive engagement), salvageCargo (EVA cargo extraction from a wreck), sellCargo (sell all held cargo at a market station), engageTarget (arm a weapon system against a target vessel), ceaseTarget (disarm a weapon system). Planned future kinds include: setThrottle, dockAtArtifact, extractResource, buySupplies, swapWeapon, 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. The NPC and PC client architectures are detailed in Client Architecture.

NPC distance-aware destination selection

With distance-based transit, NPC pickDestination() must incorporate transit cost into destination selection. The previous approach — selecting destinations based solely on expected profit and safety — no longer works because a high-value wreck in the outer belt is not worth pursuing if the captain cannot afford the fuel or the transit time makes the effective profit rate low.

Cost-benefit scoring

Each candidate destination is scored by expected profit per unit time:

    score = expectedProfit / (chargeTime + transitTime + operationTime)

    chargeTime   = 33   (flat, seconds at 0.03/s)
    transitTime  = distance × TIME_PER_UNIT  (0.35 s per unit)
    operationTime = 30  (estimated salvage/sell cycle, seconds)
    expectedProfit = estimated cargo value at destination (from zone summaries)

The NPC selects the destination with the highest score, subject to fuel feasibility.

Fuel feasibility check

Before considering a destination, the NPC checks:

    fuelCost = FUEL_COST_PER_UNIT × distance
    available = currentFuel − FUEL_LOW_THRESHOLD  (20.0)
    feasible = (fuelCost ≤ available)

Destinations that cannot be reached with available fuel are excluded entirely. This prevents NPCs from stranding themselves in remote zones.

XO fuel-for-distance advisory

The XO safety layer (clientLib/xo/safety.d) provides a distance-aware fuel advisory. Instead of the simple FUEL_LOW_THRESHOLD check, the XO computes the fuel cost to return to the nearest Artifact-affiliated station and warns or rejects navigation goals when remaining fuel would be insufficient for the return trip. This is a soft advisory (the captain can override it), distinct from the server-side hard fuel check that prevents engagement when fuel is strictly insufficient.

Economic loop timing comparison

The distance-based system significantly changes NPC economic loop timing. Representative full trade cycles (salvage at wreck zone, sell at market, return to refuel):

LoopOld timingNew timingFuel used
earth → outer-belt → inner-belt → earth~330s~696s183
earth → mars-transit → earth~230s~288s29
inner-belt → outer-belt → inner-belt~230s~394s82

Short loops near Earth remain quick and cheap. Long-haul outer-belt runs take roughly double the time and consume 36% of starting fuel per cycle. The scoring formula naturally steers NPCs toward nearby profitable destinations when fuel is limited, and toward high-value distant runs when they have a full tank.


Ship-to-Ship Communication

Status: deferred. The initial NPC client will operate on game state alone, without communication capabilities.

When implemented, ship-to-ship communication will use a controlled vocabulary / signal system rather than free-form text. The vocabulary includes structured signal types: hail, threat, trade request, distress, surrender, and similar predefined intents. This approach keeps the protocol machine-readable and ensures NPC captains can participate in communication on equal footing with PC captains.

The final NPC client should have full comms capabilities so that it behaves identically to a PC client from the server’s perspective. The communication system will be modeled as a command kind (e.g. sendSignal) with a payload specifying the signal type, target vessel, and any structured parameters. Received signals will appear in the game state snapshot for the recipient vessel.


Sensor Visibility and Information Limits

Status: implemented (zone-scoped, Phase 1). The filtered state endpoint GET /game/state?vessel={vesselId} returns zone-scoped data. The unfiltered endpoint (no query parameter) remains available for GM tools.

Zone-scoped visibility model

A captain sees all entities in their current zone at full accuracy: vessels, stations, wrecks, and planets. Nothing outside the current zone is included in the detailed data. This is the fundamental fog-of-war boundary.

Zone summaries (long-range intelligence)

For all zones other than the captain’s current zone, the server includes a lightweight zoneSummaries array. Each summary contains:

Zone metadata (ID, coordinates, planets) is always included in full for all zones, so the captain always knows where they can navigate.

Sensor noise (tacticalSkill)

Zone summary data is subject to sensor noise, giving Vessel.tacticalSkill its first gameplay purpose. The noise formula:

    noiseFactor = baseFuzz * (1.0 - tacticalSkill / 100.0 * 0.7)
    baseFuzz    = 0.15  (±15%)

    tacticalSkill   0 → ±15.0% noise
    tacticalSkill  50 → ±9.75% noise
    tacticalSkill 100 → ±4.5%  noise

Station prices are multiplied by 1.0 + uniform(-noiseFactor, +noiseFactor). Wreck counts are offset by round(uniform(-1, +1) * noiseFactor / baseFuzz), clamped to ≥ 0. Noise is re-rolled on every state request — the captain receives a fresh “sensor reading” each poll.

Uses the existing Dice module for server-authoritative randomness.

In-zone vessel sensor accuracy (future)

A future phase will add noise to observed data for other vessels within the captain’s zone — resource levels reported with ±10–20% error, hull integrity slightly off, possible missed detections for damaged sensor systems. This will further differentiate fully-operational ships from damaged ones and give tacticalSkill additional tactical depth.

GM and admin clients

When the ?vessel= query parameter is absent, GET /game/state returns the full unfiltered snapshot with no zone summaries. This preserves backward compatibility for galaxyManager and any future GM dashboards.

Filtered response schema

    {
      "galaxy":        { "zones": [...] },   // all zones (id, coords, planets)
      "vessels":       [...],                 // only vessels in captain's zone
      "stations":      [...],                 // only stations in captain's zone
      "wrecks":        [...],                 // only wrecks in captain's zone
      "zoneSummaries": [                      // all OTHER zones
        {
          "zoneId":              "mars-orbit",
          "stations":            [ { "id", "displayName", "hasMarket",
                                     "creditsPerCargoUnit", ... } ],
          "unlootedWreckCount":  2
        }
      ],
      "time":   123.4,
      "paused": false
    }

Division of Work (Target)

For a more detailed breakdown across the three client layers (Decision, AI/XO, Operations), see Division of Work Revisited in the Client Architecture document.

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
Weapon systemcore.system.WeaponSystemExists (combat stats, integrity, targetId, cycleTimer)
Weapon blueprints (reference data)core.blueprint.WeaponBlueprint, BlueprintRegistryExists (loaded from MongoDB at startup; read-only during simulation)
Weapon blueprint persistencedb.blueprintRepo.BlueprintRepository, IBlueprintStoreExists (weaponBlueprints MongoDB collection; upsert semantics)
Weapon instantiation factorycore.weapon.instantiateWeaponExists (rolls stats from blueprint ranges and quality tier)
Combat loop (per-vessel, per-tick)SimulationController.updateVessel step 4.5; applyWeaponDamage; destroyVesselExists (d20 to-hit, shield absorption, hull damage, deferred destroy)
Targeting commandscommands.engageTargetHandler.EngageTargetHandler, commands.ceaseTargetHandler.CeaseTargetHandlerExists (arm / disarm a weapon; no SimEvent queue)
Artifact Drive systemcore.system.ArtifactDriveSystemExists (includes driveIntegrity, wearRate, applyTick self-wear, applyExternalWear)
Inertial Dampener systemcore.system.InertialDampenerSystemExists
System health persistencedb.gameStateRepo.SystemHealthRecord, GameSnapshot.systemHealthExists (drive integrity + dampener integrity persisted to MongoDB per-sector snapshot)
Locations and movement graphcore.location.LocationGraphExists
Crewdroid activity FSMcore.crew.ActorActivity, core.crew.nextActivityExists (to be merged into unified crewdroid type)
Crewdroids and registrycharacters.crewdroid.Crewdroid, CrewdroidRegistryExists (convergence with CrewMember FSM 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
Sector infrastructure persistencedb.sectorRepo.ISectorStore, MongoSectorStore; core.composition.seedDefaultSector, buildScenarioFromDataExists (zones, stations, wrecks collections; auto-seed on empty; POST /sector/seed)
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