<commit_before>/**
 * Team Validator
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * Handles team validation, and specifically learnset checking.
 *
 * @license MIT
 */

'use strict';

/**
 * Keeps track of how a pokemon with a given set might be obtained.
 *
 * `sources` is a list of possible PokemonSources, and a nonzero
 * sourcesBefore means the Pokemon is compatible with all possible
 * PokemonSources from that gen or earlier.
 *
 * `limitedEgg` tracks moves that can only be obtained from an egg with
 * another father in gen 2-5. If there are multiple such moves,
 * potential fathers need to be checked to see if they can actually
 * learn the move combination in question.
 *
 * @typedef {Object} PokemonSources
 * @property {PokemonSource[]} sources
 * @property {number} sourcesBefore
 * @property {string} [babyOnly]
 * @property {string} [sketchMove] limit 1 in fakemon Sketch-as-egg-move formats
 * @property {string} [hm] limit 1 HM transferred from gen 4 to 5
 * @property {(string | 'self')[]} [limitedEgg] list of egg moves
 * @property {true} [fastCheck]
 */

class Validator {
	/**
	 * @param {string | Format} format
	 */
	constructor(format) {
		this.format = Dex.getFormat(format);
		this.dex = Dex.forFormat(this.format);
		this.ruleTable = this.dex.getRuleTable(this.format);
	}

	/**
	 * @param {PokemonSet[]?} team
	 * @return {string[]?}
	 */
	validateTeam(team, removeNicknames = false) {
		if (this.format.validateTeam) return this.format.validateTeam.call(this, team, removeNicknames);
		return this.baseValidateTeam(team, removeNicknames);
	}

