Documentation Index
Fetch the complete documentation index at: https://mintlify.com/kwsch/PKHeX/llms.txt
Use this file to discover all available pages before exploring further.
External Legality Checkers
PKHeX allows you to register custom validation logic that runs after the standard legality checks. This is done through theIExternalLegalityChecker interface.
Creating a Custom Checker
Step 1: Implement the Interface
using PKHeX.Core;
public class MyCustomChecker : IExternalLegalityChecker
{
// Unique identifier for this checker
public ushort Identity => 1000; // Use a unique number
// Display name
public string Name => "My Custom Validator";
// Perform validation
public void Verify(LegalityAnalysis analysis)
{
// Your custom validation logic here
var pk = analysis.Entity;
// Example: Check if nickname contains profanity
if (ContainsProfanity(pk.Nickname))
{
var result = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = CheckIdentifier.Nickname,
Result = LegalityCheckResultCode.External,
Value = Identity // Store our identity for localization
};
analysis.AddLine(result);
}
}
// Provide localized messages
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
// Return custom message for your results
return "Nickname contains inappropriate content.";
}
private bool ContainsProfanity(string text)
{
// Your implementation
return false;
}
}
Step 2: Register the Checker
using PKHeX.Core;
public class Program
{
static Program()
{
// Register at application startup
var checker = new MyCustomChecker();
ExternalLegalityCheck.RegisterChecker(checker);
}
static void Main()
{
// Now all legality checks will include your custom validator
var pk = LoadPokemon();
var analysis = new LegalityAnalysis(pk);
// Your checker ran automatically
if (!analysis.Valid)
{
Console.WriteLine("Validation failed (including custom checks)");
}
}
}
Example: Competitive Battle Validator
Validate Pokémon for competitive battle format rules:using PKHeX.Core;
using System.Collections.Generic;
using System.Linq;
public class CompetitiveBattleChecker : IExternalLegalityChecker
{
public ushort Identity => 2000;
public string Name => "Competitive Battle Validator";
private static readonly HashSet<ushort> BannedSpecies = new()
{
150, // Mewtwo
382, // Kyogre
383, // Groudon
384, // Rayquaza
// ... etc
};
private static readonly HashSet<ushort> BannedAbilities = new()
{
95, // Moody
// ... etc
};
private static readonly HashSet<ushort> BannedMoves = new()
{
560, // Dark Void
// ... etc
};
public void Verify(LegalityAnalysis analysis)
{
var pk = analysis.Entity;
// Check for banned species
if (BannedSpecies.Contains(pk.Species))
{
AddInvalid(analysis, CheckIdentifier.Encounter,
"Species is banned in competitive play.");
}
// Check for banned abilities
if (BannedAbilities.Contains((ushort)pk.Ability))
{
AddInvalid(analysis, CheckIdentifier.Ability,
"Ability is banned in competitive play.");
}
// Check for banned moves
var moves = new[] { pk.Move1, pk.Move2, pk.Move3, pk.Move4 };
foreach (var move in moves)
{
if (BannedMoves.Contains(move))
{
AddInvalid(analysis, CheckIdentifier.CurrentMove,
$"Move {move} is banned in competitive play.");
}
}
// Check level restriction (e.g., Level 50 for VGC)
if (pk.CurrentLevel > 50)
{
AddInvalid(analysis, CheckIdentifier.Level,
"Level must be 50 or lower for VGC.");
}
}
private void AddInvalid(LegalityAnalysis analysis, CheckIdentifier identifier, string message)
{
var result = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = identifier,
Result = LegalityCheckResultCode.External,
Value = Identity
};
analysis.AddLine(result);
// Store message for localization
_messages[identifier] = message;
}
private Dictionary<CheckIdentifier, string> _messages = new();
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
if (_messages.TryGetValue(chk.Identifier, out var message))
return message;
return "Failed competitive battle validation.";
}
}
Example: Event Distribution Validator
Validate event Pokémon against distribution dates:using PKHeX.Core;
using System;
public class EventDateChecker : IExternalLegalityChecker
{
public ushort Identity => 3000;
public string Name => "Event Date Validator";
public void Verify(LegalityAnalysis analysis)
{
var pk = analysis.Entity;
var encounter = analysis.EncounterMatch;
// Only check Mystery Gifts
if (encounter is not MysteryGift gift)
return;
// Check if the met date is within distribution window
if (pk is IEncounterDate dated)
{
var metDate = new DateTime(dated.MetYear + 2000, dated.MetMonth, dated.MetDay);
// Example: Check against known distribution dates
// You would load this from a database
var distributionStart = new DateTime(2023, 1, 1);
var distributionEnd = new DateTime(2023, 12, 31);
if (metDate < distributionStart || metDate > distributionEnd)
{
var result = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = CheckIdentifier.Encounter,
Result = LegalityCheckResultCode.DateOutsideDistributionWindow,
Value = Identity
};
analysis.AddLine(result);
}
}
}
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
return "Met date is outside the event distribution window.";
}
}
Example: Shiny Lock Validator
Add additional shiny lock validation:using PKHeX.Core;
using System.Collections.Generic;
public class ShinyLockChecker : IExternalLegalityChecker
{
public ushort Identity => 4000;
public string Name => "Shiny Lock Validator";
// Species that are shiny locked in certain games
private static readonly Dictionary<ushort, EntityContext> ShinyLocks = new()
{
{ 249, EntityContext.Gen7 }, // Lugia in USUM
{ 250, EntityContext.Gen7 }, // Ho-Oh in USUM
{ 716, EntityContext.Gen7 }, // Xerneas in USUM
{ 717, EntityContext.Gen7 }, // Yveltal in USUM
// ... etc
};
public void Verify(LegalityAnalysis analysis)
{
var pk = analysis.Entity;
if (!pk.IsShiny)
return; // Not shiny, no problem
var encounter = analysis.EncounterMatch;
// Check if this species has a shiny lock
if (ShinyLocks.TryGetValue(pk.Species, out var lockedContext))
{
if (encounter.Context == lockedContext)
{
var result = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = CheckIdentifier.Shiny,
Result = LegalityCheckResultCode.External,
Value = Identity
};
analysis.AddLine(result);
}
}
}
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
return "This Pokémon is shiny locked in its origin game.";
}
}
Example: Ribbon Validator
Validate that ribbon combinations are possible:using PKHeX.Core;
public class RibbonLogicChecker : IExternalLegalityChecker
{
public ushort Identity => 5000;
public string Name => "Ribbon Logic Validator";
public void Verify(LegalityAnalysis analysis)
{
var pk = analysis.Entity;
// Example: Champion Ribbon requires beating Elite Four
// but Partner Ribbon is only for starter Pokémon
if (pk is IRibbonSetCommon6 ribbons)
{
if (ribbons.RibbonChampionKalos && ribbons.RibbonPartner)
{
// This combination might be suspicious
var result = new CheckResult
{
Judgement = Severity.Fishy,
Identifier = CheckIdentifier.Ribbon,
Result = LegalityCheckResultCode.External,
Value = Identity
};
analysis.AddLine(result);
}
}
// Check for impossible ribbon counts
if (pk is IRibbonSetCommon8 ribbons8)
{
int ribbonCount = CountRibbons(ribbons8);
// Example: No single Pokémon should have every ribbon
if (ribbonCount > 50) // Arbitrary threshold
{
var result = new CheckResult
{
Judgement = Severity.Fishy,
Identifier = CheckIdentifier.Ribbon,
Result = LegalityCheckResultCode.External,
Value = Identity
};
analysis.AddLine(result);
}
}
}
private int CountRibbons(object ribbons)
{
// Count non-zero ribbon properties
int count = 0;
var type = ribbons.GetType();
foreach (var prop in type.GetProperties())
{
if (prop.PropertyType == typeof(bool))
{
if ((bool)prop.GetValue(ribbons))
count++;
}
}
return count;
}
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
return "Ribbon combination is unusual or impossible.";
}
}
Accessing Analysis Data
Your checker has full access to the analysis:public void Verify(LegalityAnalysis analysis)
{
// The Pokémon being checked
var pk = analysis.Entity;
// Personal info (species data)
var personal = analysis.PersonalInfo;
// Matched encounter
var encounter = analysis.EncounterMatch;
var original = analysis.EncounterOriginal;
// Where it came from
var origin = analysis.SlotOrigin;
// Detailed info
var info = analysis.Info;
var generation = info.Generation;
var moves = info.Moves;
var relearn = info.Relearn;
var evolutions = info.EvoChainsAllGens;
var pidiv = info.PIDIV;
// Existing check results
var results = analysis.Results;
// You can check what other verifiers found
var abilityCheck = results.FirstOrDefault(r =>
r.Identifier == CheckIdentifier.Ability);
if (abilityCheck.Valid)
{
// Ability passed standard checks
}
}
Creating Check Results
public void Verify(LegalityAnalysis analysis)
{
// Invalid result
var invalid = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = CheckIdentifier.Misc,
Result = LegalityCheckResultCode.External,
Value = Identity // Your checker ID
};
// Fishy/suspicious result
var fishy = new CheckResult
{
Judgement = Severity.Fishy,
Identifier = CheckIdentifier.Trainer,
Result = LegalityCheckResultCode.External,
Value = Identity
};
// Valid with custom message
var valid = new CheckResult
{
Judgement = Severity.Valid,
Identifier = CheckIdentifier.Encounter,
Result = LegalityCheckResultCode.External,
Value = Identity
};
// Add results
analysis.AddLine(invalid);
analysis.AddLine(fishy);
analysis.AddLine(valid);
}
Including Numeric Arguments
public void Verify(LegalityAnalysis analysis)
{
var pk = analysis.Entity;
// Single 32-bit argument
var result1 = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = CheckIdentifier.Level,
Result = LegalityCheckResultCode.External,
Value = 50 // Max level allowed
};
// Two 16-bit arguments
var result2 = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = CheckIdentifier.CurrentMove,
Result = LegalityCheckResultCode.External,
Argument = 2, // Move slot
Argument2 = 123 // Move ID
};
analysis.AddLine(result1);
analysis.AddLine(result2);
}
Localization
Provide messages in multiple languages:using System.Collections.Generic;
public class MultilingualChecker : IExternalLegalityChecker
{
public ushort Identity => 6000;
public string Name => "Multilingual Validator";
private Dictionary<string, Dictionary<string, string>> _messages = new()
{
["en"] = new()
{
["nickname_invalid"] = "Nickname contains invalid characters.",
["trainer_suspicious"] = "Trainer name is suspicious."
},
["es"] = new()
{
["nickname_invalid"] = "El apodo contiene caracteres no válidos.",
["trainer_suspicious"] = "El nombre del entrenador es sospechoso."
},
["ja"] = new()
{
["nickname_invalid"] = "ニックネームに無効な文字が含まれています。",
["trainer_suspicious"] = "トレーナー名が疑わしいです。"
}
};
public void Verify(LegalityAnalysis analysis)
{
// Your validation logic
}
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
// Get current language from localization settings
var language = localization.Language.ToString().ToLower();
// Determine which message to return based on context
var messageKey = chk.Identifier switch
{
CheckIdentifier.Nickname => "nickname_invalid",
CheckIdentifier.Trainer => "trainer_suspicious",
_ => "unknown"
};
// Return localized message
if (_messages.TryGetValue(language, out var messages))
{
if (messages.TryGetValue(messageKey, out var message))
return message;
}
// Fallback to English
return _messages["en"][messageKey];
}
}
Unregistering Checkers
var checker = new MyCustomChecker();
// Register
ExternalLegalityCheck.RegisterChecker(checker);
// Later, if needed, unregister
ExternalLegalityCheck.UnregisterChecker(checker);
Best Practices
Use Unique Identity Numbers
Use Unique Identity Numbers
Choose identity numbers that won’t conflict with other checkers. Use a range like 1000-9999 for custom checkers.
public ushort Identity => 1234; // Pick a unique number
Don't Duplicate Standard Checks
Don't Duplicate Standard Checks
PKHeX already validates most standard legality. Focus on:
- Game-specific rules not in PKHeX
- Competitive format restrictions
- Community-specific rules
- Additional sanity checks
Use Appropriate Severity
Use Appropriate Severity
Severity.Invalid: Definitively illegalSeverity.Fishy: Suspicious but technically validSeverity.Valid: Passed your custom check (rare)
Provide Good Error Messages
Provide Good Error Messages
Your
Localize method should return clear, actionable messages.public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
return "Move is banned in VGC 2024 Series 1.";
}
Complete Example
using PKHeX.Core;
using System;
using System.Collections.Generic;
public class CustomValidator : IExternalLegalityChecker
{
public ushort Identity => 7000;
public string Name => "Custom Validator";
private List<string> _errors = new();
public void Verify(LegalityAnalysis analysis)
{
_errors.Clear();
var pk = analysis.Entity;
// Check 1: Validate nickname length
if (pk.Nickname.Length > 12)
{
_errors.Add("Nickname too long");
AddInvalid(analysis, CheckIdentifier.Nickname);
}
// Check 2: Validate level
if (pk.CurrentLevel > 100)
{
_errors.Add("Level exceeds maximum");
AddInvalid(analysis, CheckIdentifier.Level);
}
// Check 3: Validate moves
var moves = new[] { pk.Move1, pk.Move2, pk.Move3, pk.Move4 };
var duplicates = moves.Where(m => m != 0)
.GroupBy(m => m)
.Where(g => g.Count() > 1);
if (duplicates.Any())
{
_errors.Add("Duplicate moves detected");
AddInvalid(analysis, CheckIdentifier.CurrentMove);
}
}
private void AddInvalid(LegalityAnalysis analysis, CheckIdentifier identifier)
{
var result = new CheckResult
{
Judgement = Severity.Invalid,
Identifier = identifier,
Result = LegalityCheckResultCode.External,
Value = Identity
};
analysis.AddLine(result);
}
public string Localize(CheckResult chk, LegalityLocalizationSet localization, LegalityAnalysis data)
{
if (_errors.Count > 0)
return string.Join("; ", _errors);
return "Custom validation failed.";
}
}
// Usage
public class Program
{
static void Main()
{
// Register at startup
ExternalLegalityCheck.RegisterChecker(new CustomValidator());
// Now use LegalityAnalysis normally
var pk = LoadPokemon();
var analysis = new LegalityAnalysis(pk);
if (!analysis.Valid)
{
foreach (var result in analysis.Results.Where(r => !r.Valid))
{
Console.WriteLine($"{result.Identifier}: {result.Result}");
}
}
}
}
Next Steps
Overview
Return to legality system overview
Running Checks
Learn how to perform legality checks