chore: apply prettier with new conf

This commit is contained in:
Helias 2022-05-06 18:44:54 +02:00
parent 6a4720e59b
commit db2a04c03f
13 changed files with 2107 additions and 2109 deletions

View file

@ -1,259 +1,259 @@
import * as path from 'path'; import * as path from "path";
import * as uuid from 'uuid'; import * as uuid from "uuid";
import { Express } from 'express'; import { Express } from "express";
import * as express from 'express'; import * as express from "express";
import * as winston from 'winston'; import * as winston from "winston";
import * as morgan from 'morgan'; import * as morgan from "morgan";
import { Pool, createPool } from 'mysql2/promise'; import { Pool, createPool } from "mysql2/promise";
import { engine as handlebarsEngine } from 'express-handlebars'; import { engine as handlebarsEngine } from "express-handlebars";
import { Config, IRealmConfig } from './Config'; import { Config, IRealmConfig } from "./Config";
import { DbcManager } from './data/DbcReader'; import { DbcManager } from "./data/DbcReader";
import { CharacterCustomization } from './data/CharacterCustomization'; import { CharacterCustomization } from "./data/CharacterCustomization";
import { IndexController } from './controllers/IndexController'; import { IndexController } from "./controllers/IndexController";
import { CharacterController } from './controllers/CharacterController'; import { CharacterController } from "./controllers/CharacterController";
import { GuildController } from './controllers/GuildController'; import { GuildController } from "./controllers/GuildController";
export class Armory { export class Armory {
public characterCustomization: CharacterCustomization; public characterCustomization: CharacterCustomization;
public dbc: DbcManager; public dbc: DbcManager;
public config: Config; public config: Config;
public worldDb: Pool; public worldDb: Pool;
public logger: winston.Logger; public logger: winston.Logger;
public charsetCache: { [key: string]: string }; public charsetCache: { [key: string]: string };
private charsDbs: { [key: string]: Pool }; private charsDbs: { [key: string]: Pool };
private errorNames: { [key: number]: string }; private errorNames: { [key: number]: string };
private errorDescriptions: { [key: number]: string }; private errorDescriptions: { [key: number]: string };
public constructor() { public constructor() {
this.dbc = new DbcManager(); this.dbc = new DbcManager();
this.characterCustomization = new CharacterCustomization(); this.characterCustomization = new CharacterCustomization();
this.charsDbs = {}; this.charsDbs = {};
this.logger = winston.createLogger({ this.logger = winston.createLogger({
level: 'info', level: "info",
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
winston.format.printf((info) => `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`), winston.format.printf((info) => `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`),
), ),
transports: [ transports: [
new winston.transports.Console({ level: 'debug' }), new winston.transports.Console({ level: "debug" }),
new winston.transports.File({ filename: path.join('logs', 'armory.error.log'), level: 'error' }), new winston.transports.File({ filename: path.join("logs", "armory.error.log"), level: "error" }),
new winston.transports.File({ filename: path.join('logs', 'armory.combined.log'), level: 'http' }), new winston.transports.File({ filename: path.join("logs", "armory.combined.log"), level: "http" }),
], ],
}); });
this.charsetCache = {}; this.charsetCache = {};
this.errorNames = { this.errorNames = {
400: 'Bad Request', 400: "Bad Request",
401: 'Unauthorized', 401: "Unauthorized",
403: 'Forbidden', 403: "Forbidden",
404: 'Not Found', 404: "Not Found",
500: 'Internal Server Error', 500: "Internal Server Error",
}; };
this.errorDescriptions = { this.errorDescriptions = {
400: 'Invalid request.', 400: "Invalid request.",
404: 'Sorry, we could not find what you were looking for.', 404: "Sorry, we could not find what you were looking for.",
500: 'An unexpected internal error has occurred. Please contact the site owner.', 500: "An unexpected internal error has occurred. Please contact the site owner.",
}; };
} }
public async start(): Promise<void> { public async start(): Promise<void> {
const app: Express = express(); const app: Express = express();
const listenPort = 48733; const listenPort = 48733;
this.logger.info('Loading config...'); this.logger.info("Loading config...");
this.config = await Config.load(this.logger); this.config = await Config.load(this.logger);
this.logger.info('Loading data files...'); this.logger.info("Loading data files...");
if (this.config.loadDbcs) { if (this.config.loadDbcs) {
await this.dbc.loadAllFiles(); await this.dbc.loadAllFiles();
} }
await this.characterCustomization.loadData(); await this.characterCustomization.loadData();
this.logger.info('Connecting to databases...'); this.logger.info("Connecting to databases...");
this.worldDb = createPool(this.config.worldDatabase); this.worldDb = createPool(this.config.worldDatabase);
for (const realm of this.config.realms) { for (const realm of this.config.realms) {
this.charsDbs[realm.name.toLowerCase()] = createPool(realm.charactersDatabase); this.charsDbs[realm.name.toLowerCase()] = createPool(realm.charactersDatabase);
} }
this.logger.info('Starting server...'); this.logger.info("Starting server...");
const locals = { const locals = {
aowow: this.config.aowowUrl, aowow: this.config.aowowUrl,
websiteUrl: this.config.websiteUrl, websiteUrl: this.config.websiteUrl,
websiteName: this.config.websiteName, websiteName: this.config.websiteName,
websiteRoot: this.config.websiteRoot, websiteRoot: this.config.websiteRoot,
iframeMode: this.config.iframeMode, iframeMode: this.config.iframeMode,
}; };
for (const key in locals) { for (const key in locals) {
if (locals.hasOwnProperty(key)) { if (locals.hasOwnProperty(key)) {
app.locals[key] = locals[key]; app.locals[key] = locals[key];
} }
} }
app.locals.locals = locals; app.locals.locals = locals;
app.engine( app.engine(
'.hbs', ".hbs",
handlebarsEngine({ handlebarsEngine({
extname: 'hbs', extname: "hbs",
partialsDir: path.join(process.cwd(), 'static', 'partials'), partialsDir: path.join(process.cwd(), "static", "partials"),
layoutsDir: path.join(process.cwd(), 'static'), layoutsDir: path.join(process.cwd(), "static"),
defaultLayout: 'layout.hbs', defaultLayout: "layout.hbs",
helpers: { helpers: {
...require('handlebars-helpers')(), ...require("handlebars-helpers")(),
}, },
}), }),
); );
app.set('view engine', 'handlebars'); app.set("view engine", "handlebars");
app.set('views', path.join(process.cwd(), 'static')); app.set("views", path.join(process.cwd(), "static"));
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
req.id = uuid.v4(); req.id = uuid.v4();
next(); next();
}); });
morgan.token('id', (req: express.Request) => { morgan.token("id", (req: express.Request) => {
return req.id; return req.id;
}); });
morgan.token('ip', (req: express.Request) => { morgan.token("ip", (req: express.Request) => {
const forwardedFor = req.headers['x-forwarded-for']; const forwardedFor = req.headers["x-forwarded-for"];
if (forwardedFor) { if (forwardedFor) {
if (typeof forwardedFor === 'string') { if (typeof forwardedFor === "string") {
return forwardedFor; return forwardedFor;
} }
return forwardedFor.join(', '); return forwardedFor.join(", ");
} }
return req.socket.remoteAddress; return req.socket.remoteAddress;
}); });
app.use( app.use(
morgan(':method :url :status - ID :id - IP :ip - :response-time ms', { morgan(":method :url :status - ID :id - IP :ip - :response-time ms", {
stream: { stream: {
write: (msg) => this.logger.http(msg.trim()), write: (msg) => this.logger.http(msg.trim()),
}, },
}), }),
); );
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)));
app.get('/search', this.wrapRoute(indexController.search.bind(indexController))); app.get("/search", this.wrapRoute(indexController.search.bind(indexController)));
const charsController = new CharacterController(this); const charsController = new CharacterController(this);
await charsController.load(); await charsController.load();
app.get('/character/:realm/:name', this.wrapRoute(charsController.character.bind(charsController))); app.get("/character/:realm/:name", this.wrapRoute(charsController.character.bind(charsController)));
app.get('/character/:realm/:name/talents', this.wrapRoute(charsController.talents.bind(charsController))); app.get("/character/:realm/:name/talents", this.wrapRoute(charsController.talents.bind(charsController)));
app.get('/character/:realm/:name/achievements', this.wrapRoute(charsController.achievements.bind(charsController))); app.get("/character/:realm/:name/achievements", this.wrapRoute(charsController.achievements.bind(charsController)));
app.get('/character/:realm/:character/achievements/data', this.wrapRoute(charsController.achievementsData.bind(charsController))); app.get("/character/:realm/:character/achievements/data", this.wrapRoute(charsController.achievementsData.bind(charsController)));
app.get('/character/:realm/:name/pvp', this.wrapRoute(charsController.pvp.bind(charsController))); app.get("/character/:realm/:name/pvp", this.wrapRoute(charsController.pvp.bind(charsController)));
const guildsController = new GuildController(this); const guildsController = new GuildController(this);
app.get('/guild/:realm/:name', this.wrapRoute(guildsController.guild.bind(guildsController))); app.get("/guild/:realm/:name", this.wrapRoute(guildsController.guild.bind(guildsController)));
app.get('/guild/:realm/:guild/members', this.wrapRoute(guildsController.members.bind(guildsController))); app.get("/guild/:realm/:guild/members", this.wrapRoute(guildsController.members.bind(guildsController)));
app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => { app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => {
// Error handler // Error handler
if (err instanceof Error) { if (err instanceof Error) {
const contents = err.stack ?? `${err.name}: ${err.message}`; const contents = err.stack ?? `${err.name}: ${err.message}`;
this.logger.error(`Error on request ${req.id}. ${contents}`); this.logger.error(`Error on request ${req.id}. ${contents}`);
} }
let status = 500; let status = 500;
if (typeof err === 'number') { if (typeof err === "number") {
status = err; status = err;
} }
res.status(status).render('error.hbs', this.getErrorViewData(status, req)); res.status(status).render("error.hbs", this.getErrorViewData(status, req));
}); });
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// 404 handler // 404 handler
res.status(404); res.status(404);
// Respond with html page // Respond with html page
if (req.accepts('html')) { if (req.accepts("html")) {
return res.render('error.hbs', this.getErrorViewData(404, req)); return res.render("error.hbs", this.getErrorViewData(404, req));
} }
// Respond with json // Respond with json
if (req.accepts('json')) { if (req.accepts("json")) {
return res.json({ error: this.errorNames[404] }); return res.json({ error: this.errorNames[404] });
} }
// Default to plain-text // Default to plain-text
res.type('txt').send(this.errorNames[404]); res.type("txt").send(this.errorNames[404]);
}); });
this.gc(); this.gc();
app.listen(listenPort, '0.0.0.0', () => { app.listen(listenPort, "0.0.0.0", () => {
this.logger.info(`Server is listening on 0.0.0.0:${listenPort}.`); this.logger.info(`Server is listening on 0.0.0.0:${listenPort}.`);
}); });
} }
public getCharactersDb(realm: string): Pool { public getCharactersDb(realm: string): Pool {
return this.charsDbs[realm.toLowerCase()]; return this.charsDbs[realm.toLowerCase()];
} }
public getRealm(realm: string): IRealmConfig { public getRealm(realm: string): IRealmConfig {
return this.config.realms.find((r) => r.name.toLowerCase() === realm.toLowerCase()); return this.config.realms.find((r) => r.name.toLowerCase() === realm.toLowerCase());
} }
public async getDatabaseCharset(realm: string): Promise<string> { public async getDatabaseCharset(realm: string): Promise<string> {
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, fields] = 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
WHERE CCSA.collation_name = T.table_collation WHERE CCSA.collation_name = T.table_collation
AND T.table_schema = "${(await db.getConnection()).config.database}" AND T.table_schema = "${(await db.getConnection()).config.database}"
AND T.table_name = "characters" AND T.table_name = "characters"
`, `,
timeout: this.config.dbQueryTimeout, timeout: this.config.dbQueryTimeout,
}); });
this.charsetCache[realm] = rows[0].charset; this.charsetCache[realm] = rows[0].charset;
} }
return this.charsetCache[realm]; return this.charsetCache[realm];
} }
public gc(): void { public gc(): void {
if (this.config.loadDbcs) { if (this.config.loadDbcs) {
return; return;
} }
setTimeout(() => { setTimeout(() => {
if (global.gc) { if (global.gc) {
global.gc(); global.gc();
} }
}, 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<any>) {
// 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 {
await fn(req, res, next); await fn(req, res, next);
} catch (e) { } catch (e) {
next(e); next(e);
} }
}; };
} }
private getErrorViewData(status: number, req: express.Request) { private getErrorViewData(status: number, req: express.Request) {
return { return {
status, status,
name: this.errorNames[status] || 'An error occurred', name: this.errorNames[status] || "An error occurred",
description: this.errorDescriptions[status] || '', description: this.errorDescriptions[status] || "",
reqId: req.id, reqId: req.id,
}; };
} }
} }

