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:
1 parent
62ece25902
commit
ea03cebf6a
21 files changed
+1264
-93
No files matched your search
@@ -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")
|
||||
Reference in new issue
Block a user