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

+99 -99
View File
@@ -1,99 +1,99 @@
from datetime import datetime
from typing import Any
from sqlalchemy import (
ARRAY,
BigInteger,
Boolean,
DateTime,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(64), unique=True)
permissions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
is_system: Mapped[bool] = mapped_column(Boolean, default=False)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(64), unique=True)
password_hash: Mapped[str] = mapped_column(String(255))
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
must_change_password: Mapped[bool] = mapped_column(Boolean, default=True)
must_change_username: Mapped[bool] = mapped_column(Boolean, default=False)
token_version: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
role: Mapped[Role] = relationship(lazy="joined")
@property
def permissions(self) -> list[str]:
return sorted(self.role.permissions) if self.is_active else []
class Game(Base):
__tablename__ = "games"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(255))
description: Mapped[str] = mapped_column(Text, default="")
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
engine: Mapped[str] = mapped_column(String(32), default="renpy")
cover_hash: Mapped[str | None] = mapped_column(String(80))
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_by: Mapped[User | None] = relationship(lazy="joined")
class GameVersion(Base):
__tablename__ = "game_versions"
id: Mapped[int] = mapped_column(primary_key=True)
game_id: Mapped[int] = mapped_column(ForeignKey("games.id", ondelete="CASCADE"), index=True)
parent_id: Mapped[int | None] = mapped_column(ForeignKey("game_versions.id", ondelete="SET NULL"))
owner_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
name: Mapped[str] = mapped_column(String(255))
description: Mapped[str] = mapped_column(Text, default="")
# uploading -> queued -> converting -> ready | failed
status: Mapped[str] = mapped_column(String(16), default="uploading", index=True)
progress: Mapped[str] = mapped_column(String(255), default="")
upload_name: Mapped[str | None] = mapped_column(String(255))
manifest: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
report: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
size_bytes: Mapped[int] = mapped_column(BigInteger, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
owner: Mapped[User | None] = relationship(lazy="joined")
class Save(Base):
__tablename__ = "saves"
__table_args__ = (UniqueConstraint("user_id", "version_id", "slot"),)
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
version_id: Mapped[int] = mapped_column(ForeignKey("game_versions.id", ondelete="CASCADE"))
slot: Mapped[str] = mapped_column(String(32))
title: Mapped[str] = mapped_column(String(255), default="")
data: Mapped[dict[str, Any]] = mapped_column(JSONB)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
from datetime import datetime
from typing import Any
from sqlalchemy import (
ARRAY,
BigInteger,
Boolean,
DateTime,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(64), unique=True)
permissions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
is_system: Mapped[bool] = mapped_column(Boolean, default=False)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(64), unique=True)
password_hash: Mapped[str] = mapped_column(String(255))
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
must_change_password: Mapped[bool] = mapped_column(Boolean, default=True)
must_change_username: Mapped[bool] = mapped_column(Boolean, default=False)
token_version: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
role: Mapped[Role] = relationship(lazy="joined")
@property
def permissions(self) -> list[str]:
return sorted(self.role.permissions) if self.is_active else []
class Game(Base):
__tablename__ = "games"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(255))
description: Mapped[str] = mapped_column(Text, default="")
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
engine: Mapped[str] = mapped_column(String(32), default="renpy")
cover_hash: Mapped[str | None] = mapped_column(String(80))
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_by: Mapped[User | None] = relationship(lazy="joined")
class GameVersion(Base):
__tablename__ = "game_versions"
id: Mapped[int] = mapped_column(primary_key=True)
game_id: Mapped[int] = mapped_column(ForeignKey("games.id", ondelete="CASCADE"), index=True)
parent_id: Mapped[int | None] = mapped_column(ForeignKey("game_versions.id", ondelete="SET NULL"))
owner_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
name: Mapped[str] = mapped_column(String(255))
description: Mapped[str] = mapped_column(Text, default="")
# uploading -> queued -> converting -> ready | failed; importing = app.import_dir run
status: Mapped[str] = mapped_column(String(16), default="uploading", index=True)
progress: Mapped[str] = mapped_column(String(255), default="")
upload_name: Mapped[str | None] = mapped_column(String(255))
manifest: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
report: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
size_bytes: Mapped[int] = mapped_column(BigInteger, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
owner: Mapped[User | None] = relationship(lazy="joined")
class Save(Base):
__tablename__ = "saves"
__table_args__ = (UniqueConstraint("user_id", "version_id", "slot"),)
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
version_id: Mapped[int] = mapped_column(ForeignKey("game_versions.id", ondelete="CASCADE"))
slot: Mapped[str] = mapped_column(String(32))
title: Mapped[str] = mapped_column(String(255), default="")
data: Mapped[dict[str, Any]] = mapped_column(JSONB)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)