feat: ATL pictures/contains/function/time, colour matrices and more Python for Ren'Py

ATL
- Image blocks become timelines: picture steps with dissolve, parallel, contains,
  finite repeat, time, function f(trans, st, at), calls of other transforms with
  arguments, on hover/idle events for buttons. Animations stop when their element
  leaves the page
- Pictures defined by an expression (get_face_path("base")) are drawn at show time
- Purely graphical hints (shader, mesh, gl_*, u_*) are accepted and dropped

Colour
- matrixcolor and im.matrix.* as 5x5 matrices combined with `*`, applied through SVG
  filters; im.MatrixColor/Scale/Crop/Blur/Composite and Text/Image as values

Python
- match statement, re/copy/time/datetime/defaultdict/functools.partial, more
  str/dict methods, Character(...) created by code
- narrator("..."), Character calls, renpy.say and renpy.show/hide with expressions in
  Python blocks become script nodes
- Audit follows reachability: functions and classes nothing refers to (custom
  statement lexers, unused tools) no longer block an import

Docs: README and ARCHITECTURE now describe a multi-engine library (GameMaker and
QSP planned). Tests for the new runtime pieces.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 14:05:14 +03:00
1 parent 62ece25902
commit ea03cebf6a
21 files changed
+1264 -93

No files matched your search

+72 -3
View File
@@ -111,11 +111,27 @@ SetScreenVariable ToggleScreenVariable Show Hide ShowTransient Function NullActi
Quit MainMenu If GetTooltip StaticValue VariableValue AnimatedValue FieldValue SelectedIf
SensitiveIf Queue ToggleScreen Language ShowMenu OpenURL Crop LiveCrop Solid Frame Null Transform
DynamicDisplayable Color Dissolve Fade ImageDissolve MoveTransition CropMove PushMove Pixellate
ComposeTransition With""".split())
ComposeTransition With Text SetLocalVariable QuickSave QuickLoad AlphaMask Fixed At
TintMatrix BrightnessMatrix SaturationMatrix ContrastMatrix InvertMatrix OpacityMatrix HueMatrix
ColorizeMatrix IdentityMatrix print Exception BaseException Character""".split())
SCREEN_CALLS |= {"im." + name for name in (
"MatrixColor Scale FactorScale Flip Crop Grayscale Sepia Alpha Blur Composite Tile Rotozoom "
"matrix.brightness matrix.contrast matrix.saturation matrix.tint matrix.identity matrix.invert "
"matrix.opacity matrix.hue matrix.colorize").split()}
SCREEN_CALLS |= {"Image", "re.sub", "re.match", "re.search", "re.findall", "re.split", "re.escape",
"copy.copy", "copy.deepcopy", "time.time", "time.strftime", "time.sleep",
"datetime.now", "datetime.datetime.now", "datetime.today", "datetime.date.today",
"collections.defaultdict", "collections.OrderedDict", "renpy.partial", "renpy.loadable",
"renpy.is_init_phase", "renpy.call_stack_depth", "renpy.const", "renpy.screenshot",
"renpy.Keymap", "renpy.register_shader", "renpy.register_statement", "renpy.redraw",
"renpy.stop_predict", "renpy.map_event", "renpy.count_displayables_in_layer",
"renpy.displayable", "renpy.game.context", "renpy.unlink_save", "json.dumps", "json.loads",
"_window_hide", "_window_show", "functools.partial"}
CALLS |= SCREEN_CALLS
CALLS |= SCREEN_CALLS | {"globals", "renpy.variant", "renpy.change_language", "renpy.seen_image",
"renpy.show_screen", "renpy.hide_screen", "renpy.restart_interaction",
"renpy.get_screen", "renpy.call_screen"}
METHODS = set("append extend insert pop remove index count reverse sort clear copy add discard get keys values items setdefault update lower upper capitalize title strip lstrip rstrip startswith endswith replace split join format find isdigit isalpha".split())
METHODS = set("append extend insert pop remove index count reverse sort clear copy add discard get keys values items setdefault update lower upper capitalize title strip lstrip rstrip startswith endswith replace split join format find isdigit isalpha zfill ljust rjust center partition rpartition rsplit splitlines isupper islower isspace isalnum swapcase rfind has_key iteritems iterkeys itervalues strftime isoformat weekday group groups groupdict start end".split())
def _bound_names(root) -> tuple[set[str], set[str]]:
@@ -197,6 +213,59 @@ def mark_screen_calls(data) -> None:
[data.screens[n] for n in data.screen_used if n in data.screens]])
IMPLICIT_METHODS = {"__init__", "__str__", "__repr__"}
def reachable_init_code(data, used_screens) -> list:
"""The part of `init python` that can run: startup statements plus every function, class and
method that something reachable refers to (by name or as an attribute).
Definitions nobody refers to - lexers of custom statements, helpers of unused tools - are left
out of the audit; if the game ever calls one, the player stops with an explicit error.
"""
roots: list = []
definitions: dict[str, list] = {}
def collect(statements: list) -> None:
for statement in statements:
op = statement.get("op")
if op == "def":
definitions.setdefault(statement["name"], []).append(statement)
elif op == "class":
definitions.setdefault(statement["name"], []).append(statement)
collect([m for m in statement["body"] if m.get("op") == "def"])
else:
roots.append(statement)
collect(data.init_code)
result: list = list(roots)
taken: set[int] = set()
queue: list = [roots, list(data.modules.values()), data.defaults, data.images, list(used_screens.values())]
while queue:
fragment = queue.pop()
stack = [fragment]
while stack:
value = stack.pop()
if isinstance(value, list):
stack.extend(value)
elif isinstance(value, dict):
name = value.get("id") if value.get("k") == "name" else value.get("a") if value.get("k") == "attr" else None
for node in definitions.get(name, ()) if name else ():
if id(node) in taken:
continue
taken.add(id(node))
if node["op"] == "class":
# class-level assignments and the methods Python calls implicitly
own = [m for m in node["body"] if m.get("op") != "def" or m["name"] in IMPLICIT_METHODS]
result.extend(own)
queue.append(own)
else:
result.append(node)
queue.append(node)
stack.extend(value.values())
return result
def audit_ir(data) -> None:
"""Reject calls which the browser would otherwise silently replace by None."""
mark_screen_calls(data)
@@ -241,5 +310,5 @@ def audit_ir(data) -> None:
visit(definition, f"screen {name}")
for name in sorted(data.screen_used - set(data.screens)):
data.warn("Не найден screen", name)
visit(data.init_code, "init")
visit(reachable_init_code(data, used_screens), "init")
visit(data.images, "image")