/** * The saves of persistent Variables assets (#166), kept in this browser per project. Played and * editor runs write to separate areas, so trying a behavior in the editor leaves the player's save * alone. A released game keeps its own, under the token of its link. Storage is best effort: a browser that refuses `localStorage` plays every run from the * declared values. */ import type { SaveArea, SavedValues } from '../core/graph'; import type { PropertyBag } from '../core/model/Properties'; /** `release` is a released game's (#266), kept per link rather than per project. */ export type SaveAreaName = 'play' | 'editor' | 'release'; /** The areas a project's own saves are kept in – what the editor shows and clears. */ const AREAS: readonly SaveAreaName[] = ['play', 'editor']; type Store = Pick; function browserStore(): Store | null { try { return window.localStorage; } catch { return null; } } export function saveKey(projectId: string, area: SaveAreaName): string { return `game-maker.saves.${projectId}.${area}`; } function readArea(store: Store | null, key: string): SavedValues { try { const read: unknown = JSON.parse(store?.getItem(key) ?? 'null'); if (!read || typeof read !== 'object' || Array.isArray(read)) return {}; const saved: SavedValues = {}; for (const [assetId, bag] of Object.entries(read)) if (bag && typeof bag === 'object' && !Array.isArray(bag)) saved[assetId] = bag as PropertyBag; return saved; } catch { return {}; } } function writeArea(store: Store | null, key: string, saved: SavedValues): void { try { if (Object.keys(saved).length === 0) store?.removeItem(key); else store?.setItem(key, JSON.stringify(saved)); } catch { // Full or refused: the run goes on with its values, it just will not find them next time. } } export function saveArea(projectId: string, area: SaveAreaName, store = browserStore()): SaveArea { const key = saveKey(projectId, area); return { read: () => readArea(store, key), // Only the assets the run knows are written; one saved by another area or since switched off // stays as it was until it is cleared. write: (saved) => writeArea(store, key, { ...readArea(store, key), ...saved }), }; } /** Whether either area holds anything for the asset – or, without one, for the project. */ export function hasSaved(projectId: string, assetId?: string, store = browserStore()): boolean { return AREAS.some((area) => { const saved = readArea(store, saveKey(projectId, area)); return assetId === undefined ? Object.keys(saved).length > 0 : assetId in saved; }); } /** Forgets what both areas saved for one asset, or for the whole project without one. */ export function clearSaved(projectId: string, assetId?: string, store = browserStore()): void { for (const area of AREAS) { const key = saveKey(projectId, area); if (assetId === undefined) { writeArea(store, key, {}); continue; } const saved = readArea(store, key); delete saved[assetId]; writeArea(store, key, saved); } }