PKHeX/PKHeX.Core/Legality/Analysis.cs

447 lines
16 KiB
C#
Raw Normal View History

2017-11-09 06:59:18 +00:00
#define SUPPRESS
using System;
using System.Collections.Generic;
using System.Linq;
2017-03-24 17:59:45 +00:00
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 partial class LegalityAnalysis
{
internal readonly PKM pkm;
2018-07-21 03:22:46 +00:00
internal readonly PersonalInfo PersonalInfo;
private readonly bool Error;
private readonly List<CheckResult> Parse = new List<CheckResult>();
/// <summary>
/// Parse result list allowing view of the legality parse.
/// </summary>
public IReadOnlyList<CheckResult> Results => Parse;
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
private IEncounterable EncounterOriginalGB;
2018-07-21 03:22:46 +00:00
/// <summary>
/// Matched encounter data for the <see cref="pkm"/>.
/// </summary>
public IEncounterable EncounterMatch => Info.EncounterMatch;
2018-07-21 03:22:46 +00:00
/// <summary>
/// Original encounter data for the <see cref="pkm"/>.
/// </summary>
/// <remarks>
/// Generation 1/2 <see cref="pkm"/> 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 => EncounterOriginalGB ?? EncounterMatch;
2018-07-21 03:22:46 +00:00
/// <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;
2018-07-21 03:22:46 +00:00
/// <summary>
/// Indicates if all checks returned a <see cref="Severity.Valid"/> result.
/// </summary>
public readonly bool Valid;
2018-07-21 03:22:46 +00:00
/// <summary>
/// Contains various data reused for multiple checks.
/// </summary>
public LegalInfo Info { get; private set; }
2018-07-21 03:22:46 +00:00
/// <summary>
/// Creates a report message with optional verbosity for in-depth analysis.
/// </summary>
/// <param name="verbose">Include all details in the parse, including valid check messages.</param>
/// <returns>Single line string</returns>
public string Report(bool verbose = false) => verbose ? GetVerboseLegalityReport() : GetLegalityReport();
2018-07-21 03:22:46 +00:00
private IEnumerable<int> AllSuggestedMoves
{
get
{
2017-06-07 03:52:21 +00:00
if (_allSuggestedMoves != null)
return _allSuggestedMoves;
if (Error || Info == null)
return new int[4];
return _allSuggestedMoves = GetSuggestedMoves(true, true, true);
}
}
private IEnumerable<int> AllSuggestedRelearnMoves
{
get
{
2017-06-07 03:52:21 +00:00
if (_allSuggestedRelearnMoves != null)
return _allSuggestedRelearnMoves;
if (Error || Info == null)
return new int[4];
2017-06-07 03:52:21 +00:00
var gender = pkm.PersonalInfo.Gender;
var inheritLvlMoves = (gender > 0 && gender < 255) || Legal.MixedGenderBreeding.Contains(Info.EncounterMatch.Species);
return _allSuggestedRelearnMoves = Legal.GetValidRelearn(pkm, Info.EncounterMatch.Species, inheritLvlMoves).ToArray();
}
}
private int[] _allSuggestedMoves, _allSuggestedRelearnMoves;
public int[] AllSuggestedMovesAndRelearn => AllSuggestedMoves.Concat(AllSuggestedRelearnMoves).ToArray();
private string EncounterName
{
get
{
var enc = EncounterOriginal;
return $"{enc.GetEncounterTypeName()} ({SpeciesStrings[enc.Species]})";
}
}
private string EncounterLocation
{
get
{
var enc = EncounterOriginal as ILocation;
return enc?.GetEncounterLocation(Info.Generation, pkm.Version);
}
}
/// <summary>
/// Checks the input <see cref="PKM"/> data for legality.
/// </summary>
/// <param name="pk">Input data to check</param>
/// <param name="table"><see cref="SaveFile"/> specific personal data</param>
public LegalityAnalysis(PKM pk, PersonalTable table = null)
{
pkm = pk;
#if SUPPRESS
try
#endif
{
PersonalInfo = table?.GetFormeEntry(pkm.Species, pkm.AltForm) ?? pkm.PersonalInfo;
ParseLegality();
if (Parse.Count <= 0)
return;
2018-05-12 15:13:39 +00:00
Valid = Parse.All(chk => chk.Valid)
&& Info.Moves.All(m => m.Valid)
&& Info.Relearn.All(m => m.Valid);
if (pkm.FatefulEncounter && Info.Relearn.Any(chk => !chk.Valid) && EncounterMatch is EncounterInvalid)
AddLine(Severity.Indeterminate, LFatefulGiftMissing, CheckIdentifier.Fateful);
}
#if SUPPRESS
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
Valid = false;
AddLine(Severity.Invalid, L_AError, CheckIdentifier.Misc);
Error = true;
}
#endif
Parsed = true;
}
private void ParseLegality()
{
if (!pkm.IsOriginValid)
AddLine(Severity.Invalid, LEncConditionBadSpecies, CheckIdentifier.GameOrigin);
if (pkm.Format <= 2) // prior to storing GameVersion
{
ParsePK1();
return;
}
switch (pkm.GenNumber)
{
case 3: ParsePK3(); return;
case 4: ParsePK4(); return;
case 5: ParsePK5(); return;
case 6: ParsePK6(); return;
case 1: case 2:
case 7: ParsePK7(); return;
}
}
private void ParsePK1()
{
pkm.TradebackStatus = GBRestrictions.GetTradebackStatusInitial(pkm);
UpdateInfo();
2018-07-25 02:33:42 +00:00
if (pkm.TradebackStatus == TradebackType.Any && Info.Generation != pkm.Format)
pkm.TradebackStatus = TradebackType.WasTradeback; // Example: GSC Pokemon with only possible encounters in RBY, like the legendary birds
Nickname.Verify(this);
Level.Verify(this);
Level.VerifyG1(this);
Trainer.VerifyOTG1(this);
Misc.VerifyMiscG1(this);
if (pkm.Format == 2)
Item.Verify(this);
}
private void ParsePK3()
{
UpdateInfo();
UpdateChecks();
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
if (pkm.Format > 3)
Transfer.VerifyTransferLegalityG3(this);
if (pkm.Version == (int)GameVersion.CXD)
CXD.Verify(this);
if (Info.EncounterMatch is WC3 z && z.NotDistributed)
AddLine(Severity.Invalid, LEncUnreleased, CheckIdentifier.Encounter);
}
private void ParsePK4()
{
UpdateInfo();
UpdateChecks();
Refactor encounter matching exercise in deferred execution/state machine, only calculate possible matches until a sufficiently valid match is obtained. Previous setup would try to calculate the 'best match' and had band-aid workarounds in cases where a subsequent check may determine it to be a false match. There's still more ways to improve speed: - precalculate relationships for Encounter Slots rather than iterating over every area - yielding individual slots instead of an entire area - group non-egg wondercards by ID in a dict/hashtable for faster retrieval reworked some internals: - EncounterMatch is always an IEncounterable instead of an object, for easy pattern matching. - Splitbreed checking is done per encounter and is stored in the EncounterEgg result - Encounter validation uses Encounter/Move/RelearnMove/Evolution to whittle to the final encounter. As a part of the encounter matching, a lazy peek is used to check if an invalid encounter should be retained instead of discarded; if another encounter has not been checked, it'll stop the invalid checks and move on. If it is the last encounter, no other valid encounters exist so it will keep the parse for the invalid encounter. If no encounters are yielded, then there is no encountermatch. An EncounterInvalid is created to store basic details, and the parse is carried out. Breaks some legality checking features for flagging invalid moves in more detail, but those can be re-added in a separate check (if splitbreed & any move invalid -> check for other split moves). Should now be easier to follow the flow & maintain :smile:
2017-05-28 04:17:53 +00:00
if (pkm.Format > 4)
Transfer.VerifyTransferLegalityG4(this);
}
private void ParsePK5()
{
UpdateInfo();
UpdateChecks();
NHarmonia.Verify(this);
}
private void ParsePK6()
{
UpdateInfo();
UpdateChecks();
}
private void ParsePK7()
{
UpdateInfo();
if (pkm.VC)
UpdateVCTransferInfo();
UpdateChecks();
}
2018-07-21 03:22:46 +00:00
/// <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, c, i));
/// <summary>
/// Adds a new Check parse value.
/// </summary>
/// <param name="chk">Check result to add.</param>
2018-07-21 03:22:46 +00:00
internal void AddLine(CheckResult chk) => Parse.Add(chk);
2017-12-12 00:01:24 +00:00
private void UpdateVCTransferInfo()
{
EncounterOriginalGB = EncounterMatch;
if (EncounterOriginalGB is EncounterInvalid)
return;
Info.EncounterMatch = EncounterStaticGenerator.GetVCStaticTransferEncounter(pkm);
if (!(Info.EncounterMatch is EncounterStatic s) || !EncounterStaticGenerator.IsVCStaticTransferEncounterValid(pkm, s))
{ AddLine(Severity.Invalid, LEncInvalid, CheckIdentifier.Encounter); return; }
foreach (var z in Transfer.VerifyVCEncounter(pkm, EncounterOriginalGB, s, Info.Moves))
AddLine(z);
Transfer.VerifyTransferLegalityG12(this);
}
private void UpdateInfo()
{
Info = EncounterFinder.FindVerifiedEncounter(pkm);
Parse.AddRange(Info.Parse);
}
private void UpdateChecks()
{
PIDEC.Verify(this);
Nickname.Verify(this);
Language.Verify(this);
Trainer.Verify(this);
IndividualValues.Verify(this);
EffortValues.Verify(this);
Level.Verify(this);
Ribbon.Verify(this);
Ability.Verify(this);
Ball.Verify(this);
Form.Verify(this);
Misc.Verify(this);
Gender.Verify(this);
Item.Verify(this);
2017-12-12 00:01:24 +00:00
if (pkm.Format <= 6 && pkm.Format >= 4)
EncounterType.Verify(this); // Gen 6->7 transfer deletes encounter type data
if (pkm.Format < 6)
return;
2018-11-14 03:10:31 +00:00
if (pkm.Format < 8 && !(pkm is PB7))
{
Memory.Verify(this);
Medal.Verify(this);
ConsoleRegion.Verify(this);
}
if (pkm.Format >= 7)
{
HyperTraining.Verify(this);
Misc.VerifyVersionEvolution(this);
}
}
private string GetLegalityReport()
{
if (!Parsed || Info == null)
return L_AnalysisUnavailable;
2018-05-12 15:13:39 +00:00
var lines = new List<string>();
var vMoves = Info.Moves;
var vRelearn = Info.Relearn;
for (int i = 0; i < 4; i++)
{
if (!vMoves[i].Valid)
lines.Add(string.Format(L_F0_M_1_2, vMoves[i].Rating, i + 1, vMoves[i].Comment));
}
if (pkm.Format >= 6)
{
for (int i = 0; i < 4; i++)
{
if (!vRelearn[i].Valid)
lines.Add(string.Format(L_F0_RM_1_2, vRelearn[i].Rating, i + 1, vRelearn[i].Comment));
}
}
if (lines.Count == 0 && Parse.All(chk => chk.Valid) && Valid)
return L_ALegal;
2018-05-12 15:13:39 +00:00
// Build result string...
var outputLines = Parse.Where(chk => !chk.Valid); // Only invalid
lines.AddRange(outputLines.Select(chk => string.Format(L_F0_1, chk.Rating, chk.Comment)));
if (lines.Count == 0)
return L_AError;
return string.Join(Environment.NewLine, lines);
}
private string GetVerboseLegalityReport()
{
if (!Parsed || Info == null)
return L_AnalysisUnavailable;
const string separator = "===";
string[] br = {separator, string.Empty};
var lines = new List<string> {br[1]};
lines.AddRange(br);
int rl = lines.Count;
var vMoves = Info.Moves;
var vRelearn = Info.Relearn;
for (int i = 0; i < 4; i++)
{
var move = vMoves[i];
if (!move.Valid)
continue;
var msg = string.Format(L_F0_M_1_2, move.Rating, i + 1, move.Comment);
if (pkm.Format != move.Generation)
msg += $" [Gen{move.Generation}]";
lines.Add(msg);
}
if (pkm.Format >= 6)
{
for (int i = 0; i < 4; i++)
{
if (vRelearn[i].Valid)
lines.Add(string.Format(L_F0_RM_1_2, vRelearn[i].Rating, i + 1, vRelearn[i].Comment));
}
}
if (rl != lines.Count) // move info added, break for next section
lines.Add(br[1]);
2018-05-12 15:13:39 +00:00
var outputLines = Parse.Where(chk => chk?.Valid == true && chk.Comment != L_AValid).OrderBy(chk => chk.Judgement); // Fishy sorted to top
lines.AddRange(outputLines.Select(chk => string.Format(L_F0_1, chk.Rating, chk.Comment)));
lines.AddRange(br);
lines.Add(string.Format(L_FEncounterType_0, EncounterName));
var loc = EncounterLocation;
if (!string.IsNullOrEmpty(loc))
lines.Add(string.Format(L_F0_1, "Location", loc));
if (pkm.VC)
lines.Add(string.Format(L_F0_1, nameof(GameVersion), Info.Game));
var pidiv = Info.PIDIV ?? MethodFinder.Analyze(pkm);
if (pidiv != null)
{
if (!pidiv.NoSeed)
lines.Add(string.Format(L_FOriginSeed_0, pidiv.OriginSeed.ToString("X8")));
lines.Add(string.Format(L_FPIDType_0, pidiv.Type));
}
if (!Valid && Info.InvalidMatches != null)
{
lines.Add("Other match(es):");
lines.AddRange(Info.InvalidMatches.Select(z => $"{z.LongName}: {z.Reason}"));
}
2018-05-12 15:13:39 +00:00
return GetLegalityReport() + string.Join(Environment.NewLine, lines);
}
2018-07-21 03:22:46 +00:00
/// <summary>
/// Gets the current <see cref="PKM.RelearnMoves"/> array of four moves that might be legal.
/// </summary>
public int[] GetSuggestedRelearn()
{
if (Info?.RelearnBase == null || Info.Generation < 6)
return new int[4];
if (!EncounterMatch.EggEncounter)
return Info.RelearnBase;
List<int> window = new List<int>(Info.RelearnBase.Where(z => z != 0));
window.AddRange(pkm.Moves.Where((_, i) => !Info.Moves[i].Valid || Info.Moves[i].Flag));
window = window.Distinct().ToList();
int[] moves = new int[4];
int start = Math.Max(0, window.Count - 4);
int count = Math.Min(4, window.Count);
window.CopyTo(start, moves, 0, count);
return moves;
}
2018-07-21 03:22:46 +00:00
/// <summary>
/// Gets four moves which can be learned depending on the input arguments.
/// </summary>
/// <param name="tm">Allow TM moves</param>
/// <param name="tutor">Allow Tutor moves</param>
/// <param name="reminder">Allow Move Reminder</param>
public int[] GetSuggestedMoves(bool tm, bool tutor, bool reminder)
{
if (!Parsed)
return new int[4];
if (pkm.IsEgg && pkm.Format <= 5) // pre relearn
return Legal.GetBaseEggMoves(pkm, pkm.Species, (GameVersion)pkm.Version, pkm.CurrentLevel);
if (!(tm || tutor || reminder) && (Info.Generation <= 2 || pkm.Species == EncounterOriginal.Species))
{
var lvl = Info.Generation <= 2 && pkm.Format >= 7 ? pkm.Met_Level : pkm.CurrentLevel;
var ver = Info.Generation <= 2 && EncounterOriginal is IVersion v ? v.Version : (GameVersion)pkm.Version;
return MoveLevelUp.GetEncounterMoves(pkm, lvl, ver);
}
var evos = Info.EvoChainsAllGens;
return Legal.GetValidMoves(pkm, evos, Tutor: tutor, Machine: tm, MoveReminder: reminder).Skip(1).ToArray(); // skip move 0
}
2018-07-21 03:22:46 +00:00
/// <summary>
/// Gets an object containing met data properties that might be legal.
/// </summary>
public EncounterStatic GetSuggestedMetInfo() => EncounterSuggestion.GetSuggestedMetInfo(pkm);
}
}