feat: eval/exec of strings, gallery Replay, callable actions; iNSight and SomethingBetter now pass
- pyparse.ts: parser for expressions and simple statements built at run time, producing
the sandbox's own IR; backs eval() and exec() (e.g. exec("persistent." + var + " = 0"))
- Replay/EndReplay and renpy.end_replay: the engine stashes the full game state, runs the
label with the given scope and restores everything when it ends; locked replays are
insensitive. Save loading shared as applySave()
- Screen actions are callable (MainMenu(confirm=False)())
- jump main_menu_screen is Ren'Py's own label: it opens the player's main menu
- Screens: a closing quote ends the word before a letter ("text"size 15) but not before
an operator ("a"+b); Text(...) pictures in image blocks
- README: current capabilities and limits of the Ren'Py support
Strict compatibility check on the local collection: Tomie, Twisted-Thoughts, Hooked,
iNSight-of-you and SomethingBetter pass; TheFixer (custom AlphaMask wardrobe, missing
labels) and That_New_Teacher (custom statements, Render API) do not.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3923ee72fb
commit
3ac4f0371f
11 files changed
+448
-36
No files matched your search
@@ -215,10 +215,14 @@ def mark_screen_calls(data) -> None:
|
||||
|
||||
IMPLICIT_METHODS = {"__init__", "__str__", "__repr__"}
|
||||
|
||||
# Labels Ren'Py itself defines (renpy/common); jumping to them goes to the player's own menu
|
||||
ENGINE_LABELS = {"main_menu_screen", "_main_menu_screen", "main_menu"}
|
||||
|
||||
# Calls that make the player wait; Python containing them runs on the player's generator interpreter
|
||||
BLOCKING_BUILTINS = {"renpy.input", "renpy.say", "renpy.pause", "renpy.display_menu", "renpy.with_statement",
|
||||
"renpy.call_screen"}
|
||||
CALLS |= BLOCKING_BUILTINS | {"renpy.show", "renpy.hide", "renpy.scene", "Borders", "renpy.set_tag_attributes"}
|
||||
CALLS |= BLOCKING_BUILTINS | {"renpy.show", "renpy.hide", "renpy.scene", "Borders", "renpy.set_tag_attributes",
|
||||
"eval", "exec", "Replay", "EndReplay", "renpy.end_replay"}
|
||||
|
||||
|
||||
def blocking_names(data) -> list[str]:
|
||||
@@ -340,6 +344,9 @@ def audit_ir(data) -> None:
|
||||
is_method = (function.get("k") == "attr" and not name.startswith(("renpy.", "math.", "random."))
|
||||
and (function.get("a") in METHODS or function.get("a") in user_methods
|
||||
or function.get("a") in user_names))
|
||||
# `MainMenu(confirm=False)()`: calling the action a screen action constructor returned
|
||||
if function.get("k") == "call" and dotted(function.get("f", {})) in SCREEN_CALLS:
|
||||
is_method = True
|
||||
if name not in CALLS and name not in user_names and not is_method:
|
||||
data.warn(f"Вызов не поддерживается плеером: {name or 'динамическая функция'}", location)
|
||||
if value.get("op") in {"jump", "call"} and value.get("target"):
|
||||
@@ -349,7 +356,7 @@ def audit_ir(data) -> None:
|
||||
visit(child, location)
|
||||
|
||||
from app.converter.pipeline import anchors
|
||||
labels = {label["name"] for module in data.modules.values() for label in module}
|
||||
labels = {label["name"] for module in data.modules.values() for label in module} | ENGINE_LABELS
|
||||
labels |= {name for module in data.modules.values() for name in anchors(module)}
|
||||
for path, module in data.modules.items():
|
||||
visit(module, path)
|
||||
|
||||
@@ -18,7 +18,7 @@ _SHOW_KEYWORDS = {"at", "as", "behind", "onlayer", "with", "zorder"}
|
||||
_NUMBER_LINE = re.compile(r"^\d+(\.\d*)?$|^\.\d+$")
|
||||
_CALL_LINE = re.compile(r"^([A-Za-z_][\w.]*)\s*\(")
|
||||
_DISPLAYABLE_CALLS = {"Movie", "Solid", "Transform", "Composite", "LiveComposite", "Fixed",
|
||||
"ConditionSwitch", "Image", "im.Scale", "im.FactorScale", "im.Flip",
|
||||
"ConditionSwitch", "Image", "Text", "im.Scale", "im.FactorScale", "im.Flip",
|
||||
"im.Composite", "im.Crop", "im.Grayscale", "im.Sepia", "Frame", "Flatten"}
|
||||
_MEDIA_EXTS = {"png", "jpg", "jpeg", "webp", "gif", "bmp", "avif", "ogg", "mp3", "wav", "opus",
|
||||
"flac", "webm", "ogv", "mp4", "mkv", "avi", "m4a"}
|
||||
|
||||
@@ -632,6 +632,8 @@ class RpyParser:
|
||||
args, kw = e["a"], e["kw"]
|
||||
if fname == "Solid" and args:
|
||||
return {"color": _const(args[0])}
|
||||
if fname == "Text" and args:
|
||||
return {"textExpr": args[0]}
|
||||
if fname in ("Movie",):
|
||||
play = kw.get("play") or (args[0] if args else None)
|
||||
return {"movie": _const(play)} if play else None
|
||||
|
||||
@@ -79,8 +79,13 @@ def ignorable(name: str) -> bool:
|
||||
|
||||
def tokens(text: str) -> list[str]:
|
||||
"""Split on whitespace outside brackets and quotes (Ren'Py "simple expressions")."""
|
||||
out, cur, depth, quote, escaped = [], [], 0, "", False
|
||||
out, cur, depth, quote, escaped, closed = [], [], 0, "", False, False
|
||||
for ch in text:
|
||||
if closed: # `"text"size 15`: a quote that ends a word ends the token
|
||||
closed = False
|
||||
if (ch.isalpha() or ch == "_") and depth == 0:
|
||||
out.append("".join(cur))
|
||||
cur = []
|
||||
if quote:
|
||||
cur.append(ch)
|
||||
if escaped:
|
||||
@@ -89,6 +94,7 @@ def tokens(text: str) -> list[str]:
|
||||
escaped = True
|
||||
elif ch == quote:
|
||||
quote = ""
|
||||
closed = True
|
||||
elif ch in "\"'":
|
||||
quote = ch
|
||||
cur.append(ch)
|
||||
|
||||
Reference in new issue
Block a user