`init -490 screen`, `import x as y` and `from m import A as B`, classes with @renpy.pure, pickle hooks
on classes, `*` items in tuples and lists, generator functions (yield statements; the player runs them
to the end), multi-line expressions that need brackets, Null() and Animation() images, images given as
expressions (`"path/{}.png".format(x)`, f-strings), window statements in init, `offset` in ATL, input
widgets with their values and the focus/confirm actions, `continue` and `break` in screens, `drag`
and DiamondBox-style `fixed` with cell_size, `getattr(...)()` calls, transforms called with arguments,
and the whitelist for the new runtime. JSON files of a game are shipped as data files. Also fixes
blocking_names failing on a call of a call during full conversion.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
262 lines
12 KiB
Python
262 lines
12 KiB
Python
import pytest
|
|
|
|
from app.converter.compatibility import CompatibilityError, audit_ir, require_supported_scripts
|
|
from app.converter.rpy_lexer import parse_tree
|
|
from app.converter.rpy_parser import RpyParser, ScriptData
|
|
|
|
HUD = '''
|
|
style hud_text:
|
|
size 28
|
|
color "#fff"
|
|
unknown_cosmetic_thing 3
|
|
|
|
transform pulse:
|
|
zoom 1.0
|
|
linear 0.5 zoom 1.1
|
|
repeat
|
|
|
|
screen hud(day=1):
|
|
zorder 5
|
|
tag hud
|
|
default open = False
|
|
frame:
|
|
xalign 0.0 yalign 0.0
|
|
background "#0008"
|
|
vbox:
|
|
spacing 4
|
|
text "Day [day]" style "hud_text"
|
|
if money > 10:
|
|
text "Rich" color "#0f0"
|
|
else:
|
|
text "Poor"
|
|
for item in inventory:
|
|
textbutton item action [SetVariable("held", item), Hide("hud")] at pulse
|
|
key "K_h" action ToggleScreenVariable("open")
|
|
timer 1.0 action NullAction() repeat True
|
|
use footer("bye")
|
|
|
|
screen footer(label):
|
|
imagebutton auto "btn_%s.png" action Return(1) hover_sound "tick.ogg"
|
|
|
|
label start:
|
|
$ money = 20
|
|
$ inventory = ["key"]
|
|
show screen hud(2)
|
|
call screen footer("x")
|
|
$ r = _return
|
|
hide screen hud
|
|
return
|
|
'''
|
|
|
|
|
|
def parse(source):
|
|
data = ScriptData()
|
|
RpyParser(data, "script.rpy").parse(source)
|
|
return data
|
|
|
|
|
|
def test_screens_styles_and_calls_become_ir():
|
|
data = parse(HUD)
|
|
assert not data.blockers, data.blockers
|
|
hud = data.screens["hud"]
|
|
assert hud["params"][0]["n"] == "day"
|
|
assert hud["props"]["zorder"] == {"k": "const", "v": 5} and hud["props"]["tag"]["id"] == "hud"
|
|
assert hud["defaults"] == [["open", {"k": "const", "v": False}]]
|
|
frame = hud["body"][0]
|
|
assert frame["w"] == "frame" and frame["props"]["background"]["v"] == "#0008"
|
|
box = frame["body"][0]
|
|
assert box["w"] == "vbox" and [n.get("w") or n["c"] for n in box["body"]] == ["text", "if", "for"]
|
|
button = box["body"][2]["body"][0]
|
|
assert button["w"] == "textbutton" and button["props"]["action"]["k"] == "list"
|
|
assert button["props"]["at"] == [{"props": {"zoom": 1.0}, "name": "pulse", "atl": "pulse"}]
|
|
assert [n.get("c") for n in hud["body"][1:]] == ["key", "timer", "use"]
|
|
assert data.styles["hud_text"]["props"]["size"]["v"] == 28
|
|
assert "unknown_cosmetic_thing" not in data.styles["hud_text"]["props"]
|
|
ops = [n["op"] for label in data.modules["script.rpy"] for n in label["body"] if n["op"].endswith("screen")]
|
|
assert ops == ["show_screen", "call_screen", "hide_screen"]
|
|
call = next(n for label in data.modules["script.rpy"] for n in label["body"] if n["op"] == "call_screen")
|
|
assert call["name"] == "footer" and call["args"][0] == {"k": "const", "v": "x"}
|
|
|
|
|
|
def test_shown_screens_are_audited_but_unshown_ones_are_not():
|
|
data = parse('screen odd():\n frobnicate:\n pass\nlabel start:\n "Hi"\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data) # never shown: harmless
|
|
data = parse('screen odd():\n frobnicate:\n pass\nlabel start:\n call screen odd\n')
|
|
audit_ir(data)
|
|
with pytest.raises(CompatibilityError, match="screen"):
|
|
require_supported_scripts(data)
|
|
|
|
|
|
def test_unknown_actions_and_screens_block_the_import():
|
|
data = parse('screen s():\n textbutton "x" action UnsupportedAction("fullscreen")\nlabel start:\n call screen s\n')
|
|
audit_ir(data)
|
|
with pytest.raises(CompatibilityError, match="UnsupportedAction"):
|
|
require_supported_scripts(data)
|
|
data = parse('label start:\n call screen nowhere\n')
|
|
audit_ir(data)
|
|
with pytest.raises(CompatibilityError, match="screen"):
|
|
require_supported_scripts(data)
|
|
|
|
|
|
def test_screens_opened_from_python_and_by_actions_are_shipped():
|
|
from app.converter.pipeline import _used_screens
|
|
|
|
data = parse(
|
|
'screen a():\n textbutton "x" action Show("b")\nscreen b():\n text "B"\nscreen c():\n text "C"\n'
|
|
'screen d():\n text "D"\nlabel start:\n $ renpy.show_screen("d")\n call screen a\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
assert _used_screens(data) == {"a", "b", "d"}
|
|
|
|
|
|
def test_touch_variants_are_dropped_and_later_definitions_win():
|
|
data = parse(
|
|
'screen s():\n text "one"\nscreen s():\n variant "touch"\n text "touch"\n'
|
|
'screen t():\n text "one"\nscreen t():\n text "two"\nlabel start:\n "x"\n')
|
|
assert data.screens["s"]["body"][0]["args"][0]["v"] == "one"
|
|
assert data.screens["t"]["body"][0]["args"][0]["v"] == "two"
|
|
|
|
|
|
def test_lexer_keeps_screen_blocks_nested():
|
|
lines = parse_tree("screen s():\n vbox:\n text \"a\"\n")
|
|
assert lines[0].block[0].block[0].text.strip().startswith("text")
|
|
|
|
|
|
def test_decompiled_forms_operators_glued_properties_has_transclude_and_inline_atl():
|
|
data = parse(
|
|
'transform blink(delay, cycle=1.0):\n alpha 0.0\n pause delay\n linear cycle alpha 1.0\n'
|
|
'screen card(title):\n frame:\n has vbox\n text title\n transclude\n'
|
|
'screen deck(day):\n use card("T"):\n text "Day " + str(day) size 20\n'
|
|
' text "x" pos(5, 6) xysize(10,20)\n'
|
|
' add "bg.png":\n alpha 0.0\n linear 0.5 alpha 1.0\n'
|
|
' add "dot.png" at blink(0.2, 0.3)\n'
|
|
' on "show" action NullAction()\n'
|
|
'label start:\n call screen deck(1)\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
deck = data.screens["deck"]["body"]
|
|
use = deck[0]
|
|
assert use["c"] == "use" and use["body"][0]["args"][0]["k"] == "bin"
|
|
assert use["body"][0]["props"]["size"]["v"] == 20
|
|
card = data.screens["card"]["body"][0]
|
|
assert card["w"] == "frame" and [n.get("w") or n["c"] for n in card["body"][0]["body"]] == ["text", "transclude"]
|
|
assert deck[1]["props"]["pos"]["k"] == "tuple" and deck[1]["props"]["xysize"]["k"] == "tuple"
|
|
assert deck[2]["props"]["alpha"] == {"k": "const", "v": 0.0}
|
|
animated = deck[2]["props"]["at"][0]
|
|
assert animated["atl"]["steps"][-1]["op"] == "move" and animated["atl"]["steps"][-1]["props"] == {"alpha": 1.0}
|
|
called = deck[3]["props"]["at"][0]
|
|
assert called["name"] == "blink" and called["atl"] == "blink" and len(called["args"]) == 2
|
|
blink = data.atl["blink"]
|
|
assert [p["n"] for p in blink["params"]] == ["delay", "cycle"]
|
|
assert blink["steps"][1] == {"op": "pause", "t": {"e": {"k": "name", "id": "delay"}}}
|
|
assert deck[4] == {"c": "on", "event": "show", "action": {"k": "call", "f": {"k": "name", "id": "NullAction"}, "a": [], "kw": {}}}
|
|
|
|
|
|
def test_computed_atl_values_make_a_transform_animated():
|
|
data = parse('transform shift(x):\n xoffset x\nlabel start:\n "Hi"\n')
|
|
block = data.atl["shift"]
|
|
assert block["params"][0]["n"] == "x"
|
|
assert block["steps"][0]["props"]["xoffset"] == {"e": {"k": "name", "id": "x"}}
|
|
assert data.transforms["shift"] == {}
|
|
|
|
|
|
def test_a_closing_quote_ends_the_word_and_text_pictures_are_understood():
|
|
data = parse(
|
|
'screen hud():\n text "D [a] A [b]"size 15\n text "x" size 20\n'
|
|
'image log_line:\n Text(log.message())\n alpha 0.0\n linear 0.5 alpha 1.0\n'
|
|
'label start:\n call screen hud\n')
|
|
first = data.screens["hud"]["body"][0]
|
|
assert first["props"]["size"] == {"k": "const", "v": 15}
|
|
assert data.images["log_line"]["textExpr"]["k"] == "call"
|
|
|
|
|
|
def test_mousearea_is_a_leaf_with_hover_actions():
|
|
data = parse('screen s():\n mousearea:\n xysize (100, 50)\n hovered SetVariable("h", True)\n'
|
|
' unhovered SetVariable("h", False)\nlabel start:\n call screen s\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
node = data.screens["s"]["body"][0]
|
|
assert node["w"] == "mousearea" and {"hovered", "unhovered", "xysize"} <= set(node["props"])
|
|
|
|
|
|
def test_at_picks_a_transform_by_condition_and_atl_commas_split_statements():
|
|
data = parse('transform grow:\n zoom 1.0\n on selected:\n zoom 1.2, 0.2\n on deselected:\n'
|
|
' zoom 1.0, 0.2\ntransform still:\n zoom 1.0\n'
|
|
'screen s(flag):\n frame at (grow if flag else still):\n text "x"\n'
|
|
'label start:\n call screen s(True)\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
node = data.screens["s"]["body"][0]
|
|
choice = node["props"]["at"][0]["choose"]
|
|
assert choice["then"]["name"] == "grow" and choice["else"]["name"] == "still"
|
|
steps = data.atl["grow"]["steps"]
|
|
handler = steps[1]["block"]["steps"]
|
|
assert [s["op"] for s in handler] == ["set", "pause"] and handler[1]["t"] == 0.2
|
|
assert steps[1]["events"] == ["selected"] and steps[2]["events"] == ["deselected"]
|
|
|
|
|
|
def test_atl_warper_with_duration_in_brackets():
|
|
data = parse('transform t:\n easein(1.0) zoom 1.3 ypos 1.0\n linear(0.5) xpos 0.2\n')
|
|
steps = data.atl["t"]["steps"] if "t" in data.atl else data.transforms["t"]
|
|
assert not data.blockers
|
|
assert [(s["warp"], s["t"]) for s in steps if s["op"] == "move"] == [("easein", 1.0), ("linear", 0.5)]
|
|
|
|
|
|
def test_atl_warper_brackets_glued_to_the_property():
|
|
data = parse('transform t:\n ease(0.1)rotate 5\n easein(0.4)xpos 0.3\n')
|
|
steps = data.atl["t"]["steps"] if "t" in data.atl else data.transforms["t"]
|
|
assert not data.blockers
|
|
assert [(s["t"], sorted(s["props"])) for s in steps if s["op"] == "move"] == [(0.1, ["rotate"]), (0.4, ["xpos"])]
|
|
|
|
|
|
def test_atl_bracketed_durations_offset_and_gpu_hints():
|
|
data = parse('transform t:\n ease (0.1)xpos 0.503\n linear 0.2 offset (3, 4)\n pause (0.8)\n'
|
|
' shader "x"\n u_width 3.0\n transform_anchor True\n xpos 1.0\n')
|
|
steps = data.atl["t"]["steps"] if "t" in data.atl else data.transforms["t"]
|
|
assert not data.blockers
|
|
moves = [s for s in steps if s["op"] == "move"]
|
|
assert [m["t"] for m in moves] == [0.1, 0.2]
|
|
assert moves[0]["props"] == {"xpos": 0.503} and moves[1]["props"] == {"xoffset": 3, "yoffset": 4}
|
|
assert any(s["op"] == "pause" and s["t"] == 0.8 for s in steps)
|
|
|
|
|
|
def test_screen_for_index_glued_strings_and_onlayer():
|
|
data = parse('screen s(items):\n for item index item.id in items:\n text"[item]"\n add"cover_0"\n'
|
|
'label start:\n show screen s(items=[1]) onlayer hud\n call screen s([2]) onlayer overlay\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
body = data.screens["s"]["body"]
|
|
assert body[0]["c"] == "for" and body[0]["body"][0]["w"] == "text" and body[1]["w"] == "add"
|
|
|
|
|
|
def test_atl_pause_with_brackets():
|
|
data = parse('transform t:\n ease(1.0) alpha 0.1\n pause(1.5)\n ease(1.0) alpha 0.8\n')
|
|
steps = data.atl["t"]["steps"] if "t" in data.atl else data.transforms["t"]
|
|
assert not data.blockers
|
|
assert [(s["op"], s["t"]) for s in steps] == [("move", 1.0), ("pause", 1.5), ("move", 1.0)]
|
|
|
|
|
|
def test_dynamic_at_value_and_draggable_box():
|
|
data = parse('transform spin:\n rotate 5\nscreen s(icon):\n imagebutton:\n idle "a" at icon["at"]\n'
|
|
' drag:\n drag_name "box"\n draggable True\n text "x"\n'
|
|
'label start:\n call screen s({"at": spin})\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
body = data.screens["s"]["body"]
|
|
assert body[0]["props"]["at"][0]["dyn"]["k"] == "sub"
|
|
assert body[1]["w"] == "drag" and "draggable" in body[1]["props"]
|
|
|
|
|
|
def test_input_widget_values_and_focus_actions_are_accepted():
|
|
data = parse('screen s():\n default name_value = VariableInputValue("name")\n input:\n id "player_name_input"\n'
|
|
' value name_value\n length 12\n input:\n default "Bob"\n allow "ab"\n'
|
|
' textbutton "x" action [name_value.Toggle(), Confirm("sure?", yes=NullAction()), CaptureFocus("tip")]\n'
|
|
' for i in [1, 2]:\n if i == 1:\n continue\n text "[i]"\n'
|
|
'label start:\n call screen s\n')
|
|
audit_ir(data)
|
|
require_supported_scripts(data)
|
|
body = data.screens["s"]["body"]
|
|
assert [n.get("w") for n in body[:3]] == ["input", "input", "textbutton"]
|
|
assert "default" in body[1]["props"] and "value" in body[0]["props"]
|