forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user