Pirate Holdings — Schema¶
Status: 🚧 Partial —
PirateHoldingships as a FOUNDATION slice: the population-score / target / cap / eligible-sector math the pirate ecosystem tick reads and writes (models/pirate_holding.py, migratione8b619503a44). Raid/capture fields split two ways: (a) dormant kernel —combat_lock_held_by,combat_lock_team_snapshot,owner_team_id,captured_at, andformation_id(doc design name wasspecial_formation_id) are committed columns plus wired service helpers (acquire_combat_lock/can_engage/release_combat_lock/capture_holdinginpirate_ecosystem_service.py); zero live callers yet, awaiting a raid-initiation route; (b) still absent —outlaw_base_id,interior_sector_ids,parent_holding_id,composition_profile,name,combat_lock_held_until,last_visit_at,last_pirate_recovery_at, andgrowth_eligible_atare not on the model. (re-verified 2026-08-21 vs Sectorwars2102 HEAD46bce720)
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¶
region↔Region.pirate_holdings(cascade delete-orphan from region).outlaw_base↔OutlawBase.pirate_holding(1:1 — every holding has exactly one base).special_formation↔SpecialFormation.pirate_holding(0:1 — holding optionally references a formation).anchor_sector↔Sector.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.planetsandstationsare not direct FKs — they're queried viainterior_sector_idsagainst theSectortable:
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¶
- 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. - Defended in steady state by NPC scheduler Loop B (NPC spawn/maintenance), runtime sector-defense placement, and the daily recovery service.
- Engaged when a player initiates combat — concurrent-attacker lock claimed for Outpost/Stronghold tiers, atomic capture trigger evaluated on each NPC KIA.
- Captured atomically when all
HOSTILE_RAIDERNPCs anchored to theOutlawBaseare KIA. Ownership cross-flips planets, stations, formation, and converts OutlawBase to NPCBarracks in one transaction. - Recovered organically while pirate-controlled — defenses regen at the per-tier rate; KIA NPCs respawn on the per-tier cooldown.
- Player-owned: recovery service is paused; planets and stations operate as normal player-owned facilities. Owner can colonize, station fleets, withdraw treasury, etc.
- 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 |
Related¶
../ADR/0047-pirate-holdings.md— canonical decision.../FEATURES/galaxy/pirate-holdings.md— player-facing reference.../SYSTEMS/pirate-holding-raid.md— raid mechanics../special-formations.md—SpecialFormationschema; gainspirate_holding_idFK../npc-lodging.md—OutlawBaseschema; one-to-one withPirateHolding../npcs.md—NPCRoster,NPCCharacter,HOSTILE_RAIDERarchetype../jsonb-schema.md—Sector.defenses.pirate_patrol_shipssub-shape.../SYSTEMS/galaxy-generator-design.md— Phase 12.6 placement.