text: /**
   * Room settings commands
   * Pokemon Showdown - http://pokemonshowdown.com/
   *
   * Commands for settings relating to room setting filtering.
   *
   * @license MIT
   */
  
  'use strict';
  
  const RANKS = Config.groupsranking;
  
  const SLOWCHAT_MINIMUM = 2;
  const SLOWCHAT_MAXIMUM = 60;
  const SLOWCHAT_USER_REQUIREMENT = 10;
  
  const MAX_CHATROOM_ID_LENGTH = 225;
  
  class RoomSettings {
  	constructor(user, room, connection) {
  		this.room = room;
  		this.user = user;
  		this.connection = connection;
  		this.sameCommand = true;
  	}
  	updateSetting(command) {
  		this.sameCommand = false;
  		this.generateDisplay();
  	}
  	button(setting, disable, command) {
  		if (disable) {
  			return Chat.html`<button class="button disabled" style="font-weight:bold; color:#575757; font-weight:bold; background-color:#d3d3d3;">${setting}</button> `;
  		}
  		return Chat.html`<button class="button" name="send" value="/roomsetting ${command}">${setting}</button> `;
  	}
  	modchat() {
  		if (!this.user.can('modchat', null, this.room)) return this.button(this.room.modchat ? this.room.modchat : 'off', true);
  		let modchatOutput = [];
  		for (const rank of RANKS) {
  			if (rank === Config.groupsranking[0] && !this.room.modchat) {
  				modchatOutput.push(this.button('off', true));
  			} else if (rank === Config.groupsranking[0]) {
  				modchatOutput.push(this.button('off', null, 'modchat off'));
  			} else if (rank === this.room.modchat) {
  				modchatOutput.push(this.button(rank, true));
  			} else if (rank) {
  				let rankIndex = RANKS.indexOf(rank);
  				let roomAuth = (this.room.auth && this.room.auth[this.user.id] ? this.room.auth[this.user.id] : false);
  				let roomAuthIndex = (roomAuth ? RANKS.indexOf(roomAuth) : false);
  				if (rankIndex > 1 && !this.user.can('modchatall', null, this.room)) continue;
  				if (roomAuth && !this.user.can('bypassall')) {
  					if (rankIndex > roomAuthIndex) continue;
  				}
  				modchatOutput.push(this.button(rank, null, `modchat ${rank}`));
  			}
  		}
  		// Since autoconfirmed isn't technically a Config rank...
  		let acStatus = this.room.modchat === 'autoconfirmed';
  		modchatOutput.splice(1, 0, this.button('AC', acStatus, 'modchat autoconfirmed'));
  		return modchatOutput.join(' ');
  	}
  	modjoin() {
  		if (!this.user.can('makeroom') && !this.room.isPersonal ||
  			!this.user.can('editroom', null, this.room)) {
  			return this.button(this.room.modjoin ? this.room.modjoin : 'off', true);
  		}
  		let modjoinOutput = [];
  		for (const rank of RANKS) {
  			if (rank === Config.groupsranking[0] && !this.room.modjoin) {
  				modjoinOutput.push(this.button('off', true));
  			} else if (rank === Config.groupsranking[0]) {
  				modjoinOutput.push(this.button('off', null, 'modjoin off'));
  			} else if (rank === this.room.modjoin) {
  				modjoinOutput.push(this.button(rank, true));
  			} else if (rank) {
  				// groupchat hosts can set modjoin, but only to +
  				if (this.room.isPersonal && !this.user.can('makeroom') && rank !== '+') continue;
  
  				modjoinOutput.push(this.button(rank, false, `modjoin ${rank}`));
  			}
  		}
  		return modjoinOutput.join(' ');
  	}
  	stretching() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.filterStretching ? 'filter stretching' : 'off', true);
  		if (this.room.filterStretching) {
  			return `${this.button('off', null, 'stretchfilter off')} ${this.button('filter stretching', true)}`;
  		} else {
  			return `${this.button('off', true)} ${this.button('filter stretching', null, 'stretchfilter on')}`;
  		}
  	}
  	capitals() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.filterCaps ? 'filter capitals' : 'off', true);
  		if (this.room.filterCaps) {
  			return `${this.button('off', null, 'capsfilter off')} ${this.button('filter capitals', true)}`;
  		} else {
  			return `${this.button('off', true)} ${this.button('filter capitals', null, 'capsfilter on')}`;
  		}
  	}
  	emojis() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.filterEmojis ? 'filter emojis' : 'off', true);
  		if (this.room.filterEmojis) {
  			return `${this.button('off', null, 'emojifilter off')} ${this.button('filter emojis', true)}`;
  		} else {
  			return `${this.button('off', true)} ${this.button('filter emojis', null, 'emojifilter on')}`;
  		}
  	}
  	slowchat() {
  		if (!this.user.can('editroom', null, this.room) || (!this.user.can('bypassall') && this.room.userCount < SLOWCHAT_USER_REQUIREMENT)) return this.button(this.room.slowchat ? this.room.slowchat : 'off', true);
  
  		let slowchatOutput = [];
  		for (let i of [5, 10, 20, 30, 60]) {
  			if (this.room.slowchat === i) {
  				slowchatOutput.push(this.button(`${i}s`, true));
  			} else {
  				slowchatOutput.push(this.button(`${i}s`, null, `slowchat ${i}`));
  			}
  		}
  		if (!this.room.slowchat) {
  			slowchatOutput.unshift(this.button('off', true));
  		} else {
  			slowchatOutput.unshift(this.button('off', null, 'slowchat false'));
  		}
  		return slowchatOutput.join(' ');
  	}
  	tourStatus() {
  		if (!this.user.can('gamemanagement', null, this.room)) return this.button(this.room.toursEnabled === true ? '@' : this.room.toursEnabled === '%' ? '%' : '#', true);
  
  		if (this.room.toursEnabled === true) {
  			return `${this.button('%', null, 'tournament enable %')} ${this.button('@', true)} ${this.button('#', null, 'tournament disable')}`;
  		} else if (this.room.toursEnabled === '%') {
  			return `${this.button('%', true)} ${this.button('@', null, 'tournament enable @')} ${this.button('#', null, 'tournament disable')}`;
  		} else {
  			return `${this.button('%', null, 'tournament enable %')} ${this.button('@', null, 'tournament enable @')} ${this.button('#', true)}`;
  		}
  	}
  	uno() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.unoDisabled ? 'off' : 'UNO enabled', true);
  		if (this.room.unoDisabled) {
  			return `${this.button('UNO enabled', null, 'uno enable')} ${this.button('off', true)}`;
  		} else {
  			return `${this.button('UNO enabled', true)} ${this.button('off', null, 'uno disable')}`;
  		}
  	}
  	hangman() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.hangmanDisabled ? 'off' : 'Hangman enabled', true);
  		if (this.room.hangmanDisabled) {
  			return `${this.button('Hangman enabled', null, 'hangman enable')} ${this.button('off', true)}`;
  		} else {
  			return `${this.button('Hangman enabled', true)} ${this.button('off', null, 'hangman disable')}`;
  		}
  	}
  	mafia() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.mafiaDisabled ? 'off' : 'Mafia enabled', true);
  		if (this.room.mafiaDisabled) {
  			return `${this.button('Mafia enabled', null, 'mafia enable')} ${this.button('off', true)}`;
  		} else {
  			return `${this.button('Mafia enabled', true)} ${this.button('off', null, 'mafia disable')}`;
  		}
  	}
  	blackjack() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.blackjackDisabled ? 'off' : 'Blackjack enabled', true);
  		if (this.room.blackjackDisabled) {
  			return `${this.button('Blackjack enabled', null, 'blackjack enable')} ${this.button('off', true)}`;
  		} else {
  			return `${this.button('Blackjack enabled', true)} ${this.button('off', null, 'blackjack disable')}`;
  		}
  	}
  	language() {
  		if (!this.user.can('editroom', null, this.room)) return this.button(this.room.language ? Chat.languages.get(this.room.language) : 'English', true);
  
  		let languageOutput = [];
  		languageOutput.push(this.button(`English`, !this.room.language, 'roomlanguage english'));
  		for (let [id, text] of Chat.languages) {
  			languageOutput.push(this.button(text, this.room.language === id, `roomlanguage ${id}`));
  		}
  		return languageOutput.join(' ');
  	}
  	generateDisplay(user, room, connection) {
  		let output = Chat.html`<div class="infobox">Room Settings for ${this.room.title}<br />`;
  		output += `<strong>Language:</strong> <br />${this.language()}<br />`;
  		output += `<strong>Modchat:</strong> <br />${this.modchat()}<br />`;
  		output += `<strong>Modjoin:</strong> <br />${this.modjoin()}<br />`;
  		output += `<strong>Stretch filter:</strong> <br />${this.stretching()}<br />`;
  		output += `<strong>Caps filter:</strong> <br />${this.capitals()}<br />`;
  		output += `<strong>Emoji filter:</strong> <br />${this.emojis()}<br />`;
  		output += `<strong>Slowchat:</strong> <br />${this.slowchat()}<br />`;
  		output += `<strong>Tournaments:</strong> <br />${this.tourStatus()}<br />`;
  		output += `<strong>UNO:</strong> <br />${this.uno()}<br />`;
  		output += `<strong>Hangman:</strong> <br />${this.hangman()}<br />`;
  		output += `<strong>Blackjack:</strong> <br />${this.blackjack()}<br />`;
  		output += `<strong>Mafia:</strong> <br />${this.mafia()}<br />`;
  		output += '</div>';
  
  		this.user.sendTo(this.room, `|uhtml${(this.sameCommand ? '' : 'change')}|roomsettings|${output}`);
  	}
  }
  
  exports.commands = {
  	roomsetting: 'roomsettings',
  	roomsettings(target, room, user, connection) {
  		if (room.battle) return this.errorReply("This command cannot be used in battle rooms.");
  		const settings = new RoomSettings(user, room, connection);
  
  		if (!target) {
  			room.update();
  			settings.generateDisplay(user, room, connection);
  		} else {
  			this.parse(`/${target}`);
  			settings.updateSetting(target);
  		}
  	},
  	roomsettingshelp: [`/roomsettings - Shows current room settings with buttons to change them (if you can).`],
  
  	modchat(target, room, user) {
  		if (!target) {
  			const modchatSetting = (room.modchat || "OFF");
  			return this.sendReply(`Moderated chat is currently set to: ${modchatSetting}`);
  		}
  		if (!this.can('modchat', null, room)) return false;
  
  		// 'modchat' lets you set up to 1 (ac/trusted also allowed)
  		// 'modchatall' lets you set up to your current rank
  		// 'makeroom' lets you set any rank, no limit
  		let threshold = 1;
  		let roomGroup = Config.groups[room.getAuth(user)];
  		if (roomGroup && user.can('modchatall', null, room)) {
  			if (user.can('makeroom')) {
  				threshold = Infinity;
  			} else {
  				threshold = roomGroup.rank;
  			}
  		}
  
  		if (room.modchat && room.modchat.length <= 1 && Config.groupsranking.indexOf(room.modchat) > threshold) {
  			return this.errorReply(`/modchat - Access denied for changing a setting higher than ${Config.groupsranking[threshold]}.`);
  		}
  		if (room.requestModchat) {
  			const error = room.requestModchat(user);
  			if (error) return this.errorReply(error);
  		}
  
  		target = target.toLowerCase().trim();
  		const currentModchat = room.modchat;
  		switch (target) {
  		case 'off':
  		case 'false':
  		case 'no':
  		case 'disable':
  			room.modchat = false;
  			break;
  		case 'ac':
  		case 'autoconfirmed':
  			room.modchat = 'autoconfirmed';
  			break;
  		case 'trusted':
  			room.modchat = 'trusted';
  			break;
  		case 'player':
  			target = Users.PLAYER_SYMBOL;
  			/* falls through */
  		default:
  			if (!Config.groups[target]) {
  				this.errorReply(`The rank '${target}' was unrecognized as a modchat level.`);
  				return this.parse('/help modchat');
  			}
  			if (Config.groupsranking.indexOf(target) > threshold) {
  				return this.errorReply(`/modchat - Access denied for setting higher than ${Config.groupsranking[threshold]}.`);
  			}
  			room.modchat = target;
  			break;
  		}
  		if (currentModchat === room.modchat) {
  			return this.errorReply(`Modchat is already set to ${currentModchat}.`);
  		}
  		if (!room.modchat) {
  			this.add("|raw|<div class=\"broadcast-blue\"><strong>Moderated chat was disabled!</strong><br />Anyone may talk now.</div>");
  		} else {
  			const modchatSetting = Chat.escapeHTML(room.modchat);
  			this.add(`|raw|<div class="broadcast-red"><strong>Moderated chat was set to ${modchatSetting}!</strong><br />Only users of rank ${modchatSetting} and higher can talk.</div>`);
  		}
  		if (room.requestModchat && !room.modchat) room.requestModchat(null);
  		this.privateModAction(`(${user.name} set modchat to ${room.modchat})`);
  		this.modlog('MODCHAT', null, `to ${room.modchat}`);
  
  		if (room.chatRoomData) {
  			room.chatRoomData.modchat = room.modchat;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	modchathelp: [`/modchat [off/autoconfirmed/+/%/@/*/player/#/&/~] - Set the level of moderated chat. Requires: * @ \u2606 for off/autoconfirmed/+ options, # & ~ for all the options`],
  
  	ioo(target, room, user) {
  		return this.parse('/modjoin %');
  	},
  	'!ionext': true,
  	inviteonlynext: 'ionext',
  	ionext(target, room, user) {
  		const groupConfig = Config.groups[Users.PLAYER_SYMBOL];
  		if (!(groupConfig && groupConfig.editprivacy)) return this.errorReply(`/ionext - Access denied.`);
  		if (this.meansNo(target)) {
  			user.inviteOnlyNextBattle = false;
  			user.update('inviteOnlyNextBattle');
  			this.sendReply("Your next battle will be publicly visible.");
  		} else {
  			user.inviteOnlyNextBattle = true;
  			user.update('inviteOnlyNextBattle');
  			if (user.forcedPublic) return this.errorReply(`Your next battle will be invite-only provided it is not rated, otherwise your '${user.forcedPublic}' prefix will force the battle to be public.`);
  			this.sendReply("Your next battle will be invite-only.");
  		}
  	},
  	ionexthelp: [
  		`/ionext - Sets your next battle to be invite-only.`,
  		`/ionext off - Sets your next battle to be publicly visible.`,
  	],
  
  	inviteonly(target, room, user) {
  		if (!target) return this.parse('/help inviteonly');
  		if (this.meansYes(target)) {
  			return this.parse("/modjoin %");
  		} else {
  			return this.parse(`/modjoin ${target}`);
  		}
  	},
  	inviteonlyhelp: [
  		`/inviteonly [on|off] - Sets modjoin %. Users can't join unless invited with /invite. Requires: # & ~`,
  		`/ioo - Shortcut for /inviteonly on`,
  		`/inviteonlynext OR /ionext - Sets your next battle to be invite-only.`,
  		`/ionext off - Sets your next battle to be publicly visible.`,
  	],
  
  	modjoin(target, room, user) {
  		if (!target) {
  			const modjoinSetting = room.modjoin === true ? "SYNC" : room.modjoin || "OFF";
  			return this.sendReply(`Modjoin is currently set to: ${modjoinSetting}`);
  		}
  		if (room.isPersonal) {
  			if (!this.can('editroom', null, room)) return;
  		} else if (room.battle) {
  			if (!this.can('editprivacy', null, room)) return;
  			const prefix = room.battle.forcedPublic();
  			if (prefix && !user.can('editprivacy')) return this.errorReply(`This battle is required to be public due to a player having a name prefixed by '${prefix}'.`);
  		} else {
  			if (!this.can('makeroom')) return;
  		}
  		if (room.tour && !room.tour.modjoin) return this.errorReply(`You can't do this in tournaments where modjoin is prohibited.`);
  		if (target === 'player') target = Users.PLAYER_SYMBOL;
  		if (this.meansNo(target)) {
  			if (!room.modjoin) return this.errorReply(`Modjoin is already turned off in this room.`);
  			room.modjoin = null;
  			this.add(`|raw|<div class="broadcast-blue"><strong>This room is no longer invite only!</strong><br />Anyone may now join.</div>`);
  			this.addModAction(`${user.name} turned off modjoin.`);
  			this.modlog('MODJOIN', null, 'OFF');
  			if (room.chatRoomData) {
  				room.chatRoomData.modjoin = null;
  				Rooms.global.writeChatRoomData();
  			}
  			return;
  		} else if (target === 'sync') {
  			if (room.modjoin === true) return this.errorReply(`Modjoin is already set to sync modchat in this room.`);
  			room.modjoin = true;
  			this.add(`|raw|<div class="broadcast-red"><strong>Moderated join is set to sync with modchat!</strong><br />Only users who can speak in modchat can join.</div>`);
  			this.addModAction(`${user.name} set modjoin to sync with modchat.`);
  			this.modlog('MODJOIN SYNC');
  		} else if (target === 'ac' || target === 'autoconfirmed') {
  			if (room.modjoin === 'autoconfirmed') return this.errorReply(`Modjoin is already set to autoconfirmed.`);
  			room.modjoin = 'autoconfirmed';
  			this.add(`|raw|<div class="broadcast-red"><strong>Moderated join is set to autoconfirmed!</strong><br />Users must be rank autoconfirmed or invited with <code>/invite</code> to join</div>`);
  			this.addModAction(`${user.name} set modjoin to autoconfirmed.`);
  			this.modlog('MODJOIN', null, 'autoconfirmed');
  		} else if (target in Config.groups || target === 'trusted') {
  			if (room.battle && !user.can('makeroom') && !'+%'.includes(target)) {
  				return this.errorReply(`/modjoin - Access denied from setting modjoin past % in battles.`);
  			}
  			if (room.isPersonal && !user.can('makeroom') && !'+%'.includes(target)) {
  				return this.errorReply(`/modjoin - Access denied from setting modjoin past % in group chats.`);
  			}
  			if (room.modjoin === target) return this.errorReply(`Modjoin is already set to ${target} in this room.`);
  			room.modjoin = target;
  			this.add(`|raw|<div class="broadcast-red"><strong>This room is now invite only!</strong><br />Users must be rank ${target} or invited with <code>/invite</code> to join</div>`);
  			this.addModAction(`${user.name} set modjoin to ${target}.`);
  			this.modlog('MODJOIN', null, target);
  		} else {
  			this.errorReply(`Unrecognized modjoin setting.`);
  			this.parse('/help modjoin');
  			return false;
  		}
  		if (room.chatRoomData) {
  			room.chatRoomData.modjoin = room.modjoin;
  			Rooms.global.writeChatRoomData();
  		}
  		if (target === 'sync' && !room.modchat) this.parse(`/modchat ${Config.groupsranking[1]}`);
  		if (!room.isPrivate) this.parse('/hiddenroom');
  	},
  	modjoinhelp: [
  		`/modjoin [+|%|@|*|player|&|~|#|off] - Sets modjoin. Users lower than the specified rank can't join this room unless they have a room rank. Requires: \u2606 # & ~`,
  		`/modjoin [sync|off] - Sets modjoin. Only users who can speak in modchat can join this room. Requires: \u2606 # & ~`,
  	],
  
  	roomlanguage(target, room, user) {
  		if (!target) return this.sendReply(`This room's primary language is ${Chat.languages.get(room.language) || 'English'}`);
  		if (!this.can('editroom', null, room)) return false;
  
  		let targetLanguage = toID(target);
  		if (!Chat.languages.has(targetLanguage)) return this.errorReply(`"${target}" is not a supported language.`);
  
  		room.language = targetLanguage === 'english' ? false : targetLanguage;
  
  		if (room.chatRoomData) {
  			room.chatRoomData.language = room.language;
  			Rooms.global.writeChatRoomData();
  		}
  		this.modlog(`LANGUAGE`, null, Chat.languages.get(targetLanguage));
  		this.sendReply(`The room's language has been set to ${Chat.languages.get(targetLanguage)}`);
  	},
  	roomlanguagehelp: [
  		`/roomlanguage [language] - Sets the the language for the room, which changes language of a few commands. Requires # & ~`,
  		`Supported Languages: English, Spanish, Italian, French, Simplified Chinese, Traditional Chinese, Japanese, Hindi, Turkish, Dutch, German.`,
  	],
  
  	slowchat(target, room, user) {
  		if (!target) {
  			const slowchatSetting = (room.slowchat || "OFF");
  			return this.sendReply(`Slow chat is currently set to: ${slowchatSetting}`);
  		}
  		if (!this.canTalk()) return;
  		if (!this.can('modchat', null, room)) return false;
  
  		let targetInt = parseInt(target);
  		if (this.meansNo(target)) {
  			if (!room.slowchat) return this.errorReply(`Slow chat is already disabled in this room.`);
  			room.slowchat = false;
  		} else if (targetInt) {
  			if (!user.can('bypassall') && room.userCount < SLOWCHAT_USER_REQUIREMENT) return this.errorReply(`This room must have at least ${SLOWCHAT_USER_REQUIREMENT} users to set slowchat; it only has ${room.userCount} right now.`);
  			if (room.slowchat === targetInt) return this.errorReply(`Slow chat is already set to ${room.slowchat} seconds in this room.`);
  			if (targetInt < SLOWCHAT_MINIMUM) targetInt = SLOWCHAT_MINIMUM;
  			if (targetInt > SLOWCHAT_MAXIMUM) targetInt = SLOWCHAT_MAXIMUM;
  			room.slowchat = targetInt;
  		} else {
  			return this.parse("/help slowchat");
  		}
  		const slowchatSetting = (room.slowchat || "OFF");
  		this.privateModAction(`(${user.name} set slowchat to ${slowchatSetting})`);
  		this.modlog('SLOWCHAT', null, '' + slowchatSetting);
  
  		if (room.chatRoomData) {
  			room.chatRoomData.slowchat = room.slowchat;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	slowchathelp: [
  		`/slowchat [number] - Sets a limit on how often users in the room can send messages, between 2 and 60 seconds. Requires @ # & ~`,
  		`/slowchat off - Disables slowchat in the room. Requires @ # & ~`,
  	],
  
  	stretching: 'stretchfilter',
  	stretchingfilter: 'stretchfilter',
  	stretchfilter(target, room, user) {
  		if (!target) {
  			const stretchSetting = (room.filterStretching ? "ON" : "OFF");
  			return this.sendReply(`This room's stretch filter is currently: ${stretchSetting}`);
  		}
  		if (!this.canTalk()) return;
  		if (!this.can('editroom', null, room)) return false;
  
  		if (this.meansYes(target)) {
  			if (room.filterStretching) return this.errorReply(`This room's stretch filter is already ON`);
  			room.filterStretching = true;
  		} else if (this.meansNo(target)) {
  			if (!room.filterStretching) return this.errorReply(`This room's stretch filter is already OFF`);
  			room.filterStretching = false;
  		} else {
  			return this.parse("/help stretchfilter");
  		}
  		const stretchSetting = (room.filterStretching ? "ON" : "OFF");
  		this.privateModAction(`(${user.name} turned the stretch filter ${stretchSetting})`);
  		this.modlog('STRETCH FILTER', null, stretchSetting);
  
  		if (room.chatRoomData) {
  			room.chatRoomData.filterStretching = room.filterStretching;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	stretchfilterhelp: [`/stretchfilter [on/off] - Toggles filtering messages in the room for stretchingggggggg. Requires # & ~`],
  
  	capitals: 'capsfilter',
  	capitalsfilter: 'capsfilter',
  	capsfilter(target, room, user) {
  		if (!target) {
  			const capsSetting = (room.filterCaps ? "ON" : "OFF");
  			return this.sendReply(`This room's caps filter is currently: ${capsSetting}`);
  		}
  		if (!this.canTalk()) return;
  		if (!this.can('editroom', null, room)) return false;
  
  		if (this.meansYes(target)) {
  			if (room.filterCaps) return this.errorReply(`This room's caps filter is already ON`);
  			room.filterCaps = true;
  		} else if (this.meansNo(target)) {
  			if (!room.filterCaps) return this.errorReply(`This room's caps filter is already OFF`);
  			room.filterCaps = false;
  		} else {
  			return this.parse("/help capsfilter");
  		}
  		const capsSetting = (room.filterCaps ? "ON" : "OFF");
  		this.privateModAction(`(${user.name} turned the caps filter ${capsSetting})`);
  		this.modlog('CAPS FILTER', null, capsSetting);
  
  		if (room.chatRoomData) {
  			room.chatRoomData.filterCaps = room.filterCaps;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	capsfilterhelp: [`/capsfilter [on/off] - Toggles filtering messages in the room for EXCESSIVE CAPS. Requires # & ~`],
  
  	emojis: 'emojifilter',
  	emoji: 'emojifilter',
  	emojifilter(target, room, user) {
  		if (!target) {
  			const emojiSetting = (room.filterEmojis ? "ON" : "OFF");
  			return this.sendReply(`This room's emoji filter is currently: ${emojiSetting}`);
  		}
  		if (!this.canTalk()) return;
  		if (!this.can('editroom', null, room)) return false;
  
  		if (this.meansYes(target)) {
  			if (room.filterEmojis) return this.errorReply(`This room's emoji filter is already ON`);
  			room.filterEmojis = true;
  		} else if (this.meansNo(target)) {
  			if (!room.filterEmojis) return this.errorReply(`This room's emoji filter is already OFF`);
  			room.filterEmojis = false;
  		} else {
  			return this.parse("/help emojifilter");
  		}
  		const emojiSetting = (room.filterEmojis ? "ON" : "OFF");
  		this.privateModAction(`(${user.name} turned the emoji filter ${emojiSetting})`);
  		this.modlog('EMOJI FILTER', null, emojiSetting);
  
  		if (room.chatRoomData) {
  			room.chatRoomData.filterEmojis = room.filterEmojis;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	emojifilterhelp: [`/emojifilter [on/off] - Toggles filtering messages in the room for emojis. Requires # & ~`],
  
  	banwords: 'banword',
  	banword: {
  		regexadd: 'add',
  		addregex: 'add',
  		add(target, room, user, connection, cmd) {
  			if (!target || target === ' ') return this.parse('/help banword');
  			if (!this.can('declare', null, room)) return false;
  
  			const regex = cmd.includes('regex');
  			if (regex && !user.can('makeroom')) return this.errorReply("Regex banwords are only allowed for leaders or above.");
  
  			if (!room.banwords) room.banwords = [];
  
  			// Most of the regex code is copied from the client. TODO: unify them?
  			// Regex banwords can have commas in the {1,5} pattern
  			let words = (regex ? target.match(/[^,]+(,\d*}[^,]*)?/g) : target.split(','))
  				.map(word => word.replace(/\n/g, '').trim());
  			if (!words) return this.parse('/help banword');
  
  			// Escape any character with a special meaning in regex
  			if (!regex) {
  				words = words.map(word => {
  					if (/[\\^$*+?()|{}[\]]/.test(word)) this.errorReply(`"${word}" might be a regular expression, did you mean "/banword addregex"?`);
  					return word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  				});
  			}
  			// PS adds a preamble to the banword regex that's 32 chars long
  			let banwordRegexLen = (room.banwordRegex instanceof RegExp) ? room.banwordRegex.source.length : 32;
  			for (let word of words) {
  				try {
  					new RegExp(word); // eslint-disable-line no-new
  				} catch (e) {
  					return this.errorReply(e.message.startsWith('Invalid regular expression: ') ? e.message : `Invalid regular expression: /${word}/: ${e.message}`);
  				}
  				if (room.banwords.includes(word)) return this.errorReply(`${word} is already a banned phrase.`);
  
  				// Banword strings are joined, so account for the first string not having the prefix
  				banwordRegexLen += (banwordRegexLen === 32) ? word.length : `|${word}`.length;
  				// RegExp instances whose source is greater than or equal to
  				// v8's RegExpMacroAssembler::kMaxRegister in length will crash
  				// the server on compile. In this case, that would happen each
  				// time a chat message gets tested for any banned phrases.
  				if (banwordRegexLen >= (1 << 16 - 1)) return this.errorReply("This room has too many banned phrases to add the ones given.");
  			}
  
  			room.banwords = room.banwords.concat(words);
  			room.banwordRegex = null;
  			if (words.length > 1) {
  				this.privateModAction(`(The banwords ${words.map(w => `'${w}'`).join(', ')} were added by ${user.name}.)`);
  				this.modlog('BANWORD', null, words.map(w => `'${w}'`).join(', '));
  				this.sendReply(`Banned phrases successfully added.`);
  			} else {
  				this.privateModAction(`(The banword '${words[0]}' was added by ${user.name}.)`);
  				this.modlog('BANWORD', null, words[0]);
  				this.sendReply(`Banned phrase successfully added.`);
  			}
  			this.sendReply(`The list is currently: ${room.banwords.join(', ')}`);
  
  			if (room.chatRoomData) {
  				room.chatRoomData.banwords = room.banwords;
  				Rooms.global.writeChatRoomData();
  			}
  		},
  
  		delete(target, room, user) {
  			if (!target) return this.parse('/help banword');
  			if (!this.can('declare', null, room)) return false;
  
  			if (!room.banwords) return this.errorReply("This room has no banned phrases.");
  
  			let words = target.match(/[^,]+(,\d*}[^,]*)?/g);
  			if (!words) return this.parse('/help banword');
  
  			words = words.map(word => word.replace(/\n/g, '').trim());
  
  			for (let word of words) {
  				if (!room.banwords.includes(word)) return this.errorReply(`${word} is not a banned phrase in this room.`);
  			}
  
  			room.banwords = room.banwords.filter(w => !words.includes(w));
  			if (!room.banwords.length) room.banwords = null;
  			room.banwordRegex = null;
  			if (words.length > 1) {
  				this.privateModAction(`(The banwords ${words.map(w => `'${w}'`).join(', ')} were removed by ${user.name}.)`);
  				this.modlog('UNBANWORD', null, words.map(w => `'${w}'`).join(', '));
  				this.sendReply(`Banned phrases successfully deleted.`);
  			} else {
  				this.privateModAction(`(The banword '${words[0]}' was removed by ${user.name}.)`);
  				this.modlog('UNBANWORD', null, words[0]);
  				this.sendReply(`Banned phrase successfully deleted.`);
  			}
  			this.sendReply(room.banwords ? `The list is currently: ${room.banwords.join(', ')}` : `The list is now empty.`);
  
  			if (room.chatRoomData) {
  				room.chatRoomData.banwords = room.banwords;
  				if (!room.banwords) delete room.chatRoomData.banwords;
  				Rooms.global.writeChatRoomData();
  			}
  		},
  
  		list(target, room, user) {
  			if (!this.can('mute', null, room)) return false;
  
  			if (!room.banwords) return this.sendReply("This room has no banned phrases.");
  
  			return this.sendReply(`Banned phrases in room ${room.roomid}: ${room.banwords.join(', ')}`);
  		},
  
  		""(target, room, user) {
  			return this.parse("/help banword");
  		},
  	},
  	banwordhelp: [
  		`/banword add [words] - Adds the comma-separated list of phrases to the banword list of the current room. Requires: # & ~`,
  		`/banword addregex [words] - Adds the comma-separated list of regular expressions to the banword list of the current room. Requires & ~`,
  		`/banword delete [words] - Removes the comma-separated list of phrases from the banword list. Requires: # & ~`,
  		`/banword list - Shows the list of banned words in the current room. Requires: % @ # & ~`,
  	],
  
  	hightraffic(target, room, user) {
  		if (!target) return this.sendReply(`This room is${!room.highTraffic ? ' not' : ''} currently marked as high traffic.`);
  		if (!this.can('makeroom')) return false;
  
  		if (this.meansYes(target)) {
  			room.highTraffic = true;
  		} else if (this.meansNo(target)) {
  			room.highTraffic = false;
  		} else {
  			return this.parse('/help hightraffic');
  		}
  
  		if (room.chatRoomData) {
  			room.chatRoomData.highTraffic = room.highTraffic;
  			Rooms.global.writeChatRoomData();
  		}
  		this.modlog(`HIGHTRAFFIC`, null, room.highTraffic);
  		this.addModAction(`This room was marked as high traffic by ${user.name}.`);
  	},
  	hightraffichelp: [
  		`/hightraffic [true|false] - (Un)marks a room as a high traffic room. Requires & ~`,
  		`When a room is marked as high-traffic, PS requires all messages sent to that room to contain at least 2 letters.`,
  	],
  
  	/*********************************************************
  	 * Room management
  	 *********************************************************/
  
  	makeprivatechatroom: 'makechatroom',
  	makechatroom(target, room, user, connection, cmd) {
  		if (!this.can('makeroom')) return;
  
  		// `,` is a delimiter used by a lot of /commands
  		// `|` and `[` are delimiters used by the protocol
  		// `-` has special meaning in roomids
  		if (target.includes(',') || target.includes('|') || target.includes('[') || target.includes('-')) {
  			return this.errorReply("Room titles can't contain any of: ,|[-");
  		}
  
  		let id = toID(target);
  		if (!id) return this.parse('/help makechatroom');
  		if (id.length > MAX_CHATROOM_ID_LENGTH) return this.errorReply("The given room title is too long.");
  		// Check if the name already exists as a room or alias
  		if (Rooms.search(id)) return this.errorReply(`The room '${target}' already exists.`);
  		if (!Rooms.global.addChatRoom(target)) return this.errorReply(`An error occurred while trying to create the room '${target}'.`);
  
  		if (cmd === 'makeprivatechatroom') {
  			let targetRoom = Rooms.search(target);
  			targetRoom.isPrivate = true;
  			targetRoom.chatRoomData.isPrivate = true;
  			Rooms.global.writeChatRoomData();
  			if (Rooms.get('upperstaff')) {
  				Rooms.get('upperstaff').add(`|raw|<div class="broadcast-green">Private chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
  			}
  			this.sendReply(`The private chat room '${target}' was created.`);
  		} else {
  			if (Rooms.get('staff')) {
  				Rooms.get('staff').add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
  			}
  			if (Rooms.get('upperstaff')) {
  				Rooms.get('upperstaff').add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
  			}
  			this.sendReply(`The chat room '${target}' was created.`);
  		}
  	},
  	makechatroomhelp: [`/makechatroom [roomname] - Creates a new room named [roomname]. Requires: & ~`],
  
  	subroomgroupchat: 'makegroupchat',
  	makegroupchat(target, room, user, connection, cmd) {
  		if (!this.canTalk()) return;
  		if (!user.autoconfirmed) {
  			return this.errorReply("You must be autoconfirmed to make a groupchat.");
  		}
  		if (cmd === 'subroomgroupchat') {
  			if (!user.can('mute', null, room)) return this.errorReply("You can only create subroom groupchats for rooms you're staff in.");
  			if (room.battle) return this.errorReply("You cannot create a subroom of a battle.");
  			if (room.isPersonal) return this.errorReply("You cannot create a subroom of a groupchat.");
  		}
  		let parent = cmd === 'subroomgroupchat' ? room.roomid : null;
  		// if (!this.can('makegroupchat')) return false;
  
  		// Title defaults to a random 8-digit number.
  		let title = target.trim();
  		if (title.length >= 32) {
  			return this.errorReply("Title must be under 32 characters long.");
  		} else if (!title) {
  			title = (`${Math.floor(Math.random() * 100000000)}`);
  		} else if (Config.chatfilter) {
  			let filterResult = Config.chatfilter.call(this, title, user, null, connection);
  			if (!filterResult) return;
  			if (title !== filterResult) {
  				return this.errorReply("Invalid title.");
  			}
  		}
  		// `,` is a delimiter used by a lot of /commands
  		// `|` and `[` are delimiters used by the protocol
  		// `-` has special meaning in roomids
  		if (title.includes(',') || title.includes('|') || title.includes('[') || title.includes('-')) {
  			return this.errorReply("Room titles can't contain any of: ,|[-");
  		}
  
  		// Even though they're different namespaces, to cut down on confusion, you
  		// can't share names with registered chatrooms.
  		let existingRoom = Rooms.search(toID(title));
  		if (existingRoom && !existingRoom.modjoin) return this.errorReply(`The room '${title}' already exists.`);
  		// Room IDs for groupchats are groupchat-TITLEID
  		let titleid = toID(title);
  		if (!titleid) {
  			titleid = `${Math.floor(Math.random() * 100000000)}`;
  		}
  		let roomid = `groupchat-${parent || user.id}-${titleid}`;
  		// Titles must be unique.
  		if (Rooms.search(roomid)) return this.errorReply(`A group chat named '${title}' already exists.`);
  		// Tab title is prefixed with '[G]' to distinguish groupchats from
  		// registered chatrooms
  
  		if (Monitor.countGroupChat(connection.ip)) {
  			this.errorReply("Due to high load, you are limited to creating 4 group chats every hour.");
  			return;
  		}
  
  		let titleMsg = Chat.html `Welcome to ${parent ? room.title : user.name}'s${!/^[0-9]+$/.test(title) ? ` ${title}` : ''}${parent ? ' subroom' : ''} groupchat!`;
  		let targetRoom = Rooms.createChatRoom(roomid, `[G] ${title}`, {
  			isPersonal: true,
  			isPrivate: 'hidden',
  			creationTime: parent ? null : Date.now(),
  			modjoin: parent ? null : '+',
  			parentid: parent,
  			auth: {},
  			introMessage: `<div style="text-align: center"><table style="margin:auto;"><tr><td><img src="//${Config.routes.client}/fx/groupchat.png" width=120 height=100></td><td><h2>${titleMsg}</h2><p>Follow the <a href="/rules">Pokémon Showdown Global Rules</a>!<br>Don't be disruptive to the rest of the site.</p></td></tr></table></div>`,
  			staffMessage: `<p>Groupchats are temporary rooms, and will expire if there hasn't been any activity in 40 minutes.</p><p>You can invite new users using <code>/invite</code>. Be careful with who you invite!</p><p>Commands: <button class="button" name="send" value="/roomhelp">Room Management</button> | <button class="button" name="send" value="/roomsettings">Room Settings</button> | <button class="button" name="send" value="/tournaments help">Tournaments</button></p><p>As creator of this groupchat, <u>you are entirely responsible for what occurs in this chatroom</u>. Global rules apply at all times.</p><p>If this room is used to break global rules or disrupt other areas of the server, <strong>you as the creator will be held accountable and punished</strong>.</p>`,
  		});
  		if (targetRoom) {
  			// The creator is a Room Owner in subroom groupchats and a Host otherwise..
  			targetRoom.auth[user.id] = parent ? '#' : Users.HOST_SYMBOL;
  			// Join after creating room. No other response is given.
  			user.joinRoom(targetRoom.roomid);
  			user.popup(`You've just made a groupchat; it is now your responsibility, regardless of whether or not you actively partake in the room. For more info, read your groupchat's staff intro.`);
  			if (parent) this.modlog('SUBROOMGROUPCHAT', null, title);
  			return;
  		}
  		return this.errorReply(`An unknown error occurred while trying to create the room '${title}'.`);
  	},
  	makegroupchathelp: [
  		`/makegroupchat [roomname] - Creates an invite-only group chat named [roomname].`,
  		`/subroomgroupchat [roomname] - Creates a subroom groupchat of the current room. Can only be used in a public room you have staff in.`,
  	],
  
  	'!groupchatuptime': true,
  	groupchatuptime(target, room, user) {
  		if (!room || !room.creationTime) return this.errorReply("Can only be used in a groupchat.");
  		if (!this.runBroadcast()) return;
  		const uptime = Chat.toDurationString(Date.now() - room.creationTime);
  		this.sendReplyBox(`Groupchat uptime: <b>${uptime}</b>`);
  	},
  	groupchatuptimehelp: [`/groupchatuptime - Displays the uptime if the current room is a groupchat.`],
  
  	deregisterchatroom(target, room, user) {
  		if (!this.can('makeroom')) return;
  		this.errorReply("NOTE: You probably want to use `/deleteroom` now that it exists.");
  		let id = toID(target);
  		if (!id) return this.parse('/help deregisterchatroom');
  		let targetRoom = Rooms.search(id);
  		if (!targetRoom) return this.errorReply(`The room '${target}' doesn't exist.`);
  		target = targetRoom.title || targetRoom.roomid;
  		const isPrivate = targetRoom.isPrivate;
  		const staffRoom = Rooms.get('staff');
  		const upperStaffRoom = Rooms.get('upperstaff');
  		if (Rooms.global.deregisterChatRoom(id)) {
  			this.sendReply(`The room '${target}' was deregistered.`);
  			this.sendReply("It will be deleted as of the next server restart.");
  			target = Chat.escapeHTML(target);
  			if (isPrivate) {
  				if (upperStaffRoom) upperStaffRoom.add(`|raw|<div class="broadcast-red">Private chat room deregistered by ${user.id}: <b>${target}</b></div>`).update();
  			} else {
  				if (staffRoom) staffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered: <b>${target}</b></div>`).update();
  				if (upperStaffRoom) upperStaffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered by ${user.id}: <b>${target}</b></div>`).update();
  			}
  			return;
  		}
  		return this.errorReply(`The room "${target}" isn't registered.`);
  	},
  	deregisterchatroomhelp: [`/deregisterchatroom [roomname] - Deletes room [roomname] after the next server restart. Requires: & ~`],
  
  	deletechatroom: 'deleteroom',
  	deletegroupchat: 'deleteroom',
  	deleteroom(target, room, user, connection, cmd) {
  		let roomid = target.trim();
  		if (!roomid) {
  			// allow deleting personal rooms without typing out the room name
  			if (!room.isPersonal || cmd !== "deletegroupchat") {
  				return this.parse(`/help deleteroom`);
  			}
  		} else {
  			let targetRoom = Rooms.search(roomid);
  			if (targetRoom !== room) {
  				return this.parse(`/help deleteroom`);
  			}
  		}
  
  		if (room.isPersonal) {
  			if (!this.can('gamemanagement', null, room)) return;
  		} else {
  			if (!this.can('makeroom')) return;
  		}
  
  		const title = room.title || room.roomid;
  
  		if (room.roomid === 'global') {
  			return this.errorReply(`This room can't be deleted.`);
  		}
  
  		if (room.chatRoomData) {
  			if (room.isPrivate) {
  				if (Rooms.get('upperstaff')) {
  					Rooms.get('upperstaff').add(Chat.html`|raw|<div class="broadcast-red">Private chat room deleted by ${user.id}: <b>${title}</b></div>`).update();
  				}
  			} else {
  				if (Rooms.get('staff')) {
  					Rooms.get('staff').add(Chat.html`|raw|<div class="broadcast-red">Public chat room deleted: <b>${title}</b></div>`).update();
  				}
  				if (Rooms.get('upperstaff')) {
  					Rooms.get('upperstaff').add(Chat.html`|raw|<div class="broadcast-red">Public chat room deleted by ${user.id}: <b>${title}</b></div>`).update();
  				}
  			}
  		}
  
  		if (room.subRooms) {
  			for (const subRoom of room.subRooms.values()) subRoom.parent = null;
  		}
  
  		room.add(`|raw|<div class="broadcast-red"><b>This room has been deleted.</b></div>`);
  		room.update(); // |expire| needs to be its own message
  		room.add(`|expire|This room has been deleted.`);
  		this.sendReply(`The room "${title}" was deleted.`);
  		room.update();
  		room.destroy();
  	},
  	deleteroomhelp: [
  		`/deleteroom [roomname] - Deletes room [roomname]. Must be typed in the room to delete. Requires: & ~`,
  		`/deletegroupchat - Deletes the current room, if it's a groupchat. Requires: ★ # & ~`,
  	],
  
  	hideroom: 'privateroom',
  	hiddenroom: 'privateroom',
  	secretroom: 'privateroom',
  	publicroom: 'privateroom',
  	privateroom(target, room, user, connection, cmd) {
  		if (room.isPersonal) {
  			if (!this.can('editroom', null, room)) return;
  		} else if (room.battle) {
  			if (!this.can('editprivacy', null, room)) return;
  			const prefix = room.battle.forcedPublic();
  			if (prefix && !user.can('editprivacy')) return this.errorReply(`This battle is required to be public due to a player having a name prefixed by '${prefix}'.`);
  		} else {
  			// registered chatrooms show up on the room list and so require
  			// higher permissions to modify privacy settings
  			if (!this.can('makeroom')) return;
  		}
  		let setting;
  		switch (cmd) {
  		case 'privateroom':
  			return this.parse('/help privateroom');
  		case 'publicroom':
  			setting = false;
  			break;
  		case 'secretroom':
  			setting = true;
  			break;
  		default:
  			if (room.isPrivate === true && target !== 'force') {
  				return this.sendReply(`This room is a secret room. Use "/publicroom" to make it public, or "/hiddenroom force" to force it hidden.`);
  			}
  			setting = 'hidden';
  			break;
  		}
  
  		if ((setting === true || room.isPrivate === true) && !room.isPersonal) {
  			if (!this.can('makeroom')) return;
  		}
  
  		if (this.meansNo(target) || !setting) {
  			if (!room.isPrivate) {
  				return this.errorReply(`This room is already public.`);
  			}
  			if (room.parent && room.parent.isPrivate) {
  				return this.errorReply(`This room's parent ${room.parent.title} must be public for this room to be public.`);
  			}
  			if (room.isPersonal) return this.errorReply(`This room can't be made public.`);
  			if (room.privacySetter && user.can('nooverride', null, room) && !user.can('makeroom')) {
  				if (!room.privacySetter.has(user.id)) {
  					const privacySetters = [...room.privacySetter].join(', ');
  					return this.errorReply(`You can't make the room public since you didn't make it private - only ${privacySetters} can.`);
  				}
  				room.privacySetter.delete(user.id);
  				if (room.privacySetter.size) {
  					const privacySetters = [...room.privacySetter].join(', ');
  					return this.sendReply(`You are no longer forcing the room to stay private, but ${privacySetters} also need${Chat.plural(room.privacySetter, "", "s")} to use /publicroom to make the room public.`);
  				}
  			}
  			delete room.isPrivate;
  			room.privacySetter = null;
  			this.addModAction(`${user.name} made this room public.`);
  			this.modlog('PUBLICROOM');
  			if (room.chatRoomData) {
  				delete room.chatRoomData.isPrivate;
  				Rooms.global.writeChatRoomData();
  			}
  		} else {
  			const settingName = (setting === true ? 'secret' : setting);
  			if (room.subRooms) {
  				if (settingName === 'secret') return this.errorReply("Secret rooms cannot have subrooms.");
  				for (const subRoom of room.subRooms.values()) {
  					if (!subRoom.isPrivate) return this.errorReply(`Subroom ${subRoom.title} must be private to make this room private.`);
  				}
  			}
  			if (room.isPrivate === setting) {
  				if (room.privacySetter && !room.privacySetter.has(user.id)) {
  					room.privacySetter.add(user.id);
  					return this.sendReply(`This room is already ${settingName}, but is now forced to stay that way until you use /publicroom.`);
  				}
  				return this.errorReply(`This room is already ${settingName}.`);
  			}
  			room.isPrivate = setting;
  			this.addModAction(`${user.name} made this room ${settingName}.`);
  			this.modlog(`${settingName.toUpperCase()}ROOM`);
  			if (room.chatRoomData) {
  				room.chatRoomData.isPrivate = setting;
  				Rooms.global.writeChatRoomData();
  			}
  			room.privacySetter = new Set([user.id]);
  		}
  	},
  	privateroomhelp: [
  		`/secretroom - Makes a room secret. Secret rooms are visible to & and up. Requires: & ~`,
  		`/hiddenroom [on/off] - Makes a room hidden. Hidden rooms are visible to % and up, and inherit global ranks. Requires: \u2606 & ~`,
  		`/publicroom - Makes a room public. Requires: \u2606 & ~`,
  	],
  
  	officialchatroom: 'officialroom',
  	officialroom(target, room, user) {
  		if (!this.can('makeroom')) return;
  		if (!room.chatRoomData) {
  			return this.errorReply(`/officialroom - This room can't be made official`);
  		}
  		if (this.meansNo(target)) {
  			if (!room.isOfficial) return this.errorReply(`This chat room is already unofficial.`);
  			delete room.isOfficial;
  			this.addModAction(`${user.name} made this chat room unofficial.`);
  			this.modlog('UNOFFICIALROOM');
  			delete room.chatRoomData.isOfficial;
  			Rooms.global.writeChatRoomData();
  		} else {
  			if (room.isOfficial) return this.errorReply(`This chat room is already official.`);
  			room.isOfficial = true;
  			this.addModAction(`${user.name} made this chat room official.`);
  			this.modlog('OFFICIALROOM');
  			room.chatRoomData.isOfficial = true;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	psplwinnerroom(target, room, user) {
  		if (!this.can('makeroom')) return;
  		if (!room.chatRoomData) {
  			return this.errorReply(`/psplwinnerroom - This room can't be marked as a PSPL Winner room`);
  		}
  		if (this.meansNo(target)) {
  			if (!room.pspl) return this.errorReply(`This chat room is already not a PSPL Winner room.`);
  			delete room.pspl;
  			this.addModAction(`${user.name} made this chat room no longer a PSPL Winner room.`);
  			this.modlog('PSPLROOM');
  			delete room.chatRoomData.pspl;
  			Rooms.global.writeChatRoomData();
  		} else {
  			if (room.pspl) return this.errorReply("This chat room is already a PSPL Winner room.");
  			room.pspl = true;
  			this.addModAction(`${user.name} made this chat room a PSPL Winner room.`);
  			this.modlog('UNPSPLROOM');
  			room.chatRoomData.pspl = true;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	setsubroom: 'subroom',
  	subroom(target, room, user) {
  		if (!user.can('makeroom')) return this.errorReply(`/subroom - Access denied. Did you mean /subrooms?`);
  		if (!target) return this.parse('/help subroom');
  
  		if (!room.chatRoomData) return this.errorReply(`Temporary rooms cannot be subrooms.`);
  		if (room.parent) return this.errorReply(`This room is already a subroom. To change which room this subroom belongs to, remove the subroom first.`);
  		if (room.subRooms) return this.errorReply(`This room is already a parent room, and a parent room cannot be made as a subroom.`);
  
  		const main = Rooms.search(target);
  
  		if (!main) return this.errorReply(`The room '${target}' does not exist.`);
  		if (main.parent) return this.errorReply(`Subrooms cannot have subrooms.`);
  		if (main.isPrivate === true) return this.errorReply(`Only public and hidden rooms can have subrooms.`);
  		if (main.isPrivate && !room.isPrivate) return this.errorReply(`Private rooms cannot have public subrooms.`);
  		if (!main.chatRoomData) return this.errorReply(`Temporary rooms cannot be parent rooms.`);
  		if (room === main) return this.errorReply(`You cannot set a room to be a subroom of itself.`);
  
  		room.parent = main;
  		if (!main.subRooms) main.subRooms = new Map();
  		main.subRooms.set(room.roomid, room);
  
  		const mainIdx = Rooms.global.chatRoomDataList.findIndex(r => r.title === main.title);
  		const subIdx = Rooms.global.chatRoomDataList.findIndex(r => r.title === room.title);
  
  		// This is needed to ensure that the main room gets loaded before the subroom.
  		if (mainIdx > subIdx) {
  			const tmp = Rooms.global.chatRoomDataList[mainIdx];
  			Rooms.global.chatRoomDataList[mainIdx] = Rooms.global.chatRoomDataList[subIdx];
  			Rooms.global.chatRoomDataList[subIdx] = tmp;
  		}
  
  		room.chatRoomData.parentid = main.roomid;
  		Rooms.global.writeChatRoomData();
  
  		for (let userid in room.users) {
  			room.users[userid].updateIdentity(room.roomid);
  		}
  
  		this.modlog('SUBROOM', null, `of ${main.title}`);
  		return this.addModAction(`This room was set as a subroom of ${main.title} by ${user.name}.`);
  	},
  
  	removesubroom: 'unsubroom',
  	desubroom: 'unsubroom',
  	unsubroom(target, room, user) {
  		if (!this.can('makeroom')) return;
  		if (!room.parent || !room.chatRoomData) return this.errorReply(`This room is not currently a subroom of a public room.`);
  
  		const parent = room.parent;
  		if (parent && parent.subRooms) {
  			parent.subRooms.delete(room.roomid);
  			if (!parent.subRooms.size) parent.subRooms = null;
  		}
  
  		room.parent = null;
  
  		delete room.chatRoomData.parentid;
  		Rooms.global.writeChatRoomData();
  
  		for (let userid in room.users) {
  			room.users[userid].updateIdentity(room.roomid);
  		}
  
  		this.modlog('UNSUBROOM');
  		return this.addModAction(`This room was unset as a subroom by ${user.name}.`);
  	},
  
  	parentroom: 'subrooms',
  	subrooms(target, room, user, connection, cmd) {
  		if (cmd === 'parentroom') {
  			if (!room.parent) return this.errorReply(`This room is not a subroom.`);
  			return this.sendReply(`This is a subroom of ${room.parent.title}.`);
  		}
  		if (!room.chatRoomData) return this.errorReply(`Temporary rooms cannot have subrooms.`);
  
  		if (!this.runBroadcast()) return;
  
  		const showSecret = !this.broadcasting && user.can('mute', null, room);
  
  		const subRooms = room.getSubRooms(showSecret);
  
  		if (!subRooms.length) return this.sendReply(`This room doesn't have any subrooms.`);
  
  		const subRoomText = subRooms.map(room => Chat.html`<a href="/${room.roomid}">${room.title}</a><br/><small>${room.desc}</small>`);
  
  		return this.sendReplyBox(`<p style="font-weight:bold;">${Chat.escapeHTML(room.title)}'s subroom${Chat.plural(subRooms)}:</p><ul><li>${subRoomText.join('</li><br/><li>')}</li></ul></strong>`);
  	},
  
  	subroomhelp: [
  		`/subroom [room] - Marks the current room as a subroom of [room]. Requires: & ~`,
  		`/unsubroom - Unmarks the current room as a subroom. Requires: & ~`,
  		`/subrooms - Displays the current room's subrooms.`,
  		`/parentroom - Displays the current room's parent room.`,
  	],
  
  	roomdesc(target, room, user) {
  		if (!target) {
  			if (!this.runBroadcast()) return;
  			if (!room.desc) return this.sendReply(`This room does not have a description set.`);
  			this.sendReplyBox(Chat.html`The room description is: ${room.desc}`);
  			return;
  		}
  		if (!this.can('declare')) return false;
  		if (target.length > 80) return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`);
  		let normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' ';
  
  		if (normalizedTarget.includes(' welcome ')) {
  			return this.errorReply(`Error: Room description must not contain the word "welcome".`);
  		}
  		if (normalizedTarget.slice(0, 9) === ' discuss ') {
  			return this.errorReply(`Error: Room description must not start with the word "discuss".`);
  		}
  		if (normalizedTarget.slice(0, 12) === ' talk about ' || normalizedTarget.slice(0, 17) === ' talk here about ') {
  			return this.errorReply(`Error: Room description must not start with the phrase "talk about".`);
  		}
  
  		room.desc = target;
  		this.sendReply(`(The room description is now: ${target})`);
  
  		this.privateModAction(`(${user.name} changed the roomdesc to: "${target}".)`);
  		this.modlog('ROOMDESC', null, `to "${target}"`);
  
  		if (room.chatRoomData) {
  			room.chatRoomData.desc = room.desc;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	topic: 'roomintro',
  	roomintro(target, room, user, connection, cmd) {
  		if (!target) {
  			if (!this.runBroadcast()) return;
  			if (!room.introMessage) return this.sendReply("This room does not have an introduction set.");
  			this.sendReply('|raw|<div class="infobox infobox-limited">' + room.introMessage.replace(/\n/g, '') + '</div>');
  			if (!this.broadcasting && user.can('declare', null, room) && cmd !== 'topic') {
  				this.sendReply('Source:');
  				const code = Chat.escapeHTML(room.introMessage).replace(/\n/g, '<br />');
  				this.sendReplyBox(`<code style="white-space: pre-wrap">/roomintro ${code}</code>`);
  			}
  			return;
  		}
  		if (!this.can('editroom', null, room)) return false;
  		if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deleteroomintro"?');
  		target = this.canHTML(target);
  		if (!target) return;
  		if (!/</.test(target)) {
  			// not HTML, do some simple URL linking
  			let re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
  			target = target.replace(re, '<a href="$1">$1</a>');
  		}
  		if (target.substr(0, 11) === '/roomintro ') target = target.substr(11);
  
  		room.introMessage = target.replace(/\r/g, '');
  		this.sendReply("(The room introduction has been changed to:)");
  		this.sendReply(`|raw|<div class="infobox infobox-limited">${room.introMessage.replace(/\n/g, '')}</div>`);
  
  		this.privateModAction(`(${user.name} changed the roomintro.)`);
  		this.modlog('ROOMINTRO');
  		this.roomlog(room.introMessage.replace(/\n/g, ''));
  
  		if (room.chatRoomData) {
  			room.chatRoomData.introMessage = room.introMessage;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	deletetopic: 'deleteroomintro',
  	deleteroomintro(target, room, user) {
  		if (!this.can('declare', null, room)) return false;
  		if (!room.introMessage) return this.errorReply("This room does not have a introduction set.");
  
  		this.privateModAction(`(${user.name} deleted the roomintro.)`);
  		this.modlog('DELETEROOMINTRO');
  		this.roomlog(target);
  
  		delete room.introMessage;
  		if (room.chatRoomData) {
  			delete room.chatRoomData.introMessage;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	stafftopic: 'staffintro',
  	staffintro(target, room, user, connection, cmd) {
  		if (!target) {
  			if (!this.can('mute', null, room)) return false;
  			if (!room.staffMessage) return this.sendReply("This room does not have a staff introduction set.");
  			this.sendReply(`|raw|<div class="infobox">${room.staffMessage.replace(/\n/g, ``)}</div>`);
  			if (user.can('ban', null, room) && cmd !== 'stafftopic') {
  				this.sendReply('Source:');
  				const code = Chat.escapeHTML(room.staffMessage).replace(/\n/g, '<br />');
  				this.sendReplyBox(`<code style="white-space: pre-wrap">/staffintro ${code}</code>`);
  			}
  			return;
  		}
  		if (!this.can('ban', null, room)) return false;
  		if (!this.canTalk()) return;
  		if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deletestaffintro"?');
  		target = this.canHTML(target);
  		if (!target) return;
  		if (!/</.test(target)) {
  			// not HTML, do some simple URL linking
  			let re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
  			target = target.replace(re, '<a href="$1">$1</a>');
  		}
  		if (target.substr(0, 12) === '/staffintro ') target = target.substr(12);
  
  		room.staffMessage = target.replace(/\r/g, '');
  		this.sendReply("(The staff introduction has been changed to:)");
  		this.sendReply(`|raw|<div class="infobox">${target.replace(/\n/g, ``)}</div>`);
  
  		this.privateModAction(`(${user.name} changed the staffintro.)`);
  		this.modlog('STAFFINTRO');
  		this.roomlog(room.staffMessage.replace(/\n/g, ``));
  
  		if (room.chatRoomData) {
  			room.chatRoomData.staffMessage = room.staffMessage;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	deletestafftopic: 'deletestaffintro',
  	deletestaffintro(target, room, user) {
  		if (!this.can('ban', null, room)) return false;
  		if (!room.staffMessage) return this.errorReply("This room does not have a staff introduction set.");
  
  		this.privateModAction(`(${user.name} deleted the staffintro.)`);
  		this.modlog('DELETESTAFFINTRO');
  		this.roomlog(target);
  
  		delete room.staffMessage;
  		if (room.chatRoomData) {
  			delete room.chatRoomData.staffMessage;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  
  	roomalias(target, room, user) {
  		if (!target) {
  			if (!this.runBroadcast()) return;
  			if (!room.aliases || !room.aliases.length) return this.sendReplyBox("This room does not have any aliases.");
  			return this.sendReplyBox(`This room has the following aliases: ${room.aliases.join(", ")}`);
  		}
  		if (!this.can('makeroom')) return false;
  		if (target.includes(',')) {
  			this.errorReply(`Invalid room alias: ${target.trim()}`);
  			return this.parse('/help roomalias');
  		}
  
  		let alias = toID(target);
  		if (!alias.length) return this.errorReply("Only alphanumeric characters are valid in an alias.");
  		if (Rooms.get(alias) || Rooms.aliases.has(alias)) return this.errorReply("You cannot set an alias to an existing room or alias.");
  		if (room.isPersonal) return this.errorReply("Personal rooms can't have aliases.");
  
  		Rooms.aliases.set(alias, room.roomid);
  		this.privateModAction(`(${user.name} added the room alias '${alias}'.)`);
  		this.modlog('ROOMALIAS', null, alias);
  
  		if (!room.aliases) room.aliases = [];
  		room.aliases.push(alias);
  		if (room.chatRoomData) {
  			room.chatRoomData.aliases = room.aliases;
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	roomaliashelp: [
  		`/roomalias - displays a list of all room aliases of the room the command was entered in.`,
  		`/roomalias [alias] - adds the given room alias to the room the command was entered in. Requires: & ~`,
  		`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: & ~`,
  	],
  
  	deleteroomalias: 'removeroomalias',
  	deroomalias: 'removeroomalias',
  	unroomalias: 'removeroomalias',
  	removeroomalias(target, room, user) {
  		if (!room.aliases) return this.errorReply("This room does not have any aliases.");
  		if (!this.can('makeroom')) return false;
  		if (target.includes(',')) {
  			this.errorReply(`Invalid room alias: ${target.trim()}`);
  			return this.parse('/help removeroomalias');
  		}
  
  		let alias = toID(target);
  		if (!alias || !Rooms.aliases.has(alias)) return this.errorReply("Please specify an existing alias.");
  		if (Rooms.aliases.get(alias) !== room.roomid) return this.errorReply("You may only remove an alias from the current room.");
  
  		this.privateModAction(`(${user.name} removed the room alias '${alias}'.)`);
  		this.modlog('REMOVEALIAS', null, alias);
  
  		let aliasIndex = room.aliases.indexOf(alias);
  		if (aliasIndex >= 0) {
  			room.aliases.splice(aliasIndex, 1);
  			Rooms.aliases.delete(alias);
  			Rooms.global.writeChatRoomData();
  		}
  	},
  	removeroomaliashelp: [`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: & ~`],
  
  };
  