feat: generator interpreter for Python that waits for the player (Ren'Py)
Python containing renpy.input, narrator("..."), character calls, renpy.say,
renpy.pause, renpy.display_menu, renpy.with_statement or renpy.call_screen now
runs through loops, try, functions, methods and expressions.
Player
- python.ts: execG/evalG/callG run only the code that may wait (mayBlock);
everything else stays on the synchronous fast path. A waiting call returns a
BlockingCall; outside the generator interpreter it is an explicit error. The
interpreter scope is set aside while the player decides
- engine: drives the generator and answers requests (dialogue, text input with
length/allow/exclude, pause, choice menu, transition, call screen)
- renpy.show/hide/scene, transitions and transforms as Python values,
Character(...) objects, Borders, set_tag_attributes
Converter
- blocking_names() lists game functions that may wait, transitively, and ships
them as game.json "blocking"; character calls are allowed
- hide screen of an unknown screen is harmless; images defined by a parenthesised
expression are supported
Hooked now passes the strict compatibility check (with Tomie and Twisted-Thoughts).
Tests: waiting.py fixture run through the generator interpreter; blocking-name analysis.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea03cebf6a
commit
3923ee72fb
11 files changed
+698
-27
No files matched your search
@@ -215,6 +215,53 @@ def mark_screen_calls(data) -> None:
|
||||
|
||||
IMPLICIT_METHODS = {"__init__", "__str__", "__repr__"}
|
||||
|
||||
# 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"}
|
||||
|
||||
|
||||
def blocking_names(data) -> list[str]:
|
||||
"""Names of game functions and methods that may wait for the player, directly or through each other."""
|
||||
definitions: dict[str, list] = {}
|
||||
|
||||
def collect(statements: list) -> None:
|
||||
for statement in statements:
|
||||
if statement.get("op") == "def":
|
||||
definitions.setdefault(statement["name"], []).append(statement)
|
||||
elif statement.get("op") == "class":
|
||||
collect([m for m in statement["body"] if m.get("op") == "def"])
|
||||
|
||||
collect(data.init_code)
|
||||
waiting = BLOCKING_BUILTINS | set(data.characters) | {"narrator"}
|
||||
found: set[str] = set()
|
||||
|
||||
def waits(node, known: set[str]) -> bool:
|
||||
stack = [node]
|
||||
while stack:
|
||||
value = stack.pop()
|
||||
if isinstance(value, list):
|
||||
stack.extend(value)
|
||||
elif isinstance(value, dict):
|
||||
if value.get("k") == "call":
|
||||
function = value["f"]
|
||||
last = function.get("id") if function.get("k") == "name" else function.get("a")
|
||||
dotted = ("renpy." + function["a"]) if (function.get("k") == "attr" and
|
||||
function["o"].get("k") == "name" and function["o"]["id"] == "renpy") else last
|
||||
if dotted in waiting or last in known or last in waiting:
|
||||
return True
|
||||
stack.extend(value.values())
|
||||
return False
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for name, nodes in definitions.items():
|
||||
if name not in found and any(waits(n["body"], found) for n in nodes):
|
||||
found.add(name)
|
||||
changed = True
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def reachable_init_code(data, used_screens) -> list:
|
||||
"""The part of `init python` that can run: startup statements plus every function, class and
|
||||
@@ -273,6 +320,7 @@ def audit_ir(data) -> None:
|
||||
user_names, user_methods = _bound_names([data.modules, data.init_code, data.defaults, data.images,
|
||||
list(used_screens.values())])
|
||||
user_names |= {name for name, _ in data.defaults}
|
||||
user_names |= set(data.characters) | {"narrator"}
|
||||
|
||||
def dotted(value):
|
||||
if value.get("k") == "name":
|
||||
|
||||
Reference in new issue
Block a user