feat: File* save/load actions, hashlib/uuid, dict spread and small renpy helpers in the player
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:
1 parent
5c1e1aee01
commit
16ba20816c
8 files changed
+366
-20
No files matched your search
@@ -8,6 +8,7 @@ import { isNativeAction } from "./musicroom";
|
||||
import { galleryClass } from "./gallery";
|
||||
import { parseExpression } from "./pyparse";
|
||||
import { movieDisplayable } from "./movie";
|
||||
import { strftime } from "./pymodules";
|
||||
import { PyDict, PyInstance, callableOf, pyEq, pyStr, truthy, type Python } from "./python";
|
||||
import type { AtSpec, Expr } from "./types";
|
||||
|
||||
@@ -43,6 +44,13 @@ export interface ScreenHost {
|
||||
openUrl(url: string): void;
|
||||
quickSave(): void;
|
||||
quickLoad(): void;
|
||||
/** Slot metadata for File* actions; null when empty or not yet read. */
|
||||
slotInfo(slot: string): { title: string; time: string } | null;
|
||||
saveSlot(slot: string, confirm: boolean): void;
|
||||
loadSlot(slot: string): void;
|
||||
deleteSlot(slot: string, confirm: boolean): void;
|
||||
/** The page of the save/load screen (`persistent._file_page`). */
|
||||
filePage: string;
|
||||
startReplay(label: string, scope: Record<string, any> | null): void;
|
||||
endReplay(): void;
|
||||
setLanguage(code: string | null): void;
|
||||
@@ -1406,6 +1414,11 @@ export class ScreenManager {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** The save slot a File* action means: its name on the current (or the given) page. */
|
||||
private slotId(name: any, page?: any): string {
|
||||
return `file:${page ?? this.host.filePage}-${pyStr(name)}`;
|
||||
}
|
||||
|
||||
private perform(action: any, screen: Instance | null) {
|
||||
if (action === null || action === undefined || action === false) return;
|
||||
if (Array.isArray(action)) {
|
||||
@@ -1454,6 +1467,33 @@ export class ScreenManager {
|
||||
throw new ActionAbort();
|
||||
}
|
||||
case "EndReplay": this.host.endReplay(); throw new ActionAbort();
|
||||
case "FileAction":
|
||||
case "FileSave":
|
||||
case "FileLoad": {
|
||||
const slot = this.slotId(a, action.kw.page);
|
||||
const loading = action.kind === "FileLoad" || (action.kind === "FileAction" && /load/i.test(owner?.name ?? ""));
|
||||
const sure = truthy(action.kw.confirm ?? (action.kind === "FileLoad" ? b : false) ?? false);
|
||||
if (loading) { this.host.loadSlot(slot); throw new ActionAbort(); }
|
||||
this.host.saveSlot(slot, sure);
|
||||
return;
|
||||
}
|
||||
case "FileDelete": this.host.deleteSlot(this.slotId(a, action.kw.page), truthy(action.kw.confirm ?? true)); return;
|
||||
case "FileTakeScreenshot": return;
|
||||
case "FilePage": this.host.filePage = String(a); return;
|
||||
case "FilePageNext":
|
||||
case "FilePagePrevious": {
|
||||
const kw = action.kw;
|
||||
const max = typeof kw.max === "number" ? kw.max : typeof a === "number" ? a : null;
|
||||
const order = [...(kw.auto ?? true ? ["auto"] : []), ...(kw.quick ?? true ? ["quick"] : [])];
|
||||
const numbered = max === null ? 0 : max;
|
||||
const pages = [...order, ...Array.from({ length: numbered || 99 }, (_, i) => String(i + 1))];
|
||||
const here = pages.indexOf(this.host.filePage);
|
||||
const step = action.kind === "FilePageNext" ? 1 : -1;
|
||||
let next = (here < 0 ? pages.indexOf("1") : here) + step;
|
||||
if (next < 0 || next >= pages.length) next = truthy(kw.wrap) ? (next + pages.length) % pages.length : Math.max(0, Math.min(pages.length - 1, next));
|
||||
this.host.filePage = pages[next];
|
||||
return;
|
||||
}
|
||||
case "QuickSave": this.host.quickSave(); return;
|
||||
case "QuickLoad": this.host.quickLoad(); throw new ActionAbort();
|
||||
case "ToggleScreenVariable": {
|
||||
@@ -1532,7 +1572,37 @@ export class ScreenManager {
|
||||
"SetScreenVariable", "ToggleScreenVariable", "Show", "Hide", "ShowTransient", "Function", "NullAction", "Play",
|
||||
"Queue", "Stop", "Notify", "Quit", "MainMenu", "SelectedIf", "SensitiveIf", "ToggleScreen", "Language", "ShowMenu",
|
||||
"OpenURL", "SetLocalVariable", "QuickSave", "QuickLoad", "Replay", "EndReplay", "ToggleSetMembership",
|
||||
"ToggleFocus", "PauseAudio", "SetMixer"]) out[kind] = make(kind);
|
||||
"ToggleFocus", "PauseAudio", "SetMixer", "FileAction", "FileSave", "FileLoad", "FileDelete", "FileTakeScreenshot",
|
||||
"FilePage", "FilePageNext", "FilePagePrevious"]) out[kind] = make(kind);
|
||||
// values of the save/load screens; screenshots are not stored, so a slot has none
|
||||
const kwOf = (a: any[]) => (a.length && a[a.length - 1]?.__kw) || {};
|
||||
const slotOf = (a: any[]) => this.slotId(a[0], kwOf(a).page);
|
||||
out.FileLoadable = (...a: any[]) => this.host.slotInfo(slotOf(a)) !== null;
|
||||
out.FileUsedSlot = out.FileLoadable;
|
||||
out.FileNewest = () => false;
|
||||
out.FileSaveName = (...a: any[]) => this.host.slotInfo(slotOf(a))?.title ?? "";
|
||||
out.FileTime = (...a: any[]) => {
|
||||
const kw = kwOf(a), info = this.host.slotInfo(slotOf(a));
|
||||
if (!info) return kw.empty ?? a[2] ?? "";
|
||||
return strftime(String(kw.format ?? (typeof a[1] === "string" ? a[1] : "%b %d, %H:%M")), new Date(info.time));
|
||||
};
|
||||
out.FileJson = (...a: any[]) => {
|
||||
const kw = kwOf(a), info = this.host.slotInfo(slotOf(a));
|
||||
if (!info) return kw.empty ?? null;
|
||||
return kw.key !== undefined ? kw.missing ?? null : new PyDict();
|
||||
};
|
||||
out.FileScreenshot = (...a: any[]) => kwOf(a).empty ?? null;
|
||||
out.FileCurrentScreenshot = (...a: any[]) => kwOf(a).empty ?? null;
|
||||
out.FileCurrentPage = () => this.host.filePage;
|
||||
out.FilePageName = (...a: any[]) => {
|
||||
const kw = kwOf(a), page = this.host.filePage;
|
||||
return page === "auto" ? kw.auto ?? a[0] ?? "Auto " : page === "quick" ? kw.quick ?? a[1] ?? "Quick " : page;
|
||||
};
|
||||
out.FileSlotName = (slot: any, perPage: any) => {
|
||||
const n = Number(slot) - 1, size = Number(perPage) || 1;
|
||||
const page = Math.floor(n / size) + 1;
|
||||
return `${page}-${(n % size) + 1}`;
|
||||
};
|
||||
out.GetFocusRect = (id: any) => (this.focus?.id === pyStr(id) ? this.focus.rect : null);
|
||||
out.MixerValue = (mixer: any) => {
|
||||
return new BarValue(() => this.host.audio.mixerVolume(String(mixer)), () => 1, (v) => this.host.setMixer(String(mixer), v));
|
||||
@@ -1589,6 +1659,9 @@ export class ScreenManager {
|
||||
{ ...properties, idle: image, insensitive: image, sensitive: enabled, action }),
|
||||
});
|
||||
out.Fixed = shape("composite");
|
||||
out.Composite = shape("composite");
|
||||
out.LiveComposite = shape("composite");
|
||||
out.Flatten = (child: any) => child; // flattening only changes how the browser composites, not what is drawn
|
||||
out.AlphaMask = shape("alphamask");
|
||||
out.TrackCursor = shape("trackcursor");
|
||||
out.At = (child: any) => child;
|
||||
|
||||
Reference in new issue
Block a user