<commit_before>/**
 * Rooms
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * Every chat room and battle is a room, and what they do is done in
 * rooms.js. There's also a global room which every user is in, and
 * handles miscellaneous things like welcoming the user.
 *
 * @license MIT license
 */

const TIMEOUT_EMPTY_DEALLOCATE = 10 * 60 * 1000;
const TIMEOUT_INACTIVE_DEALLOCATE = 40 * 60 * 1000;
const REPORT_USER_STATS_INTERVAL = 1000 * 60 * 10;

var fs = require('fs');

/* global Rooms: true */
var Rooms = module.exports = getRoom;

var rooms = Rooms.rooms = Object.create(null);

var aliases = Object.create(null);

var Room = (function () {
	function Room(roomid, title) {
		this.id = roomid;
		this.title = (title || roomid);

		this.users = Object.create(null);

		this.log = [];

		this.bannedUsers = Object.create(null);
		this.bannedIps = Object.create(null);
	}
	Room.prototype.title = "";
	Room.prototype.type = 'chat';

	Room.prototype.lastUpdate = 0;
	Room.prototype.log = null;
	Room.prototype.users = null;
	Room.prototype.userCount = 0;

	Room.prototype.send = function (message, errorArgument) {
		if (errorArgument) throw new Error("Use Room#sendUser");
		if (this.id !== 'lobby') message = '>' + this.id + '\n' + message;
		Sockets.channelBroadcast(this.id, message);
	};
	Room.prototype.sendAuth = function (message) {
		for (var i in this.users) {
			var user = this.users[i];
			if (user.connected && user.can('receiveauthmessages', null, this)) {
				user.sendTo(this, message);
			}
		}
	};
	Room.prototype.sendUser = function (user, message) {
		user.sendTo(this, message);
	};
	Room.prototype.add = function (message) {
		if (typeof message !== 'string') throw new Error("Deprecated message type");
		this.logEntry(message);
		if (this.logTimes && message.substr(0, 3) === '|c|') {
			message = '|c:|' + (~~(Date.now() / 1000)) + '|' + message.substr(3);
		}
		this.log.push(message);
	};
	Room.prototype.logEntry = function () {};
	Room.prototype.addRaw = function (message) {
		this.add('|raw|' + message);
	};
	Room.prototype.getLogSlice = function (amount) {
		var log = this.log.slice(amount);
		log.unshift('|:|' + (~~(Date.now() / 1000)));
		return log;
	};
	Room.prototype.chat = function (user, message, connection) {
		// Battle actions are actually just text commands that are handled in
		// parseCommand(), which in turn often calls Simulator.prototype.sendFor().
		// Sometimes the call to sendFor is done indirectly, by calling
		// room.decision(), where room.constructor === BattleRoom.

		message = CommandParser.parse(message, this, user, connection);

		if (message) {
			this.add('|c|' + user.getIdentity(this.id) + '|' + message);
		}
		this.update();
	};

	return Room;
})();

