Skip to content

Pirate Holdings — Schema

Status: 🚧 PartialPirateHolding ships as a FOUNDATION slice: the population-score / target / cap / eligible-sector math the pirate ecosystem tick reads and writes (models/pirate_holding.py, migration e8b619503a44). Raid/capture fields split two ways: (a) dormant kernelcombat_lock_held_by, combat_lock_team_snapshot, owner_team_id, captured_at, and formation_id (doc design name was special_formation_id) are committed columns plus wired service helpers (acquire_combat_lock / can_engage / release_combat_lock / capture_holding in pirate_ecosystem_service.py); zero live callers yet, awaiting a raid-initiation route; (b) still absentoutlaw_base_id, interior_sector_ids, parent_holding_id, composition_profile, name, combat_lock_held_until, last_visit_at, last_pirate_recovery_at, and growth_eligible_at are not on the model. (re-verified 2026-08-21 vs Sectorwars2102 HEAD 46bce720)

Canonical decision in ../ADR/0047-pirate-holdings.md. Player-facing reference in ../FEATURES/galaxy/pirate-holdings.md. Raid runtime in ../SYSTEMS/pirate-holding-raid.md.

A PirateHolding is the central row representing a pre-seeded hostile site in the universe — a Camp, Outpost, or Stronghold. Each holding aggregates its OutlawBase lodging anchor, optional SpecialFormation topology backing, named pirate NPC roster, and the planets/stations/sector defenses that compose its presence. The holding is also where ownership flips on player capture (atomically, with cross-table updates per the canonical capture transaction in pirate-holding-raid.md).

PirateHolding table

Source: services/gameserver/src/models/pirate_holding.py (FOUNDATION + dormant raid/capture kernel committed; see status banner for which columns are live vs still absent).

Committed columns (FOUNDATION + dormant raid kernel)

Column Type Constraints Notes
id UUID PK
region_id UUID FK regions.id, not null, indexed, CASCADE Holdings are region-scoped.
sector_id Integer not null, indexed Code column for canon's anchor_sector_id — stores global sectors.sector_id (mirrors Station.sector_id), not a UUID FK to sectors.id. Only the anchor sector is modeled; multi-sector interior_sector_ids is deferred.
tier Enum (CAMP, OUTPOST, STRONGHOLD) not null Code uses UPPERCASE name==value; canon vocabulary is lowercase camp/outpost/stronghold.
owner_player_id UUID FK players.id, nullable, indexed Set on capture; null while pirate-controlled.
current_strength Float not null, default 1.0, CHECK 0.0–1.0 Holding-level strength for ecosystem math.
last_damage_at DateTime nullable Last time the holding took player damage.
formation_id UUID FK special_formations.id, nullable, indexed Code column for canon's special_formation_id. Required by Stronghold CHECK (R-F1).
combat_lock_held_by UUID FK players.id, nullable Dormant kernel — concurrent-attacker lock holder.
combat_lock_team_snapshot UUID[] nullable Dormant kernel — frozen team-mate set at lock acquisition (ADR-0060 G-F2).
owner_team_id UUID FK teams.id, nullable, indexed Dormant kernel — set on team capture.
captured_at DateTime nullable Dormant kernel — first capture timestamp.
evolution_clock_started_at DateTime nullable Dormant kernel — G-I1 evolution-clock reset threshold.
created_at DateTime server default now
updated_at DateTime server default now, auto-update on write

Committed indexes/constraints (on tip): (region_id, owner_player_id) composite index; CHECK current_strength in [0,1]; CHECK Stronghold requires formation_id IS NOT NULL (R-F1).

Design-only columns (ADR-0047 target, not yet migrated)

The rows below describe the launch target from ADR-0047. They are not present on pirate_holding.py as of 46bce720:

