feat: converter accepts the constructs of goodluke and SaintlyCorruption
`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>
This commit is contained in:
1 parent
f2fcd05467
commit
e66160cffa
12 files changed
+176
-31
No files matched your search
@@ -80,7 +80,7 @@ def test_renpy_only_declarations_are_not_execution_blockers(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source,reason", [
|
||||
('init python:\n def helper():\n yield 1\nlabel start:\n $ helper()', "init python"),
|
||||
('init python:\n def helper():\n received = yield 1\nlabel start:\n $ helper()', "init python"),
|
||||
('label start:\n $ result = renpy.show_displayable("map")', "renpy.show_displayable"),
|
||||
('label start:\n jump missing', "missing"),
|
||||
])
|
||||
@@ -362,3 +362,14 @@ def test_upload_rejects_unsupported_engine_without_queueing(tmp_path, monkeypatc
|
||||
assert not upload_path(123).exists()
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_blocking_names_survives_a_call_of_a_call():
|
||||
from app.converter.compatibility import blocking_names
|
||||
|
||||
data = ScriptData()
|
||||
source = ('init python:\n def helper():\n return getattr(renpy.store, "x", lambda: 1)()\n'
|
||||
'label start:\n $ helper()\n')
|
||||
data.prescan(source)
|
||||
RpyParser(data, "t.rpy").parse(source)
|
||||
assert blocking_names(data) == []
|
||||
@@ -57,10 +57,10 @@ def test_if_chain_and_menu():
|
||||
|
||||
|
||||
def test_unsupported_python_is_reported_not_fatal():
|
||||
d = parse("init python:\n def f():\n yield 1\n x = 1\nlabel start:\n $ y = x\n")
|
||||
d = parse("init python:\n def f():\n received = yield 1\n x = 1\nlabel start:\n $ y = x\n")
|
||||
assert d.init_code[0]["op"] == "set" and "f" in d.deferred # reported once something calls it
|
||||
from app.converter.compatibility import audit_ir
|
||||
d = parse("init python:\n def f():\n yield 1\nlabel start:\n $ f()\n")
|
||||
d = parse("init python:\n def f():\n received = yield 1\nlabel start:\n $ f()\n")
|
||||
audit_ir(d)
|
||||
assert d.blockers
|
||||
|
||||
|
||||
@@ -235,3 +235,33 @@ def test_init_if_has_live2d_keeps_only_the_else_branch_and_crop_call_glued_to_a_
|
||||
def test_static_atl_line_with_a_spaced_crop_tuple():
|
||||
data = parse('image plap:\n "base"\n crop (1697,54, 172, 94)\nimage slurp:\n "base"\n crop (1, 2, 3, 4) xanchor 0.5\n')
|
||||
assert not data.blockers
|
||||
|
||||
|
||||
def test_priority_screen_continue_and_offset_with_computed_halves():
|
||||
data = parse('init -490 screen s(items):\n for item in items:\n if item == 1:\n continue\n text "[item]"\n'
|
||||
'transform lift:\n linear 0.2 offset (0, -LIFT)\nlabel start:\n call screen s([1, 2])\n')
|
||||
assert "s" in data.screens
|
||||
loop = data.screens["s"]["body"][0]
|
||||
assert loop["c"] == "for" and loop["body"][0]["branches"][0]["body"] == [{"c": "continue"}]
|
||||
assert not data.blockers
|
||||
|
||||
|
||||
def test_import_alias_decorated_class_null_image_window_in_init_and_offset_line():
|
||||
from app.converter import pyexpr
|
||||
|
||||
code = pyexpr.parse_stmts("import time as stamp\nfrom renpy.text.text import Text as RenpyText\n"
|
||||
"@renpy.pure\nclass Typing(VariableInputValue):\n pass\n")
|
||||
assert code[0] == {"op": "set", "t": {"k": "name", "id": "stamp"}, "v": {"k": "name", "id": "time"}}
|
||||
assert code[1]["v"] == {"k": "name", "id": "Text"} and code[2]["op"] == "class"
|
||||
data = parse('init 1:\n image blank = Null()\ninit -1:\n window hide\ntransform t:\n offset (0, 0)\n on idle:\n linear 0.2 offset (1, 2)\n')
|
||||
assert data.images["blank"] == {"color": "#00000000"} and not data.blockers
|
||||
|
||||
|
||||
def test_starred_tuple_items_and_pickle_hooks_are_accepted():
|
||||
from app import pgvn
|
||||
from app.converter import pyexpr
|
||||
|
||||
code = pyexpr.parse_stmts("pair = (first, *rest)\nclass Scene:\n def __setstate__(self, state):\n pass\n def run(self):\n return 1\n")
|
||||
pgvn.statements(code)
|
||||
assert code[0]["v"]["v"][1] == {"k": "star", "v": {"k": "name", "id": "rest"}}
|
||||
assert [m["name"] for m in code[1]["body"]] == ["run"]
|
||||
@@ -246,3 +246,16 @@ def test_dynamic_at_value_and_draggable_box():
|
||||
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"]
|
||||
Reference in new issue
Block a user