var GlobalRoom = (function () {
	function GlobalRoom(roomid) {
		this.id = roomid;

		// init battle rooms
		this.battleCount = 0;
		this.searchers = [];

		// Never do any other file IO synchronously
		// but this is okay to prevent race conditions as we start up PS
		this.lastBattle = 0;
		try {
			this.lastBattle = parseInt(fs.readFileSync('logs/lastbattle.txt')) || 0;
		} catch (e) {} // file doesn't exist [yet]

		this.chatRoomData = [];
		try {
			this.chatRoomData = JSON.parse(fs.readFileSync('config/chatrooms.json'));
			if (!Array.isArray(this.chatRoomData)) this.chatRoomData = [];
		} catch (e) {} // file doesn't exist [yet]

		if (!this.chatRoomData.length) {
			this.chatRoomData = [{
				title: 'Lobby',
				isOfficial: true,
				autojoin: true
			}, {
				title: 'Staff',
				isPrivate: true,
				staffRoom: true,
				staffAutojoin: true
			}];
		}

		this.chatRooms = [];

		this.autojoin = []; // rooms that users autojoin upon connecting
		this.staffAutojoin = []; // rooms that staff autojoin upon connecting
		for (var i = 0; i < this.chatRoomData.length; i++) {
			if (!this.chatRoomData[i] || !this.chatRoomData[i].title) {
				console.log('ERROR: Room number ' + i + ' has no data.');
				continue;
			}
			var id = toId(this.chatRoomData[i].title);
			console.log("NEW CHATROOM: " + id);
			var room = Rooms.createChatRoom(id, this.chatRoomData[i].title, this.chatRoomData[i]);
			if (room.aliases) {
				for (var a = 0; a < room.aliases.length; a++) {
					aliases[room.aliases[a]] = room;
				}
			}
			this.chatRooms.push(room);
			if (room.autojoin) this.autojoin.push(id);
			if (room.staffAutojoin) this.staffAutojoin.push(id);
		}

		// this function is complex in order to avoid several race conditions
		var self = this;
		this.writeNumRooms = (function () {
			var writing = false;
			var lastBattle;	// last lastBattle to be written to file
			var finishWriting = function () {
				writing = false;
				if (lastBattle < self.lastBattle) {
					self.writeNumRooms();
				}
			};
			return function () {
				if (writing) return;

				// batch writing lastbattle.txt for every 10 battles
				if (lastBattle >= self.lastBattle) return;
				lastBattle = self.lastBattle + 10;

				writing = true;
				fs.writeFile('logs/lastbattle.txt.0', '' + lastBattle, function () {
					// rename is atomic on POSIX, but will throw an error on Windows
					fs.rename('logs/lastbattle.txt.0', 'logs/lastbattle.txt', function (err) {
						if (err) {
							// This should only happen on Windows.
							fs.writeFile('logs/lastbattle.txt', '' + lastBattle, finishWriting);
							return;
						}
						finishWriting();
					});
				});
			};
		})();

		this.writeChatRoomData = (function () {
			var writing = false;
			var writePending = false; // whether or not a new write is pending
			var finishWriting = function () {
				writing = false;
				if (writePending) {
					writePending = false;
					self.writeChatRoomData();
				}
			};
			return function () {
				if (writing) {
					writePending = true;
					return;
				}
				writing = true;
				var data = JSON.stringify(self.chatRoomData).replace(/\{"title"\:/g, '\n{"title":').replace(/\]$/, '\n]');
				fs.writeFile('config/chatrooms.json.0', data, function () {
					// rename is atomic on POSIX, but will throw an error on Windows
					fs.rename('config/chatrooms.json.0', 'config/chatrooms.json', function (err) {
						if (err) {
							// This should only happen on Windows.
							fs.writeFile('config/chatrooms.json', data, finishWriting);
							return;
						}
						finishWriting();
					});
				});
			};
		})();

		// init users
		this.users = {};
		this.userCount = 0; // cache of `Object.size(this.users)`
		this.maxUsers = 0;
		this.maxUsersDate = 0;

		this.reportUserStatsInterval = setInterval(
			this.reportUserStats.bind(this),
			REPORT_USER_STATS_INTERVAL
		);
	}
	GlobalRoom.prototype.type = 'global';

	GlobalRoom.prototype.formatListText = '|formats';

	GlobalRoom.prototype.reportUserStats = function () {
		if (this.maxUsersDate) {
			LoginServer.request('updateuserstats', {
				date: this.maxUsersDate,
				users: this.maxUsers
			}, function () {});
			this.maxUsersDate = 0;
		}
		LoginServer.request('updateuserstats', {
			date: Date.now(),
			users: this.userCount
		}, function () {});
	};

	GlobalRoom.prototype.getFormatListText = function () {
		var formatListText = '|formats';
		var curSection = '';
		for (var i in Tools.data.Formats) {
			var format = Tools.data.Formats[i];
			if (!format.challengeShow && !format.searchShow) continue;

			var section = format.section;
			if (section === undefined) section = format.mod;
			if (!section) section = '';
			if (section !== curSection) {
				curSection = section;
				formatListText += '|,' + (format.column || 1) + '|' + section;
			}
			formatListText += '|' + format.name;
			if (!format.challengeShow) formatListText += ',,';
			else if (!format.searchShow) formatListText += ',';
			if (format.team) formatListText += ',#';
		}
		return formatListText;
	};

	GlobalRoom.prototype.getRoomList = function (filter) {
		var roomList = {};
		var total = 0;
		var skipCount = 0;
		if (this.battleCount > 150) {
			skipCount = this.battleCount - 150;
		}
		for (var i in Rooms.rooms) {
			var room = Rooms.rooms[i];
			if (!room || !room.active || room.isPrivate) continue;
			if (filter && filter !== room.format && filter !== true) continue;
			if (skipCount && skipCount--) continue;
			var roomData = {};
			if (room.active && room.battle) {
				if (room.battle.players[0]) roomData.p1 = room.battle.players[0].getIdentity();
				if (room.battle.players[1]) roomData.p2 = room.battle.players[1].getIdentity();
			}
			if (!roomData.p1 || !roomData.p2) continue;
			roomList[room.id] = roomData;

			total++;
			if (total >= 100) break;
		}
		return roomList;
	};
	GlobalRoom.prototype.getRooms = function () {
		var roomsData = {official:[], chat:[], userCount: this.userCount, battleCount: this.battleCount};
		for (var i = 0; i < this.chatRooms.length; i++) {
			var room = this.chatRooms[i];
			if (!room) continue;
			if (room.isPrivate) continue;
			(room.isOfficial ? roomsData.official : roomsData.chat).push({
				title: room.title,
				desc: room.desc,
				userCount: room.userCount
			});
		}
		return roomsData;
	};
	GlobalRoom.prototype.cancelSearch = function (user) {
		var success = false;
		user.cancelChallengeTo();
		for (var i = 0; i < this.searchers.length; i++) {
			var search = this.searchers[i];
			var searchUser = Users.get(search.userid);
			if (!searchUser.connected) {
				this.searchers.splice(i, 1);
				i--;
				continue;
			}
			if (searchUser === user) {
				this.searchers.splice(i, 1);
				i--;
				if (!success) {
					searchUser.send('|updatesearch|' + JSON.stringify({searching: false}));
					success = true;
				}
				continue;
			}
		}
		return success;
	};
	GlobalRoom.prototype.searchBattle = function (user, formatid) {
		if (!user.connected) return;

		formatid = toId(formatid);

		user.prepBattle(formatid, 'search', null, this.finishSearchBattle.bind(this, user, formatid));
	};
	GlobalRoom.prototype.finishSearchBattle = function (user, formatid, result) {
		if (!result) return;

		// tell the user they've started searching
		var newSearchData = {
			format: formatid
		};
		user.send('|updatesearch|' + JSON.stringify({searching: newSearchData}));

		// get the user's rating before actually starting to search
		var newSearch = {
			userid: user.userid,
			formatid: formatid,
			team: user.team,
			rating: 1000,
			time: new Date().getTime()
		};
		var self = this;
		user.doWithMMR(formatid, function (mmr, error) {
			if (error) {
				user.popup("Connection to ladder server failed with error: " + error + "; please try again later");
				return;
			}
			newSearch.rating = mmr;
			self.addSearch(newSearch, user);
		});
	};
	GlobalRoom.prototype.matchmakingOK = function (search1, search2, user1, user2) {
		// users must be different
		if (user1 === user2) return false;

		// users must have different IPs
		if (user1.latestIp === user2.latestIp) return false;

		// users must not have been matched immediately previously
		if (user1.lastMatch === user2.userid || user2.lastMatch === user1.userid) return false;

		// search must be within range
		var searchRange = 100, formatid = search1.formatid, elapsed = Math.abs(search1.time - search2.time);
		if (formatid === 'ou' || formatid === 'oucurrent' || formatid === 'randombattle') searchRange = 50;
		searchRange += elapsed / 300; // +1 every .3 seconds
		if (searchRange > 300) searchRange = 300;
		if (Math.abs(search1.rating - search2.rating) > searchRange) return false;

		user1.lastMatch = user2.userid;
		user2.lastMatch = user1.userid;
		return true;
	};
	GlobalRoom.prototype.addSearch = function (newSearch, user) {
		if (!user.connected) return;
		for (var i = 0; i < this.searchers.length; i++) {
			var search = this.searchers[i];
			var searchUser = Users.get(search.userid);
			if (!searchUser || !searchUser.connected) {
				this.searchers.splice(i, 1);
				i--;
				continue;
			}
			if (newSearch.formatid === search.formatid && searchUser === user) return; // only one search per format
			if (newSearch.formatid === search.formatid && this.matchmakingOK(search, newSearch, searchUser, user)) {
				this.cancelSearch(user, true);
				this.cancelSearch(searchUser, true);
				user.send('|updatesearch|' + JSON.stringify({searching: false}));
				this.startBattle(searchUser, user, search.formatid, search.team, newSearch.team, {rated: true});
				return;
			}
		}
		this.searchers.push(newSearch);
	};
	GlobalRoom.prototype.send = function (message, user) {
		if (user) {
			user.sendTo(this, message);
		} else {
			Sockets.channelBroadcast(this.id, message);
		}
	};
	GlobalRoom.prototype.sendAuth = function (message) {
		for (var i in this.users) {
			var user = this.users[i];
			if (user.connected && user.can('receiveauthmessages', null, this)) {
				user.sendTo(this, message);
			}
		}
	};
	GlobalRoom.prototype.add = function (message) {
		if (rooms.lobby) rooms.lobby.add(message);
	};
	GlobalRoom.prototype.addRaw = function (message) {
		if (rooms.lobby) rooms.lobby.addRaw(message);
	};
	GlobalRoom.prototype.addChatRoom = function (title) {
		var id = toId(title);
		if (rooms[id]) return false;

		var chatRoomData = {
			title: title
		};
		var room = Rooms.createChatRoom(id, title, chatRoomData);
		this.chatRoomData.push(chatRoomData);
		this.chatRooms.push(room);
		this.writeChatRoomData();
		return true;
	};
	GlobalRoom.prototype.deregisterChatRoom = function (id) {
		id = toId(id);
		var room = rooms[id];
		if (!room) return false; // room doesn't exist
		if (!room.chatRoomData) return false; // room isn't registered
		// deregister from global chatRoomData
		// looping from the end is a pretty trivial optimization, but the
		// assumption is that more recently added rooms are more likely to
		// be deleted
		for (var i = this.chatRoomData.length - 1; i >= 0; i--) {
			if (id === toId(this.chatRoomData[i].title)) {
				this.chatRoomData.splice(i, 1);
				this.writeChatRoomData();
				break;
			}
		}
		delete room.chatRoomData;
		return true;
	};
	GlobalRoom.prototype.delistChatRoom = function (id) {
		id = toId(id);
		if (!rooms[id]) return false; // room doesn't exist
		for (var i = this.chatRooms.length - 1; i >= 0; i--) {
			if (id === this.chatRooms[i].id) {
				this.chatRooms.splice(i, 1);
				break;
			}
		}
	};
	GlobalRoom.prototype.removeChatRoom = function (id) {
		id = toId(id);
		var room = rooms[id];
		if (!room) return false; // room doesn't exist
		room.destroy();
		return true;
	};
	GlobalRoom.prototype.autojoinRooms = function (user, connection) {
		// we only autojoin regular rooms if the client requests it with /autojoin
		// note that this restriction doesn't apply to staffAutojoin
		for (var i = 0; i < this.autojoin.length; i++) {
			user.joinRoom(this.autojoin[i], connection);
		}
	};
	GlobalRoom.prototype.checkAutojoin = function (user, connection) {
		for (var i = 0; i < this.staffAutojoin.length; i++) {
			var room = Rooms.get(this.staffAutojoin[i]);
			if (!room) {
				this.staffAutojoin.splice(i, 1);
				i--;
				continue;
			}
			if (room.staffAutojoin === true && user.isStaff ||
					typeof room.staffAutojoin === 'string' && room.staffAutojoin.indexOf(user.group) >= 0) {
				// if staffAutojoin is true: autojoin if isStaff
				// if staffAutojoin is String: autojoin if user.group in staffAutojoin
				user.joinRoom(room.id, connection);
			}
		}

		for (var i = 0; i < this.autojoin.length; i++) {
			var room = Rooms.get(this.autojoin[i]);
			if (!room) {
				this.autojoin.splice(i, 1);
				i--;
				continue;
			}
			if (room.autojoin === true) {
				user.joinRoom(room.id, connection);
			}
		}
	};
	GlobalRoom.prototype.onJoinConnection = function (user, connection) {
		var initdata = '|updateuser|' + user.name + '|' + (user.named ? '1' : '0') + '|' + user.avatar + '\n';
		connection.send(initdata + this.formatListText);
		if (this.chatRooms.length > 2) connection.send('|queryresponse|rooms|null'); // should display room list
	};
	GlobalRoom.prototype.onJoin = function (user, connection, merging) {
		if (!user) return false; // ???
		if (this.users[user.userid]) return user;

		this.users[user.userid] = user;
		if (++this.userCount > this.maxUsers) {
			this.maxUsers = this.userCount;
			this.maxUsersDate = Date.now();
		}

		if (!merging) {
			var initdata = '|updateuser|' + user.name + '|' + (user.named ? '1' : '0') + '|' + user.avatar + '\n';
			connection.send(initdata + this.formatListText);
			if (this.chatRooms.length > 2) connection.send('|queryresponse|rooms|null'); // should display room list
		}

		return user;
	};
	GlobalRoom.prototype.onRename = function (user, oldid, joining) {
		delete this.users[oldid];
		this.users[user.userid] = user;
		return user;
	};
	GlobalRoom.prototype.onUpdateIdentity = function () {};
	GlobalRoom.prototype.onLeave = function (user) {
		if (!user) return; // ...
		delete this.users[user.userid];
		--this.userCount;
		this.cancelSearch(user, true);
	};
	GlobalRoom.prototype.startBattle = function (p1, p2, format, p1team, p2team, options) {
		var newRoom;
		p1 = Users.get(p1);
		p2 = Users.get(p2);

		if (!p1 || !p2) {
			// most likely, a user was banned during the battle start procedure
			this.cancelSearch(p1, true);
			this.cancelSearch(p2, true);
			return;
		}
		if (p1 === p2) {
			this.cancelSearch(p1, true);
			this.cancelSearch(p2, true);
			p1.popup("You can't battle your own account. Please use something like Private Browsing to battle yourself.");
			return;
		}

		if (this.lockdown === true) {
			this.cancelSearch(p1, true);
			this.cancelSearch(p2, true);
			p1.popup("The server is shutting down. Battles cannot be started at this time.");
			p2.popup("The server is shutting down. Battles cannot be started at this time.");
			return;
		}

		//console.log('BATTLE START BETWEEN: ' + p1.userid + ' ' + p2.userid);
		var i = this.lastBattle + 1;
		var formaturlid = format.toLowerCase().replace(/[^a-z0-9]+/g, '');
		while (rooms['battle-' + formaturlid + i]) {
			i++;
		}
		this.lastBattle = i;
		rooms.global.writeNumRooms();
		newRoom = this.addRoom('battle-' + formaturlid + '-' + i, format, p1, p2, options);
		p1.joinRoom(newRoom);
		p2.joinRoom(newRoom);
		newRoom.joinBattle(p1, p1team);
		newRoom.joinBattle(p2, p2team);
		this.cancelSearch(p1, true);
		this.cancelSearch(p2, true);
		if (Config.reportbattles && rooms.lobby) {
			rooms.lobby.add('|b|' + newRoom.id + '|' + p1.getIdentity() + '|' + p2.getIdentity());
		}
		if (Config.logladderip && options.rated) {
			if (!this.ladderIpLog) {
				this.ladderIpLog = fs.createWriteStream('logs/ladderip/ladderip.txt', {flags: 'a'});
			}
			this.ladderIpLog.write(p1.userid + ': ' + p1.latestIp + '\n');
			this.ladderIpLog.write(p2.userid + ': ' + p2.latestIp + '\n');
		}
		return newRoom;
	};
	GlobalRoom.prototype.addRoom = function (room, format, p1, p2, options) {
		room = Rooms.createBattle(room, format, p1, p2, options);
		return room;
	};
	GlobalRoom.prototype.chat = function (user, message, connection) {
		if (rooms.lobby) return rooms.lobby.chat(user, message, connection);
		message = CommandParser.parse(message, this, user, connection);
		if (message) {
			connection.popup("You can't send messages directly to the server.");
		}
	};
	return GlobalRoom;
})();<commit_msg>Display battles in Chat instead of Lobby<commit_after>/**
 * Rooms
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * Every chat room and battle is a room, and what they do is done in
 * rooms.js. There's also a global room which every user is in, and
 * handles miscellaneous things like welcoming the user.
 *
 * @license MIT license
 */

