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

@@ -15,6 +15,15 @@ const defaultReferenceOutput = path.join(
"generated",
"azerothCoreReference.json",
);
const defaultItemDbc = path.resolve(
projectRoot,
"..",
"wow335a",
"Data",
"patch-A.MPQ",
"DBFilesClient",
"Item.dbc",
);
const defaultBaseStatsOutput = path.join(
projectRoot,
"src",
@@ -55,11 +64,48 @@ export const PINNED_CREATURE_IMMUNITIES_SOURCE = Object.freeze({
),
});
export const PINNED_CREATURE_EQUIPMENT_SOURCE = Object.freeze({
repository: "https://github.com/azerothcore/azerothcore-wotlk",
commit: DEFAULT_AZEROTHCORE_COMMIT,
path: "data/sql/base/db_world/creature_equip_template.sql",
url: `https://raw.githubusercontent.com/azerothcore/azerothcore-wotlk/${DEFAULT_AZEROTHCORE_COMMIT}/data/sql/base/db_world/creature_equip_template.sql`,
sha256: "877205c149fd447b08f0ff2c65976f20c862cd42f068a6fcc294a76c7108936a",
fixtureRoot: path.join(
projectRoot,
"dungeon-pipeline",
"source-fixtures",
`azerothcore-${DEFAULT_AZEROTHCORE_COMMIT}`,
),
});
export const PINNED_ITEM_DBC_SOURCE = Object.freeze({
label: "WoW 3.3.5a Item.dbc",
path: "../wow335a/Data/patch-A.MPQ/DBFilesClient/Item.dbc",
sha256: "22061a811523790ddd2ee95b36ade2ac6e0c79a8b3121324d32a639acf50567f",
fields: 8,
recordSize: 32,
classField: 1,
inventoryTypeField: 6,
});
const EQUIPMENT_HAND_INVENTORY_TYPES = Object.freeze(new Set([
13, // INVTYPE_WEAPON
14, // INVTYPE_SHIELD
15, // INVTYPE_RANGED
17, // INVTYPE_2HWEAPON
21, // INVTYPE_WEAPONMAINHAND
22, // INVTYPE_WEAPONOFFHAND
23, // INVTYPE_HOLDABLE
25, // INVTYPE_THROWN
26, // INVTYPE_RANGEDRIGHT
]));
const SOURCE_TABLES = Object.freeze([
"creature",
"creature_template",
"creature_template_model",
"creature_template_spell",
"creature_equip_template",
"creature_classlevelstats",
"creature_summon_groups",
"creature_addon",
@@ -319,6 +365,11 @@ function bitMaskNames(maskValue, names, bitOffset = 0) {
export function creatureTemplateReference(row, creatureImmunitiesById = new Map()) {
const entry = numeric(firstValue(row, ["entry", "CreatureID", "creatureId"]));
const sourceFlagsExtra = firstValue(row, ["flags_extra", "flagsExtra"]);
const flagsExtra = sourceFlagsExtra === undefined ? undefined : Number(sourceFlagsExtra);
if (flagsExtra !== undefined && (!Number.isSafeInteger(flagsExtra) || flagsExtra < 0 || flagsExtra > 0xffff_ffff)) {
throw new Error(`Creature ${entry} has unsupported flags_extra ${sourceFlagsExtra}.`);
}
const legacyMechanicImmuneMask = firstValue(row, ["mechanic_immune_mask", "mechanicImmuneMask"]);
const creatureImmunitiesId = firstValue(row, ["CreatureImmunitiesId", "creatureImmunitiesId"]);
let mechanicImmuneMask = numeric(legacyMechanicImmuneMask);
@@ -360,6 +411,7 @@ export function creatureTemplateReference(row, creatureImmunitiesById = new Map(
baseAttackTimeMs: numeric(firstValue(row, ["BaseAttackTime", "baseAttackTime"])) || 2_000,
baseVariance: numeric(firstValue(row, ["BaseVariance", "baseVariance"])) || 1,
unitClass: numeric(firstValue(row, ["unit_class", "unitClass"])) || 1,
...(flagsExtra === undefined ? {} : { flagsExtra }),
healthModifier: numeric(firstValue(row, ["HealthModifier", "ModHealth"])) || 1,
manaModifier: numeric(firstValue(row, ["ManaModifier", "ModMana"])) || 1,
armorModifier: numeric(firstValue(row, ["ArmorModifier", "ModArmor"])) || 1,
@@ -645,6 +697,82 @@ export async function parseTable(sourceRoot, tableName, visitor) {
};
}
/**
* Reads the pinned WotLK Item.dbc fields used by AzerothCore's equipment
* loader. Item.dbc field 1 is Class and field 6 is InventoryType.
*/
export function parseItemDbc(buffer) {
if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer);
if (buffer.length < 20 || buffer.toString("ascii", 0, 4) !== "WDBC") {
throw new Error("Item.dbc has an invalid WDBC header.");
}
const recordCount = buffer.readUInt32LE(4);
const fieldCount = buffer.readUInt32LE(8);
const recordSize = buffer.readUInt32LE(12);
const stringBlockSize = buffer.readUInt32LE(16);
if (fieldCount !== PINNED_ITEM_DBC_SOURCE.fields
|| recordSize !== PINNED_ITEM_DBC_SOURCE.recordSize
|| recordSize !== fieldCount * 4
|| 20 + recordCount * recordSize + stringBlockSize !== buffer.length) {
throw new Error(
`Item.dbc schema is unsupported (records=${recordCount}, fields=${fieldCount}, `
+ `recordSize=${recordSize}, stringBlockSize=${stringBlockSize}).`,
);
}
const itemsById = new Map();
for (let index = 0; index < recordCount; index += 1) {
const offset = 20 + index * recordSize;
const id = buffer.readUInt32LE(offset);
if (id === 0 || itemsById.has(id)) {
throw new Error(`Item.dbc contains an invalid or duplicate item ID ${id}.`);
}
itemsById.set(id, Object.freeze({
itemClass: buffer.readUInt32LE(offset + PINNED_ITEM_DBC_SOURCE.classField * 4),
inventoryType: buffer.readUInt32LE(offset + PINNED_ITEM_DBC_SOURCE.inventoryTypeField * 4),
}));
}
return Object.freeze({
recordCount,
fieldCount,
recordSize,
stringBlockSize,
itemsById,
});
}
/**
* Mirrors ObjectMgr::LoadEquipmentTemplates item lookup and hand-slot
* InventoryType validation. Invalid or absent Item.dbc rows become empty
* virtual item slots before Creature::HasWeapon can inspect them.
*/
export function creatureEquipmentTemplateReference(row, itemMetadataById) {
const creatureId = numeric(firstValue(row, ["CreatureID", "creatureId", "entry"]));
const equipmentId = numeric(firstValue(row, ["ID", "id", "equipmentId"]));
if (!Number.isSafeInteger(creatureId) || creatureId <= 0
|| !Number.isSafeInteger(equipmentId) || equipmentId <= 0 || equipmentId > 255) {
throw new Error(`Creature equipment row has invalid key ${creatureId}/${equipmentId}.`);
}
const sourceItems = [
numeric(firstValue(row, ["ItemID1", "itemId1"])),
numeric(firstValue(row, ["ItemID2", "itemId2"])),
numeric(firstValue(row, ["ItemID3", "itemId3"])),
];
if (sourceItems.some((itemId) => !Number.isSafeInteger(itemId) || itemId < 0 || itemId > 0xffff_ffff)) {
throw new Error(`Creature equipment row ${creatureId}/${equipmentId} has an invalid item ID.`);
}
const items = sourceItems.map((itemId) => {
if (itemId === 0) return 0;
const item = itemMetadataById.get(itemId);
return item && EQUIPMENT_HAND_INVENTORY_TYPES.has(item.inventoryType) ? itemId : 0;
});
return Object.freeze({
creatureId,
equipmentId,
sourceItems,
items,
});
}
async function fetchSources(sourceRoot, commit) {
await mkdir(sourceRoot, { recursive: true });
const base = `https://raw.githubusercontent.com/azerothcore/azerothcore-wotlk/${commit}/data/sql/base/db_world`;
@@ -855,6 +983,7 @@ export async function importAzerothCoreWorld({
outputRoot = defaultOutputRoot,
referenceOutput = defaultReferenceOutput,
baseStatsOutput = defaultBaseStatsOutput,
itemDbcFile = defaultItemDbc,
commit = DEFAULT_AZEROTHCORE_COMMIT,
branch = "master",
dirty = false,
@@ -865,6 +994,15 @@ export async function importAzerothCoreWorld({
const resolvedOutputRoot = path.resolve(outputRoot);
const resolvedReferenceOutput = path.resolve(referenceOutput);
if (fetch) await fetchSources(resolvedSourceRoot, commit);
const itemDbcBuffer = await readFile(path.resolve(itemDbcFile));
const itemDbcSha256 = sha256(itemDbcBuffer);
if (itemDbcSha256 !== PINNED_ITEM_DBC_SOURCE.sha256) {
throw new Error(
`Item.dbc SHA-256 ${itemDbcSha256} does not match pinned `
+ `${PINNED_ITEM_DBC_SOURCE.sha256}.`,
);
}
const itemDbc = parseItemDbc(itemDbcBuffer);
const recipes = await loadDungeonRecipes();
const recipesByMap = groupByMap(recipes);
@@ -979,6 +1117,110 @@ export async function importAzerothCoreWorld({
const entry = numeric(firstValue(row, ["CreatureID", "entry", "creatureId"]));
if (creatureEntries.has(entry)) groupedPush(spellsByEntry, entry, row);
}));
let equipmentSourceRoot = resolvedSourceRoot;
try {
await readFile(path.join(equipmentSourceRoot, "creature_equip_template.sql"));
} catch (error) {
if (error?.code !== "ENOENT" || commit !== PINNED_CREATURE_EQUIPMENT_SOURCE.commit) throw error;
equipmentSourceRoot = PINNED_CREATURE_EQUIPMENT_SOURCE.fixtureRoot;
}
const equipmentCreatureEntries = new Set(creatureEntries);
for (const rows of creaturesByMap.values()) {
for (const row of rows) {
for (const column of ["id1", "id2", "id3"]) {
const entry = numeric(firstValue(row, [column]));
if (entry > 0) equipmentCreatureEntries.add(entry);
}
}
}
const equipmentTemplateRowsByEntry = new Map();
const equipmentTemplateReport = await parseTable(
equipmentSourceRoot,
"creature_equip_template",
(row) => {
const entry = numeric(firstValue(row, ["CreatureID", "creatureId", "entry"]));
const equipmentId = numeric(firstValue(row, ["ID", "id", "equipmentId"]));
if (!equipmentCreatureEntries.has(entry)) return;
const rows = equipmentTemplateRowsByEntry.get(entry) ?? new Map();
if (rows.has(equipmentId)) {
throw new Error(`creature_equip_template has duplicate key ${entry}/${equipmentId}.`);
}
rows.set(equipmentId, row);
equipmentTemplateRowsByEntry.set(entry, rows);
},
);
if (commit === PINNED_CREATURE_EQUIPMENT_SOURCE.commit
&& equipmentTemplateReport.sha256 !== PINNED_CREATURE_EQUIPMENT_SOURCE.sha256) {
throw new Error(
`creature_equip_template.sql does not match pinned ${commit} SHA-256 `
+ `${PINNED_CREATURE_EQUIPMENT_SOURCE.sha256}.`,
);
}
tableReports.push({
...equipmentTemplateReport,
url: `https://raw.githubusercontent.com/azerothcore/azerothcore-wotlk/${commit}/data/sql/base/db_world/creature_equip_template.sql`,
});
const normalizedEquipmentTemplatesByEntry = new Map();
const equipmentItemsById = new Map();
for (const [entry, equipmentRows] of equipmentTemplateRowsByEntry) {
const normalizedRows = new Map();
for (const [equipmentId, row] of equipmentRows) {
const normalized = creatureEquipmentTemplateReference(row, itemDbc.itemsById);
normalizedRows.set(equipmentId, normalized);
for (const itemId of normalized.sourceItems) {
if (!itemId || equipmentItemsById.has(itemId)) continue;
equipmentItemsById.set(itemId, itemDbc.itemsById.get(itemId) ?? null);
}
}
normalizedEquipmentTemplatesByEntry.set(entry, normalizedRows);
}
const equipmentAssignmentsBySpawnGuid = new Map();
for (const rows of creaturesByMap.values()) {
for (const row of rows) {
const guid = numeric(firstValue(row, ["guid", "GUID"]));
const entry = numeric(firstValue(row, ["id1", "entry", "creatureId"]));
const alternateEntries = ["id2", "id3"]
.map((column) => numeric(firstValue(row, [column])))
.filter((alternateEntry) => alternateEntry > 0);
const selectorValue = firstValue(row, ["equipment_id", "equipmentId"]);
const selector = Number(selectorValue);
if (!Number.isSafeInteger(guid) || guid <= 0
|| !Number.isSafeInteger(entry) || entry <= 0
|| !Number.isInteger(selector) || selector < -128 || selector > 127) {
throw new Error(`Creature spawn ${guid} has an invalid equipment assignment.`);
}
if (equipmentAssignmentsBySpawnGuid.has(guid)) {
throw new Error(`Creature spawn GUID ${guid} appears more than once in imported maps.`);
}
equipmentAssignmentsBySpawnGuid.set(guid, { entry, alternateEntries, selector });
}
}
const equipmentTemplatesReference = Object.fromEntries(
[...normalizedEquipmentTemplatesByEntry.entries()]
.sort(([left], [right]) => left - right)
.map(([entry, rows]) => [
entry,
Object.fromEntries([...rows.entries()]
.sort(([left], [right]) => left - right)
.map(([equipmentId, row]) => [equipmentId, {
sourceItems: row.sourceItems,
items: row.items,
}])),
]),
);
const equipmentItemsReference = Object.fromEntries(
[...equipmentItemsById.entries()]
.sort(([left], [right]) => left - right)
.map(([itemId, item]) => [itemId, item]),
);
const equipmentAssignmentsReference = Object.fromEntries(
[...equipmentAssignmentsBySpawnGuid.entries()]
.sort(([left], [right]) => left - right),
);
for (const [entry, rows] of spellsByEntry) {
const template = templateByEntry.get(entry);
if (!template) continue;
@@ -1152,6 +1394,13 @@ export async function importAzerothCoreWorld({
const objectEntries = new Set(
gameObjectRows.map((row) => numeric(firstValue(row, ["id", "entry", "gameObjectId"]))),
);
const mapEquipmentEntries = new Set(entries);
for (const row of creatureRows) {
for (const column of ["id2", "id3"]) {
const entry = numeric(firstValue(row, [column]));
if (entry > 0) mapEquipmentEntries.add(entry);
}
}
const output = {
schemaVersion: 2,
revision: `azerothcore-wotlk@${commit}`,
@@ -1171,6 +1420,9 @@ export async function importAzerothCoreWorld({
},
tables: {
creature: creatureRows,
creature_equip_template: [...mapEquipmentEntries].flatMap(
(entry) => [...(equipmentTemplateRowsByEntry.get(entry)?.values() ?? [])],
),
creature_template: [...entries].flatMap((entry) => templateByEntry.get(entry) ?? []),
creature_template_model: [...entries].flatMap((entry) => modelsByEntry.get(entry) ?? []),
creature_template_spell: [...entries].flatMap((entry) => spellsByEntry.get(entry) ?? []),
@@ -1305,6 +1557,21 @@ export async function importAzerothCoreWorld({
creature: {
classLevelStats: normalizedCreatureClassStats,
templates: normalizedTemplates,
equipmentTemplatesByEntry: equipmentTemplatesReference,
equipmentAssignmentsBySpawnGuid: equipmentAssignmentsReference,
equipmentItemsById: equipmentItemsReference,
equipmentSource: {
creatureEquipTemplate: {
commit,
path: "data/sql/base/db_world/creature_equip_template.sql",
sha256: equipmentTemplateReport.sha256,
},
itemDbc: {
...PINNED_ITEM_DBC_SOURCE,
recordCount: itemDbc.recordCount,
byteLength: itemDbcBuffer.length,
},
},
},
wailingCaverns: {
requiredBossEntries: WAILING_CAVERNS_REQUIRED_ENCOUNTERS
@@ -1387,6 +1654,13 @@ export async function importAzerothCoreWorld({
playerBaseStats: 2,
},
files: sourceFiles,
externalInputs: {
itemDbc: {
...PINNED_ITEM_DBC_SOURCE,
recordCount: itemDbc.recordCount,
byteLength: itemDbcBuffer.length,
},
},
},
);
if (baseStatsOutput) {
@@ -1429,6 +1703,7 @@ function commandOptions(argv) {
else if (argument === "--source") options.sourceRoot = argv[++index];
else if (argument === "--output") options.outputRoot = argv[++index];
else if (argument === "--reference-output") options.referenceOutput = argv[++index];
else if (argument === "--item-dbc") options.itemDbcFile = argv[++index];
else if (argument === "--base-stats-output") options.baseStatsOutput = argv[++index];
else if (argument === "--commit") options.commit = argv[++index];
else if (argument === "--branch") options.branch = argv[++index];