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:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 17:28:13 +03:00
1 parent f2fcd05467
commit e66160cffa
12 files changed
+176 -31

No files matched your search

+20 -4
View File
@@ -124,6 +124,15 @@ CALLS |= set(NATIVE_CLASSES)
CALLS |= {"renpy.Render", "renpy.render", "renpy.redraw", "renpy.displayable", "renpy.IgnoreEvent",
"renpy.display.layout.LiveComposite", "renpy.display.layout.Fixed", "pygame.key.get_mods",
"pygame.mouse.get_pressed", "pygame.mouse.get_pos"}
CALLS |= {"ConditionSwitch", "DynamicImage", "renpy.register_sl_displayable", "renpy.capture_focus", "renpy.clear_capture_focus", "renpy.get_widget", "renpy.display.focus.set_focused",
"renpy.file", "renpy.open_file", "loads", "dumps", "json.loads", "json.dumps", "Animation", "im.matrix", "renpy.context", "renpy.call_in_new_context", "renpy.get_focus_rect", "gui.rebuild", "FontGroup", "RenpyText",
"MatrixColor", "im.Image", "im.AlphaMask", "GLMovie", "renpy.display.im.Image", "colorsys.rgb_to_hls",
"colorsys.hls_to_rgb"}
CALLS |= {"next", "repr", "dict.fromkeys", "colorsys.rgb_to_hsv", "colorsys.hsv_to_rgb", "fnmatch.fnmatch",
"traceback.format_exc", "traceback.print_exc", "math.log1p", "math.expm1", "renpy.in_rollback",
"renpy.is_in_test", "renpy.retain_after_load", "renpy.context_nesting_level", "renpy.current_screen",
"renpy.substitutions.substitute", "renpy.set_screen_variable", "renpy.music.register_channel",
"renpy.music.get_loop", "renpy.music.get_channel"}
CALLS |= {"Matrix", "renpy.has_live2d", "renpy.fetch", "renpy.display.draw.kill_textures",
"renpy.display.render.free_memory", "cache.get_total_size"}
CALLS |= {"Move", "absolute", "type", "id", "vars", "Flatten", "LiveComposite", "Composite", "defaultdict", "OrderedDict", "deepcopy",
@@ -149,6 +158,10 @@ SCREEN_CALLS |= set("""FileAction FileSave FileLoad FileDelete FileTakeScreensho
FileLoadable FileUsedSlot FileNewest FileSaveName FileTime FileJson FileScreenshot FileCurrentScreenshot
FileCurrentPage FilePageName FileSlotName""".split())
SCREEN_CALLS |= {"renpy.translation.translate_string"}
# Text fields and the actions around them (see InputValue in screens.ts)
SCREEN_CALLS |= set("""VariableInputValue ScreenVariableInputValue FieldInputValue DictInputValue FilePageNameInputValue
CurrentScreenName CaptureFocus ClearFocus InvertSelected UploadSync DownloadSync Confirm""".split())
METHODS_INPUT = {"Enable", "Disable", "Toggle"}
# Screen language helpers for audio, focus and pictures that play (see screens.ts)
SCREEN_CALLS |= {"Movie", "MixerValue", "AudioPositionValue", "PauseAudio", "ToggleSetMembership",
"GetFocusRect", "ToggleFocus", "XScrollValue", "YScrollValue", "Preference", "Start"}
@@ -173,7 +186,7 @@ METHODS = set("append extend insert pop remove index count reverse sort clear co
METHODS |= NATIVE_METHODS
# Methods of generators made by `renpy.random.Random()` / `random.Random()`
METHODS |= {"blit", "subpixel_blit", "fill", "canvas", "get_size", "add_shader", "add_uniform", "add_property",
METHODS |= {"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"}
METHODS |= {"fullmatch", "hexdigest", "randint", "randrange", "random", "choice", "choices", "sample", "shuffle", "uniform"}
@@ -380,7 +393,8 @@ def blocking_names(data) -> list[str]:
elif isinstance(value, dict):
if value.get("k") == "call":
function = value["f"]
last = function.get("id") if function.get("k") == "name" else function.get("a")
last = function.get("id") if function.get("k") == "name" else (
function.get("a") if function.get("k") == "attr" else None) # `f()()` has neither
dotted = (function["o"]["id"] + "." + function["a"]) if (function.get("k") == "attr" and
function["o"].get("k") == "name") else last
if dotted in waiting or last in known or last in waiting:
@@ -504,6 +518,7 @@ def audit_ir(data) -> None:
list(used_screens.values())])
user_names |= {name for name, _ in data.defaults}
user_names |= set(data.characters) | {"narrator"}
user_names |= set(data.transforms) | set(data.atl) # `store.move_in(0.3)`: a transform called with arguments
def dotted(value):
if value.get("k") == "name":
@@ -524,8 +539,9 @@ def audit_ir(data) -> None:
and (function.get("a") in METHODS or function.get("a") in user_methods
or function.get("a") in user_names))
# `MainMenu(confirm=False)()`: calling the action a screen action constructor returned
if function.get("k") == "call" and dotted(function.get("f", {})) in SCREEN_CALLS:
is_method = True
if function.get("k") == "call" and (dotted(function.get("f", {})) in SCREEN_CALLS
or dotted(function.get("f", {})) == "getattr"):
is_method = True # an action a constructor returned, or `getattr(obj, "f", fallback)()`
# `globals()[who + "_here"](...)`: a function picked by name; some function must carry that ending
if function.get("k") == "sub" and function["o"].get("k") == "call" \
and dotted(function["o"]["f"]) == "globals":