"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GameServer = exports.saveDb = exports.initDb = void 0; const got_1 = __importDefault(require("got")); const node8_gamedig_1 = require("node8-gamedig"); const lowdb_1 = require("@commonify/lowdb"); const ipregex_1 = __importDefault(require("./lib/ipregex")); const getip_1 = __importDefault(require("./lib/getip")); const STEAM_WEB_API_KEY = process.env.STEAM_WEB_API_KEY || ''; const DATA_PATH = process.env.DATA_PATH || './data/'; const DBG = Boolean(process.env.DBG || false); const adapter = new lowdb_1.JSONFile(DATA_PATH + 'servers.json'); const db = new lowdb_1.Low(adapter); async function initDb() { await db.read(); db.data = db.data || { population: {} }; } exports.initDb = initDb; async function saveDb() { try { return await db.write(); } catch (e) { console.error('gs.saveDb', e.message || e); } } exports.saveDb = saveDb; class GameServer { constructor(config) { this.online = false; console.log('game-server init', config.host, config.port, config.type, config.appId); this.ip = '0.0.0.0'; this.config = config; this.history = new ServerHistory(config.host + ':' + config.port, config.graphHistoryHours, config.timezoneOffset); this._niceName = config.host + ':' + config.port; } async update() { await this.getIp(); if (DBG) console.log('gs.up', this.config.host, this.config.port); let info = await this.gamedig(); if (DBG) console.log('gs.gamedig', Object.assign({}, info, { players: info === null || info === void 0 ? void 0 : info.players.length })); if (!info && STEAM_WEB_API_KEY && this.config.appId) { info = await this.steam(); if (DBG) console.log('gs.steam', info); } if (info) { if (info.players.length > 0 && DBG) { console.log('gs.players.0', info.players[0]); } this.online = true; this.info = info; this.history.add(info.playersNum); } else { this.online = false; console.error('game-server not available', this.config.host, this.config.port); this.history.cleanStats(); } } async gamedig() { try { const res = await (0, node8_gamedig_1.query)({ host: this.config.host, port: this.config.port, type: this.config.type, }); const raw = res.raw; const game = raw.game || raw.folder || this.config.type; const players = res.players.map((p) => { return new GsPlayer(p); }); return { connect: res.connect, name: res.name, game: game, map: res.map || '', playersNum: res.numplayers || res.players.length, playersMax: res.maxplayers, players }; } catch (e) { console.error(['gs.gamedig', this.config.host, this.config.port].join(':'), e.message || e); } return null; } async steam() { const reqUrl = 'https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\\appid\\' + this.config.appId + '\\addr\\' + this.ip + '&key=' + STEAM_WEB_API_KEY; try { const res = await (0, got_1.default)(reqUrl, { responseType: 'json', headers: { 'user-agent': 'game-server-watcher/1.0' } }).json(); if (Array.isArray(res.response.servers)) { const matching = res.response.servers.find((s) => s.gameport === this.config.port); if (matching) { return { connect: matching.addr, name: matching.name, game: matching.gamedir, map: matching.map, playersNum: matching.players, playersMax: matching.max_players, players: [] }; } } } catch (e) { console.error(['gs.steam', this.config.host, this.config.port].join(':'), e.message || e); } return null; } get niceName() { var _a; let nn = (_a = this.info) === null || _a === void 0 ? void 0 : _a.name; if (nn) { for (let i = 0; i < nn.length; i++) { if (nn[i] == '^') { nn = nn.slice(0, i) + ' ' + nn.slice(i + 2); } else if (nn[i] == '█') { nn = nn.slice(0, i) + ' ' + nn.slice(i + 1); } else if (nn[i] == '�') { nn = nn.slice(0, i) + ' ' + nn.slice(i + 2); } ; } ; if (nn) this._niceName = nn; } return this._niceName; } async getIp() { if (this.ip === '0.0.0.0') { if (ipregex_1.default.test(this.config.host)) { this.ip = this.config.host; } else { this.ip = await (0, getip_1.default)(this.config.host) || '0.0.0.0'; } } return this.ip; } } exports.GameServer = GameServer; class GsPlayer { constructor(p) { this._player = p; } get(prop) { const p = this._player; let re; if (p[prop] !== undefined) { re = String(p[prop]); } else if (p.raw && p.raw[prop] !== undefined) { re = String(p.raw[prop]); } if (re === 'NaN') { re = undefined; } return re; } } class ServerHistory { constructor(id, graphHistoryHours = 12, timezoneOffset = 0) { this._stats = []; this.id = id; this.graphHistoryHours = graphHistoryHours; this.timezoneOffset = timezoneOffset; } yyyymmddhh(d) { return parseInt(d.toISOString().slice(0, 13).replace(/\D/g, ''), 10); } formatHour(h) { const ampm = (h >= 12) ? 'pm' : 'am'; const hours = (h > 12) ? h - 12 : h; return hours + ampm; } add(playersNum) { var _a; if (!((_a = db.data) === null || _a === void 0 ? void 0 : _a.population)) return; if (!db.data.population[this.id]) db.data.population[this.id] = []; const dh = this.yyyymmddhh(new Date()); db.data.population[this.id].push({ dateHour: dh, playersNum }); this.cleanStats(); } cleanStats() { var _a; if (!((_a = db.data) === null || _a === void 0 ? void 0 : _a.population)) return; if (!db.data.population[this.id]) db.data.population[this.id] = []; const d = new Date(); d.setHours(d.getHours() - this.graphHistoryHours - 1); const minDh = this.yyyymmddhh(d); db.data.population[this.id] = db.data.population[this.id].filter(i => i.dateHour > minDh); this._stats = []; } stats() { var _a; if (!((_a = db.data) === null || _a === void 0 ? void 0 : _a.population)) return []; if (this._stats.length === 0) { const grouped = {}; if (this.id in db.data.population) { for (const d of db.data.population[this.id]) { if (!grouped[d.dateHour]) grouped[d.dateHour] = []; grouped[d.dateHour].push(d); } } const stats = []; for (const dh in grouped) { const avg = grouped[dh].reduce((total, next) => total + next.playersNum, 0) / grouped[dh].length; const max = grouped[dh].reduce((max, next) => next.playersNum > max ? next.playersNum : max, 0); stats.push({ dateHour: parseInt(dh, 10), avg, max }); } this._stats = stats.sort((a, b) => a.dateHour - b.dateHour); } return this._stats; } statsChart() { const stats = this.stats(); const e = encodeURIComponent; const avg = []; const max = []; const xlabels = []; const dh = this.yyyymmddhh(new Date()); if (stats.length === 0 || stats[stats.length - 1].dateHour < dh) { stats.push({ dateHour: dh, avg: -1, max: -1 }); } const firstDateHour = new Date(); firstDateHour.setHours(firstDateHour.getHours() - this.graphHistoryHours); const fdh = this.yyyymmddhh(firstDateHour); if (stats[0].dateHour > fdh) { stats.unshift({ dateHour: fdh, avg: -1, max: -1 }); } let lastH; for (const s of stats) { const sh = s.dateHour % 100; const d = new Date(); d.setHours(sh); d.setTime(d.getTime() + (this.timezoneOffset * 60 * 60 * 1000)); const h = d.getHours(); if (lastH !== undefined) { let nextH = lastH; do { nextH++; if (nextH > 23) nextH = 0; if (nextH !== h) { avg.push('_'); max.push('_'); xlabels.push(this.formatHour(nextH)); } } while (nextH !== h); } avg.push(s.avg > -1 ? String(Math.round(s.avg)) : '_'); max.push(s.max > -1 ? String(s.max) : '_'); xlabels.push(this.formatHour(h)); lastH = h; } const values = avg.concat(max); return [ 'https://image-charts.com/chart?cht=lc', 'chs=600x300', 'chf=' + e('bg,s,202225'), 'chma=' + e('0,0,10,0'), 'chls=' + e('2|2'), // 'chm='+e('B,011040,0,0,0'), // line fill 'chg=' + e('1,1,2,2,303030'), // 'chm='+e('d,ffffff,0,-1,3|d,ffffff,1,-1,3'), // value markers 'chdl=' + e('AVG|MAX'), 'chdlp=t', 'chdls=' + e('ffffff,8'), 'chxt=' + e('x,y'), 'chxs=' + e('0,ffffff,12,s|1,ffffff,15'), 'chds=a', 'chd=' + e('t:' + avg.join(',') + '|' + max.join(',')), 'chl=' + e(values.join('|')), 'chlps=' + e('color,ffffff|anchor,end|font.size,12|align,top'), 'chxl=' + e('0:|' + xlabels.join('|')), // 'chxr='+e('1,0,'+playersMax), // axis range 'chco=' + e('1234ef,fd7501') // data colors ].join('&'); } }