text: /**
   * Monitor
   * Pokemon Showdown - http://pokemonshowdown.com/
   *
   * Various utility functions to make sure PS is running healthily.
   *
   * @license MIT license
   */
  'use strict';
  
  const FS = require('./lib/fs');
  
  const MONITOR_CLEAN_TIMEOUT = 2 * 60 * 60 * 1000;
  
  /**
   * This counts the number of times an action has been committed, and tracks the
   * delta of time since the last time it was committed. Actions include
   * connecting to the server, starting a battle, validating a team, and
   * sending/receiving data over a connection's socket.
   * @augments {Map<string, [number, number]>}
   */
  // @ts-ignore TypeScript bug
  class TimedCounter extends Map {
  	/**
  	 * Increments the number of times an action has been committed by one, and
  	 * updates the delta of time since it was last committed.
  	 *
  	 * @param {string} key
  	 * @param {number} timeLimit
  	 * @return {[number, number]} - [action count, time delta]
  	 */
  	increment(key, timeLimit) {
  		let val = this.get(key);
  		let now = Date.now();
  		if (!val || now > val[1] + timeLimit) {
  			this.set(key, [1, Date.now()]);
  			return [1, 0];
  		} else {
  			val[0]++;
  			return [val[0], now - val[1]];
  		}
  	}
  }
  
  // Config.loglevel is:
  // 0 = everything
  // 1 = debug (same as 0 for now)
  // 2 = notice (default)
  // 3 = warning
  // (4 is currently unused)
  // 5 = supposedly completely silent, but for now a lot of PS output doesn't respect loglevel
  if (('Config' in global) &&
  		(typeof Config.loglevel !== 'number' || Config.loglevel < 0 || Config.loglevel > 5)) {
  	Config.loglevel = 2;
  }
  
  // @ts-ignore
  const Monitor = module.exports = {
  	/*********************************************************
  	 * Logging
  	 *********************************************************/
  
  	/** @param {Error} error */
  	crashlog(error, source = 'The main process') {
  		require('./lib/crashlogger')(error, source);
  	},
  
  	/**
  	 * @param {string} text
  	 */
  	log(text) {
  		this.notice(text);
  		if (Rooms('staff')) {
  			Rooms('staff').add(`|c|~|${text}`).update();
  		}
  	},
  
  	/**
  	 * @param {string} text
  	 */
  	adminlog(text) {
  		this.notice(text);
  		if (Rooms('upperstaff')) {
  			Rooms('upperstaff').add(`|c|~|${text}`).update();
  		}
  	},
  
  	/**
  	 * @param {string} text
  	 */
  	logHTML(text) {
  		this.notice(text);
  		if (Rooms('staff')) {
  			Rooms('staff').add(`|html|${text}`).update();
  		}
  	},
  
  	/**
  	 * @param {string} text
  	 */
  	debug(text) {
  		if (Config.loglevel <= 1) console.log(text);
  	},
  
  	/**
  	 * @param {string} text
  	 */
  	warn(text) {
  		if (Config.loglevel <= 3) console.log(text);
  	},
  
  	/**
  	 * @param {string} text
  	 */
  	notice(text) {
  		if (Config.loglevel <= 2) console.log(text);
  	},
  
  	/*********************************************************
  	 * Resource Monitor
  	 *********************************************************/
  
  	clean() {
  		this.clearNetworkUse();
  		this.battlePreps.clear();
  		this.battles.clear();
  		this.connections.clear();
  		Dnsbl.cache.clear();
  	},
  
  	connections: new TimedCounter(),
  	battles: new TimedCounter(),
  	battlePreps: new TimedCounter(),
  	groupChats: new TimedCounter(),
  	tickets: new TimedCounter(),
  
  	/** @type {string | null} */
  	activeIp: null,
  	networkUse: {},
  	networkCount: {},
  	hotpatchLock: {},
  
  	/**
  	 * Counts a connection. Returns true if the connection should be terminated for abuse.
  	 *
  	 * @param {string} ip
  	 * @param {string} [name = '']
  	 * @return {boolean}
  	 */
  	countConnection(ip, name = '') {
  		let [count, duration] = this.connections.increment(ip, 30 * 60 * 1000);
  		if (count === 500) {
  			this.adminlog(`[ResourceMonitor] IP ${ip} banned for cflooding (${count} times in ${Chat.toDurationString(duration)}${name ? ': ' + name : ''})`);
  			return true;
  		}
  
  		if (count > 500) {
  			if (count % 500 === 0) {
  				let c = count / 500;
  				if (c === 2 || c === 4 || c === 10 || c === 20 || c % 40 === 0) {
  					this.adminlog(`[ResourceMonitor] IP ${ip} still cflooding (${count} times in ${Chat.toDurationString(duration)}${name ? ': ' + name : ''})`);
  				}
  			}
  			return true;
  		}
  
  		return false;
  	},
  
  	/**
  	 * Counts battles created. Returns true if the connection should be
  	 * terminated for abuse.
  	 *
  	 * @param {string} ip
  	 * @param {string} [name = '']
  	 * @return {boolean}
  	 */
  	countBattle(ip, name = '') {
  		let [count, duration] = this.battles.increment(ip, 30 * 60 * 1000);
  		if (duration < 5 * 60 * 1000 && count % 30 === 0) {
  			this.adminlog(`[ResourceMonitor] IP ${ip} has battled ${count} times in the last ${Chat.toDurationString(duration)}${name ? ': ' + name : ''})`);
  			return true;
  		}
  
  		if (count % 150 === 0) {
  			this.adminlog(`[ResourceMonitor] IP ${ip} has battled ${count} times in the last ${Chat.toDurationString(duration)}${name ? ': ' + name : ''}`);
  			return true;
  		}
  
  		return false;
  	},
  
  	/**
  	 * Counts team validations. Returns true if too many.
  	 *
  	 * @param {string} ip
  	 * @param {Connection} connection
  	 * @return {boolean}
  	 */
  	countPrepBattle(ip, connection) {
  		let count = this.battlePreps.increment(ip, 3 * 60 * 1000)[0];
  		if (count <= 12) return false;
  		if (count < 120 && Punishments.sharedIps.has(ip)) return false;
  		connection.popup('Due to high load, you are limited to 12 battles and team validations every 3 minutes.');
  		return true;
  	},
  
  	/**
  	 * Counts concurrent battles. Returns true if too many.
  	 *
  	 * @param {number} count
  	 * @param {Connection} connection
  	 * @return {boolean}
  	 */
  	countConcurrentBattle(count, connection) {
  		if (count <= 5) return false;
  		connection.popup(`Due to high load, you are limited to 5 games at the same time.`);
  		return true;
  	},
  	/**
  	 * Counts group chat creation. Returns true if too much.
  	 *
  	 * @param {string} ip
  	 * @return {boolean}
  	 */
  	countGroupChat(ip) {
  		let count = this.groupChats.increment(ip, 60 * 60 * 1000)[0];
  		return count > 4;
  	},
  
  	/**
  	 * Counts ticket creation. Returns true if too much.
  	 *
  	 * @param {string} ip
  	 * @return {boolean}
  	 */
  	countTickets(ip) {
  		let count = this.tickets.increment(ip, 60 * 60 * 1000)[0];
  		if (Punishments.sharedIps.has(ip) && count >= 50) return true;
  		if (count >= 5) return true;
  		return false;
  	},
  
  	/**
  	 * Counts the data length received by the last connection to send a
  	 * message, as well as the data length in the server's response.
  	 *
  	 * @param {number} size
  	 */
  	countNetworkUse(size) {
  		if (!Config.emergency || typeof this.activeIp !== 'string') return;
  		// @ts-ignore
  		if (this.activeIp in this.networkUse) {
  			// @ts-ignore
  			this.networkUse[this.activeIp] += size;
  			// @ts-ignore
  			this.networkCount[this.activeIp]++;
  		} else {
  			// @ts-ignore
  			this.networkUse[this.activeIp] = size;
  			// @ts-ignore
  			this.networkCount[this.activeIp] = 1;
  		}
  	},
  
  	writeNetworkUse() {
  		let buf = '';
  		for (let i in this.networkUse) {
  			buf += `${this.networkUse[i]}\t${this.networkCount[i]}\t${i}\n`;
  		}
  		FS('logs/networkuse.tsv').write(buf);
  	},
  
  	clearNetworkUse() {
  		if (Config.emergency) {
  			this.networkUse = {};
  			this.networkCount = {};
  		}
  	},
  
  	/**
  	 * Counts roughly the size of an object to have an idea of the server load.
  	 *
  	 * @param {any} object
  	 * @return {number}
  	 */
  	sizeOfObject(object) {
  		/** @type {Set<(Array | Object)>} */
  		let objectCache = new Set();
  		let stack = [object];
  		let bytes = 0;
  
  		while (stack.length) {
  			let value = stack.pop();
  			switch (typeof value) {
  			case 'boolean':
  				bytes += 4;
  				break;
  			case 'string':
  				bytes += value.length * 2;
  				break;
  			case 'number':
  				bytes += 8;
  				break;
  			case 'object':
  				if (!objectCache.has(value)) objectCache.add(value);
  				if (Array.isArray(value)) {
  					for (let el of value) stack.push(el);
  				} else {
  					for (let i in value) stack.push(value[i]);
  				}
  				break;
  			}
  		}
  
  		return bytes;
  	},
  
  	/** @type {{new(entries: [any, [number, number]]): TimedCounter}} */
  	TimedCounter,
  };
  
  Monitor.cleanInterval = setInterval(() => Monitor.clean(), MONITOR_CLEAN_TIMEOUT);
  