feat: improve RenPy conversion and support MIST runtime
This commit is contained in:
1 parent
f7f7f5b1b3
commit
f9eb5c1212
44 files changed
+2822
-174
No files matched your search
@@ -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"}
|
||||
"itertools", "string", "functools", "operator", "json", "gc"}
|
||||
|
||||
|
||||
class Unsupported(Exception):
|
||||
@@ -36,25 +36,67 @@ def const(v: Any) -> Expr:
|
||||
return {"k": "const", "v": v}
|
||||
|
||||
|
||||
def python2_operators(source: str) -> str:
|
||||
"""Ren'Py 7 games are Python 2: `a <> b` means `a != b`. Quoted text is left alone."""
|
||||
out: list[str] = []
|
||||
quote = ""
|
||||
i = 0
|
||||
while i < len(source):
|
||||
ch = source[i]
|
||||
if quote:
|
||||
out.append(ch)
|
||||
if ch == "\\" and i + 1 < len(source):
|
||||
out.append(source[i + 1])
|
||||
i += 1
|
||||
elif ch == quote:
|
||||
quote = ""
|
||||
elif ch in "\"'":
|
||||
quote = ch
|
||||
out.append(ch)
|
||||
elif source.startswith("<>", i):
|
||||
out.append("!=")
|
||||
i += 1
|
||||
else:
|
||||
out.append(ch)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def parse_expr(source: str) -> Expr:
|
||||
try:
|
||||
tree = ast.parse(source.strip(), mode="eval")
|
||||
except SyntaxError as e:
|
||||
raise Unsupported(f"синтаксическая ошибка в выражении: {source!r}") from e
|
||||
try:
|
||||
tree = ast.parse(python2_operators(source).strip(), mode="eval")
|
||||
except SyntaxError:
|
||||
raise Unsupported(f"синтаксическая ошибка в выражении: {source!r}") from e
|
||||
return expr(tree.body)
|
||||
|
||||
|
||||
def parse_stmts(source: str) -> list[Stmt]:
|
||||
try:
|
||||
tree = ast.parse(_dedent(source))
|
||||
tree = python_ast(source)
|
||||
except SyntaxError as e:
|
||||
raise Unsupported(f"синтаксическая ошибка в python: {e.msg} ({source[:60]!r})") from e
|
||||
try:
|
||||
tree = python_ast(python2_operators(source))
|
||||
except SyntaxError:
|
||||
raise Unsupported(f"синтаксическая ошибка в python: {e.msg} ({source[:60]!r})") from e
|
||||
return stmts(tree.body)
|
||||
|
||||
|
||||
def _dedent(source: str) -> str:
|
||||
import textwrap
|
||||
return textwrap.dedent(source)
|
||||
def python_ast(source: str, mode: str = "exec") -> ast.AST:
|
||||
"""Parse an embedded block as Ren'Py does, preserving string contents/columns.
|
||||
|
||||
Physical continuation lines may start at column zero. Wrapping the indented
|
||||
code avoids dedenting those lines or changing whitespace inside literals.
|
||||
"""
|
||||
first = next((line for line in source.splitlines() if line.strip()), "")
|
||||
indented = mode == "exec" and first.startswith((" ", "\t"))
|
||||
tree = ast.parse("if True:\n" + source if indented else source, mode=mode)
|
||||
if indented:
|
||||
tree.body = tree.body[0].body
|
||||
ast.increment_lineno(tree, -1)
|
||||
return tree
|
||||
|
||||
|
||||
def literal(source: str) -> Any:
|
||||
@@ -164,6 +206,8 @@ def stmt(node: ast.stmt) -> list[Stmt]:
|
||||
if type(op) not in BIN_OPS:
|
||||
raise Unsupported(f"оператор {type(op).__name__}")
|
||||
return [{"op": "aug", "t": target(t), "o": BIN_OPS[type(op)], "v": expr(value)}]
|
||||
case ast.Expr(value=ast.Call(func=func)) if ast.unparse(func).startswith("build."):
|
||||
return [] # distribution settings (build.classify, build.archive ...) have no meaning in the browser
|
||||
case ast.Expr(value=ast.Constant()):
|
||||
return [] # docstring / bare literal
|
||||
case ast.Expr(value=value):
|
||||
@@ -220,7 +264,7 @@ def stmt(node: ast.stmt) -> list[Stmt]:
|
||||
|
||||
# Methods the player can run implicitly. Any other dunder method would silently
|
||||
# never be called, so the converter refuses it instead.
|
||||
SUPPORTED_DUNDERS = {"__init__", "__str__", "__repr__"}
|
||||
SUPPORTED_DUNDERS = {"__init__", "__str__", "__repr__", "__call__", "__eq__"}
|
||||
INACTIVE_VARIANTS = {"gui.variant", "renpy.variant"}
|
||||
TRANSPARENT_DECORATORS = {"renpy.pure"} # marks a function as side-effect free; no runtime meaning
|
||||
DECORATORS = {"staticmethod", "classmethod", "property"}
|
||||
|
||||
Reference in new issue
Block a user