import * as fs from "fs"; import * as path from "path"; import * as camelCase from "camelcase"; export interface IGlyphProperties { id: number; spellId: number; } export interface IAchievement { id: number; faction: number; titleLang0: string; descriptionLang0: string; category: number; points: number; flags: number; iconId: number; } export interface IAchievementCategory { id: number; parent: number; nameLang0: string; } export interface IItemDbc { id: number; classId: number; subclassId: number; displayInfoId: number; inventoryType: number; } export interface IItemRetailDbc { id: number; inventoryType: number; } export interface IItemAppearanceDbc { id: number; itemDisplayInfoId: number; } export interface IItemModifiedAppearanceDbc { id: number; itemId: number; itemAppearanceId: number; } export interface IItemDisplayInfoDbc { id: number; inventoryIcon0: number; } export interface IMountDbc { id: number; sourceSpellId: number; } export interface IMountXDisplayDbc { id: number; creatureDisplayInfoId: number; mountId: number; } export interface ISkillDbc { id: number; categoryId: number; skillCostId: number; name: string; spellIcon: number; altVerb: string; canLink: number; } export interface ISpellDbc { id: number; mechanic: number; spellIconId: number; } export interface ISpellItemEnchantmentDbc { id: number; srcItemId: number; } export interface ISpellIcon { id: number; textureFilename: string; } export interface ITalent { id: number; tabId: number; tierId: number; columnIndex: number; spellRank0: number; spellRank1: number; spellRank2: number; spellRank3: number; spellRank4: number; prereqTalent0: number; prereqRank0: number; } export interface ITalentTab { id: number; nameLang0: string; spellIconId: number; classMask: number; } export interface IFactionDbc { id: number; reputationId: number; name: string; } export interface IAreas { id: number; zoneName: string; mapId: number; areaId: number; } interface IAsyncGeneratorWithArrayMethods { [Symbol.asyncIterator](): AsyncGenerator; toArray(): Promise; map(fn: (t: T) => M): IAsyncGeneratorWithArrayMethods; filter(fn: (t: T) => boolean): IAsyncGeneratorWithArrayMethods; find(fn: (t: T) => boolean): Promise; } class ArrayAsAsyncGenerator implements IAsyncGeneratorWithArrayMethods { private data: T[]; public constructor(data: T[]) { this.data = data; } async toArray(): Promise { return this.data; } async *[Symbol.asyncIterator](): AsyncGenerator { for (const x of this.data) { yield x; } } public map(fn: (t: T) => M): ArrayAsAsyncGenerator { return new ArrayAsAsyncGenerator(this.data.map(fn)); } filter(fn: (t: T) => boolean): ArrayAsAsyncGenerator { return new ArrayAsAsyncGenerator(this.data.filter(fn)); } async find(fn: (t: T) => boolean): Promise { return this.data.find(fn); } } class AsyncGenWrapper implements IAsyncGeneratorWithArrayMethods { private gen: AsyncGenerator; public constructor(gen: AsyncGenerator) { this.gen = gen; } public static from(array: T[]): AsyncGenWrapper { return new AsyncGenWrapper( (async function* () { for (const x of array) { yield x; } })(), ); } public async *[Symbol.asyncIterator](): AsyncGenerator { for await (const x of this.gen) { yield x; } } public async toArray(): Promise { const values = []; for await (const x of this) { values.push(x); } return values; } private wrap(g: (that: AsyncGenWrapper) => AsyncGenerator): AsyncGenWrapper { return new AsyncGenWrapper(g(this)); } public map(fn: (t: T) => M): AsyncGenWrapper { return this.wrap(async function* (me) { for await (const x of me) { yield fn(x); } }); } public filter(fn: (t: T) => boolean): AsyncGenWrapper { return this.wrap(async function* (me) { for await (const x of me) { if (fn(x)) { yield x; } } }); } public async find(fn: (t: T) => boolean): Promise { for await (const x of this) { if (fn(x)) { return x; } } } } class DbcReader { private filePath: string; private fields: string[]; public constructor(filePath: string, keepFields: string[] = []) { this.filePath = filePath; this.fields = keepFields; } public async *read(): AsyncGenerator { const stream = fs.createReadStream(this.filePath); const itr = this.parseCsv(stream); const headerLine = await itr.next(); if (headerLine.done === true) { return; } const headerCols = headerLine.value.map((header) => camelCase(header).replace(/[[\]]/g, "")); for await (const arr of itr) { const cols = arr.map((value) => { if (value === "") { return value; } return isNaN(+value) ? value : parseInt(value, 10); }); const row = {}; headerCols.forEach((header, headerIdx) => { if (this.fields.length === 0 || this.fields.includes(header)) { row[header] = cols[headerIdx]; } }); yield row as T; } } private async *parseCsv(stream: fs.ReadStream): AsyncGenerator { // Adapted from https://stackoverflow.com/a/14991797 const arr = []; let col = 0; let quote = false; // 'true' means we're inside a quoted field for await (const chunk of stream) { const str = chunk.toString(); // Iterate over each character, keep track of current column (of the returned array) for (let c = 0; c < str.length; ++c) { const ch = str[c]; const nch = str[c + 1]; // Current character, next character if (!(col in arr)) { arr[col] = ""; // Create a new column (start with empty string) if necessary } // If the current character is a quotation mark, and we're inside a // quoted field, and the next character is also a quotation mark, // add a quotation mark to the current column and skip the next character if (ch == '"' && quote && nch == '"') { arr[col] += ch; ++c; continue; } // If it's just one quotation mark, begin/end quoted field if (ch == '"') { quote = !quote; continue; } // If it's a comma and we're not in a quoted field, move on to the next column if (ch == "," && !quote) { ++col; continue; } // If it's a newline (CRLF) and we're not in a quoted field, skip the next character // and move on to the next row and move to column 0 of that new row if (ch == "\r" && nch == "\n" && !quote) { yield arr; arr.length = 0; // Clear the row col = 0; ++c; continue; } // If it's a newline (LF or CR) and we're not in a quoted field, // move on to the next row and move to column 0 of that new row if (!quote && (ch == "\r" || ch == "\n")) { yield arr; arr.length = 0; // Clear the row col = 0; continue; } // Otherwise, append the current character to the current column arr[col] += ch; } } } } const dir = path.join(process.cwd(), "data"); export const DbcFiles = { achievement: path.join(dir, "Achievement_3.3.5_12340.csv"), achievementCategory: path.join(dir, "AchievementCategory_3.3.5_12340.csv"), areas: path.join(dir, "Areas.csv"), faction: path.join(dir, "Factions.csv"), glyphProperties: path.join(dir, "GlyphProperties_3.3.5_12340.csv"), item: path.join(dir, "Item_3.3.5_12340.csv"), itemRetail: path.join(dir, "Item_9.2.0_41462.csv"), itemAppearance: path.join(dir, "ItemAppearance_9.2.0_41462.csv"), itemModifiedAppearance: path.join(dir, "ItemModifiedAppearance_9.2.0_41462.csv"), itemDisplayInfo: path.join(dir, "ItemDisplayInfo_3.3.5_12340.csv"), mount: path.join(dir, "Mount_9.2.0_41462.csv"), mountDisplay: path.join(dir, "MountXDisplay_9.2.0_41462.csv"), skill: path.join(dir, "Skills.csv"), spell: path.join(dir, "Spell_3.3.5_12340.csv"), spellItemEnchantment: path.join(dir, "SpellItemEnchantment_3.3.5_12340.csv"), spellIcon: path.join(dir, "SpellIcon_3.3.5_12340.csv"), talent: path.join(dir, "Talent_3.3.5_12340.csv"), talentTab: path.join(dir, "TalentTab_3.3.5_12340.csv"), }; const dbcFields = { achievement: ["id", "faction", "titleLang0", "descriptionLang0", "category", "points", "flags", "iconId"], achievementCategory: ["id", "parent", "nameLang0"], areas: ["id", "zoneName", "mapId", "areaId"], glyphProperties: ["id", "spellId"], item: ["id", "classId", "subclassId", "displayInfoId", "inventoryType"], itemRetail: ["id", "inventoryType"], itemAppearance: ["id", "itemDisplayInfoId"], itemModifiedAppearance: ["id", "itemId", "itemAppearanceId"], itemDisplayInfo: ["id", "inventoryIcon0"], mount: ["id", "sourceSpellId"], mountDisplay: ["id", "creatureDisplayInfoId", "mountId"], skill: ["id", "name"], spell: ["id", "mechanic", "spellIconId"], spellItemEnchantment: ["id", "srcItemId"], spellIcon: ["id", "textureFilename"], talent: [ "id", "tabId", "tierId", "columnIndex", "spellRank0", "spellRank1", "spellRank2", "spellRank3", "spellRank4", "prereqTalent0", "prereqRank0", ], talentTab: ["id", "nameLang0", "spellIconId", "classMask"], faction: ["id", "reputationId", "name"], }; export class DbcManager { private _achievement: IAchievement[]; private _achievementCategory: IAchievementCategory[]; private _areas: IAreas[]; private _glyphProperties: IGlyphProperties[]; private _faction: IFactionDbc[]; private _item: IItemDbc[]; private _itemRetail: IItemRetailDbc[]; private _itemAppearance: IItemAppearanceDbc[]; private _itemModifiedAppearance: IItemModifiedAppearanceDbc[]; private _itemDisplayInfo: IItemDisplayInfoDbc[]; private _mount: IMountDbc[]; private _mountDisplay: IMountXDisplayDbc[]; private _skill: ISkillDbc[]; private _spell: ISpellDbc[]; private _spellItemEnchantment: ISpellItemEnchantmentDbc[]; private _spellIcon: ISpellIcon[]; private _talent: ITalent[]; private _talentTab: ITalentTab[]; public async loadAllFiles(): Promise { this._achievement = await this.read(DbcFiles.achievement, dbcFields.achievement).toArray(); this._achievementCategory = await this.read( DbcFiles.achievementCategory, dbcFields.achievementCategory, ).toArray(); this._areas = await this.read(DbcFiles.areas).toArray(); this._glyphProperties = await this.read(DbcFiles.glyphProperties, dbcFields.glyphProperties).toArray(); this._item = await this.read(DbcFiles.item, dbcFields.item).toArray(); this._itemRetail = await this.read(DbcFiles.itemRetail, dbcFields.itemRetail).toArray(); this._itemAppearance = await this.read(DbcFiles.itemAppearance, dbcFields.itemAppearance).toArray(); this._itemModifiedAppearance = await this.read( DbcFiles.itemModifiedAppearance, dbcFields.itemModifiedAppearance, ).toArray(); this._itemDisplayInfo = await this.read(DbcFiles.itemDisplayInfo, dbcFields.itemDisplayInfo).toArray(); this._mount = await this.read(DbcFiles.mount, dbcFields.mount).toArray(); this._mountDisplay = await this.read(DbcFiles.mountDisplay, dbcFields.mountDisplay).toArray(); this._skill = await this.read(DbcFiles.skill).toArray(); this._spell = await this.read(DbcFiles.spell, dbcFields.spell).toArray(); this._spellItemEnchantment = await this.read( DbcFiles.spellItemEnchantment, dbcFields.spellItemEnchantment, ).toArray(); this._spellIcon = await this.read(DbcFiles.spellIcon, dbcFields.spellIcon).toArray(); this._talent = await this.read(DbcFiles.talent, dbcFields.talent).toArray(); this._talentTab = await this.read(DbcFiles.talentTab, dbcFields.talentTab).toArray(); } public achievement() { return this.getLoadedDataOrRead(DbcFiles.achievement, this._achievement, dbcFields.achievement); } public achievementCategory() { return this.getLoadedDataOrRead(DbcFiles.achievementCategory, this._achievementCategory, dbcFields.achievementCategory); } public areas() { return this.getLoadedDataOrRead(DbcFiles.areas, this._areas); } public faction() { return this.getLoadedDataOrRead(DbcFiles.faction, this._faction, dbcFields.faction); } public glyphProperties() { return this.getLoadedDataOrRead(DbcFiles.glyphProperties, this._glyphProperties, dbcFields.glyphProperties); } public item() { return this.getLoadedDataOrRead(DbcFiles.item, this._item, dbcFields.item); } public itemRetail() { return this.getLoadedDataOrRead(DbcFiles.itemRetail, this._itemRetail, dbcFields.itemRetail); } public itemAppearance() { return this.getLoadedDataOrRead(DbcFiles.itemAppearance, this._itemAppearance, dbcFields.itemAppearance); } public itemModifiedAppearance() { return this.getLoadedDataOrRead(DbcFiles.itemModifiedAppearance, this._itemModifiedAppearance, dbcFields.itemModifiedAppearance); } public itemDisplayInfo() { return this.getLoadedDataOrRead(DbcFiles.itemDisplayInfo, this._itemDisplayInfo, dbcFields.itemDisplayInfo); } public mount() { return this.getLoadedDataOrRead(DbcFiles.mount, this._mount, dbcFields.mount); } public mountDisplay() { return this.getLoadedDataOrRead(DbcFiles.mountDisplay, this._mountDisplay, dbcFields.mountDisplay); } public skill() { return this.getLoadedDataOrRead(DbcFiles.skill, this._skill, dbcFields.skill); } public spell() { return this.getLoadedDataOrRead(DbcFiles.spell, this._spell, dbcFields.spell); } public spellItemEnchantment() { return this.getLoadedDataOrRead(DbcFiles.spellItemEnchantment, this._spellItemEnchantment, dbcFields.spellItemEnchantment); } public spellIcon() { return this.getLoadedDataOrRead(DbcFiles.spellIcon, this._spellIcon, dbcFields.spellIcon); } public talent() { return this.getLoadedDataOrRead(DbcFiles.talent, this._talent, dbcFields.talent); } public talentTab() { return this.getLoadedDataOrRead(DbcFiles.talentTab, this._talentTab, dbcFields.talentTab); } private read(file: string, keepFields: string[] = []): AsyncGenWrapper { const reader = new DbcReader(file, keepFields); return new AsyncGenWrapper(reader.read()); } private getLoadedDataOrRead(path: string, data: T[], keepFields: string[] = []): IAsyncGeneratorWithArrayMethods { return data === undefined ? this.read(path, keepFields) : new ArrayAsAsyncGenerator(data); } }