Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

+158 -4
View File
@@ -1,5 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { spellDbcEquipmentMetadata } from "./spellDbcEquipmentMetadata.mjs";
import {
collectRealmSpellIds,
expandCoaSpellIds,
@@ -17,9 +19,12 @@ const extractedClientRoot = path.resolve(
"client-current",
);
const cliArguments = process.argv.slice(2);
const equipmentMetadataOnly = cliArguments.includes("--equipment-metadata-only");
const fetchLiveTooltips = process.env.ASCENSION_COA_FETCH_TOOLTIPS === "1"
|| cliArguments.includes("--tooltips");
const spellDbcArgument = cliArguments.find((argument) => argument !== "--tooltips");
const spellDbcArgument = cliArguments.find((argument) => (
argument !== "--tooltips" && argument !== "--equipment-metadata-only"
));
const legacyExtractedRoot = path.resolve(projectRoot, "..", "LadiksMPQEditor");
const sourcePaths = resolveCoaSourcePaths({
extractedClientRoot,
@@ -75,6 +80,150 @@ function assert(condition, message) {
if (!condition) throw new Error(message);
}
function canonicalJson(value) {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
function withoutEquipmentMetadataAndTimestamp(value) {
const copy = JSON.parse(JSON.stringify(value));
if (copy.source) delete copy.source.synchronizedAt;
for (const row of [...(copy.spells ?? []), ...(copy.coaSpellOverrides ?? [])]) {
if (!row.dbc) continue;
delete row.dbc.equippedItemClass;
delete row.dbc.equippedItemSubclassMask;
delete row.dbc.equippedItemInventoryMask;
}
return copy;
}
function metadataProfileForSpell(row, sourceClassesBySpellId) {
const classIds = [
...(Array.isArray(row.classIds) ? row.classIds.map(Number) : []),
...(sourceClassesBySpellId.get(Number(row.id)) ?? []),
];
if (classIds.some((classId) => classicClassIds.includes(classId))) return "classic";
if (classIds.some((classId) => coaClassIds.includes(classId))) return "coa";
return null;
}
function assertEquipmentMetadataSourceProfile(data) {
const profiles = data?.source?.spellDbcSources;
assert(profiles?.classic?.sourceId === "classic-area52-preserved", "Generated CoA data is not bound to the pinned classic Area 52 Spell.dbc profile");
assert(profiles?.coaBase?.sourceId === "global-spell-base", "Generated CoA data is not bound to the pinned global CoA Spell.dbc profile");
const classicProfilePath = path.resolve(extractedClientRoot, "area-52", "patch-D", "DBFilesClient", "Spell.dbc");
const coaProfilePath = path.resolve(extractedClientRoot, "patch-T", "DBFilesClient", "Spell.dbc");
assert(path.resolve(classicSpellDbcPath).toLowerCase() === classicProfilePath.toLowerCase(), `Classic Spell.dbc resolved outside the pinned profile: ${classicSpellDbcPath}`);
assert(path.resolve(coaSpellDbcPath).toLowerCase() === coaProfilePath.toLowerCase(), `CoA Spell.dbc resolved outside the pinned profile: ${coaSpellDbcPath}`);
const storedClassicPath = String(profiles.classic.path ?? "").replaceAll("\\", "/").toLowerCase();
const storedCoaPath = String(profiles.coaBase.path ?? "").replaceAll("\\", "/").toLowerCase();
assert(storedClassicPath.includes("/area-52/patch-d/dbfilesclient/spell.dbc"), "Generated classic source path does not identify Area 52 patch-D");
assert(storedCoaPath.includes("/patch-t/dbfilesclient/spell.dbc"), "Generated CoA source path does not identify global patch-T");
}
function applyEquipmentMetadataRefresh(data, classicMetadata, coaMetadata, synchronizedAt) {
assertEquipmentMetadataSourceProfile(data);
const counts = { classic: 0, coa: 0, skippedUnprofiled: 0, missingDbc: 0, missingSpell: 0 };
const sourceClassesBySpellId = new Map();
const addSourceClass = (spellId, classId) => {
const classes = sourceClassesBySpellId.get(Number(spellId)) ?? new Set();
classes.add(Number(classId));
sourceClassesBySpellId.set(Number(spellId), classes);
};
for (const chain of data.progressionChains ?? []) {
if (!Number.isFinite(Number(chain.classId))) continue;
for (const rank of chain.ranks ?? []) addSourceClass(rank.spellId, chain.classId);
}
for (const entry of data.entries ?? []) {
if (!Number.isFinite(Number(entry.classId))) continue;
for (const rank of entry.rankDescriptions ?? []) addSourceClass(rank.spellId, entry.classId);
}
for (const row of data.spells ?? []) {
if (!row.dbc) {
counts.missingDbc += 1;
continue;
}
const profile = metadataProfileForSpell(row, sourceClassesBySpellId);
if (!profile) {
counts.skippedUnprofiled += 1;
continue;
}
const metadata = (profile === "classic" ? classicMetadata : coaMetadata).get(Number(row.id));
if (!metadata) {
counts.missingSpell += 1;
continue;
}
Object.assign(row.dbc, metadata);
counts[profile] += 1;
}
for (const row of data.coaSpellOverrides ?? []) {
if (!row.dbc) {
counts.missingDbc += 1;
continue;
}
if (!Array.isArray(row.classIds) || !row.classIds.some((classId) => coaClassIds.includes(Number(classId)))) {
counts.skippedUnprofiled += 1;
continue;
}
const metadata = coaMetadata.get(Number(row.id));
if (!metadata) {
counts.missingSpell += 1;
continue;
}
Object.assign(row.dbc, metadata);
counts.coa += 1;
}
assert(data.source && typeof data.source === "object", "Generated CoA data has no source metadata");
data.source.synchronizedAt = synchronizedAt;
return counts;
}
function refreshEquipmentMetadataOnly() {
assert(spellDbcArgument === undefined, "--equipment-metadata-only does not accept a Spell.dbc path override");
assert(fs.existsSync(classicSpellDbcPath), `Classic Spell.dbc was not found at ${classicSpellDbcPath}`);
assert(fs.existsSync(coaSpellDbcPath), `CoA base Spell.dbc was not found at ${coaSpellDbcPath}`);
const classicMetadata = spellDbcEquipmentMetadata(fs.readFileSync(classicSpellDbcPath));
const coaMetadata = spellDbcEquipmentMetadata(fs.readFileSync(coaSpellDbcPath));
for (let attempt = 1; attempt <= 5; attempt += 1) {
assert(fs.existsSync(outputPath), `Generated CoA data was not found at ${outputPath}`);
const originalBuffer = fs.readFileSync(outputPath);
const originalHash = crypto.createHash("sha256").update(originalBuffer).digest("hex");
const data = JSON.parse(originalBuffer.toString("utf8"));
const counts = applyEquipmentMetadataRefresh(data, classicMetadata, coaMetadata, new Date().toISOString());
const original = JSON.parse(originalBuffer.toString("utf8"));
assert(
canonicalJson(withoutEquipmentMetadataAndTimestamp(original))
=== canonicalJson(withoutEquipmentMetadataAndTimestamp(data)),
"Equipment metadata refresh changed fields outside the three equipment values and source timestamp",
);
const latestHash = crypto.createHash("sha256").update(fs.readFileSync(outputPath)).digest("hex");
if (latestHash !== originalHash) continue;
fs.writeFileSync(outputPath, `${JSON.stringify(data)}\n`, "utf8");
const writtenHash = crypto.createHash("sha256").update(fs.readFileSync(outputPath)).digest("hex");
console.log(JSON.stringify({
mode: "equipment-metadata-only",
outputPath,
sourcePaths: {
classicSpellDbcPath: path.resolve(classicSpellDbcPath),
coaSpellDbcPath: path.resolve(coaSpellDbcPath),
},
sourceHashes: {
classic: crypto.createHash("sha256").update(fs.readFileSync(classicSpellDbcPath)).digest("hex"),
coa: crypto.createHash("sha256").update(fs.readFileSync(coaSpellDbcPath)).digest("hex"),
},
counts,
beforeSha256: originalHash,
afterSha256: writtenHash,
}, null, 2));
return;
}
throw new Error("Generated CoA data changed during five metadata refresh attempts; no output was written");
}
async function fetchText(url, attempts = 4) {
let lastError = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -386,9 +535,9 @@ function dbcSpells(buffer, wantedIds, castTimes, durations, radii, ranges) {
rangeMax: ranges.get(unsigned(46))?.max ?? 0,
projectileSpeed: float(47),
stackAmount: unsigned(49),
equippedItemClass: signed(67),
equippedItemSubclassMask: signed(68),
equippedItemInventoryMask: signed(69),
equippedItemClass: signed(68),
equippedItemSubclassMask: signed(69),
equippedItemInventoryMask: unsigned(70),
iconId: unsigned(133),
powerCostPercentage: unsigned(204),
startRecoveryCategory: unsigned(205),
@@ -463,6 +612,11 @@ function slimEntry(entry) {
};
}
if (equipmentMetadataOnly) {
refreshEquipmentMetadataOnly();
process.exit(0);
}
const builderPayload = JSON.parse(await fetchText(builderUrl));
assert(Array.isArray(builderPayload), "CoA builder response was not an array");
const realm = builderPayload.find((candidate) => candidate.slug === realmSlug);