diff --git a/tooling/planet-gen/atlas_common.py b/tooling/planet-gen/atlas_common.py new file mode 100644 index 000000000..10dee9241 --- /dev/null +++ b/tooling/planet-gen/atlas_common.py @@ -0,0 +1,164 @@ +#!/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 + ] diff --git a/tooling/planet-gen/import_heightmaps.py b/tooling/planet-gen/import_heightmaps.py index 4b3f53e9b..a6829b1a8 100644 --- a/tooling/planet-gen/import_heightmaps.py +++ b/tooling/planet-gen/import_heightmaps.py @@ -41,7 +41,7 @@ if _venv_python.exists() and Path(sys.executable).resolve() != _venv_python.reso import numpy as np import sqlite3 -from generate_atlas import ( +from atlas_common import ( GRID_W, GRID_H, DB_PATH, diff --git a/tooling/planet-gen/import_province_boundaries.py b/tooling/planet-gen/import_province_boundaries.py index 3da0f25f6..1dd1dfe39 100644 --- a/tooling/planet-gen/import_province_boundaries.py +++ b/tooling/planet-gen/import_province_boundaries.py @@ -59,7 +59,7 @@ if _venv_python.exists() and Path(sys.executable).resolve() != _venv_python.reso import numpy as np import sqlite3 -from generate_atlas import ( +from atlas_common import ( DB_PATH, ensure_atlas_schema, query_inhabited_bodies,