Add native Crystal exploration port and reference tooling
Checks / web (push) Canceled after 0s

This commit is contained in:
Sébastien QUEROL committed 2026-09-23 09:23:29 +02:00
1 parent 2127f5f459
commit 8b80d05ca2
56 files changed
+6910 -1

No files matched your search

+90
View File
@@ -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;
}
}
}