Files
settled-reach/tooling/fill-missing-globes.py
T
jpmschweitzerandClaude Opus 4.6 903efda191 feat(assets): 100% globe coverage — generic images + donor fill
Generated oort cloud and asteroid belt generic globe images via Gemini.
Background-masked with flood-fill (shadow-preserving). Donor fill script
copies matching globe.png from same planet_class+body_type pool with
per-body hash selection, no repeats within a system. 629 bodies filled,
3240/3240 now have globe.png.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-03 20:12:00 +02:00

177 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""
One-shot script: fill missing globe.png files in the wiki by copying a donor
globe from the same planet_class + body_type pool.
Donor selection:
- Hash body_id to pick deterministically from the pool
- Track used donors per system to avoid visual repetition
- Skip oort_cloud and asteroid_belt (handled by generic images)
Run once, then the files are baked into the wiki like authored content.
Usage:
python3 tooling/fill-missing-globes.py [--dry-run]
"""
import hashlib
import os
import shutil
import sqlite3
import sys
from collections import defaultdict
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
WIKI_ROOT = REPO_ROOT / "wiki" / "star-systems"
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
GENERIC_TYPES = {"oort_cloud", "asteroid_belt"}
GENERIC_DIR = WIKI_ROOT / "_generics"
def globe_path(system_id: str, body_id: str) -> Path:
sys_dir = system_id.replace(" ", "-")
return WIKI_ROOT / sys_dir / "bodies" / body_id / "globe.png"
def body_hash(body_id: str) -> int:
return int(hashlib.sha256(body_id.encode()).hexdigest()[:8], 16)
def main():
dry_run = "--dry-run" in sys.argv
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
bodies = conn.execute(
"SELECT body_id, system_id, body_type, planet_class FROM bodies"
).fetchall()
# Build pools of existing globes by (body_type, planet_class)
pools: dict[tuple[str, str], list[Path]] = defaultdict(list)
missing: list[dict] = []
for b in bodies:
bid = b["body_id"]
bt = b["body_type"] or "unknown"
pc = (b["planet_class"] or "unknown").lower()
path = globe_path(b["system_id"], bid)
if bt in GENERIC_TYPES:
# Handled by generic images, not donors
if not path.exists():
generic = GENERIC_DIR / bt / "globe.png"
if generic.exists():
missing.append({
"body_id": bid,
"system_id": b["system_id"],
"body_type": bt,
"planet_class": pc,
"donor_path": generic,
"source": "generic",
})
continue
if path.exists():
pools[(bt, pc)].append(path)
else:
missing.append({
"body_id": bid,
"system_id": b["system_id"],
"body_type": bt,
"planet_class": pc,
"donor_path": None,
"source": "donor",
})
# For each missing body, pick a donor
used_per_system: dict[str, set[str]] = defaultdict(set)
assigned = 0
skipped = 0
log_lines = []
for m in missing:
if m["source"] == "generic":
# Generic image copy
target = globe_path(m["system_id"], m["body_id"])
if dry_run:
log_lines.append(
f"[GENERIC] {m['body_id']} <- {m['donor_path'].name} ({m['body_type']})"
)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(m["donor_path"], target)
log_lines.append(
f"[GENERIC] {m['body_id']} <- {m['donor_path']}"
)
assigned += 1
continue
bt = m["body_type"]
pc = m["planet_class"]
key = (bt, pc)
pool = pools.get(key, [])
if not pool:
# Try broader match: same body_type, any class
for (pbt, ppc), p in pools.items():
if pbt == bt and p:
pool = p
break
if not pool:
log_lines.append(
f"[SKIP] {m['body_id']} — no donors for {bt}/{pc}"
)
skipped += 1
continue
# Pick donor by hash, avoid repeats in same system
sys_id = m["system_id"]
h = body_hash(m["body_id"])
pool_size = len(pool)
used = used_per_system[sys_id]
donor = None
for offset in range(pool_size):
candidate = pool[(h + offset) % pool_size]
candidate_key = str(candidate)
if candidate_key not in used:
donor = candidate
used.add(candidate_key)
break
if donor is None:
# All donors used in this system — allow repeat from least-used
donor = pool[h % pool_size]
target = globe_path(sys_id, m["body_id"])
donor_bid = donor.parent.name
if dry_run:
log_lines.append(
f"[DONOR] {m['body_id']} <- {donor_bid} ({bt}/{pc})"
)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(donor, target)
log_lines.append(
f"[DONOR] {m['body_id']} <- {donor_bid} ({bt}/{pc})"
)
assigned += 1
conn.close()
# Report
prefix = "[DRY RUN] " if dry_run else ""
print(f"{prefix}Assigned: {assigned}, Skipped: {skipped}")
print()
for line in log_lines:
print(line)
if __name__ == "__main__":
main()