240 lines
9.6 KiB
Python
240 lines
9.6 KiB
Python
"""Copies approved sprites from the variants directory to the game assets directory.
|
|
|
|
Also appends a ledger row to `<assets_dir>/LICENSES.md` for each install
|
|
so `tools/sprite-license-audit.py` stays clean (p2-28). The row carries
|
|
the on-disk SHA256 plus model attribution from the registry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as _dt
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from engine.registry import SpriteRegistry
|
|
|
|
|
|
_LEDGER_HEADER_MARKER = "## Assets"
|
|
_LEDGER_PLACEHOLDER = "*(empty"
|
|
|
|
|
|
def _sha256_of(path: Path) -> str:
|
|
h = hashlib.sha256()
|
|
with path.open("rb") as fh:
|
|
for chunk in iter(lambda: fh.read(1 << 16), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def _resolve_model_attribution(sprite: dict, variant_id: int | None = None) -> tuple[str, str]:
|
|
"""Return (license_value, author) for the ledger row.
|
|
|
|
Reads `tools/sprite-generation/sprite-config.json` for the active
|
|
model name and fits the ledger's `model-commercial:<model-name>`
|
|
license token. Author defaults to the model id; commissioned art
|
|
can be added by hand.
|
|
"""
|
|
cfg_path = Path(__file__).resolve().parents[1] / "sprite-config.json"
|
|
model = "unknown"
|
|
if cfg_path.exists():
|
|
try:
|
|
cfg = json.loads(cfg_path.read_text())
|
|
model = cfg.get("model", model)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
return f"model-commercial:{model}", model
|
|
|
|
|
|
def _append_ledger_row(assets_dir: Path, install_rel: str, sprite: dict) -> None:
|
|
"""Append one ledger row for the just-installed sprite. Idempotent — if a
|
|
row for this path already exists, leaves the file alone.
|
|
"""
|
|
ledger = assets_dir / "LICENSES.md"
|
|
if not ledger.exists():
|
|
# Don't auto-create the ledger; its header is hand-authored. Skip silently.
|
|
return
|
|
text = ledger.read_text()
|
|
# Skip if the path already has a row.
|
|
if f"| `{install_rel}` |" in text or f"| {install_rel} |" in text:
|
|
return
|
|
|
|
full = assets_dir / install_rel
|
|
if not full.exists():
|
|
return
|
|
sha = _sha256_of(full)
|
|
license_val, author = _resolve_model_attribution(sprite)
|
|
today = _dt.date.today().isoformat()
|
|
row = f"| {install_rel} | ai-generated | {license_val} | {author} | model-card | {sha} | {today} |"
|
|
|
|
lines = text.splitlines()
|
|
out: list[str] = []
|
|
inserted = False
|
|
in_assets = False
|
|
for line in lines:
|
|
if line.strip().startswith(_LEDGER_HEADER_MARKER):
|
|
in_assets = True
|
|
out.append(line)
|
|
continue
|
|
# Drop the placeholder once we hit it inside the Assets section.
|
|
if in_assets and line.strip().startswith(_LEDGER_PLACEHOLDER):
|
|
continue
|
|
if in_assets and not inserted and line.startswith("##"):
|
|
# Hit next section — append before it.
|
|
out.append(row)
|
|
out.append("")
|
|
inserted = True
|
|
out.append(line)
|
|
if in_assets and not inserted:
|
|
out.append(row)
|
|
ledger.write_text("\n".join(out) + "\n")
|
|
|
|
|
|
class SpriteInstaller:
|
|
def __init__(self, assets_dir: Path, registry: SpriteRegistry) -> None:
|
|
self.assets_dir = assets_dir
|
|
self.registry = registry
|
|
|
|
def install_approved(self, category: str | None = None, dry_run: bool = False) -> int:
|
|
"""Install all approved, uninstalled sprites. Returns count installed."""
|
|
entries = self.registry.get_approved_uninstalled(category)
|
|
count = 0
|
|
|
|
for sprite in entries:
|
|
sprite_id = sprite["id"]
|
|
install_path = sprite.get("install_path")
|
|
if not install_path:
|
|
continue
|
|
|
|
# Check if sprite-level has an approved variant (no dimension)
|
|
if sprite["status"] == "approved":
|
|
variants = self.registry.get_variants(sprite_id)
|
|
approved = [v for v in variants if v["is_approved"] and v["dimension_id"] is None]
|
|
if approved:
|
|
source = approved[0].get("processed_path")
|
|
if source and Path(source).exists():
|
|
dest = self.assets_dir / install_path
|
|
if dry_run:
|
|
print(f"[DRY RUN] Would install {install_path}")
|
|
else:
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, dest)
|
|
self.registry.mark_installed(sprite_id)
|
|
print(f"Installed {install_path}")
|
|
count += 1
|
|
|
|
# Handle approved dimensions
|
|
for dim in sprite.get("approved_dimensions", []):
|
|
dim_install = dim.get("install_path")
|
|
if not dim_install:
|
|
continue
|
|
if dim["status"] != "approved":
|
|
continue
|
|
dim_id = dim["id"]
|
|
approved_vid = dim.get("approved_variant_id")
|
|
if not approved_vid:
|
|
continue
|
|
dim_variants = self.registry.get_variants(sprite_id, dim_id)
|
|
approved = [v for v in dim_variants if v["id"] == approved_vid]
|
|
if not approved:
|
|
continue
|
|
source = approved[0].get("processed_path")
|
|
if not source or not Path(source).exists():
|
|
continue
|
|
dest = self.assets_dir / dim_install
|
|
if dry_run:
|
|
print(f"[DRY RUN] Would install {dim_install}")
|
|
else:
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, dest)
|
|
self.registry.mark_installed(sprite_id, dim_id)
|
|
print(f"Installed {dim_install}")
|
|
count += 1
|
|
|
|
return count
|
|
|
|
def install_one(self, sprite_id: str, dimension_id: int | None = None) -> Path | None:
|
|
"""Install a single sprite/dimension. Returns install path or None."""
|
|
sprite = self.registry.get_sprite(sprite_id)
|
|
if not sprite:
|
|
print(f"Sprite '{sprite_id}' not found")
|
|
return None
|
|
|
|
if dimension_id is not None:
|
|
dim = next((d for d in sprite["dimensions_list"] if d["id"] == dimension_id), None)
|
|
if not dim:
|
|
print(f"Dimension {dimension_id} not found for '{sprite_id}'")
|
|
return None
|
|
if dim["status"] not in ("approved", "installed"):
|
|
print(f"Dimension {dimension_id} is not approved (status: {dim['status']})")
|
|
return None
|
|
install_path = dim.get("install_path")
|
|
approved_vid = dim.get("approved_variant_id")
|
|
if not install_path or not approved_vid:
|
|
print(f"Dimension {dimension_id} has no install_path or approved variant")
|
|
return None
|
|
approved = [v for v in dim.get("variants", []) if v["id"] == approved_vid]
|
|
if not approved:
|
|
print(f"Approved variant {approved_vid} not found")
|
|
return None
|
|
source = approved[0].get("processed_path")
|
|
else:
|
|
if sprite["status"] not in ("approved", "installed"):
|
|
print(f"Sprite '{sprite_id}' is not approved (status: {sprite['status']})")
|
|
return None
|
|
install_path = sprite.get("install_path")
|
|
if not install_path:
|
|
print(f"Sprite '{sprite_id}' has no install_path")
|
|
return None
|
|
approved = [v for v in sprite.get("variants", []) if v["is_approved"]]
|
|
if not approved:
|
|
print(f"No approved variant for '{sprite_id}'")
|
|
return None
|
|
source = approved[0].get("processed_path")
|
|
|
|
if not source or not Path(source).exists():
|
|
print(f"Source file not found: {source}")
|
|
return None
|
|
|
|
dest = self.assets_dir / install_path
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, dest)
|
|
self.registry.mark_installed(sprite_id, dimension_id)
|
|
# p2-28: append the ledger row so sprite-license-audit.py stays clean.
|
|
_append_ledger_row(self.assets_dir, install_path, sprite)
|
|
print(f"Installed {install_path}")
|
|
return dest
|
|
|
|
def uninstall(self, sprite_id: str, dimension_id: int | None = None) -> bool:
|
|
"""Remove an installed sprite, revert status to approved."""
|
|
sprite = self.registry.get_sprite(sprite_id)
|
|
if not sprite:
|
|
print(f"Sprite '{sprite_id}' not found")
|
|
return False
|
|
|
|
if dimension_id is not None:
|
|
dim = next((d for d in sprite["dimensions_list"] if d["id"] == dimension_id), None)
|
|
if not dim or dim["status"] != "installed":
|
|
print(f"Dimension {dimension_id} is not installed")
|
|
return False
|
|
install_path = dim.get("install_path")
|
|
if install_path:
|
|
dest = self.assets_dir / install_path
|
|
if dest.exists():
|
|
dest.unlink()
|
|
self.registry.update_dimension_status(dimension_id, "approved")
|
|
print(f"Uninstalled dimension {dimension_id} of '{sprite_id}'")
|
|
return True
|
|
|
|
if sprite["status"] != "installed":
|
|
print(f"Sprite '{sprite_id}' is not installed (status: {sprite['status']})")
|
|
return False
|
|
install_path = sprite.get("install_path")
|
|
if install_path:
|
|
dest = self.assets_dir / install_path
|
|
if dest.exists():
|
|
dest.unlink()
|
|
self.registry.update_sprite_status(sprite_id, "approved")
|
|
print(f"Uninstalled '{sprite_id}'")
|
|
return True
|