Column Type Constraints Notes
name String(120) not null, UNIQUE per (region_id, name) AI-generated per ADR-0044.
composition_profile Enum not null One of the thirteen profiles in the composition table below; rolled at worldgen.
outlaw_base_id UUID FK outlaw_bases.id, not null Lodging anchor — blocked on OutlawBase table wiring (lodging slice).
interior_sector_ids UUID[] not null, default {} Multi-sector defense perimeter; deferred with raid mechanics.
parent_holding_id UUID FK pirate_holdings.id (self-reference), nullable Daughter-holding lineage from ecosystem growth tick.
last_visit_at DateTime nullable Owner visit tracking for abandonment cascade.
last_pirate_recovery_at DateTime nullable Recovery service tick timestamp while pirate-controlled.
combat_lock_held_until DateTime nullable 15-minute auto-expiry for Outpost/Stronghold concurrent-attacker lock.
growth_eligible_at DateTime not null, default created_at + 7 days Earliest tier-promotion date for evolution service.

ADR naming cross-ref: canon's anchor_sector_id → committed sector_id (global integer); canon's special_formation_id → committed formation_id.

Indexes (target schema — not all present on FOUNDATION slice): - (region_id, tier) — "all holdings of tier X in region Y." - (region_id, owner_player_id) — "all pirate-controlled holdings in region Y" (where owner_player_id IS NULL). - (owner_player_id) — "what does this player own." - (owner_team_id) — "what does this team own." - (special_formation_id) — reverse lookup from formation (code index column: formation_id). - (anchor_sector_id) — sector-side lookup ("is this sector a holding's anchor?"). - GIN on interior_sector_ids — "is this sector inside any holding's interior?" - (combat_lock_held_until) — for periodic lock-expiry sweeps. - (parent_holding_id) — "what daughters has this holding spawned?" (ecosystem analytics). - (growth_eligible_at) — used by the weekly evolution tick to filter eligible candidates.

Constraints: - CHECK: owner_player_id IS NULL OR owner_team_id IS NULL (mutually exclusive). - CHECK: tier != 'stronghold' OR special_formation_id IS NOT NULL — Strongholds are always formation-anchored. Code column in the CHECK is formation_id. Per ADR-0060 R-F1 this is enforced at the DB layer (previously stated only as design intent in ADR-0048). Both tier-promotion paths (capture-evolution and worldgen pre-seed) set formation_id in the same statement; the constraint is the canonical defense. - UNIQUE on (region_id, name) — names are unique within a region per ADR-0044.

Composition profile reference

Each profile is a worldgen-time roll (per the tier composition table in ADR-0047). The profile dictates which related entities exist for this holding:

Profile Has station? Has planet? Open-sector defenses? Formation-anchored? Tier
hideout No No Yes (drones + mines) Optional Camp
trading_post 1 (Class 2–3) No Light Optional Camp
haven_planet No 1 (L1–L2 citadel) Light Optional Camp
combined_small 1 1 (L1) Modest Optional Camp
roving_fleet No No No (just NPC ships) No Camp
fortified_empty No No Heavy (multi-sector mines + drones + sometimes parked ships) Optional Camp
fortress_planet 50% chance, small 1 (L3 + orbital platform) Heavy Typical Outpost
pirate_dock_cluster 1–2 (Class 3–4) No Heavy Typical Outpost
mixed_outpost 1 1 (L2–L3) Modest Yes (Bubble or Tunnel) Outpost
defended_formation No No Heavy Required (Bubble interior empty by design) Outpost
fortress_complex 2 (Class 3–4) 2 (L4 + orbital) Heavy Required (Bubble) Stronghold
citadel_state 1 1 (L5 + orbital + rail guns) Heavy Required (Bubble or Dead-End Bubble) Stronghold
trade_nexus 3 (Class 3–5) 1 (L3) Maximum Required (Bubble or Dead-End Bubble) Stronghold

