167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
"""End-to-end approval pipeline: approve → process → install → manifest.
|
|
|
|
Single entry point for shipping a sprite variant from raw generation
|
|
through to game-ready installed asset with manifest entry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from engine.processor import SpriteProcessor
|
|
from engine.registry import SpriteRegistry
|
|
|
|
|
|
class SpritePipeline:
|
|
"""Orchestrates the approve → process → install → manifest flow."""
|
|
|
|
def __init__(
|
|
self,
|
|
registry: SpriteRegistry,
|
|
raw_dir: Path,
|
|
variants_dir: Path,
|
|
assets_dir: Path,
|
|
game_db_path: Path,
|
|
) -> None:
|
|
self.registry = registry
|
|
self.raw_dir = raw_dir
|
|
self.variants_dir = variants_dir
|
|
self.assets_dir = assets_dir
|
|
self.game_db_path = game_db_path
|
|
self.processor = SpriteProcessor()
|
|
self._ensure_game_db()
|
|
|
|
def _ensure_game_db(self) -> None:
|
|
"""Create the game-side sprites.db manifest if it doesn't exist."""
|
|
self.game_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(str(self.game_db_path))
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS sprites (
|
|
entity_type TEXT NOT NULL,
|
|
entity_id TEXT NOT NULL,
|
|
quality INTEGER,
|
|
variant TEXT,
|
|
path TEXT NOT NULL,
|
|
width INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
installed_at TEXT NOT NULL,
|
|
PRIMARY KEY (entity_type, entity_id, quality, variant)
|
|
)
|
|
""")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def approve_and_install(
|
|
self,
|
|
variant_id: int,
|
|
alt_name: str | None = None,
|
|
) -> Path | None:
|
|
"""Full pipeline for a single variant.
|
|
|
|
1. Mark variant as approved in pipeline sprites.db
|
|
2. Process raw image (background removal + resize)
|
|
3. Copy processed image to game assets directory
|
|
4. Write entry to game-side sprites.db manifest
|
|
|
|
Args:
|
|
variant_id: The variant to approve and install.
|
|
alt_name: If set, install as an alternate (e.g. "alt" → founder_alt.png).
|
|
If None, installs as the primary sprite.
|
|
|
|
Returns:
|
|
Path to installed asset, or None on failure.
|
|
"""
|
|
# 1. Get variant and sprite data
|
|
variant = self.registry.conn.execute(
|
|
"SELECT * FROM variants WHERE id=?", (variant_id,)
|
|
).fetchone()
|
|
if not variant:
|
|
print(f"Variant #{variant_id} not found")
|
|
return None
|
|
|
|
sprite_id = variant["sprite_id"]
|
|
sprite = self.registry.get_sprite(sprite_id)
|
|
if not sprite:
|
|
print(f"Sprite '{sprite_id}' not found")
|
|
return None
|
|
|
|
raw_path = Path(variant["raw_path"])
|
|
if not raw_path.exists():
|
|
print(f"Raw image not found: {raw_path}")
|
|
return None
|
|
|
|
category = sprite["category"]
|
|
|
|
# 2. Approve in pipeline DB
|
|
self.registry.approve_variant(variant_id)
|
|
print(f" [1/4] Approved #{variant_id} for {sprite_id}")
|
|
|
|
# 3. Process (background removal + resize)
|
|
safe_name = sprite_id.replace("/", "_")
|
|
processed_filename = f"{safe_name}_{variant_id}.png"
|
|
processed_path = self.variants_dir / processed_filename
|
|
self.variants_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if not self.processor.process(raw_path, category, processed_path):
|
|
print(f" Processing failed for #{variant_id}")
|
|
return None
|
|
|
|
# Update processed_path in pipeline DB
|
|
self.registry.update_variant_status(
|
|
variant_id, variant["job_status"],
|
|
processed_path=str(processed_path),
|
|
)
|
|
print(f" [2/4] Processed → {processed_path.name}")
|
|
|
|
# 4. Install to game assets
|
|
# Use the sprite's registered install_path (follows {id}_{race}_{gender} convention)
|
|
# or fall back to entity_id-based path
|
|
install_path = sprite.get("install_path")
|
|
if install_path:
|
|
asset_rel_path = install_path
|
|
else:
|
|
entity_id = sprite["entity_id"]
|
|
asset_rel_path = f"sprites/{category}/{entity_id}.png"
|
|
if alt_name:
|
|
# Insert alt name before extension: spearmen_dwarves_m.png → spearmen_dwarves_m_front.png
|
|
base, ext = asset_rel_path.rsplit(".", 1)
|
|
asset_rel_path = f"{base}_{alt_name}.{ext}"
|
|
asset_full_path = self.assets_dir / asset_rel_path
|
|
asset_full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
import shutil
|
|
shutil.copy2(processed_path, asset_full_path)
|
|
|
|
# Mark installed in pipeline DB
|
|
self.registry.mark_installed(sprite_id)
|
|
print(f" [3/4] Installed → {asset_rel_path}")
|
|
|
|
# 5. Write to game manifest DB
|
|
from PIL import Image
|
|
img = Image.open(asset_full_path)
|
|
width, height = img.size
|
|
|
|
from datetime import datetime, timezone
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
conn = sqlite3.connect(str(self.game_db_path))
|
|
conn.execute(
|
|
"""INSERT OR REPLACE INTO sprites
|
|
(entity_type, entity_id, quality, variant, path, width, height, installed_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(category, entity_id, 0, alt_name or "default", asset_rel_path, width, height, now),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
print(f" [4/4] Manifest → {self.game_db_path.name} ({category}/{entity_id})")
|
|
|
|
return asset_full_path
|
|
|
|
def list_installed(self) -> list[dict]:
|
|
"""List all entries in the game manifest DB."""
|
|
conn = sqlite3.connect(str(self.game_db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
rows = conn.execute("SELECT * FROM sprites ORDER BY entity_type, entity_id").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|