#!/usr/bin/env python3 """ Batch populate terrain_reference column in systems.db bodies table. For each inhabited body with NULL terrain_reference: 1. Construct expected wiki heightmap path: wiki/star-systems/{system_slug}/bodies/{body_id}/heightmap.png where system_slug = system_id with spaces replaced by hyphens. 2. Verify the file exists. 3. Update terrain_reference to the relative path. Bodies with missing heightmaps are logged to stdout for remediation. This is the prerequisite for generate_atlas.py (#832). Usage: python3 tooling/planet-gen/populate_terrain_reference.py python3 tooling/planet-gen/populate_terrain_reference.py --dry-run python3 tooling/planet-gen/populate_terrain_reference.py --db path/to/systems.db Decisions: D-191 (atlas pipeline prerequisites) """ import argparse import sqlite3 from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = (SCRIPT_DIR / ".." / "..").resolve() DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" WIKI_DIR = REPO_ROOT / "wiki" / "star-systems" def system_slug(system_id: str) -> str: """Convert a system_id to its wiki directory slug. 'GJ 71' → 'GJ-71' 'GJ 244A' → 'GJ-244A' 'GJ 559B' → 'GJ-559B' """ return system_id.replace(" ", "-") def expected_heightmap_path(system_id: str, body_id: str) -> Path: """Return the expected absolute heightmap path for a body.""" return WIKI_DIR / system_slug(system_id) / "bodies" / body_id / "heightmap.png" def relative_terrain_reference(system_id: str, body_id: str) -> str: """Return the terrain_reference value to store in the DB. Stored as a repo-root-relative path so it is portable across checkouts. """ return f"wiki/star-systems/{system_slug(system_id)}/bodies/{body_id}/heightmap.png" def main(): parser = argparse.ArgumentParser( description="Populate terrain_reference column in systems.db bodies table" ) parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db") parser.add_argument( "--dry-run", action="store_true", help="Report without writing changes", ) args = parser.parse_args() db_path = Path(args.db) if not db_path.exists(): print(f"error: {db_path} not found") raise SystemExit(1) conn = sqlite3.connect(str(db_path)) conn.execute("PRAGMA foreign_keys=ON") # Fetch all inhabited bodies with NULL terrain_reference. rows = conn.execute(""" SELECT b.body_id, b.system_id FROM bodies b WHERE b.terrain_reference IS NULL ORDER BY b.system_id, b.body_id """).fetchall() print(f"\n terrain_reference population pass") print(f" DB: {db_path}") if args.dry_run: print(f" Mode: DRY RUN") print(f"\n {len(rows)} bodies with NULL terrain_reference\n") found = [] missing = [] for body_id, system_id in rows: path = expected_heightmap_path(system_id, body_id) if path.exists(): found.append((body_id, system_id)) else: missing.append((body_id, system_id, str(path))) # Report missing heightmaps before writing — helps flag gaps early. if missing: print(f" MISSING heightmaps ({len(missing)} bodies — no update for these):") for body_id, system_id, path in missing: print(f" {body_id} ({system_id}) → {path}") print() if found: print(f" Updating {len(found)} bodies with terrain_reference:") for body_id, system_id in found: ref = relative_terrain_reference(system_id, body_id) print(f" {body_id} ({system_id}) → {ref}") if not args.dry_run: conn.execute( "UPDATE bodies SET terrain_reference = ? WHERE body_id = ?", (ref, body_id), ) if not args.dry_run: conn.commit() print(f"\n Committed {len(found)} terrain_reference updates.") else: print(f"\n Dry run — no changes written.") conn.close() # Summary print(f"\n Summary:") print(f" Updated: {len(found)}") print(f" Missing: {len(missing)}") print(f" Total: {len(rows)}\n") if missing: print(f" Action required: generate heightmaps for {len(missing)} bodies " f"before running generate_atlas.py (#832).") print(f" Use: make generate-terrain (or run generate.py per body)\n") if __name__ == "__main__": main()