View file

@ -1,180 +1,180 @@
import * as fs from 'fs'; import * as fs from "fs";
const fsp = fs.promises; const fsp = fs.promises;
import * as winston from 'winston'; import * as winston from "winston";
export interface IDatabaseConfig { export interface IDatabaseConfig {
host: string; host: string;
port: number; port: number;
user: string; user: string;
password: string; password: string;
database: string; database: string;
} }
export interface IRealmConfig { export interface IRealmConfig {
name: string; name: string;
realmId: number; realmId: number;
authDatabase: string; authDatabase: string;
charactersDatabase: IDatabaseConfig; charactersDatabase: IDatabaseConfig;
} }
export interface IIframeModeConfig { export interface IIframeModeConfig {
enabled: boolean; enabled: boolean;
url: string; url: string;
} }
export class Config { export class Config {
public aowowUrl: string; public aowowUrl: string;
public websiteUrl: string; public websiteUrl: string;
public websiteName: string; public websiteName: string;
public websiteRoot: string; public websiteRoot: string;
public iframeMode: IIframeModeConfig; public iframeMode: IIframeModeConfig;
public loadDbcs: boolean; public loadDbcs: boolean;
public hideGameMasters: boolean; public hideGameMasters: boolean;
public realms: IRealmConfig[]; public realms: IRealmConfig[];
public worldDatabase: IDatabaseConfig; public worldDatabase: IDatabaseConfig;
public dbQueryTimeout: number; public dbQueryTimeout: number;
private static envPrefix: string = 'ACORE_ARMORY'; private static envPrefix: string = "ACORE_ARMORY";
private static checkedMissingField: boolean = false; private static checkedMissingField: boolean = false;
public static async load(logger: winston.Logger): Promise<Config> { public static async load(logger: winston.Logger): Promise<Config> {
try { try {
await fsp.access('config.json'); await fsp.access("config.json");
return await Config.loadFromFile(logger); return await Config.loadFromFile(logger);
} catch (err) { } catch (err) {
return await Config.loadFromEnv(logger); return await Config.loadFromEnv(logger);
} }
} }
private static async loadFromFile(logger: winston.Logger): Promise<Config> { private static async loadFromFile(logger: winston.Logger): Promise<Config> {
const json: Buffer = await fsp.readFile('config.json'); const json: Buffer = await fsp.readFile("config.json");
const config = JSON.parse(json.toString()) as Config; const config = JSON.parse(json.toString()) as Config;
if (!Config.checkedMissingField) { if (!Config.checkedMissingField) {
const defaultConfigJson = await fsp.readFile('config.default.json'); const defaultConfigJson = await fsp.readFile("config.default.json");
const defaultConfig = JSON.parse(defaultConfigJson.toString()); const defaultConfig = JSON.parse(defaultConfigJson.toString());
Config.checkAllMissingFields(logger, config, defaultConfig); Config.checkAllMissingFields(logger, config, defaultConfig);
Config.checkedMissingField = true; Config.checkedMissingField = true;
} }
return config; return config;
} }
private static async loadFromEnv(logger: winston.Logger): Promise<Config> { private static async loadFromEnv(logger: winston.Logger): Promise<Config> {
const config = {}; const config = {};
const json = await fsp.readFile('config.default.json'); const json = await fsp.readFile("config.default.json");
const defaultConfig = JSON.parse(json.toString()); const defaultConfig = JSON.parse(json.toString());
Config.loadObjFromEnv(logger, config, defaultConfig); Config.loadObjFromEnv(logger, config, defaultConfig);
Config.checkedMissingField = true; Config.checkedMissingField = true;
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: string = "") {
if (parentName !== '') { if (parentName !== "") {
parentName += '.'; parentName += ".";
} }
for (const field in model) { for (const field in model) {
if (!model.hasOwnProperty(field)) { if (!model.hasOwnProperty(field)) {
continue; continue;
} }
if (Array.isArray(model[field])) { if (Array.isArray(model[field])) {
obj[field] = Config.loadArrayFromEnv(logger, model[field][0], parentName + field); obj[field] = Config.loadArrayFromEnv(logger, model[field][0], parentName + field);
} 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 (!obj.hasOwnProperty(field)) {
const key = Config.getEnvKey(parentName + field); const key = Config.getEnvKey(parentName + field);
if (process.env.hasOwnProperty(key)) { if (process.env.hasOwnProperty(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!`);
} }
} }
} }
} }
private static loadArrayFromEnv(logger: winston.Logger, model: any, parentName: string = ''): any[] { private static loadArrayFromEnv(logger: winston.Logger, model: any, parentName: string = ""): any[] {
if (parentName !== '') { if (parentName !== "") {
parentName += '.'; parentName += ".";
} }
const arr = []; const arr = [];
let i = 0; let i = 0;
while (true) { while (true) {
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) {
break; break;
} }
if (Array.isArray(model)) { if (Array.isArray(model)) {
arr.push(Config.loadArrayFromEnv(logger, model[0], parentName + i)); arr.push(Config.loadArrayFromEnv(logger, model[0], parentName + i));
} else if (typeof model === 'object') { } else if (typeof model === "object") {
const obj = {}; const obj = {};
Config.loadObjFromEnv(logger, obj, model, parentName + i); Config.loadObjFromEnv(logger, obj, model, parentName + i);
if (Object.keys(obj).length > 0) { if (Object.keys(obj).length > 0) {
arr.push(obj); arr.push(obj);
} }
} else if (process.env.hasOwnProperty(key)) { } else if (process.env.hasOwnProperty(key)) {
arr.push(Config.parseEnvValue(process.env[key], model)); arr.push(Config.parseEnvValue(process.env[key], model));
} else { } else {
break; break;
} }
++i; ++i;
} }
return arr; return arr;
} }
private static getEnvKey(key: string): string { private static getEnvKey(key: string): string {
return ( return (
Config.envPrefix + Config.envPrefix +
'_' + "_" +
key key
.replace(/\./g, '__') .replace(/\./g, "__")
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
.toUpperCase() .toUpperCase()
); );
} }
private static parseEnvValue(value: string, model: any): any { private static parseEnvValue(value: string, model: any): any {
const type = typeof model; const type = typeof model;
const lower = value.toLowerCase(); const lower = value.toLowerCase();
if (type === 'boolean') { if (type === "boolean") {
return lower === 'true' || value === '1'; return lower === "true" || value === "1";
} }
if (type === 'number') { if (type === "number") {
return parseFloat(value); return parseFloat(value);
} }
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: string = "") {
const missing = Config.hasMissingFields(obj, model); const missing = Config.hasMissingFields(obj, model);
if (parentName !== '') { if (parentName !== "") {
parentName += '.'; parentName += ".";
} }
for (const field of missing) { for (const field of missing) {
logger.warn(`Field ${parentName}${field} is missing from config.json!`); logger.warn(`Field ${parentName}${field} is missing from config.json!`);
} }
for (const key in model) { for (const key in model) {
if (typeof model[key] === 'object' && obj.hasOwnProperty(key)) { if (typeof model[key] === "object" && obj.hasOwnProperty(key)) {
Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key); Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key);
} }
} }
} }
private static hasMissingFields(obj: object, model: object): string[] { private static hasMissingFields(obj: object, model: object): string[] {
const missing = []; const missing = [];
for (const key in model) { for (const key in model) {
if (!obj.hasOwnProperty(key)) { if (!obj.hasOwnProperty(key)) {
missing.push(key); missing.push(key);
} }
} }
return missing; return missing;
} }
} }

View file

@ -1,170 +1,170 @@
import { Pool } from 'mysql2/promise'; import { Pool } 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: any[][];
} }
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: any) => string;
table?: string; table?: string;
database?: string; database?: string;
} }
export interface IColumnJoin { export interface IColumnJoin {
table1: string; table1: string;
column1: string; column1: string;
table2: string; table2: string;
column2: string; column2: string;
database2?: string; database2?: string;
kind: 'INNER' | 'FULL OUTER' | 'LEFT' | 'RIGHT'; kind: "INNER" | "FULL OUTER" | "LEFT" | "RIGHT";
} }
export class DataTablesSsp { export class DataTablesSsp {
public draw: number; public draw: number;
public joins: IColumnJoin[] = []; public joins: IColumnJoin[] = [];
public extraDataColumns: string[] = []; public extraDataColumns: string[] = [];
private db: Pool; private db: Pool;
private table: string; private table: string;
private primaryKey: string; private primaryKey: string;
private columnSettings: IColumnSettings[]; private columnSettings: IColumnSettings[];
private start: number; private start: number;
private length: number; private length: number;
private _order: { private _order: {
column: number; column: number;
dir: string; dir: string;
}[]; }[];
private columns: { private columns: {
data: number; data: number;
name: string; name: string;
searchable: boolean; searchable: boolean;
orderable: boolean; orderable: boolean;
search: { search: {
value: string; value: string;
regex: boolean; regex: boolean;
}; };
}[]; }[];
private search: { private search: {
value: string; value: string;
regex: boolean; regex: boolean;
}; };
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: string = "1";
private customWhereSql: string = '1'; private customWhereSql: string = "1";
private limitSql: string = ''; private limitSql: string = "";
private orderSql: string = ''; private orderSql: string = "";
private joinSql: string = ''; private joinSql: string = "";
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);
this.length = parseInt(query.length as string, 10); this.length = parseInt(query.length as string, 10);
this.draw = parseInt(query.draw as string, 10); this.draw = parseInt(query.draw as string, 10);
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: any }[]).map((column) => {
return { return {
data: parseInt(column.data, 10), data: parseInt(column.data, 10),
name: column.name, name: column.name,
searchable: column.searchable === 'true', searchable: column.searchable === "true",
orderable: column.orderable === 'true', orderable: column.orderable === "true",
search: { value: column.search.value, regex: column.search.regex === 'true' }, search: { value: column.search.value, regex: column.search.regex === "true" },
}; };
}); });
this.search = { this.search = {
value: (query.search as any).value as string, value: (query.search as any).value as string,
regex: (query.search as any).regex === 'true', regex: (query.search as any).regex === "true",
}; };
this.db = db; this.db = db;
this.table = table; this.table = table;
this.primaryKey = primaryKey; this.primaryKey = primaryKey;
this.columnSettings = columnSettings; this.columnSettings = columnSettings;
} }
private colSettingsToStr(colSettings: IColumnSettings) { private colSettingsToStr(colSettings: IColumnSettings) {
const db = colSettings.database ? '`' + colSettings.database + '`.' : ''; const db = colSettings.database ? "`" + colSettings.database + "`." : "";
return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``; return `${db}\`${colSettings.table || this.table}\`.\`${colSettings.name}\``;
} }
private limit() { private limit() {
if (this.start !== undefined && this.length !== -1) { if (this.start !== undefined && this.length !== -1) {
this.limitSql = `LIMIT ${this.length} OFFSET ${this.start}`; this.limitSql = `LIMIT ${this.length} OFFSET ${this.start}`;
} }
return this; return this;
} }
private order() { private order() {
if (this._order === undefined) { if (this._order === undefined) {
return this; return this;
} }
const orderBy = []; const orderBy = [];
for (const order of this._order) { for (const order of this._order) {
const requestColumn = this.columns[order.column]; const requestColumn = this.columns[order.column];
if (!requestColumn.orderable) { if (!requestColumn.orderable) {
continue; continue;
} }
const colSettings = this.columnSettings[requestColumn.data]; const colSettings = this.columnSettings[requestColumn.data];
orderBy.push(`${this.colSettingsToStr(colSettings)} ${order.dir}`); orderBy.push(`${this.colSettingsToStr(colSettings)} ${order.dir}`);
} }
orderBy.push(`\`${this.table}\`.\`${this.primaryKey}\``); orderBy.push(`\`${this.table}\`.\`${this.primaryKey}\``);
if (orderBy.length > 0) { if (orderBy.length > 0) {
this.orderSql = 'ORDER BY ' + orderBy.join(', '); this.orderSql = "ORDER BY " + orderBy.join(", ");
} }
return this; return this;
} }
private join() { private join() {
for (const join of this.joins) { for (const join of this.joins) {
const db2 = join.database2 ? '`' + join.database2 + '`.' : ''; const db2 = join.database2 ? "`" + join.database2 + "`." : "";
this.joinSql += `${join.kind} JOIN ${db2}\`${join.table2}\` ON ${db2}\`${join.table2}\`.\`${join.column2}\` = \`${join.table1}\`.\`${join.column1}\`\n`; this.joinSql += `${join.kind} JOIN ${db2}\`${join.table2}\` ON ${db2}\`${join.table2}\`.\`${join.column2}\` = \`${join.table1}\`.\`${join.column1}\`\n`;
} }
return this; return this;
} }
private filter() { private filter() {
if (this.search.value?.length > 0) { if (this.search.value?.length > 0) {
const filterWheres = []; const filterWheres = [];
this.filterBindings = []; this.filterBindings = [];
for (const col of this.columns) { for (const col of this.columns) {
if (!col.searchable) { if (!col.searchable) {
continue; continue;
} }
const colSettings = this.columnSettings[col.data]; const colSettings = this.columnSettings[col.data];
const collate = colSettings.collation !== undefined ? `COLLATE ${colSettings.collation} ` : ''; const collate = colSettings.collation !== undefined ? `COLLATE ${colSettings.collation} ` : "";
filterWheres.push(`${this.colSettingsToStr(colSettings)} ${collate}LIKE ?`); filterWheres.push(`${this.colSettingsToStr(colSettings)} ${collate}LIKE ?`);
this.filterBindings.push(`%${this.search.value}%`); this.filterBindings.push(`%${this.search.value}%`);
} }
if (filterWheres.length > 0) { if (filterWheres.length > 0) {
this.filterWhereSql = '(' + filterWheres.map((w) => `(${w})`).join(' OR ') + ')'; this.filterWhereSql = "(" + filterWheres.map((w) => `(${w})`).join(" OR ") + ")";
} }
} }
this.customWhereSql = this.wheres.map((w) => `(${w})`).join(' AND '); this.customWhereSql = this.wheres.map((w) => `(${w})`).join(" AND ");
return this; return this;
} }
public sql(): string { public sql(): string {
const columns = [...this.columnSettings.map((c) => this.colSettingsToStr(c)), ...this.extraDataColumns]; const columns = [...this.columnSettings.map((c) => this.colSettingsToStr(c)), ...this.extraDataColumns];
return ` return `
SELECT ${columns.join(', ')} SELECT ${columns.join(", ")}
FROM ${this.table} FROM ${this.table}
${this.joinSql} ${this.joinSql}
WHERE WHERE
@ -173,19 +173,19 @@ export class DataTablesSsp {
${this.orderSql} ${this.orderSql}
${this.limitSql} ${this.limitSql}
`; `;
} }
private buildTotalCountSql(): string { private buildTotalCountSql(): string {
return ` return `
SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\` SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\`
FROM ${this.table} FROM ${this.table}
${this.joinSql} ${this.joinSql}
WHERE ${this.customWhereSql} WHERE ${this.customWhereSql}
`; `;
} }
private buildFilteredCountSql(): string { private buildFilteredCountSql(): string {
return ` return `
SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\` SELECT COUNT(\`${this.table}\`.\`${this.primaryKey}\`) AS \`count\`
FROM ${this.table} FROM ${this.table}
${this.joinSql} ${this.joinSql}
@ -193,56 +193,56 @@ export class DataTablesSsp {
${this.filterWhereSql} AND ${this.filterWhereSql} AND
${this.customWhereSql} ${this.customWhereSql}
`; `;
} }
public async run(queryTimeout: number = 10_000): Promise<IResult> { public async run(queryTimeout: number = 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, fields] = 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, fields] = 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, fields] = 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 any[][]).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) {
row[i] = col.formatter(row[i], row); row[i] = col.formatter(row[i], row);
} }
} }
return row; return row;
}); });
return { return {
recordsTotal, recordsTotal,
recordsFiltered, recordsFiltered,
draw: this.draw, draw: this.draw,
data: rows, data: rows,
}; };
} }
public where(condition: string, binding?: string | number) { public where(condition: string, binding?: string | number) {
this.wheres.push(condition); this.wheres.push(condition);
if (binding !== undefined) { if (binding !== undefined) {
this.customBindings.push(binding); this.customBindings.push(binding);
} }
return this; return this;
} }
} }

View file

@ -1,54 +1,54 @@
export enum EFaction { export enum EFaction {
Horde = 0, Horde = 0,
Alliance = 1, Alliance = 1,
} }
export interface IEmblem { export interface IEmblem {
icon: string; icon: string;
iconColor: string; iconColor: string;
border: string; border: string;
borderColor: string; borderColor: string;
background: string; background: string;
} }
export class Utils { export class Utils {
public static raceNames = { public static raceNames = {
1: 'human', 1: "human",
2: 'orc', 2: "orc",
3: 'dwarf', 3: "dwarf",
4: 'nightelf', 4: "nightelf",
5: 'scourge', 5: "scourge",
6: 'tauren', 6: "tauren",
7: 'gnome', 7: "gnome",
8: 'troll', 8: "troll",
10: 'bloodelf', 10: "bloodelf",
11: 'draenei', 11: "draenei",
}; };
public static classNames = { public static classNames = {
1: 'warrior', 1: "warrior",
2: 'paladin', 2: "paladin",
3: 'hunter', 3: "hunter",
4: 'rogue', 4: "rogue",
5: 'priest', 5: "priest",
6: 'deathknight', 6: "deathknight",
7: 'shaman', 7: "shaman",
8: 'mage', 8: "mage",
9: 'warlock', 9: "warlock",
11: 'druid', 11: "druid",
}; };
public static getFactionFromRaceId(race: number): EFaction { public static getFactionFromRaceId(race: number): EFaction {
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: any, padWithZeroes: boolean = 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"),
iconColor: obj.emblemColor.toString().padStart(padLength, '0'), iconColor: obj.emblemColor.toString().padStart(padLength, "0"),
border: obj.borderStyle.toString().padStart(padLength, '0'), border: obj.borderStyle.toString().padStart(padLength, "0"),
borderColor: obj.borderColor.toString().padStart(padLength, '0'), borderColor: obj.borderColor.toString().padStart(padLength, "0"),
background: obj.background.toString().padStart(padLength, '0'), background: obj.background.toString().padStart(padLength, "0"),
}; };
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,188 +1,188 @@
import * as express from 'express'; 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 { 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";
interface IGuildRank { interface IGuildRank {
id: number; id: number;
name: string; name: string;
} }
export class GuildController { export class GuildController {
private armory: Armory; private armory: Armory;
public constructor(armory: Armory) { public constructor(armory: Armory) {
this.armory = armory; this.armory = armory;
} }
public async guild(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> { public async guild(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.params.realm; const realmName = req.params.realm;
const guildName = req.params.name; const guildName = req.params.name;
const realm = this.armory.getRealm(realmName); const realm = this.armory.getRealm(realmName);
if (realm === undefined) { if (realm === undefined) {
// Could not find realm // Could not find realm
return next(404); return next(404);
} }
const guildData = await this.getGuildData(realm, guildName); const guildData = await this.getGuildData(realm, guildName);
if (guildData === null) { if (guildData === null) {
// Could not find guild // Could not find guild
return next(404); return next(404);
} }
res.render('guild.hbs', { res.render("guild.hbs", {
title: `Armory - ${guildName}`, title: `Armory - ${guildName}`,
realm: realm.name, realm: realm.name,
...guildData, ...guildData,
}); });
} }
public async members(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> { public async members(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.params.realm; const realmName = req.params.realm;
const guildId = parseInt(req.params.guild); const guildId = parseInt(req.params.guild);
const realm = this.armory.getRealm(realmName); const realm = this.armory.getRealm(realmName);
if (realm === undefined) { if (realm === undefined) {
// Could not find realm // Could not find realm
return next(404); return next(404);
} }
if (isNaN(guildId) || !(await this.guildExists(realm, guildId))) { if (isNaN(guildId) || !(await this.guildExists(realm, guildId))) {
// Could not find guild // Could not find guild
return next(404); return next(404);
} }
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
const charSet = await this.armory.getDatabaseCharset(realm.name); const charSet = await this.armory.getDatabaseCharset(realm.name);
let ssp = new DataTablesSsp(req.query, db, 'guild_member', 'guid', [ let ssp = new DataTablesSsp(req.query, db, "guild_member", "guid", [
{ name: 'name', table: 'characters', collation: `${charSet}_general_ci` }, { name: "name", table: "characters", collation: `${charSet}_general_ci` },
{ name: 'rank' }, { name: "rank" },
{ name: 'level', table: 'characters' }, { name: "level", table: "characters" },
{ name: 'class', table: 'characters', formatter: (cls) => Utils.classNames[cls] }, { name: "class", table: "characters", formatter: (cls) => Utils.classNames[cls] },
{ name: 'race', table: 'characters', formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? 'male' : 'female'}` }, { name: "race", table: "characters", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
{ name: 'online', table: 'characters', formatter: (online) => online === 1 }, { name: "online", table: "characters", formatter: (online) => online === 1 },
]); ]);
ssp.joins = [{ table1: 'guild_member', column1: 'guid', table2: 'characters', column2: 'guid', kind: 'LEFT' }]; ssp.joins = [{ table1: "guild_member", column1: "guid", table2: "characters", column2: "guid", kind: "LEFT" }];
ssp.extraDataColumns = ['`characters`.`gender`']; ssp.extraDataColumns = ["`characters`.`gender`"];
if (this.armory.config.hideGameMasters) { if (this.armory.config.hideGameMasters) {
ssp.joins.push({ ssp.joins.push({
table1: 'characters', table1: "characters",
column1: 'account', column1: "account",
table2: 'account_access', table2: "account_access",
column2: 'id', column2: "id",
database2: realm.authDatabase, database2: realm.authDatabase,
kind: 'LEFT', kind: "LEFT",
}); });
ssp = ssp.where( ssp = ssp.where(
`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`, `\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`,
); );
} }
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 = {}; (result as any).ranks = {};
for (const rank of ranks) { for (const rank of ranks) {
(result as any).ranks[rank.id] = encode(rank.name); (result as any).ranks[rank.id] = encode(rank.name);
} }
res.json(result); res.json(result);
} }
private async getGuildData(realm: IRealmConfig, name: string): Promise<any> { private async getGuildData(realm: IRealmConfig, name: string): Promise<any> {
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
let [rows, fields] = await db.query({ let [rows, fields] = 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 = ?
`, `,
values: [name], values: [name],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
if ((rows as RowDataPacket[]).length === 0) { if ((rows as RowDataPacket[]).length === 0) {
return null; return null;
} }
const guild = rows[0]; const guild = rows[0];
[rows, fields] = await db.query({ [rows, fields] = await db.query({
sql: ` sql: `
SELECT name, race FROM characters SELECT name, race FROM characters
WHERE guid = ? WHERE guid = ?
`, `,
values: [guild.leaderguid], values: [guild.leaderguid],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
const leader = rows[0]; const leader = rows[0];
[rows, fields] = await db.query({ [rows, fields] = 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 = ?
`, `,
values: [guild.guildid], values: [guild.guildid],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
const membersCount = rows[0].count; const membersCount = rows[0].count;
return { return {
id: guild.guildid, id: guild.guildid,
name: guild.name, name: guild.name,
leader: leader.name, leader: leader.name,
faction: Utils.getFactionFromRaceId(leader.race), faction: Utils.getFactionFromRaceId(leader.race),
emblem: Utils.makeEmblemObject(guild), emblem: Utils.makeEmblemObject(guild),
membersCount, membersCount,
}; };
} }
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, fields] = await db.query({
sql: ` sql: `
SELECT guildid SELECT guildid
FROM guild WHERE name = ? FROM guild WHERE name = ?
`, `,
values: [name], values: [name],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
if ((rows as RowDataPacket[]).length === 0) { if ((rows as RowDataPacket[]).length === 0) {
return null; return null;
} }
return rows[0].guildid; return rows[0].guildid;
} }
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, fields] = await db.query({
sql: ` sql: `
SELECT guildid SELECT guildid
FROM guild WHERE guildid FROM guild WHERE guildid
`, `,
values: [id], values: [id],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
return (rows as RowDataPacket[]).length !== 0; return (rows as RowDataPacket[]).length !== 0;
} }
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, fields] = 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 = ?
`, `,
values: [id], values: [id],
timeout: this.armory.config.dbQueryTimeout, timeout: this.armory.config.dbQueryTimeout,
}); });
return rows as IGuildRank[]; return rows as IGuildRank[];
} }
} }

View file

@ -1,64 +1,64 @@
import * as express from 'express'; import * as express from "express";
import { Utils } from '../Utils'; import { Utils } from "../Utils";
import { Armory } from '../Armory'; import { Armory } from "../Armory";
import { DataTablesSsp } from '../DataTablesSsp'; import { DataTablesSsp } from "../DataTablesSsp";
export class IndexController { export class IndexController {
private armory: Armory; private armory: Armory;
public constructor(armory: Armory) { public constructor(armory: Armory) {
this.armory = armory; this.armory = armory;
} }
public async index(req: express.Request, res: express.Response): Promise<void> { public async index(req: express.Request, res: express.Response): Promise<void> {
res.render('index.hbs', { res.render("index.hbs", {
title: 'Armory', title: "Armory",
realms: this.armory.config.realms.map((r) => r.name), realms: this.armory.config.realms.map((r) => r.name),
}); });
} }
public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> { public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.query.realm as string; const realmName = req.query.realm as string;
const realm = realmName === undefined ? this.armory.config.realms[0] : this.armory.config.realms.find((r) => r.name === realmName); const realm = realmName === undefined ? this.armory.config.realms[0] : this.armory.config.realms.find((r) => r.name === realmName);
if (realm === undefined) { if (realm === undefined) {
return next(400); return next(400);
} }
const db = this.armory.getCharactersDb(realm.name); const db = this.armory.getCharactersDb(realm.name);
const charSet = await this.armory.getDatabaseCharset(realm.name); const charSet = await this.armory.getDatabaseCharset(realm.name);
let ssp = new DataTablesSsp(req.query, db, 'characters', 'guid', [ let ssp = new DataTablesSsp(req.query, db, "characters", "guid", [
{ name: 'name', collation: `${charSet}_general_ci` }, { name: "name", collation: `${charSet}_general_ci` },
{ table: 'guild', name: 'name' }, { table: "guild", name: "name" },
{ name: 'level' }, { name: "level" },
{ name: 'class', formatter: (cls) => Utils.classNames[cls] }, { name: "class", formatter: (cls) => Utils.classNames[cls] },
{ name: 'race', formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? 'male' : 'female'}` }, { name: "race", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
{ name: 'online', formatter: (online) => online === 1 }, { name: "online", formatter: (online) => online === 1 },
]); ]);
ssp.joins = [ ssp.joins = [
{ table1: 'characters', column1: 'guid', table2: 'guild_member', column2: 'guid', kind: 'LEFT' }, { table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" },
{ table1: 'guild_member', column1: 'guildid', table2: 'guild', column2: 'guildid', kind: 'LEFT' }, { table1: "guild_member", column1: "guildid", table2: "guild", column2: "guildid", kind: "LEFT" },
]; ];
ssp.extraDataColumns = ['`characters`.`gender`']; ssp.extraDataColumns = ["`characters`.`gender`"];
if (this.armory.config.hideGameMasters) { if (this.armory.config.hideGameMasters) {
ssp.joins.push({ ssp.joins.push({
table1: 'characters', table1: "characters",
column1: 'account', column1: "account",
table2: 'account_access', table2: "account_access",
column2: 'id', column2: "id",
database2: realm.authDatabase, database2: realm.authDatabase,
kind: 'LEFT', kind: "LEFT",
}); });
ssp = ssp.where( ssp = ssp.where(
`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`, `\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`,
); );
} }
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; (result as any).realm = realm.name;
res.json(result); res.json(result);
} }
} }

View file

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

6
src/index.d.ts vendored
View file

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

View file

@ -1,79 +1,79 @@
import * as fs from 'fs'; import * as fs from "fs";
const fsp = fs.promises; 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 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 { Response } from "node-fetch";
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 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(); 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 {
private startTime: number; private startTime: number;
public constructor() { public constructor() {
this.start(); this.start();
} }
public start(): void { public start(): void {
this.startTime = Date.now(); this.startTime = Date.now();
} }
public stop(text?: string): void { public stop(text?: string): void {
const dt = Date.now() - this.startTime; const dt = Date.now() - this.startTime;
const txt = text ?? 'Done in {time}'; const txt = text ?? "Done in {time}";
console.log(txt.replace('{time}', prettyMs(dt))); console.log(txt.replace("{time}", prettyMs(dt)));
} }
} }
class Progress { class Progress {
private bar: cliProgress.SingleBar; private bar: cliProgress.SingleBar;
private stopwatch: Stopwatch; private stopwatch: Stopwatch;
public constructor(text: string, operations: number) { public constructor(text: string, operations: number) {
this.bar = this.createProgressBar(text, operations); this.bar = this.createProgressBar(text, operations);
this.stopwatch = new Stopwatch(); this.stopwatch = new Stopwatch();
} }
public increment(step?: number): void { public increment(step?: number): void {
this.bar.increment(step); this.bar.increment(step);
} }
public stop(): void { public stop(): void {
this.bar.stop(); this.bar.stop();
this.stopwatch.stop(); this.stopwatch.stop();
} }
private createProgressBar(text: string, total: number): cliProgress.SingleBar { private createProgressBar(text: string, total: number): cliProgress.SingleBar {
const progress = new cliProgress.SingleBar( const progress = new cliProgress.SingleBar(
{ {
format: `${text} {bar} {percentage}% ({value} / {total})`, format: `${text} {bar} {percentage}% ({value} / {total})`,
}, },
cliProgress.Presets.shades_classic, cliProgress.Presets.shades_classic,
); );
progress.start(total, 0); progress.start(total, 0);
return progress; return progress;
} }
} }
class HttpRequestError extends Error { class HttpRequestError extends Error {
public response: Response; public response: Response;
public constructor(response: Response) { public constructor(response: Response) {
super(`Could not download ${response.url} (${response.status})`); super(`Could not download ${response.url} (${response.status})`);
this.name = 'HttpRequestError'; this.name = "HttpRequestError";
this.response = response; this.response = response;
Object.setPrototypeOf(this, HttpRequestError.prototype); Object.setPrototypeOf(this, HttpRequestError.prototype);
} }
} }
let dbc: DbcManager; let dbc: DbcManager;
@ -93,339 +93,339 @@ 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 | any> {
const dataDir = path.join(process.cwd(), 'data'); const dataDir = path.join(process.cwd(), "data");
const fullPath = `${dir}/${file}`; const fullPath = `${dir}/${file}`;
try { 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);
} }
await mkdirp(path.join(dataDir, dir)); await mkdirp(path.join(dataDir, dir));
if (res.headers.get('Content-Type') === 'application/json') { if (res.headers.get("Content-Type") === "application/json") {
const json = await res.json(); const json = await res.json();
fsp.writeFile(path.join(dataDir, fullPath), JSON.stringify(json)); fsp.writeFile(path.join(dataDir, fullPath), JSON.stringify(json));
return json; return json;
} else { } else {
const fileStream = fs.createWriteStream(path.join(dataDir, fullPath)); const fileStream = fs.createWriteStream(path.join(dataDir, fullPath));
await new Promise((resolve, rej) => { await new Promise((resolve, rej) => {
res.body.pipe(fileStream); res.body.pipe(fileStream);
res.body.on('error', rej); res.body.on("error", rej);
fileStream.on('finish', resolve); fileStream.on("finish", resolve);
}); });
return fileStream.path.toString(); return fileStream.path.toString();
} }
} catch (err) { } catch (err) {
throw err; throw err;
} }
} }
function queueTexturesAndModels(item: any): void { function queueTexturesAndModels(item: any): void {
if (item.TextureFiles !== null) { if (item.TextureFiles !== null) {
for (const key in item.TextureFiles) { for (const key in item.TextureFiles) {
for (const file of item.TextureFiles[key]) { for (const file of item.TextureFiles[key]) {
if (file.FileDataId !== 0) { if (file.FileDataId !== 0) {
texturesDownloadQueue.add(file.FileDataId); texturesDownloadQueue.add(file.FileDataId);
} }
} }
} }
} }
if (item.ModelFiles !== null) { if (item.ModelFiles !== null) {
for (const key in item.ModelFiles) { for (const key in item.ModelFiles) {
for (const file of item.ModelFiles[key]) { for (const file of item.ModelFiles[key]) {
if (file.FileDataId !== 0) { if (file.FileDataId !== 0) {
modelsDownloadQueue.add(file.FileDataId); modelsDownloadQueue.add(file.FileDataId);
} }
} }
} }
} }
if (typeof item.Model === 'number' && item.Model !== 0) { if (typeof item.Model === "number" && item.Model !== 0) {
modelsDownloadQueue.add(item.Model); modelsDownloadQueue.add(item.Model);
} }
if (item.Textures !== null) { if (item.Textures !== null) {
for (const key in item.Textures) { for (const key in item.Textures) {
if (item.Textures[key] !== 0) { if (item.Textures[key] !== 0) {
texturesDownloadQueue.add(item.Textures[key]); texturesDownloadQueue.add(item.Textures[key]);
} }
} }
} }
if (item.Textures2 !== null) { if (item.Textures2 !== null) {
for (const key in item.Textures2) { for (const key in item.Textures2) {
if (item.Textures2[key] !== 0) { if (item.Textures2[key] !== 0) {
texturesDownloadQueue.add(item.Textures2[key]); texturesDownloadQueue.add(item.Textures2[key]);
} }
} }
} }
} }
async function downloadRaces(): Promise<void> { async function downloadRaces(): Promise<void> {
const races = ['human', 'nightelf', 'dwarf', 'gnome', 'draenei', 'orc', 'troll', 'tauren', 'bloodelf', 'scourge']; const races = ["human", "nightelf", "dwarf", "gnome", "draenei", "orc", "troll", "tauren", "bloodelf", "scourge"];
const genders = ['male', 'female']; const genders = ["male", "female"];
const raceGenderCombo = races.map((race) => [race + genders[0], race + genders[1]]).flat(); const raceGenderCombo = races.map((race) => [race + genders[0], race + genders[1]]).flat();
const progress = new Progress('Downloading races data...', raceGenderCombo.length); const progress = new Progress("Downloading races data...", raceGenderCombo.length);
await promisepool.PromisePool.for(raceGenderCombo) await promisepool.PromisePool.for(raceGenderCombo)
.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 (
element.SkinnedModel !== null && element.SkinnedModel !== null &&
typeof element.SkinnedModel.CollectionFileDataID === 'number' && typeof element.SkinnedModel.CollectionFileDataID === "number" &&
element.SkinnedModel.CollectionFileDataID !== 0 element.SkinnedModel.CollectionFileDataID !== 0
) { ) {
modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID); modelsDownloadQueue.add(element.SkinnedModel.CollectionFileDataID);
} }
if (element.BoneSet !== null && typeof element.BoneSet.BoneFileDataID === 'number' && element.BoneSet.BoneFileDataID !== 0) { if (element.BoneSet !== null && typeof element.BoneSet.BoneFileDataID === "number" && element.BoneSet.BoneFileDataID !== 0) {
bonesDownloadQueue.add(element.BoneSet.BoneFileDataID); bonesDownloadQueue.add(element.BoneSet.BoneFileDataID);
} }
} }
} }
} }
const textureFiles = Object.keys(customizationJson.TextureFiles) const textureFiles = Object.keys(customizationJson.TextureFiles)
.map((key) => customizationJson.TextureFiles[key]) .map((key) => customizationJson.TextureFiles[key])
.flat(); .flat();
for (const file of textureFiles) { for (const file of textureFiles) {
texturesDownloadQueue.add(file.FileDataId); texturesDownloadQueue.add(file.FileDataId);
} }
progress.increment(); progress.increment();
}); });
progress.stop(); progress.stop();
} }
async function downloadArmors(): Promise<void> { async function downloadArmors(): Promise<void> {
const rows = await dbc const rows = await dbc
.item() .item()
.filter((row) => row.classId === classIdArmor) .filter((row) => row.classId === classIdArmor)
.toArray(); .toArray();
const progress = new Progress('Downloading armor data...', rows.length); const progress = new Progress("Downloading armor data...", rows.length);
await promisepool.PromisePool.for(rows) await promisepool.PromisePool.for(rows)
.withConcurrency(50) .withConcurrency(50)
.process(async (row) => { .process(async (row) => {
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id]; const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
if (modifiedAppearance === undefined) { if (modifiedAppearance === undefined) {
progress.increment(); progress.increment();
return; return;
} }
const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId]; const appearance = dbcItemAppearanceById[modifiedAppearance.itemAppearanceId];
const metaPath = [invTypeShield, invTypeOffHand].includes(row.inventoryType) ? 'item' : `armor/${row.inventoryType}`; const metaPath = [invTypeShield, invTypeOffHand].includes(row.inventoryType) ? "item" : `armor/${row.inventoryType}`;
try { try {
const itemJson = await download(`meta/${metaPath}`, `${appearance.itemDisplayInfoId}.json`); const itemJson = await download(`meta/${metaPath}`, `${appearance.itemDisplayInfoId}.json`);
queueTexturesAndModels(itemJson); queueTexturesAndModels(itemJson);
progress.increment(); progress.increment();
} catch (err) { } catch (err) {
if (err instanceof HttpRequestError && err.response.status === 404) { if (err instanceof HttpRequestError && err.response.status === 404) {
progress.increment(); progress.increment();
return; return;
} }
throw err; throw err;
} }
}); });
progress.stop(); progress.stop();
} }
async function downloadWeapons(): Promise<void> { async function downloadWeapons(): Promise<void> {
const rows = await dbc const rows = await dbc
.item() .item()
.filter((row) => row.classId === classIdWeapon) .filter((row) => row.classId === classIdWeapon)
.toArray(); .toArray();
const progress = new Progress('Downloading weapon data...', rows.length); const progress = new Progress("Downloading weapon data...", rows.length);
await promisepool.PromisePool.for(rows) await promisepool.PromisePool.for(rows)
.withConcurrency(50) .withConcurrency(50)
.process(async (row) => { .process(async (row) => {
const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id]; const modifiedAppearance = dbcItemModifiedAppearanceByItemId[row.id];
if (modifiedAppearance === undefined) { if (modifiedAppearance === undefined) {
progress.increment(); progress.increment();
return; return;
} }
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) {
if (err instanceof HttpRequestError && err.response.status === 404) { if (err instanceof HttpRequestError && err.response.status === 404) {
progress.increment(); progress.increment();
return; return;
} }
throw err; throw err;
} }
}); });
progress.stop(); progress.stop();
} }
async function readDbcData(): Promise<void> { async function readDbcData(): Promise<void> {
console.log('Reading DBC data...'); console.log("Reading DBC data...");
dbc = new DbcManager(); dbc = new DbcManager();
await dbc.loadAllFiles(); await dbc.loadAllFiles();
dbcItemAppearanceById = {}; dbcItemAppearanceById = {};
for await (const row of dbc.itemAppearance()) { for await (const row of dbc.itemAppearance()) {
dbcItemAppearanceById[row.id] = row; dbcItemAppearanceById[row.id] = row;
} }
dbcItemModifiedAppearanceByItemId = {}; dbcItemModifiedAppearanceByItemId = {};
for await (const row of dbc.itemModifiedAppearance()) { for await (const row of dbc.itemModifiedAppearance()) {
dbcItemModifiedAppearanceByItemId[row.itemId] = row; dbcItemModifiedAppearanceByItemId[row.itemId] = row;
} }
dbcMountBySourceSpellId = {}; dbcMountBySourceSpellId = {};
for await (const row of dbc.mount()) { for await (const row of dbc.mount()) {
dbcMountBySourceSpellId[row.sourceSpellId] = row; dbcMountBySourceSpellId[row.sourceSpellId] = row;
} }
dbcMountDisplayByMountId = {}; dbcMountDisplayByMountId = {};
for await (const row of dbc.mountDisplay()) { for await (const row of dbc.mountDisplay()) {
if (!(row.mountId in dbcMountDisplayByMountId)) { if (!(row.mountId in dbcMountDisplayByMountId)) {
dbcMountDisplayByMountId[row.mountId] = row; dbcMountDisplayByMountId[row.mountId] = row;
} }
} }
} }
async function downloadMounts(): Promise<void> { async function downloadMounts(): Promise<void> {
const mountSpells = await dbc const mountSpells = await dbc
.spell() .spell()
.filter((spell) => spell.mechanic === spellMechanicMounted) .filter((spell) => spell.mechanic === spellMechanicMounted)
.toArray(); .toArray();
const progress = new Progress('Downloading mount data...', mountSpells.length); const progress = new Progress("Downloading mount data...", mountSpells.length);
await promisepool.PromisePool.for(mountSpells) await promisepool.PromisePool.for(mountSpells)
.withConcurrency(50) .withConcurrency(50)
.process(async (spell) => { .process(async (spell) => {
const mount = dbcMountBySourceSpellId[spell.id]; const mount = dbcMountBySourceSpellId[spell.id];
if (mount === undefined) { if (mount === undefined) {
progress.increment(); progress.increment();
return; return;
} }
const display = dbcMountDisplayByMountId[mount.id]; const display = dbcMountDisplayByMountId[mount.id];
const json = await download('meta/npc', `${display.creatureDisplayInfoId}.json`); const json = await download("meta/npc", `${display.creatureDisplayInfoId}.json`);
queueTexturesAndModels(json); queueTexturesAndModels(json);
progress.increment(); progress.increment();
}); });
progress.stop(); progress.stop();
} }
async function downloadTextures(): Promise<void> { async function downloadTextures(): Promise<void> {
const progress = new Progress('Downloading textures...', texturesDownloadQueue.size); const progress = new Progress("Downloading textures...", texturesDownloadQueue.size);
await promisepool.PromisePool.for(Array.from(texturesDownloadQueue)) await promisepool.PromisePool.for(Array.from(texturesDownloadQueue))
.withConcurrency(25) .withConcurrency(25)
.process(async (fileDataId) => { .process(async (fileDataId) => {
await download('textures', `${fileDataId}.png`); await download("textures", `${fileDataId}.png`);
progress.increment(); progress.increment();
}); });
progress.stop(); progress.stop();
} }
async function downloadModels(): Promise<void> { async function downloadModels(): Promise<void> {
const progress = new Progress('Downloading models...', modelsDownloadQueue.size); const progress = new Progress("Downloading models...", modelsDownloadQueue.size);
await promisepool.PromisePool.for(Array.from(modelsDownloadQueue)) await promisepool.PromisePool.for(Array.from(modelsDownloadQueue))
.withConcurrency(25) .withConcurrency(25)
.process(async (fileDataId) => { .process(async (fileDataId) => {
await download('mo3', `${fileDataId}.mo3`); await download("mo3", `${fileDataId}.mo3`);
progress.increment(); progress.increment();
}); });
progress.stop(); progress.stop();
} }
async function downloadBones(): Promise<void> { async function downloadBones(): Promise<void> {
const progress = new Progress('Downloading bones...', bonesDownloadQueue.size); const progress = new Progress("Downloading bones...", bonesDownloadQueue.size);
await promisepool.PromisePool.for(Array.from(bonesDownloadQueue)) await promisepool.PromisePool.for(Array.from(bonesDownloadQueue))
.withConcurrency(25) .withConcurrency(25)
.process(async (fileDataId) => { .process(async (fileDataId) => {
try { try {
await download('bone', `${fileDataId}.bone`); await download("bone", `${fileDataId}.bone`);
progress.increment(); progress.increment();
} catch (err) { } catch (err) {
if (err instanceof HttpRequestError && err.response.status === 404) { if (err instanceof HttpRequestError && err.response.status === 404) {
progress.increment(); progress.increment();
return; return;
} }
throw err; throw err;
} }
}); });
progress.stop(); progress.stop();
} }
async function parseModels(): Promise<void> { async function parseModels(): Promise<void> {
const files = await glob('data/mo3/*.mo3'); const files = await glob("data/mo3/*.mo3");
const progress = new Progress('Reading model files for texture references...', files.length); const progress = new Progress("Reading model files for texture references...", files.length);
await promisepool.PromisePool.for(files) await promisepool.PromisePool.for(files)
.withConcurrency(20) .withConcurrency(20)
.process(async (file) => { .process(async (file) => {
const buffer = await fsp.readFile(file); const buffer = await fsp.readFile(file);
const texturesOffset = buffer.readUInt32LE(60); const texturesOffset = buffer.readUInt32LE(60);
const uncompressedSize = buffer.readUInt32LE(112); const uncompressedSize = buffer.readUInt32LE(112);
const compressedData = buffer.slice(116); const compressedData = buffer.slice(116);
const data = Buffer.from(pako.inflate(compressedData)); const data = Buffer.from(pako.inflate(compressedData));
if (data.length !== uncompressedSize) { if (data.length !== uncompressedSize) {
throw `Unexpected data size ${data.length}, expected ${uncompressedSize}`; throw `Unexpected data size ${data.length}, expected ${uncompressedSize}`;
} }
const nbTextures = data.readInt32LE(texturesOffset); const nbTextures = data.readInt32LE(texturesOffset);
let offset = texturesOffset + 4; let offset = texturesOffset + 4;
for (let i = 0; i < nbTextures; ++i) { for (let i = 0; i < nbTextures; ++i) {
const textureId = data.readUInt32LE(offset + 4 + 4); const textureId = data.readUInt32LE(offset + 4 + 4);
if (textureId !== 0) { if (textureId !== 0) {
texturesDownloadQueue.add(textureId); texturesDownloadQueue.add(textureId);
} }
offset += 4 + 4 + 4; offset += 4 + 4 + 4;
} }
progress.increment(); progress.increment();
}); });
progress.stop(); progress.stop();
} }
async function main(): Promise<void> { async function main(): Promise<void> {
const sw = new Stopwatch(); const sw = new Stopwatch();
await readDbcData(); await readDbcData();
await downloadRaces(); // Download info for all races await downloadRaces(); // Download info for all races
await downloadArmors(); // Download info for all armors await downloadArmors(); // Download info for all armors
await downloadWeapons(); // Download info for all weapons await downloadWeapons(); // Download info for all weapons
await downloadMounts(); // Download info for all mounts await downloadMounts(); // Download info for all mounts
await downloadModels(); // Download all queued models await downloadModels(); // Download all queued models
await parseModels(); // Read model files to find texture references await parseModels(); // Read model files to find texture references
await downloadTextures(); // Download all queued textures await downloadTextures(); // Download all queued textures
await downloadBones(); // Download all queued bones await downloadBones(); // Download all queued bones
sw.stop('Everything done in {time}'); sw.stop("Everything done in {time}");
} }
main(); main();

View file

@ -1,118 +1,118 @@
function waitForEmblemImages($emblem) { function waitForEmblemImages($emblem) {
return Promise.all( return Promise.all(
$emblem.find('.images img').map((idx, img) => { $emblem.find(".images img").map((idx, img) => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
if (img.complete) { if (img.complete) {
res(); res();
} else { } else {
img.addEventListener('load', res); img.addEventListener("load", res);
img.addEventListener('error', rej); img.addEventListener("error", rej);
} }
}); });
}), }),
); );
} }
function createGuildEmblem(emblem, el) { function createGuildEmblem(emblem, el) {
const $emblem = $(el); const $emblem = $(el);
const canvas = $emblem.find('canvas')[0]; const canvas = $emblem.find("canvas")[0];
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const imgUrl = (type, section, value, value2) => const imgUrl = (type, section, value, value2) =>
`${handlebarsData.websiteRoot}/img/guild-emblems/${type}_${value}${value2 ? '_' + value2 : ''}_T${section}_U.PNG`; `${handlebarsData.websiteRoot}/img/guild-emblems/${type}_${value}${value2 ? "_" + value2 : ""}_T${section}_U.PNG`;
const $images = $('<div>').addClass('images').appendTo($emblem); const $images = $("<div>").addClass("images").appendTo($emblem);
const bgUpper = $('<img>').attr('src', imgUrl('Background', 'U', emblem.background)).appendTo($images)[0]; const bgUpper = $("<img>").attr("src", imgUrl("Background", "U", emblem.background)).appendTo($images)[0];
const bgLower = $('<img>').attr('src', imgUrl('Background', 'L', emblem.background)).appendTo($images)[0]; const bgLower = $("<img>").attr("src", imgUrl("Background", "L", emblem.background)).appendTo($images)[0];
const iconUpper = $('<img>').attr('src', imgUrl('Emblem', 'U', emblem.icon, emblem.iconColor)).appendTo($images)[0]; const iconUpper = $("<img>").attr("src", imgUrl("Emblem", "U", emblem.icon, emblem.iconColor)).appendTo($images)[0];
const iconLower = $('<img>').attr('src', imgUrl('Emblem', 'L', emblem.icon, emblem.iconColor)).appendTo($images)[0]; const iconLower = $("<img>").attr("src", imgUrl("Emblem", "L", emblem.icon, emblem.iconColor)).appendTo($images)[0];
const borderUpper = $('<img>').attr('src', imgUrl('Border', 'U', emblem.border, emblem.borderColor)).appendTo($images)[0]; const borderUpper = $("<img>").attr("src", imgUrl("Border", "U", emblem.border, emblem.borderColor)).appendTo($images)[0];
const borderLower = $('<img>').attr('src', imgUrl('Border', 'L', emblem.border, emblem.borderColor)).appendTo($images)[0]; const borderLower = $("<img>").attr("src", imgUrl("Border", "L", emblem.border, emblem.borderColor)).appendTo($images)[0];
const drawEmblemLayer = (ctx, layer) => { const drawEmblemLayer = (ctx, layer) => {
const [upper, lower] = layer; const [upper, lower] = layer;
const w = upper.width / 2; const w = upper.width / 2;
const uh = upper.height; const uh = upper.height;
const lh = lower.height; const lh = lower.height;
ctx.drawImage(upper, 0, 0, w, uh, w, 0, w, uh); ctx.drawImage(upper, 0, 0, w, uh, w, 0, w, uh);
ctx.drawImage(lower, 0, 0, w, lh, w, upper.height, w, lh); ctx.drawImage(lower, 0, 0, w, lh, w, upper.height, w, lh);
ctx.save(); ctx.save();
ctx.scale(-1, 1); ctx.scale(-1, 1);
ctx.drawImage(upper, 0, 0, w, uh, 0, 0, -w, uh); ctx.drawImage(upper, 0, 0, w, uh, 0, 0, -w, uh);
ctx.drawImage(lower, 0, 0, w, lh, 0, upper.height, -w, lh); ctx.drawImage(lower, 0, 0, w, lh, 0, upper.height, -w, lh);
ctx.restore(); ctx.restore();
}; };
waitForEmblemImages($emblem).then(() => { waitForEmblemImages($emblem).then(() => {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, 0); ctx.moveTo(0, 0);
ctx.lineTo(26, 0); ctx.lineTo(26, 0);
ctx.lineTo(64, 28); ctx.lineTo(64, 28);
ctx.lineTo(102, 0); ctx.lineTo(102, 0);
ctx.lineTo(128, 0); ctx.lineTo(128, 0);
ctx.lineTo(128, 96); ctx.lineTo(128, 96);
ctx.lineTo(0, 96); ctx.lineTo(0, 96);
ctx.closePath(); ctx.closePath();
ctx.clip(); ctx.clip();
drawEmblemLayer(ctx, [bgUpper, bgLower]); drawEmblemLayer(ctx, [bgUpper, bgLower]);
drawEmblemLayer(ctx, [iconUpper, iconLower]); drawEmblemLayer(ctx, [iconUpper, iconLower]);
drawEmblemLayer(ctx, [borderUpper, borderLower]); drawEmblemLayer(ctx, [borderUpper, borderLower]);
}); });
} }
function createArenaEmblem(teamSize, emblem, el) { function createArenaEmblem(teamSize, emblem, el) {
const $emblem = $(el); const $emblem = $(el);
const canvas = $emblem.find('canvas')[0]; const canvas = $emblem.find("canvas")[0];
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const imgUrl = (teamSize, type, value) => const imgUrl = (teamSize, type, value) =>
`${handlebarsData.websiteRoot}/img/arena-banners/PVP-Banner${teamSize ? '-' + teamSize : ''}${type ? '-' + type : ''}${ `${handlebarsData.websiteRoot}/img/arena-banners/PVP-Banner${teamSize ? "-" + teamSize : ""}${type ? "-" + type : ""}${
value ? '-' + value : '' value ? "-" + value : ""
}.PNG`; }.PNG`;
const $images = $('<div>').addClass('images').appendTo($emblem); const $images = $("<div>").addClass("images").appendTo($emblem);
const banner = $('<img>').attr('src', imgUrl(teamSize)).appendTo($images)[0]; const banner = $("<img>").attr("src", imgUrl(teamSize)).appendTo($images)[0];
const bannerCrop = $('<img>').attr('src', imgUrl(teamSize, 'Crop')).appendTo($images)[0]; const bannerCrop = $("<img>").attr("src", imgUrl(teamSize, "Crop")).appendTo($images)[0];
const border = $('<img>').attr('src', imgUrl(teamSize, 'Border', emblem.border)).appendTo($images)[0]; const border = $("<img>").attr("src", imgUrl(teamSize, "Border", emblem.border)).appendTo($images)[0];
const icon = $('<img>').attr('src', imgUrl(undefined, 'Emblem', emblem.icon)).appendTo($images)[0]; const icon = $("<img>").attr("src", imgUrl(undefined, "Emblem", emblem.icon)).appendTo($images)[0];
const colorToHex = (color) => '#' + parseInt(color).toString(16).substring(2); const colorToHex = (color) => "#" + parseInt(color).toString(16).substring(2);
waitForEmblemImages($emblem).then(() => { waitForEmblemImages($emblem).then(() => {
const srcH = 224; const srcH = 224;
const h = 128; const h = 128;
const w = (banner.width / srcH) * h; const w = (banner.width / srcH) * h;
const iconW = icon.width * 0.35; const iconW = icon.width * 0.35;
const iconH = icon.height * 0.35; const iconH = icon.height * 0.35;
ctx.drawImage(banner, 0, 0, banner.width, srcH, 0, 0, w, h); ctx.drawImage(banner, 0, 0, banner.width, srcH, 0, 0, w, h);
ctx.drawImage(tintImage(bannerCrop, colorToHex(emblem.background)), 0, 0, banner.width, srcH, 0, 0, w, h); ctx.drawImage(tintImage(bannerCrop, colorToHex(emblem.background)), 0, 0, banner.width, srcH, 0, 0, w, h);
ctx.drawImage(tintImage(border, colorToHex(emblem.borderColor)), 0, 0, border.width, srcH, 0, 0, w, h); ctx.drawImage(tintImage(border, colorToHex(emblem.borderColor)), 0, 0, border.width, srcH, 0, 0, w, h);
ctx.drawImage(tintImage(icon, colorToHex(emblem.iconColor)), w * 0.385 - iconW / 2, h * 0.325 - iconH / 2, iconW, iconH); ctx.drawImage(tintImage(icon, colorToHex(emblem.iconColor)), w * 0.385 - iconW / 2, h * 0.325 - iconH / 2, iconW, iconH);
}); });
} }
const tintCanvas = document.createElement('canvas'); const tintCanvas = document.createElement("canvas");
const tintContext = tintCanvas.getContext('2d'); const tintContext = tintCanvas.getContext("2d");
function tintImage(image, color, opacity = 1.0) { function tintImage(image, color, opacity = 1.0) {
const ctx = tintContext; const ctx = tintContext;
ctx.canvas.width = image.width; ctx.canvas.width = image.width;
ctx.canvas.height = image.height; ctx.canvas.height = image.height;
// First draw the image to the buffer // First draw the image to the buffer
ctx.drawImage(image, 0, 0); ctx.drawImage(image, 0, 0);
// Multiply with a rectangle of the specified color // Multiply with a rectangle of the specified color
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.globalCompositeOperation = 'multiply'; ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// Finally, fix masking issues and globalAlpha // Finally, fix masking issues and globalAlpha
ctx.globalAlpha = opacity; ctx.globalAlpha = opacity;
ctx.globalCompositeOperation = 'destination-in'; ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(image, 0, 0); ctx.drawImage(image, 0, 0);
return ctx.canvas; return ctx.canvas;
} }

View file

@ -1,6 +1,6 @@
window.parent.postMessage( window.parent.postMessage(
{ {
url: window.location.pathname.replace(handlebarsData.websiteRoot, ''), url: window.location.pathname.replace(handlebarsData.websiteRoot, ""),
}, },
'*', "*",
); );

View file

@ -4,8 +4,6 @@
"outDir": "build", "outDir": "build",
"moduleResolution": "node" "moduleResolution": "node"
}, },
"exclude": [ "exclude": ["node_modules"],
"node_modules"
],
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }