Release Healer Man 0.1.51
This commit is contained in:
1 parent
79153ca40d
commit
dd583023ac
13 files changed
+405
-31
No files matched your search
@@ -15,6 +15,7 @@ import { GameDatabaseError } from "./database.mjs";
|
||||
|
||||
const SOURCES = new Set(["wow", "coa", "rom", "all"]);
|
||||
const BOSS_CATEGORIES = new Set(["all", "wow", "rom"]);
|
||||
const SPEC_SELECTIONS = new Set(["random", "all", "specific"]);
|
||||
const JOB_STATUSES = new Set(["queued", "running", "complete", "failed"]);
|
||||
const JOB_METADATA_NAME = "job.json";
|
||||
const JOB_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
@@ -43,12 +44,16 @@ function integer(value, name, minimum, maximum) {
|
||||
export function normalizeBalanceLabJobSpec(input) {
|
||||
const source = String(input?.source ?? "all").toLowerCase();
|
||||
const bossCategory = String(input?.bossCategory ?? "all").toLowerCase();
|
||||
const specSelection = String(input?.specSelection ?? "random").toLowerCase();
|
||||
if (!SOURCES.has(source)) {
|
||||
throw new GameDatabaseError("Party source must be wow, coa, rom, or all.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
if (!BOSS_CATEGORIES.has(bossCategory)) {
|
||||
throw new GameDatabaseError("Boss category must be all, wow, or rom.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
if (!SPEC_SELECTIONS.has(specSelection)) {
|
||||
throw new GameDatabaseError("Class/spec selection must be random, all, or specific.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
const rawBossKeys = Array.isArray(input?.bossKeys) ? input.bossKeys : [];
|
||||
if (rawBossKeys.length > 500) {
|
||||
throw new GameDatabaseError("Select no more than 500 bosses.", 400, "invalid_balance_lab_job");
|
||||
@@ -57,8 +62,21 @@ export function normalizeBalanceLabJobSpec(input) {
|
||||
if (bossKeys.some((key) => key.length > 240 || !/^[A-Za-z0-9._:/-]+$/.test(key))) {
|
||||
throw new GameDatabaseError("A selected boss key is invalid.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
const rawBuildIds = Array.isArray(input?.selectedBuildIds) ? input.selectedBuildIds : [];
|
||||
if (rawBuildIds.length > 200) {
|
||||
throw new GameDatabaseError("Select no more than 200 class/spec builds.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
const selectedBuildIds = [...new Set(rawBuildIds.map((value) => String(value).trim()).filter(Boolean))];
|
||||
if (selectedBuildIds.some((id) => id.length > 240 || !/^[A-Za-z0-9._:/-]+$/.test(id))) {
|
||||
throw new GameDatabaseError("A selected class/spec build ID is invalid.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
if (specSelection === "specific" && selectedBuildIds.length === 0) {
|
||||
throw new GameDatabaseError("Select at least one class/spec build.", 400, "invalid_balance_lab_job");
|
||||
}
|
||||
const spec = Object.freeze({
|
||||
source,
|
||||
specSelection,
|
||||
selectedBuildIds: Object.freeze(specSelection === "specific" ? selectedBuildIds : []),
|
||||
level: integer(input?.level ?? 60, "Level", 1, 80),
|
||||
partyConfigurations: integer(input?.partyConfigurations ?? 10, "Party configurations", 1, 100),
|
||||
simulationsPerParty: integer(input?.simulationsPerParty ?? 3, "Simulations per party", 1, 100),
|
||||
@@ -69,7 +87,12 @@ export function normalizeBalanceLabJobSpec(input) {
|
||||
});
|
||||
const sourceCount = spec.source === "all" ? 3 : 1;
|
||||
const bossEstimate = Math.max(1, spec.bossKeys.length || (spec.bossCategory === "all" ? 500 : 300));
|
||||
const estimatedRuns = sourceCount * spec.partyConfigurations * spec.simulationsPerParty * bossEstimate;
|
||||
const partyEstimate = spec.specSelection === "random"
|
||||
? sourceCount * spec.partyConfigurations
|
||||
: spec.specSelection === "specific"
|
||||
? Math.max(spec.partyConfigurations, spec.selectedBuildIds.length)
|
||||
: Math.max(spec.partyConfigurations, spec.source === "all" ? 100 : 50);
|
||||
const estimatedRuns = partyEstimate * spec.simulationsPerParty * bossEstimate;
|
||||
if (estimatedRuns > 250_000) {
|
||||
throw new GameDatabaseError(
|
||||
"That job is too large for one server request. Reduce parties, simulations, or selected bosses.",
|
||||
@@ -90,6 +113,7 @@ export function balanceLabCommand(spec, outputDir, options = {}) {
|
||||
"--source", spec.source,
|
||||
"--level", String(spec.level),
|
||||
"--party-mode", "automatic",
|
||||
"--spec-selection", spec.specSelection,
|
||||
"--party-seed", String(spec.partySeed),
|
||||
"--party-configurations", String(spec.partyConfigurations),
|
||||
"--simulations-per-party", String(spec.simulationsPerParty),
|
||||
@@ -97,6 +121,7 @@ export function balanceLabCommand(spec, outputDir, options = {}) {
|
||||
"--boss-category", spec.bossCategory,
|
||||
"--output-dir", outputDir,
|
||||
];
|
||||
for (const buildId of spec.selectedBuildIds) args.push("--spec", buildId);
|
||||
for (const bossKey of spec.bossKeys) args.push("--boss", bossKey);
|
||||
return { executable, args, cwd: projectRoot };
|
||||
}
|
||||
@@ -167,6 +192,12 @@ function reportSummary(outputDir) {
|
||||
}
|
||||
}
|
||||
|
||||
function expectedPartyConfigurations(spec) {
|
||||
return spec.specSelection === "random"
|
||||
? spec.partyConfigurations * (spec.source === "all" ? 3 : 1)
|
||||
: spec.partyConfigurations;
|
||||
}
|
||||
|
||||
function lineCollector(onLine) {
|
||||
let pending = "";
|
||||
return {
|
||||
@@ -236,8 +267,12 @@ export function createBalanceLabJobService(options = {}) {
|
||||
requestedBy: "unknown (legacy run)",
|
||||
spec: {
|
||||
source: legacyConfig.source,
|
||||
specSelection: legacyConfig.specSelection ?? "random",
|
||||
selectedBuildIds: legacyConfig.selectedBuildIds ?? [],
|
||||
level: legacyConfig.level,
|
||||
partyConfigurations: legacyConfig.partyConfigurationsPerSource,
|
||||
partyConfigurations: legacyConfig.specSelection && legacyConfig.specSelection !== "random"
|
||||
? legacyConfig.partyConfigurations
|
||||
: legacyConfig.partyConfigurationsPerSource,
|
||||
simulationsPerParty: legacyConfig.simulationsPerParty,
|
||||
durationSeconds: legacyConfig.durationSeconds,
|
||||
partySeed: legacyConfig.partySeed,
|
||||
@@ -252,7 +287,8 @@ export function createBalanceLabJobService(options = {}) {
|
||||
const hasReport = existsSync(reportPath);
|
||||
const storedStatus = JOB_STATUSES.has(stored.status) ? stored.status : "failed";
|
||||
const interrupted = !hasReport && (storedStatus === "queued" || storedStatus === "running");
|
||||
const totalParties = spec.partyConfigurations * (spec.source === "all" ? 3 : 1);
|
||||
const restoredSummary = hasReport ? reportSummary(outputDir) : null;
|
||||
const totalParties = restoredSummary?.partyConfigurations || expectedPartyConfigurations(spec);
|
||||
const createdAt = typeof stored.createdAt === "string"
|
||||
? stored.createdAt
|
||||
: statSync(outputDir).birthtime.toISOString();
|
||||
@@ -282,7 +318,7 @@ export function createBalanceLabJobService(options = {}) {
|
||||
logTail: Array.isArray(stored.logTail)
|
||||
? stored.logTail.filter((line) => typeof line === "string").slice(-80)
|
||||
: [],
|
||||
summary: hasReport ? reportSummary(outputDir) : null,
|
||||
summary: restoredSummary,
|
||||
outputDir,
|
||||
};
|
||||
jobs.set(job.id, job);
|
||||
@@ -401,7 +437,7 @@ export function createBalanceLabJobService(options = {}) {
|
||||
const id = randomUUID();
|
||||
const outputDir = path.join(jobsDir, id);
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const totalParties = spec.partyConfigurations * (spec.source === "all" ? 3 : 1);
|
||||
const totalParties = expectedPartyConfigurations(spec);
|
||||
const job = {
|
||||
id,
|
||||
status: "queued",
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
test("normalizes and bounds server Balance Lab requests", () => {
|
||||
const spec = normalizeBalanceLabJobSpec({
|
||||
source: "ALL",
|
||||
specSelection: "specific",
|
||||
selectedBuildIds: ["balance-wow-priest-healer-holy-level-40", "balance-wow-priest-healer-holy-level-40"],
|
||||
level: 40,
|
||||
partyConfigurations: 2,
|
||||
simulationsPerParty: 3,
|
||||
@@ -24,6 +26,8 @@ test("normalizes and bounds server Balance Lab requests", () => {
|
||||
bossKeys: ["gundrak/entry-29932", "gundrak/entry-29932"],
|
||||
});
|
||||
assert.equal(spec.source, "all");
|
||||
assert.equal(spec.specSelection, "specific");
|
||||
assert.deepEqual(spec.selectedBuildIds, ["balance-wow-priest-healer-holy-level-40"]);
|
||||
assert.deepEqual(spec.bossKeys, ["gundrak/entry-29932"]);
|
||||
assert.throws(
|
||||
() => normalizeBalanceLabJobSpec({ source: "wow", level: 81 }),
|
||||
@@ -33,12 +37,17 @@ test("normalizes and bounds server Balance Lab requests", () => {
|
||||
() => normalizeBalanceLabJobSpec({ source: "wow", bossKeys: ["boss; shutdown"] }),
|
||||
/boss key is invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeBalanceLabJobSpec({ source: "wow", specSelection: "specific", selectedBuildIds: [] }),
|
||||
/Select at least one class\/spec build/,
|
||||
);
|
||||
});
|
||||
|
||||
test("builds an argument-array command without a shell", () => {
|
||||
const spec = normalizeBalanceLabJobSpec({
|
||||
source: "rom", level: 60, partyConfigurations: 4, simulationsPerParty: 2,
|
||||
durationSeconds: 30, partySeed: 9, bossCategory: "rom", bossKeys: ["abbey/boss-1"],
|
||||
specSelection: "specific", selectedBuildIds: ["balance-rom-mage-damage-primary-level-60"],
|
||||
});
|
||||
const command = balanceLabCommand(spec, "C:/reports/job", {
|
||||
projectRoot: "C:/app",
|
||||
@@ -47,6 +56,8 @@ test("builds an argument-array command without a shell", () => {
|
||||
assert.equal(command.executable, "python-custom");
|
||||
assert.equal(command.args.includes("--party-mode"), true);
|
||||
assert.equal(command.args.includes("automatic"), true);
|
||||
assert.equal(command.args.includes("specific"), true);
|
||||
assert.equal(command.args.includes("balance-rom-mage-damage-primary-level-60"), true);
|
||||
assert.equal(command.args.includes("abbey/boss-1"), true);
|
||||
assert.equal(command.cwd, path.resolve("C:/app"));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user