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
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user