"""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")