const TIMEOUT_EMPTY_DEALLOCATE = 10 * 60 * 1000;
const TIMEOUT_INACTIVE_DEALLOCATE = 40 * 60 * 1000;
const REPORT_USER_STATS_INTERVAL = 1000 * 60 * 10;

var fs = require('fs');

/* global Rooms: true */
var Rooms = module.exports = getRoom;

var rooms = Rooms.rooms = Object.create(null);

var aliases = Object.create(null);

var Room = (function () {
	function Room(roomid, title) {
		this.id = roomid;
		this.title = (title || roomid);

		this.users = Object.create(null);

		this.log = [];

		this.bannedUsers = Object.create(null);
		this.bannedIps = Object.create(null);
	}
	Room.prototype.title = "";
	Room.prototype.type = 'chat';

	Room.prototype.lastUpdate = 0;
	Room.prototype.log = null;
	Room.prototype.users = null;
	Room.prototype.userCount = 0;

	Room.prototype.send = function (message, errorArgument) {
		if (errorArgument) throw new Error("Use Room#sendUser");
		if (this.id !== 'lobby') message = '>' + this.id + '\n' + message;
		Sockets.channelBroadcast(this.id, message);
	};
	Room.prototype.sendAuth = function (message) {
		for (var i in this.users) {
			var user = this.users[i];
			if (user.connected && user.can('receiveauthmessages', null, this)) {
				user.sendTo(this, message);
			}
		}
	};
	Room.prototype.sendUser = function (user, message) {
		user.sendTo(this, message);
	};
	Room.prototype.add = function (message) {
		if (typeof message !== 'string') throw new Error("Deprecated message type");
		this.logEntry(message);
		if (this.logTimes && message.substr(0, 3) === '|c|') {
			message = '|c:|' + (~~(Date.now() / 1000)) + '|' + message.substr(3);
		}
		this.log.push(message);
	};
	Room.prototype.logEntry = function () {};
	Room.prototype.addRaw = function (message) {
		this.add('|raw|' + message);
	};
	Room.prototype.getLogSlice = function (amount) {
		var log = this.log.slice(amount);
		log.unshift('|:|' + (~~(Date.now() / 1000)));
		return log;
	};
	Room.prototype.chat = function (user, message, connection) {
		// Battle actions are actually just text commands that are handled in
		// parseCommand(), which in turn often calls Simulator.prototype.sendFor().
		// Sometimes the call to sendFor is done indirectly, by calling
		// room.decision(), where room.constructor === BattleRoom.

		message = CommandParser.parse(message, this, user, connection);

		if (message) {
			this.add('|c|' + user.getIdentity(this.id) + '|' + message);
		}
		this.update();
	};

	return Room;
})();

