Update Healer Man server
This commit is contained in:
1 parent
15a5254380
commit
9d7e0e2f9e
71 files changed
+3490
-81
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import { BITE_SIZED_DEFAULT_CONFIG, BITE_SIZED_LAYOUT_CATALOG, normalizeBiteSizedConfig, normalizeBiteSizedSelection, biteSizedGameplayKey, selectBiteSizedRun } from "../shared/bite-sized-layouts.mjs";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import { validateBiteSizedSpotNavigation } from "./generated/bite-sized-navigation.mjs";
|
||||
|
||||
const navigationGraphs = JSON.parse(gunzipSync(readFileSync(new URL("./generated/bite-sized-navigation.json.gz", import.meta.url))).toString("utf8"));
|
||||
|
||||
export function createBiteSizedLayoutRepository(database, { ErrorType, isGameMaster }) {
|
||||
database.exec(`CREATE TABLE IF NOT EXISTS bite_sized_layout_config (
|
||||
id INTEGER PRIMARY KEY CHECK(id = 1), revision INTEGER NOT NULL, updated_at INTEGER, config_json TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE TABLE IF NOT EXISTS bite_sized_spot_revisions (
|
||||
spot_id TEXT NOT NULL, revision INTEGER NOT NULL, spot_json TEXT NOT NULL, PRIMARY KEY(spot_id, revision)
|
||||
) STRICT;`);
|
||||
database.prepare("INSERT OR IGNORE INTO bite_sized_layout_config VALUES (1, 0, NULL, ?)").run(JSON.stringify(normalizeBiteSizedConfig(BITE_SIZED_DEFAULT_CONFIG)));
|
||||
const archive = database.prepare("INSERT OR IGNORE INTO bite_sized_spot_revisions VALUES (?, ?, ?)");
|
||||
for (const spot of normalizeBiteSizedConfig(BITE_SIZED_DEFAULT_CONFIG).spots) archive.run(spot.id, spot.revision, JSON.stringify(spot));
|
||||
function getBiteSizedLayouts() {
|
||||
const row = database.prepare("SELECT * FROM bite_sized_layout_config WHERE id = 1").get();
|
||||
return { revision: row.revision, updatedAt: row.updated_at, config: normalizeBiteSizedConfig(JSON.parse(row.config_json)) };
|
||||
}
|
||||
function getBiteSizedSpotRevision(id, revision) {
|
||||
const row = database.prepare("SELECT spot_json FROM bite_sized_spot_revisions WHERE spot_id = ? AND revision = ?").get(id, revision);
|
||||
if (!row) throw new ErrorType("That spot revision is unavailable.", 404, "spot_not_found");
|
||||
return JSON.parse(row.spot_json);
|
||||
}
|
||||
function listBiteSizedSpotRevisions() {
|
||||
return database.prepare("SELECT spot_json FROM bite_sized_spot_revisions ORDER BY spot_id, revision DESC").all().map((row) => JSON.parse(row.spot_json));
|
||||
}
|
||||
function putBiteSizedLayouts(account, expectedRevision, input) {
|
||||
if (!isGameMaster(account)) throw new ErrorType("Game Master access is required.", 403, "gm_required");
|
||||
let config;
|
||||
try { config = normalizeBiteSizedConfig(input); }
|
||||
catch (error) { throw new ErrorType(error.message, 400, "invalid_bite_sized_layout"); }
|
||||
database.exec("BEGIN IMMEDIATE");
|
||||
try {
|
||||
const current = getBiteSizedLayouts();
|
||||
if (!Number.isSafeInteger(expectedRevision) || expectedRevision !== current.revision) throw new ErrorType("Layouts changed on the server. Refresh before publishing; your draft is still available.", 409, "revision_conflict");
|
||||
for (const spot of config.spots) {
|
||||
const previous = current.config.spots.find((s) => s.id === spot.id);
|
||||
if (previous && biteSizedGameplayKey(previous) === biteSizedGameplayKey(spot)) continue;
|
||||
try {
|
||||
const graph = navigationGraphs[spot.sourceDungeonId];
|
||||
const blockers = spot.legacyLayout ? BITE_SIZED_LAYOUT_CATALOG.dungeons.find((d) => d.id === spot.dungeonId).legacyBlockers : [];
|
||||
validateBiteSizedSpotNavigation(spot, graph, spot.legacyLayout ? graph.legacyRegions ?? graph.regions : graph.regions, blockers);
|
||||
}
|
||||
catch (error) { throw new ErrorType(error.message, 400, "invalid_bite_sized_navigation"); }
|
||||
}
|
||||
const spots = config.spots.map((spot) => {
|
||||
const previous = current.config.spots.find((s) => s.id === spot.id);
|
||||
const latest = database.prepare("SELECT MAX(revision) AS revision FROM bite_sized_spot_revisions WHERE spot_id = ?").get(spot.id).revision;
|
||||
const revision = previous && biteSizedGameplayKey(previous) === biteSizedGameplayKey(spot) ? previous.revision : (latest ?? -1) + 1;
|
||||
const next = { ...spot, revision };
|
||||
archive.run(next.id, revision, JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
const next = { schemaVersion: 1, spots };
|
||||
database.prepare("UPDATE bite_sized_layout_config SET revision = revision + 1, updated_at = ?, config_json = ? WHERE id = 1").run(Date.now(), JSON.stringify(next));
|
||||
database.exec("COMMIT");
|
||||
return getBiteSizedLayouts();
|
||||
} catch (error) { database.exec("ROLLBACK"); throw error; }
|
||||
}
|
||||
function resolveBiteSizedRun(dungeonId, seed, eligibleDungeonIds) {
|
||||
return selectBiteSizedRun(getBiteSizedLayouts().config, dungeonId, seed, { eligibleDungeonIds });
|
||||
}
|
||||
function validateBiteSizedRunSelection(input, dungeonId) {
|
||||
let selection;
|
||||
try { selection = normalizeBiteSizedSelection(input); }
|
||||
catch (error) { throw new ErrorType(error.message, 400, "invalid_bite_sized_selection"); }
|
||||
const archived = getBiteSizedSpotRevision(selection.spot.id, selection.spot.revision);
|
||||
if (selection.dungeonId !== dungeonId || biteSizedGameplayKey(archived) !== biteSizedGameplayKey(selection.spot)) {
|
||||
throw new ErrorType("The run does not match its published spot revision.", 400, "invalid_bite_sized_selection");
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
return { getBiteSizedLayouts, getBiteSizedSpotRevision, listBiteSizedSpotRevisions, putBiteSizedLayouts, resolveBiteSizedRun, validateBiteSizedRunSelection };
|
||||
}
|
||||
Reference in new issue
Block a user