	/**
	 * @param {PokemonSet[]?} team
	 * @return {string[]?}
	 */
	baseValidateTeam(team, removeNicknames = false) {
		let format = this.format;
		let dex = this.dex;

		let problems = /** @type {string[]} */ ([]);
		const ruleTable = this.ruleTable;
		if (format.team) {
			return null;
		}
		if (!team || !Array.isArray(team)) {
			if (format.canUseRandomTeam) {
				return null;
			}
			return [`You sent invalid team data. If you're not using a custom client, please report this as a bug.`];
		}

		let lengthRange = format.teamLength && format.teamLength.validate;
		if (!lengthRange) lengthRange = [1, 6];
		if (format.gameType === 'doubles' && lengthRange[0] < 2) lengthRange[0] = 2;
		if ((format.gameType === 'triples' || format.gameType === 'rotation') && lengthRange[0] < 3) lengthRange[0] = 3;
		if (team.length < lengthRange[0]) problems.push(`You must bring at least ${lengthRange[0]} Pok\u00E9mon.`);
		if (team.length > lengthRange[1]) return [`You may only bring up to ${lengthRange[1]} Pok\u00E9mon.`];

		// A limit is imposed here to prevent too much engine strain or
		// too much layout deformation - to be exact, this is the limit
		// allowed in Custom Game.
		// The usual limit of 6 pokemon is handled elsewhere - currently
		// in the cartridge-compliant set validator: rulesets.js:pokemon
		if (team.length > 24) {
			problems.push(`Your team has more than than 24 Pok\u00E9mon, which the simulator can't handle.`);
			return problems;
		}

		let teamHas = {};
		for (let i = 0; i < team.length; i++) { // Changing this loop to for-of would require another loop/map statement to do removeNicknames
			if (!team[i]) return [`You sent invalid team data. If you're not using a custom client, please report this as a bug.`];
			let setProblems = (format.validateSet || this.validateSet).call(this, team[i], teamHas);
			if (setProblems) {
				problems = problems.concat(setProblems);
			}
			if (removeNicknames) team[i].name = dex.getTemplate(team[i].species).baseSpecies;
		}

		for (const [rule, source, limit, bans] of ruleTable.complexTeamBans) {
			let count = 0;
			for (const ban of bans) {
				if (teamHas[ban] > 0) {
					count += limit ? teamHas[ban] : 1;
				}
			}
			if (limit && count > limit) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`You are limited to ${limit} of ${rule}${clause}.`);
			} else if (!limit && count >= bans.length) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`Your team has the combination of ${rule}, which is banned${clause}.`);
			}
		}

		for (const [rule] of ruleTable) {
			let subformat = dex.getFormat(rule);
			if (subformat.onValidateTeam && ruleTable.has(subformat.id)) {
				problems = problems.concat(subformat.onValidateTeam.call(dex, team, format, teamHas) || []);
			}
		}
		if (format.onValidateTeam) {
			problems = problems.concat(format.onValidateTeam.call(dex, team, format, teamHas) || []);
		}

		if (!problems.length) return null;
		return problems;
	}

	/**
	 * @param {PokemonSet} set
	 * @param {AnyObject} teamHas
	 * @return {string[]?}
	 */
	validateSet(set, teamHas) {
		let format = this.format;
		let dex = this.dex;

		let problems = /** @type {string[]} */ ([]);
		if (!set) {
			return [`This is not a Pokemon.`];
		}

		let template = dex.getTemplate(set.species);
		set.species = Dex.getSpecies(set.species);
		set.name = dex.getName(set.name);
		let item = dex.getItem(Dex.getString(set.item));
		set.item = item.name;
		let ability = dex.getAbility(Dex.getString(set.ability));
		set.ability = ability.name;
		set.nature = dex.getNature(Dex.getString(set.nature)).name;
		if (!Array.isArray(set.moves)) set.moves = [];

		let maxLevel = format.maxLevel || 100;
		let maxForcedLevel = format.maxForcedLevel || maxLevel;
		let forcedLevel = /** @type {number?} */ (null);
		if (!set.level) {
			set.level = (format.defaultLevel || maxLevel);
		}
		if (format.forcedLevel) {
			forcedLevel = format.forcedLevel;
		} else if (set.level >= maxForcedLevel) {
			forcedLevel = maxForcedLevel;
		}
		if (set.level > maxLevel || set.level === forcedLevel || set.level === maxForcedLevel) {
			// Note that we're temporarily setting level 50 pokemon in VGC to level 100
			// This allows e.g. level 50 Hydreigon even though it doesn't evolve until level 64.
			// Leveling up can't make an obtainable pokemon unobtainable, so this is safe.
			// Just remember to set the level back to forcedLevel at the end of the file.
			set.level = maxLevel;
		}

		let nameTemplate = dex.getTemplate(set.name);
		if (nameTemplate.exists && nameTemplate.name.toLowerCase() === set.name.toLowerCase()) {
			// Name must not be the name of another pokemon
			// @ts-ignore
			set.name = null;
		}
		set.name = set.name || template.baseSpecies;
		let name = set.species;
		if (set.species !== set.name && template.baseSpecies !== set.name) name = `${set.name} (${set.species})`;
		let isHidden = false;
		let lsetData = /** @type {PokemonSources} */ ({sources: [], sourcesBefore: dex.gen});

		let setHas = {};
		const ruleTable = this.ruleTable;

		for (const [rule] of ruleTable) {
			let subformat = dex.getFormat(rule);
			if (subformat.onChangeSet && ruleTable.has(subformat.id)) {
				problems = problems.concat(subformat.onChangeSet.call(dex, set, format) || []);
			}
		}
		if (format.onChangeSet) {
			problems = problems.concat(format.onChangeSet.call(dex, set, format, setHas, teamHas) || []);
		}

		// onChangeSet can modify set.species, set.item, set.ability
		template = dex.getTemplate(set.species);
		item = dex.getItem(set.item);
		ability = dex.getAbility(set.ability);

		if (ability.id === 'battlebond' && template.id === 'greninja' && !ruleTable.has('ignoreillegalabilities')) {
			template = dex.getTemplate('greninjaash');
			if (set.gender && set.gender !== 'M') {
				problems.push(`Battle Bond Greninja must be male.`);
			}
			set.gender = 'M';
		}
		if (ability.id === 'owntempo' && template.id === 'rockruff') {
			template = dex.getTemplate('rockruffdusk');
		}
		if (!template.exists) {
			return [`The Pokemon "${set.species}" does not exist.`];
		}

		if (item.id && !item.exists) {
			return [`"${set.item}" is an invalid item.`];
		}
		if (ability.id && !ability.exists) {
			if (dex.gen < 3) {
				// gen 1-2 don't have abilities, just silently remove
				ability = dex.getAbility('');
				set.ability = '';
			} else {
				return [`"${set.ability}" is an invalid ability.`];
			}
		}
		if (set.nature && !dex.getNature(set.nature).exists) {
			if (dex.gen < 3) {
				// gen 1-2 don't have natures, just remove them
				set.nature = '';
			} else {
				problems.push(`${name}'s nature is invalid.`);
			}
		}
		if (set.happiness !== undefined && isNaN(set.happiness)) {
			problems.push(`${name} has an invalid happiness.`);
		}

		let banReason = ruleTable.check('pokemon:' + template.id, setHas);
		let templateOverride = ruleTable.has('+pokemon:' + template.id);
		if (!templateOverride) {
			banReason = banReason || ruleTable.check('basepokemon:' + toId(template.baseSpecies), setHas);
		}
		if (banReason) {
			return [`${set.species} is ${banReason}.`];
		}
		templateOverride = templateOverride || ruleTable.has('+basepokemon:' + toId(template.baseSpecies));
		let postMegaTemplate = template;
		if (item.megaEvolves === template.species) {
			if (!item.megaStone) throw new Error(`Item ${item.name} has no base form for mega evolution`);
			postMegaTemplate = dex.getTemplate(item.megaStone);
		}
		if (['Mega', 'Mega-X', 'Mega-Y'].includes(postMegaTemplate.forme)) {
			banReason = ruleTable.check('pokemontag:mega', setHas);
			const megaTemplateOverride = ruleTable.has('+pokemon:' + postMegaTemplate.id);
			if (megaTemplateOverride) {
				templateOverride = true;
			} else if (banReason) {
				problems.push(`Mega evolutions are ${banReason}.`);
			} else {
				banReason = ruleTable.check('pokemon:' + postMegaTemplate.id, setHas);
				if (banReason) problems.push(`${postMegaTemplate.species} is ${banReason}.`);
			}
		}
		if (!templateOverride && postMegaTemplate.tier) {
			banReason = ruleTable.check('pokemontag:' + toId(postMegaTemplate.tier), setHas);
			if (banReason) {
				problems.push(`${postMegaTemplate.species} is in ${postMegaTemplate.tier}, which is ${banReason}.`);
			}
		}
		if (!templateOverride && postMegaTemplate.doublesTier) {
			banReason = ruleTable.check('pokemontag:' + toId(postMegaTemplate.doublesTier), setHas);
			if (banReason) {
				problems.push(`${postMegaTemplate.species} is in ${postMegaTemplate.doublesTier}, which is ${banReason}.`);
			}
		}
		if (!templateOverride && ruleTable.has('-unreleased') && postMegaTemplate.isUnreleased) {
			problems.push(`${name} (${postMegaTemplate.species}) is unreleased.`);
		}

		banReason = ruleTable.check('ability:' + toId(set.ability), setHas);
		if (banReason) {
			problems.push(`${name}'s ability ${set.ability} is ${banReason}.`);
		}
		banReason = ruleTable.check('item:' + toId(set.item), setHas);
		if (banReason) {
			problems.push(`${name}'s item ${set.item} is ${banReason}.`);
		}
		if (ruleTable.has('-unreleased') && item.isUnreleased) {
			problems.push(`${name}'s item ${set.item} is unreleased.`);
		}

		setHas[toId(set.ability)] = true;
		if (ruleTable.has('-illegal')) {
			// Don't check abilities for metagames with All Abilities
			if (dex.gen <= 2) {
				set.ability = 'None';
			} else if (!ruleTable.has('ignoreillegalabilities')) {
				if (!ability.name) {
					problems.push(`${name} needs to have an ability.`);
				} else if (!Object.values(template.abilities).includes(ability.name)) {
					problems.push(`${name} can't have ${set.ability}.`);
				}
				if (ability.name === template.abilities['H']) {
					isHidden = true;

					if (template.unreleasedHidden && ruleTable.has('-unreleased')) {
						problems.push(`${name}'s Hidden Ability is unreleased.`);
					} else if (['entei', 'suicune', 'raikou'].includes(template.id) && format.requirePlus) {
						problems.push(`${name}'s Hidden Ability is only available from Virtual Console, which is not allowed in this format.`);
					} else if (dex.gen === 6 && (set.species.endsWith('Orange') || set.species.endsWith('White')) && ability.name === 'Symbiosis') {
						problems.push(`${name}'s Hidden Ability is unreleased for the Orange and White forms.`);
					} else if (dex.gen === 5 && set.level < 10 && (template.maleOnlyHidden || template.gender === 'N')) {
						problems.push(`${name} must be at least level 10 with its Hidden Ability.`);
					}
					if (template.maleOnlyHidden) {
						set.gender = 'M';
						lsetData.sources = ['5D'];
					}
				}
			}
		}
		if (set.moves && Array.isArray(set.moves)) {
			set.moves = set.moves.filter(val => val);
		}
		if (!set.moves || !set.moves.length) {
			problems.push(`${name} has no moves.`);
			set.moves = [];
		}
		// A limit is imposed here to prevent too much engine strain or
		// too much layout deformation - to be exact, this is the limit
		// allowed in Custom Game.
		// The usual limit of 4 moves is handled elsewhere - currently
		// in the cartridge-compliant set validator: rulesets.js:pokemon
		if (set.moves.length > 24) {
			problems.push(`${name} has more than 24 moves, which the simulator can't handle.`);
			return problems;
		}

		set.ivs = Validator.fillStats(set.ivs, 31);
		let ivs = /** @type {StatsTable} */ (set.ivs);
		let maxedIVs = Object.values(ivs).every(stat => stat === 31);

		let lsetProblem = null;
		for (const moveName of set.moves) {
			if (!moveName) continue;
			let move = dex.getMove(Dex.getString(moveName));
			if (!move.exists) return [`"${move.name}" is an invalid move.`];
			banReason = ruleTable.check('move:' + move.id, setHas);
			if (banReason) {
				problems.push(`${name}'s move ${move.name} is ${banReason}.`);
			}

			// Note that we don't error out on multiple Hidden Power types
			// That is checked in rulesets.js rule Pokemon
			if (move.id === 'hiddenpower' && move.type !== 'Normal' && !set.hpType) {
				set.hpType = move.type;
			}

			if (ruleTable.has('-unreleased')) {
				if (move.isUnreleased) problems.push(`${name}'s move ${move.name} is unreleased.`);
			}

			if (ruleTable.has('-illegal')) {
				const checkLearnset = (ruleTable.checkLearnset && ruleTable.checkLearnset[0] || this.checkLearnset);
				lsetProblem = checkLearnset.call(this, move, template, lsetData, set);
				if (lsetProblem) {
					lsetProblem.moveName = move.name;
					break;
				}
			}
		}

		const canBottleCap = (dex.gen >= 7 && set.level === 100);
		if (set.hpType && maxedIVs && ruleTable.has('pokemon')) {
			if (dex.gen <= 2) {
				let HPdvs = dex.getType(set.hpType).HPdvs;
				ivs = set.ivs = {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30};
				for (let i in HPdvs) {
					// @ts-ignore TypeScript index signature bug
					ivs[i] = HPdvs[i] * 2;
				}
				ivs.hp = -1;
			} else if (!canBottleCap) {
				ivs = set.ivs = Validator.fillStats(dex.getType(set.hpType).HPivs, 31);
			}
		}
		if (set.hpType === 'Fighting' && ruleTable.has('pokemon')) {
			if (template.gen >= 6 && template.eggGroups[0] === 'Undiscovered' && !template.nfe && (template.baseSpecies !== 'Diancie' || !set.shiny)) {
				// Legendary Pokemon must have at least 3 perfect IVs in gen 6+
				problems.push(`${name} must not have Hidden Power Fighting because it starts with 3 perfect IVs because it's a gen 6+ legendary.`);
			}
		}
		const ivHpType = dex.getHiddenPower(set.ivs).type;
		if (!canBottleCap && ruleTable.has('pokemon') && set.hpType && set.hpType !== ivHpType) {
			problems.push(`${name} has Hidden Power ${set.hpType}, but its IVs are for Hidden Power ${ivHpType}.`);
		}
		if (dex.gen <= 2) {
			// validate DVs
			const atkDV = Math.floor(ivs.atk / 2);
			const defDV = Math.floor(ivs.def / 2);
			const speDV = Math.floor(ivs.spe / 2);
			const spcDV = Math.floor(ivs.spa / 2);
			const expectedHpDV = (atkDV % 2) * 8 + (defDV % 2) * 4 + (speDV % 2) * 2 + (spcDV % 2);
			if (ivs.hp === -1) ivs.hp = expectedHpDV * 2;
			const hpDV = Math.floor(ivs.hp / 2);
			if (expectedHpDV !== hpDV) {
				problems.push(`${name} has an HP DV of ${hpDV}, but its Atk, Def, Spe, and Spc DVs give it an HP DV of ${expectedHpDV}.`);
			}
			if (ivs.spa !== ivs.spd) {
				if (dex.gen === 2) {
					problems.push(`${name} has different SpA and SpD DVs, which is not possible in Gen 2.`);
				} else {
					ivs.spd = ivs.spa;
				}
			}
			if (dex.gen > 1 && !template.gender) {
				// Gen 2 gender is calculated from the Atk DV.
				// High Atk DV <-> M. The meaning of "high" depends on the gender ratio.
				let genderThreshold = template.genderRatio.F * 16;
				if (genderThreshold === 4) genderThreshold = 5;
				if (genderThreshold === 8) genderThreshold = 7;

				const expectedGender = (atkDV >= genderThreshold ? 'M' : 'F');
				if (set.gender && set.gender !== expectedGender) {
					problems.push(`${name} is ${set.gender}, but it has an Atk DV of ${atkDV}, which makes its gender ${expectedGender}.`);
				} else {
					set.gender = expectedGender;
				}
			}
			if (dex.gen > 1) {
				const expectedShiny = !!(defDV === 10 && speDV === 10 && spcDV === 10 && atkDV % 4 >= 2);
				if (expectedShiny && !set.shiny) {
					problems.push(`${name} is not shiny, which does not match its DVs.`);
				} else if (!expectedShiny && set.shiny) {
					problems.push(`${name} is shiny, which does not match its DVs (its DVs must all be 10, except Atk which must be 2, 3, 6, 7, 10, 11, 14, or 15).`);
				}
			}
		}
		if (dex.gen <= 2 || dex.gen !== 6 && (format.id.endsWith('hackmons') || format.name.includes('BH'))) {
			if (!set.evs) set.evs = Validator.fillStats(null, 252);
			let evTotal = (set.evs.hp || 0) + (set.evs.atk || 0) + (set.evs.def || 0) + (set.evs.spa || 0) + (set.evs.spd || 0) + (set.evs.spe || 0);
			if (evTotal === 508 || evTotal === 510) {
				problems.push(`${name} has exactly 510 EVs, but this format does not restrict you to 510 EVs: you can max out every EV (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`);
			}
		}
		// @ts-ignore TypeScript index signature bug
		if (set.evs && !Object.values(set.evs).some(value => value > 0)) {
			problems.push(`${name} has exactly 0 EVs - did you forget to EV it? (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`);
		}

		let lsetProblems = this.reconcileLearnset(template, lsetData, lsetProblem, name);
		if (lsetProblems) problems.push(...lsetProblems);

		if (isHidden && lsetData.sourcesBefore < 5) {
			lsetData.sources = lsetData.sources.filter(source =>
				parseInt(source.charAt(0)) >= 5
			);
			lsetData.sourcesBefore = 0;
			if (!lsetData.sources.length) {
				problems.push(`${name} has a hidden ability - it can't have moves only learned before gen 5.`);
			}
		}

		if (!lsetData.sourcesBefore && lsetData.sources.length && lsetData.sources.every(source => 'SVD'.includes(source.charAt(1)))) {
			// Every source is restricted
			let legal = false;
			for (const source of lsetData.sources) {
				if (this.validateSource(set, source, template)) continue;
				legal = true;
				break;
			}

			if (!legal) {
				if (lsetData.sources.length > 1) {
					problems.push(`${name} has an event-exclusive move that it doesn't qualify for (only one of several ways to get the move will be listed):`);
				}
				let eventProblems = this.validateSource(set, lsetData.sources[0], template, ` because it has a move only available`);
				// @ts-ignore TypeScript overload syntax bug
				if (eventProblems) problems.push(...eventProblems);
			}
		} else if (ruleTable.has('-illegal') && template.eventOnly) {
			let eventTemplate = !template.learnset && template.baseSpecies !== template.species ? dex.getTemplate(template.baseSpecies) : template;
			const eventPokemon = eventTemplate.eventPokemon;
			if (!eventPokemon) throw new Error(`Event-only template ${template.species} has no eventPokemon table`);
			let legal = false;
			for (const eventData of eventPokemon) {
				if (this.validateEvent(set, eventData, eventTemplate)) continue;
				legal = true;
				if (eventData.gender) set.gender = eventData.gender;
				break;
			}
			if (!legal && template.id === 'celebi' && dex.gen >= 7 && !this.validateSource(set, '7V', template)) {
				legal = true;
			}
			if (!legal) {
				if (eventPokemon.length === 1) {
					problems.push(`${template.species} is only obtainable from an event - it needs to match its event:`);
				} else {
					problems.push(`${template.species} is only obtainable from events - it needs to match one of its events, such as:`);
				}
				let eventData = eventPokemon[0];
				const minPastGen = (format.requirePlus ? 7 : format.requirePentagon ? 6 : 1);
				let eventNum = 1;
				for (let i = 0; i < eventPokemon.length; i++) {
					if (eventPokemon[i].generation <= dex.gen && eventPokemon[i].generation >= minPastGen) {
						eventData = eventPokemon[i];
						eventNum = i + 1;
						break;
					}
				}
				let eventName = eventPokemon.length > 1 ? ` #${eventNum}` : ``;
				let eventProblems = this.validateEvent(set, eventData, eventTemplate, ` to be`, `from its event${eventName}`);
				// @ts-ignore TypeScript overload syntax bug
				if (eventProblems) problems.push(...eventProblems);
			}
		}
		if (ruleTable.has('-illegal') && set.level < (template.evoLevel || 0)) {
			// FIXME: Event pokemon given at a level under what it normally can be attained at gives a false positive
			problems.push(`${name} must be at least level ${template.evoLevel} to be evolved.`);
		}
		if (!lsetData.sources && lsetData.sourcesBefore <= 3 && dex.getAbility(set.ability).gen === 4 && !template.prevo && dex.gen <= 5) {
			problems.push(`${name} has a gen 4 ability and isn't evolved - it can't use anything from gen 3.`);
		}
		if (!lsetData.sources && lsetData.sourcesBefore < 6 && lsetData.sourcesBefore >= 3 && (isHidden || dex.gen <= 5) && template.gen <= lsetData.sourcesBefore) {
			let oldAbilities = dex.mod('gen' + lsetData.sourcesBefore).getTemplate(set.species).abilities;
			if (ability.name !== oldAbilities['0'] && ability.name !== oldAbilities['1'] && !oldAbilities['H']) {
				problems.push(`${name} has moves incompatible with its ability.`);
			}
		}

		if (teamHas) {
			for (let i in setHas) {
				if (i in teamHas) {
					teamHas[i]++;
				} else {
					teamHas[i] = 1;
				}
			}
		}
		for (const [rule, source, limit, bans] of ruleTable.complexBans) {
			let count = 0;
			for (const ban of bans) {
				if (setHas[ban] > 0) {
					count += limit ? setHas[ban] : 1;
				}
			}
			if (limit && count > limit) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`${name} is limited to ${limit} of ${rule}${clause}.`);
			} else if (!limit && count >= bans.length) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`${name} has the combination of ${rule}, which is banned${clause}.`);
			}
		}

		for (const [rule] of ruleTable) {
			if (rule.startsWith('!')) continue;
			let subformat = dex.getFormat(rule);
			if (subformat.onValidateSet && ruleTable.has(subformat.id)) {
				problems = problems.concat(subformat.onValidateSet.call(dex, set, format, setHas, teamHas) || []);
			}
		}
		if (format.onValidateSet) {
			problems = problems.concat(format.onValidateSet.call(dex, set, format, setHas, teamHas) || []);
		}

		if (!problems.length) {
			if (forcedLevel) set.level = forcedLevel;
			return null;
		}

		return problems;
	}

	/**
	 * Returns array of error messages if invalid, undefined if valid
	 *
	 * If `because` is not passed, instead returns true if invalid.
	 * @param {PokemonSet} set
	 * @param {PokemonSource} source
	 * @param {Template} template
	 * @param {string} [because]
	 * @param {string} [from]
	 */
	validateSource(set, source, template, because, from) {
		let eventData = /** @type {?EventInfo} */ (null);
		let eventTemplate = template;
		if (source.charAt(1) === 'S') {
			let splitSource = source.substr(source.charAt(2) === 'T' ? 3 : 2).split(' ');
			eventTemplate = this.dex.getTemplate(splitSource[1]);
			if (eventTemplate.eventPokemon) eventData = eventTemplate.eventPokemon[parseInt(splitSource[0])];
			if (!eventData) {
				throw new Error(`${eventTemplate.species} from ${template.species} doesn't have data for event ${source}`);
			}
		} else if (source.charAt(1) === 'V') {
			const isMew = template.speciesid === 'mew';
			const isCelebi = template.speciesid === 'celebi';
			eventData = {
				generation: 2,
				level: isMew ? 5 : isCelebi ? 30 : undefined),
				perfectIVs: isMew || isCelebi ? 5 : 3,
				isHidden: true,
				shiny: isMew ? undefined : 1,
				pokeball: 'pokeball',
				from: 'Gen 1-2 Virtual Console transfer',
			};
		} else if (source.charAt(1) === 'D') {
			eventData = {
				generation: 5,
				level: 10,
				from: 'Gen 5 Dream World',<commit_msg>Correctly refactor Mew/Celebi checks<commit_after>/**
 * Team Validator
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * Handles team validation, and specifically learnset checking.
 *
 * @license MIT
 */

'use strict';

/**
 * Keeps track of how a pokemon with a given set might be obtained.
 *
 * `sources` is a list of possible PokemonSources, and a nonzero
 * sourcesBefore means the Pokemon is compatible with all possible
 * PokemonSources from that gen or earlier.
 *
 * `limitedEgg` tracks moves that can only be obtained from an egg with
 * another father in gen 2-5. If there are multiple such moves,
 * potential fathers need to be checked to see if they can actually
 * learn the move combination in question.
 *
 * @typedef {Object} PokemonSources
 * @property {PokemonSource[]} sources
 * @property {number} sourcesBefore
 * @property {string} [babyOnly]
 * @property {string} [sketchMove] limit 1 in fakemon Sketch-as-egg-move formats
 * @property {string} [hm] limit 1 HM transferred from gen 4 to 5
 * @property {(string | 'self')[]} [limitedEgg] list of egg moves
 * @property {true} [fastCheck]
 */

class Validator {
	/**
	 * @param {string | Format} format
	 */
	constructor(format) {
		this.format = Dex.getFormat(format);
		this.dex = Dex.forFormat(this.format);
		this.ruleTable = this.dex.getRuleTable(this.format);
	}

	/**
	 * @param {PokemonSet[]?} team
	 * @return {string[]?}
	 */
	validateTeam(team, removeNicknames = false) {
		if (this.format.validateTeam) return this.format.validateTeam.call(this, team, removeNicknames);
		return this.baseValidateTeam(team, removeNicknames);
	}

	/**
	 * @param {PokemonSet[]?} team
	 * @return {string[]?}
	 */
	baseValidateTeam(team, removeNicknames = false) {
		let format = this.format;
		let dex = this.dex;

		let problems = /** @type {string[]} */ ([]);
		const ruleTable = this.ruleTable;
		if (format.team) {
			return null;
		}
		if (!team || !Array.isArray(team)) {
			if (format.canUseRandomTeam) {
				return null;
			}
			return [`You sent invalid team data. If you're not using a custom client, please report this as a bug.`];
		}

		let lengthRange = format.teamLength && format.teamLength.validate;
		if (!lengthRange) lengthRange = [1, 6];
		if (format.gameType === 'doubles' && lengthRange[0] < 2) lengthRange[0] = 2;
		if ((format.gameType === 'triples' || format.gameType === 'rotation') && lengthRange[0] < 3) lengthRange[0] = 3;
		if (team.length < lengthRange[0]) problems.push(`You must bring at least ${lengthRange[0]} Pok\u00E9mon.`);
		if (team.length > lengthRange[1]) return [`You may only bring up to ${lengthRange[1]} Pok\u00E9mon.`];

		// A limit is imposed here to prevent too much engine strain or
		// too much layout deformation - to be exact, this is the limit
		// allowed in Custom Game.
		// The usual limit of 6 pokemon is handled elsewhere - currently
		// in the cartridge-compliant set validator: rulesets.js:pokemon
		if (team.length > 24) {
			problems.push(`Your team has more than than 24 Pok\u00E9mon, which the simulator can't handle.`);
			return problems;
		}

		let teamHas = {};
		for (let i = 0; i < team.length; i++) { // Changing this loop to for-of would require another loop/map statement to do removeNicknames
			if (!team[i]) return [`You sent invalid team data. If you're not using a custom client, please report this as a bug.`];
			let setProblems = (format.validateSet || this.validateSet).call(this, team[i], teamHas);
			if (setProblems) {
				problems = problems.concat(setProblems);
			}
			if (removeNicknames) team[i].name = dex.getTemplate(team[i].species).baseSpecies;
		}

		for (const [rule, source, limit, bans] of ruleTable.complexTeamBans) {
			let count = 0;
			for (const ban of bans) {
				if (teamHas[ban] > 0) {
					count += limit ? teamHas[ban] : 1;
				}
			}
			if (limit && count > limit) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`You are limited to ${limit} of ${rule}${clause}.`);
			} else if (!limit && count >= bans.length) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`Your team has the combination of ${rule}, which is banned${clause}.`);
			}
		}

		for (const [rule] of ruleTable) {
			let subformat = dex.getFormat(rule);
			if (subformat.onValidateTeam && ruleTable.has(subformat.id)) {
				problems = problems.concat(subformat.onValidateTeam.call(dex, team, format, teamHas) || []);
			}
		}
		if (format.onValidateTeam) {
			problems = problems.concat(format.onValidateTeam.call(dex, team, format, teamHas) || []);
		}

		if (!problems.length) return null;
		return problems;
	}

	/**
	 * @param {PokemonSet} set
	 * @param {AnyObject} teamHas
	 * @return {string[]?}
	 */
	validateSet(set, teamHas) {
		let format = this.format;
		let dex = this.dex;

		let problems = /** @type {string[]} */ ([]);
		if (!set) {
			return [`This is not a Pokemon.`];
		}

		let template = dex.getTemplate(set.species);
		set.species = Dex.getSpecies(set.species);
		set.name = dex.getName(set.name);
		let item = dex.getItem(Dex.getString(set.item));
		set.item = item.name;
		let ability = dex.getAbility(Dex.getString(set.ability));
		set.ability = ability.name;
		set.nature = dex.getNature(Dex.getString(set.nature)).name;
		if (!Array.isArray(set.moves)) set.moves = [];

		let maxLevel = format.maxLevel || 100;
		let maxForcedLevel = format.maxForcedLevel || maxLevel;
		let forcedLevel = /** @type {number?} */ (null);
		if (!set.level) {
			set.level = (format.defaultLevel || maxLevel);
		}
		if (format.forcedLevel) {
			forcedLevel = format.forcedLevel;
		} else if (set.level >= maxForcedLevel) {
			forcedLevel = maxForcedLevel;
		}
		if (set.level > maxLevel || set.level === forcedLevel || set.level === maxForcedLevel) {
			// Note that we're temporarily setting level 50 pokemon in VGC to level 100
			// This allows e.g. level 50 Hydreigon even though it doesn't evolve until level 64.
			// Leveling up can't make an obtainable pokemon unobtainable, so this is safe.
			// Just remember to set the level back to forcedLevel at the end of the file.
			set.level = maxLevel;
		}

		let nameTemplate = dex.getTemplate(set.name);
		if (nameTemplate.exists && nameTemplate.name.toLowerCase() === set.name.toLowerCase()) {
			// Name must not be the name of another pokemon
			// @ts-ignore
			set.name = null;
		}
		set.name = set.name || template.baseSpecies;
		let name = set.species;
		if (set.species !== set.name && template.baseSpecies !== set.name) name = `${set.name} (${set.species})`;
		let isHidden = false;
		let lsetData = /** @type {PokemonSources} */ ({sources: [], sourcesBefore: dex.gen});

		let setHas = {};
		const ruleTable = this.ruleTable;

		for (const [rule] of ruleTable) {
			let subformat = dex.getFormat(rule);
			if (subformat.onChangeSet && ruleTable.has(subformat.id)) {
				problems = problems.concat(subformat.onChangeSet.call(dex, set, format) || []);
			}
		}
		if (format.onChangeSet) {
			problems = problems.concat(format.onChangeSet.call(dex, set, format, setHas, teamHas) || []);
		}

		// onChangeSet can modify set.species, set.item, set.ability
		template = dex.getTemplate(set.species);
		item = dex.getItem(set.item);
		ability = dex.getAbility(set.ability);

		if (ability.id === 'battlebond' && template.id === 'greninja' && !ruleTable.has('ignoreillegalabilities')) {
			template = dex.getTemplate('greninjaash');
			if (set.gender && set.gender !== 'M') {
				problems.push(`Battle Bond Greninja must be male.`);
			}
			set.gender = 'M';
		}
		if (ability.id === 'owntempo' && template.id === 'rockruff') {
			template = dex.getTemplate('rockruffdusk');
		}
		if (!template.exists) {
			return [`The Pokemon "${set.species}" does not exist.`];
		}

		if (item.id && !item.exists) {
			return [`"${set.item}" is an invalid item.`];
		}
		if (ability.id && !ability.exists) {
			if (dex.gen < 3) {
				// gen 1-2 don't have abilities, just silently remove
				ability = dex.getAbility('');
				set.ability = '';
			} else {
				return [`"${set.ability}" is an invalid ability.`];
			}
		}
		if (set.nature && !dex.getNature(set.nature).exists) {
			if (dex.gen < 3) {
				// gen 1-2 don't have natures, just remove them
				set.nature = '';
			} else {
				problems.push(`${name}'s nature is invalid.`);
			}
		}
		if (set.happiness !== undefined && isNaN(set.happiness)) {
			problems.push(`${name} has an invalid happiness.`);
		}

		let banReason = ruleTable.check('pokemon:' + template.id, setHas);
		let templateOverride = ruleTable.has('+pokemon:' + template.id);
		if (!templateOverride) {
			banReason = banReason || ruleTable.check('basepokemon:' + toId(template.baseSpecies), setHas);
		}
		if (banReason) {
			return [`${set.species} is ${banReason}.`];
		}
		templateOverride = templateOverride || ruleTable.has('+basepokemon:' + toId(template.baseSpecies));
		let postMegaTemplate = template;
		if (item.megaEvolves === template.species) {
			if (!item.megaStone) throw new Error(`Item ${item.name} has no base form for mega evolution`);
			postMegaTemplate = dex.getTemplate(item.megaStone);
		}
		if (['Mega', 'Mega-X', 'Mega-Y'].includes(postMegaTemplate.forme)) {
			banReason = ruleTable.check('pokemontag:mega', setHas);
			const megaTemplateOverride = ruleTable.has('+pokemon:' + postMegaTemplate.id);
			if (megaTemplateOverride) {
				templateOverride = true;
			} else if (banReason) {
				problems.push(`Mega evolutions are ${banReason}.`);
			} else {
				banReason = ruleTable.check('pokemon:' + postMegaTemplate.id, setHas);
				if (banReason) problems.push(`${postMegaTemplate.species} is ${banReason}.`);
			}
		}
		if (!templateOverride && postMegaTemplate.tier) {
			banReason = ruleTable.check('pokemontag:' + toId(postMegaTemplate.tier), setHas);
			if (banReason) {
				problems.push(`${postMegaTemplate.species} is in ${postMegaTemplate.tier}, which is ${banReason}.`);
			}
		}
		if (!templateOverride && postMegaTemplate.doublesTier) {
			banReason = ruleTable.check('pokemontag:' + toId(postMegaTemplate.doublesTier), setHas);
			if (banReason) {
				problems.push(`${postMegaTemplate.species} is in ${postMegaTemplate.doublesTier}, which is ${banReason}.`);
			}
		}
		if (!templateOverride && ruleTable.has('-unreleased') && postMegaTemplate.isUnreleased) {
			problems.push(`${name} (${postMegaTemplate.species}) is unreleased.`);
		}

		banReason = ruleTable.check('ability:' + toId(set.ability), setHas);
		if (banReason) {
			problems.push(`${name}'s ability ${set.ability} is ${banReason}.`);
		}
		banReason = ruleTable.check('item:' + toId(set.item), setHas);
		if (banReason) {
			problems.push(`${name}'s item ${set.item} is ${banReason}.`);
		}
		if (ruleTable.has('-unreleased') && item.isUnreleased) {
			problems.push(`${name}'s item ${set.item} is unreleased.`);
		}

		setHas[toId(set.ability)] = true;
		if (ruleTable.has('-illegal')) {
			// Don't check abilities for metagames with All Abilities
			if (dex.gen <= 2) {
				set.ability = 'None';
			} else if (!ruleTable.has('ignoreillegalabilities')) {
				if (!ability.name) {
					problems.push(`${name} needs to have an ability.`);
				} else if (!Object.values(template.abilities).includes(ability.name)) {
					problems.push(`${name} can't have ${set.ability}.`);
				}
				if (ability.name === template.abilities['H']) {
					isHidden = true;

					if (template.unreleasedHidden && ruleTable.has('-unreleased')) {
						problems.push(`${name}'s Hidden Ability is unreleased.`);
					} else if (['entei', 'suicune', 'raikou'].includes(template.id) && format.requirePlus) {
						problems.push(`${name}'s Hidden Ability is only available from Virtual Console, which is not allowed in this format.`);
					} else if (dex.gen === 6 && (set.species.endsWith('Orange') || set.species.endsWith('White')) && ability.name === 'Symbiosis') {
						problems.push(`${name}'s Hidden Ability is unreleased for the Orange and White forms.`);
					} else if (dex.gen === 5 && set.level < 10 && (template.maleOnlyHidden || template.gender === 'N')) {
						problems.push(`${name} must be at least level 10 with its Hidden Ability.`);
					}
					if (template.maleOnlyHidden) {
						set.gender = 'M';
						lsetData.sources = ['5D'];
					}
				}
			}
		}
		if (set.moves && Array.isArray(set.moves)) {
			set.moves = set.moves.filter(val => val);
		}
		if (!set.moves || !set.moves.length) {
			problems.push(`${name} has no moves.`);
			set.moves = [];
		}
		// A limit is imposed here to prevent too much engine strain or
		// too much layout deformation - to be exact, this is the limit
		// allowed in Custom Game.
		// The usual limit of 4 moves is handled elsewhere - currently
		// in the cartridge-compliant set validator: rulesets.js:pokemon
		if (set.moves.length > 24) {
			problems.push(`${name} has more than 24 moves, which the simulator can't handle.`);
			return problems;
		}

		set.ivs = Validator.fillStats(set.ivs, 31);
		let ivs = /** @type {StatsTable} */ (set.ivs);
		let maxedIVs = Object.values(ivs).every(stat => stat === 31);

		let lsetProblem = null;
		for (const moveName of set.moves) {
			if (!moveName) continue;
			let move = dex.getMove(Dex.getString(moveName));
			if (!move.exists) return [`"${move.name}" is an invalid move.`];
			banReason = ruleTable.check('move:' + move.id, setHas);
			if (banReason) {
				problems.push(`${name}'s move ${move.name} is ${banReason}.`);
			}

			// Note that we don't error out on multiple Hidden Power types
			// That is checked in rulesets.js rule Pokemon
			if (move.id === 'hiddenpower' && move.type !== 'Normal' && !set.hpType) {
				set.hpType = move.type;
			}

			if (ruleTable.has('-unreleased')) {
				if (move.isUnreleased) problems.push(`${name}'s move ${move.name} is unreleased.`);
			}

			if (ruleTable.has('-illegal')) {
				const checkLearnset = (ruleTable.checkLearnset && ruleTable.checkLearnset[0] || this.checkLearnset);
				lsetProblem = checkLearnset.call(this, move, template, lsetData, set);
				if (lsetProblem) {
					lsetProblem.moveName = move.name;
					break;
				}
			}
		}

		const canBottleCap = (dex.gen >= 7 && set.level === 100);
		if (set.hpType && maxedIVs && ruleTable.has('pokemon')) {
			if (dex.gen <= 2) {
				let HPdvs = dex.getType(set.hpType).HPdvs;
				ivs = set.ivs = {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30};
				for (let i in HPdvs) {
					// @ts-ignore TypeScript index signature bug
					ivs[i] = HPdvs[i] * 2;
				}
				ivs.hp = -1;
			} else if (!canBottleCap) {
				ivs = set.ivs = Validator.fillStats(dex.getType(set.hpType).HPivs, 31);
			}
		}
		if (set.hpType === 'Fighting' && ruleTable.has('pokemon')) {
			if (template.gen >= 6 && template.eggGroups[0] === 'Undiscovered' && !template.nfe && (template.baseSpecies !== 'Diancie' || !set.shiny)) {
				// Legendary Pokemon must have at least 3 perfect IVs in gen 6+
				problems.push(`${name} must not have Hidden Power Fighting because it starts with 3 perfect IVs because it's a gen 6+ legendary.`);
			}
		}
		const ivHpType = dex.getHiddenPower(set.ivs).type;
		if (!canBottleCap && ruleTable.has('pokemon') && set.hpType && set.hpType !== ivHpType) {
			problems.push(`${name} has Hidden Power ${set.hpType}, but its IVs are for Hidden Power ${ivHpType}.`);
		}
		if (dex.gen <= 2) {
			// validate DVs
			const atkDV = Math.floor(ivs.atk / 2);
			const defDV = Math.floor(ivs.def / 2);
			const speDV = Math.floor(ivs.spe / 2);
			const spcDV = Math.floor(ivs.spa / 2);
			const expectedHpDV = (atkDV % 2) * 8 + (defDV % 2) * 4 + (speDV % 2) * 2 + (spcDV % 2);
			if (ivs.hp === -1) ivs.hp = expectedHpDV * 2;
			const hpDV = Math.floor(ivs.hp / 2);
			if (expectedHpDV !== hpDV) {
				problems.push(`${name} has an HP DV of ${hpDV}, but its Atk, Def, Spe, and Spc DVs give it an HP DV of ${expectedHpDV}.`);
			}
			if (ivs.spa !== ivs.spd) {
				if (dex.gen === 2) {
					problems.push(`${name} has different SpA and SpD DVs, which is not possible in Gen 2.`);
				} else {
					ivs.spd = ivs.spa;
				}
			}
			if (dex.gen > 1 && !template.gender) {
				// Gen 2 gender is calculated from the Atk DV.
				// High Atk DV <-> M. The meaning of "high" depends on the gender ratio.
				let genderThreshold = template.genderRatio.F * 16;
				if (genderThreshold === 4) genderThreshold = 5;
				if (genderThreshold === 8) genderThreshold = 7;

				const expectedGender = (atkDV >= genderThreshold ? 'M' : 'F');
				if (set.gender && set.gender !== expectedGender) {
					problems.push(`${name} is ${set.gender}, but it has an Atk DV of ${atkDV}, which makes its gender ${expectedGender}.`);
				} else {
					set.gender = expectedGender;
				}
			}
			if (dex.gen > 1) {
				const expectedShiny = !!(defDV === 10 && speDV === 10 && spcDV === 10 && atkDV % 4 >= 2);
				if (expectedShiny && !set.shiny) {
					problems.push(`${name} is not shiny, which does not match its DVs.`);
				} else if (!expectedShiny && set.shiny) {
					problems.push(`${name} is shiny, which does not match its DVs (its DVs must all be 10, except Atk which must be 2, 3, 6, 7, 10, 11, 14, or 15).`);
				}
			}
		}
		if (dex.gen <= 2 || dex.gen !== 6 && (format.id.endsWith('hackmons') || format.name.includes('BH'))) {
			if (!set.evs) set.evs = Validator.fillStats(null, 252);
			let evTotal = (set.evs.hp || 0) + (set.evs.atk || 0) + (set.evs.def || 0) + (set.evs.spa || 0) + (set.evs.spd || 0) + (set.evs.spe || 0);
			if (evTotal === 508 || evTotal === 510) {
				problems.push(`${name} has exactly 510 EVs, but this format does not restrict you to 510 EVs: you can max out every EV (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`);
			}
		}
		// @ts-ignore TypeScript index signature bug
		if (set.evs && !Object.values(set.evs).some(value => value > 0)) {
			problems.push(`${name} has exactly 0 EVs - did you forget to EV it? (If this was intentional, add exactly 1 to one of your EVs, which won't change its stats but will tell us that it wasn't a mistake).`);
		}

		let lsetProblems = this.reconcileLearnset(template, lsetData, lsetProblem, name);
		if (lsetProblems) problems.push(...lsetProblems);

		if (isHidden && lsetData.sourcesBefore < 5) {
			lsetData.sources = lsetData.sources.filter(source =>
				parseInt(source.charAt(0)) >= 5
			);
			lsetData.sourcesBefore = 0;
			if (!lsetData.sources.length) {
				problems.push(`${name} has a hidden ability - it can't have moves only learned before gen 5.`);
			}
		}

		if (!lsetData.sourcesBefore && lsetData.sources.length && lsetData.sources.every(source => 'SVD'.includes(source.charAt(1)))) {
			// Every source is restricted
			let legal = false;
			for (const source of lsetData.sources) {
				if (this.validateSource(set, source, template)) continue;
				legal = true;
				break;
			}

			if (!legal) {
				if (lsetData.sources.length > 1) {
					problems.push(`${name} has an event-exclusive move that it doesn't qualify for (only one of several ways to get the move will be listed):`);
				}
				let eventProblems = this.validateSource(set, lsetData.sources[0], template, ` because it has a move only available`);
				// @ts-ignore TypeScript overload syntax bug
				if (eventProblems) problems.push(...eventProblems);
			}
		} else if (ruleTable.has('-illegal') && template.eventOnly) {
			let eventTemplate = !template.learnset && template.baseSpecies !== template.species ? dex.getTemplate(template.baseSpecies) : template;
			const eventPokemon = eventTemplate.eventPokemon;
			if (!eventPokemon) throw new Error(`Event-only template ${template.species} has no eventPokemon table`);
			let legal = false;
			for (const eventData of eventPokemon) {
				if (this.validateEvent(set, eventData, eventTemplate)) continue;
				legal = true;
				if (eventData.gender) set.gender = eventData.gender;
				break;
			}
			if (!legal && template.id === 'celebi' && dex.gen >= 7 && !this.validateSource(set, '7V', template)) {
				legal = true;
			}
			if (!legal) {
				if (eventPokemon.length === 1) {
					problems.push(`${template.species} is only obtainable from an event - it needs to match its event:`);
				} else {
					problems.push(`${template.species} is only obtainable from events - it needs to match one of its events, such as:`);
				}
				let eventData = eventPokemon[0];
				const minPastGen = (format.requirePlus ? 7 : format.requirePentagon ? 6 : 1);
				let eventNum = 1;
				for (let i = 0; i < eventPokemon.length; i++) {
					if (eventPokemon[i].generation <= dex.gen && eventPokemon[i].generation >= minPastGen) {
						eventData = eventPokemon[i];
						eventNum = i + 1;
						break;
					}
				}
				let eventName = eventPokemon.length > 1 ? ` #${eventNum}` : ``;
				let eventProblems = this.validateEvent(set, eventData, eventTemplate, ` to be`, `from its event${eventName}`);
				// @ts-ignore TypeScript overload syntax bug
				if (eventProblems) problems.push(...eventProblems);
			}
		}
		if (ruleTable.has('-illegal') && set.level < (template.evoLevel || 0)) {
			// FIXME: Event pokemon given at a level under what it normally can be attained at gives a false positive
			problems.push(`${name} must be at least level ${template.evoLevel} to be evolved.`);
		}
		if (!lsetData.sources && lsetData.sourcesBefore <= 3 && dex.getAbility(set.ability).gen === 4 && !template.prevo && dex.gen <= 5) {
			problems.push(`${name} has a gen 4 ability and isn't evolved - it can't use anything from gen 3.`);
		}
		if (!lsetData.sources && lsetData.sourcesBefore < 6 && lsetData.sourcesBefore >= 3 && (isHidden || dex.gen <= 5) && template.gen <= lsetData.sourcesBefore) {
			let oldAbilities = dex.mod('gen' + lsetData.sourcesBefore).getTemplate(set.species).abilities;
			if (ability.name !== oldAbilities['0'] && ability.name !== oldAbilities['1'] && !oldAbilities['H']) {
				problems.push(`${name} has moves incompatible with its ability.`);
			}
		}

		if (teamHas) {
			for (let i in setHas) {
				if (i in teamHas) {
					teamHas[i]++;
				} else {
					teamHas[i] = 1;
				}
			}
		}
		for (const [rule, source, limit, bans] of ruleTable.complexBans) {
			let count = 0;
			for (const ban of bans) {
				if (setHas[ban] > 0) {
					count += limit ? setHas[ban] : 1;
				}
			}
			if (limit && count > limit) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`${name} is limited to ${limit} of ${rule}${clause}.`);
			} else if (!limit && count >= bans.length) {
				const clause = source ? ` by ${source}` : ``;
				problems.push(`${name} has the combination of ${rule}, which is banned${clause}.`);
			}
		}

		for (const [rule] of ruleTable) {
			if (rule.startsWith('!')) continue;
			let subformat = dex.getFormat(rule);
			if (subformat.onValidateSet && ruleTable.has(subformat.id)) {
				problems = problems.concat(subformat.onValidateSet.call(dex, set, format, setHas, teamHas) || []);
			}
		}
		if (format.onValidateSet) {
			problems = problems.concat(format.onValidateSet.call(dex, set, format, setHas, teamHas) || []);
		}

		if (!problems.length) {
			if (forcedLevel) set.level = forcedLevel;
			return null;
		}

		return problems;
	}

	/**
	 * Returns array of error messages if invalid, undefined if valid
	 *
	 * If `because` is not passed, instead returns true if invalid.
	 * @param {PokemonSet} set
	 * @param {PokemonSource} source
	 * @param {Template} template
	 * @param {string} [because]
	 * @param {string} [from]
	 */
	validateSource(set, source, template, because, from) {
		let eventData = /** @type {?EventInfo} */ (null);
		let eventTemplate = template;
		if (source.charAt(1) === 'S') {
			let splitSource = source.substr(source.charAt(2) === 'T' ? 3 : 2).split(' ');
			eventTemplate = this.dex.getTemplate(splitSource[1]);
			if (eventTemplate.eventPokemon) eventData = eventTemplate.eventPokemon[parseInt(splitSource[0])];
			if (!eventData) {
				throw new Error(`${eventTemplate.species} from ${template.species} doesn't have data for event ${source}`);
			}
		} else if (source.charAt(1) === 'V') {
			const isMew = template.speciesid === 'mew';
			const isCelebi = template.speciesid === 'celebi';
			eventData = {
				generation: 2,
				level: isMew ? 5 : isCelebi ? 30 : undefined,
				perfectIVs: isMew || isCelebi ? 5 : 3,
				isHidden: true,
				shiny: isMew ? undefined : 1,
				pokeball: 'pokeball',
				from: 'Gen 1-2 Virtual Console transfer',
			};
		} else if (source.charAt(1) === 'D') {
			eventData = {
				generation: 5,
				level: 10,
				from: 'Gen 5 Dream World',
{'fraction_non_alphanumeric': 0.1582924836601307, 'fraction_numerical': 0.009436274509803921, 'mean_word_length': 3.4392964003989484, 'pattern_counts': {'":': 0, '<': 45, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6235492', 'n_tokens_mistral': 19410, 'n_tokens_neox': 16372, 'n_words': 4556}