forked from phenom/healer-man
Add atomic account Boss Card checkpoints and preserve cards in legacy saves (#7)
This commit is contained in:
1 parent
ff20ab8779
commit
5385b14e3a
5 files changed
+392
-4
No files matched your search
+70
-3
@@ -26,6 +26,7 @@ import { createBiteSizedLayoutRepository } from "./bite-sized-layouts.mjs";
|
||||
import { BITE_SIZED_LAYOUT_CATALOG, isLayoutBiteSizedDungeon } from "../shared/bite-sized-layouts.mjs";
|
||||
import { createDungeonRaceRepository } from "./dungeon-races.mjs";
|
||||
import { createDungeonSimulationRepository } from "./dungeon-simulation.mjs";
|
||||
import { mergeBossCardCollections } from "./boss-card-collections.mjs";
|
||||
import dungeonLeaderboardCatalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" };
|
||||
|
||||
const scrypt = promisify(scryptCallback);
|
||||
@@ -402,13 +403,78 @@ export function openGameDatabase(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function cloudSaveDataForWrite(row) {
|
||||
if (!row) return null;
|
||||
try {
|
||||
const data = JSON.parse(row.data_json);
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Cloud save data is not an object.");
|
||||
return data;
|
||||
} catch {
|
||||
throw new GameDatabaseError("The cloud save is unreadable.", 500, "cloud_save_corrupt");
|
||||
}
|
||||
}
|
||||
|
||||
function previousCloudSaveForPut(row) {
|
||||
try {
|
||||
return cloudSaveDataForWrite(row);
|
||||
} catch (error) {
|
||||
// Keep legacy PUT's ability to replace an unreadable save document.
|
||||
if (error instanceof GameDatabaseError && error.code === "cloud_save_corrupt") return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCloudData(accountId, data) {
|
||||
const now = Date.now();
|
||||
const row = statements.upsertCloudSave.get(accountId, JSON.stringify(data), now);
|
||||
return { revision: Number(row.revision), updatedAt: Number(row.updated_at), data };
|
||||
}
|
||||
|
||||
function transactCloudSave(operation) {
|
||||
database.exec("BEGIN IMMEDIATE");
|
||||
try {
|
||||
const result = operation();
|
||||
database.exec("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
database.exec("ROLLBACK");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function putCloudSave(accountId, data) {
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
||||
throw new GameDatabaseError("Cloud save data must be an object.");
|
||||
}
|
||||
const now = Date.now();
|
||||
const row = statements.upsertCloudSave.get(accountId, JSON.stringify(data), now);
|
||||
return { revision: Number(row.revision), updatedAt: Number(row.updated_at), data };
|
||||
return transactCloudSave(() => {
|
||||
const previous = previousCloudSaveForPut(statements.cloudSave.get(accountId));
|
||||
const nextData = { ...data };
|
||||
const hasPreviousCards = previous !== null && Object.hasOwn(previous, "bossCards");
|
||||
const hasIncomingCards = Object.hasOwn(data, "bossCards");
|
||||
if (hasPreviousCards || hasIncomingCards) {
|
||||
nextData.bossCards = mergeBossCardCollections([previous?.bossCards, data.bossCards]);
|
||||
}
|
||||
return saveCloudData(accountId, nextData);
|
||||
});
|
||||
}
|
||||
|
||||
function patchCloudSaveBossCards(accountId, bossCards, expectedRevision) {
|
||||
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
||||
throw new GameDatabaseError("Expected cloud save revision must be a nonnegative integer.", 400, "invalid_revision");
|
||||
}
|
||||
return transactCloudSave(() => {
|
||||
const row = statements.cloudSave.get(accountId);
|
||||
const currentRevision = row ? Number(row.revision) : 0;
|
||||
if (currentRevision !== expectedRevision) {
|
||||
throw new GameDatabaseError("Cloud save revision changed. Reload before retrying.", 409, "revision_conflict");
|
||||
}
|
||||
const previous = cloudSaveDataForWrite(row) ?? { version: 1, characters: [] };
|
||||
const nextData = {
|
||||
...previous,
|
||||
bossCards: mergeBossCardCollections([previous.bossCards, bossCards]),
|
||||
};
|
||||
return saveCloudData(accountId, nextData);
|
||||
});
|
||||
}
|
||||
|
||||
function isGameMaster(account) {
|
||||
@@ -1213,6 +1279,7 @@ export function openGameDatabase(options = {}) {
|
||||
logout: (token) => { if (typeof token === "string" && token) statements.deleteSession.run(tokenHash(token)); },
|
||||
getCloudSave,
|
||||
putCloudSave,
|
||||
patchCloudSaveBossCards,
|
||||
isGameMaster,
|
||||
getManastormConfig,
|
||||
putManastormConfig,
|
||||
|
||||
Reference in new issue
Block a user