This commit is contained in:
1 parent
2127f5f459
commit
8b80d05ca2
56 files changed
+6910
-1
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import type { Button } from "../input/types";
|
||||
|
||||
/** Application lifecycle contract, independent of emulation and hardware. */
|
||||
export interface GameRuntime {
|
||||
readonly id: string;
|
||||
load(rom: Uint8Array, canvas: HTMLCanvasElement): Promise<void>;
|
||||
play(): void;
|
||||
pause(): void;
|
||||
setButton(button: Button, pressed: boolean): void;
|
||||
captureState(): Uint8Array;
|
||||
restoreState(state: Uint8Array): void;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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<void> = 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);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user