Ship — Schema¶
Status: 🚧 Partial — Ship core (cargo/combat/quantum/NPC hulls) and ShipSpecification are committed; the entire ship-registry feature set (registered_owner_id, current_pilot_id, stolen_status, hatch_pin … · ⚠︎ contains code↔spec divergence (impl audit 2026-06-16)
A player-owned vessel; tracks position, cargo, combat readiness, and special equipment. Companion docs: ./ships.md's gameplay spec lives at ../FEATURES/gameplay/ships.md, and the registry workflow lives at ../SYSTEMS/ship-registry.md.
Schema status¶
Per ADR-0066 D-V1, schema-level implementation status is consolidated here rather than scattered across field descriptions. The descriptions below describe the target schema (per the doc philosophy: DATA_MODELS describes the target).
Design-only columns — present in the spec, not yet committed to migrations / code at the time of writing:
Ship:registration_number,registered_owner_id,current_pilot_id,stolen_status,stolen_reported_at,hatch_pin_code,salvage_break_in_progress_by_id,salvage_break_started_at,is_abandoned,abandoned_at,for_sale_price,for_sale_listed_by_id,destruction_cause,hangar,tow_state.ShipSpecification:insurableflag.
All other columns on this page are committed (per the gameserver schema as of 2026-Q2).
Ship¶
Source: services/gameserver/src/models/ship.py
Purpose: A player-owned vessel; tracks position, cargo, combat readiness, and special equipment.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| name | String(100) | not null | |
| type | Enum ship_type |
not null | ESCAPE_POD, LIGHT_FREIGHTER, CARGO_HAULER, FAST_COURIER, SCOUT, COLONY, DEFENDER, CARRIER, WARP_JUMPER, NPC_MARSHAL_INTERDICTOR (target), NPC_SENTINEL_INTERDICTOR (target). The two NPC_* variants are filtered from any player-facing API that lists ship types and rejected from any registry-event that would transfer the hull to a player (ERR_NPC_ONLY_HULL); ShipSpecification.is_npc_only = true for both. See ../FEATURES/gameplay/police-forces.md. |
| registration_number | String(15) | unique, not null | Player-visible stable identifier of the form REG-XXXX-YYYY (4-char alphanumeric block + 4-digit registration year). Immutable for the lifetime of the hull, even across ownership transfers. See ../SYSTEMS/ship-registry.md. |
| registered_owner_id | UUID FK players.id | not null, CASCADE | Legal owner of record (formerly owner_id). Updates only via successful registry transfer. |
| current_pilot_id | UUID FK players.id | nullable, SET NULL | Whoever's currently flying the ship; can be NULL (Drifting), the registered owner (Owner aboard), or any other player (Borrowed). |
| stolen_status | Boolean | default false | True after the registered owner files a stolen report. Whoever's piloting becomes Wanted (see ../FEATURES/gameplay/ranking.md#wanted-status). |
| stolen_reported_at | DateTime | nullable | Timestamp of the stolen report; cleared on retraction or successful transfer. |
| hatch_pin_code | String(8) | nullable | 4–8 alphanumeric pin gating the boarding hatch. Random at registration. Cleared by salvage break (NULL = unlocked). Owner or current pilot can change it; owner can request a port-mediated reset (1h delay). See ../SYSTEMS/ship-registry.md#hatch-pin-lock. |
| salvage_break_in_progress_by_id | UUID FK players.id | nullable | When non-NULL, a salvage break is underway on this Drifting ship by the named player. The break aborts on combat involving them or on them leaving the sector. |
| salvage_break_started_at | DateTime | nullable | Start timestamp for the salvage break; duration scales by ship class (Scout 1h / Cargo Hauler 4h / Carrier 12h). On completion, hatch_pin_code is cleared. |
| is_abandoned | Boolean | default false | True after the registered owner explicitly abandons the ship at a port. Free-claim by first taker; auto-archives after 7 days unclaimed. |
| abandoned_at | DateTime | nullable | Timestamp of abandonment; auto-archive triggers at abandoned_at + 7 days. |
| for_sale_price | Integer | nullable | When non-NULL, the ship is listed for peer-to-peer sale at the host port. Buyer at the same port can pay this price for an instant registry transfer. Seller can de-list (clear) at any time. |
| for_sale_listed_by_id | UUID FK players.id | nullable | Player who listed the sale (always the registered owner at listing time). |
| sector_id | Integer | not null | matches Sector.sector_id (integer), not the UUID |
| base_speed, current_speed | Float | not null | |
| turn_cost | Integer | not null | |
| warp_capable | Boolean | default false | |
| is_active | Boolean | default true | |
| status | Enum ship_status |
default DOCKED | DOCKED/IN_SPACE/IN_COMBAT/DESTROYED/MAINTENANCE/MINING/HARMONIZING (MINING reserved for asteroid harvesting and nebula Shard harvesting per ../FEATURES/economy/mining.md and ../FEATURES/galaxy/quantum-resources.md; HARMONIZING is the Warp Jumper's invulnerable-but-intact state during the 1-hour Phase 3 harmonization window — the hull stays alive and the destruction commit fires at timer completion, not at call time, per ../FEATURES/galaxy/warp-gates.md) |
| destruction_cause | Enum destruction_cause |
nullable | Set when status = DESTROYED. Values: COMBAT / HAZARD / SELF_DESTRUCT / ABANDONMENT_EXPIRED / WARP_GATE_ANCHOR (the Warp Jumper-consumed-at-Phase-3 case per ADR-0029). Drives Cargo-Wreck and insurance-payout branching at the destruction handler. |
| maintenance | JSONB | not null | |
| cargo | JSONB | not null | commodity → quantity |
| has_cloaking | Boolean | default false | |
| genesis_devices, max_genesis_devices | Integer | defaults 0 | |
| mines, max_mines | Integer | defaults 0 | |
| has_automated_maintenance | Boolean | default false | |
| combat | JSONB | not null | shields/hull/etc. |
| attack_turn_cost | Integer | nullable | per-ship combat initiation cost (added in migration d2e3f4a5b6c7) |
| upgrades | JSONB | default [] | |
| equipment_slots | JSONB | default {} | added in migration d2e3f4a5b6c7. Holds slot-keyed equipment installs including quantum_harvester, tractor_beam, slipdrive, and sensor (the upgrade ladder L0–L3 referenced by Quantum Jump scan accuracy — see ADR-0030). The Warp Jumper's Quantum Charge consumable lives here under a special_equipment slot key per ADR-0030 (the per-jump Phase 2 commit cost; one charge consumed per Quantum Jump regardless of band). The sensor upgrade affects the Phase 1 long-range scan's misread rate: L0 = 15% misread, L1 = 10%, L2 = 5%, L3 = 0% (per ADR-0030 §Disclosure model). |
| hangar | JSONB | nullable | only populated on capital-size ships (Carrier). Carrier ship-hangar state — see Carrier ship-hangar below. NULL on every other ship. |
| tow_state | JSONB | nullable | only populated when this ship is actively towing another via Tractor Beam. See Ship tow state below. NULL otherwise. Mutually exclusive with being a docked passenger inside a Carrier hangar. |
| insurance | JSONB | nullable | |
| is_destroyed, is_flagship | Boolean | defaults false | |
| purchase_value, current_value | Integer | not null |
Carrier ship-hangar¶
Ship.hangar JSONB shape on capital-size ships (only Carrier qualifies at launch). The hangar holds whole player ships in transit, separate from the Carrier's drone bay. Capacity is 8 size-units; per-ship size cost is 1 / 2 / 4 / 8 for tiny / small / medium / large.
{
"capacity_units": 8,
"docked": [
{
"ship_id": "uuid",
"owner_id": "uuid",
"size": "tiny | small | medium | large",
"size_units": 1,
"docked_at": "iso8601"
}
]
}
docked is an append-only array per dock event; entries are removed on undock or jettisoned-on-Carrier-destruction. Sum of docked[*].size_units ≤ capacity_units. Validated server-side before each dock request resolves; the consent flow on the docking pilot side is an application-layer guard, not a schema constraint.
See ../FEATURES/gameplay/ships.md#carrier-hangar for the gameplay spec (dock/undock turn costs, combat behavior, jettison-on-destruction).
Ship tow state¶
Ship.tow_state JSONB on the hauler (the ship doing the towing) when a Tractor Beam tow operation is active. NULL otherwise.
{
"towed_ship_id": "uuid",
"towed_owner_id": "uuid",
"towed_size": "tiny | small | medium | large",
"surcharge_per_move": 1,
"locked_at": "iso8601",
"lock_sector_id": "integer"
}
surcharge_per_move is computed at lock-on from towed_size (1 / 2 / 4 / 8 → +1 / +2 / +3 / +5 turns) and cached on the row so the movement service doesn't re-traverse ShipSpecification on every move. lock_sector_id records where the lock-on happened, for audit and tow-distance metrics.
A ship with tow_state != NULL cannot itself be towed (no nesting), cannot dock into a Carrier hangar, and cannot fire its Tractor Beam in weapon mode (mutual exclusion). The towed ship's row is unmodified — its current_pilot_id stays set, but movement_service rejects independent move attempts while it's towed.
See ../FEATURES/gameplay/ships.md#tractor-beam-tow-operations for the gameplay spec (consent flow, travel restrictions, combat behavior, detach paths).
Relationships:
- registered_owner → Player (FK registered_owner_id); the legal owner of record.
- current_pilot → Player (FK current_pilot_id, nullable); whoever's flying right now.
- flagship_of back-ref via Player.current_ship_id (post-update cycle).
- sector → Sector joined on integer sector_id.
- genesis_device_objects → GenesisDevice (1:many).
- fleet_membership → FleetMember (1:1).
- registry_history → ShipRegistry (1:many) — append-only audit trail of ownership changes.
ShipRegistry¶
Append-mostly history of every ship's registration record. Persists even after the ship is destroyed.
Source: services/gameserver/src/models/ship_registry.py (target — does not yet exist).
Purpose: Public ship-ownership ledger. Every ship has at least one row (its initial registration) and gains a row each time ownership changes or the stolen-status flips.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| ship_id | UUID FK ships.id | not null | the live ship (or DESTROYED ship row, retained for history) |
| registration_number | String(15) | not null, indexed | matches Ship.registration_number; redundant for fast lookup |
| event_type | Enum registry_event |
not null | INITIAL_REGISTRATION, OWNERSHIP_TRANSFER, STOLEN_REPORTED, STOLEN_RETRACTED, IMPOUNDED, ARCHIVED |
| original_owner_id | UUID FK players.id | not null | first-ever registered owner; immutable across all rows for a given ship |
| previous_owner_id | UUID FK players.id | nullable | for transfer events; null for initial registration |
| new_owner_id | UUID FK players.id | nullable | for transfer events; null for stolen-report / archive events |
| acting_party_id | UUID FK players.id | nullable | who triggered the event (e.g., the claimant in a transfer; the owner who reported stolen) |
| transfer_fee_paid | Integer | nullable | credits paid in a transfer event |
| port_id | UUID FK stations.id | nullable | port where the event was registered |
| created_at | DateTime | not null | event timestamp |
| metadata | JSONB | default {} | event-specific extra data (dispute outcome, impound reason, etc.) |
Relationships:
- ship → Ship (FK).
- original_owner, previous_owner, new_owner, acting_party → Player (FKs).
- port → Station (FK).
Lifecycle: rows are inserted, never updated or deleted. The full history of a ship's ownership is queryable by ship_id ordered by created_at. After the ship is destroyed, an ARCHIVED row closes out the registry; Ship.status = DESTROYED but the registry row remains for investigations / lookup.
Indexes: (ship_id, created_at) for chronological lookups; (registration_number) for player-facing registration lookup; (original_owner_id) for "ships I've ever owned" queries.
ShipSpecification¶
Source: services/gameserver/src/models/ship.py (same file)
Purpose: Static balance data per ShipType — base cost, hull, shield, cargo, drone, evasion, attack/defense ratings, etc. One row per ship type.
Key fields: type (unique), base_cost, speed, turn_cost, max_cargo, max_colonists, max_drones, max_shields, shield_recharge_rate, hull_points, evasion, genesis_compatible, max_genesis_devices, warp_compatible, warp_creation_capable, quantum_jump_capable, scanner_range, attack_rating, defense_rating, attack_turn_cost, maintenance_rate, construction_time, fuel_efficiency, max_upgrade_levels (JSONB), special_abilities (JSONB), acquisition_methods (JSONB), faction_requirements (JSONB), insurable (Boolean default true — false on Warp Jumper and Escape Pod per ADR-0029; insurance UI hides tier selector when false).
size enum: tiny / small / medium / large / capital. Set per ShipType from the canonical roster (see ../FEATURES/gameplay/ships.md#ship-size-axis). Drives Carrier hangar fit checks and Tractor Beam tow surcharge. Belongs on the spec, not the per-hull Ship row, because every hull of a given type shares the same size.
scanner_range is the integer adjacent-sector visibility radius (Scout 5, Warp Jumper 8 highest, etc. — see ../FEATURES/gameplay/ship-roster.md). Independent from the Warp Jumper's long-range quantum scan (which projects a fuzzy resonance reading 5–10 sectors out along a chosen bearing — see ADR-0031). The two abilities are distinct: scanner_range governs adjacent-sector content visibility on every ship; quantum scan is a Warp-Jumper-only multi-sector probe.