import type { GameRuntime } from "./runtime"; import type { ValidatedRom } from "../rom/validate"; import { decodeSave, type SaveRecord, type SaveRepository, } from "../storage/saves"; export type SessionState = "empty" | "loading" | "paused" | "playing" | "error"; export class GameSession { state: SessionState = "empty"; private rom?: ValidatedRom; // Queue snapshots in capture order; a slow write cannot overwrite newer progress. private writes: Promise = Promise.resolve(); constructor( readonly runtime: GameRuntime, private saves: SaveRepository, ) {} async load(rom: ValidatedRom, canvas: HTMLCanvasElement) { this.state = "loading"; this.rom = undefined; try { const saved = await this.saves.read(rom.sha1); if (saved && (saved.version !== 1 || saved.coreId !== this.runtime.id)) { throw new Error( "The stored session needs migration before this player can open it. Your save has been kept.", ); } await this.runtime.load(rom.bytes, canvas); if (saved) this.runtime.restoreState(saved.state); else this.runtime.newGame?.(); // Never retain ROM bytes in storage or the session after loading. this.rom = { ...rom, bytes: new Uint8Array() }; this.state = "paused"; return Boolean(saved); } catch (error) { this.runtime.dispose(); this.state = "error"; throw error; } } play() { if (this.state !== "paused") return; this.runtime.play(); this.state = "playing"; } pause() { if (this.state !== "playing") return; this.runtime.pause(); this.state = "paused"; } snapshot(): SaveRecord { if (!this.rom || !["paused", "playing"].includes(this.state)) throw new Error("Load a ROM before saving."); return { version: 1, romHash: this.rom.sha1, coreId: this.runtime.id, savedAt: new Date().toISOString(), state: this.runtime.captureState(), }; } save() { const record = this.snapshot(); const operation = this.writes .catch(() => {}) .then(() => this.saves.write(record)); this.writes = operation; return operation.then(() => record.savedAt); } async importSave(text: string) { if (!this.rom) throw new Error("Load your ROM before importing a backup."); const record = decodeSave(text, this.rom.sha1, this.runtime.id); this.pause(); const previous = this.runtime.captureState(); try { this.runtime.restoreState(record.state); } catch (error) { this.runtime.restoreState(previous); throw error; } try { await this.save(); } catch (error) { this.runtime.restoreState(previous); throw error; } } }