"""Read-only compatibility check, shared by uploads, CLI and the converter.""" from pathlib import Path import tempfile import zipfile import shutil import subprocess 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") process = subprocess.run([seven, "l", "-slt", "-ba", "-sccUTF-8", str(source)], capture_output=True, encoding="utf-8", errors="replace", timeout=120) 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() if engine not in {"renpy", "naninovel"}: raise CompatibilityError(engine, [Issue("unsupported_engine", f"Движок {engine} пока не поддерживается")]) return {"engine": engine, "scriptsChecked": False} def inspect_directory(root: Path, *, work: Path | None = None) -> dict: detection = detect(root) if detection.engine == "renpy": from app.converter.analyze import analyze report = analyze(detection.root) issues = [Issue(**issue) for issue in report.pop("issues", [])] if not report["hasStart"]: issues.append(Issue("missing_entry", "Не найдена точка входа start")) if issues: raise CompatibilityError("renpy", issues) return {"engine": "renpy", "compatible": True, **report} from app.converter.naninovel import inspect_naninovel return inspect_naninovel(detection.root, work=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")