forked from phenom/healer-man
209 lines
7.8 KiB
JavaScript
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}.`);
|