import fs from "node:fs"; import path from "node:path"; const dbcDirectory = (root, patch, fileName) => path.join(root, patch, "DBFilesClient", fileName); function firstExisting(candidates, exists) { return candidates.find((candidate) => exists(candidate)) ?? candidates.at(-1); } /** Resolve independent source paths for classic and CoA data in the combined importer. */ export function resolveCoaSourcePaths({ extractedClientRoot, legacyExtractedRoot, classicSpellOverride, exists = fs.existsSync, }) { const area52Spell = dbcDirectory(path.join(extractedClientRoot, "area-52"), "patch-D", "Spell.dbc"); const globalSpell = dbcDirectory(extractedClientRoot, "patch-T", "Spell.dbc"); const legacySpell = dbcDirectory(legacyExtractedRoot, "patch-T", "Spell.dbc"); const area52SkillLineAbility = dbcDirectory( path.join(extractedClientRoot, "area-52"), "patch-D", "SkillLineAbility.dbc", ); const globalSkillLineAbility = dbcDirectory(extractedClientRoot, "patch-M", "SkillLineAbility.dbc"); const legacySkillLineAbility = dbcDirectory(legacyExtractedRoot, "patch-M", "SkillLineAbility.dbc"); const classicSpellDbcPath = classicSpellOverride ? path.resolve(classicSpellOverride) : firstExisting([area52Spell, globalSpell, legacySpell], exists); const classicSiblingSkillLineAbility = path.join(path.dirname(classicSpellDbcPath), "SkillLineAbility.dbc"); return { classicSpellDbcPath, classicSkillLineAbilityDbcPath: exists(classicSiblingSkillLineAbility) ? classicSiblingSkillLineAbility : firstExisting([area52SkillLineAbility, globalSkillLineAbility, legacySkillLineAbility], exists), coaSpellDbcPath: firstExisting([globalSpell, legacySpell], exists), coaSkillLineAbilityDbcPath: firstExisting([globalSkillLineAbility, legacySkillLineAbility], exists), }; } /** Collect DBC IDs whose classic metadata must remain available beside CoA metadata. */ export function collectRealmSpellIds({ classicClassIds, coaClassIds, classSpellRows, classicRankCandidates, coaRankCandidates, coaEntrySpellIds, }) { const classicIds = new Set(); const coaIds = new Set(coaEntrySpellIds ?? []); const classicClasses = new Set(classicClassIds); const coaClasses = new Set(coaClassIds); for (const row of classSpellRows) { if (classicClasses.has(row.classId)) classicIds.add(row.id); if (coaClasses.has(row.classId)) coaIds.add(row.id); } for (const candidate of classicRankCandidates) classicIds.add(candidate.id); for (const candidate of coaRankCandidates) coaIds.add(candidate.id); return { classicIds, coaIds }; } /** Return IDs that need a separate CoA wrapper to protect classic metadata. */ export function sharedRealmSpellIds(classicIds, coaIds) { return [...coaIds].filter((spellId) => classicIds.has(spellId)).sort((left, right) => left - right); } /** Include linked base-client payloads, including effect 168's miscValue spell. */ export function expandCoaSpellIds(buffer, initialIds) { if (buffer.length < 20 || buffer.toString("ascii", 0, 4) !== "WDBC" || buffer.readUInt32LE(8) < 234) throw new Error("CoA Spell.dbc does not expose the 234-field WDBC schema"); const recordCount = buffer.readUInt32LE(4); const recordSize = buffer.readUInt32LE(12); if (recordSize < 234 * 4 || 20 + recordCount * recordSize > buffer.length) throw new Error("CoA Spell.dbc has invalid record lengths"); const rows = new Map(); for (let index = 0; index < recordCount; index += 1) { const base = 20 + index * recordSize; const id = buffer.readUInt32LE(base); if (!rows.has(id)) rows.set(id, base); } const ids = new Set(initialIds); const queue = [...ids]; for (let cursor = 0; cursor < queue.length; cursor += 1) { const base = rows.get(queue[cursor]); if (base === undefined) continue; for (let index = 0; index < 3; index += 1) { const childId = buffer.readUInt32LE(base + (116 + index) * 4) || (buffer.readUInt32LE(base + (71 + index) * 4) === 168 ? buffer.readUInt32LE(base + (110 + index) * 4) : 0); if (childId > 0 && !ids.has(childId)) { ids.add(childId); queue.push(childId); } } } return ids; }