<commit_before>/**
 * Informational Commands
 * Pokemon Showdown - https://pokemonshowdown.com/
 *
 * These are informational commands. For instance, you can define the command
 * 'whois' here, then use it by typing /whois into Pokemon Showdown.
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT
 */

'use strict';

const net = require('net');

/** @type {ChatCommands} */
const commands = {

	'!whois': true,
	ip: 'whois',
	rooms: 'whois',
	alt: 'whois',
	alts: 'whois',
	whoare: 'whois',
	whois(target, room, user, connection, cmd) {
		if (room && room.roomid === 'staff' && !this.runBroadcast()) return;
		if (!room) room = Rooms.global;
		let targetUser = this.targetUserOrSelf(target, user.group === ' ');
		let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
		if (!targetUser) {
			if (showAll) return this.parse('/offlinewhois ' + target);
			return this.errorReply("User " + this.targetUsername + " not found.");
		}
		if (showAll && !user.trusted && targetUser !== user) {
			return this.errorReply(`/${cmd} - Access denied.`);
		}

		let buf = Chat.html`<strong class="username"><small style="display:none">${targetUser.group}</small>${targetUser.name}</strong> `;
		const ac = targetUser.autoconfirmed;
		if (ac && showAll) buf += ` <small style="color:gray">(ac${targetUser.id === ac ? `` : `: <span class="username">${ac}</span>`})</small>`;
		const trusted = targetUser.trusted;
		if (trusted && showAll) {
			buf += ` <small style="color:gray">(trusted${targetUser.id === trusted ? `` : `: <span class="username">${trusted}</span>`})</small>`;
		}
		if (!targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;
		let roomauth = '';
		if (room.auth && targetUser.id in room.auth) roomauth = room.auth[targetUser.id];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) {
			buf += `<br />Global ${Config.groups[targetUser.group].name} (${targetUser.group})`;
		}
		if (targetUser.isSysop) {
			buf += `<br />(Pok&eacute;mon Showdown System Operator)`;
		}
		if (!targetUser.registered) {
			buf += `<br />(Unregistered)`;
		}
		let publicrooms = "";
		let hiddenrooms = "";
		let privaterooms = "";
		for (const roomid of targetUser.inRooms) {
			if (roomid === 'global') continue;
			let targetRoom = Rooms.get(roomid);

			let authSymbol = (targetRoom.auth && targetRoom.auth[targetUser.id] ? targetRoom.auth[targetUser.id] : '');
			let battleTitle = (roomid.battle ? ` title="${roomid.title}"` : '');
			let output = `${authSymbol}<a href="/${roomid}"${battleTitle}>${roomid}</a>`;
			if (targetRoom.isPrivate === true) {
				if (targetRoom.modjoin === '~') continue;
				if (privaterooms) privaterooms += " | ";
				privaterooms += output;
			} else if (targetRoom.isPrivate) {
				if (hiddenrooms) hiddenrooms += " | ";
				hiddenrooms += output;
			} else {
				if (publicrooms) publicrooms += " | ";
				publicrooms += output;
			}
		}
		buf += '<br />Rooms: ' + (publicrooms || '<em>(no public rooms)</em>');

		if (!showAll) {
			return this.sendReplyBox(buf);
		}
		const canViewAlts = (user === targetUser || user.can('alts', targetUser));
		const canViewPunishments = canViewAlts || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.id in room.users);
		const canViewSecretRooms = user === targetUser || (canViewAlts && targetUser.locked) || user.can('makeroom');
		buf += '<br />';

		if (canViewAlts) {
			let prevNames = Object.keys(targetUser.prevNames).map(userid => {
				const punishment = Punishments.userids.get(userid);
				return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.id ? ` as ${punishment[1]}` : ''})` : '');
			}).join(", ");
			if (prevNames) buf += Chat.html`<br />Previous names: ${prevNames}`;

			for (const targetAlt of targetUser.getAltUsers(true)) {
				if (!targetAlt.named && !targetAlt.connected) continue;
				if (targetAlt.group === '~' && user.group !== '~') continue;

				const punishment = Punishments.userids.get(targetAlt.id);
				const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.id ? ` as ${punishment[1]}` : ''})` : '';
				buf += Chat.html`<br />Alt: <span class="username">${targetAlt.name}</span>${punishMsg}`;
				if (!targetAlt.connected) buf += ` <em style="color:gray">(offline)</em>`;
				prevNames = Object.keys(targetAlt.prevNames).map(userid => {
					const punishment = Punishments.userids.get(userid);
					return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.id ? ` as ${punishment[1]}` : ''})` : '');
				}).join(", ");
				if (prevNames) buf += `<br />Previous names: ${prevNames}`;
			}
		}
		if (canViewPunishments) {
			if (targetUser.namelocked) {
				buf += `<br />NAMELOCKED: ${targetUser.namelocked}`;
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			} else if (targetUser.locked) {
				buf += `<br />LOCKED: ${targetUser.locked}`;
				switch (targetUser.locked) {
				case '#rangelock':
					buf += ` - IP or host is in a temporary range-lock`;
					break;
				case '#hostfilter':
					buf += ` - host is permanently locked for being a proxy`;
					break;
				}
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			}
			let battlebanned = Punishments.isBattleBanned(targetUser);
			if (battlebanned) {
				buf += `<br />BATTLEBANNED: ${battlebanned[1]}`;
				let expiresIn = new Date(battlebanned[2]).getTime() - Date.now();
				let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
				let expiresText = '';
				if (expiresDays >= 1) {
					expiresText = `in around ${Chat.count(expiresDays, "days")}`;
				} else {
					expiresText = `soon`;
				}
				if (expiresIn > 1) buf += ` (expires ${expiresText})`;
				if (battlebanned[3]) buf += Chat.html` (reason: ${battlebanned[3]})`;
			}
			if (targetUser.semilocked) {
				buf += `<br />Semilocked: ${targetUser.semilocked}`;
			}
		}
		if (user.can('ip', targetUser)) {
			let ips = Object.keys(targetUser.ips);
			ips = ips.map(ip => {
				let status = [];
				let punishment = Punishments.ips.get(ip);
				if (user.can('ip') && punishment) {
					let [punishType, userid] = punishment;
					let punishMsg = Punishments.punishmentTypes.get(punishType) || 'punished';
					if (userid !== targetUser.id) punishMsg += ` as ${userid}`;
					status.push(punishMsg);
				}
				if (Punishments.sharedIps.has(ip)) {
					let sharedStr = 'shared';
					if (Punishments.sharedIps.get(ip)) {
						sharedStr += `: ${Punishments.sharedIps.get(ip)}`;
					}
					status.push(sharedStr);
				}
				return `<a href="https://whatismyipaddress.com/ip/${ip}" target="_blank">${ip}</a>` + (status.length ? ` (${status.join('; ')})` : '');
			});
			buf += `<br /> IP${Chat.plural(ips)}: ${ips.join(", ")}`;
			if (user.group !== ' ' && targetUser.latestHost) {
				buf += Chat.html`<br />Host: ${targetUser.latestHost} [${targetUser.latestHostType}]`;
			}
		} else if (user === targetUser) {
			buf += `<br /> IP: <a href="https://whatismyipaddress.com/ip/${connection.ip}" target="_blank">${connection.ip}</a>`;
		}
		if (canViewAlts && hiddenrooms) {
			buf += `<br />Hidden rooms: ${hiddenrooms}`;
		}
		if (canViewSecretRooms && privaterooms) {
			buf += `<br />Secret rooms: ${privaterooms}`;
		}

		let gameRooms = [];
		for (const room of Rooms.rooms.values()) {
			if (!room.game) continue;
			if ((targetUser.id in room.game.playerTable && !targetUser.inRooms.has(room.roomid)) ||
				(room.auth && room.auth[targetUser.id] === Users.PLAYER_SYMBOL)) {
				if (room.isPrivate && !canViewAlts) {
					continue;
				}
				gameRooms.push(room.roomid);
			}
		}
		if (gameRooms.length) {
			buf += '<br />Recent games: ' + gameRooms.map(id => {
				let shortId = id.startsWith('battle-') ? id.slice(7) : id;
				return Chat.html`<a href="/${id}">${shortId}</a>`;
			}).join(' | ');
		}

		if (canViewPunishments) {
			let punishments = Punishments.getRoomPunishments(targetUser, {checkIps: true});

			if (punishments.length) {
				buf += `<br />Room punishments: `;

				buf += punishments.map(([room, punishment]) => {
					const [punishType, punishUserid, expireTime, reason] = punishment;
					let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
					if (!punishDesc) punishDesc = `punished`;
					if (punishUserid !== targetUser.id) punishDesc += ` as ${punishUserid}`;
					let expiresIn = new Date(expireTime).getTime() - Date.now();
					let expireString = Chat.toDurationString(expiresIn, {precision: 1});
					punishDesc += ` for ${expireString}`;

					if (reason) punishDesc += `: ${reason}`;
					return `<a href="/${room}">${room}</a> (${punishDesc})`;
				}).join(', ');
			}
		}
		this.sendReplyBox(buf);
	},
	whoishelp: [
		`/whois - Get details on yourself: alts, group, IP address, and rooms.`,
		`/whois [username] - Get details on a username: alts (Requires: % @ & ~), group, IP address (Requires: @ & ~), and rooms.`,
	],

	'!offlinewhois': true,
	'chp': 'offlinewhois',
	checkpunishment: 'offlinewhois',
	offlinewhois(target, room, user) {
		if (!user.trusted) {
			return this.errorReply("/offlinewhois - Access denied.");
		}
		let userid = toID(target);
		if (!userid) return this.errorReply("Please enter a valid username.");
		let targetUser = Users.get(userid);
		let buf = Chat.html`<strong class="username">${target}</strong>`;
		if (!targetUser || !targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;

		let roomauth = '';
		if (room && room.auth && userid in room.auth) roomauth = room.auth[userid];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		let group = (Users.usergroups[userid] || '').charAt(0);
		if (Config.groups[group] && Config.groups[group].name) {
			buf += `<br />Global ${Config.groups[group].name} (${group})`;
		}

		buf += `<br /><br />`;
		let atLeastOne = false;

		let punishment = Punishments.userids.get(userid);
		if (punishment) {
			const [punishType, punishUserid, , reason] = punishment;
			const punishName = (Punishments.punishmentTypes.get(punishType) || punishType).toUpperCase();
			buf += `${punishName}: ${punishUserid}`;
			let expiresIn = Punishments.checkLockExpiration(userid);
			if (expiresIn) buf += expiresIn;
			if (reason) buf += Chat.html` (reason: ${reason})`;
			buf += '<br />';
			atLeastOne = true;
		}

		if (!user.can('alts') && !atLeastOne) {
			let hasJurisdiction = room && user.can('mute', null, room) && Punishments.roomUserids.nestedHas(room.roomid, userid);
			if (!hasJurisdiction) {
				return this.errorReply("/checkpunishment - User not found.");
			}
		}

		let punishments = Punishments.getRoomPunishments(targetUser || {id: userid});

		if (punishments && punishments.length) {
			buf += `<br />Room punishments: `;

			buf += punishments.map(([room, punishment]) => {
				const [punishType, punishUserid, expireTime, reason] = punishment;
				let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
				if (!punishDesc) punishDesc = `punished`;
				if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`;
				let expiresIn = new Date(expireTime).getTime() - Date.now();
				let expireString = Chat.toDurationString(expiresIn, {precision: 1});
				punishDesc += ` for ${expireString}`;

				if (reason) punishDesc += `: ${reason}`;
				return `<a href="/${room}">${room}</a> (${punishDesc})`;
			}).join(', ');
			atLeastOne = true;
		}
		if (!atLeastOne) {
			buf += `This username has no punishments associated with it.`;
		}
		this.sendReplyBox(buf);
	},

	sharedbattles(target, room) {
		if (!this.can('lock')) return false;

		const [targetUsername1, targetUsername2] = target.split(',');
		if (!targetUsername1 || !targetUsername2) return this.parse(`/help sharedbattles`);
		const user1 = Users.get(targetUsername1);
		const user2 = Users.get(targetUsername2);
		const userID1 = toID(targetUsername1);
		const userID2 = toID(targetUsername2);

		const battles = [];
		for (const room of Rooms.rooms.values()) {
			if (!room.battle) continue;
			if ((user1 && user1.inRooms.has(room.roomid) || (room.auth && room.auth[userID1])) &&
				(user2 && user2.inRooms.has(room.roomid) || (room.auth && room.auth[userID2]))) {
				battles.push(room.roomid);
			}
		}

		if (!battles.length) return this.sendReply(`${targetUsername1} and ${targetUsername2} have no common battles.`);

		this.sendReplyBox(Chat.html`Common battles between ${targetUsername1} and ${targetUsername2}:<br />` + battles.map(id => {
			let shortId = id.startsWith('battle-') ? id.slice(7) : id;
			return Chat.html`<a href="/${id}">${shortId}</a>`;
		}).join(' | '));
	},
	sharedbattleshelp: [`/sharedbattles [user1], [user2] - Finds recent battles common to [user1] and [user2]. Requires % @ & ~`],

	sp: 'showpunishments',
	showpunishments(target, room, user) {
		if (!room.chatRoomData || room.roomid.includes('-')) return this.errorReply("This command is unavailable in temporary rooms.");
		return this.parse(`/join view-punishments-${room}`);
	},
	showpunishmentshelp: [`/showpunishments - Shows the current punishments in the room. Requires: % @ # & ~`],

	sgp: 'showglobalpunishments',
	showglobalpunishments(target, room, user) {
		if (!this.can('lock')) return;
		return this.parse(`/join view-globalpunishments`);
	},
	showglobalpunishmentshelp: [`/showpunishments - Shows the current global punishments. Requires: % @ # & ~`],

	'!host': true,
	host(target, room, user, connection, cmd) {
		if (!target) return this.parse('/help host');
		if (!this.can('rangeban')) return;
		target = target.trim();
		if (!net.isIPv4(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.');
		IPTools.lookup(target).then(({dnsbl, host, hostType}) => {
			const dnsblMessage = dnsbl ? ` [${dnsbl}]` : ``;
			this.sendReply(`IP ${target}: ${host || "ERROR"} [${hostType}]${dnsblMessage}`);
		});
	},
	hosthelp: [`/host [ip] - Gets the host for a given IP. Requires: & ~`],

	'!ipsearch': true,
	searchip: 'ipsearch',
	ipsearchall: 'ipsearch',
	hostsearch: 'ipsearch',
	ipsearch(target, room, user, connection, cmd) {
		if (!target.trim()) return this.parse(`/help ipsearch`);
		if (!this.can('rangeban')) return;

		let [ip, roomid] = this.splitOne(target);
		let targetRoom = roomid ? Rooms.get(roomid) : null;
		if (!targetRoom && targetRoom !== null) return this.errorReply(`The room "${roomid}" does not exist.`);
		let results = /** @type {string[]} */ ([]);
		let isAll = (cmd === 'ipsearchall');

		if (/[a-z]/.test(ip)) {
			// host
			this.sendReply(`Users with host ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			for (const curUser of Users.users.values()) {
				if (results.length > 100 && !isAll) continue;
				if (!curUser.latestHost || !curUser.latestHost.endsWith(ip)) continue;
				if (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			}
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else if (ip.slice(-1) === '*') {
			// IP range
			this.sendReply(`Users in IP range ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			ip = ip.slice(0, -1);
			for (const curUser of Users.users.values()) {
				if (results.length > 100 && !isAll) continue;
				if (!curUser.latestIp.startsWith(ip)) continue;
				if (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			}
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else {
			this.sendReply(`Users with IP ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			for (const curUser of Users.users.values()) {
				if (curUser.latestIp !== ip) continue;
				if (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			}
		}
		if (!results.length) {
			if (!ip.includes('.')) return this.errorReply(`${ip} is not a valid IP or host.`);
			return this.sendReply(`No results found.`);
		}
		return this.sendReply(results.join('; '));
	},
	ipsearchhelp: [`/ipsearch [ip|range|host], (room) - Find all users with specified IP, IP range, or host. If a room is provided only users in the room will be shown. Requires: & ~`],

	checkchallenges(target, room, user) {
		if (!this.can('ban', null, room)) return false;
		if (!this.runBroadcast(true)) return;
		if (!this.broadcasting) {
			this.errorReply(`This command must be broadcast:`);
			return this.parse(`/help checkchallenges`);
		}
		target = this.splitTarget(target);
		const user1 = this.targetUser;
		const user2 = Users.get(target);
		if (!user1 || !user2 || user1 === user2) return this.parse(`/help checkchallenges`);
		if (!(user1 in room.users) || !(user2 in room.users)) {
			return this.errorReply(`Both users must be in this room.`);
		}
		let challenges = [];
		const user1Challs = Ladders.challenges.get(user1.id);
		if (user1Challs) {
			for (const chall of user1Challs) {
				if (chall.from === user1.id && Users.get(chall.to) === user2) {
					challenges.push(Chat.html`${user1.name} is challenging ${user2.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		const user2Challs = Ladders.challenges.get(user2.id);
		if (user2Challs) {
			for (const chall of user2Challs) {
				if (chall.from === user2.id && Users.get(chall.to) === user1) {
					challenges.push(Chat.html`${user2.name} is challenging ${user1.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		if (!challenges.length) {
			return this.sendReplyBox(Chat.html`${user1.name} and ${user2.name} are not challenging each other.`);
		}
		this.sendReplyBox(challenges.join(`<br />`));
	},
	checkchallengeshelp: [`!checkchallenges [user1], [user2] - Check if the specified users are challenging each other. Requires: @ # & ~`],

	/*********************************************************
	 * Client fallback
	 *********************************************************/

	unignore: 'ignore',
	ignore(target, room, user) {
		if (!room) this.errorReply(`In PMs, this command can only be used by itself to ignore the person you're talking to: "/${this.cmd}", not "/${this.cmd} ${target}"`);
		this.errorReply(`You're using a custom client that doesn't support the ignore command.`);
	},

	/*********************************************************
	 * Data Search Dex
	 *********************************************************/

	'!data': true,
	pstats: 'data',
	stats: 'data',
	dex: 'data',
	pokedex: 'data',
	data(target, room, user, connection, cmd) {
		if (!this.runBroadcast()) return;

		let buffer = '';
		let sep = target.split(',');
		if (sep.length !== 2) sep = [target];
		target = sep[0].trim();
		let targetId = toID(target);
		if (!targetId) return this.parse('/help data');
		let targetNum = parseInt(target);
		if (!isNaN(targetNum) && '' + targetNum === target) {
			for (let p in Dex.data.Pokedex) {
				let pokemon = Dex.getTemplate(p);
				if (pokemon.num === targetNum) {
					target = pokemon.species;
					break;
				}
			}
		}
		let dex = Dex;
		/** @type {Format?} */
		let format = null;
		if (sep[1] && toID(sep[1]) in Dex.dexes) {
			dex = Dex.mod(toID(sep[1]));
		} else if (sep[1]) {
			format = Dex.getFormat(sep[1]);
			if (!format.exists) {
				return this.errorReply(`Unrecognized format or mod "${format.name}"`);
			}
			dex = Dex.mod(format.mod);
		} else if (room && room.battle) {
			format = Dex.getFormat(room.battle.format);
			dex = Dex.mod(format.mod);
		}
		let newTargets = dex.dataSearch(target);
		let showDetails = (cmd === 'dt' || cmd === 'details');
		if (!newTargets || !newTargets.length) {
			return this.errorReply(`No Pok\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : ""}. (Check your spelling?)`);
		}

		for (const [i, newTarget] of newTargets.entries()) {
			if (newTarget.isInexact && !i) {
				buffer = `No Pok\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : ""}. Showing the data of '${newTargets[0].name}' instead.\n`;
			}
			/** @type {AnyObject} */
			let details = null;
			switch (newTarget.searchType) {
			case 'nature':
				let nature = Dex.getNature(newTarget.name);
				buffer += "" + nature.name + " nature: ";
				if (nature.plus) {
					let statNames = {'atk': "Attack", 'def': "Defense", 'spa': "Special Attack", 'spd': "Special Defense", 'spe': "Speed"};
					buffer += "+10% " + statNames[nature.plus] + ", -10% " + statNames[nature.minus] + ".";
				} else {
					buffer += "No effect.";
				}
				return this.sendReply(buffer);
			case 'pokemon':
				let pokemon = dex.getTemplate(newTarget.name);
				if (format && format.onModifyTemplate) {
					pokemon = format.onModifyTemplate.call({dex}, pokemon) || pokemon;
				}
				let tier = pokemon.tier;
				if (room && (room.roomid === 'smogondoubles' ||
					['gen7doublesou', 'gen7doublesubers', 'gen7doublesuu'].includes(room.battle && room.battle.format))) {
					tier = pokemon.doublesTier;
				}
				buffer += `|raw|${Chat.getDataPokemonHTML(pokemon, dex.gen, tier)}\n`;
				if (showDetails) {
					let weighthit = 20;
					if (pokemon.weighthg >= 2000) {
						weighthit = 120;
					} else if (pokemon.weighthg >= 1000) {
						weighthit = 100;
					} else if (pokemon.weighthg >= 500) {
						weighthit = 80;
					} else if (pokemon.weighthg >= 250) {
						weighthit = 60;
					} else if (pokemon.weighthg >= 100) {
						weighthit = 40;
					}
					details = {
						"Dex#": pokemon.num,
						"Gen": pokemon.gen || 'CAP',
						"Height": pokemon.heightm + " m",
						"Weight": pokemon.weighthg / 10 + " kg <em>(" + weighthit + " BP)</em>",
					};
					if (pokemon.color && dex.gen >= 5) details["Dex Colour"] = pokemon.color;
					if (pokemon.eggGroups && dex.gen >= 2) details["Egg Group(s)"] = pokemon.eggGroups.join(", ");
					let evos = /** @type {string[]} */ ([]);
					for (const evoName of pokemon.evos) {
						const evo = dex.getTemplate(evoName);
						if (evo.gen <= dex.gen) {
							let condition = evo.evoCondition ? ` ${evo.evoCondition}` : ``;
							switch (evo.evoType) {
							case 'levelExtra':
								evos.push(`${evo.name} (level-up${condition})`);
								break;
							case 'levelFriendship':
								evos.push(`${evo.name} (level-up with high Friendship${condition})`);
								break;
							case 'levelHold':
								evos.push(`${evo.name} (level-up holding ${evo.evoItem}${condition})`);
								break;
							case 'useItem':
								evos.push(`${evo.name} (${evo.evoItem})`);<commit_msg>Don't display Weight for GMax Pokemon (#6091)<commit_after>/**
 * Informational Commands
 * Pokemon Showdown - https://pokemonshowdown.com/
 *
 * These are informational commands. For instance, you can define the command
 * 'whois' here, then use it by typing /whois into Pokemon Showdown.
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT
 */

'use strict';

const net = require('net');

/** @type {ChatCommands} */
const commands = {

	'!whois': true,
	ip: 'whois',
	rooms: 'whois',
	alt: 'whois',
	alts: 'whois',
	whoare: 'whois',
	whois(target, room, user, connection, cmd) {
		if (room && room.roomid === 'staff' && !this.runBroadcast()) return;
		if (!room) room = Rooms.global;
		let targetUser = this.targetUserOrSelf(target, user.group === ' ');
		let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
		if (!targetUser) {
			if (showAll) return this.parse('/offlinewhois ' + target);
			return this.errorReply("User " + this.targetUsername + " not found.");
		}
		if (showAll && !user.trusted && targetUser !== user) {
			return this.errorReply(`/${cmd} - Access denied.`);
		}

		let buf = Chat.html`<strong class="username"><small style="display:none">${targetUser.group}</small>${targetUser.name}</strong> `;
		const ac = targetUser.autoconfirmed;
		if (ac && showAll) buf += ` <small style="color:gray">(ac${targetUser.id === ac ? `` : `: <span class="username">${ac}</span>`})</small>`;
		const trusted = targetUser.trusted;
		if (trusted && showAll) {
			buf += ` <small style="color:gray">(trusted${targetUser.id === trusted ? `` : `: <span class="username">${trusted}</span>`})</small>`;
		}
		if (!targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;
		let roomauth = '';
		if (room.auth && targetUser.id in room.auth) roomauth = room.auth[targetUser.id];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) {
			buf += `<br />Global ${Config.groups[targetUser.group].name} (${targetUser.group})`;
		}
		if (targetUser.isSysop) {
			buf += `<br />(Pok&eacute;mon Showdown System Operator)`;
		}
		if (!targetUser.registered) {
			buf += `<br />(Unregistered)`;
		}
		let publicrooms = "";
		let hiddenrooms = "";
		let privaterooms = "";
		for (const roomid of targetUser.inRooms) {
			if (roomid === 'global') continue;
			let targetRoom = Rooms.get(roomid);

			let authSymbol = (targetRoom.auth && targetRoom.auth[targetUser.id] ? targetRoom.auth[targetUser.id] : '');
			let battleTitle = (roomid.battle ? ` title="${roomid.title}"` : '');
			let output = `${authSymbol}<a href="/${roomid}"${battleTitle}>${roomid}</a>`;
			if (targetRoom.isPrivate === true) {
				if (targetRoom.modjoin === '~') continue;
				if (privaterooms) privaterooms += " | ";
				privaterooms += output;
			} else if (targetRoom.isPrivate) {
				if (hiddenrooms) hiddenrooms += " | ";
				hiddenrooms += output;
			} else {
				if (publicrooms) publicrooms += " | ";
				publicrooms += output;
			}
		}
		buf += '<br />Rooms: ' + (publicrooms || '<em>(no public rooms)</em>');

		if (!showAll) {
			return this.sendReplyBox(buf);
		}
		const canViewAlts = (user === targetUser || user.can('alts', targetUser));
		const canViewPunishments = canViewAlts || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.id in room.users);
		const canViewSecretRooms = user === targetUser || (canViewAlts && targetUser.locked) || user.can('makeroom');
		buf += '<br />';

		if (canViewAlts) {
			let prevNames = Object.keys(targetUser.prevNames).map(userid => {
				const punishment = Punishments.userids.get(userid);
				return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.id ? ` as ${punishment[1]}` : ''})` : '');
			}).join(", ");
			if (prevNames) buf += Chat.html`<br />Previous names: ${prevNames}`;

			for (const targetAlt of targetUser.getAltUsers(true)) {
				if (!targetAlt.named && !targetAlt.connected) continue;
				if (targetAlt.group === '~' && user.group !== '~') continue;

				const punishment = Punishments.userids.get(targetAlt.id);
				const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.id ? ` as ${punishment[1]}` : ''})` : '';
				buf += Chat.html`<br />Alt: <span class="username">${targetAlt.name}</span>${punishMsg}`;
				if (!targetAlt.connected) buf += ` <em style="color:gray">(offline)</em>`;
				prevNames = Object.keys(targetAlt.prevNames).map(userid => {
					const punishment = Punishments.userids.get(userid);
					return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.id ? ` as ${punishment[1]}` : ''})` : '');
				}).join(", ");
				if (prevNames) buf += `<br />Previous names: ${prevNames}`;
			}
		}
		if (canViewPunishments) {
			if (targetUser.namelocked) {
				buf += `<br />NAMELOCKED: ${targetUser.namelocked}`;
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			} else if (targetUser.locked) {
				buf += `<br />LOCKED: ${targetUser.locked}`;
				switch (targetUser.locked) {
				case '#rangelock':
					buf += ` - IP or host is in a temporary range-lock`;
					break;
				case '#hostfilter':
					buf += ` - host is permanently locked for being a proxy`;
					break;
				}
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			}
			let battlebanned = Punishments.isBattleBanned(targetUser);
			if (battlebanned) {
				buf += `<br />BATTLEBANNED: ${battlebanned[1]}`;
				let expiresIn = new Date(battlebanned[2]).getTime() - Date.now();
				let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
				let expiresText = '';
				if (expiresDays >= 1) {
					expiresText = `in around ${Chat.count(expiresDays, "days")}`;
				} else {
					expiresText = `soon`;
				}
				if (expiresIn > 1) buf += ` (expires ${expiresText})`;
				if (battlebanned[3]) buf += Chat.html` (reason: ${battlebanned[3]})`;
			}
			if (targetUser.semilocked) {
				buf += `<br />Semilocked: ${targetUser.semilocked}`;
			}
		}
		if (user.can('ip', targetUser)) {
			let ips = Object.keys(targetUser.ips);
			ips = ips.map(ip => {
				let status = [];
				let punishment = Punishments.ips.get(ip);
				if (user.can('ip') && punishment) {
					let [punishType, userid] = punishment;
					let punishMsg = Punishments.punishmentTypes.get(punishType) || 'punished';
					if (userid !== targetUser.id) punishMsg += ` as ${userid}`;
					status.push(punishMsg);
				}
				if (Punishments.sharedIps.has(ip)) {
					let sharedStr = 'shared';
					if (Punishments.sharedIps.get(ip)) {
						sharedStr += `: ${Punishments.sharedIps.get(ip)}`;
					}
					status.push(sharedStr);
				}
				return `<a href="https://whatismyipaddress.com/ip/${ip}" target="_blank">${ip}</a>` + (status.length ? ` (${status.join('; ')})` : '');
			});
			buf += `<br /> IP${Chat.plural(ips)}: ${ips.join(", ")}`;
			if (user.group !== ' ' && targetUser.latestHost) {
				buf += Chat.html`<br />Host: ${targetUser.latestHost} [${targetUser.latestHostType}]`;
			}
		} else if (user === targetUser) {
			buf += `<br /> IP: <a href="https://whatismyipaddress.com/ip/${connection.ip}" target="_blank">${connection.ip}</a>`;
		}
		if (canViewAlts && hiddenrooms) {
			buf += `<br />Hidden rooms: ${hiddenrooms}`;
		}
		if (canViewSecretRooms && privaterooms) {
			buf += `<br />Secret rooms: ${privaterooms}`;
		}

		let gameRooms = [];
		for (const room of Rooms.rooms.values()) {
			if (!room.game) continue;
			if ((targetUser.id in room.game.playerTable && !targetUser.inRooms.has(room.roomid)) ||
				(room.auth && room.auth[targetUser.id] === Users.PLAYER_SYMBOL)) {
				if (room.isPrivate && !canViewAlts) {
					continue;
				}
				gameRooms.push(room.roomid);
			}
		}
		if (gameRooms.length) {
			buf += '<br />Recent games: ' + gameRooms.map(id => {
				let shortId = id.startsWith('battle-') ? id.slice(7) : id;
				return Chat.html`<a href="/${id}">${shortId}</a>`;
			}).join(' | ');
		}

		if (canViewPunishments) {
			let punishments = Punishments.getRoomPunishments(targetUser, {checkIps: true});

			if (punishments.length) {
				buf += `<br />Room punishments: `;

				buf += punishments.map(([room, punishment]) => {
					const [punishType, punishUserid, expireTime, reason] = punishment;
					let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
					if (!punishDesc) punishDesc = `punished`;
					if (punishUserid !== targetUser.id) punishDesc += ` as ${punishUserid}`;
					let expiresIn = new Date(expireTime).getTime() - Date.now();
					let expireString = Chat.toDurationString(expiresIn, {precision: 1});
					punishDesc += ` for ${expireString}`;

					if (reason) punishDesc += `: ${reason}`;
					return `<a href="/${room}">${room}</a> (${punishDesc})`;
				}).join(', ');
			}
		}
		this.sendReplyBox(buf);
	},
	whoishelp: [
		`/whois - Get details on yourself: alts, group, IP address, and rooms.`,
		`/whois [username] - Get details on a username: alts (Requires: % @ & ~), group, IP address (Requires: @ & ~), and rooms.`,
	],

	'!offlinewhois': true,
	'chp': 'offlinewhois',
	checkpunishment: 'offlinewhois',
	offlinewhois(target, room, user) {
		if (!user.trusted) {
			return this.errorReply("/offlinewhois - Access denied.");
		}
		let userid = toID(target);
		if (!userid) return this.errorReply("Please enter a valid username.");
		let targetUser = Users.get(userid);
		let buf = Chat.html`<strong class="username">${target}</strong>`;
		if (!targetUser || !targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;

		let roomauth = '';
		if (room && room.auth && userid in room.auth) roomauth = room.auth[userid];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		let group = (Users.usergroups[userid] || '').charAt(0);
		if (Config.groups[group] && Config.groups[group].name) {
			buf += `<br />Global ${Config.groups[group].name} (${group})`;
		}

		buf += `<br /><br />`;
		let atLeastOne = false;

		let punishment = Punishments.userids.get(userid);
		if (punishment) {
			const [punishType, punishUserid, , reason] = punishment;
			const punishName = (Punishments.punishmentTypes.get(punishType) || punishType).toUpperCase();
			buf += `${punishName}: ${punishUserid}`;
			let expiresIn = Punishments.checkLockExpiration(userid);
			if (expiresIn) buf += expiresIn;
			if (reason) buf += Chat.html` (reason: ${reason})`;
			buf += '<br />';
			atLeastOne = true;
		}

		if (!user.can('alts') && !atLeastOne) {
			let hasJurisdiction = room && user.can('mute', null, room) && Punishments.roomUserids.nestedHas(room.roomid, userid);
			if (!hasJurisdiction) {
				return this.errorReply("/checkpunishment - User not found.");
			}
		}

		let punishments = Punishments.getRoomPunishments(targetUser || {id: userid});

		if (punishments && punishments.length) {
			buf += `<br />Room punishments: `;

			buf += punishments.map(([room, punishment]) => {
				const [punishType, punishUserid, expireTime, reason] = punishment;
				let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
				if (!punishDesc) punishDesc = `punished`;
				if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`;
				let expiresIn = new Date(expireTime).getTime() - Date.now();
				let expireString = Chat.toDurationString(expiresIn, {precision: 1});
				punishDesc += ` for ${expireString}`;

				if (reason) punishDesc += `: ${reason}`;
				return `<a href="/${room}">${room}</a> (${punishDesc})`;
			}).join(', ');
			atLeastOne = true;
		}
		if (!atLeastOne) {
			buf += `This username has no punishments associated with it.`;
		}
		this.sendReplyBox(buf);
	},

	sharedbattles(target, room) {
		if (!this.can('lock')) return false;

		const [targetUsername1, targetUsername2] = target.split(',');
		if (!targetUsername1 || !targetUsername2) return this.parse(`/help sharedbattles`);
		const user1 = Users.get(targetUsername1);
		const user2 = Users.get(targetUsername2);
		const userID1 = toID(targetUsername1);
		const userID2 = toID(targetUsername2);

		const battles = [];
		for (const room of Rooms.rooms.values()) {
			if (!room.battle) continue;
			if ((user1 && user1.inRooms.has(room.roomid) || (room.auth && room.auth[userID1])) &&
				(user2 && user2.inRooms.has(room.roomid) || (room.auth && room.auth[userID2]))) {
				battles.push(room.roomid);
			}
		}

		if (!battles.length) return this.sendReply(`${targetUsername1} and ${targetUsername2} have no common battles.`);

		this.sendReplyBox(Chat.html`Common battles between ${targetUsername1} and ${targetUsername2}:<br />` + battles.map(id => {
			let shortId = id.startsWith('battle-') ? id.slice(7) : id;
			return Chat.html`<a href="/${id}">${shortId}</a>`;
		}).join(' | '));
	},
	sharedbattleshelp: [`/sharedbattles [user1], [user2] - Finds recent battles common to [user1] and [user2]. Requires % @ & ~`],

	sp: 'showpunishments',
	showpunishments(target, room, user) {
		if (!room.chatRoomData || room.roomid.includes('-')) return this.errorReply("This command is unavailable in temporary rooms.");
		return this.parse(`/join view-punishments-${room}`);
	},
	showpunishmentshelp: [`/showpunishments - Shows the current punishments in the room. Requires: % @ # & ~`],

	sgp: 'showglobalpunishments',
	showglobalpunishments(target, room, user) {
		if (!this.can('lock')) return;
		return this.parse(`/join view-globalpunishments`);
	},
	showglobalpunishmentshelp: [`/showpunishments - Shows the current global punishments. Requires: % @ # & ~`],

	'!host': true,
	host(target, room, user, connection, cmd) {
		if (!target) return this.parse('/help host');
		if (!this.can('rangeban')) return;
		target = target.trim();
		if (!net.isIPv4(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.');
		IPTools.lookup(target).then(({dnsbl, host, hostType}) => {
			const dnsblMessage = dnsbl ? ` [${dnsbl}]` : ``;
			this.sendReply(`IP ${target}: ${host || "ERROR"} [${hostType}]${dnsblMessage}`);
		});
	},
	hosthelp: [`/host [ip] - Gets the host for a given IP. Requires: & ~`],

	'!ipsearch': true,
	searchip: 'ipsearch',
	ipsearchall: 'ipsearch',
	hostsearch: 'ipsearch',
	ipsearch(target, room, user, connection, cmd) {
		if (!target.trim()) return this.parse(`/help ipsearch`);
		if (!this.can('rangeban')) return;

		let [ip, roomid] = this.splitOne(target);
		let targetRoom = roomid ? Rooms.get(roomid) : null;
		if (!targetRoom && targetRoom !== null) return this.errorReply(`The room "${roomid}" does not exist.`);
		let results = /** @type {string[]} */ ([]);
		let isAll = (cmd === 'ipsearchall');

		if (/[a-z]/.test(ip)) {
			// host
			this.sendReply(`Users with host ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			for (const curUser of Users.users.values()) {
				if (results.length > 100 && !isAll) continue;
				if (!curUser.latestHost || !curUser.latestHost.endsWith(ip)) continue;
				if (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			}
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else if (ip.slice(-1) === '*') {
			// IP range
			this.sendReply(`Users in IP range ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			ip = ip.slice(0, -1);
			for (const curUser of Users.users.values()) {
				if (results.length > 100 && !isAll) continue;
				if (!curUser.latestIp.startsWith(ip)) continue;
				if (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			}
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else {
			this.sendReply(`Users with IP ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			for (const curUser of Users.users.values()) {
				if (curUser.latestIp !== ip) continue;
				if (targetRoom && !curUser.inRooms.has(targetRoom.roomid)) continue;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			}
		}
		if (!results.length) {
			if (!ip.includes('.')) return this.errorReply(`${ip} is not a valid IP or host.`);
			return this.sendReply(`No results found.`);
		}
		return this.sendReply(results.join('; '));
	},
	ipsearchhelp: [`/ipsearch [ip|range|host], (room) - Find all users with specified IP, IP range, or host. If a room is provided only users in the room will be shown. Requires: & ~`],

	checkchallenges(target, room, user) {
		if (!this.can('ban', null, room)) return false;
		if (!this.runBroadcast(true)) return;
		if (!this.broadcasting) {
			this.errorReply(`This command must be broadcast:`);
			return this.parse(`/help checkchallenges`);
		}
		target = this.splitTarget(target);
		const user1 = this.targetUser;
		const user2 = Users.get(target);
		if (!user1 || !user2 || user1 === user2) return this.parse(`/help checkchallenges`);
		if (!(user1 in room.users) || !(user2 in room.users)) {
			return this.errorReply(`Both users must be in this room.`);
		}
		let challenges = [];
		const user1Challs = Ladders.challenges.get(user1.id);
		if (user1Challs) {
			for (const chall of user1Challs) {
				if (chall.from === user1.id && Users.get(chall.to) === user2) {
					challenges.push(Chat.html`${user1.name} is challenging ${user2.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		const user2Challs = Ladders.challenges.get(user2.id);
		if (user2Challs) {
			for (const chall of user2Challs) {
				if (chall.from === user2.id && Users.get(chall.to) === user1) {
					challenges.push(Chat.html`${user2.name} is challenging ${user1.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		if (!challenges.length) {
			return this.sendReplyBox(Chat.html`${user1.name} and ${user2.name} are not challenging each other.`);
		}
		this.sendReplyBox(challenges.join(`<br />`));
	},
	checkchallengeshelp: [`!checkchallenges [user1], [user2] - Check if the specified users are challenging each other. Requires: @ # & ~`],

	/*********************************************************
	 * Client fallback
	 *********************************************************/

	unignore: 'ignore',
	ignore(target, room, user) {
		if (!room) this.errorReply(`In PMs, this command can only be used by itself to ignore the person you're talking to: "/${this.cmd}", not "/${this.cmd} ${target}"`);
		this.errorReply(`You're using a custom client that doesn't support the ignore command.`);
	},

	/*********************************************************
	 * Data Search Dex
	 *********************************************************/

	'!data': true,
	pstats: 'data',
	stats: 'data',
	dex: 'data',
	pokedex: 'data',
	data(target, room, user, connection, cmd) {
		if (!this.runBroadcast()) return;

		let buffer = '';
		let sep = target.split(',');
		if (sep.length !== 2) sep = [target];
		target = sep[0].trim();
		let targetId = toID(target);
		if (!targetId) return this.parse('/help data');
		let targetNum = parseInt(target);
		if (!isNaN(targetNum) && '' + targetNum === target) {
			for (let p in Dex.data.Pokedex) {
				let pokemon = Dex.getTemplate(p);
				if (pokemon.num === targetNum) {
					target = pokemon.species;
					break;
				}
			}
		}
		let dex = Dex;
		/** @type {Format?} */
		let format = null;
		if (sep[1] && toID(sep[1]) in Dex.dexes) {
			dex = Dex.mod(toID(sep[1]));
		} else if (sep[1]) {
			format = Dex.getFormat(sep[1]);
			if (!format.exists) {
				return this.errorReply(`Unrecognized format or mod "${format.name}"`);
			}
			dex = Dex.mod(format.mod);
		} else if (room && room.battle) {
			format = Dex.getFormat(room.battle.format);
			dex = Dex.mod(format.mod);
		}
		let newTargets = dex.dataSearch(target);
		let showDetails = (cmd === 'dt' || cmd === 'details');
		if (!newTargets || !newTargets.length) {
			return this.errorReply(`No Pok\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : ""}. (Check your spelling?)`);
		}

		for (const [i, newTarget] of newTargets.entries()) {
			if (newTarget.isInexact && !i) {
				buffer = `No Pok\u00e9mon, item, move, ability or nature named '${target}' was found${Dex.gen > dex.gen ? ` in Gen ${dex.gen}` : ""}. Showing the data of '${newTargets[0].name}' instead.\n`;
			}
			/** @type {AnyObject} */
			let details = null;
			switch (newTarget.searchType) {
			case 'nature':
				let nature = Dex.getNature(newTarget.name);
				buffer += "" + nature.name + " nature: ";
				if (nature.plus) {
					let statNames = {'atk': "Attack", 'def': "Defense", 'spa': "Special Attack", 'spd': "Special Defense", 'spe': "Speed"};
					buffer += "+10% " + statNames[nature.plus] + ", -10% " + statNames[nature.minus] + ".";
				} else {
					buffer += "No effect.";
				}
				return this.sendReply(buffer);
			case 'pokemon':
				let pokemon = dex.getTemplate(newTarget.name);
				if (format && format.onModifyTemplate) {
					pokemon = format.onModifyTemplate.call({dex}, pokemon) || pokemon;
				}
				let tier = pokemon.tier;
				if (room && (room.roomid === 'smogondoubles' ||
					['gen7doublesou', 'gen7doublesubers', 'gen7doublesuu'].includes(room.battle && room.battle.format))) {
					tier = pokemon.doublesTier;
				}
				buffer += `|raw|${Chat.getDataPokemonHTML(pokemon, dex.gen, tier)}\n`;
				if (showDetails) {
					let weighthit = 20;
					if (pokemon.weighthg >= 2000) {
						weighthit = 120;
					} else if (pokemon.weighthg >= 1000) {
						weighthit = 100;
					} else if (pokemon.weighthg >= 500) {
						weighthit = 80;
					} else if (pokemon.weighthg >= 250) {
						weighthit = 60;
					} else if (pokemon.weighthg >= 100) {
						weighthit = 40;
					}
					details = {
						"Dex#": pokemon.num,
						"Gen": pokemon.gen || 'CAP',
						"Height": pokemon.heightm + " m",
					};
					if (!pokemon.forme || pokemon.forme !== "Gmax") details["Weight"] = pokemon.weighthg / 10 + " kg <em>(" + weighthit + " BP)</em>";
					else details["Weight"] = "0 kg <em>(GK/LK fail)</em>";
					if (pokemon.color && dex.gen >= 5) details["Dex Colour"] = pokemon.color;
					if (pokemon.eggGroups && dex.gen >= 2) details["Egg Group(s)"] = pokemon.eggGroups.join(", ");
					let evos = /** @type {string[]} */ ([]);
					for (const evoName of pokemon.evos) {
						const evo = dex.getTemplate(evoName);
						if (evo.gen <= dex.gen) {
							let condition = evo.evoCondition ? ` ${evo.evoCondition}` : ``;
							switch (evo.evoType) {
							case 'levelExtra':
								evos.push(`${evo.name} (level-up${condition})`);
								break;
							case 'levelFriendship':
								evos.push(`${evo.name} (level-up with high Friendship${condition})`);
								break;
							case 'levelHold':
								evos.push(`${evo.name} (level-up holding ${evo.evoItem}${condition})`);
								break;
							case 'useItem':
								evos.push(`${evo.name} (${evo.evoItem})`);
{'fraction_non_alphanumeric': 0.19721102930317927, 'fraction_numerical': 0.00778464589354655, 'mean_word_length': 3.7406740674067405, 'pattern_counts': {'":': 7, '<': 143, '<?xml version=': 0, '>': 187, 'https://': 6, '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': '7433497', 'n_tokens_mistral': 19182, 'n_tokens_neox': 16400, 'n_words': 3641}