Files
settled-reach/tooling/planet-gen/atlas_common.py
T
jpmschweitzerandClaude Opus 4.7 642be3ac41 refactor(tooling): extract atlas_common from generate_atlas (D-223 #951)
Move the shared atlas-DB utilities (schema application, inhabited-body
query, body-def loader, grid constants) out of the soon-to-be-retired
generate_atlas.py into a dedicated atlas_common.py with no dependency on
geometry-production code. Repoint the surviving build-time importers
(import_heightmaps, import_province_boundaries) at the new module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:28:38 +02:00

165 lines
6.0 KiB
Python

#!/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
]