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

View file

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

View file

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

View file

@ -1,54 +1,54 @@
export enum EFaction {
Horde = 0,
Alliance = 1,
Horde = 0,
Alliance = 1,
}
export interface IEmblem {
icon: string;
iconColor: string;
border: string;
borderColor: string;
background: string;
icon: string;
iconColor: string;
border: string;
borderColor: string;
background: string;
}
export class Utils {
public static raceNames = {
1: 'human',
2: 'orc',
3: 'dwarf',
4: 'nightelf',
5: 'scourge',
6: 'tauren',
7: 'gnome',
8: 'troll',
10: 'bloodelf',
11: 'draenei',
};
public static classNames = {
1: 'warrior',
2: 'paladin',
3: 'hunter',
4: 'rogue',
5: 'priest',
6: 'deathknight',
7: 'shaman',
8: 'mage',
9: 'warlock',
11: 'druid',
};
public static raceNames = {
1: "human",
2: "orc",
3: "dwarf",
4: "nightelf",
5: "scourge",
6: "tauren",
7: "gnome",
8: "troll",
10: "bloodelf",
11: "draenei",
};
public static classNames = {
1: "warrior",
2: "paladin",
3: "hunter",
4: "rogue",
5: "priest",
6: "deathknight",
7: "shaman",
8: "mage",
9: "warlock",
11: "druid",
};
public static getFactionFromRaceId(race: number): EFaction {
return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde;
}
public static getFactionFromRaceId(race: number): EFaction {
return [1, 3, 4, 7, 11].includes(race) ? EFaction.Alliance : EFaction.Horde;
}
public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem {
const padLength = padWithZeroes ? 2 : 0;
return {
icon: obj.emblemStyle.toString().padStart(padLength, '0'),
iconColor: obj.emblemColor.toString().padStart(padLength, '0'),
border: obj.borderStyle.toString().padStart(padLength, '0'),
borderColor: obj.borderColor.toString().padStart(padLength, '0'),
background: obj.background.toString().padStart(padLength, '0'),
};
}
public static makeEmblemObject(obj: any, padWithZeroes: boolean = true): IEmblem {
const padLength = padWithZeroes ? 2 : 0;
return {
icon: obj.emblemStyle.toString().padStart(padLength, "0"),
iconColor: obj.emblemColor.toString().padStart(padLength, "0"),
border: obj.borderStyle.toString().padStart(padLength, "0"),
borderColor: obj.borderColor.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 { encode } from 'html-entities';
import { RowDataPacket } from 'mysql2/promise';
import * as express from "express";
import { encode } from "html-entities";
import { RowDataPacket } from "mysql2/promise";
import { Utils } from '../Utils';
import { Armory } from '../Armory';
import { IRealmConfig } from '../Config';
import { DataTablesSsp } from '../DataTablesSsp';
import { Utils } from "../Utils";
import { Armory } from "../Armory";
import { IRealmConfig } from "../Config";
import { DataTablesSsp } from "../DataTablesSsp";
interface IGuildRank {
id: number;
name: string;
id: number;
name: string;
}
export class GuildController {
private armory: Armory;
private armory: Armory;
public constructor(armory: Armory) {
this.armory = armory;
}
public constructor(armory: Armory) {
this.armory = armory;
}
public async guild(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.params.realm;
const guildName = req.params.name;
public async guild(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.params.realm;
const guildName = req.params.name;
const realm = this.armory.getRealm(realmName);
if (realm === undefined) {
// Could not find realm
return next(404);
}
const realm = this.armory.getRealm(realmName);
if (realm === undefined) {
// Could not find realm
return next(404);
}
const guildData = await this.getGuildData(realm, guildName);
if (guildData === null) {
// Could not find guild
return next(404);
}
const guildData = await this.getGuildData(realm, guildName);
if (guildData === null) {
// Could not find guild
return next(404);
}
res.render('guild.hbs', {
title: `Armory - ${guildName}`,
realm: realm.name,
...guildData,
});
}
res.render("guild.hbs", {
title: `Armory - ${guildName}`,
realm: realm.name,
...guildData,
});
}
public async members(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.params.realm;
const guildId = parseInt(req.params.guild);
public async members(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const realmName = req.params.realm;
const guildId = parseInt(req.params.guild);
const realm = this.armory.getRealm(realmName);
if (realm === undefined) {
// Could not find realm
return next(404);
}
const realm = this.armory.getRealm(realmName);
if (realm === undefined) {
// Could not find realm
return next(404);
}
if (isNaN(guildId) || !(await this.guildExists(realm, guildId))) {
// Could not find guild
return next(404);
}
if (isNaN(guildId) || !(await this.guildExists(realm, guildId))) {
// Could not find guild
return next(404);
}
const db = this.armory.getCharactersDb(realm.name);
const charSet = await this.armory.getDatabaseCharset(realm.name);
const db = this.armory.getCharactersDb(realm.name);
const charSet = await this.armory.getDatabaseCharset(realm.name);
let ssp = new DataTablesSsp(req.query, db, 'guild_member', 'guid', [
{ name: 'name', table: 'characters', collation: `${charSet}_general_ci` },
{ name: 'rank' },
{ name: 'level', table: 'characters' },
{ 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: 'online', table: 'characters', formatter: (online) => online === 1 },
]);
ssp.joins = [{ table1: 'guild_member', column1: 'guid', table2: 'characters', column2: 'guid', kind: 'LEFT' }];
ssp.extraDataColumns = ['`characters`.`gender`'];
let ssp = new DataTablesSsp(req.query, db, "guild_member", "guid", [
{ name: "name", table: "characters", collation: `${charSet}_general_ci` },
{ name: "rank" },
{ name: "level", table: "characters" },
{ 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: "online", table: "characters", formatter: (online) => online === 1 },
]);
ssp.joins = [{ table1: "guild_member", column1: "guid", table2: "characters", column2: "guid", kind: "LEFT" }];
ssp.extraDataColumns = ["`characters`.`gender`"];
if (this.armory.config.hideGameMasters) {
ssp.joins.push({
table1: 'characters',
column1: 'account',
table2: 'account_access',
column2: 'id',
database2: realm.authDatabase,
kind: 'LEFT',
});
ssp = ssp.where(
`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`,
);
}
if (this.armory.config.hideGameMasters) {
ssp.joins.push({
table1: "characters",
column1: "account",
table2: "account_access",
column2: "id",
database2: realm.authDatabase,
kind: "LEFT",
});
ssp = ssp.where(
`\`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);
(result as any).ranks = {};
for (const rank of ranks) {
(result as any).ranks[rank.id] = encode(rank.name);
}
const ranks = await this.getGuildRanks(realm, guildId);
(result as any).ranks = {};
for (const rank of ranks) {
(result as any).ranks[rank.id] = encode(rank.name);
}
res.json(result);
}
res.json(result);
}
private async getGuildData(realm: IRealmConfig, name: string): Promise<any> {
const db = this.armory.getCharactersDb(realm.name);
let [rows, fields] = await db.query({
sql: `
private async getGuildData(realm: IRealmConfig, name: string): Promise<any> {
const db = this.armory.getCharactersDb(realm.name);
let [rows, fields] = await db.query({
sql: `
SELECT guildid, name, leaderguid, EmblemStyle AS emblemStyle, EmblemColor AS emblemColor, BorderStyle AS borderStyle, BorderColor AS borderColor, BackgroundColor AS background
FROM guild WHERE name = ?
`,
values: [name],
timeout: this.armory.config.dbQueryTimeout,
});
if ((rows as RowDataPacket[]).length === 0) {
return null;
}
const guild = rows[0];
values: [name],
timeout: this.armory.config.dbQueryTimeout,
});
if ((rows as RowDataPacket[]).length === 0) {
return null;
}
const guild = rows[0];
[rows, fields] = await db.query({
sql: `
[rows, fields] = await db.query({
sql: `
SELECT name, race FROM characters
WHERE guid = ?
`,
values: [guild.leaderguid],
timeout: this.armory.config.dbQueryTimeout,
});
const leader = rows[0];
values: [guild.leaderguid],
timeout: this.armory.config.dbQueryTimeout,
});
const leader = rows[0];
[rows, fields] = await db.query({
sql: `
[rows, fields] = await db.query({
sql: `
SELECT COUNT(guid) AS \`count\` FROM guild_member
WHERE guildid = ?
`,
values: [guild.guildid],
timeout: this.armory.config.dbQueryTimeout,
});
const membersCount = rows[0].count;
values: [guild.guildid],
timeout: this.armory.config.dbQueryTimeout,
});
const membersCount = rows[0].count;
return {
id: guild.guildid,
name: guild.name,
leader: leader.name,
faction: Utils.getFactionFromRaceId(leader.race),
emblem: Utils.makeEmblemObject(guild),
membersCount,
};
}
return {
id: guild.guildid,
name: guild.name,
leader: leader.name,
faction: Utils.getFactionFromRaceId(leader.race),
emblem: Utils.makeEmblemObject(guild),
membersCount,
};
}
private async getGuildId(realm: IRealmConfig, name: string): Promise<number> {
const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({
sql: `
private async getGuildId(realm: IRealmConfig, name: string): Promise<number> {
const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({
sql: `
SELECT guildid
FROM guild WHERE name = ?
`,
values: [name],
timeout: this.armory.config.dbQueryTimeout,
});
if ((rows as RowDataPacket[]).length === 0) {
return null;
}
values: [name],
timeout: this.armory.config.dbQueryTimeout,
});
if ((rows as RowDataPacket[]).length === 0) {
return null;
}
return rows[0].guildid;
}
return rows[0].guildid;
}
private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> {
const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({
sql: `
private async guildExists(realm: IRealmConfig, id: number): Promise<boolean> {
const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({
sql: `
SELECT guildid
FROM guild WHERE guildid
`,
values: [id],
timeout: this.armory.config.dbQueryTimeout,
});
values: [id],
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[]> {
const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({
sql: `
private async getGuildRanks(realm: IRealmConfig, id: number): Promise<IGuildRank[]> {
const db = this.armory.getCharactersDb(realm.name);
const [rows, fields] = await db.query({
sql: `
SELECT rid AS id, rname AS name
FROM guild_rank WHERE guildid = ?
`,
values: [id],
timeout: this.armory.config.dbQueryTimeout,
});
values: [id],
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 { Armory } from '../Armory';
import { DataTablesSsp } from '../DataTablesSsp';
import { Utils } from "../Utils";
import { Armory } from "../Armory";
import { DataTablesSsp } from "../DataTablesSsp";
export class IndexController {
private armory: Armory;
private armory: Armory;
public constructor(armory: Armory) {
this.armory = armory;
}
public constructor(armory: Armory) {
this.armory = armory;
}
public async index(req: express.Request, res: express.Response): Promise<void> {
res.render('index.hbs', {
title: 'Armory',
realms: this.armory.config.realms.map((r) => r.name),
});
}
public async index(req: express.Request, res: express.Response): Promise<void> {
res.render("index.hbs", {
title: "Armory",
realms: this.armory.config.realms.map((r) => r.name),
});
}
public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
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);
if (realm === undefined) {
return next(400);
}
public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
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);
if (realm === undefined) {
return next(400);
}
const db = this.armory.getCharactersDb(realm.name);
const charSet = await this.armory.getDatabaseCharset(realm.name);
const db = this.armory.getCharactersDb(realm.name);
const charSet = await this.armory.getDatabaseCharset(realm.name);
let ssp = new DataTablesSsp(req.query, db, 'characters', 'guid', [
{ name: 'name', collation: `${charSet}_general_ci` },
{ table: 'guild', name: 'name' },
{ name: 'level' },
{ name: 'class', formatter: (cls) => Utils.classNames[cls] },
{ name: 'race', formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? 'male' : 'female'}` },
{ name: 'online', formatter: (online) => online === 1 },
]);
ssp.joins = [
{ table1: 'characters', column1: 'guid', table2: 'guild_member', column2: 'guid', kind: 'LEFT' },
{ table1: 'guild_member', column1: 'guildid', table2: 'guild', column2: 'guildid', kind: 'LEFT' },
];
ssp.extraDataColumns = ['`characters`.`gender`'];
let ssp = new DataTablesSsp(req.query, db, "characters", "guid", [
{ name: "name", collation: `${charSet}_general_ci` },
{ table: "guild", name: "name" },
{ name: "level" },
{ name: "class", formatter: (cls) => Utils.classNames[cls] },
{ name: "race", formatter: (race, row) => `${Utils.raceNames[race]}_${row[6] === 0 ? "male" : "female"}` },
{ name: "online", formatter: (online) => online === 1 },
]);
ssp.joins = [
{ table1: "characters", column1: "guid", table2: "guild_member", column2: "guid", kind: "LEFT" },
{ table1: "guild_member", column1: "guildid", table2: "guild", column2: "guildid", kind: "LEFT" },
];
ssp.extraDataColumns = ["`characters`.`gender`"];
if (this.armory.config.hideGameMasters) {
ssp.joins.push({
table1: 'characters',
column1: 'account',
table2: 'account_access',
column2: 'id',
database2: realm.authDatabase,
kind: 'LEFT',
});
ssp = ssp.where(
`\`account_access\`.\`id\` IS NULL OR \`account_access\`.\`RealmID\` NOT IN (-1, ${realm.realmId}) OR \`account_access\`.\`gmlevel\` = 0`,
);
}
if (this.armory.config.hideGameMasters) {
ssp.joins.push({
table1: "characters",
column1: "account",
table2: "account_access",
column2: "id",
database2: realm.authDatabase,
kind: "LEFT",
});
ssp = ssp.where(
`\`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);
(result as any).realm = realm.name;
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);
}
}

View file

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

6
src/index.d.ts vendored
View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
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",
"moduleResolution": "node"
},
"exclude": [
"node_modules"
],
"exclude": ["node_modules"],
"include": ["src/**/*.ts"]
}