chore: add and apply eslint

This commit is contained in:
Axel Cocat 2022-05-14 18:44:50 +02:00
parent 7cd2748d33
commit bb6428955f
17 changed files with 1455 additions and 132 deletions

33
.eslintrc.json Normal file
View file

@ -0,0 +1,33 @@
{
"env": {
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"semi": ["error", "always"],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"args": "none"
}
],
"no-duplicate-imports": "warn",
"no-promise-executor-return": "warn",
"quote-props": ["warn", "as-needed"],
"indent": "off",
"@typescript-eslint/indent": [
"error",
"tab",
{
"ignoredNodes": [
"FunctionExpression > .params[decorators.length > 0]",
"FunctionExpression > .params > :matches(Decorator, :not(:first-child))",
"ClassBody.body > PropertyDefinition[decorators.length > 0] > .key"
],
"SwitchCase": 1
}
]
}
}

3
.gitignore vendored
View file

@ -15,3 +15,6 @@ config.json
# Log files # Log files
*.log *.log
# IDEs
.vscode/

1246
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@
"start": "node --expose-gc build/armory/main.js", "start": "node --expose-gc build/armory/main.js",
"watch": "concurrently \"tsc -w --project .\" \"npm run nodemon\"", "watch": "concurrently \"tsc -w --project .\" \"npm run nodemon\"",
"nodemon": "nodemon -q -w build -w config.json build/armory/main.js", "nodemon": "nodemon -q -w build -w config.json build/armory/main.js",
"lint": "eslint src/",
"prettier": "prettier . -w", "prettier": "prettier . -w",
"fetchdata": "node build/tools/fetchdata.js", "fetchdata": "node build/tools/fetchdata.js",
"cleardata": "rimraf data/bone data/meta data/mo3 data/textures" "cleardata": "rimraf data/bone data/meta data/mo3 data/textures"
@ -26,6 +27,7 @@
"@types/pako": "^1.0.3", "@types/pako": "^1.0.3",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"eslint": "^8.15.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.6.0", "prettier": "^2.6.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",

View file