var GlobalRoom = (function () {
	function GlobalRoom(roomid) {
		this.id = roomid;

		// init battle rooms
		this.battleCount = 0;
		this.searchers = [];

		// Never do any other file IO synchronously
		// but this is okay to prevent race conditions as we start up PS
		this.lastBattle = 0;
		try {
			this.lastBattle = parseInt(fs.readFileSync('logs/lastbattle.txt')) || 0;
		} catch (e) {} // file doesn't exist [yet]

		this.chatRoomData = [];
		try {
			this.chatRoomData = JSON.parse(fs.readFileSync('config/chatrooms.json'));
			if (!Array.isArray(this.chatRoomData)) this.chatRoomData = [];
		} catch (e) {} // file doesn't exist [yet]

		if (!this.chatRoomData.length) {
			this.chatRoomData = [{
				title: 'Lobby',
				isOfficial: true,
				autojoin: true
			}, {
				title: 'Staff',
				isPrivate: true,
				staffRoom: true,
				staffAutojoin: true
			}];
		}

		this.chatRooms = [];

		this.autojoin = []; // rooms that users autojoin upon connecting
		this.staffAutojoin = []; // rooms that staff autojoin upon connecting
		for (var i = 0; i < this.chatRoomData.length; i++) {
			if (!this.chatRoomData[i] || !this.chatRoomData[i].title) {
				console.log('ERROR: Room number ' + i + ' has no data.');
				continue;
			}
			var id = toId(this.chatRoomData[i].title);
			console.log("NEW CHATROOM: " + id);
			var room = Rooms.createChatRoom(id, this.chatRoomData[i].title, this.chatRoomData[i]);
			if (room.aliases) {
				for (var a = 0; a < room.aliases.length; a++) {
					aliases[room.aliases[a]] = room;
				}
			}
			this.chatRooms.push(room);
			if (room.autojoin) this.autojoin.push(id);
			if (room.staffAutojoin) this.staffAutojoin.push(id);
		}

		// this function is complex in order to avoid several race conditions
		var self = this;
		this.writeNumRooms = (function () {
			var writing = false;
			var lastBattle;	// last lastBattle to be written to file
			var finishWriting = function () {
				writing = false;
				if (lastBattle < self.lastBattle) {
					self.writeNumRooms();
				}
			};
			return function () {
				if (writing) return;

				// batch writing lastbattle.txt for every 10 battles
				if (lastBattle >= self.lastBattle) return;
				lastBattle = self.lastBattle + 10;

				writing = true;
				fs.writeFile('logs/lastbattle.txt.0', '' + lastBattle, function () {
					// rename is atomic on POSIX, but will throw an error on Windows
					fs.rename('logs/lastbattle.txt.0', 'logs/lastbattle.txt', function (err) {
						if (err) {
							// This should only happen on Windows.
							fs.writeFile('logs/lastbattle.txt', '' + lastBattle, finishWriting);
							return;
						}
						finishWriting();
					});
				});
			};
		})();

		this.writeChatRoomData = (function () {
			var writing = false;
			var writePending = false; // whether or not a new write is pending
			var finishWriting = function () {
				writing = false;
				if (writePending) {
					writePending = false;
					self.writeChatRoomData();
				}
			};
			return function () {
				if (writing) {
					writePending = true;
					return;
				}
				writing = true;
				var data = JSON.stringify(self.chatRoomData).replace(/\{"title"\:/g, '\n{"title":').replace(/\]$/, '\n]');
				fs.writeFile('config/chatrooms.json.0', data, function () {
					// rename is atomic on POSIX, but will throw an error on Windows
					fs.rename('config/chatrooms.json.0', 'config/chatrooms.json', function (err) {
						if (err) {
							// This should only happen on Windows.
							fs.writeFile('config/chatrooms.json', data, finishWriting);
							return;
						}
						finishWriting();
					});
				});
			};
		})();

		// init users
		this.users = {};
		this.userCount = 0; // cache of `Object.size(this.users)`
		this.maxUsers = 0;
		this.maxUsersDate = 0;

		this.reportUserStatsInterval = setInterval(
			this.reportUserStats.bind(this),
			REPORT_USER_STATS_INTERVAL
		);
	}
	GlobalRoom.prototype.type = 'global';

	GlobalRoom.prototype.formatListText = '|formats';

	GlobalRoom.prototype.reportUserStats = function () {
		if (this.maxUsersDate) {
			LoginServer.request('updateuserstats', {
				date: this.maxUsersDate,
				users: this.maxUsers
			}, function () {});
			this.maxUsersDate = 0;
		}
		LoginServer.request('updateuserstats', {
			date: Date.now(),
			users: this.userCount
		}, function () {});
	};

	GlobalRoom.prototype.getFormatListText = function () {
		var formatListText = '|formats';
		var curSection = '';
		for (var i in Tools.data.Formats) {
			var format = Tools.data.Formats[i];
			if (!format.challengeShow && !format.searchShow) continue;

			var section = format.section;
			if (section === undefined) section = format.mod;
			if (!section) section = '';
			if (section !== curSection) {
				curSection = section;
				formatListText += '|,' + (format.column || 1) + '|' + section;
			}
			formatListText += '|' + format.name;
			if (!format.challengeShow) formatListText += ',,';
			else if (!format.searchShow) formatListText += ',';
			if (format.team) formatListText += ',#';
		}
		return formatListText;
	};

	GlobalRoom.prototype.getRoomList = function (filter) {
		var roomList = {};
		var total = 0;
		var skipCount = 0;
		if (this.battleCount > 150) {
			skipCount = this.battleCount - 150;
		}
		for (var i in Rooms.rooms) {
			var room = Rooms.rooms[i];
			if (!room || !room.active || room.isPrivate) continue;
			if (filter && filter !== room.format && filter !== true) continue;
			if (skipCount && skipCount--) continue;
			var roomData = {};
			if (room.active && room.battle) {
				if (room.battle.players[0]) roomData.p1 = room.battle.players[0].getIdentity();
				if (room.battle.players[1]) roomData.p2 = room.battle.players[1].getIdentity();
			}
			if (!roomData.p1 || !roomData.p2) continue;
			roomList[room.id] = roomData;

			total++;
			if (total >= 100) break;
		}
		return roomList;
	};
	GlobalRoom.prototype.getRooms = function () {
		var roomsData = {official:[], chat:[], userCount: this.userCount, battleCount: this.battleCount};
		for (var i = 0; i < this.chatRooms.length; i++) {
			var room = this.chatRooms[i];
			if (!room) continue;
			if (room.isPrivate) continue;
			(room.isOfficial ? roomsData.official : roomsData.chat).push({
				title: room.title,
				desc: room.desc,
				userCount: room.userCount
			});
		}
		return roomsData;
	};
	GlobalRoom.prototype.cancelSearch = function (user) {
		var success = false;
		user.cancelChallengeTo();
		for (var i = 0; i < this.searchers.length; i++) {
			var search = this.searchers[i];
			var searchUser = Users.get(search.userid);
			if (!searchUser.connected) {
				this.searchers.splice(i, 1);
				i--;
				continue;
			}
			if (searchUser === user) {
				this.searchers.splice(i, 1);
				i--;
				if (!success) {
					searchUser.send('|updatesearch|' + JSON.stringify({searching: false}));
					success = true;
				}
				continue;
			}
		}
		return success;
	};
	GlobalRoom.prototype.searchBattle = function (user, formatid) {
		if (!user.connected) return;

		formatid = toId(formatid);

		user.prepBattle(formatid, 'search', null, this.finishSearchBattle.bind(this, user, formatid));
	};
	GlobalRoom.prototype.finishSearchBattle = function (user, formatid, result) {
		if (!result) return;

		// tell the user they've started searching
		var newSearchData = {
			format: formatid
		};
		user.send('|updatesearch|' + JSON.stringify({searching: newSearchData}));

		// get the user's rating before actually starting to search
		var newSearch = {
			userid: user.userid,
			formatid: formatid,
			team: user.team,
			rating: 1000,
			time: new Date().getTime()
		};
		var self = this;
		user.doWithMMR(formatid, function (mmr, error) {
			if (error) {
				user.popup("Connection to ladder server failed with error: " + error + "; please try again later");
				return;
			}
			newSearch.rating = mmr;
			self.addSearch(newSearch, user);
		});
	};
	GlobalRoom.prototype.matchmakingOK = function (search1, search2, user1, user2) {
		// users must be different
		if (user1 === user2) return false;

		// users must have different IPs
		if (user1.latestIp === user2.latestIp) return false;

		// users must not have been matched immediately previously
		if (user1.lastMatch === user2.userid || user2.lastMatch === user1.userid) return false;

		// search must be within range
		var searchRange = 100, formatid = search1.formatid, elapsed = Math.abs(search1.time - search2.time);
		if (formatid === 'ou' || formatid === 'oucurrent' || formatid === 'randombattle') searchRange = 50;
		searchRange += elapsed / 300; // +1 every .3 seconds
		if (searchRange > 300) searchRange = 300;
		if (Math.abs(search1.rating - search2.rating) > searchRange) return false;

		user1.lastMatch = user2.userid;
		user2.lastMatch = user1.userid;
		return true;
	};
	GlobalRoom.prototype.addSearch = function (newSearch, user) {
		if (!user.connected) return;
		for (var i = 0; i < this.searchers.length; i++) {
			var search = this.searchers[i];
			var searchUser = Users.get(search.userid);
			if (!searchUser || !searchUser.connected) {
				this.searchers.splice(i, 1);
				i--;
				continue;
			}
			if (newSearch.formatid === search.formatid && searchUser === user) return; // only one search per format
			if (newSearch.formatid === search.formatid && this.matchmakingOK(search, newSearch, searchUser, user)) {
				this.cancelSearch(user, true);
				this.cancelSearch(searchUser, true);
				user.send('|updatesearch|' + JSON.stringify({searching: false}));
				this.startBattle(searchUser, user, search.formatid, search.team, newSearch.team, {rated: true});
				return;
			}
		}
		this.searchers.push(newSearch);
	};
	GlobalRoom.prototype.send = function (message, user) {
		if (user) {
			user.sendTo(this, message);
		} else {
			Sockets.channelBroadcast(this.id, message);
		}
	};
	GlobalRoom.prototype.sendAuth = function (message) {
		for (var i in this.users) {
			var user = this.users[i];
			if (user.connected && user.can('receiveauthmessages', null, this)) {
				user.sendTo(this, message);
			}
		}
	};
	GlobalRoom.prototype.add = function (message) {
		if (rooms.lobby) rooms.lobby.add(message);
	};
	GlobalRoom.prototype.addRaw = function (message) {
		if (rooms.lobby) rooms.lobby.addRaw(message);
	};
	GlobalRoom.prototype.addChatRoom = function (title) {
		var id = toId(title);
		if (rooms[id]) return false;

		var chatRoomData = {
			title: title
		};
		var room = Rooms.createChatRoom(id, title, chatRoomData);
		this.chatRoomData.push(chatRoomData);
		this.chatRooms.push(room);
		this.writeChatRoomData();
		return true;
	};
	GlobalRoom.prototype.deregisterChatRoom = function (id) {
		id = toId(id);
		var room = rooms[id];
		if (!room) return false; // room doesn't exist
		if (!room.chatRoomData) return false; // room isn't registered
		// deregister from global chatRoomData
		// looping from the end is a pretty trivial optimization, but the
		// assumption is that more recently added rooms are more likely to
		// be deleted
		for (var i = this.chatRoomData.length - 1; i >= 0; i--) {
			if (id === toId(this.chatRoomData[i].title)) {
				this.chatRoomData.splice(i, 1);
				this.writeChatRoomData();
				break;
			}
		}
		delete room.chatRoomData;
		return true;
	};
	GlobalRoom.prototype.delistChatRoom = function (id) {
		id = toId(id);
		if (!rooms[id]) return false; // room doesn't exist
		for (var i = this.chatRooms.length - 1; i >= 0; i--) {
			if (id === this.chatRooms[i].id) {
				this.chatRooms.splice(i, 1);
				break;
			}
		}
	};
	GlobalRoom.prototype.removeChatRoom = function (id) {
		id = toId(id);
		var room = rooms[id];
		if (!room) return false; // room doesn't exist
		room.destroy();
		return true;
	};
	GlobalRoom.prototype.autojoinRooms = function (user, connection) {
		// we only autojoin regular rooms if the client requests it with /autojoin
		// note that this restriction doesn't apply to staffAutojoin
		for (var i = 0; i < this.autojoin.length; i++) {
			user.joinRoom(this.autojoin[i], connection);
		}
	};
	GlobalRoom.prototype.checkAutojoin = function (user, connection) {
		for (var i = 0; i < this.staffAutojoin.length; i++) {
			var room = Rooms.get(this.staffAutojoin[i]);
			if (!room) {
				this.staffAutojoin.splice(i, 1);
				i--;
				continue;
			}
			if (room.staffAutojoin === true && user.isStaff ||
					typeof room.staffAutojoin === 'string' && room.staffAutojoin.indexOf(user.group) >= 0) {
				// if staffAutojoin is true: autojoin if isStaff
				// if staffAutojoin is String: autojoin if user.group in staffAutojoin
				user.joinRoom(room.id, connection);
			}
		}

		for (var i = 0; i < this.autojoin.length; i++) {
			var room = Rooms.get(this.autojoin[i]);
			if (!room) {
				this.autojoin.splice(i, 1);
				i--;
				continue;
			}
			if (room.autojoin === true) {
				user.joinRoom(room.id, connection);
			}
		}
	};
	GlobalRoom.prototype.onJoinConnection = function (user, connection) {
		var initdata = '|updateuser|' + user.name + '|' + (user.named ? '1' : '0') + '|' + user.avatar + '\n';
		connection.send(initdata + this.formatListText);
		if (this.chatRooms.length > 2) connection.send('|queryresponse|rooms|null'); // should display room list
	};
	GlobalRoom.prototype.onJoin = function (user, connection, merging) {
		if (!user) return false; // ???
		if (this.users[user.userid]) return user;

		this.users[user.userid] = user;
		if (++this.userCount > this.maxUsers) {
			this.maxUsers = this.userCount;
			this.maxUsersDate = Date.now();
		}

		if (!merging) {
			var initdata = '|updateuser|' + user.name + '|' + (user.named ? '1' : '0') + '|' + user.avatar + '\n';
			connection.send(initdata + this.formatListText);
			if (this.chatRooms.length > 2) connection.send('|queryresponse|rooms|null'); // should display room list
		}

		return user;
	};
	GlobalRoom.prototype.onRename = function (user, oldid, joining) {
		delete this.users[oldid];
		this.users[user.userid] = user;
		return user;
	};
	GlobalRoom.prototype.onUpdateIdentity = function () {};
	GlobalRoom.prototype.onLeave = function (user) {
		if (!user) return; // ...
		delete this.users[user.userid];
		--this.userCount;
		this.cancelSearch(user, true);
	};
	GlobalRoom.prototype.startBattle = function (p1, p2, format, p1team, p2team, options) {
		var newRoom;
		p1 = Users.get(p1);
		p2 = Users.get(p2);

		if (!p1 || !p2) {
			// most likely, a user was banned during the battle start procedure
			this.cancelSearch(p1, true);
			this.cancelSearch(p2, true);
			return;
		}
		if (p1 === p2) {
			this.cancelSearch(p1, true);
			this.cancelSearch(p2, true);
			p1.popup("You can't battle your own account. Please use something like Private Browsing to battle yourself.");
			return;
		}

		if (this.lockdown === true) {
			this.cancelSearch(p1, true);
			this.cancelSearch(p2, true);
			p1.popup("The server is shutting down. Battles cannot be started at this time.");
			p2.popup("The server is shutting down. Battles cannot be started at this time.");
			return;
		}

		//console.log('BATTLE START BETWEEN: ' + p1.userid + ' ' + p2.userid);
		var i = this.lastBattle + 1;
		var formaturlid = format.toLowerCase().replace(/[^a-z0-9]+/g, '');
		while (rooms['battle-' + formaturlid + i]) {
			i++;
		}
		this.lastBattle = i;
		rooms.global.writeNumRooms();
		newRoom = this.addRoom('battle-' + formaturlid + '-' + i, format, p1, p2, options);
		p1.joinRoom(newRoom);
		p2.joinRoom(newRoom);
		newRoom.joinBattle(p1, p1team);
		newRoom.joinBattle(p2, p2team);
		this.cancelSearch(p1, true);
		this.cancelSearch(p2, true);
		if (Config.reportbattles && rooms.chat) {
			rooms.chat.add('|b|' + newRoom.id + '|' + p1.getIdentity() + '|' + p2.getIdentity());
		}
		if (Config.logladderip && options.rated) {
			if (!this.ladderIpLog) {
				this.ladderIpLog = fs.createWriteStream('logs/ladderip/ladderip.txt', {flags: 'a'});
			}
			this.ladderIpLog.write(p1.userid + ': ' + p1.latestIp + '\n');
			this.ladderIpLog.write(p2.userid + ': ' + p2.latestIp + '\n');
		}
		return newRoom;
	};
	GlobalRoom.prototype.addRoom = function (room, format, p1, p2, options) {
		room = Rooms.createBattle(room, format, p1, p2, options);
		return room;
	};
	GlobalRoom.prototype.chat = function (user, message, connection) {
		if (rooms.lobby) return rooms.lobby.chat(user, message, connection);
		message = CommandParser.parse(message, this, user, connection);
		if (message) {
			connection.popup("You can't send messages directly to the server.");
		}
	};
	return GlobalRoom;
})();
{'fraction_non_alphanumeric': 0.15451538120913755, 'fraction_numerical': 0.009637675496861243, 'mean_word_length': 3.3951917572982255, 'pattern_counts': {'":': 2, '<': 21, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5943845', 'n_tokens_mistral': 14962, 'n_tokens_neox': 12981, 'n_words': 3148}