feat: File* save/load actions, hashlib/uuid, dict spread and small renpy helpers in the player
CI / frontend (push) Failing after 21s
CI / backend (push) Failing after 36s

Games with their own save screens work: FileAction/FileSave/FileLoad/FileDelete/FilePage*, FileLoadable,
FileTime, FileSaveName, FileJson and friends use browser slots (a slot has no screenshot). Added
renpy.has_image/has_screen/list_files/get_mouse_pos/log/jump_out_of_context/random.seed,
renpy.translation.translate_string, type/id/vars/Flatten/LiveComposite/Composite, bare defaultdict/
deepcopy, md5/sha256 (checked against test vectors), uuid4, time.monotonic, re.compile().fullmatch,
math.isfinite and `{**a, "k": v}`. The converter whitelists them and accepts the hashlib/uuid imports.

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 5c1e1aee01
commit 16ba20816c
8 files changed
+366 -20

No files matched your search

+10 -1
View File
@@ -120,6 +120,10 @@ CALLS |= {"renpy.start_predict", "renpy.stop_predict", "renpy.start_predict_scre
"renpy.ui.imagebutton", "renpy.return_statement"}
from app.games.renpy_native import NATIVE_CLASSES, NATIVE_METHODS # noqa: E402
CALLS |= set(NATIVE_CLASSES)
CALLS |= {"type", "id", "vars", "Flatten", "LiveComposite", "Composite", "defaultdict", "OrderedDict", "deepcopy",
"hashlib.md5", "hashlib.sha256", "uuid.uuid4", "time.monotonic", "re.compile", "math.isfinite", "renpy.log",
"renpy.write_log", "renpy.get_filename_line", "renpy.has_image", "renpy.has_screen", "renpy.list_files",
"renpy.get_mouse_pos", "renpy.jump_out_of_context", "renpy.random.seed", "random.seed"}
CALLS |= {"dir", "datetime.date", "datetime.datetime", "datetime.timedelta", "date", "timedelta",
"renpy.style.rebuild", "renpy.image_size"}
CALLS |= {"map", "filter", "iter", "hash", "hasattr", "getattr", "setattr", "callable", "super"}
@@ -134,6 +138,11 @@ TintMatrix BrightnessMatrix SaturationMatrix ContrastMatrix InvertMatrix Opacity
ColorizeMatrix IdentityMatrix print Exception BaseException Character""".split())
SCREEN_CALLS |= set(NATIVE_CLASSES)
SCREEN_CALLS |= {"SetMixer", "ImageReference"}
# Save/load screens of the game's own (slots are kept in the browser; a slot has no screenshot, see screens.ts)
SCREEN_CALLS |= set("""FileAction FileSave FileLoad FileDelete FileTakeScreenshot FilePage FilePageNext FilePagePrevious
FileLoadable FileUsedSlot FileNewest FileSaveName FileTime FileJson FileScreenshot FileCurrentScreenshot
FileCurrentPage FilePageName FileSlotName""".split())
SCREEN_CALLS |= {"renpy.translation.translate_string"}
# 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"}
@@ -158,7 +167,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 |= {"randint", "randrange", "random", "choice", "choices", "sample", "shuffle", "uniform"}
METHODS |= {"fullmatch", "hexdigest", "randint", "randrange", "random", "choice", "choices", "sample", "shuffle", "uniform"}
def _bound_names(root) -> tuple[set[str], set[str]]:
+4 -4
View File
@@ -25,7 +25,7 @@ UNARY_OPS = {ast.Not: "not", ast.USub: "-", ast.UAdd: "+", ast.Invert: "~"}
# Modules whose import is harmless: the player provides the functions it supports.
KNOWN_MODULES = {"random", "math", "renpy", "store", "time", "datetime", "re", "copy", "collections",
"itertools", "string", "functools", "operator", "json", "gc"}
"itertools", "string", "functools", "operator", "json", "gc", "hashlib", "uuid"}
class Unsupported(Exception):
@@ -146,9 +146,9 @@ def expr(node: ast.AST) -> Expr:
case ast.Tuple(elts=elts):
return {"k": "tuple", "v": [expr(e) for e in elts]}
case ast.Dict(keys=keys, values=values):
if any(k is None for k in keys):
raise Unsupported("распаковка ** в словаре")
return {"k": "dict", "v": [[expr(k), expr(v)] for k, v in zip(keys, values)]}
# `{**other, "k": v}`: a spread entry is a star key with an empty value
return {"k": "dict", "v": [[{"k": "star", "v": expr(v)}, {"k": "const", "v": None}] if k is None
else [expr(k), expr(v)] for k, v in zip(keys, values)]}
case ast.Call(func=func, args=args, keywords=kws):
call = {"k": "call", "f": expr(func),
"a": [{"k": "star", "v": expr(a.value)} if isinstance(a, ast.Starred) else expr(a)