@ -98,6 +98,7 @@ export class Armory {
layoutsDir: path.join(process.cwd(), "static"), layoutsDir: path.join(process.cwd(), "static"),
defaultLayout: "layout.hbs", defaultLayout: "layout.hbs",
helpers: { helpers: {
// eslint-disable-next-line @typescript-eslint/no-var-requires
...require("handlebars-helpers")(), ...require("handlebars-helpers")(),
}, },
}), }),
@ -131,14 +132,14 @@ export class Armory {
}), }),
); );
app.use("/js", express.static(`static/js`)); app.use("/js", express.static("static/js"));
app.use("/css", express.static(`static/css`)); app.use("/css", express.static("static/css"));
app.use("/img", express.static(`static/img`)); app.use("/img", express.static("static/img"));
app.use("/data/mo3", express.static(`data/mo3`)); app.use("/data/mo3", express.static("data/mo3"));
app.use("/data/meta", express.static(`data/meta`)); app.use("/data/meta", express.static("data/meta"));
app.use("/data/bone", express.static(`data/bone`)); app.use("/data/bone", express.static("data/bone"));
app.use("/data/textures", express.static(`data/textures`)); app.use("/data/textures", express.static("data/textures"));
app.use("/data/background.png", express.static(`data/modelviewer-background.png`)); app.use("/data/background.png", express.static("data/modelviewer-background.png"));
const indexController = new IndexController(this); const indexController = new IndexController(this);
app.get("/", this.wrapRoute(indexController.index.bind(indexController))); app.get("/", this.wrapRoute(indexController.index.bind(indexController)));
@ -208,7 +209,7 @@ export class Armory {
const db = this.getCharactersDb(realm); const db = this.getCharactersDb(realm);
if (!(realm in this.charsetCache)) { if (!(realm in this.charsetCache)) {
const [rows, fields] = await db.query({ const [rows] = await db.query({
sql: ` sql: `
SELECT CCSA.character_set_name AS charset FROM information_schema.\`TABLES\` T, SELECT CCSA.character_set_name AS charset FROM information_schema.\`TABLES\` T,
information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA information_schema.\`COLLATION_CHARACTER_SET_APPLICABILITY\` CCSA
@ -235,7 +236,7 @@ export class Armory {
}, 500); }, 500);
} }
private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<any>) { private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void>) {
// Adds error handling for promise-based controller methods // Adds error handling for promise-based controller methods
return async (req: express.Request, res: express.Response, next: express.NextFunction) => { return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try { try {

View file

@ -36,8 +36,8 @@ export class Config {
public worldDatabase: IDatabaseConfig; public worldDatabase: IDatabaseConfig;
public dbQueryTimeout: number; public dbQueryTimeout: number;
private static envPrefix: string = "ACORE_ARMORY"; private static envPrefix = "ACORE_ARMORY";
private static checkedMissingField: boolean = false; private static checkedMissingField = false;
public static async load(logger: winston.Logger): Promise<Config> { public static async load(logger: winston.Logger): Promise<Config> {
try { try {
@ -71,13 +71,13 @@ export class Config {
return config as Config; return config as Config;
} }
private static loadObjFromEnv(logger: winston.Logger, obj: object, model: object, parentName: string = "") { private static loadObjFromEnv(logger: winston.Logger, obj: object, model: object, parentName = "") {
if (parentName !== "") { if (parentName !== "") {
parentName += "."; parentName += ".";
} }
for (const field in model) { for (const field in model) {
if (!model.hasOwnProperty(field)) { if (!Object.hasOwnProperty.call(model, field)) {
continue; continue;
} }
@ -86,9 +86,9 @@ export class Config {
} else if (typeof model[field] === "object") { } else if (typeof model[field] === "object") {
obj[field] = {}; obj[field] = {};
Config.loadObjFromEnv(logger, obj[field], model[field], parentName + field); Config.loadObjFromEnv(logger, obj[field], model[field], parentName + field);
} else if (!obj.hasOwnProperty(field)) { } else if (!Object.hasOwnProperty.call(obj, field)) {
const key = Config.getEnvKey(parentName + field); const key = Config.getEnvKey(parentName + field);
if (process.env.hasOwnProperty(key)) { if (Object.hasOwnProperty.call(process.env, key)) {
obj[field] = Config.parseEnvValue(process.env[key], model[field]); obj[field] = Config.parseEnvValue(process.env[key], model[field]);
} else if (!Config.checkedMissingField) { } else if (!Config.checkedMissingField) {
logger.warn(`Config field ${key} is missing from .env!`); logger.warn(`Config field ${key} is missing from .env!`);
@ -97,14 +97,14 @@ export class Config {
} }
} }
private static loadArrayFromEnv(logger: winston.Logger, model: any, parentName: string = ""): any[] { private static loadArrayFromEnv(logger: winston.Logger, model, parentName = ""): unknown[] {
if (parentName !== "") { if (parentName !== "") {
parentName += "."; parentName += ".";
} }
const arr = []; const arr = [];
let i = 0; let i = 0;
while (true) { for (;;) {
const key = Config.getEnvKey(parentName + i); const key = Config.getEnvKey(parentName + i);
const found = Object.keys(process.env).some((k) => k.startsWith(key)); const found = Object.keys(process.env).some((k) => k.startsWith(key));
if (!found) { if (!found) {
@ -119,7 +119,7 @@ export class Config {
if (Object.keys(obj).length) { if (Object.keys(obj).length) {
arr.push(obj); arr.push(obj);
} }
} else if (process.env.hasOwnProperty(key)) { } else if (Object.hasOwnProperty.call(process.env, key)) {
arr.push(Config.parseEnvValue(process.env[key], model)); arr.push(Config.parseEnvValue(process.env[key], model));
} else { } else {
break; break;
@ -142,7 +142,7 @@ export class Config {
); );
} }
private static parseEnvValue(value: string, model: any): any { private static parseEnvValue(value: string, model: boolean | number | string): boolean | number | string {
const type = typeof model; const type = typeof model;
const lower = value.toLowerCase(); const lower = value.toLowerCase();
if (type === "boolean") { if (type === "boolean") {
@ -154,7 +154,7 @@ export class Config {
return value; return value;
} }
private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName: string = "") { private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName = "") {
const missing = Config.hasMissingFields(obj, model); const missing = Config.hasMissingFields(obj, model);
if (parentName !== "") { if (parentName !== "") {
parentName += "."; parentName += ".";
@ -163,7 +163,7 @@ export class Config {
logger.warn(`Field ${parentName}${field} is missing from config.json!`); logger.warn(`Field ${parentName}${field} is missing from config.json!`);
} }
for (const key of Object.keys(model)) { for (const key of Object.keys(model)) {
if (typeof model[key] === "object" && obj.hasOwnProperty(key)) { if (typeof model[key] === "object" && Object.hasOwnProperty.call(obj, key)) {
Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key); Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key);
} }
} }

View file

@ -1,17 +1,17 @@
import { Pool } from "mysql2/promise"; import { Pool, RowDataPacket } from "mysql2/promise";
import { Query } from "express-serve-static-core"; import { Query } from "express-serve-static-core";
export interface IResult { export interface IResult {
recordsTotal: number; recordsTotal: number;
recordsFiltered: number; recordsFiltered: number;
draw: number; draw: number;
data: any[][]; data: unknown[][];
} }
export interface IColumnSettings { export interface IColumnSettings {
name: string; name: string;
collation?: string; collation?: string;
formatter?: (data: string | number | null, row: any) => string; formatter?: (data: string | number | null, row: unknown) => string;
table?: string; table?: string;
database?: string; database?: string;
} }
@ -60,11 +60,11 @@ export class DataTablesSsp {
private wheres: string[] = []; private wheres: string[] = [];
private filterBindings: (string | number)[] = []; private filterBindings: (string | number)[] = [];
private customBindings: (string | number)[] = []; private customBindings: (string | number)[] = [];
private filterWhereSql: string = "1"; private filterWhereSql = "1";
private customWhereSql: string = "1"; private customWhereSql = "1";
private limitSql: string = ""; private limitSql = "";
private orderSql: string = ""; private orderSql = "";
private joinSql: string = ""; private joinSql = "";
public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) { public constructor(query: Query, db: Pool, table: string, primaryKey: string, columnSettings: IColumnSettings[]) {
this.start = parseInt(query.start as string, 10); this.start = parseInt(query.start as string, 10);
@ -73,7 +73,9 @@ export class DataTablesSsp {
this._order = (query.order as { column: string; dir: string }[]).map((order) => { this._order = (query.order as { column: string; dir: string }[]).map((order) => {
return { column: parseInt(order.column, 10), dir: order.dir }; return { column: parseInt(order.column, 10), dir: order.dir };
}); });
this.columns = (query.columns as { data: string; name: string; searchable: string; orderable: string; search: any }[]).map((column) => { this.columns = (
query.columns as { data: string; name: string; searchable: string; orderable: string; search: { value: string; regex: string } }[]
).map((column) => {
return { return {
data: parseInt(column.data, 10), data: parseInt(column.data, 10),
name: column.name, name: column.name,
@ -83,8 +85,8 @@ export class DataTablesSsp {
}; };
}); });
this.search = { this.search = {
value: (query.search as any).value as string, value: (query.search as unknown)["value"] as string,
regex: (query.search as any).regex === "true", regex: (query.search as unknown)["regex"] === "true",
}; };
this.db = db; this.db = db;
@ -197,32 +199,32 @@ export class DataTablesSsp {
`; `;
} }
public async run(queryTimeout: number = 10_000): Promise<IResult> { public async run(queryTimeout = 10_000): Promise<IResult> {
this.limit().order().join().filter(); this.limit().order().join().filter();
const bindings = [...this.filterBindings, ...this.customBindings]; const bindings = [...this.filterBindings, ...this.customBindings];
let [rows, fields] = await this.db.query({ let [rows] = await this.db.query({
sql: this.buildTotalCountSql(), sql: this.buildTotalCountSql(),
values: this.customBindings, values: this.customBindings,
timeout: queryTimeout, timeout: queryTimeout,
}); });
const recordsTotal = rows[0].count; const recordsTotal = rows[0].count;
[rows, fields] = await this.db.query({ [rows] = await this.db.query({
sql: this.buildFilteredCountSql(), sql: this.buildFilteredCountSql(),
values: bindings, values: bindings,
timeout: queryTimeout, timeout: queryTimeout,
}); });
const recordsFiltered = rows[0].count; const recordsFiltered = rows[0].count;
[rows, fields] = await this.db.query({ [rows] = await this.db.query({
sql: this.sql(), sql: this.sql(),
rowsAsArray: true, rowsAsArray: true,
values: bindings, values: bindings,
timeout: queryTimeout, timeout: queryTimeout,
}); });
rows = (rows as any[][]).map((row) => { rows = (rows as RowDataPacket[]).map((row) => {
for (let i = 0; i < this.columnSettings.length; ++i) { for (let i = 0; i < this.columnSettings.length; ++i) {
const col = this.columnSettings[i]; const col = this.columnSettings[i];
if (col.formatter !== undefined) { if (col.formatter !== undefined) {
@ -236,7 +238,7 @@ export class DataTablesSsp {
recordsTotal, recordsTotal,
recordsFiltered, recordsFiltered,
draw: this.draw, draw: this.draw,
data: rows, data: rows as unknown[][],
}; };
} }

View file

@ -41,7 +41,16 @@ export class Utils {
return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde; return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde;
} }
public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem { public static makeEmblemObject(
obj: {
background: number;
emblemStyle: number;
emblemColor: number;
borderStyle: number;
borderColor: number;
},
padWithZeroes = true,
): IEmblem {
const padLength = padWithZeroes ? 2 : 0; const padLength = padWithZeroes ? 2 : 0;
return { return {
icon: obj.emblemStyle.toString().padStart(padLength, "0"), icon: obj.emblemStyle.toString().padStart(padLength, "0"),

View file

@ -1,10 +1,10 @@
import * as express from "express"; import * as express from "express";
import { RowDataPacket } from "mysql2/promise"; import { RowDataPacket } from "mysql2/promise";
import { Utils } from "../Utils";
import { Armory } from "../Armory"; import { Armory } from "../Armory";
import { IRealmConfig } from "../Config"; import { IRealmConfig } from "../Config";
import { IAchievement } from "../data/DbcReader"; import { IEmblem, Utils } from "../Utils";
import { IAchievement as IAchievementDbc } from "../data/DbcReader";
interface ICharacterData { interface ICharacterData {
guid: number; guid: number;
@ -27,12 +27,14 @@ interface IEquipmentData {
slot: number; slot: number;
itemEntry: number; itemEntry: number;
flags: number; flags: number;
enchantments: string; enchantments: string | number[];
randomPropertyId: number; randomPropertyId: number;
classId: number; classId: number;
subclassId: number; subclassId: number;
quality: number; quality: number;
transmog?: number; transmog?: number;
icon?: number;
gems?: number[];
} }
interface ICustomizationOption { interface ICustomizationOption {
@ -46,6 +48,30 @@ interface IMount {
icon: string; icon: string;
} }
interface IAchievement {
id: number;
category: number;
title: string;
description: string;
points: number;
icon: string;
}
interface IArenaTeam {
id: number;
name: string;
type: number;
rating: number;
seasonWins: number;
seasonGames: number;
background: number;
emblemStyle: number;
emblemColor: number;
borderStyle: number;
borderColor: number;
emblem?: IEmblem;
}
const ItemClassGem = 3; const ItemClassGem = 3;
const SpellMechanicMounted = 21; const SpellMechanicMounted = 21;
const RaceDisplayName = { const RaceDisplayName = {
@ -82,7 +108,7 @@ export class CharacterController {
private itemSocketBonuses: { [key: number]: number }; private itemSocketBonuses: { [key: number]: number };
private mountSpells: number[]; private mountSpells: number[];
private mountBySpellId: { [key: number]: IMount }; private mountBySpellId: { [key: number]: IMount };
private achievementById: { [key: number]: IAchievement }; private achievementById: { [key: number]: IAchievementDbc };
public constructor(armory: Armory) { public constructor(armory: Armory) {
this.armory = armory; this.armory = armory;
@ -121,7 +147,7 @@ export class CharacterController {
} }
this.itemSocketBonuses = {}; this.itemSocketBonuses = {};
const [rows, fields] = await this.armory.worldDb.query({ const [rows] = await this.armory.worldDb.query({
sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0", sql: "SELECT entry, socketBonus FROM item_template WHERE socketBonus <> 0",
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
@ -175,9 +201,9 @@ export class CharacterController {
const equipmentData = await this.getEquipmentData(realmName, charData.guid); const equipmentData = await this.getEquipmentData(realmName, charData.guid);
const customization = this.getCustomizationOptions(charData); const customization = this.getCustomizationOptions(charData);
const equipment = equipmentData.map((row) => { const equipment = equipmentData.map((row) => {
(row as any).icon = this.itemIcons[row.itemEntry]; row.icon = this.itemIcons[row.itemEntry];
(row as any).gems = this.getGemsFromEnchantments(row.enchantments); row.gems = this.getGemsFromEnchantments(row.enchantments as string);
(row as any).enchantments = this.filterEnchantments(row.itemEntry, row.enchantments); row.enchantments = this.filterEnchantments(row.itemEntry, row.enchantments as string);
return row; return row;
}); });
const mounts = await this.getMounts(realmName, charData.guid); const mounts = await this.getMounts(realmName, charData.guid);
@ -314,7 +340,7 @@ export class CharacterController {
private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> { private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> {
const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?"; const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?";
const [rows, fields] = await this.armory.getCharactersDb(realm.name).query({ const [rows] = await this.armory.getCharactersDb(realm.name).query({
sql: ` sql: `
SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\` SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\`
FROM \`characters\` FROM \`characters\`
@ -337,8 +363,10 @@ export class CharacterController {
private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> { private async getEquipmentData(realm: string, charGuid: number): Promise<IEquipmentData[]> {
const transmogSelect = this.armory.config.transmogModule ? ", custom_transmogrification.FakeEntry AS transmog" : ""; const transmogSelect = this.armory.config.transmogModule ? ", custom_transmogrification.FakeEntry AS transmog" : "";
const transmogJoin = this.armory.config.transmogModule ? "LEFT JOIN custom_transmogrification ON custom_transmogrification.GUID = item_instance.guid" : ""; const transmogJoin = this.armory.config.transmogModule
let [rows, fields] = await this.armory.getCharactersDb(realm).query({ ? "LEFT JOIN custom_transmogrification ON custom_transmogrification.GUID = item_instance.guid"
: "";
let [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT SELECT
character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId character_inventory.slot, item_instance.itemEntry, item_instance.flags, item_instance.enchantments, item_instance.randomPropertyId
@ -360,7 +388,7 @@ export class CharacterController {
row.subclassId = item.subclassId; row.subclassId = item.subclassId;
} }
[rows, fields] = await this.armory.worldDb.query({ [rows] = await this.armory.worldDb.query({
sql: "SELECT entry, quality FROM item_template WHERE entry IN (?)", sql: "SELECT entry, quality FROM item_template WHERE entry IN (?)",
values: [data.map((row) => row.itemEntry)], values: [data.map((row) => row.itemEntry)],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
@ -374,7 +402,7 @@ export class CharacterController {
} }
private async getMounts(realm: string, charGuid: number): Promise<IMount[]> { private async getMounts(realm: string, charGuid: number): Promise<IMount[]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({ const [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT spell SELECT spell
FROM character_spell FROM character_spell
@ -455,7 +483,7 @@ export class CharacterController {
const data = this.armory.characterCustomization.getCharacterCustomizationData(charData.race, charData.gender); const data = this.armory.characterCustomization.getCharacterCustomizationData(charData.race, charData.gender);
const options = []; const options = [];
const setOptionByChoiceIndex = (optionName: string, choiceIndex: number) => { const setOptionByChoiceIndex = (optionName: string, choiceIndex: number) => {
const option = data.Options.find((opt) => opt.Name === optionName); const option = data["Options"].find((opt) => opt.Name === optionName);
if (option !== undefined) { if (option !== undefined) {
const choice = option.Choices.find((choice) => choice.OrderIndex === choiceIndex); const choice = option.Choices.find((choice) => choice.OrderIndex === choiceIndex);
if (choice !== undefined) { if (choice !== undefined) {
@ -464,7 +492,7 @@ export class CharacterController {
} }
}; };
const setOptionByChoiceName = (optionName: string, choiceName: string) => { const setOptionByChoiceName = (optionName: string, choiceName: string) => {
const option = data.Options.find((opt) => opt.Name === optionName); const option = data["Options"].find((opt) => opt.Name === optionName);
if (option !== undefined) { if (option !== undefined) {
const choice = option.Choices.find((ch) => ch.Name === choiceName); const choice = option.Choices.find((ch) => ch.Name === choiceName);
if (choice !== undefined) { if (choice !== undefined) {
@ -473,7 +501,7 @@ export class CharacterController {
} }
}; };
const setOptionByChoiceId = (optionName: string, choiceId: number) => { const setOptionByChoiceId = (optionName: string, choiceId: number) => {
const option = data.Options.find((opt) => opt.Name === optionName); const option = data["Options"].find((opt) => opt.Name === optionName);
if (option !== undefined) { if (option !== undefined) {
options.push({ optionId: option.Id, choiceId: choiceId }); options.push({ optionId: option.Id, choiceId: choiceId });
} }
@ -907,7 +935,7 @@ export class CharacterController {
} }
private async getTalents(realm: string, character: number): Promise<number[][]> { private async getTalents(realm: string, character: number): Promise<number[][]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({ const [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT spell, specMask SELECT spell, specMask
FROM character_talent FROM character_talent
@ -959,8 +987,8 @@ export class CharacterController {
return texturePath.toLowerCase().replace("interface\\icons\\", "").replace("interface\\spellbook\\", "").replace(/\.$/, ""); return texturePath.toLowerCase().replace("interface\\icons\\", "").replace("interface\\spellbook\\", "").replace(/\.$/, "");
} }
private async getGlyphs(realm: string, character: number): Promise<any[][]> { private async getGlyphs(realm: string, character: number): Promise<number[][]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({ const [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6 SELECT guid, talentGroup, glyph1, glyph2, glyph3, glyph4, glyph5, glyph6
FROM character_glyphs FROM character_glyphs
@ -970,7 +998,7 @@ export class CharacterController {
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
const glyphs = [[], []]; const glyphs: number[][] = [[], []];
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
const glyphIds = [row.glyph1, row.glyph2, row.glyph3, row.glyph4, row.glyph5, row.glyph6].filter((id) => id !== 0); const glyphIds = [row.glyph1, row.glyph2, row.glyph3, row.glyph4, row.glyph5, row.glyph6].filter((id) => id !== 0);
for (const glyphId of glyphIds) { for (const glyphId of glyphIds) {
@ -985,7 +1013,10 @@ export class CharacterController {
return glyphs; return glyphs;
} }
private async getAchievements(realm: string, charData: ICharacterData): Promise<{ achievements: any[]; earned: { [key: number]: any } }> { private async getAchievements(
realm: string,
charData: ICharacterData,
): Promise<{ achievements: IAchievement[]; earned: { [key: number]: number } }> {
const promises = await this.armory.dbc const promises = await this.armory.dbc
.achievement() .achievement()
.filter((ach) => ach.faction === -1 || ach.faction === Utils.getFactionFromRaceId(charData.race)) .filter((ach) => ach.faction === -1 || ach.faction === Utils.getFactionFromRaceId(charData.race))
@ -1003,7 +1034,7 @@ export class CharacterController {
.toArray(); .toArray();
const achievements = await Promise.all(promises); const achievements = await Promise.all(promises);
const [rows, fields] = await this.armory.getCharactersDb(realm).query({ const [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT achievement, date SELECT achievement, date
FROM character_achievement FROM character_achievement
@ -1012,11 +1043,9 @@ export class CharacterController {
values: [charData.guid], values: [charData.guid],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
const earned = {}; const earned: { [key: number]: number } = {};
for (const row of rows as RowDataPacket[]) { for (const row of rows as RowDataPacket[]) {
earned[row.achievement] = { earned[row.achievement] = row.date;
date: row.date,
};
} }
return { return {
@ -1026,7 +1055,7 @@ export class CharacterController {
} }
private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number; today: number; yesterday: number }> { private async getPvpKills(realm: string, charGuid: number): Promise<{ total: number; today: number; yesterday: number }> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({ const [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT totalKills, todayKills, yesterdayKills SELECT totalKills, todayKills, yesterdayKills
FROM characters FROM characters
@ -1044,8 +1073,8 @@ export class CharacterController {
}; };
} }
private async getArenaTeams(realm: string, charGuid: number): Promise<any[]> { private async getArenaTeams(realm: string, charGuid: number): Promise<IArenaTeam[]> {
const [rows, fields] = await this.armory.getCharactersDb(realm).query({ const [rows] = await this.armory.getCharactersDb(realm).query({
sql: ` sql: `
SELECT SELECT
arena_team.arenaTeamId AS id, arena_team.name, arena_team.type, arena_team.rating, arena_team.seasonWins, arena_team.seasonGames, arena_team.arenaTeamId AS id, arena_team.name, arena_team.type, arena_team.rating, arena_team.seasonWins, arena_team.seasonGames,
@ -1059,7 +1088,7 @@ export class CharacterController {
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
return (rows as RowDataPacket[]).map((row) => { return (rows as IArenaTeam[]).map((row) => {
row.emblem = Utils.makeEmblemObject(row, false); row.emblem = Utils.makeEmblemObject(row, false);
return row; return row;
}); });

View file

@ -2,16 +2,25 @@ import * as express from "express";
import { encode } from "html-entities"; import { encode } from "html-entities";
import { RowDataPacket } from "mysql2/promise"; import { RowDataPacket } from "mysql2/promise";
import { Utils } from "../Utils";
import { Armory } from "../Armory"; import { Armory } from "../Armory";
import { IRealmConfig } from "../Config"; import { IRealmConfig } from "../Config";
import { DataTablesSsp } from "../DataTablesSsp"; import { DataTablesSsp } from "../DataTablesSsp";
import { Utils, IEmblem, EFaction } from "../Utils";
interface IGuildRank { interface IGuildRank {
id: number; id: number;
name: string; name: string;
} }
interface IGuildData {
id: number;
name: string;
leader: string;
faction: EFaction;
emblem: IEmblem;
membersCount: number;
}
export class GuildController { export class GuildController {
private armory: Armory; private armory: Armory;
@ -87,17 +96,20 @@ export class GuildController {
const result = await ssp.where("`guildid` = ?", guildId).where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout); const result = await ssp.where("`guildid` = ?", guildId).where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout);
const ranks = await this.getGuildRanks(realm, guildId); const ranks = await this.getGuildRanks(realm, guildId);
(result as any).ranks = {}; const ranksObj: { [key: number]: string } = {};
for (const rank of ranks) { for (const rank of ranks) {
(result as any).ranks[rank.id] = encode(rank.name); ranksObj[rank.id] = encode(rank.name);
} }
res.json(result); res.json({
...result,
ranks: ranksObj,
});
} }
private async getGuildData(realm: IRealmConfig, name: string): Promise<any> { private async getGuildData(realm: IRealmConfig, name: string): Promise<IGuildData> {
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
let [rows, fields] = await db.query({ let [rows] = await db.query({
sql: ` sql: `
SELECT guildid, name, leaderguid, EmblemStyle AS emblemStyle, EmblemColor AS emblemColor, BorderStyle AS borderStyle, BorderColor AS borderColor, BackgroundColor AS background SELECT guildid, name, leaderguid, EmblemStyle AS emblemStyle, EmblemColor AS emblemColor, BorderStyle AS borderStyle, BorderColor AS borderColor, BackgroundColor AS background
FROM guild WHERE name = ? FROM guild WHERE name = ?
@ -110,7 +122,7 @@ export class GuildController {
} }
const guild = rows[0]; const guild = rows[0];
[rows, fields] = await db.query({ [rows] = await db.query({
sql: ` sql: `
SELECT name, race FROM characters SELECT name, race FROM characters
WHERE guid = ? WHERE guid = ?
@ -120,7 +132,7 @@ export class GuildController {
}); });
const leader = rows[0]; const leader = rows[0];
[rows, fields] = await db.query({ [rows] = await db.query({
sql: ` sql: `
SELECT COUNT(guid) AS \`count\` FROM guild_member SELECT COUNT(guid) AS \`count\` FROM guild_member
WHERE guildid = ? WHERE guildid = ?
@ -142,7 +154,7 @@ export class GuildController {
private async getGuildId(realm: IRealmConfig, name: string): Promise<number> { private async getGuildId(realm: IRealmConfig, name: string): Promise<number> {
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({ const [rows] = await db.query({
sql: ` sql: `
SELECT guildid SELECT guildid
FROM guild WHERE name = ? FROM guild WHERE name = ?
@ -159,7 +171,7 @@ export class GuildController {
private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> { private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> {
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({ const [rows] = await db.query({
sql: ` sql: `
SELECT guildid SELECT guildid
FROM guild WHERE guildid FROM guild WHERE guildid
@ -173,7 +185,7 @@ export class GuildController {
private async getGuildRanks(realm: IRealmConfig, id: number): Promise<IGuildRank[]> { private async getGuildRanks(realm: IRealmConfig, id: number): Promise<IGuildRank[]> {
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({ const [rows] = await db.query({
sql: ` sql: `
SELECT rid AS id, rname AS name SELECT rid AS id, rname AS name
FROM guild_rank WHERE guildid = ? FROM guild_rank WHERE guildid = ?

View file

@ -56,8 +56,10 @@ export class IndexController {
} }
const result = await ssp.where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout); const result = await ssp.where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout);
(result as any).realm = realm.name;
res.json(result); res.json({
...result,
realm: realm.name,
});
} }
} }

View file

@ -3,7 +3,7 @@ const fsp = fs.promises;
import * as path from "path"; import * as path from "path";
export class CharacterCustomization { export class CharacterCustomization {
private data: { [key: number]: { [key: number]: any } }; private data: { [key: number]: { [key: number]: unknown } };
public async loadData(): Promise<void> { public async loadData(): Promise<void> {
this.data = {}; this.data = {};
@ -21,7 +21,7 @@ export class CharacterCustomization {
} }
} }
public getCharacterCustomizationData(race: number, gender: number): any { public getCharacterCustomizationData(race: number, gender: number): unknown {
return this.data[race][gender]; return this.data[race][gender];
} }
} }

View file

@ -219,10 +219,13 @@ class DbcReader<T> {
return; return;
} }
const headerCols = headerLine.value.map((header) => camelCase(header).replace(/[\[\]]/g, "")); const headerCols = headerLine.value.map((header) => camelCase(header).replace(/[[\]]/g, ""));
for await (const arr of itr) { for await (const arr of itr) {
const cols = arr.map((value) => (isNaN(value as any) ? value : parseInt(value, 10))); const cols = arr.map((value) => {
const parsed = parseInt(value, 10);
return isNaN(parsed) ? value : parsed;
});
const row = {}; const row = {};
headerCols.forEach((header, headerIdx) => { headerCols.forEach((header, headerIdx) => {
if (this.fields.length === 0 || this.fields.includes(header)) { if (this.fields.length === 0 || this.fields.includes(header)) {
@ -243,8 +246,8 @@ class DbcReader<T> {
const str = chunk.toString(); const str = chunk.toString();
// Iterate over each character, keep track of current column (of the returned array) // Iterate over each character, keep track of current column (of the returned array)
for (let c = 0; c < str.length; ++c) { for (let c = 0; c < str.length; ++c) {
let ch = str[c], const ch = str[c];
nch = str[c + 1]; // Current character, next character const nch = str[c + 1]; // Current character, next character
if (!(col in arr)) { if (!(col in arr)) {
arr[col] = ""; // Create a new column (start with empty string) if necessary arr[col] = ""; // Create a new column (start with empty string) if necessary
} }

View file

@ -1,9 +1,9 @@
import "dotenv/config"; import "dotenv/config";
import "source-map-support/register";
import { Armory } from "./Armory"; import { Armory } from "./Armory";
async function main(): Promise<void> { async function main(): Promise<void> {
require("source-map-support").install();
const armory = new Armory(); const armory = new Armory();
await armory.start(); await armory.start();
} }

2
src/index.d.ts vendored
View file

@ -1,4 +1,4 @@
declare module Express { declare namespace Express {
export interface Request { export interface Request {
id: string; id: string;
} }

View file

@ -3,18 +3,16 @@ const fsp = fs.promises;
import * as path from "path"; import * as path from "path";
import * as pako from "pako"; import * as pako from "pako";
import fetch from "node-fetch";
import * as mkdirp from "mkdirp"; import * as mkdirp from "mkdirp";
import * as glob from "glob-promise"; import * as glob from "glob-promise";
import { Response } from "node-fetch"; import "source-map-support/register";
import * as prettyMs from "pretty-ms"; import * as prettyMs from "pretty-ms";
import * as cliProgress from "cli-progress"; import * as cliProgress from "cli-progress";
import fetch, { Response } from "node-fetch";
import promisepool = require("@supercharge/promise-pool"); import promisepool = require("@supercharge/promise-pool");
import { DbcManager, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from "../armory/data/DbcReader"; import { DbcManager, IItemAppearanceDbc, IItemModifiedAppearanceDbc, IMountDbc, IMountXDisplayDbc } from "../armory/data/DbcReader";
require("source-map-support").install();
const baseUrl = "https://wow.zamimg.com/modelviewer/live"; const baseUrl = "https://wow.zamimg.com/modelviewer/live";
class Stopwatch { class Stopwatch {
@ -92,11 +90,10 @@ const modelsDownloadQueue = new Set<number>();
const texturesDownloadQueue = new Set<number>(); const texturesDownloadQueue = new Set<number>();
const bonesDownloadQueue = new Set<number>(); const bonesDownloadQueue = new Set<number>();
async function download(dir: string, file: string): Promise<string | any> { async function download(dir: string, file: string): Promise<string | unknown> {
const dataDir = path.join(process.cwd(), "data"); const dataDir = path.join(process.cwd(), "data");
const fullPath = `${dir}/${file}`; const fullPath = `${dir}/${file}`;
try {
const res = await fetch(`${baseUrl}/${fullPath}`); const res = await fetch(`${baseUrl}/${fullPath}`);
if (res.status !== 200) { if (res.status !== 200) {
throw new HttpRequestError(res); throw new HttpRequestError(res);
@ -118,12 +115,9 @@ async function download(dir: string, file: string): Promise<string | any> {
return fileStream.path.toString(); return fileStream.path.toString();
} }
} catch (err) {
throw err;
}
} }
function queueTexturesAndModels(item: any): void { function queueTexturesAndModels(item): void {
if (item.TextureFiles !== null) { if (item.TextureFiles !== null) {
for (const file in Object.values(item.TextureFiles)) { for (const file in Object.values(item.TextureFiles)) {
if (file["FileDataId"] !== 0) { if (file["FileDataId"] !== 0) {
@ -172,10 +166,10 @@ async function downloadRaces(): Promise<void> {
.withConcurrency(4) .withConcurrency(4)
.process(async (race) => { .process(async (race) => {
const characterJson = await download("meta/character", `${race}.json`); const characterJson = await download("meta/character", `${race}.json`);
modelsDownloadQueue.add(characterJson.Model); modelsDownloadQueue.add(characterJson["Model"]);
const customizationJson = await download("meta/charactercustomization2", `${characterJson.Race}_${characterJson.Gender}.json`); const customizationJson = await download("meta/charactercustomization2", `${characterJson["Race"]}_${characterJson["Gender"]}.json`);
for (const option of customizationJson.Options) { for (const option of customizationJson["Options"]) {
for (const choice of option.Choices) { for (const choice of option.Choices) {
for (const element of choice.Elements) { for (const element of choice.Elements) {
if ( if (
@ -192,7 +186,7 @@ async function downloadRaces(): Promise<void> {
} }
} }
const textureFiles = Object.values(customizationJson.TextureFiles).flat(); const textureFiles = Object.values(customizationJson["TextureFiles"]).flat();
for (const file of textureFiles) { for (const file of textureFiles) {
texturesDownloadQueue.add(file["FileDataId"]); texturesDownloadQueue.add(file["FileDataId"]);
} }
@ -255,7 +249,7 @@ async function downloadWeapons(): Promise<void> {
} }
const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId]; const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId];
try { try {
const itemJson = await download(`meta/item`, `${appearance.itemDisplayInfoId}.json`); const itemJson = await download("meta/item", `${appearance.itemDisplayInfoId}.json`);
queueTexturesAndModels(itemJson); queueTexturesAndModels(itemJson);
progress.increment(); progress.increment();
} catch (err) { } catch (err) {

View file

@ -117,8 +117,7 @@
$achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`); $achievement.find("a").attr("href", `{{aowow}}?achievement=${achievement.id}`);
if (isEarned) { if (isEarned) {
const earned = achievementsData.earned[achievement.id]; const date = new Date(achievementsData.earned[achievement.id] * 1000);
const date = new Date(earned.date * 1000);
$achievement.find(".earned-date").text(date.toLocaleDateString()); $achievement.find(".earned-date").text(date.toLocaleDateString());
$achievement.addClass("earned"); $achievement.addClass("earned");
} }