Files
healer-man/scripts/dungeon-pipeline/import-azerothcore-world.test.mjs
T
2026-10-06 22:25:34 -04:00

203 lines
8.2 KiB
JavaScript

import assert from "node:assert/strict";
import { before, describe, it } from "node:test";
import {
creatureEquipmentTemplateReference,
creatureTemplateReference,
creatureTemplateUsesImmunitiesReference,
parseItemDbc,
parseTable,
PINNED_CREATURE_EQUIPMENT_SOURCE,
PINNED_CREATURE_IMMUNITIES_SOURCE,
PINNED_ITEM_DBC_SOURCE,
} from "./import-azerothcore-world.mjs";
describe("AzerothCore creature immunity import", () => {
let creatureImmunitiesById;
let tableReport;
before(async () => {
creatureImmunitiesById = new Map();
tableReport = await parseTable(
PINNED_CREATURE_IMMUNITIES_SOURCE.fixtureRoot,
"creature_immunities",
(row) => creatureImmunitiesById.set(Number(row.ID), row),
);
});
it("uses the pinned schema, source commit, and exact table hash", () => {
assert.equal(PINNED_CREATURE_IMMUNITIES_SOURCE.commit, "4d9d1d4b5723e819c5c390f6ca1177283dbfb8e3");
assert.equal(PINNED_CREATURE_IMMUNITIES_SOURCE.path, "data/sql/base/db_world/creature_immunities.sql");
assert.equal(PINNED_CREATURE_IMMUNITIES_SOURCE.sha256,
"aee81a1d8b99986f8498a2b4c51b64eaf4b9095a0d1a12bbf3705d9588a90a13");
assert.ok(tableReport.rows > 0);
assert.equal(tableReport.sha256, PINNED_CREATURE_IMMUNITIES_SOURCE.sha256);
assert.ok([
"ID",
"SchoolMask",
"MechanicsMask",
].every((column) => tableReport.columns.includes(column)));
assert.deepEqual({
ID: creatureImmunitiesById.get(-93).ID,
SchoolMask: creatureImmunitiesById.get(-93).SchoolMask,
MechanicsMask: creatureImmunitiesById.get(-93).MechanicsMask,
}, {
ID: -93,
SchoolMask: 0,
MechanicsMask: 16_777_248,
});
});
it("resolves Lady Anacondra's immunity FK to Fear and Horror only", () => {
const lady = creatureTemplateReference({
entry: 3671,
name: "Lady Anacondra",
CreatureImmunitiesId: -93,
}, creatureImmunitiesById);
assert.equal(lady.entry, 3671);
assert.equal(lady.mechanicImmuneMask, 0x0080_0010);
assert.deepEqual(lady.mechanicImmunities, ["fear", "horror"]);
assert.equal(lady.spellSchoolImmuneMask, 0);
assert.deepEqual(lady.schoolImmunities, []);
assert.ok(!lady.mechanicImmunities.includes("silence"));
assert.ok(!lady.mechanicImmunities.includes("interrupt"));
});
it("retains source-backed Silence and Interrupt immunity on rows that declare them", () => {
const immune = creatureTemplateReference({
entry: 99_377,
name: "Source immunity control",
CreatureImmunitiesId: -377,
}, creatureImmunitiesById);
assert.ok(immune.mechanicImmunities.includes("silence"));
assert.ok(immune.mechanicImmunities.includes("interrupt"));
});
it("preserves legacy literal masks and fails closed on a missing FK", () => {
const legacy = creatureTemplateReference({
entry: 1,
mechanic_immune_mask: 0x0080_0010,
spell_school_immune_mask: 0x04,
});
assert.equal(legacy.mechanicImmuneMask, 0x0080_0010);
assert.deepEqual(legacy.mechanicImmunities, ["fear", "horror"]);
assert.equal(legacy.spellSchoolImmuneMask, 0x04);
assert.deepEqual(legacy.schoolImmunities, ["fire"]);
assert.throws(() => creatureTemplateReference({
entry: 3671,
CreatureImmunitiesId: -999_999,
}, creatureImmunitiesById), /references missing creature_immunities\.ID -999999/);
});
it("loads the FK table only for schemas that declare CreatureImmunitiesId", () => {
const legacySchema = "CREATE TABLE `creature_template` (\n `mechanic_immune_mask` int NOT NULL DEFAULT '0'\n) ENGINE=InnoDB;";
const fkSchema = "CREATE TABLE `creature_template` (\n `CreatureImmunitiesId` int NOT NULL DEFAULT '0'\n) ENGINE=InnoDB;";
assert.equal(creatureTemplateUsesImmunitiesReference(legacySchema), false);
assert.equal(creatureTemplateUsesImmunitiesReference(fkSchema), true);
});
});
describe("AzerothCore creature blocking flags import", () => {
it("preserves NO_BLOCK and other source bits without an equipment requirement", () => {
assert.equal(creatureTemplateReference({ entry: 1666, flags_extra: 0 }).flagsExtra, 0);
assert.equal(creatureTemplateReference({ entry: 2, flags_extra: 0x110 }).flagsExtra, 0x110);
assert.equal(creatureTemplateReference({ entry: 3, flagsExtra: 0x10 }).flagsExtra, 0x10);
});
it("leaves unknown flags absent and rejects invalid source masks", () => {
assert.equal(creatureTemplateReference({ entry: 4 }).flagsExtra, undefined);
for (const flags_extra of [-1, 0.5, 0x1_0000_0000, "invalid"]) {
assert.throws(() => creatureTemplateReference({ entry: 4, flags_extra }), /unsupported flags_extra/);
}
});
});
describe("AzerothCore creature equipment import", () => {
it("pins and parses the exact creature equipment template SQL", async () => {
const rows = [];
const report = await parseTable(
PINNED_CREATURE_EQUIPMENT_SOURCE.fixtureRoot,
"creature_equip_template",
(row) => {
if (Number(row.CreatureID) === 32239) rows.push(row);
},
);
assert.equal(PINNED_CREATURE_EQUIPMENT_SOURCE.commit, "4d9d1d4b5723e819c5c390f6ca1177283dbfb8e3");
assert.equal(PINNED_CREATURE_EQUIPMENT_SOURCE.path,
"data/sql/base/db_world/creature_equip_template.sql");
assert.equal(PINNED_CREATURE_EQUIPMENT_SOURCE.sha256,
"877205c149fd447b08f0ff2c65976f20c862cd42f068a6fcc294a76c7108936a");
assert.equal(report.sha256, PINNED_CREATURE_EQUIPMENT_SOURCE.sha256);
assert.equal(report.rows, 10_854);
assert.deepEqual(rows.map((row) => ({
CreatureID: row.CreatureID,
ID: row.ID,
ItemID1: row.ItemID1,
ItemID2: row.ItemID2,
ItemID3: row.ItemID3,
})), [{ CreatureID: 32239, ID: 2, ItemID1: 13262, ItemID2: 0, ItemID3: 0 }]);
assert.ok(["CreatureID", "ID", "ItemID1", "ItemID2", "ItemID3"]
.every((column) => report.columns.includes(column)));
});
it("parses Item.dbc Class/InventoryType and applies the core hand-item whitelist", () => {
const rows = [
{ id: 100, itemClass: 2, inventoryType: 13 },
{ id: 101, itemClass: 4, inventoryType: 14 },
{ id: 102, itemClass: 2, inventoryType: 2 },
];
const buffer = Buffer.alloc(20 + rows.length * 32 + 1);
buffer.write("WDBC", 0, "ascii");
buffer.writeUInt32LE(rows.length, 4);
buffer.writeUInt32LE(PINNED_ITEM_DBC_SOURCE.fields, 8);
buffer.writeUInt32LE(PINNED_ITEM_DBC_SOURCE.recordSize, 12);
buffer.writeUInt32LE(1, 16);
rows.forEach((row, index) => {
const offset = 20 + index * PINNED_ITEM_DBC_SOURCE.recordSize;
buffer.writeUInt32LE(row.id, offset);
buffer.writeUInt32LE(row.itemClass, offset + PINNED_ITEM_DBC_SOURCE.classField * 4);
buffer.writeUInt32LE(row.inventoryType, offset + PINNED_ITEM_DBC_SOURCE.inventoryTypeField * 4);
});
const itemDbc = parseItemDbc(buffer);
assert.deepEqual(itemDbc.itemsById.get(100), { itemClass: 2, inventoryType: 13 });
assert.deepEqual(itemDbc.itemsById.get(101), { itemClass: 4, inventoryType: 14 });
assert.equal(itemDbc.itemsById.get(102).inventoryType, 2);
const equipment = creatureEquipmentTemplateReference({
CreatureID: 99,
ID: 1,
ItemID1: 100,
ItemID2: 101,
ItemID3: 102,
}, itemDbc.itemsById);
assert.deepEqual(equipment, {
creatureId: 99,
equipmentId: 1,
sourceItems: [100, 101, 102],
items: [100, 101, 0],
});
assert.equal(PINNED_ITEM_DBC_SOURCE.sha256,
"22061a811523790ddd2ee95b36ade2ac6e0c79a8b3121324d32a639acf50567f");
});
it("rejects unsupported DBC shapes, duplicate item IDs, and invalid equipment keys", () => {
const invalid = Buffer.from("not DBC");
assert.throws(() => parseItemDbc(invalid), /invalid WDBC header/);
const duplicate = Buffer.alloc(20 + 2 * 32 + 1);
duplicate.write("WDBC", 0, "ascii");
duplicate.writeUInt32LE(2, 4);
duplicate.writeUInt32LE(8, 8);
duplicate.writeUInt32LE(32, 12);
duplicate.writeUInt32LE(1, 16);
duplicate.writeUInt32LE(5, 20);
duplicate.writeUInt32LE(5, 52);
assert.throws(() => parseItemDbc(duplicate), /duplicate item ID 5/);
assert.throws(() => creatureEquipmentTemplateReference({
CreatureID: 99,
ID: 0,
}, new Map()), /invalid key/);
});
});