feat: eval/exec of strings, gallery Replay, callable actions; iNSight and SomethingBetter now pass

- pyparse.ts: parser for expressions and simple statements built at run time, producing
  the sandbox's own IR; backs eval() and exec() (e.g. exec("persistent." + var + " = 0"))
- Replay/EndReplay and renpy.end_replay: the engine stashes the full game state, runs the
  label with the given scope and restores everything when it ends; locked replays are
  insensitive. Save loading shared as applySave()
- Screen actions are callable (MainMenu(confirm=False)())
- jump main_menu_screen is Ren'Py's own label: it opens the player's main menu
- Screens: a closing quote ends the word before a letter ("text"size 15) but not before
  an operator ("a"+b); Text(...) pictures in image blocks
- README: current capabilities and limits of the Ren'Py support

Strict compatibility check on the local collection: Tomie, Twisted-Thoughts, Hooked,
iNSight-of-you and SomethingBetter pass; TheFixer (custom AlphaMask wardrobe, missing
labels) and That_New_Teacher (custom statements, Render API) do not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 14:40:36 +03:00
1 parent 3923ee72fb
commit 3ac4f0371f
11 files changed
+448 -36

No files matched your search

+19 -3
View File
@@ -38,6 +38,8 @@ export interface ScreenHost {
openUrl(url: string): void;
quickSave(): void;
quickLoad(): void;
startReplay(label: string, scope: Record<string, any> | null): void;
endReplay(): void;
setLanguage(code: string | null): void;
transform(name: string): Record<string, any> | null;
atl(name: string): AtlBlock | null;
@@ -49,6 +51,8 @@ class ActionAbort extends Error {}
/** A screen action such as Jump("x"); run when the player activates a widget. */
export class ScreenAction {
/** Actions are callable in Ren'Py: `MainMenu(confirm=False)()` performs the action at once. */
__call__: () => null = () => null;
constructor(public kind: string, public args: any[], public kw: Props, public screen: Instance | null) {}
}
@@ -1084,7 +1088,8 @@ export class ScreenManager {
private actionSensitive(action: any): boolean {
const list = Array.isArray(action) ? action : [action];
return list.every((a) => !(a instanceof ScreenAction) || a.kind !== "SensitiveIf" || truthy(a.args[0]));
return list.every((a) => !(a instanceof ScreenAction) ||
(a.kind === "SensitiveIf" ? truthy(a.args[0]) : a.kind === "Replay" ? !truthy(a.kw.locked ?? a.args[2]) : true));
}
private actionSelected(action: any): boolean {
@@ -1169,6 +1174,12 @@ export class ScreenManager {
}
case "SetLocalVariable":
case "SetScreenVariable": owner?.vars.set(a, b); return;
case "Replay": {
const scope = action.kw.scope ?? b;
this.host.startReplay(String(a), scope instanceof PyDict ? Object.fromEntries([...scope.map.entries()].map(([k, v]) => [String(k), v])) : null);
throw new ActionAbort();
}
case "EndReplay": this.host.endReplay(); throw new ActionAbort();
case "QuickSave": this.host.quickSave(); return;
case "QuickLoad": this.host.quickLoad(); throw new ActionAbort();
case "ToggleScreenVariable": {
@@ -1217,13 +1228,18 @@ export class ScreenManager {
const make = (kind: string) => (...args: any[]) => {
const last = args[args.length - 1];
const hasKw = last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw");
return new ScreenAction(kind, hasKw ? args.slice(0, -1) : args, hasKw ? last.__kw : {}, this.rendering);
const action = new ScreenAction(kind, hasKw ? args.slice(0, -1) : args, hasKw ? last.__kw : {}, this.rendering);
action.__call__ = () => {
this.run(action, action.screen);
return null;
};
return action;
};
const out: Record<string, any> = {};
for (const kind of ["Jump", "Call", "Return", "SetVariable", "ToggleVariable", "SetDict", "SetField", "ToggleField",
"SetScreenVariable", "ToggleScreenVariable", "Show", "Hide", "ShowTransient", "Function", "NullAction", "Play",
"Queue", "Stop", "Notify", "Quit", "MainMenu", "SelectedIf", "SensitiveIf", "ToggleScreen", "Language", "ShowMenu",
"OpenURL", "SetLocalVariable", "QuickSave", "QuickLoad"]) out[kind] = make(kind);
"OpenURL", "SetLocalVariable", "QuickSave", "QuickLoad", "Replay", "EndReplay"]) out[kind] = make(kind);
const shape = (kind: string) => (...args: any[]) => {
const last = args[args.length - 1];
const hasKw = last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw");