Files
healer-man/scripts/generate-wow335-pet-families.mjs

209 lines
7.8 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { MpqArchive } = require("stormlib-js");
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultArchive = path.resolve(projectRoot, "..", "wow335a", "Data", "enUS", "patch-enUS.MPQ");
const defaultOutput = path.join(projectRoot, "src", "game", "generated", "wow335PetFamilyData.json");
const archivePath = path.resolve(process.argv[2] ?? defaultArchive);
const outputPath = path.resolve(process.argv[3] ?? defaultOutput);
function assertDbc(buffer, name, minimumFields) {
if (buffer.toString("ascii", 0, 4) !== "WDBC") throw new Error(`${name} is not WDBC data.`);
const fieldCount = buffer.readUInt32LE(8);
const recordSize = buffer.readUInt32LE(12);
if (fieldCount < minimumFields || recordSize < fieldCount * 4) {
throw new Error(`${name} has an unsupported schema.`);
}
}
function rowCount(buffer) {
return buffer.readUInt32LE(4);
}
function recordSize(buffer) {
return buffer.readUInt32LE(12);
}
function recordBase(index, size) {
return 20 + index * size;
}
function textAt(buffer, base, firstField, shift = 0) {
const count = rowCount(buffer);
const stringsOffset = 20 + count * recordSize(buffer);
const stringSize = buffer.readUInt32LE(16);
for (let locale = 0; locale < 16; locale += 1) {
const offset = buffer.readUInt32LE(base + (firstField + shift + locale) * 4);
if (!offset || offset >= stringSize) continue;
const start = stringsOffset + offset;
const end = buffer.indexOf(0, start);
const value = buffer.toString("utf8", start, end < 0 ? buffer.length : end);
if (value) return value;
}
return "";
}
function spellRows(buffer, durations) {
assertDbc(buffer, "Spell.dbc", 234);
const count = rowCount(buffer);
const size = recordSize(buffer);
const fields = buffer.readUInt32LE(8);
const shift = fields >= 239 ? 6 : 0;
const shifted = (field) => field >= 77 ? field + shift : field;
const rows = new Map();
for (let index = 0; index < count; index += 1) {
const base = recordBase(index, size);
const unsigned = (field) => buffer.readUInt32LE(base + field * 4);
const signed = (field) => buffer.readInt32LE(base + field * 4);
const float = (field) => buffer.readFloatLE(base + field * 4);
const effects = Array.from({ length: 3 }, (_, effectIndex) => {
const effectType = unsigned(71 + effectIndex);
const auraType = unsigned(shifted(95 + effectIndex));
const basePoints = signed(shifted(80 + effectIndex)) + 1;
const triggerSpellId = unsigned(shifted(116 + effectIndex));
return {
index: effectIndex,
effectType,
auraType,
mechanic: unsigned(shifted(83 + effectIndex)),
basePoints,
dieSides: signed(74 + effectIndex),
pointsPerLevel: float(shifted(77 + effectIndex)),
periodMs: unsigned(shifted(98 + effectIndex)),
miscValue: signed(shifted(110 + effectIndex)),
triggerSpellId,
coefficient: float(shifted(229 + effectIndex)),
};
}).filter((effect) => effect.effectType || effect.auraType || effect.basePoints !== 1 || effect.triggerSpellId);
const attributes0 = unsigned(4);
rows.set(unsigned(0), {
spellId: unsigned(0),
name: textAt(buffer, base, 136, shift),
rankText: textAt(buffer, base, 153, shift),
mechanic: unsigned(3),
requiredLevel: unsigned(39),
passive: Boolean(attributes0 & 0x40),
attributesEx1: unsigned(5),
powerType: signed(41),
powerCost: unsigned(42),
cooldownMs: unsigned(29),
categoryCooldownMs: unsigned(30),
globalCooldownMs: unsigned(shifted(206)),
auraInterruptFlags: unsigned(32),
schoolMask: unsigned(shifted(225)),
procFlags: unsigned(34),
procChance: unsigned(35),
procCharges: unsigned(36),
durationMs: durations.get(unsigned(40)) ?? 0,
effects,
});
}
return rows;
}
function durationTable(buffer) {
assertDbc(buffer, "SpellDuration.dbc", 4);
const durations = new Map();
const size = recordSize(buffer);
for (let index = 0; index < rowCount(buffer); index += 1) {
const base = recordBase(index, size);
durations.set(buffer.readUInt32LE(base), buffer.readInt32LE(base + 3 * 4));
}
return durations;
}
function familyRows(buffer) {
assertDbc(buffer, "CreatureFamily.dbc", 28);
const families = [];
const size = recordSize(buffer);
for (let index = 0; index < rowCount(buffer); index += 1) {
const base = recordBase(index, size);
const minScale = buffer.readFloatLE(base + 1 * 4);
const maxScale = buffer.readFloatLE(base + 3 * 4);
const petTalentType = buffer.readInt32LE(base + 8 * 4);
const name = textAt(buffer, base, 10);
if (!name) continue;
families.push({
id: buffer.readUInt32LE(base),
name,
minScale,
minScaleLevel: buffer.readUInt32LE(base + 2 * 4),
maxScale,
maxScaleLevel: buffer.readUInt32LE(base + 4 * 4),
petFoodMask: buffer.readUInt32LE(base + 7 * 4),
petTalentType,
familySkillLineId: buffer.readUInt32LE(base + 5 * 4),
commonPetSkillLineId: buffer.readUInt32LE(base + 6 * 4),
categoryEnumId: buffer.readUInt32LE(base + 9 * 4),
});
}
return families;
}
function skillRows(buffer, lineIds, spells) {
assertDbc(buffer, "SkillLineAbility.dbc", 14);
const result = new Map([...lineIds].map((id) => [id, []]));
const size = recordSize(buffer);
for (let index = 0; index < rowCount(buffer); index += 1) {
const base = recordBase(index, size);
const lineId = buffer.readUInt32LE(base + 1 * 4);
if (!result.has(lineId)) continue;
const spellId = buffer.readUInt32LE(base + 2 * 4);
const spell = spells.get(spellId);
if (!spell) continue;
result.get(lineId).push({
...spell,
acquireMethod: buffer.readUInt32LE(base + 9 * 4),
raceMask: buffer.readUInt32LE(base + 3 * 4),
classMask: buffer.readUInt32LE(base + 4 * 4),
minSkill: buffer.readUInt32LE(base + 7 * 4),
supersededBySpellId: buffer.readUInt32LE(base + 8 * 4),
});
}
for (const rows of result.values()) {
rows.sort((left, right) => left.requiredLevel - right.requiredLevel || left.spellId - right.spellId);
}
return result;
}
const archive = MpqArchive.open(archivePath);
let output;
try {
const extract = (name) => archive.extractFile(`DBFilesClient\\${name}.dbc`);
const familyBuffer = extract("CreatureFamily");
const skillLineBuffer = extract("SkillLineAbility");
const spellBuffer = extract("Spell");
const durations = durationTable(extract("SpellDuration"));
const spells = spellRows(spellBuffer, durations);
const families = familyRows(familyBuffer);
const lineIds = new Set(families.flatMap((family) => [family.familySkillLineId, family.commonPetSkillLineId]).filter(Boolean));
const skills = skillRows(skillLineBuffer, lineIds, spells);
output = {
source: {
product: "World of Warcraft",
build: "3.3.5a.12340",
archive: path.relative(projectRoot, archivePath).replaceAll("\\", "/"),
familyRows: rowCount(familyBuffer),
skillLineAbilityRows: rowCount(skillLineBuffer),
spellRows: rowCount(spellBuffer),
},
families: families.map((family) => ({
...family,
hunterPet: family.petTalentType >= 0,
familySkills: skills.get(family.familySkillLineId) ?? [],
commonPetSkills: skills.get(family.commonPetSkillLineId) ?? [],
})),
};
} finally {
archive.close();
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(output)}\n`);
console.log(`Generated ${output.families.filter((family) => family.hunterPet).length} Hunter pet family records at ${outputPath}.`);