Relationships

  • regionRegion.pirate_holdings (cascade delete-orphan from region).
  • outlaw_baseOutlawBase.pirate_holding (1:1 — every holding has exactly one base).
  • special_formationSpecialFormation.pirate_holding (0:1 — holding optionally references a formation).
  • anchor_sectorSector.anchored_pirate_holdings (a sector can anchor multiple if formations overlap, but typically 1).
  • owner_player / owner_team — set on capture; null while pirate-controlled.
  • planets and stations are not direct FKs — they're queried via interior_sector_ids against the Sector table:
def planets_in_holding(holding):
    return db.query(Planet).filter(Planet.sector_id.in_(holding.interior_sector_ids)).all()

def stations_in_holding(holding):
    return db.query(Station).filter(Station.sector_id.in_(holding.interior_sector_ids)).all()

When the holding flips ownership, the capture transaction updates these planets and stations atomically (per the capture trigger in ../SYSTEMS/pirate-holding-raid.md#capture-trigger).

State per defense layer (current_strength model)

Per ADR-0047, the holding's defensive state is the source of truth for raid progress (no per-player RaidProgress table). Each defense layer carries current_strength: Float [0.0, 1.0] and last_damage_at: DateTime nullable. The columns live on the entity that carries the defense:

Defense surface Where strength lives
Sector drones Sector.defenses.drone_blocks[*].current_strength (existing JSONB extended) — canonical key is drone_blocks per ./jsonb-schema.md, not deployed_drones. Per ADR-0065 M-J1.
Sector mines Sector.defenses.mines[*].current_strength (existing JSONB extended)
Pirate patrol ships Sector.defenses.pirate_patrol_ships[*].current_strength (new JSONB sub-shape — see ./jsonb-schema.md)
Station structure Station.defense_state.current_strength (new sub-shape on existing column)
Planet orbital platforms per-platform row in the existing planet-defense schema, new column current_strength
Planet rail guns per-battery row in the existing planet-defense schema, new column current_strength
Citadel Planet.citadel_state.current_strength (new sub-shape)
OutlawBase / NPC presence OutlawBase.current_strength (logical — derived as (active_npcs / target_count))

The recovery service in ../SYSTEMS/pirate-holding-raid.md#recovery-service ticks each layer per the holding's tier-bound recovery rate.

Lifecycle

  1. Created at galaxy generation by Phase 12.6 (Hostile-faction pre-seeding) per ../SYSTEMS/galaxy-generator-design.md. Tier and composition rolled deterministically from the region seed.
  2. Defended in steady state by NPC scheduler Loop B (NPC spawn/maintenance), runtime sector-defense placement, and the daily recovery service.
  3. Engaged when a player initiates combat — concurrent-attacker lock claimed for Outpost/Stronghold tiers, atomic capture trigger evaluated on each NPC KIA.
  4. Captured atomically when all HOSTILE_RAIDER NPCs anchored to the OutlawBase are KIA. Ownership cross-flips planets, stations, formation, and converts OutlawBase to NPCBarracks in one transaction.
  5. Recovered organically while pirate-controlled — defenses regen at the per-tier rate; KIA NPCs respawn on the per-tier cooldown.
  6. Player-owned: recovery service is paused; planets and stations operate as normal player-owned facilities. Owner can colonize, station fleets, withdraw treasury, etc.
  7. Abandoned after 30 days of player inactivity — fresh pirate Lord/captain spawns; ownership flips back to pirate; defenses repopulate at 50% strength (anti-grief); 90-day full re-fortification.

Source map

Concern Path (target)
PirateHolding model services/gameserver/src/models/pirate_holding.py
Ecosystem seeding (bootstrap + daughter propagation) services/gameserver/src/services/pirate_ecosystem_service.py:seed_spawn_camp / spawn_daughter_holding
Recovery service (daily tick) services/gameserver/src/services/pirate_holding_recovery_service.py
Capture trigger services/gameserver/src/services/pirate_holding_service.py:attempt_capture
Abandonment service pirate_holding_service.py:process_abandonment
Concurrent-attacker lock pirate_holding_service.py:claim_holding_for_combat
API: list holdings services/gameserver/src/api/routes/pirate_holdings.py