feat: ATL durations in brackets, offset and GPU hints; screen for-index, glued strings and onlayer

ATL: `ease(1.0) zoom 1.3`, `ease (0.1)xpos 0.5`, `ease(0.1)rotate 5` and `pause(1.5)` take the duration
in brackets; `offset (x, y)` becomes xoffset/yoffset; lines starting with shader, mesh, u_*, blend or
transform_anchor are skipped as GPU hints. thelewdesthouse drops from about 1400 to 111 rejections.
Screens: `for x index k in seq`, `add"x"` without a space, and `onlayer name` after show/call screen
(the layer is ignored, every screen lives in one layer). Transforms named with a capital letter and
declared in another file are resolved by the prescan, and `__("Name") "text"` is a translatable speaker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 04:02:22 +03:00
1 parent 25bd39dd43
commit 5c1e1aee01
4 files changed
+84 -5

No files matched your search

+30 -3
View File
@@ -38,11 +38,12 @@ GLUED = re.compile(r"^([a-z_][a-z_0-9]*)(\(.*\))$", re.S)
# Warpers differ only in easing shape; the player implements these four curves and
# maps the named variants (easein_quad, ease_back, ...) onto the closest one.
WARPER = re.compile(r"^(linear|ease|easein|easeout|easeinout)(_(quad|cubic|quart|quint|expo|circ|back|bounce|elastic|sine))?$")
WARPER_CALL = re.compile(r"^(pause|(?:linear|ease|easein|easeout|easeinout)(?:_(?:quad|cubic|quart|quint|expo|circ|back|bounce|elastic|sine))?)\(")
LEADING_ZERO = re.compile(r"(?<![\w.])0+(\d)(?![\d.]*[\w.])")
SPLIT_SIGN = re.compile(r"^([a-z_]+)(-[\d.]+)$")
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
COSMETIC = ("shader", "mesh", "gl_", "u_", "blend", "additive", "perspective", "point_to", "xrotate", "yrotate",
COSMETIC = ("transform_anchor", "shader", "mesh", "gl_", "u_", "blend", "additive", "perspective", "point_to", "xrotate", "yrotate",
"zrotate", "zpos", "zzoom", "delay", "events", "fit", "nearest", "fps", "show_cancels_hide")
NAMED_POSITIONS = {
"left": {"xpos": 0.0, "xanchor": 0.0}, "right": {"xpos": 1.0, "xanchor": 1.0},
@@ -195,7 +196,7 @@ class AtlParser:
if key in IGNORED or key.startswith(COSMETIC):
_, i = read_value(parts, i + 1) if i + 1 < len(parts) else ("", i + 1)
continue
if key not in POSITION_PROPS:
if key not in POSITION_PROPS and key != "offset":
self.warn(f"Не поддерживается ATL: свойство {key}", line)
return out
if i + 1 >= len(parts):
@@ -219,11 +220,27 @@ class AtlParser:
if key == "xycenter" and isinstance(value, (list, tuple)) and len(value) == 2:
out["xcenter"], out["ycenter"] = value
continue
if key == "offset": # `offset (x, y)` is xoffset and yoffset together
if isinstance(value, (list, tuple)) and len(value) == 2:
out["xoffset"], out["yoffset"] = value
else:
self.warn("Не поддерживается ATL: offset не пара чисел", line)
continue
out[key] = list(value) if isinstance(value, tuple) else value
return out
# --- block ----------------------------------------------------------------------
@staticmethod
def split_bracket(token: str) -> list[str]:
"""`(0.1)xpos` -> `(0.1)`, `xpos`: a bracketed duration with the property glued to it."""
depth = 0
for at, ch in enumerate(token):
depth += (ch == "(") - (ch == ")")
if depth == 0:
return [token[:at + 1], token[at + 1:]] if token[at + 1:] else [token]
return [token]
def expand(self, lines: list[Line]) -> list[Line]:
"""Lines holding several comma-separated statements become one line per statement."""
out: list[Line] = []
@@ -249,6 +266,16 @@ class AtlParser:
if split := SPLIT_SIGN.match(parts[0]): # decompiled `xzoom-1`
if split[1] in POSITION_PROPS:
parts = [split[1], split[2], *parts[1:]]
if called := WARPER_CALL.match(parts[0]): # `easein(1.0) zoom 1.3`: the duration in brackets
depth = 0
for at, ch in enumerate(parts[0][called.end() - 1:], called.end() - 1):
depth += (ch == "(") - (ch == ")")
if depth == 0:
break
glued = [parts[0][at + 1:]] if parts[0][at + 1:] else [] # `ease(0.1)rotate 5`
parts = [called[1], parts[0][called.end():at], *glued, *parts[1:]]
if len(parts) > 1 and (WARPER.match(parts[0]) or parts[0] == "pause") and parts[1].startswith("("):
parts = [parts[0], *self.split_bracket(parts[1]), *parts[2:]] # `ease (0.1)xpos 0.5`
first = parts[0]
i += 1
picture = self.image(line) if self.image and not line.block else None
@@ -266,7 +293,7 @@ class AtlParser:
i = self.compound(first, parts, lines, i - 1, steps)
elif WARPER.match(first):
self.move(first, parts, line, steps)
elif first in IGNORED:
elif first in IGNORED or first.startswith(COSMETIC):
continue
elif first in NAMED_POSITIONS:
props = dict(NAMED_POSITIONS[first])