""" Project Scanner - Detect game engine and import/export data files. RPGMaker layouts scanned (in priority order): MV/MZ : /www/data/ or /data/ Ace : /Data/ XP/VX : /Data/ (rvdata/rxdata - not currently handled) """ from __future__ import annotations import json import shutil from pathlib import Path from typing import Optional # --------------------------------------------------------------------------- # File categories used to decide what to import by default # --------------------------------------------------------------------------- # Core name/database files – almost always needed CORE_FILES = { "Actors.json", "Armors.json", "Classes.json", "Enemies.json", "Items.json", "MapInfos.json", "Skills.json", "States.json", "System.json", "Troops.json", "Weapons.json", "CommonEvents.json", "Animations.json", "Tilesets.json", } # Map files – large but contain the bulk of dialogue MAP_PATTERN = "Map[0-9]*.json" # Engine detection markers _MVMZ_MARKERS = { # files / dirs that hint at MV or MZ "www", # MV web build "package.json", # MZ desktop "Game.rpgproject", # MV "game.rmmzproject", # MZ } _ACE_MARKERS = { "Game.rgss3a", "Game.exe", # not conclusive but common } _ACE_DATA_SCRIPTS = {".rvdata2", ".rvdata"} # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def find_data_folder(game_root: str | Path) -> tuple[Optional[Path], str]: """Return (data_path, engine_name) for *game_root*. engine_name is one of: "MVMZ", "ACE", "UNKNOWN". data_path is None when nothing is found. """ root = Path(game_root) if not root.is_dir(): return None, "UNKNOWN" # ---- RPGMaker MV/MZ: www/data or data ---- for candidate in (root / "www" / "data", root / "data", root / "Data"): if candidate.is_dir() and _has_json_data(candidate): # Confirm engine variant engine = "MVMZ" for m in _MVMZ_MARKERS: if (root / m).exists(): engine = "MVMZ" break return candidate, engine # ---- RPGMaker Ace: Data/ with .rvdata2 ---- ace_data = root / "Data" if ace_data.is_dir(): rvdata = list(ace_data.glob("*.rvdata2")) + list(ace_data.glob("*.rvdata")) if rvdata: return ace_data, "ACE" # ---- Fallback: any sub-directory that holds .json files ---- for child in root.iterdir(): if child.is_dir() and _has_json_data(child): return child, "UNKNOWN" return None, "UNKNOWN" def list_data_files(data_path: str | Path, engine: str = "MVMZ") -> list[dict]: """Return a sorted list of importable file descriptors. Each item: {"name": str, "path": Path, "size_kb": float, "category": "core" | "map" | "other", "default": bool} """ data_path = Path(data_path) results: list[dict] = [] if engine == "ACE": # Ace uses binary rvdata2; not JSON-based, skip for now return results seen: set[str] = set() for fp in sorted(data_path.iterdir()): if not fp.is_file(): continue if fp.suffix.lower() != ".json": continue name = fp.name if name in seen: continue seen.add(name) size_kb = fp.stat().st_size / 1024 if name in CORE_FILES: cat = "core" default = True elif fp.match(MAP_PATTERN): cat = "map" default = True else: cat = "other" default = False results.append({ "name": name, "path": fp, "size_kb": round(size_kb, 1), "category": cat, "default": default, }) # Sort: core first, then maps (numeric), then other def _sort_key(item): if item["category"] == "core": return (0, item["name"]) if item["category"] == "map": # extract numeric part for natural order digits = "".join(c for c in item["name"] if c.isdigit()) or "0" return (1, int(digits)) return (2, item["name"]) results.sort(key=_sort_key) return results def import_to_files( file_items: list[dict], dest_dir: str | Path = "files", overwrite: bool = True, ) -> tuple[int, list[str]]: """Copy *file_items* (from list_data_files) into *dest_dir*. Returns (count_copied, list_of_errors). """ dest = Path(dest_dir) dest.mkdir(parents=True, exist_ok=True) copied = 0 errors: list[str] = [] for item in file_items: src: Path = item["path"] dst = dest / item["name"] try: if not overwrite and dst.exists(): continue shutil.copy2(src, dst) copied += 1 except Exception as exc: errors.append(f"{item['name']}: {exc}") return copied, errors def export_to_game( translated_dir: str | Path, game_data_path: str | Path, filenames: Optional[list[str]] = None, overwrite: bool = True, ) -> tuple[int, list[str]]: """Copy translated files back into the game's data folder. If *filenames* is None, all .json files in *translated_dir* are copied. Returns (count_copied, list_of_errors). """ src_dir = Path(translated_dir) dst_dir = Path(game_data_path) copied = 0 errors: list[str] = [] if not src_dir.is_dir(): return 0, [f"Translated folder not found: {src_dir}"] if not dst_dir.is_dir(): return 0, [f"Game data folder not found: {dst_dir}"] candidates = ( [src_dir / fn for fn in filenames] if filenames else list(src_dir.glob("*.json")) ) for fp in candidates: if not fp.is_file(): continue dst = dst_dir / fp.name try: if not overwrite and dst.exists(): continue shutil.copy2(fp, dst) copied += 1 except Exception as exc: errors.append(f"{fp.name}: {exc}") return copied, errors # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _has_json_data(folder: Path) -> bool: """Return True if the folder contains at least one .json data file.""" try: for p in folder.iterdir(): if p.suffix.lower() == ".json": return True except PermissionError: pass return False