feat: add mod editing, revision merges and secure game storage
Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
This commit is contained in:
1 parent
06ff19ac4a
commit
c8d83b3fe7
65 files changed
+3825
-68
No files matched your search
@@ -0,0 +1,77 @@
|
||||
"""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")
|
||||
Reference in new issue
Block a user