feat: converter reads the Phone library's register statement, named stores and generators

`init phone register` becomes a default (the group chat) plus a function of library calls that runs with
the other defaults of a new game (phone_register.py). `init/python [early|hide] in some.store` wraps its
code in an in_store node and `default some.store.name` may name a store; yield works as an expression
(generator functions are marked gen); `import x as y`, `transform -150 name(...)`, `init:` with if/else,
`config.special_namespaces` lines, `at` lists and tuples, transforms made by Python functions, `choice`
weights that are expressions, `__len__`/`__bool__`, and the whitelist for os, calendar, itertools,
pygame_sdl2 and the new runtime. The converter also records the size of every picture it converts
(`imageSizes`) so `renpy.image_size` answers without loading.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 22:39:16 +03:00
1 parent e94f9b0d9e
commit 83cc7fdd5c
11 files changed
+379 -26

No files matched your search

+9 -8
View File
@@ -25,7 +25,7 @@ UNARY_OPS = {ast.Not: "not", ast.USub: "-", ast.UAdd: "+", ast.Invert: "~"}
# Modules whose import is harmless: the player provides the functions it supports.
KNOWN_MODULES = {"random", "math", "renpy", "store", "time", "datetime", "re", "copy", "collections",
"itertools", "string", "functools", "operator", "json", "gc", "hashlib", "uuid", "pygame", "colorsys", "fnmatch", "traceback", "builtins"}
"itertools", "string", "functools", "operator", "json", "gc", "hashlib", "uuid", "pygame", "colorsys", "fnmatch", "traceback", "builtins", "os", "calendar", "pygame_sdl2"}
class Unsupported(Exception):
@@ -146,6 +146,8 @@ def expr(node: ast.AST) -> Expr:
"r": [expr(r) for r in rs]}
case ast.IfExp(test=t, body=b, orelse=e):
return {"k": "if", "c": expr(t), "t": expr(b), "e": expr(e)}
case ast.Yield(value=value): # `x = yield` and `x = yield v`: the value sent in by `.send(...)`
return {"k": "yield", "v": None if value is None else expr(value)}
case ast.List(elts=elts) | ast.Set(elts=elts):
return {"k": "list", "v": [element(e) for e in elts]}
case ast.Tuple(elts=elts):
@@ -282,7 +284,7 @@ def stmt(node: ast.stmt) -> list[Stmt]:
# Pickle hooks only matter for Ren'Py's own save files; the player keeps its own
IGNORED_DUNDERS = {"__setstate__", "__getstate__", "__reduce__", "__reduce_ex__"}
SUPPORTED_DUNDERS = {"__init__", "__str__", "__repr__", "__call__", "__eq__", "__hash__", "__iter__", "__getitem__",
"__contains__"}
"__contains__", "__len__", "__bool__"}
INACTIVE_VARIANTS = {"gui.variant", "renpy.variant"}
TRANSPARENT_DECORATORS = {"renpy.pure"} # marks a function as side-effect free; no runtime meaning
DECORATORS = {"staticmethod", "classmethod", "property"}
@@ -314,18 +316,17 @@ def decorator(node: ast.expr) -> Expr:
def is_generator(body: list[ast.stmt]) -> bool:
"""A function with `yield` statements. The player runs it to the end and hands out what it yielded, which is
right for generators that end; `x = yield` (sending values in) and async code stay unsupported."""
"""A function with `yield` in it: the player runs it step by step (next, send). `yield from` is accepted as a
statement only, and async code stays unsupported."""
module = ast.Module(body=body, type_ignores=[])
statement_yields = {id(n.value) for n in ast.walk(module)
if isinstance(n, ast.Expr) and isinstance(n.value, (ast.Yield, ast.YieldFrom))}
statement_froms = {id(n.value) for n in ast.walk(module) if isinstance(n, ast.Expr) and isinstance(n.value, ast.YieldFrom)}
found = False
for n in ast.walk(module):
if isinstance(n, ast.Await):
raise Unsupported("async")
if isinstance(n, ast.YieldFrom) and id(n) not in statement_froms:
raise Unsupported("yield from как выражение")
if isinstance(n, (ast.Yield, ast.YieldFrom)):
if id(n) not in statement_yields:
raise Unsupported("yield как выражение")
found = True
return found