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:
mrleo1nid committed 2026-10-25 04:42:48 +03:00
1 parent 06ff19ac4a
commit c8d83b3fe7
65 files changed
+3825 -68

No files matched your search

+11 -5
View File
@@ -16,15 +16,16 @@ def _safe_target(root: Path, name: str) -> Path | None:
return target if target.is_relative_to(root.resolve()) else None
def extract(archive: Path, filename: str, dest: Path) -> None:
def extract(archive: Path, filename: str, dest: Path, *, only_scripts: bool = False) -> None:
dest.mkdir(parents=True, exist_ok=True)
if filename.lower().endswith(".zip") and zipfile.is_zipfile(archive):
_extract_zip(archive, dest)
_extract_zip(archive, dest, only_scripts=only_scripts)
return
seven = shutil.which("7zz") or shutil.which("7z")
if not seven:
raise ArchiveError("Для этого формата нужен 7-Zip (7zz), он не установлен")
proc = subprocess.run([seven, "x", "-y", "-snl-", f"-o{dest}", str(archive)],
filters = [f"-ir!*{suffix}" for suffix in SCRIPT_EXTS | {".tar"}] if only_scripts else []
proc = subprocess.run([seven, "x", "-y", "-snl-", *filters, f"-o{dest}", str(archive)],
capture_output=True, text=True)
if proc.returncode not in (0, 1): # 1 = warnings
raise ArchiveError(f"7-Zip: {proc.stderr.strip() or proc.stdout.strip()[-500:]}")
@@ -33,14 +34,19 @@ def extract(archive: Path, filename: str, dest: Path) -> None:
inner = list(dest.iterdir())
if len(inner) == 1 and inner[0].suffix == ".tar":
tar = inner[0]
extract(tar, tar.name, dest / "_tar")
extract(tar, tar.name, dest / "_tar", only_scripts=only_scripts)
tar.unlink()
def _extract_zip(archive: Path, dest: Path) -> None:
SCRIPT_EXTS = {".rpy", ".rpyc", ".rpa", ".rpym", ".rpymc"}
def _extract_zip(archive: Path, dest: Path, *, only_scripts: bool = False) -> None:
with zipfile.ZipFile(archive) as zf:
for info in zf.infolist():
name = info.filename
if only_scripts and Path(name).suffix.lower() not in SCRIPT_EXTS:
continue
if info.flag_bits & 0x800 == 0:
# legacy zips store names in cp437; many were really written as cp866/utf-8
try: