@rhyster/wow-casc-dbc
Version:
Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.
190 lines (157 loc) • 6.21 kB
text/typescript
/* eslint-disable @typescript-eslint/naming-convention */
import assert from 'node:assert';
import crypto from 'node:crypto';
import BLTEReader from '../blte.ts';
const MFST_MAGIC = 0x4d465354;
const ContentFlags = {
Install: 0x4,
LoadOnWindows: 0x8,
LoadOnMacOS: 0x10,
x86_32: 0x20,
x86_64: 0x40,
LowViolence: 0x80,
DoNotLoad: 0x100,
UpdatePlugin: 0x800,
ARM64: 0x8000,
Encrypted: 0x8000000,
NoNameHash: 0x10000000,
UncommonResolution: 0x20000000,
Bundle: 0x40000000,
NoCompression: 0x80000000,
} as const;
const LocaleFlags = {
enUS: 0x2,
koKR: 0x4,
frFR: 0x10,
deDE: 0x20,
zhCN: 0x40,
esES: 0x80,
zhTW: 0x100,
enGB: 0x200,
// enCN: 0x400,
// enTW: 0x800,
esMX: 0x1000,
ruRU: 0x2000,
ptBR: 0x4000,
itIT: 0x8000,
ptPT: 0x10000,
} as const;
interface FileInfo {
cKey: string,
contentFlags: number,
localeFlags: number,
}
interface RootData {
fileDataID2CKey: Map<number, FileInfo[]>,
nameHash2FileDataID: Map<string, number>,
}
const parseRootFile = (inputBuffer: Buffer, eKey: string, cKey: string): RootData => {
const reader = new BLTEReader(inputBuffer, eKey);
reader.processBytes();
const { buffer } = reader;
const rootHash = crypto.createHash('md5').update(buffer).digest('hex');
assert(rootHash === cKey, `Invalid root hash: expected ${cKey}, got ${rootHash}`);
const fileDataID2CKey = new Map<number, FileInfo[]>();
const nameHash2FileDataID = new Map<string, number>();
const magic = buffer.readUInt32LE(0);
if (magic === MFST_MAGIC) {
// post 8.2.0
const firstEntry = buffer.readUInt32LE(4);
const newFormat = firstEntry < 100; // post 10.1.7
const headerSize = newFormat ? firstEntry : 12;
const version = newFormat ? buffer.readUInt32LE(8) : 0;
const totalFileCount = newFormat ? buffer.readUInt32LE(12) : firstEntry;
const namedFileCount = newFormat ? buffer.readUInt32LE(16) : buffer.readUInt32LE(8);
assert(version >= 0 && version <= 2, `Invalid root version: ${version.toString()}`);
const allowNonNamedFiles = totalFileCount !== namedFileCount;
let pointer = headerSize;
while (pointer < buffer.byteLength) {
const numRecords = buffer.readUInt32LE(pointer);
let contentFlags: number;
let localeFlags: number;
if (version >= 2) {
localeFlags = buffer.readUInt32LE(pointer + 4);
const contentFlags1 = buffer.readUInt32LE(pointer + 8);
const contentFlags2 = buffer.readUInt32LE(pointer + 12);
const contentFlags3 = buffer.readUInt8(pointer + 16);
// eslint-disable-next-line no-bitwise
contentFlags = (contentFlags1 | contentFlags2 | (contentFlags3 << 17));
pointer += 17;
} else {
contentFlags = buffer.readUInt32LE(pointer + 4);
localeFlags = buffer.readUInt32LE(pointer + 8);
pointer += 12;
}
const fileDataIDs = [];
let currFileDataID = -1;
for (let i = 0; i < numRecords; i += 1) {
currFileDataID += buffer.readUInt32LE(pointer) + 1;
fileDataIDs.push(currFileDataID);
pointer += 4;
}
for (let i = 0; i < numRecords; i += 1) {
const fileDataID = fileDataIDs[i];
const fileCKey = buffer.toString('hex', pointer, pointer + 16);
pointer += 16;
if (fileDataID2CKey.has(fileDataID)) {
fileDataID2CKey.get(fileDataID)?.push({
cKey: fileCKey,
contentFlags,
localeFlags,
});
} else {
fileDataID2CKey.set(fileDataID, [
{ cKey: fileCKey, contentFlags, localeFlags },
]);
}
}
// eslint-disable-next-line no-bitwise
if (!(allowNonNamedFiles && (contentFlags & ContentFlags.NoNameHash))) {
for (let i = 0; i < numRecords; i += 1) {
const fileDataID = fileDataIDs[i];
const nameHash = buffer.readBigUInt64LE(pointer).toString(16).padStart(16, '0');
pointer += 8;
nameHash2FileDataID.set(nameHash, fileDataID);
}
}
}
} else {
// pre 8.2.0
let pointer = 0;
while (pointer < buffer.byteLength) {
const numRecords = buffer.readUInt32LE(pointer);
const contentFlags = buffer.readUInt32LE(pointer + 4);
const localeFlags = buffer.readUInt32LE(pointer + 8);
pointer += 12;
const fileDataIDs = [];
let currFileDataID = -1;
for (let i = 0; i < numRecords; i += 1) {
currFileDataID += buffer.readUInt32LE(pointer) + 1;
fileDataIDs.push(currFileDataID);
pointer += 4;
}
for (let i = 0; i < numRecords; i += 1) {
const fileDataID = fileDataIDs[i];
const fileCKey = buffer.toString('hex', pointer, pointer + 16);
const nameHash = buffer.toString('hex', pointer + 16, pointer + 24);
pointer += 24;
if (fileDataID2CKey.has(fileDataID)) {
fileDataID2CKey.get(fileDataID)?.push({
cKey: fileCKey,
contentFlags,
localeFlags,
});
} else {
fileDataID2CKey.set(fileDataID, [
{ cKey: fileCKey, contentFlags, localeFlags },
]);
}
nameHash2FileDataID.set(nameHash, fileDataID);
}
}
}
return { fileDataID2CKey, nameHash2FileDataID };
};
export default parseRootFile;
export { ContentFlags, LocaleFlags };
export type { FileInfo, RootData };