feat: converter keeps layers of screens and camera, perspective, matrix calls; That New Teacher runs its own text-tag libraries

onlayer on show/hide/call screen and `camera <layer>` reach the player, `perspective`
is an ATL property now, RotateMatrix/OffsetMatrix/ScaleMatrix, Text.set_text and
renpy.fetch are accepted. The text-effect modules of That New Teacher are no longer
skipped (only the shader-based wave tag is).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-27 04:37:26 +03:00
1 parent 7652f7d010
commit 5da7ab1a90
6 files changed
+36 -14

No files matched your search

+1 -1
View File
@@ -43,7 +43,7 @@ LEADING_ZERO = re.compile(r"(?<![\w.])0+(\d)(?![\d.]*[\w.])")
SPLIT_SIGN = re.compile(r"^([a-z_]+)(-[\d.]+)$") SPLIT_SIGN = re.compile(r"^([a-z_]+)(-[\d.]+)$")
IGNORED = {"subpixel", "animation", "around", "alignaround", "rotate_pad"} # no visible effect in the browser IGNORED = {"subpixel", "animation", "around", "alignaround", "rotate_pad"} # no visible effect in the browser
# Purely graphical GPU hints; dropping them changes how a picture is drawn, never what happens # Purely graphical GPU hints; dropping them changes how a picture is drawn, never what happens
COSMETIC = ("shader", "mesh", "gl_", "u_", "perspective", "point_to", "delay", "events", "nearest", "fps", "show_cancels_hide") COSMETIC = ("shader", "mesh", "gl_", "u_", "point_to", "delay", "events", "nearest", "fps", "show_cancels_hide")
NAMED_POSITIONS = { NAMED_POSITIONS = {
"left": {"xpos": 0.0, "xanchor": 0.0}, "right": {"xpos": 1.0, "xanchor": 1.0}, "left": {"xpos": 0.0, "xanchor": 0.0}, "right": {"xpos": 1.0, "xanchor": 1.0},
"center": {"xpos": 0.5, "xanchor": 0.5}, "truecenter": {"xpos": 0.5, "xanchor": 0.5, "ypos": 0.5, "yanchor": 0.5}, "center": {"xpos": 0.5, "xanchor": 0.5}, "truecenter": {"xpos": 0.5, "xanchor": 0.5, "ypos": 0.5, "yanchor": 0.5},
+4 -2
View File
@@ -199,7 +199,7 @@ METHODS |= NATIVE_METHODS
# Methods of generators made by `renpy.random.Random()` / `random.Random()` # Methods of generators made by `renpy.random.Random()` / `random.Random()`
METHODS |= {"send", "_duplicate", "_unique", "set_child", "get_placement", "subsurface", "update_state", METHODS |= {"send", "_duplicate", "_unique", "set_child", "get_placement", "subsurface", "update_state",
"adjusted_times", "get_range", "interpolate", "from_iterable", "itermonthdays", "isnumeric", "isdecimal", "add_style_property", "add_prefix_style_property", "add_pattern", "get_context", "read", "readlines", "close", "sub", "subn", "search", "match", "findall", "finditer", "split", "time", "_clear", "Enable", "Disable", "Toggle", "encode", "blit", "subpixel_blit", "fill", "canvas", "get_size", "add_shader", "add_uniform", "add_property", "adjusted_times", "get_range", "interpolate", "from_iterable", "itermonthdays", "isnumeric", "isdecimal", "add_style_property", "add_prefix_style_property", "add_pattern", "get_context", "read", "readlines", "close", "sub", "subn", "search", "match", "findall", "finditer", "split", "time", "_clear", "Enable", "Disable", "Toggle", "encode", "blit", "subpixel_blit", "fill", "canvas", "get_size", "add_shader", "add_uniform", "add_property",
"add_focus", "circle", "rect", "line", "lines", "polygon"} "add_focus", "circle", "rect", "line", "lines", "polygon", "set_text"}
METHODS |= {"fullmatch", "hexdigest", "randint", "randrange", "random", "choice", "choices", "sample", "shuffle", "uniform"} METHODS |= {"fullmatch", "hexdigest", "randint", "randrange", "random", "choice", "choices", "sample", "shuffle", "uniform"}
@@ -370,8 +370,10 @@ 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 # 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", BLOCKING_BUILTINS = {"renpy.input", "renpy.say", "renpy.pause", "renpy.display_menu", "renpy.with_statement",
"renpy.call_screen", "renpy.movie_cutscene", "ui.interact"} "renpy.call_screen", "renpy.movie_cutscene", "renpy.fetch", "ui.interact"}
CALLS |= {"renpy.free_memory", "achievement.clear_all", "renpy.image", "ImageReference", "SetMixer"} CALLS |= {"renpy.free_memory", "achievement.clear_all", "renpy.image", "ImageReference", "SetMixer"}
# Geometry matrices of `matrixtransform` (frontend/src/player/rendertransform.ts)
CALLS |= {"RotateMatrix", "OffsetMatrix", "ScaleMatrix"}
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", "eval", "exec", "Replay", "EndReplay", "renpy.end_replay",
"renpy.get_showing_tags", "renpy.run", "renpy.pop_call", "renpy.get_showing_tags", "renpy.run", "renpy.pop_call",
+1 -1
View File
@@ -14,7 +14,7 @@ POSITION_PROPS = {"xpos", "ypos", "xanchor", "yanchor", "xalign", "yalign", "xce
"align", "pos", "anchor", "xysize", "size", "blur", "matrixcolor", "xycenter", "align", "pos", "anchor", "xysize", "size", "blur", "matrixcolor", "xycenter",
"xsize", "ysize", "crop", "xpan", "ypan", "xtile", "ytile", "xrotate", "yrotate", "zrotate", "zpos", "xsize", "ysize", "crop", "xpan", "ypan", "xtile", "ytile", "xrotate", "yrotate", "zrotate", "zpos",
"zzoom", "blend", "additive", "transform_anchor", "rotate_pad", "matrixanchor", "matrixtransform", "zzoom", "blend", "additive", "transform_anchor", "rotate_pad", "matrixanchor", "matrixtransform",
"fit", "maxsize", "crop_relative"} "fit", "maxsize", "crop_relative", "perspective"}
_SHOW_KEYWORDS = {"at", "as", "behind", "onlayer", "with", "zorder"} _SHOW_KEYWORDS = {"at", "as", "behind", "onlayer", "with", "zorder"}
+15 -5
View File
@@ -1227,8 +1227,10 @@ class RpyParser:
def st_camera(self, line: Line) -> list[Node]: def st_camera(self, line: Line) -> list[Node]:
c = self.cursor(line) c = self.cursor(line)
c.keyword("camera") c.keyword("camera")
c.word() if not c.peek("at") and not c.peek(":") else None layer = c.word() if not c.peek("at") and not c.peek(":") else None
node: Node = {"op": "camera", "at": []} node: Node = {"op": "camera", "at": []}
if layer:
node["layer"] = layer
if c.keyword("at"): if c.keyword("at"):
for item in _split_top(c.until(set(), ":")): for item in _split_top(c.until(set(), ":")):
node["at"].append(self.transform_ref(item, line)) node["at"].append(self.transform_ref(item, line))
@@ -1466,8 +1468,10 @@ class RpyParser:
def screen_call(self, c: Cursor, line: Line) -> Node | None: def screen_call(self, c: Cursor, line: Line) -> Node | None:
"""``name(args)`` after `call/show screen`; trailing `nopredict` and `with` are ignored.""" """``name(args)`` after `call/show screen`; trailing `nopredict` and `with` are ignored."""
source = re.split(r"\s+(?:nopredict|with)\b", c.rest(), maxsplit=1)[0].strip() source = re.split(r"\s+(?:nopredict|with)\b", c.rest(), maxsplit=1)[0].strip()
# `onlayer name` only picks the stacking layer; the player keeps every screen in one layer layer_match = re.search(r"\s+onlayer\s+(\w+)\s*$", source)
source = re.sub(r"\s+onlayer\s+\w+\s*$", "", source) layer = layer_match.group(1) if layer_match else None
if layer_match:
source = source[:layer_match.start()]
call = self.expr(source, line) if source else None call = self.expr(source, line) if source else None
if call is None: if call is None:
self.d.warn("Не разобран вызов screen", self.where(line)) self.d.warn("Не разобран вызов screen", self.where(line))
@@ -1480,7 +1484,10 @@ class RpyParser:
self.d.warn("Не разобран вызов screen", self.where(line)) self.d.warn("Не разобран вызов screen", self.where(line))
return None return None
self.use_screen(name) self.use_screen(name)
return {"name": name, "args": args, "kw": kw} found: Node = {"name": name, "args": args, "kw": kw}
if layer:
found["layer"] = layer
return found
def st_scene(self, line: Line) -> list[Node]: def st_scene(self, line: Line) -> list[Node]:
c = self.cursor(line) c = self.cursor(line)
@@ -1521,7 +1528,10 @@ class RpyParser:
name = c.word() name = c.word()
if not name: if not name:
return [] return []
return [{"op": "hide_screen", "name": name}] # hiding a screen that does not exist is harmless hidden: Node = {"op": "hide_screen", "name": name} # hiding a screen that does not exist is harmless
if c.keyword("onlayer"):
hidden["layer"] = c.word()
return [hidden]
spec = self.image_spec(c, line) spec = self.image_spec(c, line)
node: Node = {"op": "hide", "tag": spec.get("tag", spec.get("name", [""])[0])} node: Node = {"op": "hide", "tag": spec.get("tag", spec.get("name", [""])[0])}
self.show_clauses(c, line, node) self.show_clauses(c, line, node)
+4 -5
View File
@@ -94,11 +94,10 @@ BUILDS: dict[str, dict] = {
"id": "renpy/that-new-teacher-0.9.0", "id": "renpy/that-new-teacher-0.9.0",
# The Phone library's own statement parsers (ll.require, ll.keyword ...) run while Ren'Py reads scripts; the # The Phone library's own statement parsers (ll.require, ll.keyword ...) run while Ren'Py reads scripts; the
# converter reads `init phone register` itself (converter/phone_register.py), so the file is not needed # converter reads `init phone register` itself (converter/phone_register.py), so the file is not needed
# the old-save fixer reads Ren'Py's pickled saves (the browser keeps its own), and the text-effect modules draw # the old-save fixer reads Ren'Py's pickled saves (the browser keeps its own)
# letters through the render API: the player has its own {bt} {sc} {fi} {rotat} {glitch} {wave} (text.ts), "skip": ["01statements.rpy", "versioning/save_fixer.rpy",
# and explode/gradient/ruby text is shown plain # the wave tag is a shader on every letter (the player has no shaders yet)
"skip": ["01statements.rpy", "versioning/save_fixer.rpy", "explosion_text.rpy", "glitch_tag.rpy", "wavey_tag.rpy",
"gradient_tags.rpy", "kinetic_text_tags.rpy", "ruby.rpy", "wavey_tag.rpy",
# a shader blur patched into Ren's Transform; the player's own blur property needs no patch # a shader blur patched into Ren's Transform; the player's own blur property needs no patch
"blur.rpy"], "blur.rpy"],
# group_chats.rpy calls `charcter(char)`, a misspelling of its own `character`, in one branch # group_chats.rpy calls `charcter(char)`, a misspelling of its own `character`, in one branch
+11
View File
@@ -259,3 +259,14 @@ def test_input_widget_values_and_focus_actions_are_accepted():
body = data.screens["s"]["body"] body = data.screens["s"]["body"]
assert [n.get("w") for n in body[:3]] == ["input", "input", "textbutton"] assert [n.get("w") for n in body[:3]] == ["input", "input", "textbutton"]
assert "default" in body[1]["props"] and "value" in body[0]["props"] assert "default" in body[1]["props"] and "value" in body[0]["props"]
def test_layers_of_screens_and_camera_are_kept():
data = parse('screen s:\n text "x"\n'
'label start:\n show screen s onlayer hud\n call screen s onlayer overlay\n'
' hide screen s onlayer hud\n camera hud:\n perspective True\n zpos 5\n'
' camera:\n zoom 2\n')
body = data.modules["script.rpy"][0]["body"]
assert [(n["op"], n.get("layer")) for n in body] == [
("show_screen", "hud"), ("call_screen", "overlay"), ("hide_screen", "hud"), ("camera", "hud"), ("camera", None)]
assert body[3]["at"][-1]["props"] == {"perspective": True, "zpos": 5}