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/); }); });