PKHeX/PKHeX.Core/Legality/LegalityAnalysis.cs
Kurt f632aedd15
Encounter Templates: Searching and Creating (#3955)
We implement simple state machine iterators to iterate through every split type encounter array, and more finely control the path we iterate through. And, by using generics, we can have the compiler generate optimized code to avoid virtual calls.

In addition to this, we shift away from the big-5 encounter types and not inherit from an abstract class. This allows for creating a PK* of a specific type and directly writing properties (no virtual calls). Plus we can now fine-tune each encounter type to call specific code, and not have to worry about future game encounter types bothering the generation routines.
2023-08-12 16:01:16 -07:00

334 lines
10 KiB
C#

//#define SUPPRESS
using System;
using System.Collections.Generic;
using static PKHeX.Core.LegalityAnalyzers;
using static PKHeX.Core.LegalityCheckStrings;
namespace PKHeX.Core;
/// <summary>
/// Legality Check object containing the <see cref="CheckResult"/> data and overview values from the parse.
/// </summary>
public sealed class LegalityAnalysis
{
/// <summary> The entity we are checking. </summary>
internal readonly PKM Entity;
/// <summary> The entity's <see cref="IPersonalInfo"/>, which may have been sourced from the Save File it resides on. </summary>
/// <remarks>We store this rather than re-fetching, as some games that use the same <see cref="PKM"/> format have different values.</remarks>
internal readonly IPersonalInfo PersonalInfo;
private readonly List<CheckResult> Parse = new(8);
/// <summary>
/// Parse result list allowing view of the legality parse.
/// </summary>
public IReadOnlyList<CheckResult> Results => Parse;
/// <summary>
/// Matched encounter data for the <see cref="Entity"/>.
/// </summary>
public IEncounterable EncounterMatch => Info.EncounterMatch;
/// <summary>
/// Original encounter data for the <see cref="Entity"/>.
/// </summary>
/// <remarks>
/// Generation 1/2 <see cref="Entity"/> that are transferred forward to Generation 7 are restricted to new encounter details.
/// By retaining their original match, more information can be provided by the parse.
/// </remarks>
public IEncounterable EncounterOriginal => Info.EncounterOriginal;
/// <summary>
/// Indicates where the <see cref="Entity"/> originated.
/// </summary>
public readonly SlotOrigin SlotOrigin;
/// <summary>
/// Indicates if all checks ran to completion.
/// </summary>
/// <remarks>This value is false if any checks encountered an error.</remarks>
public readonly bool Parsed;
/// <summary>
/// Indicates if all checks returned a <see cref="Severity.Valid"/> result.
/// </summary>
public readonly bool Valid;
/// <summary>
/// Contains various data reused for multiple checks.
/// </summary>
public readonly LegalInfo Info;
/// <summary>
/// Checks the input <see cref="PKM"/> data for legality. This is the best method for checking with context, as some games do not have all Alternate Form data available.
/// </summary>
/// <param name="pk">Input data to check</param>
/// <param name="table"><see cref="SaveFile"/> specific personal data</param>
/// <param name="source">Details about where the <see cref="Entity"/> originated from.</param>
public LegalityAnalysis(PKM pk, IPersonalTable table, SlotOrigin source = SlotOrigin.Party) : this(pk, table.GetFormEntry(pk.Species, pk.Form), source) { }
/// <summary>
/// Checks the input <see cref="PKM"/> data for legality.
/// </summary>
/// <param name="pk">Input data to check</param>
/// <param name="source">Details about where the <see cref="Entity"/> originated from.</param>
public LegalityAnalysis(PKM pk, SlotOrigin source = SlotOrigin.Party) : this(pk, pk.PersonalInfo, source) { }
/// <summary>
/// Checks the input <see cref="PKM"/> data for legality.
/// </summary>
/// <param name="pk">Input data to check</param>
/// <param name="pi">Personal info to parse with</param>
/// <param name="source">Details about where the <see cref="Entity"/> originated from.</param>
public LegalityAnalysis(PKM pk, IPersonalInfo pi, SlotOrigin source = SlotOrigin.Party)
{
Entity = pk;
PersonalInfo = pi;
SlotOrigin = source;
Info = new LegalInfo(pk, Parse);
#if SUPPRESS
try
#endif
{
EncounterFinder.FindVerifiedEncounter(pk, Info);
if (!pk.IsOriginValid)
AddLine(Severity.Invalid, LEncConditionBadSpecies, CheckIdentifier.GameOrigin);
GetParseMethod()();
Valid = Parse.TrueForAll(chk => chk.Valid)
&& MoveResult.AllValid(Info.Moves)
&& MoveResult.AllValid(Info.Relearn);
if (!Valid && IsPotentiallyMysteryGift(Info, pk))
AddLine(Severity.Invalid, LFatefulGiftMissing, CheckIdentifier.Fateful);
Parsed = true;
}
#if SUPPRESS
// We want to swallow any error from malformed input data from the user. The Valid state is all that we really need.
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
Valid = false;
// Moves and Relearn arrays can potentially be empty on error.
var moves = Info.Moves;
for (var i = 0; i < moves.Length; i++)
{
ref var p = ref moves[i];
if (!p.IsParsed)
p = MoveResult.Unobtainable();
}
moves = Info.Relearn;
for (var i = 0; i < moves.Length; i++)
{
ref var p = ref moves[i];
if (!p.IsParsed)
p = MoveResult.Unobtainable();
}
AddLine(Severity.Invalid, L_AError, CheckIdentifier.Misc);
}
#endif
}
private static bool IsPotentiallyMysteryGift(LegalInfo info, PKM pk)
{
if (info.EncounterOriginal is not EncounterInvalid enc)
return false;
if (enc.Generation <= 3)
return true;
if (!pk.FatefulEncounter)
return false;
if (enc.Generation < 6)
return true;
if (!MoveResult.AllValid(info.Relearn))
return true;
return false;
}
private Action GetParseMethod()
{
if (Entity.Format <= 2) // prior to storing GameVersion
return ParsePK1;
var gen = GetParseFormat();
return gen switch
{
3 => ParsePK3,
4 => ParsePK4,
5 => ParsePK5,
6 => ParsePK6,
1 => ParsePK7,
2 => ParsePK7,
7 => ParsePK7,
8 => ParsePK8,
9 => ParsePK9,
_ => throw new ArgumentOutOfRangeException(nameof(gen)),
};
}
private int GetParseFormat()
{
int gen = Entity.Generation;
if (gen > 0)
return gen;
if (Entity is PK9 { IsUnhatchedEgg: true })
return 9;
return Entity.Format;
}
private void ParsePK1()
{
Nickname.Verify(this);
Level.Verify(this);
Level.VerifyG1(this);
Trainer.VerifyOTG1(this);
MiscValues.VerifyMiscG1(this);
if (Entity.Format == 2)
Item.Verify(this);
}
private void ParsePK3()
{
UpdateChecks();
if (Entity.Format > 3)
Transfer.VerifyTransferLegalityG3(this);
if (Entity.Version == (int)GameVersion.CXD)
CXD.Verify(this);
if (Info.EncounterMatch is WC3 {NotDistributed: true})
AddLine(Severity.Invalid, LEncUnreleased, CheckIdentifier.Encounter);
if (Entity.Format >= 8)
Transfer.VerifyTransferLegalityG8(this);
}
private void ParsePK4()
{
UpdateChecks();
if (Entity.Format > 4)
Transfer.VerifyTransferLegalityG4(this);
if (Entity.Format >= 8)
Transfer.VerifyTransferLegalityG8(this);
}
private void ParsePK5()
{
UpdateChecks();
if (Entity.Format >= 8)
Transfer.VerifyTransferLegalityG8(this);
}
private void ParsePK6()
{
UpdateChecks();
if (Entity.Format >= 8)
Transfer.VerifyTransferLegalityG8(this);
}
private void ParsePK7()
{
if (Entity.VC)
UpdateVCTransferInfo();
UpdateChecks();
if (Entity.Format >= 8)
Transfer.VerifyTransferLegalityG8(this);
else if (Entity is PB7)
Awakening.Verify(this);
}
private void ParsePK8()
{
UpdateChecks();
Transfer.VerifyTransferLegalityG8(this);
}
private void ParsePK9()
{
UpdateChecks();
Transfer.VerifyTransferLegalityG9(this);
}
/// <summary>
/// Adds a new Check parse value.
/// </summary>
/// <param name="s">Check severity</param>
/// <param name="c">Check comment</param>
/// <param name="i">Check type</param>
internal void AddLine(Severity s, string c, CheckIdentifier i) => AddLine(new CheckResult(s, i, c));
/// <summary>
/// Adds a new Check parse value.
/// </summary>
/// <param name="chk">Check result to add.</param>
internal void AddLine(CheckResult chk) => Parse.Add(chk);
private void UpdateVCTransferInfo()
{
var enc = (Info.EncounterOriginalGB = EncounterMatch);
if (enc is EncounterInvalid)
return;
var vc = EncounterGenerator7.GetVCStaticTransferEncounter(Entity, enc.Species, Info.EvoChainsAllGens.Gen7);
Info.EncounterMatch = vc;
Transfer.VerifyVCEncounter(Entity, enc, vc, this);
Transfer.VerifyTransferLegalityG12(this);
}
private void UpdateChecks()
{
PIDEC.Verify(this);
Nickname.Verify(this);
LanguageIndex.Verify(this);
Trainer.Verify(this);
TrainerID.Verify(this);
IVs.Verify(this);
EVs.Verify(this);
Level.Verify(this);
Ribbon.Verify(this);
AbilityValues.Verify(this);
BallIndex.Verify(this);
FormValues.Verify(this);
MiscValues.Verify(this);
GenderValues.Verify(this);
Item.Verify(this);
Contest.Verify(this);
Marking.Verify(this);
var format = Entity.Format;
if (format is 4 or 5 or 6) // Gen 6->7 transfer removes this property.
Gen4GroundTile.Verify(this);
if (format < 6)
return;
History.Verify(this);
if (format < 8) // Gen 7->8 transfer removes these properties.
ConsoleRegion.Verify(this);
if (Entity is ITrainerMemories)
Memory.Verify(this);
if (Entity is ISuperTrain)
Medal.Verify(this);
if (format < 7)
return;
HyperTraining.Verify(this);
MiscValues.VerifyVersionEvolution(this);
if (format < 8)
return;
Mark.Verify(this);
Arceus.Verify(this);
}
}