DazedTL/util/project_scanner.py
2026-03-12 20:20:17 -05:00

235 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Project Scanner - Detect game engine and import/export data files.
RPGMaker layouts scanned (in priority order):
MV/MZ : <root>/www/data/ or <root>/data/
Ace : <root>/Data/
XP/VX : <root>/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