Converter: cover real Ren'Py games better, add folder import and dry run

Checked against six real games (Ren'Py 7.5 and 8.x):
- nested labels and named menus become jump anchors inside label bodies
- call with arguments and labels with parameters (dynamically scoped)
- frame-by-frame ATL images, Movie frames, layeredimage, camera, Shake,
  custom audio channels via register_channel, show text with expressions
- generator expressions in Python, no SyntaxWarning noise from decompiled code

Also:
- app.import_dir converts an unpacked game folder without changing it
- app.converter.analyze dry-runs the script part of the converter
- imports use their own "importing" status so a worker restart can't hijack them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-25 03:23:20 +03:00
1 parent 35033fb86c
commit a97b46079e
18 files changed
+3456 -2695

No files matched your search

+21 -13
View File
@@ -3,6 +3,7 @@
import asyncio
import logging
import shutil
from pathlib import Path
from sqlalchemy import select, update
@@ -41,34 +42,39 @@ async def claim() -> GameVersion | None:
async def process(v: GameVersion) -> None:
settings = get_settings()
ok = await convert(v.id, upload_path(v.id), v.upload_name or "")
if ok:
remove_upload(v.id) # sources are not kept once converted
async def convert(version_id: int, source: Path, filename: str) -> bool:
"""Run the converter for a version and store the outcome. Returns success."""
loop = asyncio.get_running_loop()
work = settings.work_dir / str(v.id)
work = get_settings().work_dir / str(version_id)
def progress(text: str) -> None:
asyncio.run_coroutine_threadsafe(set_progress(v.id, text), loop)
asyncio.run_coroutine_threadsafe(set_progress(version_id, text), loop)
log.info("converting version %s (%s)", v.id, v.upload_name)
log.info("converting version %s (%s)", version_id, filename)
try:
result = await asyncio.to_thread(convert_archive, upload_path(v.id), v.upload_name or "",
work, progress)
result = await asyncio.to_thread(convert_archive, source, filename, work, progress)
except Exception as e: # noqa: BLE001
log.exception("version %s failed", v.id)
log.exception("version %s failed", version_id)
async with SessionLocal() as db:
version = await db.get(GameVersion, v.id)
version = await db.get(GameVersion, version_id)
if version:
version.status = "failed"
version.progress = f"Ошибка: {e}"[:255]
version.report = {"error": str(e)}
await db.commit()
return
return False
finally:
shutil.rmtree(work, ignore_errors=True)
async with SessionLocal() as db:
version = await db.get(GameVersion, v.id)
version = await db.get(GameVersion, version_id)
if version is None: # deleted while converting
return
return True
version.status = "ready"
version.progress = ""
version.manifest = result.manifest
@@ -81,9 +87,11 @@ async def process(v: GameVersion) -> None:
read_bytes(result.cover_source))
except OSError:
pass
if game and result.title and game.title == source.name:
game.title = result.title
await db.commit()
remove_upload(v.id) # sources are not kept once converted
log.info("version %s ready: %s", v.id, result.report.get("media"))
log.info("version %s ready: %s", version_id, result.report.get("media"))
return True
async def main() -> None: