- Add app/games: GameProfile base, registry, ReaderHooks and renpy/naninovel/ tomie profiles. pipeline, preflight, detect and mods dispatch via the registry - Move tomie_* converter modules to app/games/tomie and split out reader hooks, import, mod validation and known defects - Tolerate the 31 authoring defects of the pinned Tomie build as report warnings (exact list, per-script counts); any new defect still blocks import - Frontend: runtime registry (runtimes.ts) with lazy Tomie launcher - Add profile/registry tests and docs/GAME_PROFILES.md; update IMPORT.md Full media export of the original Tomie build is not yet verified. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
79 lines
4.2 KiB
Python
79 lines
4.2 KiB
Python
"""Read-only compatibility check, shared by uploads, CLI and the converter."""
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from app.converter.archive import extract
|
|
from app.converter.compatibility import CompatibilityError, Issue, detect
|
|
|
|
|
|
def probe_archive(source: Path, filename: str) -> dict:
|
|
"""Cheap distribution check before queueing: list entries, never extract media."""
|
|
if zipfile.is_zipfile(source):
|
|
with zipfile.ZipFile(source) as archive:
|
|
entries = [entry.filename.replace("\\", "/") for entry in archive.infolist()]
|
|
else:
|
|
seven = shutil.which("7zz") or shutil.which("7z")
|
|
if not seven:
|
|
from app.converter.archive import ArchiveError
|
|
raise ArchiveError("Для проверки этого архива нужен 7-Zip")
|
|
try:
|
|
process = subprocess.run([seven, "l", "-slt", "-ba", "-sccUTF-8", str(source)],
|
|
capture_output=True, encoding="utf-8", errors="replace", timeout=120)
|
|
except subprocess.TimeoutExpired:
|
|
from app.converter.archive import ArchiveError
|
|
raise ArchiveError("Превышено время чтения списка файлов архива") from None
|
|
if process.returncode:
|
|
from app.converter.archive import ArchiveError
|
|
raise ArchiveError("Не удалось прочитать архив: " + (process.stderr or process.stdout)[-500:])
|
|
entries = [line[7:].replace("\\", "/") for line in process.stdout.splitlines() if line.startswith("Path = ")]
|
|
from app.converter.compatibility import IGNORED
|
|
entries = [entry.lower() for entry in entries
|
|
if not any(part.lower() in IGNORED for part in Path(entry).parts)]
|
|
engines = set()
|
|
if any("naninovel" in entry.rsplit("/", 1)[-1] and entry.endswith(".dll") for entry in entries):
|
|
engines.add("naninovel")
|
|
elif any("_data/managed/" in entry or "_data/il2cpp_data/" in entry or entry.endswith("_data/data.unity3d") for entry in entries):
|
|
engines.add("unity")
|
|
if any(entry.rsplit("/", 1)[-1] == "data.win" for entry in entries):
|
|
engines.add("gamemaker")
|
|
if any(entry.endswith((".pck", "/project.godot")) or entry == "project.godot" for entry in entries):
|
|
engines.add("godot")
|
|
if any(entry.endswith((".rgss3a", ".rgss2a", ".rgssad", "/rpg_core.js", "/rmmz_core.js")) for entry in entries):
|
|
engines.add("rpgmaker")
|
|
if any(entry.endswith((".rpy", ".rpyc", ".rpa")) and "renpy/" not in entry for entry in entries):
|
|
engines.add("renpy")
|
|
if len(engines) != 1:
|
|
raise CompatibilityError("ambiguous" if engines else "unknown", [Issue(
|
|
"multiple_engines" if engines else "unknown_engine",
|
|
"В архиве несколько движков: " + ", ".join(sorted(engines)) if engines else
|
|
"Движок не распознан; нужен полный дистрибутив игры")])
|
|
engine = engines.pop()
|
|
from app.games.registry import all_profiles
|
|
if engine not in {profile.engine for profile in all_profiles()}:
|
|
raise CompatibilityError(engine, [Issue("unsupported_engine", f"Движок {engine} пока не поддерживается")])
|
|
return {"engine": engine, "scriptsChecked": False}
|
|
|
|
|
|
def inspect_directory(root: Path, *, work: Path | None = None) -> dict:
|
|
from app.games.registry import select
|
|
|
|
detection = detect(root)
|
|
profile = select(detection.engine, detection.root)
|
|
if profile is None:
|
|
raise CompatibilityError(detection.engine, [Issue(
|
|
"unsupported_engine", f"Для движка {detection.engine} нет профиля импорта")])
|
|
return profile.inspect(detection.root, work)
|
|
|
|
|
|
def inspect_archive(source: Path, filename: str) -> dict:
|
|
if source.is_dir():
|
|
return inspect_directory(source)
|
|
with tempfile.TemporaryDirectory(prefix="pgames-preflight-") as directory:
|
|
root = Path(directory)
|
|
probe = probe_archive(source, filename)
|
|
extract(source, filename, root, only_scripts=probe["engine"] == "renpy")
|
|
return inspect_directory(root, work=root / "_inspection")
|