Keep several saves, with migration, atomic writes and import previews (Closes #37)
Savegames now lists independent save slots made with the loaded ROM: new game, continue, rename, duplicate, export and delete. The native database moves to IndexedDB version 2; the singleton save becomes "Save 1" inside the upgrade transaction, so it migrates exactly once and the old store is left untouched. Each write adds a generation and drops older ones in one transaction, keeping two records with CRC-32 checksums: a damaged newest record falls back to the previous one. Slots and records from a newer player are kept and never written. Imports are checked in a scratch world before anything changes and previewed with the trainer, progress, saved time, game data version and the slots of the same trainer; they go into a new save or replace one, whose previous record stays recoverable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c12c81a27
commit
7d03f5c5ca
18 files changed
+2079
-56
No files matched your search
@@ -575,6 +575,54 @@ describe.skipIf(!existsSync(romPath))(
|
||||
expect(world.save().playerId).toBeLessThan(0x10000);
|
||||
expect(() => world.restore({ ...legacy, version: 4 })).toThrow("Invalid");
|
||||
});
|
||||
it("describes saves of every schema for the Savegames list", () => {
|
||||
const world = new GameWorld(load(), () => new Date("2026-09-22T12:00:00"));
|
||||
const flags = [27, 28, 29];
|
||||
const town = world.assets.map("24:4");
|
||||
const land = Array.from({ length: town.width * town.height }, (_, i) => ({
|
||||
x: i % town.width,
|
||||
y: Math.floor(i / town.width),
|
||||
})).find(
|
||||
({ x, y }) =>
|
||||
world.assets.permission(world.assets.collision(town, x, y)) === 0,
|
||||
)!;
|
||||
world.restore({
|
||||
...world.save(),
|
||||
name: "GOLD",
|
||||
map: "24:4",
|
||||
...land,
|
||||
flags,
|
||||
gameTime: { hours: 3, minutes: 7, seconds: 0, frames: 0, capped: false },
|
||||
pokedex: { seen: [152, 16], caught: [152] },
|
||||
});
|
||||
const current = world.save();
|
||||
expect(world.describe()).toEqual({
|
||||
player: "GOLD",
|
||||
playerId: current.playerId,
|
||||
place: "NEW BARK TOWN",
|
||||
hours: 3,
|
||||
minutes: 7,
|
||||
badges: 3,
|
||||
caught: 1,
|
||||
newGame: false,
|
||||
});
|
||||
// Indoors, the landmark is the map's own (the player's house).
|
||||
world.restore({ ...current, map: "24:6", x: 6, y: 6 });
|
||||
expect(world.describe().place).toBe("NEW BARK TOWN");
|
||||
const { scenes, phone, clock, objects, playerId, party, bag, pokedex, ...legacy } =
|
||||
current;
|
||||
void [scenes, phone, clock, objects, playerId, party, bag, pokedex];
|
||||
for (const version of [1, 2]) {
|
||||
const scratch = new GameWorld(load(), () => new Date("2026-09-22T12:00:00"));
|
||||
scratch.restore({ ...legacy, version, ...(version === 2 && { scenes: {}, phone: [], clock, objects: [] }) });
|
||||
expect(scratch.describe()).toMatchObject({
|
||||
player: "GOLD",
|
||||
place: "NEW BARK TOWN",
|
||||
badges: 3,
|
||||
caught: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
it("decodes species, moves, items and growth from the ROM", () => {
|
||||
const data = load().pokemon;
|
||||
expect([152, 155, 158].map((id) => data.speciesName(id))).toEqual([
|
||||
|
||||
Reference in new issue
Block a user