#!/usr/bin/env python3 """ atlas_common.py — shared atlas-DB utilities for the planet-gen importers. Extracted from the retired generate_atlas.py (D-223, #951). The atlas city/road/ river geometry *generator* was retired when authored geometry was dropped in favour of the deterministic server-side cascade (Phase 4); markers.json is now a flavoured name pool only. What remained were the schema/DB helpers shared by the surviving build-time importers (heightmaps, province boundaries, city names) and the naming pipeline. Those live here so there is a single home for them with no dependency on the deleted geometry-production code. This is a library module — it does NOT re-exec the venv. The importing scripts (import_heightmaps.py, import_province_boundaries.py, import_city_names.py, gemma_naming.py) own the venv bootstrap before importing this module. Decisions: D-223 (authored content as flavoured name pool), D-191 (atlas index). """ import sqlite3 from pathlib import Path import yaml # --------------------------------------------------------------------------- # Paths and grid constants # --------------------------------------------------------------------------- TOOLING_DIR = Path(__file__).resolve().parent REPO_ROOT = (TOOLING_DIR / ".." / "..").resolve() DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems" SYSTEMS_SCHEMA_PATH = REPO_ROOT / "server" / "data" / "systems-schema.sql" # Heightmap pixel-space grid (D-202). Geometry derives from this resolution. GRID_W = 512 GRID_H = 256 # Delimiters for the single canonical atlas_* DDL block in # server/data/systems-schema.sql. `_load_atlas_schema()` extracts everything # between these markers at runtime so this file does not duplicate the schema # (and drift from it). _ATLAS_SCHEMA_BEGIN_MARKER = "-- BEGIN ATLAS INDEX" _ATLAS_SCHEMA_END_MARKER = "-- END ATLAS INDEX" # --------------------------------------------------------------------------- # Schema # --------------------------------------------------------------------------- def _load_atlas_schema() -> str: """Return the atlas_* DDL block from systems-schema.sql. systems-schema.sql is the single source of truth for the atlas index tables (see the BEGIN ATLAS INDEX / END ATLAS INDEX markers). We extract just that block and run it through `executescript` so callers work against any DB state (fresh or partially-migrated) without maintaining a second copy of the DDL here. """ if not SYSTEMS_SCHEMA_PATH.exists(): raise RuntimeError( f"systems-schema.sql not found at {SYSTEMS_SCHEMA_PATH} — " "atlas importers cannot proceed without the canonical schema." ) text = SYSTEMS_SCHEMA_PATH.read_text() try: start = text.index(_ATLAS_SCHEMA_BEGIN_MARKER) end = text.index(_ATLAS_SCHEMA_END_MARKER, start) except ValueError as e: raise RuntimeError( f"systems-schema.sql is missing the {_ATLAS_SCHEMA_BEGIN_MARKER}/" f"{_ATLAS_SCHEMA_END_MARKER} block — has the schema been " "restructured?" ) from e return text[start:end] def ensure_atlas_schema(conn: sqlite3.Connection) -> None: """Apply the canonical atlas_* DDL from systems-schema.sql. Idempotent: all statements inside the block use CREATE TABLE / INDEX IF NOT EXISTS, so running this on an already-migrated DB is a no-op. Also ensures the meta stamp table exists (#855, #856). """ conn.executescript(_load_atlas_schema()) conn.executescript( """ CREATE TABLE IF NOT EXISTS meta ( generator_name TEXT PRIMARY KEY, schema_version TEXT NOT NULL, schema_sha TEXT, generator_sha TEXT NOT NULL, generated_at TEXT NOT NULL DEFAULT (datetime('now')) ); """ ) # Add schema_sha column to existing DBs that pre-date #888 (#888 migration). try: conn.execute("ALTER TABLE meta ADD COLUMN schema_sha TEXT") except sqlite3.OperationalError as e: if "duplicate column" not in str(e).lower(): raise # --------------------------------------------------------------------------- # Body definition loading # --------------------------------------------------------------------------- def load_body_def(body_dir: Path) -> dict | None: """Load body definition from body index.md frontmatter.""" index_md = body_dir / "index.md" if not index_md.exists(): return None content = index_md.read_text() if not content.startswith("---"): return None try: end = content.index("---", 3) bd = yaml.safe_load(content[3:end]) except (ValueError, yaml.YAMLError): return None if not bd or "id" not in bd or "planet_class" not in bd: return None return bd # --------------------------------------------------------------------------- # DB queries # --------------------------------------------------------------------------- def query_inhabited_bodies(conn: sqlite3.Connection) -> list[dict]: """Query systems.db for all inhabited bodies with terrain_reference set.""" rows = conn.execute(""" SELECT b.body_id, b.system_id, b.terrain_reference, b.population, b.settlement_pattern, b.planet_class, b.economic_role, COALESCE(b.cultural_corridor, s.cultural_corridor) AS cultural_corridor FROM bodies b JOIN star_systems s ON b.system_id = s.system_id WHERE b.inhabited = 1 AND b.terrain_reference IS NOT NULL AND b.population > 0 ORDER BY b.system_id, b.body_id """).fetchall() return [ { "body_id": row[0], "system_id": row[1], "terrain_reference": row[2], "population": row[3] or 0, "settlement_pattern": row[4], "planet_class": row[5], "economic_role": row[6], "cultural_corridor": row[7], } for row in rows ]