Release Healer Man 0.1.46

This commit is contained in:
phenom committed 2026-09-16 14:30:58 -04:00
1 parent f82daf1c13
commit 9ff574ac0b
73 files changed
+33486 -174

No files matched your search

+106 -1
View File
@@ -8,6 +8,7 @@ import { publicManastormAdminConfig } from "./manastorm-config.mjs";
import { createOnlineSessionManager } from "./online-session.mjs";
import { upgradeArenaWebSocket } from "./websocket.mjs";
import { createBundledContentHandler } from "./bundled-content.mjs";
import { createBalanceLabJobService } from "./balance-lab-jobs.mjs";
const MIME_TYPES = new Map([
[".css", "text/css; charset=utf-8"], [".glb", "model/gltf-binary"],
@@ -178,6 +179,11 @@ export function createGameServer(options = {}) {
const trustProxy = String(options.trustProxy ?? process.env.TRUST_PROXY ?? "").toLowerCase() === "true";
const enableShelvedModes = options.enableShelvedModes ?? booleanFlag(process.env.HEALER_MAN_ENABLE_SHELVED_MODES);
const gameDatabase = options.database ?? openGameDatabase(options);
const balanceLabJobs = options.balanceLabJobs ?? createBalanceLabJobService({
dataDir: options.dataDir ?? path.dirname(gameDatabase.databasePath),
projectRoot: options.projectRoot,
pythonExecutable: options.pythonExecutable,
});
const onlineSessions = createOnlineSessionManager();
const arenaService = createArenaService({ database: gameDatabase, autoPump: enableShelvedModes });
const allowAuthRequest = createAuthLimiter();
@@ -211,6 +217,60 @@ export function createGameServer(options = {}) {
json(response, 200, { ok: true, service: "healer-man", time: new Date().toISOString() });
return;
}
if (url.pathname === "/api/dungeon-route-approvals" && request.method === "GET") {
json(response, 200, gameDatabase.listDungeonRouteApprovals());
return;
}
if (url.pathname === "/api/gm/dungeon-routes" && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
json(response, 200, gameDatabase.listDungeonRouteReviews(account));
return;
}
const dungeonRouteReviewMatch = /^\/api\/gm\/dungeon-routes\/([^/]+)\/([^/]+)\/review$/.exec(url.pathname);
if (dungeonRouteReviewMatch && request.method === "PUT") {
const account = gameDatabase.authenticate(bearerToken(request));
const body = await readJson(request, 64 * 1024);
const review = gameDatabase.putDungeonRouteReview(
account,
decodeURIComponent(dungeonRouteReviewMatch[1]),
decodeURIComponent(dungeonRouteReviewMatch[2]),
body,
);
json(response, 200, { review });
return;
}
if (url.pathname === "/api/gm/dungeon-sim/replays" && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
json(response, 200, gameDatabase.listDungeonSimulationReplays(account, {
dungeonId: url.searchParams.get("dungeon"),
}));
return;
}
if (url.pathname === "/api/gm/dungeon-sim/replays" && request.method === "POST") {
const account = gameDatabase.authenticate(bearerToken(request));
const replay = gameDatabase.saveDungeonSimulationReplay(account, await readJson(request, 64 * 1024 * 1024));
json(response, 201, { replay });
return;
}
const dungeonSimulationReportMatch = /^\/api\/gm\/dungeon-sim\/replays\/([^/]+)\/report$/.exec(url.pathname);
if (dungeonSimulationReportMatch && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
json(response, 200, gameDatabase.getDungeonSimulationReport(account, dungeonSimulationReportMatch[1]), {
"Content-Disposition": `attachment; filename="dungeon-simulation-${dungeonSimulationReportMatch[1]}.json"`,
});
return;
}
const dungeonSimulationReplayMatch = /^\/api\/gm\/dungeon-sim\/replays\/([^/]+)$/.exec(url.pathname);
if (dungeonSimulationReplayMatch && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
json(response, 200, gameDatabase.getDungeonSimulationReplay(account, dungeonSimulationReplayMatch[1]));
return;
}
if (dungeonSimulationReplayMatch && request.method === "DELETE") {
const account = gameDatabase.authenticate(bearerToken(request));
json(response, 200, gameDatabase.deleteDungeonSimulationReplay(account, dungeonSimulationReplayMatch[1]));
return;
}
if (!enableShelvedModes && (
url.pathname === "/api/manastorm-config"
|| url.pathname.startsWith("/api/gm/manastorm-config")
@@ -438,6 +498,50 @@ export function createGameServer(options = {}) {
json(response, 200, result);
return;
}
if (url.pathname === "/api/gm/balance-lab/jobs" && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
if (!gameDatabase.isGameMaster(account)) {
throw new GameDatabaseError("Game Master access is required.", 403, "gm_required");
}
json(response, 200, { jobs: balanceLabJobs.list() });
return;
}
if (url.pathname === "/api/gm/balance-lab/jobs" && request.method === "POST") {
const account = gameDatabase.authenticate(bearerToken(request));
if (!gameDatabase.isGameMaster(account)) {
throw new GameDatabaseError("Game Master access is required.", 403, "gm_required");
}
const job = balanceLabJobs.start(await readJson(request, 32 * 1024), account);
console.info(`Balance Lab job ${job.id} queued by ${account.username}`);
json(response, 202, { job });
return;
}
const balanceLabArtifactMatch = /^\/api\/gm\/balance-lab\/jobs\/([0-9a-f-]{36})\/artifacts\/([A-Za-z0-9._-]+)$/.exec(url.pathname);
if (balanceLabArtifactMatch && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
if (!gameDatabase.isGameMaster(account)) {
throw new GameDatabaseError("Game Master access is required.", 403, "gm_required");
}
const artifact = balanceLabJobs.artifact(balanceLabArtifactMatch[1], balanceLabArtifactMatch[2]);
response.writeHead(200, {
"Content-Type": artifact.contentType,
"Content-Length": artifact.size,
"Cache-Control": "no-store",
"Content-Disposition": `inline; filename="${balanceLabArtifactMatch[2]}"`,
"X-Content-Type-Options": "nosniff",
});
artifact.stream().pipe(response);
return;
}
const balanceLabJobMatch = /^\/api\/gm\/balance-lab\/jobs\/([0-9a-f-]{36})$/.exec(url.pathname);
if (balanceLabJobMatch && request.method === "GET") {
const account = gameDatabase.authenticate(bearerToken(request));
if (!gameDatabase.isGameMaster(account)) {
throw new GameDatabaseError("Game Master access is required.", 403, "gm_required");
}
json(response, 200, { job: balanceLabJobs.get(balanceLabJobMatch[1]) });
return;
}
if (url.pathname.startsWith("/api/")) {
json(response, 404, { error: "API route not found.", code: "not_found" });
return;
@@ -491,11 +595,12 @@ export function createGameServer(options = {}) {
});
server.on("close", () => {
balanceLabJobs.close?.();
onlineSessions.clear();
arenaService.close();
gameDatabase.close();
});
return { server, database: gameDatabase, arenaService, staticDir, contentDir, enableShelvedModes };
return { server, database: gameDatabase, arenaService, balanceLabJobs, staticDir, contentDir, enableShelvedModes };
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {