From dcc0e794358c2e7d71fe0871cbc558609c98457e Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 5 Jul 2023 21:14:09 -0700 Subject: [PATCH] Evotree: Evolution Traversal Enhancements (#3936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Like move validation, evolutions are the earliest thing we wish to traverse when determining what encounters may have originated the current Pokémon. To determine the permitted species-form-levels a Pokémon could originate with, we must devolve a Pokémon by traveling down-generation to origin. Once we have an encounter, we can then evolve it to the current species, traversing upwards from origin to the current format. --- .../Editing/Applicators/BallApplicator.cs | 2 +- PKHeX.Core/Editing/PKM/QR/QRMessageUtil.cs | 2 +- PKHeX.Core/Game/Locations/LocationsHOME.cs | 45 +++ PKHeX.Core/Items/ItemStorage9SV.cs | 5 + PKHeX.Core/Legality/Core.cs | 21 +- .../Legality/Encounters/Data/EncounterUtil.cs | 2 +- .../EncounterMisc/EncounterInvalid.cs | 2 +- .../EncounterStatic/EncounterFixed9.cs | 21 +- .../EncounterStatic/EncounterMight9.cs | 20 +- .../EncounterStatic/EncounterStatic8a.cs | 50 ++- .../EncounterStatic/EncounterStatic8b.cs | 47 ++- .../EncounterStatic/EncounterStatic9.cs | 20 +- .../EncounterStatic/EncounterTera9.cs | 20 +- .../EncounterTrade/EncounterTrade8b.cs | 37 +- .../EncounterTrade/EncounterTrade9.cs | 28 +- .../ByGeneration/EncounterGenerator1.cs | 5 + .../ByGeneration/EncounterGenerator2.cs | 5 + .../ByGeneration/EncounterGenerator3.cs | 8 +- .../ByGeneration/EncounterGenerator3GC.cs | 7 +- .../ByGeneration/EncounterGenerator4.cs | 9 +- .../ByGeneration/EncounterGenerator5.cs | 7 +- .../ByGeneration/EncounterGenerator6.cs | 7 +- .../ByGeneration/EncounterGenerator7.cs | 5 + .../ByGeneration/EncounterGenerator7GG.cs | 5 + .../ByGeneration/EncounterGenerator7GO.cs | 5 + .../ByGeneration/EncounterGenerator8.cs | 5 + .../ByGeneration/EncounterGenerator8GO.cs | 5 + .../ByGeneration/EncounterGenerator8a.cs | 10 +- .../ByGeneration/EncounterGenerator8b.cs | 10 +- .../ByGeneration/EncounterGenerator9.cs | 8 +- .../ByGeneration/Lump/EncounterGenerator7X.cs | 2 +- .../ByGeneration/Lump/EncounterGenerator8X.cs | 2 +- .../Moveset/EncounterMovesetGenerator.cs | 86 ++--- .../Information/EncounterSuggestion.cs | 30 +- .../Encounters/Verifiers/EvolutionVerifier.cs | 44 +-- .../Legality/Evolutions/EncounterOrigin.cs | 187 +++------- .../Legality/Evolutions/EvolutionChain.cs | 121 +++++-- .../EvolutionGroup/EvolutionGroup1.cs | 94 +++-- .../EvolutionGroup/EvolutionGroup2.cs | 103 +++--- .../EvolutionGroup/EvolutionGroup3.cs | 97 ++--- .../EvolutionGroup/EvolutionGroup4.cs | 99 ++--- .../EvolutionGroup/EvolutionGroup5.cs | 85 ++--- .../EvolutionGroup/EvolutionGroup6.cs | 82 ++--- .../EvolutionGroup/EvolutionGroup7.cs | 112 +++--- .../EvolutionGroup/EvolutionGroup7b.cs | 84 ++--- .../EvolutionGroup/EvolutionGroup8.cs | 232 ------------ .../EvolutionGroup/EvolutionGroup9.cs | 98 ----- .../EvolutionGroup/EvolutionGroupHOME.cs | 258 +++++++++++++ .../EvolutionGroup/EvolutionGroupUtil.cs | 15 +- .../EvolutionGroup/EvolutionOrigin.cs | 12 + .../EvolutionGroup/EvolutionUtil.cs | 172 +++++++++ .../EvolutionGroup/IEvolutionGroup.cs | 47 ++- .../Legality/Evolutions/EvolutionHistory.cs | 40 +-- .../Legality/Evolutions/EvolutionLegality.cs | 35 -- .../Legality/Evolutions/EvolutionReversal.cs | 160 --------- .../Evolutions/EvolutionSets/EvolutionSet1.cs | 39 +- .../Evolutions/EvolutionSets/EvolutionSet3.cs | 61 ++-- .../Evolutions/EvolutionSets/EvolutionSet4.cs | 57 +-- .../Evolutions/EvolutionSets/EvolutionSet5.cs | 35 +- .../Evolutions/EvolutionSets/EvolutionSet6.cs | 30 +- .../Evolutions/EvolutionSets/EvolutionSet7.cs | 36 +- .../Legality/Evolutions/EvolutionTree.cs | 340 ++---------------- .../Forward/EvolutionForwardPersonal.cs | 79 ++++ .../Forward/EvolutionForwardSpecies.cs | 76 ++++ .../Evolutions/Forward/IEvolutionForward.cs | 19 + .../Legality/Evolutions/IEvolutionNetwork.cs | 68 ++++ .../Evolutions/{ => Methods}/EvoCriteria.cs | 21 ++ .../Methods/EvolutionCheckResult.cs | 13 + .../{ => Methods}/EvolutionMethod.cs | 92 ++--- .../Evolutions/{ => Methods}/EvolutionType.cs | 5 + .../Evolutions/Reversal/EvolutionLink.cs | 27 +- .../Evolutions/Reversal/EvolutionNode.cs | 16 - .../Evolutions/Reversal/EvolutionReversal.cs | 84 +++++ .../Reversal/EvolutionReverseLookup.cs | 57 ++- .../Reversal/EvolutionReversePersonal.cs | 68 ++++ .../Reversal/EvolutionReverseSpecies.cs | 70 ++++ .../Evolutions/Reversal/IEvolutionReverse.cs | 21 ++ .../Legality/LearnSource/Group/ILearnGroup.cs | 5 + .../Legality/LearnSource/Group/LearnGroup1.cs | 1 + .../Legality/LearnSource/Group/LearnGroup2.cs | 1 + .../Legality/LearnSource/Group/LearnGroup3.cs | 1 + .../Legality/LearnSource/Group/LearnGroup4.cs | 1 + .../Legality/LearnSource/Group/LearnGroup5.cs | 1 + .../Legality/LearnSource/Group/LearnGroup6.cs | 1 + .../Legality/LearnSource/Group/LearnGroup7.cs | 3 +- .../LearnSource/Group/LearnGroup7b.cs | 1 + .../Legality/LearnSource/Group/LearnGroup8.cs | 9 +- .../LearnSource/Group/LearnGroup8a.cs | 9 +- .../LearnSource/Group/LearnGroup8b.cs | 9 +- .../Legality/LearnSource/Group/LearnGroup9.cs | 17 +- .../LearnSource/Group/LearnGroupHOME.cs | 239 ++++++++++++ .../Legality/LearnSource/LearnEnvironment.cs | 1 + .../LearnSource/Sources/IHomeSource.cs | 7 + .../LearnSource/Sources/LearnSource4HGSS.cs | 10 +- .../LearnSource/Sources/LearnSource4Pt.cs | 10 +- .../LearnSource/Sources/LearnSource5B2W2.cs | 10 +- .../LearnSource/Sources/LearnSource5BW.cs | 10 +- .../LearnSource/Sources/LearnSource6AO.cs | 10 +- .../LearnSource/Sources/LearnSource6XY.cs | 10 +- .../LearnSource/Sources/LearnSource7SM.cs | 14 +- .../LearnSource/Sources/LearnSource7USUM.cs | 14 +- .../LearnSource/Sources/LearnSource8BDSP.cs | 35 +- .../LearnSource/Sources/LearnSource8PLA.cs | 35 +- .../LearnSource/Sources/LearnSource8SWSH.cs | 61 +++- .../LearnSource/Sources/LearnSource9SV.cs | 47 ++- .../LearnSource/Sources/Shared/LearnOption.cs | 16 + PKHeX.Core/Legality/Learnset/Learnset.cs | 2 +- PKHeX.Core/Legality/MoveListSuggest.cs | 22 +- .../Legality/RNG/Algorithms/XorShift128.cs | 6 +- .../RNG/Algorithms/Xoroshiro128Plus.cs | 4 +- .../RNG/Algorithms/Xoroshiro128Plus8b.cs | 4 +- .../Legality/Restrictions/GBRestrictions.cs | 127 ++++--- .../Restrictions/Memories/MemoryContext.cs | 1 + .../Restrictions/Memories/MemoryContext6.cs | 2 + .../Restrictions/Memories/MemoryContext8.cs | 2 + .../Restrictions/Memories/MemoryRules.cs | 70 ++++ .../Verifiers/AwakenedValueVerifier.cs | 17 +- .../Verifiers/FormArgumentVerifier.cs | 186 ++++++++++ PKHeX.Core/Legality/Verifiers/FormVerifier.cs | 189 +--------- .../Legality/Verifiers/HistoryVerifier.cs | 30 +- .../Verifiers/HyperTrainingVerifier.cs | 4 +- PKHeX.Core/Legality/Verifiers/MarkVerifier.cs | 2 +- .../Legality/Verifiers/MemoryVerifier.cs | 179 ++++++--- PKHeX.Core/Legality/Verifiers/MiscVerifier.cs | 87 ++--- .../Legality/Verifiers/Ribbons/MarkRules.cs | 11 +- .../Legality/Verifiers/Ribbons/RibbonRules.cs | 37 +- .../Ribbons/RibbonVerifierCommon3.cs | 2 +- .../Ribbons/RibbonVerifierCommon6.cs | 2 +- .../Ribbons/RibbonVerifierCommon8.cs | 2 +- PKHeX.Core/MysteryGifts/WA8.cs | 19 +- PKHeX.Core/MysteryGifts/WB8.cs | 32 +- PKHeX.Core/MysteryGifts/WC3.cs | 4 +- PKHeX.Core/MysteryGifts/WC9.cs | 41 ++- PKHeX.Core/PKM/HOME/GameDataPA8.cs | 4 +- PKHeX.Core/PKM/HOME/GameDataPB8.cs | 4 +- PKHeX.Core/PKM/HOME/GameDataPK9.cs | 4 +- PKHeX.Core/PKM/HOME/PKH.cs | 17 +- PKHeX.Core/PKM/Interfaces/IBattleVersion.cs | 2 +- PKHeX.Core/PKM/PK1.cs | 13 +- PKHeX.Core/PKM/PK6.cs | 2 +- PKHeX.Core/PKM/PK9.cs | 2 + .../PKM/Util/Conversion/FormConverter.cs | 41 +-- .../PKM/Util/Conversion/IEntityRejuvenator.cs | 46 ++- PKHeX.Core/PersonalInfo/Info/PersonalInfo1.cs | 37 +- .../PersonalInfo/Interfaces/IPersonalTable.cs | 2 +- .../PersonalInfo/Table/PersonalTable1.cs | 22 +- .../PersonalInfo/Table/PersonalTable2.cs | 4 +- .../PersonalInfo/Table/PersonalTable3.cs | 4 +- .../PersonalInfo/Table/PersonalTable4.cs | 4 +- .../PersonalInfo/Table/PersonalTable5B2W2.cs | 4 +- .../PersonalInfo/Table/PersonalTable5BW.cs | 4 +- .../PersonalInfo/Table/PersonalTable6AO.cs | 2 +- .../PersonalInfo/Table/PersonalTable6XY.cs | 4 +- .../PersonalInfo/Table/PersonalTable7.cs | 4 +- .../PersonalInfo/Table/PersonalTable7GG.cs | 4 +- .../PersonalInfo/Table/PersonalTable8BDSP.cs | 4 +- .../PersonalInfo/Table/PersonalTable8LA.cs | 4 +- .../PersonalInfo/Table/PersonalTable8SWSH.cs | 4 +- .../PersonalInfo/Table/PersonalTable9SV.cs | 4 +- PKHeX.Core/Resources/byte/evolve/evos_ao.pkl | Bin 42960 -> 6160 bytes PKHeX.Core/Resources/byte/evolve/evos_bs.pkl | Bin 4280 -> 4272 bytes PKHeX.Core/Resources/byte/evolve/evos_gg.pkl | Bin 66648 -> 7016 bytes PKHeX.Core/Resources/byte/evolve/evos_ss.pkl | Bin 90600 -> 7504 bytes PKHeX.Core/Resources/byte/evolve/evos_uu.pkl | Bin 66376 -> 7192 bytes .../Saves/Encryption/SwishCrypto/SCBlock.cs | 18 +- .../Encryption/SwishCrypto/SCXorShift32.cs | 35 +- .../Saves/Substructures/Gen5/FestaBlock5.cs | 8 +- .../Controls/PKM Editor/PKMEditor.cs | 5 + .../Controls/PKM Editor/StatEditor.cs | 8 +- .../Subforms/PKM Editors/MoveShopEditor.cs | 78 +++- .../PKHeX.Core.Tests/General/MarshalTests.cs | 1 + ...6F123B (has 7 out of 8 Battle Memory).pk7} | Bin 260 -> 260 bytes ...CD3317179 legal traded HT memory no HT.pk8 | Bin 0 -> 344 bytes .../PKHeX.Core.Tests/Legality/LegalityData.cs | 33 +- Tests/PKHeX.Core.Tests/Legality/TempTests.cs | 28 -- 175 files changed, 3663 insertions(+), 2503 deletions(-) delete mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup8.cs delete mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup9.cs create mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupHOME.cs create mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs create mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionUtil.cs delete mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionLegality.cs delete mode 100644 PKHeX.Core/Legality/Evolutions/EvolutionReversal.cs create mode 100644 PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardPersonal.cs create mode 100644 PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardSpecies.cs create mode 100644 PKHeX.Core/Legality/Evolutions/Forward/IEvolutionForward.cs create mode 100644 PKHeX.Core/Legality/Evolutions/IEvolutionNetwork.cs rename PKHeX.Core/Legality/Evolutions/{ => Methods}/EvoCriteria.cs (55%) create mode 100644 PKHeX.Core/Legality/Evolutions/Methods/EvolutionCheckResult.cs rename PKHeX.Core/Legality/Evolutions/{ => Methods}/EvolutionMethod.cs (67%) rename PKHeX.Core/Legality/Evolutions/{ => Methods}/EvolutionType.cs (97%) create mode 100644 PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversal.cs create mode 100644 PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversePersonal.cs create mode 100644 PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseSpecies.cs create mode 100644 PKHeX.Core/Legality/Evolutions/Reversal/IEvolutionReverse.cs create mode 100644 PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs create mode 100644 PKHeX.Core/Legality/LearnSource/Sources/IHomeSource.cs create mode 100644 PKHeX.Core/Legality/Restrictions/Memories/MemoryRules.cs create mode 100644 PKHeX.Core/Legality/Verifiers/FormArgumentVerifier.cs rename Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/{Legal (has 7 out of 8 Battle Memory).pk7 => 0248 - Tyranitar - DC689F6F123B (has 7 out of 8 Battle Memory).pk7} (63%) create mode 100644 Tests/PKHeX.Core.Tests/Legality/Legal/Generation 8/0083-01 - Egg - 219CD3317179 legal traded HT memory no HT.pk8 delete mode 100644 Tests/PKHeX.Core.Tests/Legality/TempTests.cs diff --git a/PKHeX.Core/Editing/Applicators/BallApplicator.cs b/PKHeX.Core/Editing/Applicators/BallApplicator.cs index 235541866..b6451cf45 100644 --- a/PKHeX.Core/Editing/Applicators/BallApplicator.cs +++ b/PKHeX.Core/Editing/Applicators/BallApplicator.cs @@ -73,7 +73,7 @@ public static int ApplyBallNext(PKM pk) return pk.Ball = (int)next; } - private static int ApplyFirstLegalBall(PKM pk, Span balls) + private static int ApplyFirstLegalBall(PKM pk, ReadOnlySpan balls) { foreach (var b in balls) { diff --git a/PKHeX.Core/Editing/PKM/QR/QRMessageUtil.cs b/PKHeX.Core/Editing/PKM/QR/QRMessageUtil.cs index e19fd47d7..b19a37d26 100644 --- a/PKHeX.Core/Editing/PKM/QR/QRMessageUtil.cs +++ b/PKHeX.Core/Editing/PKM/QR/QRMessageUtil.cs @@ -112,7 +112,7 @@ public static string GetMessageBase64(ReadOnlySpan data, string server) // Decode unicode string -- size might be pretty big (user input), so just rent instead of stackalloc var tmp = ArrayPool.Shared.Rent(url.Length); var result = Convert.TryFromBase64Chars(url, tmp, out int bytesWritten) ? tmp[..bytesWritten] : null; - ArrayPool.Shared.Return(tmp); + ArrayPool.Shared.Return(tmp, true); return result; } diff --git a/PKHeX.Core/Game/Locations/LocationsHOME.cs b/PKHeX.Core/Game/Locations/LocationsHOME.cs index c6436b7c9..d995a4919 100644 --- a/PKHeX.Core/Game/Locations/LocationsHOME.cs +++ b/PKHeX.Core/Game/Locations/LocationsHOME.cs @@ -1,3 +1,5 @@ +using System; + namespace PKHeX.Core; /// @@ -106,4 +108,47 @@ public static ushort GetLocationSWSHEgg(int ver, ushort egg) SWSL when ver == (int)GameVersion.SW => true, _ => false, }; + + /// + /// Checks if the location is (potentially) remapped based on visitation options. + /// + /// Relevant when a side data yields SW/SH side data with a higher priority than the original (by version) side data. + /// Original context + /// Current context + public static LocationRemapState GetRemapState(EntityContext original, EntityContext current) + { + if (current == original) + return LocationRemapState.Original; + if (current == EntityContext.Gen8) + return LocationRemapState.Remapped; + return original.Generation() switch + { + < 8 => LocationRemapState.Original, + 8 => LocationRemapState.Either, + _ => current is (EntityContext.Gen8a or EntityContext.Gen8b) // down + ? LocationRemapState.Either + : LocationRemapState.Original, + }; + } + + public static bool IsMatchLocation(EntityContext original, EntityContext current, int met, int expect, int version) + { + var state = GetRemapState(original, current); + return state switch + { + LocationRemapState.Original => met == expect, + LocationRemapState.Remapped => met == GetMetSWSH((ushort)expect, version), + LocationRemapState.Either => met == expect || met == GetMetSWSH((ushort)expect, version), + _ => false, + }; + } +} + +[Flags] +public enum LocationRemapState +{ + None, + Original = 1 << 0, + Remapped = 1 << 1, + Either = Original | Remapped, } diff --git a/PKHeX.Core/Items/ItemStorage9SV.cs b/PKHeX.Core/Items/ItemStorage9SV.cs index 3089a1fcb..0e84b39ba 100644 --- a/PKHeX.Core/Items/ItemStorage9SV.cs +++ b/PKHeX.Core/Items/ItemStorage9SV.cs @@ -82,6 +82,11 @@ public sealed class ItemStorage9SV : IItemStorage 2210, 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2218, 2219, 2220, 2221, 2222, 2223, 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, + + // DLC additions + // 2232, 2233, 2234, 2235, 2236, 2237, 2238, 2239, 2240, 2241, + // 2242, 2243, 2244, 2245, 2246, 2247, 2248, 2249, 2250, 2251, + // 2252, 2253, 2254, 2255, 2256, 2257, 2258, 2259, 2260, 2261, }; private static ReadOnlySpan Pouch_Treasure_SV => new ushort[] diff --git a/PKHeX.Core/Legality/Core.cs b/PKHeX.Core/Legality/Core.cs index 083af0e90..3c25993ca 100644 --- a/PKHeX.Core/Legality/Core.cs +++ b/PKHeX.Core/Legality/Core.cs @@ -139,20 +139,6 @@ public static class Legal internal static readonly ushort[] HeldItems_LA = Array.Empty(); internal static readonly ushort[] HeldItems_SV = ItemStorage9SV.GetAllHeld(); - internal static int GetMaxSpeciesOrigin(int generation) => generation switch - { - 1 => MaxSpeciesID_1, - 2 => MaxSpeciesID_2, - 3 => MaxSpeciesID_3, - 4 => MaxSpeciesID_4, - 5 => MaxSpeciesID_5, - 6 => MaxSpeciesID_6, - 7 => MaxSpeciesID_7b, - 8 => MaxSpeciesID_8a, - 9 => MaxSpeciesID_9, - _ => -1, - }; - internal static int GetMaxLanguageID(int generation) => generation switch { 1 => (int) LanguageID.Spanish, // 1-7 except 6 @@ -231,11 +217,12 @@ public static bool IsPPUpAvailable(PKM pk) /// public static bool GetIsFixedIVSequenceValidSkipRand(ReadOnlySpan IVs, PKM pk, uint max = 31) { - for (int i = 0; i < 6; i++) + for (int i = 5; i >= 0; i--) { - if ((uint) IVs[i] > max) // random + var iv = IVs[i]; + if ((uint)iv > max) // random continue; - if (IVs[i] != pk.GetIV(i)) + if (iv != pk.GetIV(i)) return false; } return true; diff --git a/PKHeX.Core/Legality/Encounters/Data/EncounterUtil.cs b/PKHeX.Core/Legality/Encounters/Data/EncounterUtil.cs index 4f12f76cf..8c7a6b997 100644 --- a/PKHeX.Core/Legality/Encounters/Data/EncounterUtil.cs +++ b/PKHeX.Core/Legality/Encounters/Data/EncounterUtil.cs @@ -48,7 +48,7 @@ internal static class EncounterUtil return temp; } - internal static T? GetMinByLevel(EvoCriteria[] chain, IEnumerable possible) where T : class, IEncounterTemplate + internal static T? GetMinByLevel(ReadOnlySpan chain, IEnumerable possible) where T : class, IEncounterTemplate { // MinBy grading: prefer species-form match, select lowest min level encounter. // Minimum allocation :) diff --git a/PKHeX.Core/Legality/Encounters/EncounterMisc/EncounterInvalid.cs b/PKHeX.Core/Legality/Encounters/EncounterMisc/EncounterInvalid.cs index e2f1ffa1e..59e1032a9 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterMisc/EncounterInvalid.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterMisc/EncounterInvalid.cs @@ -14,7 +14,7 @@ public sealed record EncounterInvalid : IEncounterable public byte LevelMin { get; } public byte LevelMax { get; } public bool EggEncounter { get; } - public int Generation { get; init; } + public int Generation { get; } public EntityContext Context { get; } public GameVersion Version { get; } public bool IsShiny => false; diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs index 9cd96b83f..38af13106 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs @@ -53,8 +53,25 @@ private static EncounterFixed9 ReadEncounter(ReadOnlySpan data) => new() protected override bool IsMatchLocation(PKM pk) { - if (pk is PK8) - return LocationsHOME.IsValidMetSV((ushort)pk.Met_Location, pk.Version); + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetSV(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; + } + + private bool IsMatchLocationExact(PKM pk) + { var loc = pk.Met_Location; return loc == Location0 || loc == Location1 || loc == Location2 || loc == Location3; } diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterMight9.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterMight9.cs index f0aabff80..de3538e9f 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterMight9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterMight9.cs @@ -213,9 +213,23 @@ private static EncounterMight9 ReadEncounter(ReadOnlySpan data) => new() protected override bool IsMatchLocation(PKM pk) { - if (pk is PK8) - return LocationsHOME.IsValidMetSV((ushort)pk.Met_Location, pk.Version); - return base.IsMatchLocation(pk); + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetSV(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; } protected override EncounterMatchRating IsMatchDeferred(PKM pk) diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8a.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8a.cs index e04d473c5..42497236e 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8a.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8a.cs @@ -72,10 +72,38 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) if (pk is IScaledSize s) { - if (HasFixedHeight && s.HeightScalar != HeightScalar) - return false; - if (HasFixedWeight && s.WeightScalar != WeightScalar) - return false; + // 3 of the Alpha statics were mistakenly set as 127 scale. If they enter HOME on 3.0.1, they'll get bumped to 255. + if (IsAlpha && this is { HeightScalar: 127, WeightScalar: 127 }) // Average Size Alphas + { + // HOME >=3.0.1 ensures 255 scales for the 127's + // PLA and S/V could have safe-harbored them via <=3.0.0 + if (pk.Context is EntityContext.Gen8a or EntityContext.Gen9) + { + if (s is not { HeightScalar: 127, WeightScalar: 127 }) // Original? + { + // Must match the HOME updated values AND must have the Alpha ribbon (visited HOME). + if (s is not { HeightScalar: 255, WeightScalar: 255 }) + return false; + if (pk is IRibbonSetMark9 { RibbonMarkAlpha: false }) + return false; + if (pk.IsUntraded) + return false; + } + } + else + { + // Must match the HOME updated values + if (s is not { HeightScalar: 255, WeightScalar: 255 }) + return false; + } + } + else + { + if (HasFixedHeight && s.HeightScalar != HeightScalar) + return false; + if (HasFixedWeight && s.WeightScalar != WeightScalar) + return false; + } } if (pk is IAlpha a && a.IsAlpha != IsAlpha) @@ -86,14 +114,16 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) protected override bool IsMatchLocation(PKM pk) { - if (pk is PK8) - return pk.Met_Location == LocationsHOME.SWLA; - if (pk is PB8 { Version: (int)GameVersion.PLA, Met_Location: LocationsHOME.SWLA }) - return true; - - return base.IsMatchLocation(pk); + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return base.IsMatchLocation(pk); + if (metState == LocationRemapState.Remapped) + return IsMetRemappedSWSH(pk); + return base.IsMatchLocation(pk) || IsMetRemappedSWSH(pk); } + private static bool IsMetRemappedSWSH(PKM pk) => pk.Met_Location == LocationsHOME.SWLA; + public override EncounterMatchRating GetMatchRating(PKM pk) { var result = GetMatchRatingInternal(pk); diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs index e5398fa74..3f15989a5 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs @@ -19,8 +19,25 @@ public sealed record EncounterStatic8b : EncounterStatic, IStaticCorrelation8b protected override bool IsMatchLocation(PKM pk) { - if (pk is PK8) - return LocationsHOME.IsValidMetBDSP((ushort)pk.Met_Location, pk.Version); + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetBDSP(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; + } + + private bool IsMatchLocationExact(PKM pk) + { if (!Roaming) return base.IsMatchLocation(pk); return IsRoamingLocation(pk); @@ -48,18 +65,24 @@ public bool IsStaticCorrelationCorrect(PKM pk) protected override bool IsMatchEggLocation(PKM pk) { - if (pk is not PB8) - { - if (!EggEncounter) - return pk.Egg_Location == 0; + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchEggLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchEggLocationRemapped(pk); + // Either + return IsMatchEggLocationExact(pk) || IsMatchEggLocationRemapped(pk); + } - if (pk is PK8) - return LocationsHOME.IsLocationSWSHEgg(pk.Version, pk.Met_Location, pk.Egg_Location, (ushort)EggLocation); - - // Hatched - return pk.Egg_Location == EggLocation || pk.Egg_Location == Locations.LinkTrade6NPC; - } + private bool IsMatchEggLocationRemapped(PKM pk) + { + if (!EggEncounter) + return pk.Egg_Location == 0; + return LocationsHOME.IsLocationSWSHEgg(pk.Version, pk.Met_Location, pk.Egg_Location, (ushort)EggLocation); + } + private bool IsMatchEggLocationExact(PKM pk) + { var eggloc = pk.Egg_Location; if (!EggEncounter) return eggloc == EggLocation; diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic9.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic9.cs index 6c4d31707..6dd028a41 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic9.cs @@ -21,9 +21,23 @@ public sealed record EncounterStatic9(GameVersion Version) : EncounterStatic(Ver protected override bool IsMatchLocation(PKM pk) { - if (pk is PK8) - return LocationsHOME.IsValidMetSV((ushort)pk.Met_Location, pk.Version); - return base.IsMatchLocation(pk); + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetSV(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; } protected override bool IsMatchPartial(PKM pk) diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterTera9.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterTera9.cs index 1e7389087..0ab5ff25c 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterTera9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterTera9.cs @@ -97,9 +97,23 @@ private static EncounterTera9 ReadEncounter(ReadOnlySpan data) => new() protected override bool IsMatchLocation(PKM pk) { - if (pk is PK8) - return LocationsHOME.IsValidMetSV((ushort)pk.Met_Location, pk.Version); - return base.IsMatchLocation(pk); + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetSV(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; } protected override EncounterMatchRating IsMatchDeferred(PKM pk) diff --git a/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade8b.cs b/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade8b.cs index 993ec08b9..2a351ee95 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade8b.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade8b.cs @@ -36,17 +36,46 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) return false; if (pk is IScaledSize w && w.WeightScalar != WeightScalar) return false; + if (!IsMatchLocation(pk)) + return false; return base.IsMatchExact(pk, evo); } + private bool IsMatchLocation(PKM pk) + { + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetBDSP(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; + } + protected override bool IsMatchEggLocation(PKM pk) { - var expect = EggLocation; - if (pk is not PB8 && expect == Locations.Default8bNone) - expect = 0; - return pk.Egg_Location == expect; + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchEggLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchEggLocationRemapped(pk); + // Either + return IsMatchEggLocationExact(pk) || IsMatchEggLocationRemapped(pk); } + private static bool IsMatchEggLocationRemapped(PKM pk) => pk.Egg_Location == 0; + private bool IsMatchEggLocationExact(PKM pk) => pk.Egg_Location == EggLocation; + protected override void ApplyDetails(ITrainerInfo sav, EncounterCriteria criteria, PKM pk) { base.ApplyDetails(sav, criteria, pk); diff --git a/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade9.cs b/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade9.cs index 588d14c4f..fe428bc14 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterTrade/EncounterTrade9.cs @@ -41,6 +41,32 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) { if (TeraType != GemType.Random && pk is ITeraType t && !Tera9RNG.IsMatchTeraType(TeraType, Species, Form, (byte)t.TeraTypeOriginal)) return false; - return base.IsMatchExact(pk, evo); + if (!base.IsMatchExact(pk, evo)) + return false; + if (!IsMatchLocation(pk)) + return false; + return true; + } + + + private bool IsMatchLocation(PKM pk) + { + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetSV(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; } } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator1.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator1.cs index cfe655b13..880f0dc89 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator1.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator1.cs @@ -13,6 +13,9 @@ public sealed class EncounterGenerator1 : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = GetGifts(); @@ -109,6 +112,8 @@ public IEnumerable GetEncounters(PKM pk, GameVersion game) // Calculate all 3 at the same time and pick the best result (by species). // Favor special event move gifts as Static Encounters when applicable var chain = EncounterOrigin.GetOriginChain12(pk, game); + if (chain.Length == 0) + return Array.Empty(); return GetEncounters(pk, chain, game); } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator2.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator2.cs index cb7a2dc82..68cca2369 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator2.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator2.cs @@ -13,6 +13,9 @@ public sealed class EncounterGenerator2 : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + bool korean = pk.Korean; if (groups.HasFlag(Mystery)) { @@ -116,6 +119,8 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le public IEnumerable GetEncounters(PKM pk, GameVersion game) { var chain = EncounterOrigin.GetOriginChain12(pk, game); + if (chain.Length == 0) + return Array.Empty(); return GetEncounters(pk, chain, game); } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs index 79309dbc0..046c7d231 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs @@ -14,6 +14,9 @@ public sealed class EncounterGenerator3 : IEncounterGenerator public IEnumerable GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncountersWC3.Encounter_WC3; @@ -117,12 +120,15 @@ private static IEnumerable GetPossibleStatic(EvoCriteria[] chain public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 3); return GetEncounters(pk, chain, info); } public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; + info.PIDIV = MethodFinder.Analyze(pk); IEncounterable? partial = null; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs index d19e7cda1..4192733c1 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs @@ -13,6 +13,9 @@ public sealed class EncounterGenerator3GC : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncountersWC3.Encounter_WC3CXD; @@ -67,12 +70,14 @@ private static IEnumerable GetPossibleSlots(EvoCriteria[] chain, public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 3); return GetEncounters(pk, chain, info); } public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; IEncounterable? partial = null; info.PIDIV = MethodFinder.Analyze(pk); foreach (var z in IterateInner(pk, chain)) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs index 3c87190b3..de4dada0e 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs @@ -17,12 +17,17 @@ public sealed class EncounterGenerator4 : IEncounterGenerator public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); - return GetEncounters(pk, chain, info); + var chain = EncounterOrigin.GetOriginChain(pk, 4); + if (chain.Length == 0) + return Array.Empty(); + return GetEncounters(pk, chain, info); } public IEnumerable GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { if (chain[^1].Species == (int)Species.Manaphy) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs index ab646462d..5a877dad3 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs @@ -13,12 +13,17 @@ public sealed class EncounterGenerator5 : IEncounterGenerator public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 5); + if (chain.Length == 0) + return Array.Empty(); return GetEncounters(pk, chain, info); } public IEnumerable GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G5; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs index 4351a8163..949b8fb49 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs @@ -13,6 +13,9 @@ public sealed class EncounterGenerator6 : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G6; @@ -134,12 +137,14 @@ private static IEnumerable GetPossibleTrades(EvoCriteria[] chain public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 6); return GetEncounters(pk, chain, info); } public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; var game = (GameVersion)pk.Version; IEncounterable? deferred = null; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs index f5bd99376..6a7da8317 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs @@ -13,6 +13,9 @@ public sealed class EncounterGenerator7 : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G7; @@ -108,6 +111,8 @@ private static IEnumerable GetPossibleTrades(EvoCriteria[] chain public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; var game = (GameVersion)pk.Version; bool yielded = false; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GG.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GG.cs index 101a2d2e3..3dc77ea7b 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GG.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GG.cs @@ -15,6 +15,9 @@ public sealed class EncounterGenerator7GG : IEncounterGenerator public IEnumerable GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G7GG; @@ -102,6 +105,8 @@ private static IEnumerable GetPossibleTrade(EvoCriteria[] chain, public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; bool yielded = false; if (pk.FatefulEncounter) { diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GO.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GO.cs index 30919aaaf..6349b0728 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GO.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7GO.cs @@ -12,6 +12,9 @@ public sealed class EncounterGenerator7GO : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Slot)) { var areas = EncountersGO.SlotsGO_GG; @@ -38,6 +41,8 @@ private static IEnumerable GetPossibleSlots(EvoCriteria[] chain, public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; if (!CanBeWildEncounter(pk)) yield break; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs index 2ff469763..cb01de0b2 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs @@ -12,6 +12,9 @@ public sealed class EncounterGenerator8 : IEncounterGenerator public IEnumerable GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G8; @@ -108,6 +111,8 @@ private static IEnumerable GetPossibleTrades(EvoCriteria[] chain public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; if (pk.FatefulEncounter) { bool yielded = false; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8GO.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8GO.cs index 81fb679bf..e6478849f 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8GO.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8GO.cs @@ -12,6 +12,9 @@ public sealed class EncounterGenerator8GO : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Slot)) { var table = EncountersGO.SlotsGO; @@ -38,6 +41,8 @@ private static IEnumerable GetPossibleSlots(EvoCriteria[] chain, public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; if (!CanBeWildEncounter(pk)) yield break; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8a.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8a.cs index 9cb07124b..a65863ce6 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8a.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8a.cs @@ -12,6 +12,9 @@ public sealed class EncounterGenerator8a : IEncounterGenerator public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G8A; @@ -80,6 +83,8 @@ private static IEnumerable GetPossibleSlots(EvoCriteria[] chain, public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; if (pk is PK8 { SWSH: false }) yield break; if (pk.IsEgg) @@ -139,8 +144,11 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le // Encounter Slots if (CanBeWildEncounter(pk)) { - bool hasOriginalLocation = pk is not (PK8 or PB8 { Met_Location: LocationsHOME.SWLA }); var location = pk.Met_Location; + var remap = LocationsHOME.GetRemapState(EntityContext.Gen8a, pk.Context); + bool hasOriginalLocation = true; + if (remap.HasFlag(LocationRemapState.Remapped)) + hasOriginalLocation = location != LocationsHOME.SWLA; foreach (var area in Encounters8a.SlotsLA) { if (hasOriginalLocation && !area.IsMatchLocation(location)) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs index 40899c7b1..2f38b6345 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs @@ -12,6 +12,9 @@ public sealed class EncounterGenerator8b : IEncounterGenerator public IEnumerable GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G8B; @@ -107,6 +110,8 @@ private static IEnumerable GetPossibleTrades(EvoCriteria[] chain public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info) { + if (chain.Length == 0) + yield break; if (pk is PK8) yield break; @@ -182,8 +187,11 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le if (CanBeWildEncounter(pk)) { - bool hasOriginalLocation = pk is not PK8; var location = pk.Met_Location; + var remap = LocationsHOME.GetRemapState(EntityContext.Gen8b, pk.Context); + bool hasOriginalLocation = true; + if (remap.HasFlag(LocationRemapState.Remapped)) + hasOriginalLocation = location != LocationsHOME.GetMetSWSH((ushort)location, (int)game); var encWild = game == GameVersion.BD ? Encounters8b.SlotsBD : Encounters8b.SlotsSP; foreach (var area in encWild) { diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs index e0b0e9d70..889004241 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs @@ -13,7 +13,10 @@ public sealed class EncounterGenerator9 : IEncounterGenerator public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 9); + if (chain.Length == 0) + return System.Array.Empty(); + return (GameVersion)pk.Version switch { SW when pk.Met_Location == LocationsHOME.SWSL => Instance.GetEncountersSWSH(pk, chain, SL), @@ -24,6 +27,9 @@ public IEnumerable GetEncounters(PKM pk, LegalInfo info) public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups) { + if (chain.Length == 0) + yield break; + if (groups.HasFlag(Mystery)) { var table = EncounterEvent.MGDB_G9; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator7X.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator7X.cs index 495ab13e2..b12ce3bf1 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator7X.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator7X.cs @@ -15,7 +15,7 @@ public sealed class EncounterGenerator7X : IEncounterGenerator public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 7); return GetEncounters(pk, chain, info); } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator8X.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator8X.cs index fcd0a7bbd..0e9f586bc 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator8X.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator8X.cs @@ -17,7 +17,7 @@ public sealed class EncounterGenerator8X : IEncounterGenerator public IEnumerable GetEncounters(PKM pk, LegalInfo info) { - var chain = EncounterOrigin.GetOriginChain(pk); + var chain = EncounterOrigin.GetOriginChain(pk, 8); return GetEncounters(pk, chain, info); } diff --git a/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs b/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs index cff446f19..160b5fb5e 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs @@ -31,10 +31,10 @@ public static class EncounterMovesetGenerator /// Moves that the resulting must be able to learn. /// Any specific version(s) to iterate for. If left blank, all will be checked. /// A consumable list of possible results. - /// When updating, update the sister method. - public static IEnumerable GeneratePKMs(PKM pk, ITrainerInfo info, ushort[] moves, params GameVersion[] versions) + /// When updating, update the sister method. + public static IEnumerable GeneratePKMs(PKM pk, ITrainerInfo info, ReadOnlyMemory moves, params GameVersion[] versions) { - if (!IsSane(pk, moves)) + if (!IsSane(pk, moves.Span)) yield break; OptimizeCriteria(pk, info); @@ -63,10 +63,10 @@ public static IEnumerable GeneratePKMs(PKM pk, ITrainerInfo info, ushort[] /// Moves that the resulting must be able to learn. /// Any specific version(s) to iterate for. If left blank, all will be checked. /// A consumable list of possible results. - /// When updating, update the sister method. - public static IEnumerable GenerateEncounters(PKM pk, ITrainerInfo info, ushort[] moves, params GameVersion[] versions) + /// When updating, update the sister method. + public static IEnumerable GenerateEncounters(PKM pk, ITrainerInfo info, ReadOnlyMemory moves, params GameVersion[] versions) { - if (!IsSane(pk, moves)) + if (!IsSane(pk, moves.Span)) yield break; OptimizeCriteria(pk, info); @@ -99,7 +99,7 @@ public static void OptimizeCriteria(PKM pk, ITrainerID16 info) /// Trainer information of the receiver. /// Specific generation to iterate versions for. /// Moves that the resulting must be able to learn. - public static IEnumerable GeneratePKMs(PKM pk, ITrainerInfo info, int generation, ushort[] moves) + public static IEnumerable GeneratePKMs(PKM pk, ITrainerInfo info, int generation, ReadOnlyMemory moves) { var vers = GameUtil.GetVersionsInGeneration(generation, pk.Version); return GeneratePKMs(pk, info, moves, vers); @@ -112,7 +112,7 @@ public static IEnumerable GeneratePKMs(PKM pk, ITrainerInfo info, int gener /// Specific generation to iterate versions for. /// Moves that the resulting must be able to learn. /// A consumable list of possible encounters. - public static IEnumerable GenerateEncounter(PKM pk, int generation, ushort[] moves) + public static IEnumerable GenerateEncounter(PKM pk, int generation, ReadOnlyMemory moves) { var vers = GameUtil.GetVersionsInGeneration(generation, pk.Version); return GenerateEncounters(pk, moves, vers); @@ -125,9 +125,9 @@ public static IEnumerable GenerateEncounter(PKM pk, int generati /// Moves that the resulting must be able to learn. /// Any specific version(s) to iterate for. If left blank, all will be checked. /// A consumable list of possible encounters. - public static IEnumerable GenerateEncounters(PKM pk, ushort[] moves, params GameVersion[] versions) + public static IEnumerable GenerateEncounters(PKM pk, ReadOnlyMemory moves, params GameVersion[] versions) { - if (!IsSane(pk, moves)) + if (!IsSane(pk, moves.Span)) yield break; if (versions.Length > 0) @@ -152,9 +152,9 @@ public static IEnumerable GenerateEncounters(PKM pk, ushort[] mo /// Moves that the resulting must be able to learn. /// Any specific version(s) to iterate for. If left blank, all will be checked. /// A consumable list of possible encounters. - public static IEnumerable GenerateEncounters(PKM pk, ushort[] moves, IReadOnlyList vers) + public static IEnumerable GenerateEncounters(PKM pk, ReadOnlyMemory moves, IReadOnlyList vers) { - if (!IsSane(pk, moves)) + if (!IsSane(pk, moves.Span)) yield break; foreach (var ver in vers) @@ -171,17 +171,22 @@ public static IEnumerable GenerateEncounters(PKM pk, ushort[] mo /// Moves that the resulting must be able to learn. /// Specific version to iterate for. /// A consumable list of possible encounters. - private static IEnumerable GenerateVersionEncounters(PKM pk, ushort[] moves, GameVersion version) + private static IEnumerable GenerateVersionEncounters(PKM pk, ReadOnlyMemory moves, GameVersion version) { pk.Version = (int)version; - var context = pk.Context; - if (context is EntityContext.Gen2 && version is GameVersion.RD or GameVersion.GN or GameVersion.BU or GameVersion.YW) - context = EntityContext.Gen1; // try excluding baby pokemon from our evolution chain, for move learning purposes. - var et = EvolutionTree.GetEvolutionTree(context); - var chain = et.GetValidPreEvolutions(pk, levelMax: 100, skipChecks: true); + var generation = (byte)version.GetGeneration(); + if (version is GameVersion.GO) + generation = (byte)pk.Format; + if (pk.Context is EntityContext.Gen2 && version is GameVersion.RD or GameVersion.GN or GameVersion.BU or GameVersion.YW) + generation = 1; // try excluding baby pokemon from our evolution chain, for move learning purposes. - var needs = GetNeededMoves(pk, moves, version); + var origin = new EvolutionOrigin(pk.Species, (byte)version, generation, 1, 100, true); + var chain = EvolutionChain.GetOriginChain(pk, origin); + if (chain.Length == 0) + yield break; + + ReadOnlyMemory needs = GetNeededMoves(pk, moves.Span, version, generation); var generator = EncounterGenerator.GetGenerator(version); foreach (var type in PriorityList) @@ -224,27 +229,17 @@ private static bool IsPlausibleSmeargleMoveset(EntityContext context, ReadOnlySp return true; } - private static ushort[] GetNeededMoves(PKM pk, ReadOnlySpan moves, GameVersion ver) + private static ushort[] GetNeededMoves(PKM pk, ReadOnlySpan moves, GameVersion ver, int generation) { if (pk.Species == (int)Species.Smeargle) return Array.Empty(); - // Roughly determine the generation the PKM is originating from - int origin = pk.Generation; - if (origin < 0) - origin = ver.GetGeneration(); - - // Temporarily replace the Version for VC1 transfers, so that they can have VC2 moves if needed. - bool vcBump = origin == 1 && pk.Format >= 7; - if (vcBump) - pk.Version = (int)GameVersion.C; - var length = pk.MaxMoveID + 1; var rent = ArrayPool.Shared.Rent(length); var permitted = rent.AsSpan(0, length); - var enc = new EvolutionOrigin(0, (byte)ver, (byte)origin, 1, 100, true); - var history = EvolutionChain.GetEvolutionChainsSearch(pk, enc); - var e = EncounterInvalid.Default with { Generation = origin }; + var enc = new EvolutionOrigin(pk.Species, (byte)ver, (byte)generation, 1, 100, true); + var history = EvolutionChain.GetEvolutionChainsSearch(pk, enc, ver.GetContext(), 0); + var e = EncounterInvalid.Default; // default empty LearnPossible.Get(pk, e, history, permitted); int ctr = 0; // count of moves that can be learned @@ -260,15 +255,12 @@ private static ushort[] GetNeededMoves(PKM pk, ReadOnlySpan moves, GameV permitted.Clear(); ArrayPool.Shared.Return(rent); - if (vcBump) - pk.Version = (int)ver; - if (ctr == 0) return Array.Empty(); return result[..ctr].ToArray(); } - private static IEnumerable GetPossibleOfType(PKM pk, ushort[] needs, GameVersion version, EncounterTypeGroup type, EvoCriteria[] chain, IEncounterGenerator generator) + private static IEnumerable GetPossibleOfType(PKM pk, ReadOnlyMemory needs, GameVersion version, EncounterTypeGroup type, EvoCriteria[] chain, IEncounterGenerator generator) => type switch { Egg => GetEggs(pk, needs, chain, version, generator), @@ -288,7 +280,7 @@ private static IEnumerable GetPossibleOfType(PKM pk, ushort[] ne /// Specific version to iterate for. Necessary for retrieving possible Egg Moves. /// /// A consumable list of possible encounters. - private static IEnumerable GetEggs(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) + private static IEnumerable GetEggs(PKM pk, ReadOnlyMemory needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) { if (!Breeding.CanGameGenerateEggs(version)) yield break; // no eggs from these games @@ -296,7 +288,7 @@ private static IEnumerable GetEggs(PKM pk, ushort[] needs, EvoCr var eggs = generator.GetPossible(pk, chain, version, Egg); foreach (var egg in eggs) { - if (needs.Length == 0 || HasAllNeededMovesEgg(needs, egg)) + if (needs.Length == 0 || HasAllNeededMovesEgg(needs.Span, egg)) yield return egg; } } @@ -310,7 +302,7 @@ private static IEnumerable GetEggs(PKM pk, ushort[] needs, EvoCr /// Specific version to iterate for. /// Generator /// A consumable list of possible encounters. - private static IEnumerable GetGifts(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) + private static IEnumerable GetGifts(PKM pk, ReadOnlyMemory needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) { var context = pk.Context; var gifts = generator.GetPossible(pk, chain, version, Mystery); @@ -318,7 +310,7 @@ private static IEnumerable GetGifts(PKM pk, ushort[] needs, EvoC { if (!IsSane(chain, enc, context)) continue; - if (needs.Length == 0 || GetHasAllNeededMoves(needs, enc)) + if (needs.Length == 0 || GetHasAllNeededMoves(needs.Span, enc)) yield return enc; } } @@ -332,7 +324,7 @@ private static IEnumerable GetGifts(PKM pk, ushort[] needs, EvoC /// Specific version to iterate for. /// /// A consumable list of possible encounters. - private static IEnumerable GetStatic(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) + private static IEnumerable GetStatic(PKM pk, ReadOnlyMemory needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) { var context = pk.Context; var encounters = generator.GetPossible(pk, chain, version, Static); @@ -340,7 +332,7 @@ private static IEnumerable GetStatic(PKM pk, ushort[] needs, Evo { if (!IsSane(chain, enc, context)) continue; - if (needs.Length == 0 || GetHasAllNeededMovesConsiderGen2(needs, enc)) + if (needs.Length == 0 || GetHasAllNeededMovesConsiderGen2(needs.Span, enc)) yield return enc; } } @@ -354,7 +346,7 @@ private static IEnumerable GetStatic(PKM pk, ushort[] needs, Evo /// Specific version to iterate for. /// /// A consumable list of possible encounters. - private static IEnumerable GetTrades(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) + private static IEnumerable GetTrades(PKM pk, ReadOnlyMemory needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) { var context = pk.Context; var trades = generator.GetPossible(pk, chain, version, Trade); @@ -362,7 +354,7 @@ private static IEnumerable GetTrades(PKM pk, ushort[] needs, Evo { if (!IsSane(chain, enc, context)) continue; - if (needs.Length == 0 || GetHasAllNeededMovesConsiderGen2(needs, enc)) + if (needs.Length == 0 || GetHasAllNeededMovesConsiderGen2(needs.Span, enc)) yield return enc; } } @@ -376,7 +368,7 @@ private static IEnumerable GetTrades(PKM pk, ushort[] needs, Evo /// Origin version /// /// A consumable list of possible encounters. - private static IEnumerable GetSlots(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) + private static IEnumerable GetSlots(PKM pk, ReadOnlyMemory needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator) { var context = pk.Context; var slots = generator.GetPossible(pk, chain, version, Slot); @@ -384,7 +376,7 @@ private static IEnumerable GetSlots(PKM pk, ushort[] needs, EvoC { if (!IsSane(chain, slot, context)) continue; - if (needs.Length == 0 || HasAllNeededMovesSlot(needs, slot)) + if (needs.Length == 0 || HasAllNeededMovesSlot(needs.Span, slot)) yield return slot; } } diff --git a/PKHeX.Core/Legality/Encounters/Information/EncounterSuggestion.cs b/PKHeX.Core/Legality/Encounters/Information/EncounterSuggestion.cs index 2807f7104..8951a44e2 100644 --- a/PKHeX.Core/Legality/Encounters/Information/EncounterSuggestion.cs +++ b/PKHeX.Core/Legality/Encounters/Information/EncounterSuggestion.cs @@ -18,11 +18,15 @@ public static class EncounterSuggestion if (pk.WasEgg) return GetSuggestedEncounterEgg(pk, loc); - var chain = EvolutionChain.GetValidPreEvolutions(pk, maxLevel: 100, skipChecks: true); + Span chain = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions]; + var origin = new EvolutionOrigin(pk.Species, (byte)pk.Version, (byte)pk.Generation, (byte)pk.CurrentLevel, (byte)pk.CurrentLevel, SkipChecks: true); + var count = EvolutionChain.GetOriginChain(chain, pk, origin); var ver = (GameVersion)pk.Version; var generator = EncounterGenerator.GetGenerator(ver); - var w = EncounterUtil.GetMinByLevel(chain, generator.GetPossible(pk, chain, ver, EncounterTypeGroup.Slot)); - var s = EncounterUtil.GetMinByLevel(chain, generator.GetPossible(pk, chain, ver, EncounterTypeGroup.Static)); + + var evos = chain[..count].ToArray(); + var w = EncounterUtil.GetMinByLevel(evos, generator.GetPossible(pk, evos, ver, EncounterTypeGroup.Slot)); + var s = EncounterUtil.GetMinByLevel(evos, generator.GetPossible(pk, evos, ver, EncounterTypeGroup.Static)); if (w is null) return s is null ? null : GetSuggestedEncounter(pk, s, loc); @@ -116,22 +120,22 @@ public static int GetLowestLevel(PKM pk, byte startLevel) if (startLevel >= 100) startLevel = 100; - var table = EvolutionTree.GetEvolutionTree(pk.Context); - int count = 1; - byte i = 100; + int most = 1; + Span chain = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions]; + var origin = new EvolutionOrigin(pk.Species, (byte)pk.Version, (byte)pk.Generation, startLevel, 100, SkipChecks: true); while (true) { - var evos = table.GetValidPreEvolutions(pk, levelMax: i, skipChecks: true, levelMin: startLevel); - if (evos.Length < count) // lost an evolution, prior level was minimum current level - return GetMaxLevelMax(evos) + 1; - count = evos.Length; - if (i == startLevel) + var count = EvolutionChain.GetOriginChain(chain, pk, origin); + if (count < most) // lost an evolution, prior level was minimum current level + return GetMaxLevelMax(chain) + 1; + most = count; + if (origin.LevelMax == origin.LevelMin) return startLevel; - --i; + origin = origin with { LevelMax = (byte)(origin.LevelMax - 1) }; } } - private static int GetMaxLevelMax(EvoCriteria[] evos) + private static int GetMaxLevelMax(ReadOnlySpan evos) { int max = 0; foreach (var evo in evos) diff --git a/PKHeX.Core/Legality/Encounters/Verifiers/EvolutionVerifier.cs b/PKHeX.Core/Legality/Encounters/Verifiers/EvolutionVerifier.cs index f3b0eba69..603a96052 100644 --- a/PKHeX.Core/Legality/Encounters/Verifiers/EvolutionVerifier.cs +++ b/PKHeX.Core/Legality/Encounters/Verifiers/EvolutionVerifier.cs @@ -8,6 +8,8 @@ namespace PKHeX.Core; /// public static class EvolutionVerifier { + private static readonly CheckResult VALID = new(CheckIdentifier.Evolution); + /// /// Verifies Evolution scenarios of an for an input and relevant . /// @@ -16,7 +18,7 @@ public static class EvolutionVerifier public static CheckResult VerifyEvolution(PKM pk, LegalInfo info) { // Check if basic evolution methods are satisfiable with this encounter. - if (!IsValidEvolution(pk, info)) + if (!IsValidEvolution(pk, info.EvoChainsAllGens, info.EncounterOriginal)) return new CheckResult(Severity.Invalid, CheckIdentifier.Evolution, LEvoInvalid); // Check if complex evolution methods are satisfiable with this encounter. @@ -26,36 +28,34 @@ public static CheckResult VerifyEvolution(PKM pk, LegalInfo info) return VALID; } - private static readonly CheckResult VALID = new(CheckIdentifier.Evolution); - /// /// Checks if the Evolution from the source is valid. /// /// Source data to verify - /// Source supporting information to verify with + /// Source supporting information to verify with + /// Matched encounter /// Evolution is valid or not - private static bool IsValidEvolution(PKM pk, LegalInfo info) + private static bool IsValidEvolution(PKM pk, EvolutionHistory history, IEncounterTemplate enc) { - var chains = info.EvoChainsAllGens; - if (chains.Get(pk.Context).Length == 0) + // OK if un-evolved from original encounter + var encSpecies = enc.Species; + var curSpecies = pk.Species; + if (curSpecies == encSpecies) + return true; // never evolved + + var current = history.Get(pk.Context); + if (!EvolutionUtil.Contains(current, curSpecies)) return false; // Can't exist as current species - // OK if un-evolved from original encounter - ushort species = pk.Species; - var enc = info.EncounterMatch; - if (species == enc.Species) // never evolved - return true; - - // Bigender->Fixed (non-Genderless) destination species, accounting for PID-Gender relationship - if (species == (int)Species.Vespiquen && enc.Generation < 6 && (pk.EncryptionConstant & 0xFF) >= 0x1F) // Combee->Vespiquen Invalid Evolution + // Double check that our encounter was able to exist as the encounter species. + var original = history.Get(enc.Context); + if (!EvolutionUtil.Contains(original, encSpecies)) return false; - // Double check that our encounter was able to exist as the encounter species. - foreach (var z in chains.Get(enc.Context)) - { - if (z.Species == enc.Species) - return true; - } - return false; + // Bigender->Fixed (non-Genderless) destination species, accounting for PID-Gender relationship + if (curSpecies == (int)Species.Vespiquen && enc.Generation < 6 && (pk.EncryptionConstant & 0xFF) >= 0x1F) // Combee->Vespiquen Invalid Evolution + return false; + + return true; } } diff --git a/PKHeX.Core/Legality/Evolutions/EncounterOrigin.cs b/PKHeX.Core/Legality/Evolutions/EncounterOrigin.cs index 61cd646ce..fd3ea037f 100644 --- a/PKHeX.Core/Legality/Evolutions/EncounterOrigin.cs +++ b/PKHeX.Core/Legality/Evolutions/EncounterOrigin.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using static PKHeX.Core.Species; namespace PKHeX.Core; @@ -13,14 +11,14 @@ public static class EncounterOrigin /// Gets possible evolution details for the input /// /// Current state of the Pokémon + /// Original Generation /// Possible origin species-form-levels to match against encounter data. /// Use if the originated from Generation 1 or 2. - public static EvoCriteria[] GetOriginChain(PKM pk) + public static EvoCriteria[] GetOriginChain(PKM pk, byte generation) { - bool hasOriginMet = pk.HasOriginalMetLocation; - var maxLevel = GetLevelOriginMax(pk, hasOriginMet); - var minLevel = GetLevelOriginMin(pk, hasOriginMet); - return GetOriginChain(pk, -1, (byte)maxLevel, (byte)minLevel, hasOriginMet); + var (minLevel, maxLevel) = GetMinMax(pk, generation); + var origin = new EvolutionOrigin(pk.Species, (byte)pk.Version, generation, minLevel, maxLevel); + return EvolutionChain.GetOriginChain(pk, origin); } /// @@ -31,150 +29,67 @@ public static EvoCriteria[] GetOriginChain(PKM pk) /// Possible origin species-form-levels to match against encounter data. public static EvoCriteria[] GetOriginChain12(PKM pk, GameVersion gameSource) { + var (minLevel, maxLevel) = GetMinMaxGB(pk); bool rby = gameSource == GameVersion.RBY; - var maxSpecies = rby ? Legal.MaxSpeciesID_1 : Legal.MaxSpeciesID_2; - - bool hasOriginMet; - int maxLevel, minLevel; - if (pk is ICaughtData2 pk2) - { - hasOriginMet = pk2.CaughtData != 0; - maxLevel = rby && Future_LevelUp2.Contains(pk.Species) ? pk.CurrentLevel - 1 : pk.CurrentLevel; - minLevel = !hasOriginMet ? 2 : pk.IsEgg ? 5 : pk2.Met_Level; - } - else if (pk is PK1 pk1) - { - hasOriginMet = false; - maxLevel = pk1.CurrentLevel; - minLevel = 2; - } - else if (rby) - { - hasOriginMet = false; - maxLevel = Future_LevelUp2.Contains(pk.Species) ? pk.CurrentLevel - 1 : GetLevelOriginMaxTransfer(pk, pk.Met_Level, 1); - minLevel = 2; - } - else // GSC - { - hasOriginMet = false; - maxLevel = GetLevelOriginMaxTransfer(pk, pk.Met_Level, 2); - minLevel = 2; - } - - return GetOriginChain(pk, maxSpecies, (byte)maxLevel, (byte)minLevel, hasOriginMet); + byte ver = rby ? (byte)GameVersion.RBY : (byte)GameVersion.GSC; + byte gen = rby ? (byte)1 : (byte)2; + var origin = new EvolutionOrigin(pk.Species, ver, gen, minLevel, maxLevel); + return EvolutionChain.GetOriginChain(pk, origin); } - private static EvoCriteria[] GetOriginChain(PKM pk, int maxSpecies, byte maxLevel, byte minLevel, bool hasOriginMet) + private static (byte minLevel, byte maxLevel) GetMinMax(PKM pk, byte generation) { - if (maxLevel < minLevel) - return Array.Empty(); - - if (hasOriginMet) - return EvolutionChain.GetValidPreEvolutions(pk, maxSpecies, maxLevel, minLevel); - - // Permit the maximum to be all the way up to Current Level; we'll trim these impossible evolutions out later. - var tempMax = pk.CurrentLevel; - var chain = EvolutionChain.GetValidPreEvolutions(pk, maxSpecies, tempMax, minLevel); - - for (var i = 0; i < chain.Length; i++) - chain[i] = chain[i] with { LevelMax = maxLevel, LevelMin = minLevel }; - - return chain; + byte maxLevel = (byte)pk.CurrentLevel; + byte minLevel = GetLevelOriginMin(pk, generation); + return (minLevel, maxLevel); } - private static int GetLevelOriginMin(PKM pk, bool hasMet) + private static (byte minLevel, byte maxLevel) GetMinMaxGB(PKM pk) { - if (pk.Format <= 3) - { - if (pk.IsEgg) - return 5; - return Math.Max(2, pk.Met_Level); - } - if (!hasMet) - return 1; - return Math.Max(1, pk.Met_Level); + byte maxLevel = (byte)pk.CurrentLevel; + byte minLevel = GetLevelOriginMinGB(pk); + return (minLevel, maxLevel); } - private static int GetLevelOriginMax(PKM pk, bool hasMet) + private static byte GetLevelOriginMin(PKM pk, byte generation) => generation switch { - var met = pk.Met_Level; - if (hasMet) - return pk.CurrentLevel; - - int generation = pk.Generation; - if (generation >= 4) - return met; - - var downLevel = GetLevelOriginMaxTransfer(pk, pk.CurrentLevel, generation); - return Math.Min(met, downLevel); - } - - private static int GetLevelOriginMaxTransfer(PKM pk, int met, int generation) - { - var species = pk.Species; - - if (Future_LevelUp.TryGetValue((ushort)(species | (pk.Form << 11)), out var delta)) - return met - delta; - - if (generation < 4 && Future_LevelUp4.Contains(species) && (pk.Format <= 7 || !Future_LevelUp4_Not8.Contains(species))) - return met - 1; - - return met; - } - - /// - /// Species introduced in Generation 2 that require a level up to evolve into from a specimen that originated in a previous generation. - /// - private static readonly HashSet Future_LevelUp2 = new() - { - (int)Crobat, - (int)Espeon, - (int)Umbreon, - (int)Blissey, + 3 => GetLevelOriginMin3(pk), + 4 => GetLevelOriginMin4(pk), + _ => Math.Max((byte)1, (byte)pk.Met_Level), }; - /// - /// Species introduced in Generation 4 that require a level up to evolve into from a specimen that originated in a previous generation. - /// - private static readonly HashSet Future_LevelUp4 = new() + private static bool IsEggLocationNonZero(PKM pk) => pk.Egg_Location != LocationEdits.GetNoneLocation(pk.Context); + + private static byte GetLevelOriginMinGB(PKM pk) { - (int)Ambipom, - (int)Weavile, - (int)Magnezone, - (int)Lickilicky, - (int)Tangrowth, - (int)Yanmega, - (int)Leafeon, - (int)Glaceon, - (int)Mamoswine, - (int)Gliscor, - (int)Probopass, - }; + const byte EggLevel = 5; + const byte MinWildLevel = 2; + if (pk.IsEgg) + return EggLevel; + if (pk is not ICaughtData2 { CaughtData: not 0 } pk2) + return MinWildLevel; + return (byte)pk2.Met_Level; + } - /// - /// Species introduced in Generation 4 that used to require a level up to evolve prior to Generation 8. - /// - private static readonly HashSet Future_LevelUp4_Not8 = new() + private static byte GetLevelOriginMin3(PKM pk) { - (int)Magnezone, // Thunder Stone - (int)Leafeon, // Leaf Stone - (int)Glaceon, // Ice Stone - }; + const byte EggLevel = 5; + const byte MinWildLevel = 2; + if (pk.Format != 3) + return MinWildLevel; + if (pk.IsEgg) + return EggLevel; + return (byte)pk.Met_Level; + } - /// - /// Species introduced in Generation 6+ that require a level up to evolve into from a specimen that originated in a previous generation. - /// - private static readonly Dictionary Future_LevelUp = new() + private static byte GetLevelOriginMin4(PKM pk) { - // Gen6 - {(int)Sylveon, 1}, - - // Gen7 - {(int)Marowak | (1 << 11), 1}, - - // Gen8 - {(int)Weezing | (1 << 11), 1}, - {(int)MrMime | (1 << 11), 1}, - {(int)MrRime, 2}, - }; + const byte EggLevel = 1; + const byte MinWildLevel = 2; + if (pk.Format != 4) + return IsEggLocationNonZero(pk) ? EggLevel : MinWildLevel; + if (pk.IsEgg || IsEggLocationNonZero(pk)) + return EggLevel; + return (byte)pk.Met_Level; + } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionChain.cs b/PKHeX.Core/Legality/Evolutions/EvolutionChain.cs index 36e65fc5e..c5079bfeb 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionChain.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionChain.cs @@ -1,7 +1,5 @@ using System; -using static PKHeX.Core.Legal; - namespace PKHeX.Core; /// @@ -11,29 +9,53 @@ public static class EvolutionChain { public static EvolutionHistory GetEvolutionChainsAllGens(PKM pk, IEncounterTemplate enc) { - var origin = new EvolutionOrigin(enc.Species, (byte)enc.Version, (byte)enc.Generation, enc.LevelMin, (byte)pk.CurrentLevel); + var min = GetMinLevel(pk, enc); + var origin = new EvolutionOrigin(pk.Species, (byte)enc.Version, (byte)enc.Generation, min, (byte)pk.CurrentLevel); if (!pk.IsEgg && enc is not EncounterInvalid) - return GetEvolutionChainsSearch(pk, origin); + return GetEvolutionChainsSearch(pk, origin, enc.Context, enc.Species); - var history = new EvolutionHistory(); - var group = EvolutionGroupUtil.GetCurrentGroup(pk); - var chain = group.GetInitialChain(pk, origin, pk.Species, pk.Form); - history.Set(pk.Context, chain); - return history; + return GetEvolutionChainsSearch(pk, origin, pk.Context, enc.Species); } - public static EvolutionHistory GetEvolutionChainsSearch(PKM pk, EvolutionOrigin enc) + private static byte GetMinLevel(PKM pk, IEncounterTemplate enc) => enc.Generation switch { - var group = EvolutionGroupUtil.GetCurrentGroup(pk); - ReadOnlySpan chain = group.GetInitialChain(pk, enc, pk.Species, pk.Form); + 2 => pk is ICaughtData2 c2 ? Math.Max((byte)c2.Met_Level, enc.LevelMin) : enc.LevelMin, + <= 4 when pk.Format != enc.Generation => enc.LevelMin, + _ => Math.Max((byte)pk.Met_Level, enc.LevelMin), + }; + public static EvolutionHistory GetEvolutionChainsSearch(PKM pk, EvolutionOrigin enc, EntityContext context, ushort encSpecies) + { + Span chain = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions]; + return EvolutionChainsSearch(pk, enc, context, encSpecies, chain); + } + + private static EvolutionHistory EvolutionChainsSearch(PKM pk, EvolutionOrigin enc, EntityContext context, ushort encSpecies, Span chain) + { var history = new EvolutionHistory(); + var length = GetOriginChain(chain, pk, enc, encSpecies, false); + if (length == 0) + return history; + chain = chain[..length]; + + // Update the chain to only include the current species, leave future evolutions as unknown + if (encSpecies != 0) + EvolutionUtil.ConditionBaseChainForward(chain, encSpecies); + if (context == EntityContext.Gen2) + { + EvolutionGroup2.Instance.Evolve(chain, pk, enc, history); + EvolutionGroup1.Instance.Evolve(chain, pk, enc, history); + if (pk.Format > 2) + context = EntityContext.Gen7; + else + return history; + } + + var group = EvolutionGroupUtil.GetGroup(context); while (true) { - var any = group.Append(pk, history, ref chain, enc); - if (!any) - break; - var previous = group.GetPrevious(pk, enc); + group.Evolve(chain, pk, enc, history); + var previous = group.GetNext(pk, enc); if (previous is null) break; group = previous; @@ -41,18 +63,65 @@ public static EvolutionHistory GetEvolutionChainsSearch(PKM pk, EvolutionOrigin return history; } - public static EvoCriteria[] GetValidPreEvolutions(PKM pk, int maxspeciesorigin = -1, int maxLevel = -1, int minLevel = 1, bool skipChecks = false) + public static EvoCriteria[] GetOriginChain(PKM pk, EvolutionOrigin enc, ushort encSpecies = 0, bool discard = true) { - if (maxLevel < 0) - maxLevel = pk.CurrentLevel; + Span result = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions]; + int count = GetOriginChain(result, pk, enc, encSpecies, discard); + if (count == 0) + return Array.Empty(); - if (maxspeciesorigin == -1 && ParseSettings.AllowGen1Tradeback && pk is { Format: <= 2, Generation: 1 }) - maxspeciesorigin = MaxSpeciesID_2; + var chain = result[..count]; + return chain.ToArray(); + } - var context = pk.Context; - if (context < EntityContext.Gen2) - context = EntityContext.Gen2; - var et = EvolutionTree.GetEvolutionTree(context); - return et.GetValidPreEvolutions(pk, levelMax: (byte)maxLevel, maxSpeciesOrigin: maxspeciesorigin, skipChecks: skipChecks, levelMin: (byte)minLevel); + public static int GetOriginChain(Span result, PKM pk, EvolutionOrigin enc, ushort encSpecies = 0, bool discard = true) + { + ushort species = enc.Species; + byte form = pk.Form; + if (pk.IsEgg && !enc.SkipChecks) + { + result[0] = new EvoCriteria { Species = species, Form = form, LevelMax = enc.LevelMax, LevelMin = enc.LevelMax }; + return 1; + } + + result[0] = new EvoCriteria { Species = species, Form = form, LevelMax = enc.LevelMax }; + var count = DevolveFrom(result, pk, enc, pk.Context, encSpecies, discard); + + var chain = result[..count]; + EvolutionUtil.CleanDevolve(chain, enc.LevelMin); + return count; + } + + private static int DevolveFrom(Span result, PKM pk, EvolutionOrigin enc, EntityContext context, ushort encSpecies, bool discard) + { + var group = EvolutionGroupUtil.GetGroup(context); + while (true) + { + group.Devolve(result, pk, enc); + var previous = group.GetPrevious(pk, enc); + if (previous is null) + break; + group = previous; + } + + if (discard) + group.DiscardForOrigin(result, pk); + if (encSpecies != 0) + return EvolutionUtil.IndexOf(result, encSpecies) + 1; + return GetCount(result); + } + + private static int GetCount(Span result) + { + // return the count of species != 0 + int count = 0; + foreach (var evo in result) + { + if (evo.Species == 0) + break; + count++; + } + + return count; } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup1.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup1.cs index 9bb84a550..9790a2798 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup1.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup1.cs @@ -2,60 +2,76 @@ namespace PKHeX.Core; -public sealed class EvolutionGroup1 : IEvolutionGroup +public sealed class EvolutionGroup1 : IEvolutionGroup, IEvolutionEnvironment { public static readonly EvolutionGroup1 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves1; - private const int MaxSpecies = Legal.MaxSpeciesID_1; - private static PersonalTable1 Personal => PersonalTable.RB; public IEvolutionGroup GetNext(PKM pk, EvolutionOrigin enc) => EvolutionGroup2.Instance; - public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => pk.Format == 1 ? EvolutionGroup2.Instance : null; + public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => pk.Format == 1 && ParseSettings.AllowGen1Tradeback ? EvolutionGroup2.Instance : null; - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public void DiscardForOrigin(Span result, PKM pk) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; + if (!ParseSettings.AllowGen1Tradeback) + return; // no other groups were iterated, so no need to discard - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = enc.LevelMin }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen1 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; + EvolutionUtil.Discard(result, PersonalTable.C); } - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + if (pk.Format >= 7 && !enc.SkipChecks) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) + var max = pk.Met_Level; + enc = enc with { LevelMin = 2, LevelMax = (byte)max }; + } + int present = 1; + for (int i = 1; i < result.Length; i++) + { + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) continue; - result = evo; - return true; + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; } + return present; + } - result = default; - return false; + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (pk.Format > 2) + enc = enc with { LevelMax = (byte)pk.Met_Level }; + + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen1 = EvolutionUtil.SetHistory(result, PersonalTable.RB); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup2.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup2.cs index cb17a37c7..0912ca47c 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup2.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup2.cs @@ -6,64 +6,69 @@ public sealed class EvolutionGroup2 : IEvolutionGroup { public static readonly EvolutionGroup2 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves2; - private const int MaxSpecies = Legal.MaxSpeciesID_2; private const int Generation = 2; private static PersonalTable2 Personal => PersonalTable.C; public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup7.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => pk.Format != 1 ? EvolutionGroup1.Instance : null; + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - byte min; - if (pk.Format > Generation) - min = enc.LevelMin; - else if (pk is ICaughtData2 { CaughtData: not 0 } c) - min = (byte)c.Met_Level; - else - min = enc.LevelMin; - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = min }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen2 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + if (pk.Format > Generation && !enc.SkipChecks) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) - continue; - - result = evo; - return true; + var max = pk.Met_Level; + EvolutionUtil.UpdateCeiling(result, max); + enc = enc with { LevelMin = 2, LevelMax = (byte)max }; } - result = default; - return false; + int present = 1; + for (int i = 1; i < result.Length; i++) + { + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) + continue; + + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; + } + return present; + } + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (pk.Format > Generation) + enc = enc with { LevelMax = (byte)pk.Met_Level }; + + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen2 = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs index 4c5bbf513..42d0ac29f 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs @@ -6,58 +6,69 @@ public sealed class EvolutionGroup3 : IEvolutionGroup { public static readonly EvolutionGroup3 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves3; - private const int MaxSpecies = Legal.MaxSpeciesID_3; private const int Generation = 3; private static PersonalTable3 Personal => PersonalTable.E; public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup4.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => null; + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var min = pk.Format > Generation ? enc.LevelMin : (byte)pk.Met_Level; - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = min }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen3 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + if (pk.Format == 4 && !enc.SkipChecks) // 5+ already have been revised { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) - continue; - - result = evo; - return true; + var max = pk.Met_Level; + EvolutionUtil.UpdateCeiling(result, max); + enc = enc with { LevelMin = 1, LevelMax = (byte)max }; } - result = default; - return false; + int present = 1; + for (int i = 1; i < result.Length; i++) + { + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) + continue; + + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; + } + return present; + } + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (pk.Format > Generation) + enc = enc with { LevelMax = (byte)pk.Met_Level }; + + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen3 = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup4.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup4.cs index 93352e814..35020e8f4 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup4.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup4.cs @@ -6,58 +6,71 @@ public sealed class EvolutionGroup4 : IEvolutionGroup { public static readonly EvolutionGroup4 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves4; - private const int MaxSpecies = Legal.MaxSpeciesID_4; private const int Generation = 4; private static PersonalTable4 Personal => PersonalTable.HGSS; public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup5.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => enc.Generation == 3 ? EvolutionGroup3.Instance : null; + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var min = pk.Format > Generation ? enc.LevelMin : (byte)pk.Met_Level; - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = min }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen4 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + if (pk.Format > Generation && !enc.SkipChecks) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) - continue; - - result = evo; - return true; + var max = pk.Met_Level; + EvolutionUtil.UpdateCeiling(result, max); + enc = enc with { LevelMin = 1, LevelMax = (byte)max }; } - result = default; - return false; + int present = 1; + for (int i = 1; i < result.Length; i++) + { + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) + continue; + + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; + } + return present; + } + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (pk.Format > Generation) + enc = enc with { LevelMax = (byte)pk.Met_Level }; + else if (enc.Generation < Generation) + EvolutionUtil.UpdateFloor(result, pk.Met_Level); + + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen4 = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup5.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup5.cs index 257ddd578..3c50a4898 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup5.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup5.cs @@ -6,57 +6,62 @@ public sealed class EvolutionGroup5 : IEvolutionGroup { public static readonly EvolutionGroup5 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves5; - private const int MaxSpecies = Legal.MaxSpeciesID_5; private const int Generation = 5; private static PersonalTable5B2W2 Personal => PersonalTable.B2W2; public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup6.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => enc.Generation < Generation ? EvolutionGroup4.Instance : null; + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = (byte)pk.Met_Level }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen5 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + int present = 1; + for (int i = 1; i < result.Length; i++) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) continue; - result = evo; - return true; + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; } + return present; + } - result = default; - return false; + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (enc.Generation < Generation) + EvolutionUtil.UpdateFloor(result, pk.Met_Level); + + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen5 = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup6.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup6.cs index 7867bd07d..1588d6bce 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup6.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup6.cs @@ -6,57 +6,59 @@ public sealed class EvolutionGroup6 : IEvolutionGroup { public static readonly EvolutionGroup6 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves6; - private const int MaxSpecies = Legal.MaxSpeciesID_6; private const int Generation = 6; private static PersonalTable6AO Personal => PersonalTable.AO; public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup7.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => enc.Generation < Generation ? EvolutionGroup5.Instance : null; + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = (byte)pk.Met_Level }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen6 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + int present = 1; + for (int i = 1; i < result.Length; i++) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) continue; - result = evo; - return true; + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; } + return present; + } - result = default; - return false; + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen6 = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7.cs index 8a0872b0d..d6671f87c 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7.cs @@ -6,64 +6,90 @@ public sealed class EvolutionGroup7 : IEvolutionGroup { public static readonly EvolutionGroup7 Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves7; - private const int MaxSpecies = Legal.MaxSpeciesID_7_USUM; private const int Generation = 7; private static PersonalTable7 Personal => PersonalTable.USUM; - public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup8.Instance : null; + public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroupHOME.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) { - if (enc.Generation <= 2) + if (enc.Generation is 1 or 2) return EvolutionGroup2.Instance; if (enc.Generation < Generation) return EvolutionGroup6.Instance; return null; } - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); + + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = (byte)pk.Met_Level }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen7 = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + int present = 1; + for (int i = 1; i < result.Length; i++) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) + ref var prev = ref result[i - 1]; + RevertMutatedForms(ref prev); + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) continue; - result = evo; - return true; + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; } - - result = default; - return false; + return present; } + + private static void RevertMutatedForms(ref EvoCriteria evo) + { + // Zygarde's 10% Form and 50% Form can be changed with the help of external tools: the Reassembly Unit and the Zygarde Cube. + if (evo is { Species: (ushort)Species.Zygarde, Form: not (0 or 1) }) + evo = evo with { Form = evo.LevelMax == 63 ? (byte)1 : (byte)0 }; // 50% Forme + else if (evo is { Species: (ushort)Species.Silvally, Form: not 0 }) + evo = evo with { Form = 0 }; // Normal + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (enc.Generation <= 2) // VC Transfer + EvolutionUtil.UpdateFloor(result, pk.Met_Level); + + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen7 = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + var b = Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); + return b && !IsEvolutionBanned(pk, result); + } + + // Kanto Evolutions are not accessible unless it visits US/UM. + private static bool IsEvolutionBanned(PKM pk, in ISpeciesForm dest) => pk is PK7 { SM: true, IsUntraded: true } && dest switch + { + { Species: (ushort)Species.Raichu, Form: 0 } => true, + { Species: (ushort)Species.Marowak, Form: 0 } => true, + { Species: (ushort)Species.Exeggutor, Form: 0 } => true, + _ => false, + }; } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7b.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7b.cs index 3f52dc90c..4ed405645 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7b.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup7b.cs @@ -6,57 +6,59 @@ public sealed class EvolutionGroup7b : IEvolutionGroup { public static readonly EvolutionGroup7b Instance = new(); private static readonly EvolutionTree Tree = EvolutionTree.Evolves7b; - private const int MaxSpecies = Legal.MaxSpeciesID_7b; private const int Generation = 7; private static PersonalTable7GG Personal => PersonalTable.GG; - public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup8.Instance : null; + public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroupHOME.Instance : null; public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => null; + public void DiscardForOrigin(Span result, PKM pk) => EvolutionUtil.Discard(result, Personal); - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = (byte)pk.Met_Level }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - history.Gen7b = revised; - - // Retain a reference to the current chain for future appending as we step backwards. - chain = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - return Tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private static bool GetFirstEvolution(ReadOnlySpan chain, out EvoCriteria result) - { - var pt = Personal; - foreach (var evo in chain) + int present = 1; + for (int i = 1; i < result.Length; i++) { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) + var prev = result[i - 1]; + if (!TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) continue; - result = evo; - return true; + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; } + return present; + } - result = default; - return false; + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + history.Gen7b = EvolutionUtil.SetHistory(result, Personal); + return present; + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup8.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup8.cs deleted file mode 100644 index 2b29d7f55..000000000 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup8.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using static PKHeX.Core.GameVersion; - -namespace PKHeX.Core; - -public sealed class EvolutionGroup8 : IEvolutionGroup -{ - public static readonly EvolutionGroup8 Instance = new(); - private static readonly EvolutionTree Tree8 = EvolutionTree.Evolves8; - private static readonly EvolutionTree Tree8a = EvolutionTree.Evolves8a; - private static readonly EvolutionTree Tree8b = EvolutionTree.Evolves8b; - private const int MaxSpecies = Legal.MaxSpeciesID_8a; - private const int Generation = 8; - - public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => EvolutionGroup9.Instance; - public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) - { - if ((GameVersion)enc.Version is GP or GE or GG or GO) - return EvolutionGroup7b.Instance; - if (enc.Generation >= Generation) - return null; - return EvolutionGroup7.Instance; - } - - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) - { - if (chain.Length == 0) - return false; - - var swsh = Append(pk, chain, enc, PersonalTable.SWSH, Tree8 , ref history.Gen8 ); - var pla = Append(pk, chain, enc, PersonalTable.LA , Tree8a, ref history.Gen8a); - var bdsp = Append(pk, chain, enc, PersonalTable.BDSP, Tree8b, ref history.Gen8b); - - if (!(swsh || pla || bdsp)) - return false; - - // Block BD/SP transfers that are impossible - BlockBDSP(history, enc); - - if (!pk.IsUntraded && !(ParseSettings.IgnoreTransferIfNoTracker && pk is IHomeTrack { HasTracker: false })) - { - CrossPropagate(history); - } - else - { - DeleteAdjacent(pk, history); - - if (!HasVisited(history)) - return false; - } - - chain = GetMaxChain(history); - - return chain.Length != 0; - } - - private static bool HasVisited(EvolutionHistory history) - { - return history.Gen8.Length != 0 || history.Gen8a.Length != 0 || history.Gen8b.Length != 0; - } - - private static void DeleteAdjacent(PKM pk, EvolutionHistory history) - { - if (pk is not PK8) - history.Gen8 = Array.Empty(); - if (pk is not PA8) - history.Gen8a = Array.Empty(); - if (pk is not PB8) - history.Gen8b = Array.Empty(); - } - - private static void BlockBDSP(EvolutionHistory history, EvolutionOrigin enc) - { - var bdsp = history.Gen8b; - if (bdsp.Length == 0) - return; - - // Spinda and Nincada cannot transfer in or out as the current species. - // Remove them from their non-origin game evolution chains. - var last = bdsp[^1]; - if (last.Species == (int)Species.Nincada) - RemoveIfSpecies(history, enc); - else if (last.Species == (int)Species.Spinda) - RemoveIfSpecies(history, enc); - - static void RemoveIfSpecies(EvolutionHistory history, EvolutionOrigin enc) - { - var wasBDSP = BDSP.Contains(enc.Version); - ref var evos = ref wasBDSP ? ref history.Gen8 : ref history.Gen8b; - evos = evos.Length < 2 ? Array.Empty() : evos.AsSpan(0, evos.Length - 1).ToArray(); - } - } - - private static ReadOnlySpan GetMaxChain(EvolutionHistory history) - { - var arr0 = history.Gen8; - var arr1 = history.Gen8a; - var arr2 = history.Gen8b; - if (arr0.Length >= arr1.Length && arr0.Length >= arr2.Length) - return arr0; - if (arr1.Length >= arr2.Length) - return arr1; - return arr2; - } - - private static void CrossPropagate(EvolutionHistory history) - { - var arr0 = history.Gen8; - var arr1 = history.Gen8a; - var arr2 = history.Gen8b; - - ReplaceIfBetter(arr0, arr1, arr2); - ReplaceIfBetter(arr1, arr0, arr2); - ReplaceIfBetter(arr2, arr0, arr1); - } - - private static void ReplaceIfBetter(Span local, ReadOnlySpan other1, ReadOnlySpan other2) - { - for (int i = 0; i < local.Length; i++) - { - ReplaceIfBetter(local, other1, i); - ReplaceIfBetter(local, other2, i); - } - } - - private static void ReplaceIfBetter(Span local, ReadOnlySpan other, int parentIndex) - { - // Replace the evolution entry if another connected game has a better evolution method (different min/max). - var native = local[parentIndex]; - - // Check if the evo is in the other game; if not, we're done here. - var index = IndexOfSpecies(other, native.Species); - if (index == -1) - return; - - var alt = other[index]; - if (alt.LevelMin < native.LevelMin || alt.LevelMax > native.LevelMax) - local[parentIndex] = alt; - } - - private static int IndexOfSpecies(ReadOnlySpan evos, ushort species) - { - // Returns the index of the first evo that matches the species - for (int i = 0; i < evos.Length; i++) - { - if (evos[i].Species == species) - return i; - } - return -1; - } - - private static bool Append(PKM pk, ReadOnlySpan chain, EvolutionOrigin enc, T pt, EvolutionTree tree, ref EvoCriteria[] dest) where T : IPersonalTable - { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(pt, chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = (byte)pk.Met_Level }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form, tree); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - dest = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - if (!GetPreferredGroup(species, form, out var group)) - return Array.Empty(); - var tree = GetTree(group); - return GetInitialChain(pk, enc, species, form, tree); - } - - private static EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form, EvolutionTree tree) - { - if (species is (int)Species.Dialga or (int)Species.Palkia or (int)Species.Arceus) - form = 0; // PLA forms; play nice for yielding SW/SH context - return tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvolutionTree GetTree(PreferredGroup group) => group switch - { - PreferredGroup.BDSP => Tree8b, - PreferredGroup.LA => Tree8a, - _ => Tree8, - }; - - private static bool GetPreferredGroup(ushort species, byte form, out PreferredGroup result) - { - if (PersonalTable.LA.IsPresentInGame(species, form)) - result = PreferredGroup.LA; - else if (PersonalTable.SWSH.IsPresentInGame(species, form)) - result = PreferredGroup.SWSH; - else if (PersonalTable.BDSP.IsPresentInGame(species, form)) - result = PreferredGroup.BDSP; - else - result = PreferredGroup.None; - return result != 0; - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private enum PreferredGroup - { - None, - LA, - SWSH, - BDSP, - } - - private static bool GetFirstEvolution(T pt, ReadOnlySpan chain, out EvoCriteria result) where T : IPersonalTable - { - foreach (var evo in chain) - { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) - continue; - - result = evo; - return true; - } - - result = default; - return false; - } -} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup9.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup9.cs deleted file mode 100644 index 3a3b47582..000000000 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup9.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; - -namespace PKHeX.Core; - -public sealed class EvolutionGroup9 : IEvolutionGroup -{ - public static readonly EvolutionGroup9 Instance = new(); - private static readonly EvolutionTree Tree9 = EvolutionTree.Evolves9; - private const int MaxSpecies = Legal.MaxSpeciesID_9; - - public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => null; - public IEvolutionGroup GetPrevious(PKM pk, EvolutionOrigin enc) => EvolutionGroup8.Instance; - - public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc) - { - if (chain.Length == 0) - return false; - - var sv = Append(pk, chain, enc, PersonalTable.SV, Tree9, ref history.Gen9); - - if (!sv) - return false; - - chain = history.Gen9; - return chain.Length != 0; - } - - private static bool Append(PKM pk, ReadOnlySpan chain, EvolutionOrigin enc, T pt, EvolutionTree tree, ref EvoCriteria[] dest) where T : IPersonalTable - { - // Get the first evolution in the chain that can be present in this group - var any = GetFirstEvolution(pt, chain, out var evo); - if (!any) - return false; - - // Get the evolution tree from this group and get the new chain from it. - var criteria = enc with { LevelMax = evo.LevelMax, LevelMin = (byte)pk.Met_Level }; - var local = GetInitialChain(pk, criteria, evo.Species, evo.Form, tree); - - // Revise the tree - var revised = Prune(local); - - // Set the tree to the history field - dest = revised; - - return revised.Length != 0; - } - - public EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form) - { - if (!GetPreferredGroup(species, form, out var group)) - return Array.Empty(); - var tree = GetTree(group); - return GetInitialChain(pk, enc, species, form, tree); - } - - private static EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form, EvolutionTree tree) - { - return tree.GetExplicitLineage(species, form, pk, enc.LevelMin, enc.LevelMax, MaxSpecies, enc.SkipChecks, enc.Species); - } - - private static EvolutionTree GetTree(PreferredGroup group) => group switch - { - _ => Tree9, - }; - - private static bool GetPreferredGroup(ushort species, byte form, out PreferredGroup result) - { - if (PersonalTable.SV.IsPresentInGame(species, form)) - result = PreferredGroup.SV; - else - result = PreferredGroup.None; - return result != 0; - } - - private static EvoCriteria[] Prune(EvoCriteria[] chain) => chain; - - private enum PreferredGroup - { - None, - SV, - } - - private static bool GetFirstEvolution(T pt, ReadOnlySpan chain, out EvoCriteria result) where T : IPersonalTable - { - foreach (var evo in chain) - { - // If the evo can't exist in the game, it must be a future evolution. - if (!pt.IsPresentInGame(evo.Species, evo.Form)) - continue; - - result = evo; - return true; - } - - result = default; - return false; - } -} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupHOME.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupHOME.cs new file mode 100644 index 000000000..d5469ed50 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupHOME.cs @@ -0,0 +1,258 @@ +using System; +using System.Runtime.CompilerServices; +using static PKHeX.Core.GameVersion; +using static PKHeX.Core.EvolutionUtil; + +namespace PKHeX.Core; + +public sealed class EvolutionGroupHOME : IEvolutionGroup +{ + public static readonly EvolutionGroupHOME Instance = new(); + + private static readonly EvolutionEnvironment8 SWSH = new(); + private static readonly EvolutionEnvironment8a LA = new(); + private static readonly EvolutionEnvironment8b BDSP = new(); + private static readonly EvolutionEnvironment9 SV = new(); + + public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => null; + + public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) + { + if (enc.Generation >= 8) + return null; + if ((GameVersion)enc.Version is GP or GE or GG or GO) + return EvolutionGroup7b.Instance; + return EvolutionGroup7.Instance; + } + + public void DiscardForOrigin(Span result, PKM pk) + { + if (pk.SV) + Discard(result, PersonalTable.SV); + else if (pk.LA) + Discard(result, PersonalTable.LA); + else if (pk.BDSP) + Discard(result, PersonalTable.BDSP); + else if (pk.SWSH) + Discard(result, PersonalTable.SWSH); + // GO can be otherwise, don't discard any. + } + + /// + /// Checks if we should check all adjacent evolution sources in addition to the current one. + /// + /// True if we should check all adjacent evolution sources. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CheckAllAdjacent(PKM pk, EvolutionOrigin enc) => enc.SkipChecks || pk is IHomeTrack { HasTracker: true } || !ParseSettings.IgnoreTransferIfNoTracker; + + public int Devolve(Span result, PKM pk, EvolutionOrigin enc) + { + if (CheckAllAdjacent(pk, enc)) + return DevolveMulti(result, pk, enc); + return DevolveSingle(result, pk, enc); + } + + public int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history) + { + if (CheckAllAdjacent(pk, enc)) + return EvolveMulti(result, pk, enc, history); + return EvolveSingle(result, pk, enc, history); + } + + private int DevolveMulti(Span result, PKM pk, in EvolutionOrigin enc) + { + int present = 1; + for (int i = 1; i < result.Length; i++) + { + ref var prev = ref result[i - 1]; + RevertMutatedForms(ref prev); + ref var reference = ref result[i]; + + bool devolvedAny = false; + if (SWSH.TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) + devolvedAny = UpdateIfBetter(ref reference, evo); + if (LA .TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out evo)) + devolvedAny = UpdateIfBetter(ref reference, evo); + if (BDSP.TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out evo)) + devolvedAny = UpdateIfBetter(ref reference, evo); + if (SV .TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out evo)) + devolvedAny = UpdateIfBetter(ref reference, evo); + + if (devolvedAny) + present++; + + static bool UpdateIfBetter(ref EvoCriteria reference, in EvoCriteria evo) + { + if (evo.IsBetterDevolution(reference)) + reference = evo; + return true; + } + } + + return present; + } + + private int EvolveMulti(Span result, PKM pk, in EvolutionOrigin enc, EvolutionHistory history) + { + int present = 1; + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + + bool devolvedAny = false; + if (SWSH.TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + devolvedAny = UpdateIfBetter(ref dest, evo); + if (LA .TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out evo)) + devolvedAny = UpdateIfBetter(ref dest, evo); + if (BDSP.TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out evo)) + devolvedAny = UpdateIfBetter(ref dest, evo); + if (SV .TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out evo)) + devolvedAny = UpdateIfBetter(ref dest, evo); + + if (devolvedAny) + present++; + else if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + + static bool UpdateIfBetter(ref EvoCriteria reference, in EvoCriteria evo) + { + if (evo.IsBetterEvolution(reference)) + reference = evo; + return true; + } + } + + history.Gen8 = SetHistory(result, PersonalTable.SWSH); + history.Gen8a = SetHistory(result, PersonalTable.LA); + history.Gen8b = SetHistory(result, PersonalTable.BDSP); + history.Gen9 = SetHistory(result, PersonalTable.SV); + + return present; + } + + private static int DevolveSingle(Span result, PKM pk, in EvolutionOrigin enc) + { + int present = 1; + var env = GetSingleEnv(pk); + for (int i = 1; i < result.Length; i++) + { + ref var prev = ref result[i - 1]; + RevertMutatedForms(ref prev); + if (!env.TryDevolve(prev, pk, prev.LevelMax, enc.LevelMin, enc.SkipChecks, out var evo)) + continue; + + ref var reference = ref result[i]; + if (evo.IsBetterDevolution(reference)) + reference = evo; + present++; + } + + return present; + } + + private static void RevertMutatedForms(ref EvoCriteria evo) + { + if (evo is { Species: (ushort)Species.Dialga, Form: not 0 }) + evo = evo with { Form = 0 }; // Normal + else if (evo is { Species: (ushort)Species.Palkia, Form: not 0 }) + evo = evo with { Form = 0 }; // Normal + else if (evo is { Species: (ushort)Species.Silvally, Form: not 0 }) + evo = evo with { Form = 0 }; // Normal + } + + private int EvolveSingle(Span result, PKM pk, in EvolutionOrigin enc, EvolutionHistory history) + { + int present = 1; + var env = GetSingleEnv(pk); + for (int i = result.Length - 1; i >= 1; i--) + { + ref var dest = ref result[i - 1]; + var devolved = result[i]; + if (!env.TryEvolve(devolved, dest, pk, enc.LevelMax, devolved.LevelMin, enc.SkipChecks, out var evo)) + { + if (dest.Method == EvoCriteria.SentinelNotReached) + break; // Don't continue for higher evolutions. + continue; + } + + if (evo.IsBetterEvolution(dest)) + dest = evo; + present++; + } + + if (pk is PK8) history.Gen8 = SetHistory(result, PersonalTable.SWSH); + else if (pk is PA8) history.Gen8a = SetHistory(result, PersonalTable.LA); + else if (pk is PB8) history.Gen8b = SetHistory(result, PersonalTable.BDSP); + else if (pk is PK9) history.Gen9 = SetHistory(result, PersonalTable.SV); + + return present; + } + + private static IEvolutionEnvironment GetSingleEnv(PKM pk) => pk switch + { + PK8 => SWSH, + PA8 => LA, + PB8 => BDSP, + PK9 => SV, + _ => throw new ArgumentOutOfRangeException(nameof(pk), pk, null), + }; +} + +public sealed class EvolutionEnvironment8 : IEvolutionEnvironment +{ + private static readonly EvolutionTree Tree = EvolutionTree.Evolves8; + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + return Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + var b = Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); + return b && !IsEvolutionBanned(pk, head); + } + + // Gigantamax Pikachu, Meowth, and Eevee are prevented from evolving. + private static bool IsEvolutionBanned(PKM pk, in ISpeciesForm head) => head.Species switch + { + (int)Species.Pikachu => pk is PK8 { CanGigantamax: true }, + (int)Species.Meowth => pk is PK8 { CanGigantamax: true }, + (int)Species.Eevee => pk is PK8 { CanGigantamax: true }, + _ => false, + }; +} + +public sealed class EvolutionEnvironment8a : IEvolutionEnvironment +{ + private static readonly EvolutionTree Tree = EvolutionTree.Evolves8a; + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + => Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + => Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); +} + +public sealed class EvolutionEnvironment8b : IEvolutionEnvironment +{ + private static readonly EvolutionTree Tree = EvolutionTree.Evolves8b; + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + => Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + => Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); +} + +public sealed class EvolutionEnvironment9 : IEvolutionEnvironment +{ + private static readonly EvolutionTree Tree = EvolutionTree.Evolves9; + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + => Tree.Reverse.TryDevolve(head, pk, currentMaxLevel, levelMin, skipChecks, out result); + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + => Tree.Forward.TryEvolve(head, next, pk, currentMaxLevel, levelMin, skipChecks, out result); +} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupUtil.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupUtil.cs index 278b6e0fa..529515e49 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupUtil.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroupUtil.cs @@ -1,4 +1,3 @@ -using System; using static PKHeX.Core.EntityContext; namespace PKHeX.Core; @@ -8,15 +7,10 @@ namespace PKHeX.Core; /// public static class EvolutionGroupUtil { - /// - /// Gets the for the . - /// - public static IEvolutionGroup GetCurrentGroup(PKM pk) => GetCurrentGroup(pk.Context); - /// /// Gets the for the . /// - public static IEvolutionGroup GetCurrentGroup(EntityContext context) => context switch + public static IEvolutionGroup GetGroup(EntityContext context) => context switch { Gen1 => EvolutionGroup1.Instance, Gen2 => EvolutionGroup2.Instance, @@ -25,13 +19,8 @@ public static class EvolutionGroupUtil Gen5 => EvolutionGroup5.Instance, Gen6 => EvolutionGroup6.Instance, Gen7 => EvolutionGroup7.Instance, - Gen8 => EvolutionGroup8.Instance, - Gen9 => EvolutionGroup9.Instance, - Gen7b => EvolutionGroup7b.Instance, - Gen8a => EvolutionGroup8.Instance, - Gen8b => EvolutionGroup8.Instance, - _ => throw new ArgumentOutOfRangeException(nameof(context), context, null), + _ => EvolutionGroupHOME.Instance, }; } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs new file mode 100644 index 000000000..521f3f4b3 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs @@ -0,0 +1,12 @@ +namespace PKHeX.Core; + +/// +/// Details about the original encounter. +/// +/// Species the encounter originated as +/// Version the encounter originated on +/// Generation the encounter originated in +/// Minimum level the encounter originated at +/// Maximum level in final state +/// Skip enforcement of legality for evolution criteria +public readonly record struct EvolutionOrigin(ushort Species, byte Version, byte Generation, byte LevelMin, byte LevelMax, bool SkipChecks = false); diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionUtil.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionUtil.cs new file mode 100644 index 000000000..dddcb2e1a --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionUtil.cs @@ -0,0 +1,172 @@ +using System; + +namespace PKHeX.Core; + +internal static class EvolutionUtil +{ + public static EvoCriteria[] SetHistory(Span result, T pt) where T : IPersonalTable + { + // Get the range of {result} that is present within pt. + int start = 0; + int count = 0; + + // Find first index that exists in the game, and take while they do. + foreach (ref readonly var evo in result) + { + if (evo.Method == EvoCriteria.SentinelNotReached) + { + // Unable to evolve to this stage, skip everything before it + start += count + 1; + count = 0; + } + else if (pt.IsPresentInGame(evo.Species, evo.Form)) + { + // Found a valid entry, increment count. + count++; + } + else if (count == 0) + { + // No valid entries found yet, increment start. + start++; + } + else + { + // Found an invalid entry, stop. + break; + } + } + + return GetLocalEvolutionArray(result.Slice(start, count)); + } + + private static EvoCriteria[] GetLocalEvolutionArray(Span result) + { + if (result.Length == 0) + return Array.Empty(); + + var array = result.ToArray(); + var length = CleanEvolve(array); + if (length != array.Length) + Array.Resize(ref array, length); + return array; + } + + public static void Discard(Span result, T pt) where T : IPersonalTable + { + // Iterate through the result, and if an entry is not present in the game, shift other entries down and zero out the last entry. + for (int i = 0; i < result.Length; i++) + { + var evo = result[i]; + if (evo.Species == 0) + break; + if (pt.IsPresentInGame(evo.Species, evo.Form)) + continue; + + ShiftDown(result[i..]); + } + } + + private static void ShiftDown(Span result) + { + for (int i = 1; i < result.Length; i++) + result[i - 1] = result[i]; + result[^1] = default; // zero out the last entry + } + + public static int IndexOf(Span result, ushort species) + { + for (int i = 0; i < result.Length; i++) + { + if (result[i].Species == species) + return i; + } + return -1; + } + + public static bool Contains(ReadOnlySpan result, ushort species) + { + foreach (ref readonly var z in result) + { + if (z.Species == species) + return true; + } + return false; + } + + /// + /// Revises the to account for a new maximum . + /// + public static void UpdateCeiling(Span result, int level) + { + foreach (ref var evo in result) + { + if (evo.Species == 0) + break; + evo = evo with { LevelMax = (byte)Math.Min(evo.LevelMax, level) }; + } + } + + /// + /// Revises the to account for a new minimum . + /// + public static void UpdateFloor(Span result, int level) + { + foreach (ref var evo in result) + { + if (evo.Species == 0) + break; + evo = evo with { LevelMin = (byte)Math.Max(evo.LevelMin, level) }; + } + } + + /// + /// Mutates the result to leave placeholder data for the species that the evolves into. + /// + /// All evolutions for the species. + /// Species encountered as. + public static void ConditionBaseChainForward(Span result, ushort encSpecies) + { + foreach (ref var evo in result) + { + if (evo.Species == encSpecies) + break; + evo = evo with { Method = EvoCriteria.SentinelNotReached }; + } + } + + public static void CleanDevolve(Span result, byte levelMin) + { + // Rectify minimum levels. + // trickle our two temp variables up the chain (essentially evolving from min). + byte req = 0; + EvolutionType method = EvolutionType.None; + for (int i = result.Length - 1; i >= 0; i--) + { + ref var evo = ref result[i]; + var nextMin = evo.LevelMin; // to evolve + var nextReq = evo.LevelUpRequired; + var nextMethod = evo.Method; + evo = evo with { LevelMin = Math.Min(evo.LevelMax, levelMin), LevelUpRequired = req, Method = method }; + levelMin = Math.Max(nextMin, levelMin); + req = nextReq; + method = nextMethod; + } + } + + private static int CleanEvolve(Span result) + { + // Rectify maximum levels. + int i = 1; + for (; i < result.Length; i++) + { + var next = result[i - 1]; + // Ignore LevelUp byte as it can learn moves prior to evolving. + var newMax = next.LevelMax; + ref var evo = ref result[i]; + if (evo.LevelMin > newMax - next.LevelUpRequired) + break; + evo = evo with { LevelMax = newMax }; + } + return i; + } +} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/IEvolutionGroup.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/IEvolutionGroup.cs index c5f571750..2c72ef6d6 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/IEvolutionGroup.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/IEvolutionGroup.cs @@ -17,18 +17,45 @@ public interface IEvolutionGroup /// IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc); - bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan chain, EvolutionOrigin enc); + /// + /// Walks down the evolution chain to find previous evolutions. + /// + /// Array to store results in. + /// PKM to check. + /// Cached metadata about the PKM. + /// Number of results found. + int Devolve(Span result, PKM pk, EvolutionOrigin enc); - EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form); + /// + /// Walks up the evolution chain to find the evolution path. + /// + /// Array to store best results in. + /// PKM to check. + /// Cached metadata about the PKM. + /// History of evolutions to cache arrays for individual contexts. + /// Number of results found. + int Evolve(Span result, PKM pk, EvolutionOrigin enc, EvolutionHistory history); + + /// + /// Discards all entries that do not exist in the group. + /// + void DiscardForOrigin(Span result, PKM pk); } /// -/// Details about the original encounter. +/// Provides information about how to evolve to the next evolution stage. /// -/// Species the encounter originated as -/// Version the encounter originated on -/// Generation the encounter originated in -/// Minimum level the encounter originated at -/// Maximum level in final state -/// Skip enforcement of legality for evolution criteria -public readonly record struct EvolutionOrigin(ushort Species, byte Version, byte Generation, byte LevelMin, byte LevelMax, bool SkipChecks = false); +public interface IEvolutionEnvironment +{ + /// + /// Attempts to devolve the provided to the previous evolution. + /// + /// True if the de-evolution was successful and should be used. + bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result); + + /// + /// Attempts to evolve the provided to the next evolution. + /// + /// True if the evolution was successful and should be used. + bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result); +} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionHistory.cs b/PKHeX.Core/Legality/Evolutions/EvolutionHistory.cs index 6fad85b55..9a699ec30 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionHistory.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionHistory.cs @@ -37,28 +37,26 @@ public sealed class EvolutionHistory public bool HasVisitedPLA => Gen8a.Length != 0; public bool HasVisitedBDSP => Gen8b.Length != 0; - public ref EvoCriteria[] Get(EntityContext context) + public ReadOnlySpan Get(EntityContext context) => context switch { - if (context == EntityContext.Gen1) return ref Gen1; - if (context == EntityContext.Gen2) return ref Gen2; - if (context == EntityContext.Gen3) return ref Gen3; - if (context == EntityContext.Gen4) return ref Gen4; - if (context == EntityContext.Gen5) return ref Gen5; - if (context == EntityContext.Gen6) return ref Gen6; - if (context == EntityContext.Gen7) return ref Gen7; - if (context == EntityContext.Gen8) return ref Gen8; - if (context == EntityContext.Gen9) return ref Gen9; + EntityContext.Gen1 => Gen1, + EntityContext.Gen2 => Gen2, + EntityContext.Gen3 => Gen3, + EntityContext.Gen4 => Gen4, + EntityContext.Gen5 => Gen5, + EntityContext.Gen6 => Gen6, + EntityContext.Gen7 => Gen7, + EntityContext.Gen8 => Gen8, + EntityContext.Gen9 => Gen9, - if (context == EntityContext.Gen7b) return ref Gen7b; - if (context == EntityContext.Gen8a) return ref Gen8a; - if (context == EntityContext.Gen8b) return ref Gen8b; + EntityContext.Gen7b => Gen7b, + EntityContext.Gen8a => Gen8a, + EntityContext.Gen8b => Gen8b, + _ => throw new ArgumentOutOfRangeException(nameof(context), context, null), + }; - throw new ArgumentOutOfRangeException(nameof(context)); - } - - public bool HasVisited(EntityContext context, ushort species) + public static bool HasVisited(ReadOnlySpan evos, ushort species) { - var evos = Get(context); foreach (var evo in evos) { if (evo.Species == species) @@ -66,10 +64,4 @@ public bool HasVisited(EntityContext context, ushort species) } return false; } - - public void Set(EntityContext context, EvoCriteria[] chain) - { - ref var arr = ref Get(context); - arr = chain; - } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionLegality.cs b/PKHeX.Core/Legality/Evolutions/EvolutionLegality.cs deleted file mode 100644 index e98904860..000000000 --- a/PKHeX.Core/Legality/Evolutions/EvolutionLegality.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; - -namespace PKHeX.Core; - -internal static class EvolutionLegality -{ - internal static readonly HashSet FutureEvolutionsGen1 = new() - { - (int)Species.Crobat, - (int)Species.Bellossom, - (int)Species.Politoed, - (int)Species.Espeon, - (int)Species.Umbreon, - (int)Species.Slowking, - (int)Species.Steelix, - (int)Species.Scizor, - (int)Species.Kingdra, - (int)Species.Porygon2, - (int)Species.Blissey, - - (int)Species.Magnezone, - (int)Species.Lickilicky, - (int)Species.Rhyperior, - (int)Species.Tangrowth, - (int)Species.Electivire, - (int)Species.Magmortar, - (int)Species.Leafeon, - (int)Species.Glaceon, - (int)Species.PorygonZ, - - (int)Species.Sylveon, - - (int)Species.Kleavor, - }; -} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionReversal.cs b/PKHeX.Core/Legality/Evolutions/EvolutionReversal.cs deleted file mode 100644 index 769672092..000000000 --- a/PKHeX.Core/Legality/Evolutions/EvolutionReversal.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; - -namespace PKHeX.Core; - -/// -/// Evolution Reversal logic -/// -public static class EvolutionReversal -{ - /// - /// Reverses from current state to see what evolutions the may have existed as. - /// - /// Evolution Method lineage reversal object - /// Species to devolve from - /// Form to devolve from - /// Entity reference to sanity check evolutions with - /// Minimum level the entity may exist as - /// Maximum the entity may exist as - /// Maximum species ID that may exist - /// Option to bypass some evolution criteria - /// Species ID that should be the last node, if at all. - /// Reversed evolution lineage, with the lowest index being the current species-form. - public static EvoCriteria[] Reverse(this IEvolutionLookup lineage, ushort species, byte form, PKM pk, byte levelMin, byte levelMax, int maxSpeciesID, bool skipChecks, int stopSpecies) - { - // Sometimes we have to sanitize the inputs. - switch (species) - { - case (int)Species.Silvally: - form = 0; - break; - } - - // Store our results -- trim at the end when we place it on the heap. - const int maxEvolutions = 3; - Span evos = stackalloc EvoCriteria[maxEvolutions]; - - var lvl = levelMax; // highest level, any level-up method will decrease. - evos[0] = new EvoCriteria { Species = species, Form = form, LevelMax = lvl }; // current species-form - - // There aren't any circular evolution paths, and all lineages have at most 3 evolutions total. - // There aren't any convergent evolution paths, so only yield the first connection. - int ctr = 1; // count in the 'evos' span - while (species != stopSpecies) - { - var key = EvolutionTree.GetLookupKey(species, form); - var node = lineage[key]; - - bool oneValid = false; - for (int i = 0; i < 2; i++) - { - ref var link = ref i == 0 ? ref node.First : ref node.Second; - if (link.IsEmpty) - break; - - if (link.IsEvolutionBanned(pk) && !skipChecks) - continue; - - var evo = link.Method; - if (!evo.Valid(pk, lvl, skipChecks)) - continue; - - if (evo.RequiresLevelUp && levelMin >= lvl) - break; // impossible evolution - - UpdateMinValues(evos[..ctr], evo, levelMin); - - species = link.Species; - form = link.Form; - evos[ctr++] = evo.GetEvoCriteria(species, form, lvl); - if (evo.RequiresLevelUp) - lvl--; - - oneValid = true; - break; - } - if (!oneValid) - break; - } - - // Remove future gen pre-evolutions; no Munchlax from a Gen3 Snorlax, no Pichu from a Gen1-only Raichu, etc - ref var last = ref evos[ctr - 1]; - if (last.Species > maxSpeciesID) - { - for (int i = 0; i < ctr; i++) - { - if (evos[i].Species > maxSpeciesID) - continue; - ctr--; - break; - } - } - - // Last species is the wild/hatched species, the minimum level is because it has not evolved from previous species - var result = evos[..ctr]; - last = ref result[^1]; - last = last with { LevelMin = levelMin, LevelUpRequired = 0 }; - - // Rectify minimum levels - RectifyMinimumLevels(result); - - return result.ToArray(); - } - - private static void RectifyMinimumLevels(Span result) - { - for (int i = result.Length - 2; i >= 0; i--) - { - ref var evo = ref result[i]; - var prev = result[i + 1]; - var min = (byte)Math.Max(prev.LevelMin + evo.LevelUpRequired, evo.LevelMin); - evo = evo with { LevelMin = min }; - } - } - - private static void UpdateMinValues(Span evos, EvolutionMethod evo, byte minLevel) - { - ref var last = ref evos[^1]; - if (!evo.RequiresLevelUp) - { - // Evolutions like elemental stones, trade, etc - last = last with { LevelMin = minLevel }; - return; - } - if (evo.Level == 0) - { - // Friendship based Level Up Evolutions, Pichu -> Pikachu, Eevee -> Umbreon, etc - last = last with { LevelMin = (byte)(minLevel + 1) }; - - // Raichu from Pikachu would have a minimum level of 1; accounting for Pichu (level up required) results in a minimum level of 2 - if (evos.Length > 1) - { - ref var first = ref evos[0]; - if (!first.RequiresLvlUp) - first = first with { LevelMin = (byte)(minLevel + 1) }; - } - } - else // level up evolutions - { - last = last with { LevelMin = evo.Level }; - - if (evos.Length > 1) - { - ref var first = ref evos[0]; - if (first.RequiresLvlUp) - { - // Pokemon like Crobat, its minimum level is Golbat minimum level + 1 - if (first.LevelMin <= evo.Level) - first = first with { LevelMin = (byte)(evo.Level + 1) }; - } - else - { - // Pokemon like Nidoqueen who evolve with an evolution stone, minimum level is prior evolution minimum level - if (first.LevelMin < evo.Level) - first = first with { LevelMin = evo.Level }; - } - } - } - last = last with { LevelUpRequired = evo.RequiresLevelUp ? (byte)1 : (byte)0 }; - } -} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet1.cs b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet1.cs index 0e09159fa..35f4a134d 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet1.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet1.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace PKHeX.Core; @@ -19,28 +18,24 @@ private static EvolutionMethod GetMethod(ReadOnlySpan data) : new EvolutionMethod((EvolutionType)method, species, Argument: arg); } - public static IReadOnlyList GetArray(ReadOnlySpan data, int maxSpecies) + public static EvolutionMethod[][] GetArray(ReadOnlySpan data, int maxSpecies) { - var evos = new EvolutionMethod[maxSpecies + 1][]; - int ofs = 0; + var result = new EvolutionMethod[maxSpecies + 1][]; + for (int i = 0, offset = 0; i < result.Length; i++) + result[i] = GetEntry(data, ref offset); + return result; + } + + private static EvolutionMethod[] GetEntry(ReadOnlySpan data, ref int offset) + { + var count = data[offset++]; + if (count == 0) + return Array.Empty(); + const int bpe = 3; - for (int i = 0; i < evos.Length; i++) - { - int count = data[ofs]; - ofs++; - if (count == 0) - { - evos[i] = Array.Empty(); - continue; - } - var m = new EvolutionMethod[count]; - for (int j = 0; j < m.Length; j++) - { - m[j] = GetMethod(data.Slice(ofs, bpe)); - ofs += bpe; - } - evos[i] = m; - } - return evos; + var result = new EvolutionMethod[count]; + for (int i = 0; i < result.Length; i++, offset += bpe) + result[i] = GetMethod(data.Slice(offset, bpe)); + return result; } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet3.cs b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet3.cs index aa1cc1586..0744cba3f 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet3.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet3.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -45,37 +44,43 @@ private static EvolutionMethod GetMethod(ReadOnlySpan data) } } - public static IReadOnlyList GetArray(ReadOnlySpan data) + public static EvolutionMethod[][] GetArray(ReadOnlySpan data) { - var evos = new EvolutionMethod[Legal.MaxSpeciesID_3 + 1][]; - evos[0] = Array.Empty(); + var result = new EvolutionMethod[Legal.MaxSpeciesID_3 + 1][]; + result[0] = Array.Empty(); for (ushort i = 1; i <= Legal.MaxSpeciesIndex_3; i++) { int g4species = SpeciesConverter.GetNational3(i); - if (g4species == 0) - continue; - - const int maxCount = 5; - const int size = 8; - - int offset = i * (maxCount * size); - int count = 0; - for (; count < maxCount; count++) - { - if (data[offset + (count * size)] == 0) - break; - } - if (count == 0) - { - evos[g4species] = Array.Empty(); - continue; - } - - var set = new EvolutionMethod[count]; - for (int j = 0; j < set.Length; j++) - set[j] = GetMethod(data.Slice(offset + (j * size), size)); - evos[g4species] = set; + if (g4species != 0) + result[g4species] = GetEntry(data, i); } - return evos; + return result; + } + + private const int maxCount = 5; + private const int size = 8; + + private static EvolutionMethod[] GetEntry(ReadOnlySpan data, ushort index) + { + var span = data.Slice(index * (maxCount * size), maxCount * size); + int count = ScanCountEvolutions(span); + + if (count == 0) + return Array.Empty(); + + var result = new EvolutionMethod[count]; + for (int i = 0, offset = 0; i < result.Length; i++, offset += size) + result[i] = GetMethod(span.Slice(offset, size)); + return result; + } + + private static int ScanCountEvolutions(ReadOnlySpan span) + { + for (int count = 0; count < maxCount; count++) + { + if (span[count * size] == 0) + return count; + } + return maxCount; } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet4.cs b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet4.cs index 6f6c31953..898a4389f 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet4.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet4.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -29,35 +28,39 @@ private static EvolutionMethod GetMethod(ReadOnlySpan data) return new EvolutionMethod(type, species, Argument: arg, Level: (byte)lvl, LevelUp: lvlup); } - public static IReadOnlyList GetArray(ReadOnlySpan data) + private const int bpe = 6; // bytes per evolution entry + private const int entries = 7; // amount of entries per species + private const int size = (entries * bpe) + 2; // bytes per species entry, + 2 alignment bytes + + public static EvolutionMethod[][] GetArray(ReadOnlySpan data) { - const int bpe = 6; // bytes per evolution entry - const int entries = 7; // amount of entries per species - const int size = (entries * bpe) + 2; // bytes per species entry, + 2 alignment bytes + var result = new EvolutionMethod[data.Length / size][]; + for (int i = 0, offset = 0; i < result.Length; i++, offset += size) + result[i] = GetEntry(data.Slice(offset, size)); + return result; + } - var evos = new EvolutionMethod[data.Length / size][]; - for (int i = 0; i < evos.Length; i++) + private static EvolutionMethod[] GetEntry(ReadOnlySpan data) + { + int count = ScanCountEvolutions(data); + if (count == 0) + return Array.Empty(); + + var result = new EvolutionMethod[count]; + for (int i = 0, offset = 0; i < result.Length; i++, offset += bpe) + result[i] = GetMethod(data.Slice(offset, bpe)); + return result; + } + + private static int ScanCountEvolutions(ReadOnlySpan data) + { + for (int count = 0; count < entries; count++) { - int offset = i * size; - int count = 0; - for (; count < entries; count++) - { - var methodOffset = offset + (count * bpe); - var method = data[methodOffset]; - if (method == 0) - break; - } - if (count == 0) - { - evos[i] = Array.Empty(); - continue; - } - - var set = new EvolutionMethod[count]; - for (int j = 0; j < set.Length; j++) - set[j] = GetMethod(data.Slice(offset + (j * bpe), bpe)); - evos[i] = set; + var methodOffset = count * bpe; + var method = data[methodOffset]; + if (method == 0) + return count; } - return evos; + return entries; } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet5.cs b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet5.cs index 51ab04809..6235f32b7 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet5.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet5.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -28,26 +27,24 @@ private static EvolutionMethod GetMethod(ReadOnlySpan data) private const int entries = 7; // amount of entries per species private const int size = entries * bpe; // bytes per species entry - public static IReadOnlyList GetArray(ReadOnlySpan data) + public static EvolutionMethod[][] GetArray(ReadOnlySpan data) { - var evos = new EvolutionMethod[data.Length / size][]; - for (int i = 0; i < evos.Length; i++) - { - int offset = i * size; - var rawEntries = data.Slice(offset, size); - var count = ScanCountEvolutions(rawEntries); - if (count == 0) - { - evos[i] = Array.Empty(); - continue; - } + var result = new EvolutionMethod[data.Length / size][]; + for (int i = 0, offset = 0; i < result.Length; i++, offset += size) + result[i] = GetEntry(data.Slice(offset, size)); + return result; + } - var set = new EvolutionMethod[count]; - for (int j = 0; j < set.Length; j++) - set[j] = GetMethod(rawEntries.Slice(j * bpe, bpe)); - evos[i] = set; - } - return evos; + private static EvolutionMethod[] GetEntry(ReadOnlySpan data) + { + var count = ScanCountEvolutions(data); + if (count == 0) + return Array.Empty(); + + var result = new EvolutionMethod[count]; + for (int i = 0, offset = 0; i < result.Length; i++, offset += bpe) + result[i] = GetMethod(data.Slice(offset, bpe)); + return result; } private static int ScanCountEvolutions(ReadOnlySpan data) diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet6.cs b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet6.cs index b014d332c..a9ccef079 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet6.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet6.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -13,15 +12,20 @@ public static class EvolutionSet6 internal static bool IsMethodWithArg(byte method) => (0b100000011111110000000101000000 & (1 << method)) != 0; private const int SIZE = 6; - private static EvolutionMethod[] GetMethods(ReadOnlySpan data) + public static EvolutionMethod[][] GetArray(BinLinkerAccessor data) { - var evos = new EvolutionMethod[data.Length / SIZE]; - for (int i = 0; i < data.Length; i += SIZE) - { - var entry = data.Slice(i, SIZE); - evos[i/SIZE] = GetMethod(entry); - } - return evos; + var result = new EvolutionMethod[data.Length][]; + for (int i = 0; i < result.Length; i++) + result[i] = GetEntry(data[i]); + return result; + } + + private static EvolutionMethod[] GetEntry(ReadOnlySpan data) + { + var result = new EvolutionMethod[data.Length / SIZE]; + for (int i = 0, offset = 0; i < result.Length; i++, offset += SIZE) + result[i] = GetMethod(data.Slice(offset, SIZE)); + return result; } private static EvolutionMethod GetMethod(ReadOnlySpan entry) @@ -36,12 +40,4 @@ private static EvolutionMethod GetMethod(ReadOnlySpan entry) var lvlup = type.IsLevelUpRequired() ? (byte)1 : (byte)0; return new EvolutionMethod(type, species, Argument: arg, Level: (byte)lvl, LevelUp: lvlup); } - - public static IReadOnlyList GetArray(BinLinkerAccessor data) - { - var evos = new EvolutionMethod[data.Length][]; - for (int i = 0; i < evos.Length; i++) - evos[i] = GetMethods(data[i]); - return evos; - } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet7.cs b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet7.cs index 8fcef6274..9cad92a7e 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet7.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionSets/EvolutionSet7.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -11,40 +10,33 @@ public static class EvolutionSet7 { private const int SIZE = 8; - private static EvolutionMethod[] GetMethods(ReadOnlySpan data, bool LevelUpBypass) + public static EvolutionMethod[][] GetArray(BinLinkerAccessor data, byte levelUp = 1) + { + var result = new EvolutionMethod[data.Length][]; + for (int i = 0; i < result.Length; i++) + result[i] = GetEntry(data[i], levelUp); + return result; + } + + private static EvolutionMethod[] GetEntry(ReadOnlySpan data, byte levelUp) { if (data.Length == 0) return Array.Empty(); var result = new EvolutionMethod[data.Length / SIZE]; - int i = 0, offset = 0; - while (true) - { - var entry = data.Slice(offset, SIZE); - result[i] = ReadEvolution(entry, LevelUpBypass); - offset += SIZE; - if (offset >= data.Length) - return result; - i++; - } + for (int i = 0, offset = 0; i < result.Length; i++, offset += SIZE) + result[i] = GetMethod(data.Slice(offset, SIZE), levelUp); + return result; } - private static EvolutionMethod ReadEvolution(ReadOnlySpan entry, bool levelUpBypass) + private static EvolutionMethod GetMethod(ReadOnlySpan entry, byte levelUp) { var type = (EvolutionType)entry[0]; var arg = ReadUInt16LittleEndian(entry[2..]); var species = ReadUInt16LittleEndian(entry[4..]); var form = entry[6]; var level = entry[7]; - var lvlup = !levelUpBypass && type.IsLevelUpRequired() ? (byte)1 : (byte)0; + var lvlup = type.IsLevelUpRequired() ? levelUp : (byte)0; return new EvolutionMethod(type, species, form, arg, level, lvlup); } - - public static IReadOnlyList GetArray(BinLinkerAccessor data, bool LevelUpBypass) - { - var evos = new EvolutionMethod[data.Length][]; - for (int i = 0; i < evos.Length; i++) - evos[i] = GetMethods(data[i], LevelUpBypass); - return evos; - } } diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs b/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs index 0f18d3ec8..03165b08d 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionTree.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using static PKHeX.Core.GameVersion; -using static PKHeX.Core.Legal; namespace PKHeX.Core; @@ -11,30 +8,38 @@ namespace PKHeX.Core; /// /// Used to determine if a can evolve from prior steps in its evolution branch. /// -public sealed class EvolutionTree +public sealed class EvolutionTree : EvolutionNetwork { - public static readonly EvolutionTree Evolves1 = new(GetResource("rby"), Gen1, PersonalTable.Y, MaxSpeciesID_1); - public static readonly EvolutionTree Evolves2 = new(GetResource("gsc"), Gen2, PersonalTable.C, MaxSpeciesID_2); - public static readonly EvolutionTree Evolves3 = new(GetResource("g3"), Gen3, PersonalTable.RS, MaxSpeciesID_3); - public static readonly EvolutionTree Evolves4 = new(GetResource("g4"), Gen4, PersonalTable.DP, MaxSpeciesID_4); - public static readonly EvolutionTree Evolves5 = new(GetResource("g5"), Gen5, PersonalTable.BW, MaxSpeciesID_5); - public static readonly EvolutionTree Evolves6 = new(GetReader("ao"), Gen6, PersonalTable.AO, MaxSpeciesID_6); - public static readonly EvolutionTree Evolves7 = new(GetReader("uu"), Gen7, PersonalTable.USUM, MaxSpeciesID_7_USUM); - public static readonly EvolutionTree Evolves7b = new(GetReader("gg"), Gen7, PersonalTable.GG, MaxSpeciesID_7b); - public static readonly EvolutionTree Evolves8 = new(GetReader("ss"), Gen8, PersonalTable.SWSH, MaxSpeciesID_8); - public static readonly EvolutionTree Evolves8a = new(GetReader("la"), PLA, PersonalTable.LA, MaxSpeciesID_8a); - public static readonly EvolutionTree Evolves8b = new(GetReader("bs"), BDSP, PersonalTable.BDSP, MaxSpeciesID_8b); - public static readonly EvolutionTree Evolves9 = new(GetReader("sv"), Gen9, PersonalTable.SV, MaxSpeciesID_9); + public const int MaxEvolutions = 3; + public static readonly EvolutionTree Evolves1 = GetViaSpecies (PersonalTable.Y, EvolutionSet1.GetArray(GetResource("rby"), 151)); + public static readonly EvolutionTree Evolves2 = GetViaSpecies (PersonalTable.C, EvolutionSet1.GetArray(GetResource("gsc"), 251)); + public static readonly EvolutionTree Evolves3 = GetViaSpecies (PersonalTable.RS, EvolutionSet3.GetArray(GetResource("g3"))); + public static readonly EvolutionTree Evolves4 = GetViaSpecies (PersonalTable.DP, EvolutionSet4.GetArray(GetResource("g4"))); + public static readonly EvolutionTree Evolves5 = GetViaSpecies (PersonalTable.BW, EvolutionSet5.GetArray(GetResource("g5"))); + public static readonly EvolutionTree Evolves6 = GetViaSpecies (PersonalTable.AO, EvolutionSet6.GetArray(GetReader("ao"))); + public static readonly EvolutionTree Evolves7 = GetViaPersonal(PersonalTable.USUM, EvolutionSet7.GetArray(GetReader("uu"))); + public static readonly EvolutionTree Evolves7b = GetViaPersonal(PersonalTable.GG, EvolutionSet7.GetArray(GetReader("gg"))); + public static readonly EvolutionTree Evolves8 = GetViaPersonal(PersonalTable.SWSH, EvolutionSet7.GetArray(GetReader("ss"))); + public static readonly EvolutionTree Evolves8a = GetViaPersonal(PersonalTable.LA, EvolutionSet7.GetArray(GetReader("la"), 0)); + public static readonly EvolutionTree Evolves8b = GetViaPersonal(PersonalTable.BDSP, EvolutionSet7.GetArray(GetReader("bs"))); + public static readonly EvolutionTree Evolves9 = GetViaPersonal(PersonalTable.SV, EvolutionSet7.GetArray(GetReader("sv"))); private static ReadOnlySpan GetResource(string resource) => Util.GetBinaryResource($"evos_{resource}.pkl"); private static BinLinkerAccessor GetReader(string resource) => BinLinkerAccessor.Get(GetResource(resource), resource); + private EvolutionTree(IEvolutionForward forward, IEvolutionReverse reverse) : base(forward, reverse) { } - static EvolutionTree() + private static EvolutionTree GetViaSpecies(IPersonalTable t, EvolutionMethod[][] entries) { - // Add in banned evolution data! - Evolves7.FixEvoTreeSM(); - Evolves8.FixEvoTreeSS(); - Evolves8b.FixEvoTreeBS(); + var forward = new EvolutionForwardSpecies(entries); + var reverse = new EvolutionReverseSpecies(entries, t); + return new EvolutionTree(forward, reverse); + } + + private static EvolutionTree GetViaPersonal(IPersonalTable t, EvolutionMethod[][] entries) + { + var forward = new EvolutionForwardPersonal(entries, t); + var reverse = new EvolutionReversePersonal(entries, t); + return new EvolutionTree(forward, reverse); } public static EvolutionTree GetEvolutionTree(EntityContext context) => context switch @@ -53,297 +58,4 @@ static EvolutionTree() EntityContext.Gen8b => Evolves8b, _ => throw new ArgumentOutOfRangeException(nameof(context), context, null), }; - - private readonly IReadOnlyList Entries; - private readonly IPersonalTable Personal; - private readonly int MaxSpeciesTree; - private readonly EvolutionReverseLookup Lineage; - private bool FormSeparateEvoData => MaxSpeciesTree > MaxSpeciesID_6; - - internal static int GetLookupKey(ushort species, byte form) => species | (form << 11); - - #region Constructor - - private EvolutionTree(ReadOnlySpan data, GameVersion game, IPersonalTable personal, int maxSpeciesTree) - { - Personal = personal; - MaxSpeciesTree = maxSpeciesTree; - Entries = GetEntries(data, game); - var connections = CreateTreeOld(); - Lineage = new(connections, maxSpeciesTree); - } - - private EvolutionTree(BinLinkerAccessor data, GameVersion game, IPersonalTable personal, int maxSpeciesTree) - { - Personal = personal; - MaxSpeciesTree = maxSpeciesTree; - Entries = GetEntries(data, game); - - // Starting in Generation 7, forms have separate evolution data. - var oldStyle = !FormSeparateEvoData; - var connections = oldStyle ? CreateTreeOld() : CreateTree(); - Lineage = new(connections, maxSpeciesTree); - } - - private IEnumerable<(int Key, EvolutionLink Value)> CreateTreeOld() - { - for (ushort sSpecies = 1; sSpecies <= MaxSpeciesTree; sSpecies++) - { - var fc = Personal[sSpecies].FormCount; - for (byte sForm = 0; sForm < fc; sForm++) - { - var index = sSpecies; - var evos = Entries[index]; - foreach (var evo in evos) - { - var dSpecies = evo.Species; - if (dSpecies == 0) - continue; - - var dForm = sSpecies == (int)Species.Espurr && evo.Method == EvolutionType.LevelUpFormFemale1 ? (byte)1 : sForm; - var key = GetLookupKey(dSpecies, dForm); - - var link = new EvolutionLink(sSpecies, sForm, evo); - yield return (key, link); - } - } - } - } - - private IEnumerable<(int Key, EvolutionLink Value)> CreateTree() - { - for (ushort sSpecies = 1; sSpecies <= MaxSpeciesTree; sSpecies++) - { - var fc = Personal[sSpecies].FormCount; - for (byte sForm = 0; sForm < fc; sForm++) - { - var index = Personal.GetFormIndex(sSpecies, sForm); - var evos = Entries[index]; - foreach (var evo in evos) - { - var dSpecies = evo.Species; - if (dSpecies == 0) - break; - - var dForm = evo.GetDestinationForm(sForm); - var key = GetLookupKey(dSpecies, dForm); - - var link = new EvolutionLink(sSpecies, sForm, evo); - yield return (key, link); - } - } - } - } - - private static IReadOnlyList GetEntries(ReadOnlySpan data, GameVersion game) => game switch - { - Gen1 => EvolutionSet1.GetArray(data, 151), - Gen2 => EvolutionSet1.GetArray(data, 251), - Gen3 => EvolutionSet3.GetArray(data), - Gen4 => EvolutionSet4.GetArray(data), - Gen5 => EvolutionSet5.GetArray(data), - _ => throw new ArgumentOutOfRangeException(nameof(game)), - }; - - private static IReadOnlyList GetEntries(BinLinkerAccessor data, GameVersion game) => game switch - { - Gen6 => EvolutionSet6.GetArray(data), - Gen7 or Gen8 or BDSP or Gen9 => EvolutionSet7.GetArray(data, false), - PLA => EvolutionSet7.GetArray(data, true), - _ => throw new ArgumentOutOfRangeException(nameof(game)), - }; - - private void FixEvoTreeSM() - { - // Sun/Moon lack Ultra's Kantonian evolution methods. - static bool BanOnlySM(PKM pk) => pk is { IsUntraded: true, SM: true }; - BanEvo((int)Species.Raichu, 0, BanOnlySM); - BanEvo((int)Species.Marowak, 0, BanOnlySM); - BanEvo((int)Species.Exeggutor, 0, BanOnlySM); - } - - private void FixEvoTreeSS() - { - // Gigantamax Pikachu, Meowth-0, and Eevee are prevented from evolving. - // Raichu cannot be evolved to the Alolan variant at this time. - static bool BanGmax(PKM pk) => pk is IGigantamax { CanGigantamax: true }; - BanEvo((int)Species.Raichu, 0, BanGmax); - BanEvo((int)Species.Raichu, 1, pk => (pk is IGigantamax {CanGigantamax: true}) || pk.Version is (int)GO or >= (int)GP); - BanEvo((int)Species.Persian, 0, BanGmax); - BanEvo((int)Species.Persian, 1, BanGmax); - BanEvo((int)Species.Perrserker, 0, BanGmax); - - BanEvo((int)Species.Exeggutor, 1, pk => pk.Version is (int)GO or >= (int)GP); - BanEvo((int)Species.Marowak, 1, pk => pk.Version is (int)GO or >= (int)GP); - BanEvo((int)Species.Weezing, 0, pk => pk.Version >= (int)SW); - BanEvo((int)Species.MrMime, 0, pk => pk.Version >= (int)SW); - - foreach (var s in GetEvolutions((int)Species.Eevee, 0)) // Eeveelutions - BanEvo(s.Species, s.Form, BanGmax); - } - - private void FixEvoTreeBS() - { - BanEvo((int)Species.Glaceon, 0, pk => pk.CurrentLevel == pk.Met_Level); // Ice Stone is unreleased, requires Route 217 Ice Rock Level Up instead - BanEvo((int)Species.Milotic, 0, pk => pk is IContestStatsReadOnly { CNT_Beauty: < 170 } || pk.CurrentLevel == pk.Met_Level); // Prism Scale is unreleased, requires 170 Beauty Level Up instead - } - - private void BanEvo(ushort species, byte form, Func func) - { - var key = GetLookupKey(species, form); - ref var node = ref Lineage[key]; - node.Ban(func); - } - - #endregion - - /// - /// Gets a list of evolutions for the input by checking each evolution in the chain. - /// - /// Pokémon data to check with. - /// Maximum level to permit before the chain breaks. - /// Maximum species ID to permit within the chain. - /// Ignores an evolution's criteria, causing the returned list to have all possible evolutions. - /// Minimum level to permit before the chain breaks. - /// Final species to stop at, if known - public EvoCriteria[] GetValidPreEvolutions(PKM pk, byte levelMax, int maxSpeciesOrigin = -1, bool skipChecks = false, byte levelMin = 1, int stopSpecies = 0) - { - if (maxSpeciesOrigin <= 0) - maxSpeciesOrigin = GetMaxSpeciesOrigin(pk); - - ushort species = pk.Species; - byte form = pk.Form; - - return GetExplicitLineage(species, form, pk, levelMin, levelMax, maxSpeciesOrigin, skipChecks, stopSpecies); - } - - private static int GetMaxSpeciesOrigin(PKM pk) - { - if (pk.Format == 1) - return MaxSpeciesID_1; - if (pk.Format == 2 || pk.VC) - return MaxSpeciesID_2; - return Legal.GetMaxSpeciesOrigin(pk.Generation); - } - - public bool IsSpeciesDerivedFrom(ushort species, byte form, int otherSpecies, int otherForm, bool ignoreForm = true) - { - var evos = GetEvolutionsAndPreEvolutions(species, form); - foreach (var (s, f) in evos) - { - if (s != otherSpecies) - continue; - if (ignoreForm) - return true; - return f == otherForm; - } - return false; - } - - /// - /// Gets all species the - can evolve to & from, yielded in order of increasing evolution stage. - /// - /// Species ID - /// Form ID - /// Enumerable of species IDs (with the Form IDs included, left shifted by 11). - public IEnumerable<(ushort Species, byte Form)> GetEvolutionsAndPreEvolutions(ushort species, byte form) - { - foreach (var s in GetPreEvolutions(species, form)) - yield return s; - yield return (species, form); - foreach (var s in GetEvolutions(species, form)) - yield return s; - } - - public (ushort Species, byte Form) GetBaseSpeciesForm(ushort species, byte form, int skip = 0) - { - var chain = GetEvolutionsAndPreEvolutions(species, form); - foreach (var c in chain) - { - if (skip == 0) - return c; - skip--; - } - return (species, form); - } - - /// - /// Gets all species the - can evolve from, yielded in order of increasing evolution stage. - /// - /// Species ID - /// Form ID - /// Enumerable of species IDs (with the Form IDs included, left shifted by 11). - public IEnumerable<(ushort Species, byte Form)> GetPreEvolutions(ushort species, byte form) - { - int index = GetLookupKey(species, form); - var node = Lineage[index]; - { - // No convergent evolutions; first method is enough. - var s = node.First.Tuple; - if (s.Species == 0) - yield break; - - var preEvolutions = GetPreEvolutions(s.Species, s.Form); - foreach (var preEvo in preEvolutions) - yield return preEvo; - yield return s; - } - } - - /// - /// Gets all species the - can evolve to, yielded in order of increasing evolution stage. - /// - /// Species ID - /// Form ID - /// Enumerable of species IDs (with the Form IDs included, left shifted by 11). - public IEnumerable<(ushort Species, byte Form)> GetEvolutions(ushort species, byte form) - { - int index = !FormSeparateEvoData ? species : Personal.GetFormIndex(species, form); - var evos = Entries[index]; - foreach (var method in evos) - { - var s = method.Species; - if (s == 0) - continue; - var f = method.GetDestinationForm(form); - yield return (s, f); - var nextEvolutions = GetEvolutions(s, f); - foreach (var nextEvo in nextEvolutions) - yield return nextEvo; - } - } - - /// - /// Generates the reverse evolution path for the input . - /// - /// Entity Species to begin the chain - /// Entity Form to begin the chain - /// Entity data - /// Minimum level - /// Maximum level - /// Clamp for maximum species ID - /// Skip the secondary checks that validate the evolution - /// Final species to stop at, if known - public EvoCriteria[] GetExplicitLineage(ushort species, byte form, PKM pk, byte levelMin, byte levelMax, int maxSpeciesID, bool skipChecks, int stopSpecies) - { - if (pk.IsEgg && !skipChecks) - { - return new[] - { - new EvoCriteria{ Species = species, Form = form, LevelMax = levelMax, LevelMin = levelMax }, - }; - } - - // Shedinja's evolution case can be a little tricky; hard-code handling. - if (species == (int)Species.Shedinja && levelMax >= 20 && (!pk.HasOriginalMetLocation || levelMin < levelMax)) - { - var min = Math.Max(levelMin, (byte)20); - return new[] - { - new EvoCriteria { Species = (ushort)Species.Shedinja, LevelMax = levelMax, LevelMin = min, Method = EvolutionType.LevelUp }, - new EvoCriteria { Species = (ushort)Species.Nincada, LevelMax = levelMax, LevelMin = levelMin }, - }; - } - return Lineage.Reverse(species, form, pk, levelMin, levelMax, maxSpeciesID, skipChecks, stopSpecies); - } } diff --git a/PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardPersonal.cs b/PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardPersonal.cs new file mode 100644 index 000000000..8b1394335 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardPersonal.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public sealed class EvolutionForwardPersonal : IEvolutionForward +{ + private readonly IPersonalTable Personal; + private readonly EvolutionMethod[][] Entries; + + public EvolutionForwardPersonal(EvolutionMethod[][] entries, IPersonalTable personal) + { + Entries = entries; + Personal = personal; + } + + public ReadOnlyMemory GetForward(ushort species, byte form) + { + int index = Personal.GetFormIndex(species, form); + return Entries[index]; + } + + public IEnumerable<(ushort Species, byte Form)> GetEvolutions(ushort species, byte form) + { + var methods = GetForward(species, form); + return GetEvolutions(methods, form); + } + + private IEnumerable<(ushort Species, byte Form)> GetEvolutions(ReadOnlyMemory evos, byte form) + { + for (int i = 0; i < evos.Length; i++) + { + var method = evos.Span[i]; + var s = method.Species; + if (s == 0) + continue; + var f = method.GetDestinationForm(form); + yield return (s, f); + var nextEvolutions = GetEvolutions(s, f); + foreach (var nextEvo in nextEvolutions) + yield return nextEvo; + } + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + var methods = GetForward(head.Species, head.Form); + foreach (var method in methods.Span) + { + if (method.Species != next.Species) + continue; + var expectForm = method.GetDestinationForm(head.Form); + if (next.Form != expectForm) + continue; + + var chk = method.Check(pk, currentMaxLevel, levelMin, skipChecks); + if (chk != EvolutionCheckResult.Valid) + continue; + + result = Create(next.Species, next.Form, method, currentMaxLevel, levelMin); + return true; + } + + result = default; + return false; + } + + private static EvoCriteria Create(ushort species, byte form, EvolutionMethod method, byte currentMaxLevel, byte min) => new() + { + Species = species, + Form = form, + LevelMax = currentMaxLevel, + Method = method.Method, + + // Temporarily store these and overwrite them when we clean the list. + LevelMin = Math.Max(min, method.Level), + LevelUpRequired = method.RequiresLevelUp ? (byte)1 : (byte)0, + }; +} diff --git a/PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardSpecies.cs b/PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardSpecies.cs new file mode 100644 index 000000000..353456e51 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Forward/EvolutionForwardSpecies.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public sealed class EvolutionForwardSpecies : IEvolutionForward +{ + private readonly EvolutionMethod[][] Entries; + + public EvolutionForwardSpecies(EvolutionMethod[][] entries) => Entries = entries; + + public IEnumerable<(ushort Species, byte Form)> GetEvolutions(ushort species, byte form) + { + var methods = GetForward(species, form); + return GetEvolutions(methods, form); + } + + public ReadOnlyMemory GetForward(ushort species, byte form) + { + var arr = Entries; + if (species >= arr.Length) + return Array.Empty(); + return arr[species]; + } + + private IEnumerable<(ushort Species, byte Form)> GetEvolutions(ReadOnlyMemory evos, byte form) + { + for (int i = 0; i < evos.Length; i++) + { + var method = evos.Span[i]; + var s = method.Species; + if (s == 0) + continue; + var f = method.GetDestinationForm(form); + yield return (s, f); + var nextEvolutions = GetEvolutions(s, f); + foreach (var nextEvo in nextEvolutions) + yield return nextEvo; + } + } + + public bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + var methods = GetForward(head.Species, head.Form); + foreach (var method in methods.Span) + { + if (method.Species != next.Species) + continue; + var expectForm = method.GetDestinationForm(head.Form); + if (next.Form != expectForm) + continue; + + var chk = method.Check(pk, currentMaxLevel, levelMin, skipChecks); + if (chk != EvolutionCheckResult.Valid) + continue; + + result = Create(next.Species, next.Form, method, currentMaxLevel, levelMin); + return true; + } + + result = default; + return false; + } + + private static EvoCriteria Create(ushort species, byte form, EvolutionMethod method, byte currentMaxLevel, byte min) => new() + { + Species = species, + Form = form, + LevelMax = currentMaxLevel, + Method = method.Method, + + // Temporarily store these and overwrite them when we clean the list. + LevelMin = Math.Max(min, method.Level), + LevelUpRequired = method.RequiresLevelUp ? (byte)1 : (byte)0, + }; +} diff --git a/PKHeX.Core/Legality/Evolutions/Forward/IEvolutionForward.cs b/PKHeX.Core/Legality/Evolutions/Forward/IEvolutionForward.cs new file mode 100644 index 000000000..91a245058 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Forward/IEvolutionForward.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public interface IEvolutionForward +{ + ReadOnlyMemory GetForward(ushort species, byte form); + + /// + /// Gets all species the - can evolve to, yielded in order of increasing evolution stage. + /// + /// Species ID + /// Form ID + /// Enumerable of species IDs (with the Form IDs included, left shifted by 11). + IEnumerable<(ushort Species, byte Form)> GetEvolutions(ushort species, byte form); + + bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result); +} diff --git a/PKHeX.Core/Legality/Evolutions/IEvolutionNetwork.cs b/PKHeX.Core/Legality/Evolutions/IEvolutionNetwork.cs new file mode 100644 index 000000000..13607f302 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/IEvolutionNetwork.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public interface IEvolutionNetwork +{ + IEvolutionForward Forward { get; } + IEvolutionReverse Reverse { get; } +} + +/// +/// Base abstraction for +/// +public abstract class EvolutionNetwork : IEvolutionNetwork +{ + public IEvolutionForward Forward { get; } + public IEvolutionReverse Reverse { get; } + + protected EvolutionNetwork(IEvolutionForward forward, IEvolutionReverse reverse) + { + Forward = forward; + Reverse = reverse; + } + + /// + /// Gets all species the - can evolve to & from, yielded in order of increasing evolution stage. + /// + /// Species ID + /// Form ID + /// Enumerable of species IDs (with the Form IDs included, left shifted by 11). + public IEnumerable<(ushort Species, byte Form)> GetEvolutionsAndPreEvolutions(ushort species, byte form) + { + foreach (var s in Reverse.GetPreEvolutions(species, form)) + yield return s; + yield return (species, form); + foreach (var s in Forward.GetEvolutions(species, form)) + yield return s; + } + + public bool IsSpeciesDerivedFrom(ushort species, byte form, int otherSpecies, int otherForm, bool ignoreForm = true) + { + var evos = GetEvolutionsAndPreEvolutions(species, form); + foreach (var (s, f) in evos) + { + if (s != otherSpecies) + continue; + if (ignoreForm) + return true; + return f == otherForm; + } + return false; + } + + public (ushort Species, byte Form) GetBaseSpeciesForm(ushort species, byte form) + { + var chain = Reverse.GetPreEvolutions(species, form); + foreach (var evo in chain) + return evo; + return (species, form); + } + + public int Devolve(Span result, ushort species, byte form, PKM pk, byte levelMin, byte levelMax, ushort stopSpecies, + bool skipChecks) + { + return Reverse.Devolve(result, species, form, pk, levelMin, levelMax, stopSpecies, skipChecks); + } +} diff --git a/PKHeX.Core/Legality/Evolutions/EvoCriteria.cs b/PKHeX.Core/Legality/Evolutions/Methods/EvoCriteria.cs similarity index 55% rename from PKHeX.Core/Legality/Evolutions/EvoCriteria.cs rename to PKHeX.Core/Legality/Evolutions/Methods/EvoCriteria.cs index 36602f2f3..902452b0d 100644 --- a/PKHeX.Core/Legality/Evolutions/EvoCriteria.cs +++ b/PKHeX.Core/Legality/Evolutions/Methods/EvoCriteria.cs @@ -19,4 +19,25 @@ namespace PKHeX.Core; public bool InsideLevelRange(int level) => LevelMin <= level && level <= LevelMax; public override string ToString() => $"{(Species) Species}{(Form != 0 ? $"-{Form}" : "")}}} [{LevelMin},{LevelMax}] via {Method}"; + + internal const EvolutionType SentinelNotReached = EvolutionType.Invalid; + + public bool IsBetterDevolution(EvoCriteria reference) + { + if (reference.Species == 0) + return true; + + return LevelMin + LevelUpRequired < reference.LevelMin + reference.LevelUpRequired; + } + + public bool IsBetterEvolution(EvoCriteria reference) + { + if (reference.Method == SentinelNotReached) + return true; + + if (LevelMin + LevelUpRequired > reference.LevelMin + reference.LevelUpRequired) + return false; + + return LevelMax > reference.LevelMax; + } } diff --git a/PKHeX.Core/Legality/Evolutions/Methods/EvolutionCheckResult.cs b/PKHeX.Core/Legality/Evolutions/Methods/EvolutionCheckResult.cs new file mode 100644 index 000000000..13ef40e9a --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Methods/EvolutionCheckResult.cs @@ -0,0 +1,13 @@ +namespace PKHeX.Core; + +public enum EvolutionCheckResult +{ + Valid = 0, + InsufficientLevel, + BadGender, + BadForm, + WrongEC, + VisitVersion, + LowContestStat, + Untraded, +} diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs b/PKHeX.Core/Legality/Evolutions/Methods/EvolutionMethod.cs similarity index 67% rename from PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs rename to PKHeX.Core/Legality/Evolutions/Methods/EvolutionMethod.cs index 82e852b28..1139bc6cf 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionMethod.cs +++ b/PKHeX.Core/Legality/Evolutions/Methods/EvolutionMethod.cs @@ -1,4 +1,5 @@ using static PKHeX.Core.EvolutionType; +using static PKHeX.Core.EvolutionCheckResult; namespace PKHeX.Core; @@ -11,7 +12,7 @@ namespace PKHeX.Core; /// Conditional Argument (different from ) /// Conditional Argument (different from ) /// Indicates if a level up is required to trigger evolution. -public readonly record struct EvolutionMethod(EvolutionType Method, ushort Species, byte Form = 0, ushort Argument = 0, byte Level = 0, byte LevelUp = 0) : ISpeciesForm +public readonly record struct EvolutionMethod(EvolutionType Method, ushort Species, byte Form = EvolutionMethod.AnyForm, ushort Argument = 0, byte Level = 0, byte LevelUp = 0) : ISpeciesForm { /// Evolve to Species public ushort Species { get; } = Species; @@ -56,88 +57,61 @@ public byte GetDestinationForm(byte form) /// /// Entity to check /// Current level + /// Minimum level to sanity check with /// Option to skip some comparisons to return a 'possible' evolution. /// True if a evolution criteria is valid. - public bool Valid(PKM pk, byte lvl, bool skipChecks) + public EvolutionCheckResult Check(PKM pk, byte lvl, byte levelMin, bool skipChecks) { if (!Method.IsLevelUpRequired()) return ValidNotLevelUp(pk, skipChecks); - if (!IsLevelUpMethodSecondarySatisfied(pk, skipChecks)) - return false; + var chk = IsLevelUpMethodSecondarySatisfied(pk, skipChecks); + if (chk != Valid) + return chk; // Level Up (any); the above Level Up (with condition) cases will reach here if they were valid - if (!RequiresLevelUp) - return lvl >= Level; - - if (Level == 0 && lvl < 2) - return false; if (lvl < Level) - return false; + return InsufficientLevel; + if (!RequiresLevelUp) + return Valid; + if (Level == 0 && lvl < 2) + return InsufficientLevel; + if (lvl < levelMin + LevelUp && !skipChecks) + return InsufficientLevel; - if (skipChecks) - return lvl >= Level; - - // Check Met Level for extra validity - return HasMetLevelIncreased(pk, lvl); + return Valid; } - private bool IsLevelUpMethodSecondarySatisfied(PKM pk, bool skipChecks) => Method switch + private EvolutionCheckResult IsLevelUpMethodSecondarySatisfied(PKM pk, bool skipChecks) => Method switch { // Special Level Up Cases -- return false if invalid - LevelUpMale when pk.Gender != 0 => false, - LevelUpFemale when pk.Gender != 1 => false, - LevelUpFormFemale1 when pk.Gender != 1 || pk.Form != 1 => false, + LevelUpMale when pk.Gender != 0 => BadGender, + LevelUpFemale when pk.Gender != 1 => BadGender, + LevelUpFormFemale1 when pk.Gender != 1 => BadGender, + LevelUpFormFemale1 when pk.Form != 1 => BadForm, // Permit the evolution if we're exploring for mistakes. - LevelUpBeauty when pk is IContestStatsReadOnly s && s.CNT_Beauty < Argument => skipChecks, - LevelUpNatureAmped or LevelUpNatureLowKey when GetAmpLowKeyResult(pk.Nature) != pk.Form => skipChecks, + LevelUpBeauty when pk is IContestStatsReadOnly s && s.CNT_Beauty < Argument => skipChecks ? Valid : LowContestStat, + LevelUpNatureAmped or LevelUpNatureLowKey when GetAmpLowKeyResult(pk.Nature) != pk.Form => skipChecks ? Valid : BadForm, // Version checks come in pairs, check for any pair match - LevelUpVersion or LevelUpVersionDay or LevelUpVersionNight when ((pk.Version & 1) != (Argument & 1) && pk.IsUntraded) => skipChecks, + LevelUpVersion or LevelUpVersionDay or LevelUpVersionNight when ((pk.Version & 1) != (Argument & 1) && pk.IsUntraded) => skipChecks ? Valid : VisitVersion, - LevelUpKnowMoveEC100 when pk.EncryptionConstant % 100 != 0 => skipChecks, - LevelUpKnowMoveECElse when pk.EncryptionConstant % 100 == 0 => skipChecks, - LevelUpInBattleEC100 when pk.EncryptionConstant % 100 != 0 => skipChecks, - LevelUpInBattleECElse when pk.EncryptionConstant % 100 == 0 => skipChecks, + LevelUpKnowMoveEC100 when pk.EncryptionConstant % 100 != 0 => skipChecks ? Valid : WrongEC, + LevelUpKnowMoveECElse when pk.EncryptionConstant % 100 == 0 => skipChecks ? Valid : WrongEC, + LevelUpInBattleEC100 when pk.EncryptionConstant % 100 != 0 => skipChecks ? Valid : WrongEC, + LevelUpInBattleECElse when pk.EncryptionConstant % 100 == 0 => skipChecks ? Valid : WrongEC, - _ => true, + _ => Valid, }; - private bool ValidNotLevelUp(PKM pk, bool skipChecks) => Method switch + private EvolutionCheckResult ValidNotLevelUp(PKM pk, bool skipChecks) => Method switch { - UseItemMale or LevelUpRecoilDamageMale => pk.Gender == 0, - UseItemFemale or LevelUpRecoilDamageFemale => pk.Gender == 1, + UseItemMale or LevelUpRecoilDamageMale => pk.Gender == 0 ? Valid : BadGender, + UseItemFemale or LevelUpRecoilDamageFemale => pk.Gender == 1 ? Valid : BadGender, - Trade or TradeHeldItem or TradeShelmetKarrablast => !pk.IsUntraded || skipChecks, - _ => true, // no conditions - }; - - private bool HasMetLevelIncreased(PKM pk, int lvl) - { - int origin = pk.Generation; - return origin switch - { - // No met data in RBY; No met data in GS, Crystal met data can be reset - 1 or 2 => true, - - // Pal Park / PokeTransfer updates Met Level - 3 or 4 => pk.Format > origin || lvl > pk.Met_Level, - - // 5=>6 and later transfers keep current level - >=5 => lvl >= Level && (!pk.IsNative || lvl > pk.Met_Level), - - _ => false, - }; - } - - public EvoCriteria GetEvoCriteria(ushort species, byte form, byte lvl) => new() - { - Species = species, - Form = form, - LevelMax = lvl, - LevelMin = 0, - Method = Method, + Trade or TradeHeldItem or TradeShelmetKarrablast => !pk.IsUntraded || skipChecks ? Valid : Untraded, + _ => Valid, // no conditions }; public static int GetAmpLowKeyResult(int n) diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionType.cs b/PKHeX.Core/Legality/Evolutions/Methods/EvolutionType.cs similarity index 97% rename from PKHeX.Core/Legality/Evolutions/EvolutionType.cs rename to PKHeX.Core/Legality/Evolutions/Methods/EvolutionType.cs index 967434906..946488f04 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionType.cs +++ b/PKHeX.Core/Legality/Evolutions/Methods/EvolutionType.cs @@ -78,6 +78,11 @@ public enum EvolutionType : byte UseItemFullMoon = 90, // Ursaluna UseMoveAgileStyle = 91, // Wyrdeer UseMoveStrongStyle = 92, // Overqwil + + /// + /// Used as a placeholder sentinel for internal logic. + /// + Invalid = unchecked((byte)-1), } /// diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionLink.cs b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionLink.cs index 3d2220392..3ac863b4c 100644 --- a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionLink.cs +++ b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionLink.cs @@ -1,34 +1,9 @@ -using System; - namespace PKHeX.Core; /// /// Links a to the source and that the method can be triggered from. /// -public struct EvolutionLink +public readonly record struct EvolutionLink(EvolutionMethod Method, ushort Species, byte Form) { - private Func? IsBanned = null; - public readonly EvolutionMethod Method; - public readonly ushort Species; - public readonly byte Form; - - public EvolutionLink(ushort species, byte form, EvolutionMethod method) - { - Species = species; - Form = form; - Method = method; - } - public bool IsEmpty => Species == 0; - - public (ushort Species, byte Form) Tuple => (Species, Form); - - public void Ban(Func check) => IsBanned = check; - - /// - /// Checks if the is allowed. - /// - /// Entity to check - /// True if banned, false if allowed. - public bool IsEvolutionBanned(PKM pk) => IsBanned != null && IsBanned(pk); } diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionNode.cs b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionNode.cs index ccfcdb863..c29c0c365 100644 --- a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionNode.cs +++ b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionNode.cs @@ -26,20 +26,4 @@ public void Add(EvolutionLink link) else throw new InvalidOperationException($"{nameof(EvolutionNode)} already has two links."); } - - /// - /// Registers a function that disallows the reverse evolution link from being valid if the is not satisfied. - /// - /// Function that checks if the link should be allowed as an evolution path. - public void Ban(Func func) - { - ref var first = ref First; - if (first.IsEmpty) - return; - first.Ban(func); - ref var second = ref Second; - if (second.IsEmpty) - return; - second.Ban(func); - } } diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversal.cs b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversal.cs new file mode 100644 index 000000000..dae556111 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversal.cs @@ -0,0 +1,84 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Evolution Reversal logic +/// +public static class EvolutionReversal +{ + /// + /// Reverses from current state to see what evolutions the may have existed as. + /// + /// Evolution Method lineage reversal object + /// Result storage + /// Species to devolve from + /// Form to devolve from + /// Entity reference to sanity check evolutions with + /// Minimum level the entity may exist as + /// Maximum the entity may exist as + /// Species ID that should be the last node, if at all. Provide 0 to fully devolve. + /// Option to bypass some evolution criteria + /// Reversed evolution lineage, with the lowest index being the current species-form. + public static int Devolve(this IEvolutionLookup lineage, Span result, ushort species, byte form, + PKM pk, byte levelMin, byte levelMax, ushort stopSpecies, bool skipChecks) + { + // Store our results -- trim at the end when we place it on the heap. + var head = result[0] = new EvoCriteria { Species = species, Form = form, LevelMax = levelMax }; + int ctr = Devolve(lineage, result, head, pk, levelMax, levelMin, skipChecks, stopSpecies); + EvolutionUtil.CleanDevolve(result[..ctr], levelMin); + return ctr; + } + + private static int Devolve(IEvolutionLookup lineage, Span result, EvoCriteria head, + PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, ushort stopSpecies) + { + // There aren't any circular evolution paths, and all lineages have at most 3 evolutions total. + // There aren't any convergent evolution paths, so only yield the first connection. + int ctr = 1; // count in the 'evos' span + while (head.Species != stopSpecies) + { + var node = lineage[head.Species, head.Form]; + if (!node.TryDevolve(pk, currentMaxLevel, levelMin, skipChecks, out var x)) + return ctr; + + result[ctr++] = x; + currentMaxLevel -= x.LevelUpRequired; + } + return ctr; + } + + public static bool TryDevolve(this EvolutionNode node, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + // Multiple methods can exist to devolve to the same species-form. + // The first method is less restrictive (no LevelUp req), if two {level/other} methods exist. + for (int i = 0; i < 2; i++) + { + ref var link = ref i == 0 ? ref node.First : ref node.Second; + if (link.IsEmpty) + break; + + var chk = link.Method.Check(pk, currentMaxLevel, levelMin, skipChecks); + if (chk != EvolutionCheckResult.Valid) + continue; + + result = Create(link, currentMaxLevel); + return true; + } + + result = default; + return false; + } + + private static EvoCriteria Create(EvolutionLink link, byte currentMaxLevel) => new() + { + Species = link.Species, + Form = link.Form, + LevelMax = currentMaxLevel, + Method = link.Method.Method, + + // Temporarily store these and overwrite them when we clean the list. + LevelMin = link.Method.Level, + LevelUpRequired = link.Method.RequiresLevelUp ? (byte)1 : (byte)0, + }; +} diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseLookup.cs b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseLookup.cs index 541733d06..580d6f4aa 100644 --- a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseLookup.cs +++ b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseLookup.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; namespace PKHeX.Core; @@ -9,36 +8,56 @@ namespace PKHeX.Core; public sealed class EvolutionReverseLookup : IEvolutionLookup { private readonly EvolutionNode[] Nodes; - private readonly int MaxSpecies; private readonly Dictionary KeyLookup; + private readonly ushort MaxSpecies; - public EvolutionReverseLookup(IEnumerable<(int Key, EvolutionLink Value)> links, int maxSpecies) + public EvolutionReverseLookup(ushort maxSpecies) { - MaxSpecies = maxSpecies; + Nodes = new EvolutionNode[maxSpecies * 2]; KeyLookup = new Dictionary(maxSpecies); - var nodes = new EvolutionNode[maxSpecies * 2]; - int ctr = maxSpecies + 1; - foreach (var (key, value) in links) - { - var index = key <= MaxSpecies ? key : KeyLookup.TryGetValue(key, out var x) ? x : KeyLookup[key] = ctr++; - ref var node = ref nodes[index]; - node.Add(value); - } - Nodes = nodes; - Debug.Assert(KeyLookup.Count < maxSpecies); + MaxSpecies = maxSpecies; } - private int GetIndex(int key) + private void Register(EvolutionLink link, ushort species) { - if (key <= MaxSpecies) - return key; + ref var node = ref Nodes[species]; + node.Add(link); + } + + public void Register(EvolutionLink link, ushort species, byte form) + { + if (form == 0) + { + Register(link, species); + return; + } + + int key = GetKey(species, form); + if (!KeyLookup.TryGetValue(key, out var index)) + { + index = Nodes.Length - KeyLookup.Count - 1; + KeyLookup.Add(key, index); + } + + ref var node = ref Nodes[index]; + node.Add(link); + } + + private int GetIndex(ushort species, byte form) + { + if (species > MaxSpecies) + return 0; + if (form == 0) + return species; + int key = GetKey(species, form); return KeyLookup.TryGetValue(key, out var index) ? index : 0; } - public ref EvolutionNode this[int key] => ref Nodes[GetIndex(key)]; + private static int GetKey(ushort species, byte form) => species | form << 11; + public ref EvolutionNode this[ushort species, byte form] => ref Nodes[GetIndex(species, form)]; } public interface IEvolutionLookup { - ref EvolutionNode this[int key] { get; } + ref EvolutionNode this[ushort species, byte form] { get; } } diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversePersonal.cs b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversePersonal.cs new file mode 100644 index 000000000..274780a67 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReversePersonal.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public sealed class EvolutionReversePersonal : IEvolutionReverse +{ + public IEvolutionLookup Lineage { get; } + + public EvolutionReversePersonal(EvolutionMethod[][] entries, IPersonalTable t) + { + Lineage = GetLineage(t, entries); + } + + private static EvolutionReverseLookup GetLineage(IPersonalTable t, EvolutionMethod[][] entries) + { + var maxSpecies = t.MaxSpeciesID; + var lineage = new EvolutionReverseLookup(maxSpecies); + for (ushort sSpecies = 1; sSpecies <= maxSpecies; sSpecies++) + { + var fc = t[sSpecies].FormCount; + for (byte sForm = 0; sForm < fc; sForm++) + { + var index = t.GetFormIndex(sSpecies, sForm); + foreach (var evo in entries[index]) + { + var dSpecies = evo.Species; + if (dSpecies == 0) + break; + + var dForm = evo.GetDestinationForm(sForm); + var link = new EvolutionLink(evo, sSpecies, sForm); + lineage.Register(link, dSpecies, dForm); + } + } + } + return lineage; + } + + public EvolutionNode GetReverse(ushort species, byte form) => Lineage[species, form]; + + public IEnumerable<(ushort Species, byte Form)> GetPreEvolutions(ushort species, byte form) + { + var node = Lineage[species, form]; + + // No convergent evolutions; first method is enough. + var s = node.First; + if (s.Species == 0) + yield break; + + var preEvolutions = GetPreEvolutions(s.Species, s.Form); + foreach (var preEvo in preEvolutions) + yield return preEvo; + yield return (s.Species, s.Form); + } + + public int Devolve(Span result, ushort species, byte form, PKM pk, byte levelMin, byte levelMax, ushort stopSpecies, + bool skipChecks) + { + return Lineage.Devolve(result, species, form, pk, levelMin, levelMax, stopSpecies, skipChecks); + } + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + var node = Lineage[head.Species, head.Form]; + return node.TryDevolve(pk, currentMaxLevel, levelMin, skipChecks, out result); + } +} diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseSpecies.cs b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseSpecies.cs new file mode 100644 index 000000000..7d33487db --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Reversal/EvolutionReverseSpecies.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public sealed class EvolutionReverseSpecies : IEvolutionReverse +{ + public EvolutionReverseLookup Lineage { get; } + + public EvolutionReverseSpecies(EvolutionMethod[][] entries, IPersonalTable t) + { + Lineage = GetLineage(t, entries); + } + + private static EvolutionReverseLookup GetLineage(IPersonalTable t, EvolutionMethod[][] entries) + { + var maxSpecies = t.MaxSpeciesID; + var lineage = new EvolutionReverseLookup(maxSpecies); + for (ushort sSpecies = 1; sSpecies <= maxSpecies; sSpecies++) + { + var fc = t[sSpecies].FormCount; + for (byte sForm = 0; sForm < fc; sForm++) + { + foreach (var evo in entries[sSpecies]) + { + var dSpecies = evo.Species; + if (dSpecies == 0) + break; + + var dForm = sSpecies == (int)Species.Espurr && evo.Method == EvolutionType.LevelUpFormFemale1 + ? (byte)1 + : sForm; + var link = new EvolutionLink(evo, sSpecies, sForm); + lineage.Register(link, dSpecies, dForm); + } + } + } + + return lineage; + } + + public EvolutionNode GetReverse(ushort species, byte form) => Lineage[species, form]; + + public IEnumerable<(ushort Species, byte Form)> GetPreEvolutions(ushort species, byte form) + { + var node = Lineage[species, form]; + + // No convergent evolutions; first method is enough. + var s = node.First; + if (s.Species == 0) + yield break; + + var preEvolutions = GetPreEvolutions(s.Species, s.Form); + foreach (var preEvo in preEvolutions) + yield return preEvo; + yield return (s.Species, s.Form); + } + + public int Devolve(Span result, ushort species, byte form, PKM pk, byte levelMin, byte levelMax, ushort stopSpecies, + bool skipChecks) + { + return Lineage.Devolve(result, species, form, pk, levelMin, levelMax, stopSpecies, skipChecks); + } + + public bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result) + { + var node = Lineage[head.Species, head.Form]; + return node.TryDevolve(pk, currentMaxLevel, levelMin, skipChecks, out result); + } +} diff --git a/PKHeX.Core/Legality/Evolutions/Reversal/IEvolutionReverse.cs b/PKHeX.Core/Legality/Evolutions/Reversal/IEvolutionReverse.cs new file mode 100644 index 000000000..efde9a2c3 --- /dev/null +++ b/PKHeX.Core/Legality/Evolutions/Reversal/IEvolutionReverse.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace PKHeX.Core; + +public interface IEvolutionReverse +{ + EvolutionNode GetReverse(ushort species, byte form); + + /// + /// Gets all species the - can evolve from, yielded in order of increasing evolution stage. + /// + /// Species ID + /// Form ID + /// Enumerable of species IDs (with the Form IDs included, left shifted by 11). + IEnumerable<(ushort Species, byte Form)> GetPreEvolutions(ushort species, byte form); + + int Devolve(Span result, ushort species, byte form, PKM pk, byte levelMin, byte levelMax, ushort stopSpecies, bool skipChecks); + + bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result); +} diff --git a/PKHeX.Core/Legality/LearnSource/Group/ILearnGroup.cs b/PKHeX.Core/Legality/LearnSource/Group/ILearnGroup.cs index 357d14380..7406a14b5 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/ILearnGroup.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/ILearnGroup.cs @@ -7,6 +7,11 @@ namespace PKHeX.Core; /// public interface ILearnGroup { + /// + /// Gets the maximum move ID that this group can learn. + /// + ushort MaxMoveID { get; } + /// /// Gets the next group to traverse to continue checking moves. /// diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup1.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup1.cs index 6e27ca4f7..15ee099a6 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup1.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup1.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup1 : ILearnGroup { public static readonly LearnGroup1 Instance = new(); private const int Generation = 1; + public ushort MaxMoveID => Legal.MaxMoveID_1; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => pk.Context switch { diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs index 8c7222802..70fbd318f 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup2 : ILearnGroup { public static readonly LearnGroup2 Instance = new(); private const int Generation = 2; + public ushort MaxMoveID => Legal.MaxMoveID_2; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => pk.Context switch { diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs index a9223284c..7d1c7d9ab 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup3 : ILearnGroup { public static readonly LearnGroup3 Instance = new(); private const int Generation = 3; + public ushort MaxMoveID => Legal.MaxMoveID_3; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; // Gen3 is the end of the line! public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen3; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs index 9c85067b8..a8d55b190 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup4 : ILearnGroup { public static readonly LearnGroup4 Instance = new(); private const int Generation = 4; + public ushort MaxMoveID => Legal.MaxMoveID_4; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation is Generation ? null : LearnGroup3.Instance; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen4; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs index 20f413028..4077bec61 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup5 : ILearnGroup { public static readonly LearnGroup5 Instance = new(); private const int Generation = 5; + public ushort MaxMoveID => Legal.MaxMoveID_5; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation is Generation ? null : LearnGroup4.Instance; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen5; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs index 3648d64ba..4c2dac61e 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup6 : ILearnGroup { public static readonly LearnGroup6 Instance = new(); private const int Generation = 6; + public ushort MaxMoveID => Legal.MaxMoveID_6_AO; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation is Generation ? null : LearnGroup5.Instance; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen6; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs index 40724510d..8aaa4c0d4 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup7 : ILearnGroup { public static readonly LearnGroup7 Instance = new(); private const int Generation = 7; + public ushort MaxMoveID => Legal.MaxMoveID_7; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation switch { @@ -167,7 +168,7 @@ private static void GetAllMoves(Span result, PKM pk, EvoCriteria evo, Move } // Check all forms - var inst = LearnSource6AO.Instance; + var inst = LearnSource7USUM.Instance; if (!inst.TryGetPersonal(evo.Species, evo.Form, out var pi)) return; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7b.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7b.cs index 26d1c7b08..52602ec7d 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7b.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7b.cs @@ -9,6 +9,7 @@ public sealed class LearnGroup7b : ILearnGroup { public static readonly LearnGroup7b Instance = new(); private const int Generation = 7; + public ushort MaxMoveID => Legal.MaxMoveID_7b; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedLGPE; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs index 7f74f80a6..713e96e7e 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs @@ -10,6 +10,7 @@ public sealed class LearnGroup8 : ILearnGroup public static readonly LearnGroup8 Instance = new(); private const int Generation = 8; private const EntityContext Context = EntityContext.Gen8; + public ushort MaxMoveID => Legal.MaxMoveID_8; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) { @@ -48,7 +49,13 @@ public sealed class LearnGroup8 : ILearnGroup if (option is not LearnOption.Current && types.HasFlag(MoveSourceType.Encounter) && pk.IsOriginalMovesetDeleted() && enc is EncounterEgg { Generation: Generation } egg) CheckEncounterMoves(result, current, egg); - return MoveResult.AllParsed(result); + if (MoveResult.AllParsed(result)) + return true; + + var home = LearnGroupHOME.Instance; + if (option != LearnOption.HOME && home.HasVisited(pk, history)) + return home.Check(result, current, pk, history, enc, types); + return false; } private static void CheckSharedMoves(Span result, ReadOnlySpan current, EvoCriteria evo) diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs index 1ec7231ef..c577d6f04 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs @@ -10,6 +10,7 @@ public sealed class LearnGroup8a : ILearnGroup public static readonly LearnGroup8a Instance = new(); private const int Generation = 8; private const EntityContext Context = EntityContext.Gen8a; + public ushort MaxMoveID => Legal.MaxMoveID_8a; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedPLA; @@ -21,7 +22,13 @@ public sealed class LearnGroup8a : ILearnGroup for (var i = 0; i < evos.Length; i++) Check(result, current, pk, evos[i], i); - return MoveResult.AllParsed(result); + if (MoveResult.AllParsed(result)) + return true; + + var home = LearnGroupHOME.Instance; + if (option != LearnOption.HOME && home.HasVisited(pk, history)) + return home.Check(result, current, pk, history, enc, types); + return false; } private static void Check(Span result, ReadOnlySpan current, PKM pk, EvoCriteria evo, int stage) diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs index ba3212b86..3a7a42543 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs @@ -10,6 +10,7 @@ public sealed class LearnGroup8b : ILearnGroup public static readonly LearnGroup8b Instance = new(); private const int Generation = 8; private const EntityContext Context = EntityContext.Gen8b; + public ushort MaxMoveID => Legal.MaxMoveID_8b; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedBDSP; @@ -26,7 +27,13 @@ public sealed class LearnGroup8b : ILearnGroup CheckSharedMoves(result, current, evos[0]); - return MoveResult.AllParsed(result); + if (MoveResult.AllParsed(result)) + return true; + + var home = LearnGroupHOME.Instance; + if (option != LearnOption.HOME && home.HasVisited(pk, history)) + return home.Check(result, current, pk, history, enc, types); + return false; } private static void CheckSharedMoves(Span result, ReadOnlySpan current, EvoCriteria evo) diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs index 106bcc1f1..d3f3d9895 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs @@ -10,12 +10,9 @@ public sealed class LearnGroup9 : ILearnGroup public static readonly LearnGroup9 Instance = new(); private const int Generation = 9; private const EntityContext Context = EntityContext.Gen9; + public ushort MaxMoveID => Legal.MaxMoveID_9; - public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) - { - return null; - } - + public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen9; public bool Check(Span result, ReadOnlySpan current, PKM pk, EvolutionHistory history, @@ -33,7 +30,13 @@ public sealed class LearnGroup9 : ILearnGroup if (option is not LearnOption.Current && types.HasFlag(MoveSourceType.Encounter) && pk.IsOriginalMovesetDeleted() && enc is EncounterEgg { Generation: Generation } egg) CheckEncounterMoves(result, current, egg); - return MoveResult.AllParsed(result); + if (MoveResult.AllParsed(result)) + return true; + + var home = LearnGroupHOME.Instance; + if (option != LearnOption.HOME && home.HasVisited(pk, history)) + return home.Check(result, current, pk, history, enc, types); + return false; } private static void CheckSharedMoves(Span result, ReadOnlySpan current, EvoCriteria evo) @@ -129,7 +132,7 @@ private static void GetAllMoves(Span result, PKM pk, EvoCriteria evo, Move } // Check all forms - var inst = LearnSource6AO.Instance; + var inst = LearnSource9SV.Instance; if (!inst.TryGetPersonal(evo.Species, evo.Form, out var pi)) return; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs new file mode 100644 index 000000000..b2fc761f0 --- /dev/null +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs @@ -0,0 +1,239 @@ +using System; +using System.Buffers; + +namespace PKHeX.Core; + +/// +/// Encapsulates logic for HOME's Move Relearner feature. +/// +/// +/// If the Entity knew a move at any point in its history, it can be relearned if the current format can learn it. +/// +public class LearnGroupHOME : ILearnGroup +{ + public static readonly LearnGroupHOME Instance = new(); + public ushort MaxMoveID => 0; + + public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; + public bool HasVisited(PKM pk, EvolutionHistory history) => pk is IHomeTrack { HasTracker: true } || !ParseSettings.IgnoreTransferIfNoTracker; + + public bool Check(Span result, ReadOnlySpan current, PKM pk, EvolutionHistory history, + IEncounterTemplate enc, MoveSourceType types = MoveSourceType.All, LearnOption option = LearnOption.HOME) + { + var context = pk.Context; + if (context == EntityContext.None) + return false; + + var local = GetCurrent(context); + var evos = history.Get(context); + if (history.HasVisitedGen9 && pk is not PK9) + { + var instance = LearnGroup9.Instance; + instance.Check(result, current, pk, history, enc, types, option); + if (CleanPurge(result, current, pk, types, local, evos)) + return true; + } + if (history.HasVisitedSWSH && pk is not PK8) + { + var instance = LearnGroup8.Instance; + instance.Check(result, current, pk, history, enc, types, option); + if (CleanPurge(result, current, pk, types, local, evos)) + return true; + } + if (history.HasVisitedPLA && pk is not PA8) + { + var instance = LearnGroup8a.Instance; + instance.Check(result, current, pk, history, enc, types, option); + if (CleanPurge(result, current, pk, types, local, evos)) + return true; + } + if (history.HasVisitedBDSP && pk is not PB8) + { + var instance = LearnGroup8b.Instance; + instance.Check(result, current, pk, history, enc, types, option); + if (CleanPurge(result, current, pk, types, local, evos)) + return true; + } + + if (TryAddSpecialCaseMoves(pk.Species, result, current)) + return true; + + if (history.HasVisitedLGPE) + { + var instance = LearnGroup7b.Instance; + instance.Check(result, current, pk, history, enc, types, option); + if (CleanPurge(result, current, pk, types, local, evos)) + return true; + } + else if (history.HasVisitedGen7) + { + ILearnGroup instance = LearnGroup7.Instance; + while (true) + { + instance.Check(result, current, pk, history, enc, types, option); + if (CleanPurge(result, current, pk, types, local, evos)) + return true; + var prev = instance.GetPrevious(pk, history, enc, option); + if (prev is null) + break; + instance = prev; + } + } + return false; + } + + /// + /// Scan the results and remove any that are not valid for the game game. + /// + /// True if all results are valid. + private static bool CleanPurge(Span result, ReadOnlySpan current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan evos) + { + // The logic used to update the results did not check if the move was actually able to be learned in the local game. + // Double check the results and remove any that are not valid for the local game. + // SW/SH will continue to iterate downwards to previous groups after HOME is checked, so we can exactly check via Environment. + for (int i = 0; i < result.Length; i++) + { + ref var r = ref result[i]; + if (!r.Valid || r.Generation == 0) + continue; + + if (r.Info.Environment == local.Environment) + continue; + + // Check if any evolution in the local context can learn the move via HOME instruction. If none can, the move is invalid. + var move = current[i]; + if (move == 0) + continue; + + bool valid = false; + foreach (var evo in evos) + { + var chk = local.GetCanLearnHOME(pk, evo, move, types); + if (chk.Method != LearnMethod.None) + valid = true; + } + if (!valid) + r = default; + } + + return MoveResult.AllParsed(result); + } + + /// + /// Get the current HOME source for the given context. + /// + /// + private static IHomeSource GetCurrent(EntityContext context) => context switch + { + EntityContext.Gen8 => LearnSource8SWSH.Instance, + EntityContext.Gen8a => LearnSource8LA.Instance, + EntityContext.Gen8b => LearnSource8BDSP.Instance, + EntityContext.Gen9 => LearnSource9SV.Instance, + _ => throw new ArgumentOutOfRangeException(nameof(context), context, null), + }; + + public void GetAllMoves(Span result, PKM pk, EvolutionHistory history, IEncounterTemplate enc, + MoveSourceType types = MoveSourceType.All, LearnOption option = LearnOption.HOME) + { + option = LearnOption.HOME; + var local = GetCurrent(pk.Context); + var evos = history.Get(pk.Context); + + // Check all adjacent games + if (history.HasVisitedGen9 && pk is not PK9) + RentLoopGetAll(LearnGroup9. Instance, result, pk, history, enc, types, option, evos, local); + if (history.HasVisitedSWSH && pk is not PK8) + RentLoopGetAll(LearnGroup8. Instance, result, pk, history, enc, types, option, evos, local); + if (history.HasVisitedPLA && pk is not PA8) + RentLoopGetAll(LearnGroup8a.Instance, result, pk, history, enc, types, option, evos, local); + if (history.HasVisitedBDSP && pk is not PB8) + RentLoopGetAll(LearnGroup8b.Instance, result, pk, history, enc, types, option, evos, local); + + AddSpecialCaseMoves(pk.Species, result); + + // Looking backwards before HOME + if (history.HasVisitedLGPE) + { + RentLoopGetAll(LearnGroup7b.Instance, result, pk, history, enc, types, option, evos, local); + } + else if (history.HasVisitedGen7) + { + ILearnGroup instance = LearnGroup7.Instance; + while (true) + { + RentLoopGetAll(instance, result, pk, history, enc, types, option, evos, local); + var prev = instance.GetPrevious(pk, history, enc, option); + if (prev is null) + break; + instance = prev; + } + } + } + + private static void RentLoopGetAll(T instance, Span result, PKM pk, EvolutionHistory history, + IEncounterTemplate enc, + MoveSourceType types, LearnOption option, ReadOnlySpan evos, IHomeSource local) where T : ILearnGroup + { + var length = instance.MaxMoveID; + var rent = ArrayPool.Shared.Rent(length); + var temp = rent.AsSpan(0, length); + instance.GetAllMoves(temp, pk, history, enc, types, option); + LoopMerge(result, pk, evos, types, local, temp); + temp.Clear(); + ArrayPool.Shared.Return(rent); + } + + /// + /// For each move that is possible to learn in another game, check if it is possible to learn in the current game. + /// + /// Resulting array of moves that are possible to learn in the current game. + /// Entity to check. + /// Evolutions to check. + /// Move source types to check. + /// Destination game to check. + /// Temporary array of moves that are possible to learn in the checked game. + private static void LoopMerge(Span result, PKM pk, ReadOnlySpan evos, MoveSourceType types, IHomeSource dest, Span temp) + { + var length = Math.Min(result.Length, temp.Length); + for (ushort move = 0; move < length; move++) + { + if (!temp[move]) + continue; // not possible to learn in other game + if (result[move]) + continue; // already possible to learn in current game + + foreach (var evo in evos) + { + var chk = dest.GetCanLearnHOME(pk, evo, move, types); + if (chk.Method == LearnMethod.None) + continue; + result[move] = true; + break; + } + } + } + + private static bool TryAddSpecialCaseMoves(ushort species, Span result, ReadOnlySpan current) + { + if (IsPikachuLine(species)) + { + var index = current.IndexOf((ushort)Move.VoltTackle); + if (index == -1) + return false; + ref var move = ref result[index]; + if (move.Valid) + return false; + move = new MoveResult(LearnMethod.Shared, LearnEnvironment.HOME); + return MoveResult.AllValid(result); + } + return false; + } + + private static void AddSpecialCaseMoves(ushort species, Span result) + { + if (IsPikachuLine(species)) + result[(int)Move.VoltTackle] = true; + } + + private static bool IsPikachuLine(ushort species) => species is (int)Species.Raichu or (int)Species.Pikachu or (int)Species.Pichu; +} diff --git a/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs b/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs index 16b36b1c9..481c85345 100644 --- a/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs +++ b/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs @@ -19,6 +19,7 @@ public enum LearnEnvironment : byte /* Gen7 */ SM, USUM, GG, /* Gen8 */ SWSH, BDSP, PLA, /* Gen9 */ SV, + HOME, } /// diff --git a/PKHeX.Core/Legality/LearnSource/Sources/IHomeSource.cs b/PKHeX.Core/Legality/LearnSource/Sources/IHomeSource.cs new file mode 100644 index 000000000..52223600f --- /dev/null +++ b/PKHeX.Core/Legality/LearnSource/Sources/IHomeSource.cs @@ -0,0 +1,7 @@ +namespace PKHeX.Core; + +public interface IHomeSource +{ + LearnEnvironment Environment { get; } + MoveLearnInfo GetCanLearnHOME(PKM pk, EvoCriteria evo, ushort move, MoveSourceType types = MoveSourceType.All); +} diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4HGSS.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4HGSS.cs index 7a486c88d..901731e46 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4HGSS.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4HGSS.cs @@ -77,11 +77,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo4 pi, EvoCriteria evo, usho private static bool GetIsEnhancedTutor(EvoCriteria evo, ISpeciesForm current, ushort move, LearnOption option) => evo.Species is (int)Species.Rotom && move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }; diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4Pt.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4Pt.cs index ee84b398b..0d9ebb52c 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4Pt.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource4Pt.cs @@ -75,11 +75,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo4 pi, EvoCriteria evo, usho private static bool GetIsEnhancedTutor(EvoCriteria evo, ISpeciesForm current, ushort move, LearnOption option) => evo.Species is (int)Species.Rotom && move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }; diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5B2W2.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5B2W2.cs index cdf3994e8..20b0c3658 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5B2W2.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5B2W2.cs @@ -73,11 +73,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo5B2W2 pi, EvoCriteria evo, (int)Species.Meloetta => move is (int)Move.RelicSong, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, _ => false, diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5BW.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5BW.cs index 79996ca0f..88a0805a5 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5BW.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource5BW.cs @@ -70,11 +70,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo5BW pi, EvoCriteria evo, us (int)Species.Meloetta => move is (int)Move.RelicSong, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, _ => false, diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6AO.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6AO.cs index 37b1bc418..1c72d2241 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6AO.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6AO.cs @@ -74,11 +74,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo6AO pi, EvoCriteria evo, us (int)Species.Meloetta => move is (int)Move.RelicSong, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, _ => false, diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6XY.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6XY.cs index 6394a4949..aeea0d727 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6XY.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource6XY.cs @@ -71,11 +71,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo6XY pi, EvoCriteria evo, us (int)Species.Meloetta => move is (int)Move.RelicSong, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, _ => false, diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7SM.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7SM.cs index 0b221837e..0ba6d442e 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7SM.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7SM.cs @@ -72,19 +72,19 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo7 pi, EvoCriteria evo, usho (int)Species.Pikachu or (int)Species.Raichu => move is (int)Move.VoltTackle, (int)Species.Necrozma => move switch { - (int)Move.SunsteelStrike => (option == LearnOption.AtAnyTime || current.Form == 1), // Sun w/ Solgaleo - (int)Move.MoongeistBeam => (option == LearnOption.AtAnyTime || current.Form == 2), // Moon w/ Lunala + (int)Move.SunsteelStrike => option.IsPast() || current.Form == 1, // Sun w/ Solgaleo + (int)Move.MoongeistBeam => option.IsPast() || current.Form == 2, // Moon w/ Lunala _ => false, }, (int)Species.Keldeo => move is (int)Move.SecretSword, (int)Species.Meloetta => move is (int)Move.RelicSong, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, (int)Species.Zygarde => move is (int)Move.ExtremeSpeed or (int)Move.DragonDance or (int)Move.ThousandArrows or (int)Move.ThousandWaves or (int)Move.CoreEnforcer, diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7USUM.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7USUM.cs index 0a96568c0..65e316b65 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7USUM.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource7USUM.cs @@ -75,19 +75,19 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo7 pi, EvoCriteria evo, usho (int)Species.Pikachu or (int)Species.Raichu => move is (int)Move.VoltTackle, (int)Species.Necrozma => move switch { - (int)Move.SunsteelStrike => (option == LearnOption.AtAnyTime || current.Form == 1), // Sun w/ Solgaleo - (int)Move.MoongeistBeam => (option == LearnOption.AtAnyTime || current.Form == 2), // Moon w/ Lunala + (int)Move.SunsteelStrike => option.IsPast() || current.Form == 1, // Sun w/ Solgaleo + (int)Move.MoongeistBeam => option.IsPast() || current.Form == 2, // Moon w/ Lunala _ => false, }, (int)Species.Keldeo => move is (int)Move.SecretSword, (int)Species.Meloetta => move is (int)Move.RelicSong, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, (int)Species.Zygarde => move is (int)Move.ExtremeSpeed or (int)Move.DragonDance or (int)Move.ThousandArrows or (int)Move.ThousandWaves or (int)Move.CoreEnforcer, diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8BDSP.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8BDSP.cs index dc6460e57..3e4bbb134 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8BDSP.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8BDSP.cs @@ -8,7 +8,7 @@ namespace PKHeX.Core; /// /// Exposes information about how moves are learned in . /// -public sealed class LearnSource8BDSP : ILearnSource, IEggSource +public sealed class LearnSource8BDSP : ILearnSource, IEggSource, IHomeSource { public static readonly LearnSource8BDSP Instance = new(); private static readonly PersonalTable8BDSP Personal = PersonalTable.BDSP; @@ -74,11 +74,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo8BDSP pi, EvoCriteria evo, private static bool GetIsEnhancedTutor(EvoCriteria evo, ISpeciesForm current, ushort move, LearnOption option) => evo.Species is (int)Species.Rotom && move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }; @@ -147,4 +147,27 @@ public void GetAllMoves(Span result, PKM pk, EvoCriteria evo, MoveSourceTy (int)Move.HydroCannon, (int)Move.DracoMeteor, }; + + public LearnEnvironment Environment => Game; + public MoveLearnInfo GetCanLearnHOME(PKM pk, EvoCriteria evo, ushort move, MoveSourceType types = MoveSourceType.All) + { + if (!TryGetPersonal(evo.Species, evo.Form, out var pi)) + return default; + + if (types.HasFlag(MoveSourceType.LevelUp)) + { + var learn = GetLearnset(evo.Species, evo.Form); + var level = learn.GetLevelLearnMove(move); + if (level != -1) + return new(LevelUp, Game, (byte)level); + } + + if (types.HasFlag(MoveSourceType.SharedEggMove) && GetIsSharedEggMove(pi, move)) + return new(Shared, Game); + + if (types.HasFlag(MoveSourceType.Machine) && pi.GetIsLearnTM(TMHM_BDSP.IndexOf(move))) + return new(TMHM, Game); + + return default; + } } diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8PLA.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8PLA.cs index e852e9f07..022f55706 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8PLA.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8PLA.cs @@ -8,7 +8,7 @@ namespace PKHeX.Core; /// /// Exposes information about how moves are learned in . /// -public sealed class LearnSource8LA : ILearnSource +public sealed class LearnSource8LA : ILearnSource, IHomeSource { public static readonly LearnSource8LA Instance = new(); private static readonly PersonalTable8LA Personal = PersonalTable.LA; @@ -55,11 +55,11 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo8LA pi, EvoCriteria evo, us private static bool GetIsEnhancedTutor(EvoCriteria evo, ISpeciesForm current, ushort move, LearnOption option) => evo.Species is (int)Species.Rotom && move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }; @@ -86,4 +86,27 @@ public void GetAllMoves(Span result, PKM pk, EvoCriteria evo, MoveSourceTy result[MoveTutor.GetRotomFormMove(evo.Form)] = true; } } + + public LearnEnvironment Environment => Game; + public MoveLearnInfo GetCanLearnHOME(PKM pk, EvoCriteria evo, ushort move, MoveSourceType types = MoveSourceType.All) + { + if (!TryGetPersonal(evo.Species, evo.Form, out var pi)) + return default; + + if (types.HasFlag(MoveSourceType.LevelUp)) + { + var learn = GetLearnset(evo.Species, evo.Form); + var level = learn.GetLevelLearnMove(move); + if (level != -1) + return new(LevelUp, Game, (byte)level); + } + + if (types.HasFlag(MoveSourceType.Machine) && pi.GetIsLearnMoveShop(move)) + return new(TMHM, Game); + + if (types.HasFlag(MoveSourceType.EnhancedTutor) && GetIsEnhancedTutor(evo, pk, move, LearnOption.HOME)) + return new(Tutor, Game); + + return default; + } } diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8SWSH.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8SWSH.cs index 89a3facdc..11060ede7 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8SWSH.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource8SWSH.cs @@ -8,7 +8,7 @@ namespace PKHeX.Core; /// /// Exposes information about how moves are learned in . /// -public sealed class LearnSource8SWSH : ILearnSource, IEggSource +public sealed class LearnSource8SWSH : ILearnSource, IEggSource, IHomeSource { public static readonly LearnSource8SWSH Instance = new(); private static readonly PersonalTable8SWSH Personal = PersonalTable.SWSH; @@ -78,17 +78,17 @@ public MoveLearnInfo GetCanLearn(PKM pk, PersonalInfo8SWSH pi, EvoCriteria evo, { (int)Species.Necrozma => move switch { - (int)Move.SunsteelStrike => (option == LearnOption.AtAnyTime || current.Form == 1), // Sun w/ Solgaleo - (int)Move.MoongeistBeam => (option == LearnOption.AtAnyTime || current.Form == 2), // Moon w/ Lunala + (int)Move.SunsteelStrike => option.IsPast() || current.Form == 1, // Sun w/ Solgaleo + (int)Move.MoongeistBeam => option.IsPast() || current.Form == 2, // Moon w/ Lunala _ => false, }, (int)Species.Rotom => move switch { - (int)Move.Overheat => option == LearnOption.AtAnyTime || current.Form == 1, - (int)Move.HydroPump => option == LearnOption.AtAnyTime || current.Form == 2, - (int)Move.Blizzard => option == LearnOption.AtAnyTime || current.Form == 3, - (int)Move.AirSlash => option == LearnOption.AtAnyTime || current.Form == 4, - (int)Move.LeafStorm => option == LearnOption.AtAnyTime || current.Form == 5, + (int)Move.Overheat => option.IsPast() || current.Form == 1, + (int)Move.HydroPump => option.IsPast() || current.Form == 2, + (int)Move.Blizzard => option.IsPast() || current.Form == 3, + (int)Move.AirSlash => option.IsPast() || current.Form == 4, + (int)Move.LeafStorm => option.IsPast() || current.Form == 5, _ => false, }, _ => false, @@ -103,20 +103,25 @@ private bool GetIsSharedEggMove(PersonalInfo8SWSH pi, ushort move) private static bool GetIsTR(PersonalInfo8SWSH info, PKM pk, EvoCriteria evo, ushort move, LearnOption option) { - if (pk is not ITechRecord tr) - return false; - var index = info.RecordPermitIndexes.IndexOf(move); if (index == -1) return false; if (!info.GetIsLearnTR(index)) return false; - if (tr.GetMoveRecordFlag(index)) - return true; + if (pk is PK8 pk8) + { + if (pk8.GetMoveRecordFlag(index)) + return true; + if (!option.IsFlagCheckRequired()) + return true; + } + else + { + if (option != LearnOption.Current) + return true; + } - if (option != LearnOption.Current && !pk.SWSH && pk.IsOriginalMovesetDeleted()) - return true; if (index == 12 && evo is { Species: (int)Species.Calyrex, Form: 0 }) // TR12 return true; // Agility Calyrex without TR glitch. @@ -170,4 +175,30 @@ public void GetAllMoves(Span result, PKM pk, EvoCriteria evo, MoveSourceTy result[(int)Move.MoongeistBeam] = true; } } + + public LearnEnvironment Environment => Game; + public MoveLearnInfo GetCanLearnHOME(PKM pk, EvoCriteria evo, ushort move, MoveSourceType types = MoveSourceType.All) + { + if (!TryGetPersonal(evo.Species, evo.Form, out var pi)) + return default; + + if (types.HasFlag(MoveSourceType.LevelUp)) + { + var learn = GetLearnset(evo.Species, evo.Form); + var level = learn.GetLevelLearnMove(move); + if (level != -1) + return new(LevelUp, Game, (byte)level); + } + + if (types.HasFlag(MoveSourceType.SharedEggMove) && GetIsSharedEggMove(pi, move)) + return new(Shared, Game); + + if (types.HasFlag(MoveSourceType.Machine) && pi.GetIsLearnTM(move)) + return new(TMHM, Game); + + if (types.HasFlag(MoveSourceType.TechnicalRecord) && GetIsTR(pi, pk, evo, move, LearnOption.HOME)) + return new(TMHM, Game); + + return default; + } } diff --git a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource9SV.cs b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource9SV.cs index 68b5eb857..eb3ee3518 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/LearnSource9SV.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/LearnSource9SV.cs @@ -8,7 +8,7 @@ namespace PKHeX.Core; /// /// Exposes information about how moves are learned in . /// -public sealed class LearnSource9SV : ILearnSource, IEggSource, IReminderSource +public sealed class LearnSource9SV : ILearnSource, IEggSource, IReminderSource, IHomeSource { public static readonly LearnSource9SV Instance = new(); private static readonly PersonalTable9SV Personal = PersonalTable.SV; @@ -95,12 +95,19 @@ private static bool GetIsTM(PersonalInfo9SV info, PKM pk, ushort move, LearnOpti return false; if (!info.GetIsLearnTM(index)) return false; - if (pk is not ITechRecord tr) - return true; - if (tr.GetMoveRecordFlag(index)) - return true; - if (option != LearnOption.Current && !pk.SV && pk.IsOriginalMovesetDeleted()) - return true; + + if (pk is PK9 pk9) + { + if (pk9.GetMoveRecordFlag(index)) + return true; + if (!option.IsFlagCheckRequired()) + return true; + } + else + { + if (option != LearnOption.Current) + return true; + } return false; } @@ -164,4 +171,30 @@ public void GetAllMoves(Span result, PKM pk, EvoCriteria evo, MoveSourceTy result[MoveTutor.GetRotomFormMove(evo.Form)] = true; } } + + public LearnEnvironment Environment => Game; + + public MoveLearnInfo GetCanLearnHOME(PKM pk, EvoCriteria evo, ushort move, MoveSourceType types = MoveSourceType.All) + { + var pi = Personal[evo.Species, evo.Form]; + + if (types.HasFlag(MoveSourceType.LevelUp)) + { + var learn = GetLearnset(evo.Species, evo.Form); + var level = learn.GetLevelLearnMove(move); + if (level != -1) + return new(LevelUp, Game, (byte)level); + } + + if (types.HasFlag(MoveSourceType.SharedEggMove) && GetIsSharedEggMove(pi, move)) + return new(Shared, Game); + + if (types.HasFlag(MoveSourceType.Machine) && GetIsTM(pi, pk, move, LearnOption.HOME)) + return new(TMHM, Game); + + if (types.HasFlag(MoveSourceType.SpecialTutor) && GetIsReminderMove(evo.Species, evo.Form, move)) + return new(Tutor, Game); + + return default; + } } diff --git a/PKHeX.Core/Legality/LearnSource/Sources/Shared/LearnOption.cs b/PKHeX.Core/Legality/LearnSource/Sources/Shared/LearnOption.cs index 0178d7e5e..842a5b3cf 100644 --- a/PKHeX.Core/Legality/LearnSource/Sources/Shared/LearnOption.cs +++ b/PKHeX.Core/Legality/LearnSource/Sources/Shared/LearnOption.cs @@ -19,4 +19,20 @@ public enum LearnOption /// Evolution criteria is handled separately. /// AtAnyTime, + + /// + /// Checks with rules assuming the move was taught via HOME -- for sharing acquired movesets between games. + /// + /// + /// Relevant for HOME sharing sanity checks. + /// Required to be distinct in that the rules are different from the other two options. TR/TM flags aren't required if the move was learned via HOME. + /// + HOME, +} + +public static class LearnOptionExtensions +{ + public static bool IsCurrent(this LearnOption option) => option == LearnOption.Current; + public static bool IsPast(this LearnOption option) => option is LearnOption.AtAnyTime or LearnOption.HOME; + public static bool IsFlagCheckRequired(this LearnOption option) => option != LearnOption.HOME; } diff --git a/PKHeX.Core/Legality/Learnset/Learnset.cs b/PKHeX.Core/Legality/Learnset/Learnset.cs index e08bac28e..1f79c243e 100644 --- a/PKHeX.Core/Legality/Learnset/Learnset.cs +++ b/PKHeX.Core/Legality/Learnset/Learnset.cs @@ -242,7 +242,7 @@ public ReadOnlySpan GetBaseEggMoves(int level) { // Count moves <= level var count = 0; - foreach (ref var x in Levels.AsSpan()) + foreach (ref readonly var x in Levels.AsSpan()) { if (x > level) break; diff --git a/PKHeX.Core/Legality/MoveListSuggest.cs b/PKHeX.Core/Legality/MoveListSuggest.cs index 0d3200ad6..8fd662dcf 100644 --- a/PKHeX.Core/Legality/MoveListSuggest.cs +++ b/PKHeX.Core/Legality/MoveListSuggest.cs @@ -24,7 +24,7 @@ private static void GetSuggestedMoves(PKM pk, EvolutionHistory evoChains, MoveSo } // try to give current moves - if (enc.Generation <= 2) + if (enc.Generation <= 2 && pk.Format < 8) { var lvl = pk.Format >= 7 ? pk.Met_Level : pk.CurrentLevel; var source = GameData.GetLearnSource(enc.Version); @@ -32,9 +32,11 @@ private static void GetSuggestedMoves(PKM pk, EvolutionHistory evoChains, MoveSo return; } - if (pk.Species == enc.Species) + if (pk.Species == enc.Species || pk.Context.Generation() >= 8) { var game = (GameVersion)pk.Version; // account for SW/SH foreign mutated versions + if (pk.Context.Generation() >= 8) + game = pk.Context.GetSingleGameVersion(); var source = GameData.GetLearnSource(game); source.SetEncounterMoves(pk.Species, pk.Form, pk.CurrentLevel, moves); return; @@ -183,9 +185,19 @@ private static void GetSuggestedRelearnEgg(this IEncounterTemplate enc, ReadOnly // Try again with the other split-breed species if possible. var generator = EncounterGenerator.GetGenerator(enc.Version); - var tree = EvolutionTree.GetEvolutionTree(enc.Context); - var chain = tree.GetValidPreEvolutions(pk, 100, skipChecks: true, stopSpecies: enc.Species); - var other = generator.GetPossible(pk, chain, enc.Version, EncounterTypeGroup.Egg); + + Span chain = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions]; + var origin = new EvolutionOrigin(enc.Species, (byte)enc.Version, (byte)enc.Generation, 1, 100, true); + int count = EvolutionChain.GetOriginChain(chain, pk, origin); + for (int i = 0; i < count; i++) + { + if (chain[i].Species != enc.Species) + continue; + count = i; + break; + } + var evos = chain[..count].ToArray(); + var other = generator.GetPossible(pk, evos, enc.Version, EncounterTypeGroup.Egg); foreach (var incense in other) { if (incense.Species == enc.Species) diff --git a/PKHeX.Core/Legality/RNG/Algorithms/XorShift128.cs b/PKHeX.Core/Legality/RNG/Algorithms/XorShift128.cs index 63b23d468..341a4843f 100644 --- a/PKHeX.Core/Legality/RNG/Algorithms/XorShift128.cs +++ b/PKHeX.Core/Legality/RNG/Algorithms/XorShift128.cs @@ -51,9 +51,9 @@ public XorShift128(uint x, uint y, uint z, uint w) : this() public XorShift128(UInt128 state) => State = state; - public (uint x, uint y, uint z, uint w) GetState32() => (x, y, z, w); - public (ulong s0, ulong s1) GetState64() => (s0, s1); - public string FullState => $"{State:X32}"; + public readonly (uint x, uint y, uint z, uint w) GetState32() => (x, y, z, w); + public readonly (ulong s0, ulong s1) GetState64() => (s0, s1); + public readonly string FullState => $"{State:X32}"; /// /// Gets the next random . diff --git a/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus.cs b/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus.cs index 8b836343a..63b9873ae 100644 --- a/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus.cs +++ b/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus.cs @@ -17,8 +17,8 @@ namespace PKHeX.Core; private ulong s1; public Xoroshiro128Plus(ulong s0 = XOROSHIRO_CONST0, ulong s1 = XOROSHIRO_CONST) => (this.s0, this.s1) = (s0, s1); - public (ulong s0, ulong s1) GetState() => (s0, s1); - public UInt128 FullState() => new(s1, s0); + public readonly (ulong s0, ulong s1) GetState() => (s0, s1); + public readonly UInt128 FullState() => new(s1, s0); /// /// Gets the next random . diff --git a/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus8b.cs b/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus8b.cs index d1b41bea8..a0159c252 100644 --- a/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus8b.cs +++ b/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128Plus8b.cs @@ -16,8 +16,8 @@ namespace PKHeX.Core; private ulong s1; public Xoroshiro128Plus8b(ulong s0, ulong s1) => (this.s0, this.s1) = (s0, s1); - public (ulong s0, ulong s1) GetState() => (s0, s1); - public UInt128 FullState() => new(s1, s0); + public readonly (ulong s0, ulong s1) GetState() => (s0, s1); + public readonly UInt128 FullState() => new(s1, s0); public Xoroshiro128Plus8b(ulong seed) { diff --git a/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs b/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs index bb6604d07..e45beca97 100644 --- a/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs +++ b/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static PKHeX.Core.Legal; using static PKHeX.Core.GameVersion; @@ -14,47 +13,70 @@ namespace PKHeX.Core; /// internal static class GBRestrictions { - private static readonly HashSet Stadium_GiftSpecies = new() + private static bool IsStadiumGiftSpecies(byte species) => species switch { - (int)Bulbasaur, - (int)Charmander, - (int)Squirtle, - (int)Psyduck, - (int)Hitmonlee, - (int)Hitmonchan, - (int)Eevee, - (int)Omanyte, - (int)Kabuto, + (int)Bulbasaur => true, + (int)Charmander => true, + (int)Squirtle => true, + (int)Psyduck => true, + (int)Hitmonlee => true, + (int)Hitmonchan => true, + (int)Eevee => true, + (int)Omanyte => true, + (int)Kabuto => true, + _ => false, }; - /// - /// Checks if the type matches any of the type IDs extracted from the Personal Table used for R/G/B/Y games. - /// - /// Valid values: 0, 1, 2, 3, 4, 5, 7, 8, 20, 21, 22, 23, 24, 25, 26 - internal static bool TypeIDExists(byte type) => type < 32 && (0b111111100000000000110111111 & (1 << type)) != 0; - /// /// Species that have a catch rate value that is different from their pre-evolutions, and cannot be obtained directly. /// - internal static readonly HashSet Species_NotAvailable_CatchRate = new() + internal static bool IsSpeciesNotAvailableCatchRate(byte species) => species switch { - (int)Butterfree, - (int)Pidgeot, - (int)Nidoqueen, - (int)Nidoking, - (int)Ninetales, - (int)Vileplume, - (int)Persian, - (int)Arcanine, - (int)Poliwrath, - (int)Alakazam, - (int)Machamp, - (int)Victreebel, - (int)Rapidash, - (int)Cloyster, - (int)Exeggutor, - (int)Starmie, - (int)Dragonite, + (int)Butterfree => true, + (int)Pidgeot => true, + (int)Nidoqueen => true, + (int)Nidoking => true, + (int)Ninetales => true, + (int)Vileplume => true, + (int)Persian => true, + (int)Arcanine => true, + (int)Poliwrath => true, + (int)Alakazam => true, + (int)Machamp => true, + (int)Victreebel => true, + (int)Rapidash => true, + (int)Cloyster => true, + (int)Exeggutor => true, + (int)Starmie => true, + (int)Dragonite => true, + _ => false, + }; + + private static bool IsEvolvedFromGen1Species(ushort species) => species switch + { + (int)Crobat => true, + (int)Bellossom => true, + (int)Politoed => true, + (int)Espeon => true, + (int)Umbreon => true, + (int)Slowking => true, + (int)Steelix => true, + (int)Scizor => true, + (int)Kingdra => true, + (int)Porygon2 => true, + (int)Blissey => true, + (int)Magnezone => true, + (int)Lickilicky => true, + (int)Rhyperior => true, + (int)Tangrowth => true, + (int)Electivire => true, + (int)Magmortar => true, + (int)Leafeon => true, + (int)Glaceon => true, + (int)PorygonZ => true, + (int)Sylveon => true, + (int)Kleavor => true, + _ => false, }; internal static bool IsTradeEvolution1(ushort species) => species is (int)Kadabra or (int)Machoke or (int)Graveler or (int)Haunter; @@ -74,25 +96,26 @@ public static bool RateMatchesEncounter(ushort species, GameVersion version, byt private static bool GetCatchRateMatchesPreEvolution(PK1 pk, byte catch_rate) { // For species catch rate, discard any species that has no valid encounters and a different catch rate than their pre-evolutions - var table = EvolutionTree.Evolves1; - var chain = table.GetValidPreEvolutions(pk, levelMax: (byte)pk.CurrentLevel); - foreach (var entry in chain) + ISpeciesForm head = pk; + byte max = (byte)pk.CurrentLevel; + while (true) { - var s = entry.Species; - if (Species_NotAvailable_CatchRate.Contains((byte)s)) - continue; - if (catch_rate == PersonalTable.RB[s].CatchRate || catch_rate == PersonalTable.Y[s].CatchRate) - return true; + var s = head.Species; + if (!IsSpeciesNotAvailableCatchRate((byte)s)) + { + if (catch_rate == PersonalTable.RB[s].CatchRate || catch_rate == PersonalTable.Y[s].CatchRate) + return true; + } + + if (!EvolutionTree.Evolves1.Reverse.TryDevolve(head, pk, max, 2, false, out var next)) + break; + head = next; + max = next.LevelMax; } - // Krabby encounter trade special catch rate - ushort species = pk.Species; - if (catch_rate == 204 && (species is (int)Krabby or (int)Kingler)) + // Account for oddities via special catch rate encounters + if (catch_rate is 167 or 168 && IsStadiumGiftSpecies((byte)pk.Species)) return true; - - if (catch_rate is (167 or 168) && Stadium_GiftSpecies.Contains((byte)species)) - return true; - return false; } @@ -108,7 +131,7 @@ private static bool CanInhabitGen1(this PKM pk) return false; // Gen2 format with met data can't receive Gen1 moves, unless Stadium 2 is used (Oak's PC). - // If you put a Pokemon in the N64 box, the met info is retained, even if you switch over to a Gen I game to teach it TMs + // If you put a Pokemon in the N64 box, the met info is retained, even if you switch over to a Gen1 game to teach it TMs // You can use rare candies from within the lab, so level-up moves from RBY context can be learned this way as well // Stadium 2 is GB Cart Era only (not 3DS Virtual Console). if (pk is ICaughtData2 {CaughtData: not 0} && !ParseSettings.AllowGBCartEra) @@ -118,7 +141,7 @@ private static bool CanInhabitGen1(this PKM pk) ushort species = pk.Species; if (species <= MaxSpeciesID_1) return true; - return EvolutionLegality.FutureEvolutionsGen1.Contains(species); + return IsEvolvedFromGen1Species(species); } /// diff --git a/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext.cs b/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext.cs index e962917ec..2f41e99c9 100644 --- a/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext.cs +++ b/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext.cs @@ -4,6 +4,7 @@ namespace PKHeX.Core; public abstract class MemoryContext { + public abstract EntityContext Context { get; } public abstract IEnumerable GetMemoryItemParams(); public abstract bool CanUseItemGeneric(int item); diff --git a/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext6.cs b/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext6.cs index 9529128bb..465f304c0 100644 --- a/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext6.cs +++ b/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext6.cs @@ -11,6 +11,8 @@ public sealed partial class MemoryContext6 : MemoryContext public static readonly MemoryContext6 Instance = new(); private MemoryContext6() { } + public override EntityContext Context => EntityContext.Gen6; + private static ReadOnlySpan GetPokeCenterLocations(GameVersion game) { return GameVersion.XY.Contains(game) ? LocationsWithPokeCenter_XY : LocationsWithPokeCenter_AO; diff --git a/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext8.cs b/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext8.cs index c63bf2338..997cb9a3e 100644 --- a/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext8.cs +++ b/PKHeX.Core/Legality/Restrictions/Memories/MemoryContext8.cs @@ -10,6 +10,8 @@ public sealed partial class MemoryContext8 : MemoryContext public static readonly MemoryContext8 Instance = new(); private MemoryContext8() { } + public override EntityContext Context => EntityContext.Gen8; + public override IEnumerable GetMemoryItemParams() { var hashSet = new HashSet(Legal.HeldItems_SWSH); diff --git a/PKHeX.Core/Legality/Restrictions/Memories/MemoryRules.cs b/PKHeX.Core/Legality/Restrictions/Memories/MemoryRules.cs new file mode 100644 index 000000000..f19b02d78 --- /dev/null +++ b/PKHeX.Core/Legality/Restrictions/Memories/MemoryRules.cs @@ -0,0 +1,70 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Rules for memory mutation across games. +/// +public static class MemoryRules +{ + /// + /// Get the possible sources of memories for a given . + /// + /// A flag. + public static MemorySource GetPossibleSources(EvolutionHistory history) + { + var sources = MemorySource.None; + if (history.HasVisitedGen6) + sources |= MemorySource.Gen6 | MemorySource.Bank; + if (history.HasVisitedGen7) + sources |= MemorySource.Bank; // Trade encounters from Gen7 also come with hardcoded memories. + if (history.HasVisitedSWSH) + sources |= MemorySource.Gen8; + if (history.HasVisitedGen9) + sources |= MemorySource.Deleted; + return sources; + } + + /// + /// Revise the possible sources of memories for a given and flag. + /// + /// Entity to check. + /// Possible sources of memories. + /// Encounter matched to. + /// Revised flag. + public static MemorySource ReviseSourcesHandler(PKM pk, MemorySource sources, IEncounterTemplate enc) + { + // No HT Name => no HT Memory + if (pk.IsUntraded) + { + // Traded eggs in SW/SH set HT memory but not HT Name. + if (enc is { Context: EntityContext.Gen8, EggEncounter: true } && pk.Context is EntityContext.Gen8) + { + var loc = pk.IsEgg ? pk.Met_Location : pk.Egg_Location; + if (loc == Locations.LinkTrade6) + return sources; // OK + } + + return MemorySource.None; + } + + // Any Gen6 or Bank specific memory on Switch must have no HT language or else it would be replaced/erased. + if (pk is IHandlerLanguage { HT_Language: not 0 }) // Gen8+ Memory Required + sources &= ~(MemorySource.Gen6 | MemorySource.Bank); + + return sources; + } +} + +/// +/// Possible sources of memories. +/// +[Flags] +public enum MemorySource +{ + None, + Gen6 = 1 << 0, + Bank = 1 << 1, + Gen8 = 1 << 2, + Deleted = 1 << 3, +} diff --git a/PKHeX.Core/Legality/Verifiers/AwakenedValueVerifier.cs b/PKHeX.Core/Legality/Verifiers/AwakenedValueVerifier.cs index f6f02228c..b8407d57d 100644 --- a/PKHeX.Core/Legality/Verifiers/AwakenedValueVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/AwakenedValueVerifier.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.LegalityCheckStrings; namespace PKHeX.Core; @@ -13,10 +14,10 @@ public override void Verify(LegalityAnalysis data) int sum = pb7.EVTotal; if (sum != 0) - data.AddLine(GetInvalid(LegalityCheckStrings.LEffortShouldBeZero)); + data.AddLine(GetInvalid(LEffortShouldBeZero)); if (!pb7.AwakeningAllValid()) - data.AddLine(GetInvalid(LegalityCheckStrings.LAwakenedCap)); + data.AddLine(GetInvalid(LAwakenedCap)); Span required = stackalloc byte[6]; AwakeningUtil.SetExpectedMinimumAVs(required, pb7); @@ -25,16 +26,16 @@ public override void Verify(LegalityAnalysis data) // For each index of current, the value should be >= the required value. if (current[0] < required[0]) - data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LAwakenedShouldBeValue, required[0], nameof(IAwakened.AV_HP)))); + data.AddLine(GetInvalid(string.Format(LAwakenedShouldBeValue, required[0], nameof(IAwakened.AV_HP)))); if (current[1] < required[1]) - data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LAwakenedShouldBeValue, required[1], nameof(IAwakened.AV_ATK)))); + data.AddLine(GetInvalid(string.Format(LAwakenedShouldBeValue, required[1], nameof(IAwakened.AV_ATK)))); if (current[2] < required[2]) - data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LAwakenedShouldBeValue, required[2], nameof(IAwakened.AV_DEF)))); + data.AddLine(GetInvalid(string.Format(LAwakenedShouldBeValue, required[2], nameof(IAwakened.AV_DEF)))); if (current[3] < required[3]) - data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LAwakenedShouldBeValue, required[3], nameof(IAwakened.AV_SPA)))); + data.AddLine(GetInvalid(string.Format(LAwakenedShouldBeValue, required[3], nameof(IAwakened.AV_SPA)))); if (current[4] < required[4]) - data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LAwakenedShouldBeValue, required[4], nameof(IAwakened.AV_SPD)))); + data.AddLine(GetInvalid(string.Format(LAwakenedShouldBeValue, required[4], nameof(IAwakened.AV_SPD)))); if (current[5] < required[5]) - data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LAwakenedShouldBeValue, required[5], nameof(IAwakened.AV_SPE)))); + data.AddLine(GetInvalid(string.Format(LAwakenedShouldBeValue, required[5], nameof(IAwakened.AV_SPE)))); } } diff --git a/PKHeX.Core/Legality/Verifiers/FormArgumentVerifier.cs b/PKHeX.Core/Legality/Verifiers/FormArgumentVerifier.cs new file mode 100644 index 000000000..358e8c197 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/FormArgumentVerifier.cs @@ -0,0 +1,186 @@ +using static PKHeX.Core.LegalityCheckStrings; +using static PKHeX.Core.Species; + +namespace PKHeX.Core; + +public sealed class FormArgumentVerifier : Verifier +{ + protected override CheckIdentifier Identifier => CheckIdentifier.Form; + + public override void Verify(LegalityAnalysis data) + { + var pk = data.Entity; + if (pk is not IFormArgument f) + return; + + var result = VerifyFormArgument(data, f); + data.AddLine(result); + } + + private CheckResult VerifyFormArgument(LegalityAnalysis data, IFormArgument f) + { + var pk = data.Entity; + var enc = data.EncounterMatch; + var arg = f.FormArgument; + + var unusedMask = pk.Format == 6 ? 0xFFFF_FF00 : 0xFF00_0000; + if ((arg & unusedMask) != 0) + return GetInvalid(LFormArgumentHigh); + + return (Species)pk.Species switch + { + // Transfer Edge Cases -- Bank wipes the form but keeps old FormArgument value. + Furfrou when pk is { Context: EntityContext.Gen7, Form: 0 } && + ((enc.Generation == 6 && f.FormArgument <= byte.MaxValue) || IsFormArgumentDayCounterValid(f, 5, true)) + => GetValid(LFormArgumentValid), + + Furfrou when pk.Form != 0 => !IsFormArgumentDayCounterValid(f, 5, true) ? GetInvalid(LFormArgumentInvalid) : GetValid(LFormArgumentValid), + Hoopa when pk.Form == 1 => !IsFormArgumentDayCounterValid(f, 3) ? GetInvalid(LFormArgumentInvalid) : GetValid(LFormArgumentValid), + Yamask when pk.Form == 1 => arg switch + { + not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), + > 9_999 => GetInvalid(LFormArgumentHigh), + _ => GetValid(LFormArgumentValid), + }, + Basculin when pk.Form is 2 => arg switch + { + not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), + > 9_999 => GetInvalid(LFormArgumentHigh), + _ => GetValid(LFormArgumentValid), + }, + Qwilfish when pk.Form is 1 => arg switch + { + not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), + not 0 when pk.CurrentLevel < 25 => GetInvalid(LFormArgumentHigh), // Can't get requisite move + > 9_999 => GetInvalid(LFormArgumentHigh), + _ => GetValid(LFormArgumentValid), + }, + Stantler => arg switch + { + not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), + not 0 when pk.CurrentLevel < 31 => GetInvalid(LFormArgumentHigh), + > 9_999 => GetInvalid(LFormArgumentHigh), + _ => arg == 0 || HasVisitedPLA(data, Stantler) ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), + }, + Primeape => arg switch + { + > 9_999 => GetInvalid(LFormArgumentHigh), + _ => arg == 0 || HasVisitedSV(data, Primeape) ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), + }, + Bisharp => arg switch + { + > 9_999 => GetInvalid(LFormArgumentHigh), + _ => arg == 0 || HasVisitedSV(data, Bisharp) ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), + }, + Gimmighoul => arg switch + { + // Leveling up sets the save file's current coin count to the arg. If 999+, triggers level up. + // Without leveling up, cannot have a form arg value. + >= 999 => GetInvalid(LFormArgumentHigh), + 0 => GetValid(LFormArgumentValid), + _ => pk.CurrentLevel != pk.Met_Level ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), + }, + Runerigus => VerifyFormArgumentRange(enc.Species, Runerigus, arg, 49, 9999), + Alcremie => VerifyFormArgumentRange(enc.Species, Alcremie, arg, 0, (uint)AlcremieDecoration.Ribbon), + Wyrdeer => VerifyFormArgumentRange(enc.Species, Wyrdeer, arg, 20, 9999), + Basculegion => VerifyFormArgumentRange(enc.Species, Basculegion, arg, 294, 9999), + Overqwil => VerifyFormArgumentRange(enc.Species, Overqwil, arg, 20, 9999), + Annihilape => VerifyFormArgumentRange(enc.Species, Annihilape, arg, 20, 9999), + Kingambit => VerifyFormArgumentRange(enc.Species, Kingambit, arg, 3, 9999), + Gholdengo => VerifyFormArgumentRange(enc.Species, Gholdengo, arg, 999, 999), + Koraidon or Miraidon => enc switch + { + // Starter Legend has '1' when present in party, to differentiate. + // Cannot be traded to other games. + EncounterStatic9 { StarterBoxLegend: true } x when !(ParseSettings.ActiveTrainer is SAV9SV sv && sv.Version == x.Version) => GetInvalid(LTradeNotAvailable), + EncounterStatic9 { StarterBoxLegend: true } => arg switch + { + < 1 => GetInvalid(LFormArgumentLow), + 1 => data.SlotOrigin != SlotOrigin.Party ? GetInvalid(LFormParty) : GetValid(LFormArgumentValid), + > 1 => GetInvalid(LFormArgumentHigh), + }, + _ => arg switch + { + not 0 => GetInvalid(LFormArgumentNotAllowed), + _ => GetValid(LFormArgumentValid), + }, + }, + _ => VerifyFormArgumentNone(pk, f), + }; + } + + private static bool HasVisitedAs(EvoCriteria[] evos, Species species) => EvolutionHistory.HasVisited(evos, (ushort)species); + private static bool HasVisitedPLA(LegalityAnalysis data, Species species) => HasVisitedAs(data.Info.EvoChainsAllGens.Gen8a, species); + private static bool HasVisitedSV(LegalityAnalysis data, Species species) => HasVisitedAs(data.Info.EvoChainsAllGens.Gen9, species); + + private CheckResult VerifyFormArgumentRange(ushort encSpecies, Species check, uint value, uint min, uint max) + { + if (encSpecies == (ushort)check) + { + if (value == 0) + return GetValid(LFormArgumentValid); + return GetInvalid(LFormArgumentNotAllowed); + } + + if (value < min) + return GetInvalid(LFormArgumentLow); + if (value > max) + return GetInvalid(LFormArgumentHigh); + return GetValid(LFormArgumentValid); + } + + private CheckResult VerifyFormArgumentNone(PKM pk, IFormArgument f) + { + if (pk is not PK6 pk6) + { + if (f.FormArgument != 0) + { + if (pk is { Species: (int)Furfrou, Form: 0 } && (f.FormArgument & ~0xFF_00_00u) == 0) + return GetValid(LFormArgumentValid); + return GetInvalid(LFormArgumentNotAllowed); + } + return GetValid(LFormArgumentValid); + } + + if (f.FormArgument != 0) + { + if (pk is { Species: (int)Furfrou, Form: 0 } && (f.FormArgument & ~0xFFu) == 0) + return GetValid(LFormArgumentValid); + return GetInvalid(LFormArgumentNotAllowed); + } + + // Stored separately from main form argument value + if (pk6.FormArgumentRemain != 0) + return GetInvalid(LFormArgumentNotAllowed); + if (pk6.FormArgumentElapsed != 0) + return GetInvalid(LFormArgumentNotAllowed); + + return GetValid(LFormArgumentValid); + } + + private static bool IsFormArgumentDayCounterValid(IFormArgument f, uint maxSeed, bool canRefresh = false) + { + var remain = f.FormArgumentRemain; + var elapsed = f.FormArgumentElapsed; + var maxElapsed = f.FormArgumentMaximum; + if (canRefresh) + { + if (maxElapsed < elapsed) + return false; + + if (remain + elapsed < maxSeed) + return false; + } + else + { + if (maxElapsed != 0) + return false; + + if (remain + elapsed != maxSeed) + return false; + } + if (remain > maxSeed) + return false; + return remain != 0; + } +} diff --git a/PKHeX.Core/Legality/Verifiers/FormVerifier.cs b/PKHeX.Core/Legality/Verifiers/FormVerifier.cs index af3812fa2..93e406f8d 100644 --- a/PKHeX.Core/Legality/Verifiers/FormVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/FormVerifier.cs @@ -10,6 +10,7 @@ namespace PKHeX.Core; public sealed class FormVerifier : Verifier { protected override CheckIdentifier Identifier => CheckIdentifier.Form; + private static readonly FormArgumentVerifier FormArg = new(); public override void Verify(LegalityAnalysis data) { @@ -19,8 +20,7 @@ public override void Verify(LegalityAnalysis data) var result = VerifyForm(data); data.AddLine(result); - if (pk is IFormArgument f) - data.AddLine(VerifyFormArgument(data, f)); + FormArg.Verify(data); } private CheckResult VALID => GetValid(LFormValid); @@ -79,7 +79,7 @@ private CheckResult VerifyForm(LegalityAnalysis data) case Arceus: { - int arceus = GetArceusFormFromHeldItem(pk.HeldItem, pk.Format); + var arceus = GetArceusFormFromHeldItem(pk.HeldItem, pk.Format); return arceus != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem); } case Keldeo when enc.Generation != 5 || pk.Format >= 8: @@ -93,7 +93,7 @@ private CheckResult VerifyForm(LegalityAnalysis data) break; case Genesect: { - int genesect = GetGenesectFormFromHeldItem(pk.HeldItem); + var genesect = GetGenesectFormFromHeldItem(pk.HeldItem); return genesect != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem); } case Greninja: @@ -143,7 +143,7 @@ private CheckResult VerifyForm(LegalityAnalysis data) case Silvally: { - int silvally = GetSilvallyFormFromHeldItem(pk.HeldItem); + var silvally = GetSilvallyFormFromHeldItem(pk.HeldItem); return silvally != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem); } @@ -220,183 +220,4 @@ public static byte GetGenesectFormFromHeldItem(int item) return (byte)(item - 115); return 0; } - - private CheckResult VerifyFormArgument(LegalityAnalysis data, IFormArgument f) - { - var pk = data.Entity; - var enc = data.EncounterMatch; - var arg = f.FormArgument; - - var unusedMask = pk.Format == 6 ? 0xFFFF_FF00 : 0xFF00_0000; - if ((arg & unusedMask) != 0) - return GetInvalid(LFormArgumentHigh); - - return (Species)pk.Species switch - { - // Transfer Edge Cases -- Bank wipes the form but keeps old FormArgument value. - Furfrou when pk is { Context: EntityContext.Gen7, Form: 0 } && - ((enc.Generation == 6 && f.FormArgument <= byte.MaxValue) || IsFormArgumentDayCounterValid(f, 5, true)) - => GetValid(LFormArgumentValid), - - Furfrou when pk.Form != 0 => !IsFormArgumentDayCounterValid(f, 5, true) ? GetInvalid(LFormArgumentInvalid) : GetValid(LFormArgumentValid), - Hoopa when pk.Form == 1 => !IsFormArgumentDayCounterValid(f, 3) ? GetInvalid(LFormArgumentInvalid) : GetValid(LFormArgumentValid), - Yamask when pk.Form == 1 => arg switch - { - not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), - > 9_999 => GetInvalid(LFormArgumentHigh), - _ => GetValid(LFormArgumentValid), - }, - Basculin when pk.Form is 2 => arg switch - { - not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), - > 9_999 => GetInvalid(LFormArgumentHigh), - _ => GetValid(LFormArgumentValid), - }, - Qwilfish when pk.Form is 1 => arg switch - { - not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), - not 0 when pk.CurrentLevel < 25 => GetInvalid(LFormArgumentHigh), // Can't get requisite move - > 9_999 => GetInvalid(LFormArgumentHigh), - _ => GetValid(LFormArgumentValid), - }, - Stantler => arg switch - { - not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed), - not 0 when pk.CurrentLevel < 31 => GetInvalid(LFormArgumentHigh), - > 9_999 => GetInvalid(LFormArgumentHigh), - _ => arg == 0 || HasVisitedPLA(data, Stantler) ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), - }, - Primeape => arg switch - { - > 9_999 => GetInvalid(LFormArgumentHigh), - _ => arg == 0 || HasVisitedSV(data, Primeape) ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), - }, - Bisharp => arg switch - { - > 9_999 => GetInvalid(LFormArgumentHigh), - _ => arg == 0 || HasVisitedSV(data, Bisharp) ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), - }, - Gimmighoul => arg switch - { - // Leveling up sets the save file's current coin count to the arg. If 999+, triggers level up. - // Without leveling up, cannot have a form arg value. - >= 999 => GetInvalid(LFormArgumentHigh), - 0 => GetValid(LFormArgumentValid), - _ => pk.CurrentLevel != pk.Met_Level ? GetValid(LFormArgumentValid) : GetInvalid(LFormArgumentNotAllowed), - }, - Runerigus => VerifyFormArgumentRange(enc.Species, Runerigus, arg, 49, 9999), - Alcremie => VerifyFormArgumentRange(enc.Species, Alcremie, arg, 0, (uint)AlcremieDecoration.Ribbon), - Wyrdeer => VerifyFormArgumentRange(enc.Species, Wyrdeer, arg, 20, 9999), - Basculegion => VerifyFormArgumentRange(enc.Species, Basculegion, arg, 294, 9999), - Overqwil => VerifyFormArgumentRange(enc.Species, Overqwil, arg, 20, 9999), - Annihilape => VerifyFormArgumentRange(enc.Species, Annihilape, arg, 20, 9999), - Kingambit => VerifyFormArgumentRange(enc.Species, Kingambit, arg, 3, 9999), - Gholdengo => VerifyFormArgumentRange(enc.Species, Gholdengo, arg, 999, 999), - Koraidon or Miraidon => enc switch - { - // Starter Legend has '1' when present in party, to differentiate. - // Cannot be traded to other games. - EncounterStatic9 { StarterBoxLegend: true } x when !(ParseSettings.ActiveTrainer is SAV9SV sv && sv.Version == x.Version) => GetInvalid(LTradeNotAvailable), - EncounterStatic9 { StarterBoxLegend: true } => arg switch - { - < 1 => GetInvalid(LFormArgumentLow), - 1 => data.SlotOrigin != SlotOrigin.Party ? GetInvalid(LFormParty) : GetValid(LFormArgumentValid), - > 1 => GetInvalid(LFormArgumentHigh), - }, - _ => arg switch - { - not 0 => GetInvalid(LFormArgumentNotAllowed), - _ => GetValid(LFormArgumentValid), - }, - }, - _ => VerifyFormArgumentNone(pk, f), - }; - } - - private static bool HasVisitedPLA(LegalityAnalysis data, Species species) - { - var evos = data.Info.EvoChainsAllGens; - if (evos.HasVisited(EntityContext.Gen8a, (ushort)species)) - return true; - return false; - } - - private static bool HasVisitedSV(LegalityAnalysis data, Species species) - { - var evos = data.Info.EvoChainsAllGens; - if (evos.HasVisited(EntityContext.Gen9, (ushort)species)) - return true; - return false; - } - - private CheckResult VerifyFormArgumentRange(ushort encSpecies, Species check, uint value, uint min, uint max) - { - if (encSpecies == (ushort)check) - { - if (value == 0) - return GetValid(LFormArgumentValid); - return GetInvalid(LFormArgumentNotAllowed); - } - - if (value < min) - return GetInvalid(LFormArgumentLow); - if (value > max) - return GetInvalid(LFormArgumentHigh); - return GetValid(LFormArgumentValid); - } - - private CheckResult VerifyFormArgumentNone(PKM pk, IFormArgument f) - { - if (pk is not PK6 pk6) - { - if (f.FormArgument != 0) - { - if (pk is { Species: (int)Furfrou, Form: 0 } && (f.FormArgument & ~0xFF_00_00u) == 0) - return GetValid(LFormArgumentValid); - return GetInvalid(LFormArgumentNotAllowed); - } - return GetValid(LFormArgumentValid); - } - - if (f.FormArgument != 0) - { - if (pk is { Species: (int)Furfrou, Form: 0 } && (f.FormArgument & ~0xFFu) == 0) - return GetValid(LFormArgumentValid); - return GetInvalid(LFormArgumentNotAllowed); - } - - // Stored separately from main form argument value - if (pk6.FormArgumentRemain != 0) - return GetInvalid(LFormArgumentNotAllowed); - if (pk6.FormArgumentElapsed != 0) - return GetInvalid(LFormArgumentNotAllowed); - - return GetValid(LFormArgumentValid); - } - - private static bool IsFormArgumentDayCounterValid(IFormArgument f, uint maxSeed, bool canRefresh = false) - { - var remain = f.FormArgumentRemain; - var elapsed = f.FormArgumentElapsed; - var maxElapsed = f.FormArgumentMaximum; - if (canRefresh) - { - if (maxElapsed < elapsed) - return false; - - if (remain + elapsed < maxSeed) - return false; - } - else - { - if (maxElapsed != 0) - return false; - - if (remain + elapsed != maxSeed) - return false; - } - if (remain > maxSeed) - return false; - return remain != 0; - } } diff --git a/PKHeX.Core/Legality/Verifiers/HistoryVerifier.cs b/PKHeX.Core/Legality/Verifiers/HistoryVerifier.cs index 0b3f68b13..094f3413d 100644 --- a/PKHeX.Core/Legality/Verifiers/HistoryVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/HistoryVerifier.cs @@ -73,7 +73,7 @@ private void VerifyHandlerState(LegalityAnalysis data, bool neverOT) if (pk.HT_Name != tr.OT) data.AddLine(GetInvalid(LTransferHTMismatchName)); if (pk is IHandlerLanguage h && h.HT_Language != tr.Language) - data.AddLine(GetInvalid(LTransferHTMismatchLanguage)); + data.AddLine(Get(LTransferHTMismatchLanguage, Severity.Fishy)); } } @@ -192,34 +192,6 @@ private void VerifyHTMisc(LegalityAnalysis data) var htGender = pk.HT_Gender; if (htGender > 1 || (pk.IsUntraded && htGender != 0)) data.AddLine(GetInvalid(string.Format(LMemoryHTGender, htGender))); - - if (pk is IHandlerLanguage h) - VerifyHTLanguage(data, h, pk); - } - - private void VerifyHTLanguage(LegalityAnalysis data, IHandlerLanguage h, PKM pk) - { - var enc = data.EncounterOriginal; - if (enc is EncounterStatic9 { GiftWithLanguage: true }) - { - if (h.HT_Language == 0) - data.AddLine(GetInvalid(LMemoryHTLanguage)); - else if (pk.IsUntraded && h.HT_Language != pk.Language) - data.AddLine(GetInvalid(LMemoryHTLanguage)); - return; - } - - if (h.HT_Language == 0) - { - if (!string.IsNullOrWhiteSpace(pk.HT_Name)) - data.AddLine(GetInvalid(LMemoryHTLanguage)); - return; - } - - if (string.IsNullOrWhiteSpace(pk.HT_Name)) - data.AddLine(GetInvalid(LMemoryHTLanguage)); - else if (h.HT_Language > (int)LanguageID.ChineseT) - data.AddLine(GetInvalid(LMemoryHTLanguage)); } private void VerifyGeoLocationData(LegalityAnalysis data, IGeoTrack t, PKM pk) diff --git a/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs b/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs index a84cdf323..14999b327 100644 --- a/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs @@ -55,12 +55,12 @@ private static bool HasVisitedGoldBottleFlawless(EvolutionHistory evos) { // S/V gold bottle cap applies to all IVs regardless // LGP/E gold bottle cap applies to all IVs regardless - foreach (ref var x in evos.Gen9.AsSpan()) + foreach (ref readonly var x in evos.Gen9.AsSpan()) { if (x.LevelMax >= 50) return true; } - foreach (ref var x in evos.Gen7b.AsSpan()) + foreach (ref readonly var x in evos.Gen7b.AsSpan()) { if (x.LevelMax >= 100) return true; diff --git a/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs b/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs index fba295b25..85fd640ec 100644 --- a/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs @@ -81,7 +81,7 @@ private void VerifyAffixedRibbonMark(LegalityAnalysis data, IRibbonIndex m) return; var affix = (RibbonIndex)affixValue; - var max = MarkRules.GetMaxAffixValue(data.Entity.Format, m is IHomeTrack { HasTracker: true }); + var max = MarkRules.GetMaxAffixValue(data.Info.EvoChainsAllGens); if (affix > max) { data.AddLine(GetInvalid(string.Format(LRibbonMarkingAffixedF_0, GetRibbonNameSafe(affix)))); diff --git a/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs b/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs index cd0bd1e42..e23ff1edd 100644 --- a/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using static PKHeX.Core.LegalityCheckStrings; using static PKHeX.Core.MemoryPermissions; using static PKHeX.Core.EntityContext; @@ -16,31 +18,127 @@ public sealed class MemoryVerifier : Verifier public override void Verify(LegalityAnalysis data) { var pk = data.Entity; - if (ShouldHaveNoMemory(data, pk)) + var sources = MemoryRules.GetPossibleSources(data.Info.EvoChainsAllGens); + if (sources == MemorySource.None) { VerifyOTMemoryIs(data, 0, 0, 0, 0); VerifyHTMemoryNone(data, (ITrainerMemories)pk); return; } VerifyOTMemory(data); - VerifyHTMemory(data); + VerifyHTMemory(data, sources); } - private static bool ShouldHaveNoMemory(LegalityAnalysis data, PKM pk) + private void VerifyHTMemory(LegalityAnalysis data, MemorySource sources) { - if (pk.BDSP || pk.LA || pk.SV || pk is PK9) - return !data.Info.EvoChainsAllGens.HasVisitedSWSH; - return false; + var pk = data.Entity; + sources = MemoryRules.ReviseSourcesHandler(pk, sources, data.EncounterMatch); + if (sources == MemorySource.None) + { + VerifyHTMemoryNone(data, (ITrainerMemories)pk); + return; + } + VerifyHTMemoryContextVisited(data, sources); } - private CheckResult VerifyCommonMemory(PKM pk, int handler, EntityContext context, LegalInfo info, MemoryContext mem) + private void VerifyHTMemoryContextVisited(LegalityAnalysis data, MemorySource sources) + { + // Memories aren't reset when imported into formats, so it could be from any context visited. + var results = data.Info.Parse; + var start = results.Count; + if (sources.HasFlag(MemorySource.Gen6)) + { + results.RemoveRange(start, results.Count - start); + VerifyHTMemory(data, Gen6); + VerifyHTLanguage(data, MemorySource.Gen6); + if (ValidSet(results, start)) + return; + } + if (sources.HasFlag(MemorySource.Gen8)) + { + results.RemoveRange(start, results.Count - start); + VerifyHTMemory(data, Gen8); + VerifyHTLanguage(data, MemorySource.Gen8); + if (ValidSet(results, start)) + return; + } + if (sources.HasFlag(MemorySource.Bank)) + { + results.RemoveRange(start, results.Count - start); + VerifyHTMemoryTransferTo7(data, data.Entity, data.Info); + VerifyHTLanguage(data, MemorySource.Bank); + if (ValidSet(results, start)) + return; + } + if (sources.HasFlag(MemorySource.Deleted) ) + { + results.RemoveRange(start, results.Count - start); + VerifyHTMemoryNone(data, (ITrainerMemories)data.Entity); + VerifyHTLanguage(data, MemorySource.Deleted); + } + } + + private void VerifyHTLanguage(LegalityAnalysis data, MemorySource source) + { + var pk = data.Entity; + if (pk is not IHandlerLanguage h) + return; + if (!GetIsHTLanguageValid(data.EncounterMatch, pk, h.HT_Language, source)) + data.AddLine(GetInvalid(LMemoryHTLanguage)); + } + + private static bool GetIsHTLanguageValid(IEncounterTemplate enc, PKM pk, byte language, MemorySource source) + { + // Bounds check. + if (language > (int)LanguageID.ChineseT) + return false; + + // Gen6 and Bank don't have the HT language flag. + if (source is MemorySource.Gen6 or MemorySource.Bank) + return language == 0; + + // Some encounters erroneously set the HT flag. + if (enc is EncounterStatic9 { GiftWithLanguage: true }) + { + // Must be the SAV language or another-with-HT_Name. + if (language == 0) + return false; + if (pk.IsUntraded) + return language == pk.Language; + return true; + } + + if (pk.IsUntraded) + return language == 0; + + // Can be anything within bounds. + return true; + } + + private static bool ValidSet(List results, int start) + { + var count = results.Count - start; + if (count == 0) + return true; // None added. + + var span = CollectionsMarshal.AsSpan(results)[start..]; + foreach (var result in span) + { + if (result.Valid) + continue; + return false; + } + return true; + } + + private CheckResult VerifyCommonMemory(PKM pk, int handler, LegalInfo info, MemoryContext mem) { var memory = MemoryVariableSet.Read((ITrainerMemories)pk, handler); // Actionable HM moves int hmIndex = MemoryContext6.MoveSpecificMemoryHM.IndexOf(memory.MemoryID); if (hmIndex != -1) - return VerifyMemoryHM6(context, info, mem, memory, hmIndex); + return VerifyMemoryHM6(info, mem, memory, hmIndex); if (mem.IsInvalidGeneralLocationMemoryValue(memory.MemoryID, memory.Variable, info.EncounterMatch, pk)) return GetInvalid(string.Format(LMemoryArgBadLocation, memory.Handler)); @@ -55,15 +153,15 @@ private CheckResult VerifyCommonMemory(PKM pk, int handler, EntityContext contex return GetInvalid(string.Format(LMemoryArgBadLocation, memory.Handler)); // {0} saw {2} carrying {1} on its back. {4} that {3}. - case 21 when context != Gen6 || !PersonalTable.AO.GetFormEntry(memory.Variable, 0).GetIsLearnHM(2): // Fly + case 21 when mem.Context != Gen6 || !PersonalTable.AO.GetFormEntry(memory.Variable, 0).GetIsLearnHM(2): // Fly return BadSpeciesMove(memory.Handler); // {0} used {2} at {1}’s instruction, but it had no effect. {4} that {3}. // The Move Deleter that {0} met through {1} made it forget {2}. {4} that {3}. - case 16 or 48 when !CanKnowMove(pk, memory, context, info, memory.MemoryID == 16): + case 16 or 48 when !CanKnowMove(pk, memory, mem.Context, info, memory.MemoryID == 16): return BadSpeciesMove(memory.Handler); - case 49 when memory.Variable == 0 || !GetCanRelearnMove(pk, memory.Variable, context, info.EvoChainsAllGens, info.EncounterOriginal): + case 49 when memory.Variable == 0 || !GetCanRelearnMove(pk, memory.Variable, mem.Context, info.EvoChainsAllGens, info.EncounterOriginal): return BadSpeciesMove(memory.Handler); // Dynamaxing @@ -76,18 +174,18 @@ private CheckResult VerifyCommonMemory(PKM pk, int handler, EntityContext contex // Move // {0} studied about how to use {2} in a Box, thinking about {1}. {4} that {3}. // {0} practiced its cool pose for the move {2} in a Box, wishing to be praised by {1}. {4} that {3}. - case 80 or 81 when !CanKnowMove(pk, memory, context, info): + case 80 or 81 when !CanKnowMove(pk, memory, mem.Context, info): return BadSpeciesMove(memory.Handler); // Species // With {1}, {0} went fishing, and they caught {2}. {4} that {3}. - case 7 when !GetCanFishSpecies(memory.Variable, context, handler == 0 ? (GameVersion)pk.Version : GameVersion.Any): + case 7 when !GetCanFishSpecies(memory.Variable, mem.Context, handler == 0 ? (GameVersion)pk.Version : GameVersion.Any): return GetInvalid(string.Format(LMemoryArgBadSpecies, memory.Handler)); // {0} saw {1} paying attention to {2}. {4} that {3}. // {0} fought hard until it had to use Struggle when it battled at {1}’s side against {2}. {4} that {3}. // {0} was taken to a Pokémon Nursery by {1} and left with {2}. {4} that {3}. - case 9 or 60 or 75 when context == Gen8 && !PersonalTable.SWSH.IsSpeciesInGame(memory.Variable): + case 9 or 60 or 75 when mem.Context == Gen8 && !PersonalTable.SWSH.IsSpeciesInGame(memory.Variable): return GetInvalid(string.Format(LMemoryArgBadSpecies, memory.Handler)); // {0} had a great chat about {1} with the {2} that it was in a Box with. {4} that {3}. @@ -97,22 +195,22 @@ private CheckResult VerifyCommonMemory(PKM pk, int handler, EntityContext contex return GetInvalid(string.Format(LMemoryArgBadSpecies, memory.Handler)); // {0} had a very hard training session with {1}. {4} that {3}. - case 53 when context == Gen8 && pk is IHyperTrain t && !t.IsHyperTrained(): + case 53 when mem.Context == Gen8 && pk is IHyperTrain t && !t.IsHyperTrained(): return GetInvalid(string.Format(LMemoryArgBadID, memory.Handler)); // Item // {0} went to a Pokémon Center with {1} to buy {2}. {4} that {3}. - case 5 when !CanBuyItem(context, memory.Variable, handler == 0 ? (GameVersion)pk.Version : GameVersion.Any): + case 5 when !CanBuyItem(mem.Context, memory.Variable, handler == 0 ? (GameVersion)pk.Version : GameVersion.Any): // {1} used {2} when {0} was in trouble. {4} that {3}. - case 15 when !CanUseItem(context, memory.Variable, pk.Species): + case 15 when !CanUseItem(mem.Context, memory.Variable, pk.Species): // {0} saw {1} using {2}. {4} that {3}. - case 26 when !CanUseItemGeneric(context, memory.Variable): + case 26 when !CanUseItemGeneric(mem.Context, memory.Variable): // {0} planted {2} with {1} and imagined a big harvest. {4} that {3}. - case 34 when !CanPlantBerry(context, memory.Variable): + case 34 when !CanPlantBerry(mem.Context, memory.Variable): // {1} had {0} hold items like {2} to help it along. {4} that {3}. - case 40 when !CanHoldItem(context, memory.Variable): + case 40 when !CanHoldItem(mem.Context, memory.Variable): // {0} was excited when {1} won prizes like {2} through Loto-ID. {4} that {3}. - case 51 when !CanWinLotoID(context, memory.Variable): + case 51 when !CanWinLotoID(mem.Context, memory.Variable): // {0} was worried if {1} was looking for the {2} that it was holding in a Box. {4} that {3}. // When {0} was in a Box, it thought about the reason why {1} had it hold the {2}. {4} that {3}. case 84 or 88 when !Legal.HeldItems_SWSH.Contains(memory.Variable) || pk.IsEgg: @@ -122,9 +220,9 @@ private CheckResult VerifyCommonMemory(PKM pk, int handler, EntityContext contex return VerifyCommonMemoryEtc(memory, mem); } - private CheckResult VerifyMemoryHM6(EntityContext context, LegalInfo info, MemoryContext mem, MemoryVariableSet memory, int hmIndex) + private CheckResult VerifyMemoryHM6(LegalInfo info, MemoryContext mem, MemoryVariableSet memory, int hmIndex) { - if (context != Gen6) // Gen8 has no HMs, so this memory can never exist. + if (mem.Context != Gen6) // Gen8 has no HMs, so this memory can never exist. return BadSpeciesMove(memory.Handler); if (info.EncounterMatch.Species == (int)Species.Smeargle) @@ -133,7 +231,7 @@ private CheckResult VerifyMemoryHM6(EntityContext context, LegalInfo info, Memor // All AO hidden machine permissions are super-sets of Gen 3-5 games. // Don't need to check the move history -- a learned HM in a prior game can still be learned in Gen6. var pt = PersonalTable.AO; - foreach (ref var evo in info.EvoChainsAllGens.Gen6.AsSpan()) + foreach (ref readonly var evo in info.EvoChainsAllGens.Gen6.AsSpan()) { var entry = pt[evo.Species]; var canLearn = entry.GetIsLearnHM(hmIndex); @@ -190,9 +288,10 @@ private void VerifyHTMemoryNone(LegalityAnalysis data, ITrainerMemories pk) private void VerifyOTMemory(LegalityAnalysis data) { + var enc = data.Info.EncounterMatch; + var context = enc.Context; var pk = data.Entity; var mem = (ITrainerMemories)pk; - var Info = data.Info; // If the encounter has a memory from the OT that could never have it replaced, ensure it was not modified. switch (data.EncounterMatch) @@ -212,7 +311,6 @@ private void VerifyOTMemory(LegalityAnalysis data) return; } - var context = Info.EncounterOriginal.Context; var memory = mem.OT_Memory; if (pk.IsEgg) @@ -241,17 +339,17 @@ private void VerifyOTMemory(LegalityAnalysis data) { // No Memory case 0: // SW/SH trades don't set HT memories immediately, which is hilarious. - data.AddLine(Get(LMemoryMissingOT, context == Gen8 ? Severity.Fishy : Severity.Invalid)); + data.AddLine(Get(LMemoryMissingOT, mc.Context == Gen8 ? Severity.Fishy : Severity.Invalid)); VerifyOTMemoryIs(data, 0, 0, 0, 0); return; // {0} hatched from an Egg and saw {1} for the first time at... {2}. {4} that {3}. - case 2 when !Info.EncounterMatch.EggEncounter: + case 2 when !enc.EggEncounter: data.AddLine(GetInvalid(string.Format(LMemoryArgBadHatch, L_XOT))); break; // {0} became {1}’s friend when it arrived via Link Trade at... {2}. {4} that {3}. - case 4 when Info.Generation == 6: // gen8 applies this memory erroneously + case 4 when mc.Context == Gen6: // gen8 applies this memory erroneously data.AddLine(GetInvalid(string.Format(LMemoryArgBadOTEgg, L_XOT))); return; @@ -262,14 +360,14 @@ private void VerifyOTMemory(LegalityAnalysis data) // {0} was with {1} when {1} caught {2}. {4} that {3}. case 14: - var result = GetCanBeCaptured(mem.OT_TextVar, context, (GameVersion)pk.Version) // Any Game in the Handling Trainer's generation + var result = GetCanBeCaptured(mem.OT_TextVar, mc.Context, (GameVersion)pk.Version) // Any Game in the Handling Trainer's generation ? GetValid(string.Format(LMemoryArgSpecies, L_XOT)) : GetInvalid(string.Format(LMemoryArgBadSpecies, L_XOT)); data.AddLine(result); return; } - data.AddLine(VerifyCommonMemory(pk, 0, context, Info, mc)); + data.AddLine(VerifyCommonMemory(pk, 0, data.Info, mc)); } private static bool CanHaveMemoryForOT(PKM pk, EntityContext origin, int memory) @@ -291,11 +389,10 @@ private static bool CanHaveMemoryForOT(PKM pk, EntityContext origin, int memory) }; } - private void VerifyHTMemory(LegalityAnalysis data) + private void VerifyHTMemory(LegalityAnalysis data, EntityContext memoryGen) { var pk = data.Entity; var mem = (ITrainerMemories)pk; - var Info = data.Info; var memory = mem.HT_Memory; @@ -315,15 +412,13 @@ private void VerifyHTMemory(LegalityAnalysis data) if (pk.Format == 7) { - VerifyHTMemoryTransferTo7(data, pk, Info); + VerifyHTMemoryTransferTo7(data, pk, data.Info); return; } - var memoryGen = pk.Format >= 8 ? Gen8 : Gen6; - // Bounds checking - var context = Memories.GetContext(memoryGen); - if (!context.CanObtainMemoryHT((GameVersion)pk.Version, memory)) + var mc = Memories.GetContext(memoryGen); + if (!mc.CanObtainMemoryHT((GameVersion)pk.Version, memory)) data.AddLine(GetInvalid(string.Format(LMemoryArgBadID, L_XHT))); // Verify memory if specific to HT @@ -331,7 +426,7 @@ private void VerifyHTMemory(LegalityAnalysis data) { // No Memory case 0: // SW/SH memory application has an off-by-one error: [0,99] + 1 <= chance --> don't apply - var severity = memoryGen switch + var severity = mc.Context switch { Gen8 when pk is not PK8 && !pk.SWSH => Severity.Valid, Gen8 => ParseSettings.Gen8MemoryMissingHT, @@ -353,20 +448,20 @@ private void VerifyHTMemory(LegalityAnalysis data) return; // {0} went to the Pokémon Center in {2} with {1} and had its tired body healed there. {4} that {3}. - case 6 when !context.HasPokeCenter(GameVersion.Any, mem.HT_TextVar): + case 6 when !mc.HasPokeCenter(GameVersion.Any, mem.HT_TextVar): data.AddLine(GetInvalid(string.Format(LMemoryArgBadLocation, L_XHT))); return; // {0} was with {1} when {1} caught {2}. {4} that {3}. case 14: - var result = GetCanBeCaptured(mem.HT_TextVar, memoryGen, GameVersion.Any) // Any Game in the Handling Trainer's generation + var result = GetCanBeCaptured(mem.HT_TextVar, mc.Context, GameVersion.Any) // Any Game in the Handling Trainer's generation ? GetValid(string.Format(LMemoryArgSpecies, L_XHT)) : GetInvalid(string.Format(LMemoryArgBadSpecies, L_XHT)); data.AddLine(result); return; } - var commonResult = VerifyCommonMemory(pk, 1, memoryGen, Info, context); + var commonResult = VerifyCommonMemory(pk, 1, data.Info, mc); data.AddLine(commonResult); } diff --git a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs index b1ef53f94..8f369408a 100644 --- a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs @@ -133,6 +133,15 @@ public override void Verify(LegalityAnalysis data) VerifyMiscFatefulEncounter(data); VerifyMiscPokerus(data); + if (pk is IScaledSize3 s3 and IScaledSize s2 && IsHeightScaleMatchRequired(pk) && s2.HeightScalar != s3.Scale) + data.AddLine(GetInvalid(LStatIncorrectHeightValue)); + } + + private static bool IsHeightScaleMatchRequired(PKM pk) + { + if (pk is IHomeTrack { HasTracker: false }) + return false; + return true; } private void VerifySVStats(LegalityAnalysis data, PK9 pk9) @@ -142,8 +151,6 @@ private void VerifySVStats(LegalityAnalysis data, PK9 pk9) if (!pk9.IsBattleVersionValid(data.Info.EvoChainsAllGens)) data.AddLine(GetInvalid(LStatBattleVersionInvalid)); - if (pk9.Tracker != 0 && pk9.HeightScalar != pk9.Scale) - data.AddLine(GetInvalid(LStatInvalidHeightWeight)); if (!IsObedienceLevelValid(pk9, pk9.Obedience_Level, pk9.Met_Level)) data.AddLine(GetInvalid(LTransferObedienceLevel)); if (pk9.IsEgg && pk9.TeraTypeOverride != (MoveType)TeraTypeUtil.OverrideNone) @@ -160,8 +167,6 @@ private void VerifySVStats(LegalityAnalysis data, PK9 pk9) if (enc is EncounterEgg { Context: EntityContext.Gen9 } g) { - if (UnreleasedSV.Contains(g.Species | g.Form << 11)) - data.AddLine(GetInvalid(LTransferBad)); if (!Tera9RNG.IsMatchTeraTypePersonalEgg(g.Species, g.Form, (byte)pk9.TeraTypeOriginal)) data.AddLine(GetInvalid(LTeraTypeMismatch)); } @@ -174,7 +179,7 @@ private void VerifySVStats(LegalityAnalysis data, PK9 pk9) { if (pk9.TeraTypeOverride == (MoveType)TeraTypeUtil.OverrideNone) data.AddLine(GetInvalid(LTeraTypeIncorrect)); - else if (GetTeraImportMatch(data.Info.EvoChainsAllGens.Gen9, pk9.TeraTypeOriginal) == -1) + else if (GetTeraImportMatch(data.Info.EvoChainsAllGens.Gen9, pk9.TeraTypeOriginal, enc) == -1) data.AddLine(GetInvalid(LTeraTypeIncorrect)); } else if (enc is EncounterStatic9 { StarterBoxLegend: true }) @@ -185,7 +190,7 @@ private void VerifySVStats(LegalityAnalysis data, PK9 pk9) } } - public static int GetTeraImportMatch(ReadOnlySpan evos, MoveType actual) + public static int GetTeraImportMatch(ReadOnlySpan evos, MoveType actual, IEncounterTemplate enc) { // Sanitize out Form here for Arceus/Silvally -- rewrite via evotree later. if (evos.Length == 0 || evos[0].Species is (int)Species.Arceus or (int)Species.Silvally) @@ -193,10 +198,16 @@ public static int GetTeraImportMatch(ReadOnlySpan evos, MoveType ac for (int i = evos.Length - 1; i >= 0; i--) { var evo = evos[i]; - var pi = PersonalTable.SV.GetFormEntry(evo.Species, evo.Form); - var expect = TeraTypeUtil.GetTeraTypeImport(pi.Type1, pi.Type2); - if (expect == actual) - return i; + if (FormInfo.IsFormChangeable(evo.Species, enc.Form, evo.Form, enc.Context, EntityContext.Gen9)) + { + if (Tera9RNG.IsMatchTeraTypePersonalAnyForm(evo.Species, (byte)actual)) + return i; + } + else + { + if (Tera9RNG.IsMatchTeraTypePersonal(evo.Species, evo.Form, (byte)actual)) + return i; + } } return -1; } @@ -210,27 +221,6 @@ private static bool IsObedienceLevelValid(PKM pk, byte current, int expectObey) return current == expectObey; } - private static readonly HashSet UnreleasedSV = new() - { - // Silly workaround for evolution chain reversal not being iteratively implemented -- block cross-gen evolution cases - (int)Species.Raichu | (1 << 11), // Raichu-1 - (int)Species.Typhlosion | (1 << 11), // Typhlosion-1 - (int)Species.Samurott | (1 << 11), // Samurott-1 - (int)Species.Lilligant | (1 << 11), // Lilligant-1 - (int)Species.Braviary | (1 << 11), // Braviary-1 - (int)Species.Sliggoo | (1 << 11), // Sliggoo-1 - (int)Species.Avalugg | (1 << 11), // Avalugg-1 - (int)Species.Decidueye | (1 << 11), // Decidueye-1 - - (int)Species.Wyrdeer, // Wyrdeer - (int)Species.Kleavor, // Kleavor - (int)Species.Ursaluna, // Ursaluna - (int)Species.Basculegion, // Basculegion-0 - (int)Species.Basculegion | (1 << 11), // Basculegion-1 - (int)Species.Sneasler, // Sneasler - (int)Species.Overqwil, // Overqwil - }; - private void VerifyMiscPokerus(LegalityAnalysis data) { var pk = data.Entity; @@ -272,36 +262,34 @@ public void VerifyMiscG1(LegalityAnalysis data) private void VerifyMiscG1Types(LegalityAnalysis data, PK1 pk1) { - var Type_A = pk1.Type1; - var Type_B = pk1.Type2; var species = pk1.Species; if (species == (int)Species.Porygon) { // Can have any type combination of any species by using Conversion. - if (!GBRestrictions.TypeIDExists(Type_A)) + if (!PersonalTable1.TypeIDExists(pk1.Type1)) { data.AddLine(GetInvalid(LG1TypePorygonFail1)); } - if (!GBRestrictions.TypeIDExists(Type_B)) + if (!PersonalTable1.TypeIDExists(pk1.Type2)) { data.AddLine(GetInvalid(LG1TypePorygonFail2)); } else // Both types exist, ensure a Gen1 species has this combination { - var TypesAB_Match = PersonalTable.RB.IsValidTypeCombination(Type_A, Type_B); - var result = TypesAB_Match ? GetValid(LG1TypeMatchPorygon) : GetInvalid(LG1TypePorygonFail); + var matchSpecies = PersonalTable.RB.IsValidTypeCombination(pk1); + var result = matchSpecies != -1 ? GetValid(LG1TypeMatchPorygon) : GetInvalid(LG1TypePorygonFail); data.AddLine(result); } } else // Types must match species types { var pi = PersonalTable.RB[species]; - var Type_A_Match = Type_A == pi.Type1; - var Type_B_Match = Type_B == pi.Type2; + var (match1, match2) = pi.IsMatchType(pk1); + if (!match2 && ParseSettings.AllowGBCartEra) + match2 = (species is (int)Species.Magnemite or (int)Species.Magneton) && pk1.Type2 == 9; // Steel Magnemite via Stadium2 - var first = Type_A_Match ? GetValid(LG1TypeMatch1) : GetInvalid(LG1Type1Fail); - var second = Type_B_Match || (ParseSettings.AllowGBCartEra && ((species is (int)Species.Magnemite or (int)Species.Magneton) && Type_B == 9)) // Steel Magnemite via Stadium2 - ? GetValid(LG1TypeMatch2) : GetInvalid(LG1Type2Fail); + var first = match1 ? GetValid(LG1TypeMatch1) : GetInvalid(LG1Type1Fail); + var second = match2 ? GetValid(LG1TypeMatch2) : GetInvalid(LG1Type2Fail); data.AddLine(first); data.AddLine(second); } @@ -335,7 +323,7 @@ CheckResult GetWasNotTradeback(TimeCapsuleEvaluation timeCapsuleEvalution) return GetValid(LG1CatchRateMatchPrevious); // Encounters detected by the catch rate, cant be invalid if match this encounters ushort species = pk1.Species; - if (GBRestrictions.Species_NotAvailable_CatchRate.Contains((byte)species) && catch_rate == PersonalTable.RB[species].CatchRate) + if (GBRestrictions.IsSpeciesNotAvailableCatchRate((byte)species) && catch_rate == PersonalTable.RB[species].CatchRate) { if (species != (int) Species.Dragonite || catch_rate != 45 || !e.Version.Contains(GameVersion.YW)) return GetInvalid(LG1CatchRateEvo); @@ -671,12 +659,6 @@ private void VerifySWSHStats(LegalityAnalysis data, PK8 pk8) private void VerifyPLAStats(LegalityAnalysis data, PA8 pa8) { VerifyAbsoluteSizes(data, pa8); - if (!data.Info.EvoChainsAllGens.HasVisitedSWSH) - { - var affix = pa8.AffixedRibbon; - if (affix != -1) // None - data.AddLine(GetInvalid(string.Format(LRibbonMarkingAffixedF_0, affix))); - } var social = pa8.Sociability; if (social != 0) @@ -704,13 +686,6 @@ private void VerifyPLAStats(LegalityAnalysis data, PA8 pa8) private void VerifyBDSPStats(LegalityAnalysis data, PB8 pb8) { - if (!data.Info.EvoChainsAllGens.HasVisitedSWSH) - { - var affix = pb8.AffixedRibbon; - if (affix != -1) // None - data.AddLine(GetInvalid(string.Format(LRibbonMarkingAffixedF_0, affix))); - } - var social = pb8.Sociability; if (social != 0) data.AddLine(GetInvalid(LMemorySocialZero, Encounter)); diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs index 77cb6be3c..88edba4da 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs @@ -201,9 +201,12 @@ public static bool IsMarkPresentTitan(IEncounterTemplate enc) /// /// Gets the maximum obtainable value for the format. /// - public static RibbonIndex GetMaxAffixValue(int entityFormat, bool visitedHOME) => entityFormat switch + public static RibbonIndex GetMaxAffixValue(EvolutionHistory evos) { - <= 8 when !visitedHOME => MarkSlump, // Pioneer and Twinkling Star cannot be selected in SW/SH. - _ => MarkTitan, // Max ribbon visible in SV. - }; + if (evos.HasVisitedGen9) + return MarkTitan; + if (evos.HasVisitedSWSH) + return MarkSlump; // Pioneer and Twinkling Star cannot be selected in SW/SH. + return unchecked((RibbonIndex)(-1)); + } } diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs index e903aa6b4..903a76bc3 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs @@ -27,21 +27,31 @@ public static bool IsRibbonValidAlolaChamp(IRibbonSetCommon7 s7, IEncounterTempl /// /// Checks if the input can receive the ribbon. /// - public static bool IsRibbonValidEffort(PKM pk, EvolutionHistory evos, int gen) => gen switch + public static bool IsRibbonValidEffort(EvolutionHistory evos) => evos switch { - 5 when pk.Format == 5 => false, // Not available in BW/B2W2 - 8 when evos is { HasVisitedSWSH: false, HasVisitedBDSP: false } => false, // not available in PLA - _ => true, + { HasVisitedGen3: true } => true, + { HasVisitedGen4: true } => true, + // Not available in Gen5 + { HasVisitedGen6: true } => true, + { HasVisitedGen7: true } => true, + { HasVisitedSWSH: true } => true, + { HasVisitedBDSP: true } => true, + // Not available in PLA + { HasVisitedGen9: true } => true, + _ => false, }; /// /// Checks if the input can receive the ribbon. /// - public static bool IsRibbonValidBestFriends(PKM pk, EvolutionHistory evos, int gen) => gen switch + public static bool IsRibbonValidBestFriends(PKM pk, EvolutionHistory evos) => evos switch { - < 7 when pk is { IsUntraded: true } and IAffection { OT_Affection: < 255 } => false, // Gen6/7 uses affection. Can't lower it on OT! - 8 when evos is { HasVisitedSWSH: false, HasVisitedBDSP: false } => false, // Gen8+ replaced with Max Friendship. - _ => true, + { HasVisitedSWSH: true } => true, // Max Friendship + { HasVisitedBDSP: true } => true, // Max Friendship + { HasVisitedGen9: true } => true, // Max Friendship + + { HasVisitedGen7: true } when pk is not PK7 { IsUntraded: true, OT_Affection: < 255 } => true, + _ => false, }; /// @@ -82,7 +92,7 @@ public static bool IsRibbonValidMasterRank(PKM pk, IEncounterTemplate enc, Evolu if (evos.HasVisitedSWSH && IsRibbonValidMasterRankSWSH(pk, enc)) return true; - // Only Paldea natives can compete in Ranked. No Legendaries yet. + // Legendaries can not compete in ranked yet. if (evos.HasVisitedGen9 && IsRibbonValidMasterRankSV(pk)) return true; @@ -115,14 +125,13 @@ private static bool IsRibbonValidMasterRankSWSH(PKM pk, IEncounterTemplate enc) private static bool IsRibbonValidMasterRankSV(ISpeciesForm pk) { var species = pk.Species; - if (SpeciesCategory.IsMythical(species)) + if (species is (int)WalkingWake or (int)IronLeaves) return false; if (SpeciesCategory.IsLegendary(species)) return false; - - var pt = PersonalTable.SV; - var pi = pt.GetFormEntry(species, pk.Form); - return pi.IsInDex; // no foreign species, such as Charmander, Wooper-0, and Meowth-2 + if (SpeciesCategory.IsMythical(species)) + return false; + return true; } /// diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon3.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon3.cs index 7b2f29aae..f21c24a01 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon3.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon3.cs @@ -15,7 +15,7 @@ public static void Parse(this IRibbonSetCommon3 r, RibbonVerifierArguments args, list.Add(ChampionG3); if (r.RibbonArtist && !evos.HasVisitedGen3) list.Add(Artist); - if (r.RibbonEffort && !RibbonRules.IsRibbonValidEffort(pk, evos, args.Encounter.Generation)) + if (r.RibbonEffort && !RibbonRules.IsRibbonValidEffort(evos)) list.Add(Effort); } diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon6.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon6.cs index 38eddfd43..3ba13de1a 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon6.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon6.cs @@ -47,7 +47,7 @@ public static void Parse(this IRibbonSetCommon6 r, RibbonVerifierArguments args, list.Add(ContestStar, allContest); } - if (r.RibbonBestFriends && !RibbonRules.IsRibbonValidBestFriends(args.Entity, evos, args.Encounter.Generation)) + if (r.RibbonBestFriends && !RibbonRules.IsRibbonValidBestFriends(args.Entity, evos)) list.Add(BestFriends); if (!gen6) diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon8.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon8.cs index 3e40f924b..7bf3df3c0 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon8.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierCommon8.cs @@ -14,7 +14,7 @@ public static void Parse(this IRibbonSetCommon8 r, RibbonVerifierArguments args, list.Add(TowerMaster); var pk = args.Entity; - bool ranked = evos.HasVisitedSWSH || pk.SV; + bool ranked = evos.HasVisitedSWSH || evos.HasVisitedGen9; if (!evos.HasVisitedSWSH) { diff --git a/PKHeX.Core/MysteryGifts/WA8.cs b/PKHeX.Core/MysteryGifts/WA8.cs index 24237ebe4..67f56d913 100644 --- a/PKHeX.Core/MysteryGifts/WA8.cs +++ b/PKHeX.Core/MysteryGifts/WA8.cs @@ -661,24 +661,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) if (Form != evo.Form && !FormInfo.IsFormChangeable(Species, Form, pk.Form, Context, pk.Context)) return false; - if (IsEgg) - { - if (EggLocation != pk.Egg_Location) // traded - { - if (pk.Egg_Location != Locations.LinkTrade6) - return false; - if (PIDType == ShinyType8.Random && pk is { IsShiny: true, ShinyXor: > 1 }) - return false; // shiny traded egg will always have xor0/1. - } - if (!Shiny.IsValid(pk)) - { - return false; // can't be traded away for unshiny - } - - if (pk is { IsEgg: true, IsNative: false }) - return false; - } - else + // Never Egg { if (!Shiny.IsValid(pk)) return false; if (!IsMatchEggLocation(pk)) return false; diff --git a/PKHeX.Core/MysteryGifts/WB8.cs b/PKHeX.Core/MysteryGifts/WB8.cs index acbb05dc3..9f1965477 100644 --- a/PKHeX.Core/MysteryGifts/WB8.cs +++ b/PKHeX.Core/MysteryGifts/WB8.cs @@ -676,16 +676,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) { if (!Shiny.IsValid(pk)) return false; if (!IsMatchEggLocation(pk)) return false; - if (pk is PK8) - { - if (!LocationsHOME.IsValidMetBDSP((ushort)pk.Met_Location, pk.Version)) - return false; - } - else - { - if (MetLocation != pk.Met_Location) - return false; - } + if (!IsMatchLocation(pk)) return false; } if (MetLevel != 0 && MetLevel != pk.Met_Level) return false; @@ -709,6 +700,27 @@ protected override bool IsMatchEggLocation(PKM pk) return pk.Egg_Location == expect; } + private bool IsMatchLocation(PKM pk) + { + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetBDSP(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; + } + protected override bool IsMatchDeferred(PKM pk) => Species != pk.Species; protected override bool IsMatchPartial(PKM pk) => false; // no version compatibility checks yet. diff --git a/PKHeX.Core/MysteryGifts/WC3.cs b/PKHeX.Core/MysteryGifts/WC3.cs index 0e6d84c78..20d03d172 100644 --- a/PKHeX.Core/MysteryGifts/WC3.cs +++ b/PKHeX.Core/MysteryGifts/WC3.cs @@ -282,6 +282,8 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) return false; if (Level > pk.Met_Level) return false; + if (pk.Egg_Location != LocationEdits.GetNoneLocation(pk.Context)) + return false; } return true; } @@ -289,7 +291,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) private static bool GetIsValidOTMattleHoOh(ReadOnlySpan wc, ReadOnlySpan ot, bool ck3) { if (ck3) // match original if still ck3, otherwise must be truncated 7char - return wc == ot; + return wc.SequenceEqual(ot); return ot.Length == 7 && wc.StartsWith(ot, StringComparison.Ordinal); } diff --git a/PKHeX.Core/MysteryGifts/WC9.cs b/PKHeX.Core/MysteryGifts/WC9.cs index f2e0d1c0f..b1317854c 100644 --- a/PKHeX.Core/MysteryGifts/WC9.cs +++ b/PKHeX.Core/MysteryGifts/WC9.cs @@ -34,11 +34,10 @@ public enum GiftType : byte public bool CanBeReceivedByVersion(PKM pk) => RestrictVersion switch { 0 when !IsEntity => true, // Whatever, essentially unrestricted for SL/VL receipt. No Entity gifts are 0. - 1 => pk.Version is (int)GameVersion.SL || pk is PK8 { Met_Location: LocationsHOME.SWSL, Version: (int)GameVersion.SW }, - 2 => pk.Version is (int)GameVersion.VL || pk is PK8 { Met_Location: LocationsHOME.SHVL, Version: (int)GameVersion.SH }, - 3 => pk.Version is (int)GameVersion.SL || pk is PK8 { Met_Location: LocationsHOME.SWSL, Version: (int)GameVersion.SW } - || pk.Version is (int)GameVersion.VL || pk is PK8 { Met_Location: LocationsHOME.SHVL, Version: (int)GameVersion.SH }, - _ => throw new ArgumentOutOfRangeException(nameof(RestrictVersion), RestrictVersion, null), + 1 => pk.Version is (int)GameVersion.SL || pk.Met_Location == LocationsHOME.SWSL, + 2 => pk.Version is (int)GameVersion.VL || pk.Met_Location == LocationsHOME.SHVL, + 3 => pk.Version is (int)GameVersion.SL or (int)GameVersion.VL || pk.Met_Location is LocationsHOME.SWSL or LocationsHOME.SHVL, + _ => throw new ArgumentOutOfRangeException(nameof(RestrictVersion), RestrictVersion, null), }; // General Card Properties @@ -714,16 +713,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) { if (!shinyType.IsValid(pk)) return false; if (!IsMatchEggLocation(pk)) return false; - if (pk is PK8) - { - if (!LocationsHOME.IsValidMetSV((ushort)pk.Met_Location, pk.Version)) - return false; - } - else - { - if (MetLocation != pk.Met_Location) - return false; - } + if (!IsMatchLocation(pk)) return false; } if (MetLevel != 0 && MetLevel != pk.Met_Level) return false; @@ -758,6 +748,27 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo) return pk.PID == GetPID(pk, type); } + private bool IsMatchLocation(PKM pk) + { + var metState = LocationsHOME.GetRemapState(Context, pk.Context); + if (metState == LocationRemapState.Original) + return IsMatchLocationExact(pk); + if (metState == LocationRemapState.Remapped) + return IsMatchLocationRemapped(pk); + return IsMatchLocationExact(pk) || IsMatchLocationRemapped(pk); + } + + private bool IsMatchLocationExact(PKM pk) => pk.Met_Location == Location; + + private bool IsMatchLocationRemapped(PKM pk) + { + var met = (ushort)pk.Met_Location; + var version = pk.Version; + if (pk.Context == EntityContext.Gen8) + return LocationsHOME.IsValidMetSV(met, version); + return LocationsHOME.GetMetSWSH((ushort)Location, version) == met; + } + public bool IsDateRestricted => true; protected override bool IsMatchDeferred(PKM pk) => Species != pk.Species; diff --git a/PKHeX.Core/PKM/HOME/GameDataPA8.cs b/PKHeX.Core/PKM/HOME/GameDataPA8.cs index 9566b152c..bc956b696 100644 --- a/PKHeX.Core/PKM/HOME/GameDataPA8.cs +++ b/PKHeX.Core/PKM/HOME/GameDataPA8.cs @@ -185,7 +185,6 @@ public void InitializeFrom(IGameDataSide side, PKH pkh) AbilityNumber = 1; PopulateFromCore(pkh); - this.ResetMoves(pkh.Species, pkh.Form, pkh.CurrentLevel, LearnSource8LA.Instance, EntityContext.Gen8a); } private static int GetLegendBall(int ball) @@ -201,5 +200,8 @@ private void PopulateFromCore(PKH pkh) HeightAbsolute = PA8.GetHeightAbsolute(pi, pkh.HeightScalar); WeightAbsolute = PA8.GetWeightAbsolute(pi, pkh.HeightScalar, pkh.WeightScalar); Ability = (ushort)pi.GetAbilityAtIndex(AbilityNumber >> 1); + + var level = Experience.GetLevel(pkh.EXP, pi.EXPGrowth); + this.ResetMoves(pkh.Species, pkh.Form, level, LearnSource8LA.Instance, EntityContext.Gen8a); } } diff --git a/PKHeX.Core/PKM/HOME/GameDataPB8.cs b/PKHeX.Core/PKM/HOME/GameDataPB8.cs index 17cc6679f..170c4ba24 100644 --- a/PKHeX.Core/PKM/HOME/GameDataPB8.cs +++ b/PKHeX.Core/PKM/HOME/GameDataPB8.cs @@ -121,12 +121,14 @@ public void InitializeFrom(IGameDataSide side, PKH pkh) AbilityNumber = 1; PopulateFromCore(pkh); - this.ResetMoves(pkh.Species, pkh.Form, pkh.CurrentLevel, LearnSource8BDSP.Instance, EntityContext.Gen8b); } private void PopulateFromCore(PKH pkh) { var pi = PersonalTable.BDSP.GetFormEntry(pkh.Species, pkh.Form); Ability = (ushort)pi.GetAbilityAtIndex(AbilityNumber >> 1); + + var level = Experience.GetLevel(pkh.EXP, pi.EXPGrowth); + this.ResetMoves(pkh.Species, pkh.Form, level, LearnSource8BDSP.Instance, EntityContext.Gen8b); } } diff --git a/PKHeX.Core/PKM/HOME/GameDataPK9.cs b/PKHeX.Core/PKM/HOME/GameDataPK9.cs index 1e9b543e1..7762f5c4f 100644 --- a/PKHeX.Core/PKM/HOME/GameDataPK9.cs +++ b/PKHeX.Core/PKM/HOME/GameDataPK9.cs @@ -160,7 +160,6 @@ public void InitializeFrom(IGameDataSide side, PKH pkh) AbilityNumber = 1; PopulateFromCore(pkh); - this.ResetMoves(pkh.Species, pkh.Form, pkh.CurrentLevel, LearnSource9SV.Instance, EntityContext.Gen9); } private void PopulateFromCore(PKH pkh) @@ -170,5 +169,8 @@ private void PopulateFromCore(PKH pkh) var pi = PersonalTable.SV.GetFormEntry(pkh.Species, pkh.Form); Ability = (ushort)pi.GetAbilityAtIndex(AbilityNumber >> 1); TeraTypeOriginal = TeraTypeOverride = TeraTypeUtil.GetTeraTypeImport(pi.Type1, pi.Type2); + + var level = Experience.GetLevel(pkh.EXP, pi.EXPGrowth); + this.ResetMoves(pkh.Species, pkh.Form, level, LearnSource9SV.Instance, EntityContext.Gen9); } } diff --git a/PKHeX.Core/PKM/HOME/PKH.cs b/PKHeX.Core/PKM/HOME/PKH.cs index 41dfc0c23..3c164c32e 100644 --- a/PKHeX.Core/PKM/HOME/PKH.cs +++ b/PKHeX.Core/PKM/HOME/PKH.cs @@ -308,7 +308,13 @@ public override PKH Clone() => new((byte[])Data.Clone()) public IGameDataSide LatestGameData => OriginalGameData() ?? GetFallbackGameData(); - private IGameDataSide GetFallbackGameData() => Version switch + private IGameDataSide GetFallbackGameData() => DataPB7 + ?? DataPK9 + ?? DataPB8 + ?? DataPA8 + ?? DataPK8 ?? CreateFallback(); + + private IGameDataSide CreateFallback() => Version switch { (int)GP or (int)GE => DataPB7 ??= new(), (int)BD or (int)SP => DataPB8 ??= new(), @@ -319,6 +325,7 @@ public override PKH Clone() => new((byte[])Data.Clone()) private IGameDataSide? OriginalGameData() => Version switch { + (int)GameVersion.GO when DataPB7 is not null => DataPB7, (int)GP or (int)GE => DataPB7, (int)BD or (int)SP => DataPB8, (int)PLA => DataPA8, @@ -388,7 +395,13 @@ public void CopyFrom(PKM pk) private void EnsureScaleSizeExists() { - if (FirstScaleData is IScaledSize3) + if (Core.RibbonMarkAlpha) + { + // Fix for PLA static encounter Alphas with 127 scale. + Core.HeightScalar = Core.WeightScalar = 255; + return; + } + if (GO_HOME || FirstScaleData is IScaledSize3) return; // data exists for scale, keep values. while (HeightScalar == 0 && WeightScalar == 0) { diff --git a/PKHeX.Core/PKM/Interfaces/IBattleVersion.cs b/PKHeX.Core/PKM/Interfaces/IBattleVersion.cs index cb28218ac..4b8e26a71 100644 --- a/PKHeX.Core/PKM/Interfaces/IBattleVersion.cs +++ b/PKHeX.Core/PKM/Interfaces/IBattleVersion.cs @@ -21,7 +21,7 @@ public static class BattleVersionExtensions public static bool IsBattleVersionValid(this T pk, EvolutionHistory h) where T : PKM, IBattleVersion => pk.BattleVersion switch { 0 => true, - (int)GameVersion.SW or (int)GameVersion.SH => h.HasVisitedSWSH && !(pk.SWSH || pk.BDSP || pk.LA || pk.SV), + (int)GameVersion.SW or (int)GameVersion.SH => h.HasVisitedSWSH && LocationsHOME.GetVersionSWSH(pk.Version) is not ((int)GameVersion.SW or (int)GameVersion.SH), _ => false, }; diff --git a/PKHeX.Core/PKM/PK1.cs b/PKHeX.Core/PKM/PK1.cs index d85f830cd..7c9634234 100644 --- a/PKHeX.Core/PKM/PK1.cs +++ b/PKHeX.Core/PKM/PK1.cs @@ -81,9 +81,9 @@ public override PK1 Clone() public static bool IsCatchRateHeldItem(byte rate) => rate == 0 || Array.IndexOf(Legal.HeldItems_GSC, rate) >= 0; - private static bool IsCatchRatePreEvolutionRate(ushort baseSpecies, int finalSpecies, byte rate) + private static bool IsCatchRatePreEvolutionRate(int baseSpecies, int finalSpecies, byte rate) { - for (ushort species = baseSpecies; species <= finalSpecies; species++) + for (int species = baseSpecies; species <= finalSpecies; species++) { if (rate == PersonalTable.RB[species].CatchRate || rate == PersonalTable.Y[species].CatchRate) return true; @@ -118,9 +118,12 @@ private static bool IsValidCatchRateAnyPreEvo(byte species, byte rate) if (species == (int)Core.Species.Pikachu && rate == 0xA3) // Light Ball (starter) return true; - var table = EvolutionTree.Evolves1; - var baby = table.GetBaseSpeciesForm(species, 0); - return IsCatchRatePreEvolutionRate(baby.Species, species, rate); + // Get de-evolution steps we should check for. + var stage = PersonalInfo1.GetEvolutionStage(species); + // For Eevee-lutions, Eevee evolves to (134,135,136), which are (1,1,1). All 3 have the same catch rate as Eevee. + // In the event the current species is 135 or 136, we'd never check Eevee with this logic, but they're all 45. + var baby = species - stage; + return IsCatchRatePreEvolutionRate(baby, species, rate); } public override int Version { get => (int)GameVersion.RBY; set { } } diff --git a/PKHeX.Core/PKM/PK6.cs b/PKHeX.Core/PKM/PK6.cs index cbcdcd0e8..2dc3b17c1 100644 --- a/PKHeX.Core/PKM/PK6.cs +++ b/PKHeX.Core/PKM/PK6.cs @@ -506,7 +506,7 @@ public PK7 ConvertToPK7() span[0xAA..0xB0].Clear(); /* Unused/Amie Fullness & Enjoyment. */ span[0xE4..0xE8].Clear(); /* Unused. */ pk7.Data[0x72] &= 0xFC; /* Clear lower two bits of Super training flags. */ - pk7.Data[0xDE] = 0; /* Gen IV encounter type. */ + pk7.Data[0xDE] = 0; /* Gen4 encounter type. */ // Copy Form Argument data for Furfrou and Hoopa, since we're nice. pk7.FormArgumentRemain = FormArgumentRemain; diff --git a/PKHeX.Core/PKM/PK9.cs b/PKHeX.Core/PKM/PK9.cs index 674a69685..98a663fa2 100644 --- a/PKHeX.Core/PKM/PK9.cs +++ b/PKHeX.Core/PKM/PK9.cs @@ -590,6 +590,8 @@ private void TradeHT(ITrainerInfo tr) } CurrentHandler = 1; HT_Gender = tr.Gender; + if (HT_Language == 0) + this.ClearMemoriesHT(); HT_Language = (byte)tr.Language; } diff --git a/PKHeX.Core/PKM/Util/Conversion/FormConverter.cs b/PKHeX.Core/PKM/Util/Conversion/FormConverter.cs index 94a48b981..86f9dc0ef 100644 --- a/PKHeX.Core/PKM/Util/Conversion/FormConverter.cs +++ b/PKHeX.Core/PKM/Util/Conversion/FormConverter.cs @@ -702,7 +702,7 @@ private static string[] GetFormsArceus(ushort species, int generation, IReadOnly "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", - // "!", "?", not in Gen II + // "!", "?", not in Gen2 }, _ => new[] { @@ -878,31 +878,28 @@ private static string[] GetFormsGalarSlowbro(IReadOnlyList types, IReadO public static string[] GetAlcremieFormList(IReadOnlyList forms) { - var result = new string[63]; - // seed form0 with the pattern - result[0 * 7] = forms[(int)Alcremie]; // Vanilla Cream - result[1 * 7] = forms[RubyCream]; - result[2 * 7] = forms[MatchaCream]; - result[3 * 7] = forms[MintCream]; - result[4 * 7] = forms[LemonCream]; - result[5 * 7] = forms[SaltedCream]; - result[6 * 7] = forms[RubySwirl]; - result[7 * 7] = forms[CaramelSwirl]; - result[8 * 7] = forms[RainbowSwirl]; - const int deco = 7; const byte fc = 9; - for (byte f = 0; f < fc; f++) - { - int start = f * deco; - // iterate downwards using form0 as pattern ref, replacing on final loop - for (int i = deco - 1; i >= 0; i--) - { - result[start + i] = $"{result[start]} ({(AlcremieDecoration)i})"; - } - } + var result = new string[deco * fc]; // 63 + SetDecorations(result, 0, forms[(int)Alcremie]); // Vanilla Cream + SetDecorations(result, 1, forms[RubyCream]); + SetDecorations(result, 2, forms[MatchaCream]); + SetDecorations(result, 3, forms[MintCream]); + SetDecorations(result, 4, forms[LemonCream]); + SetDecorations(result, 5, forms[SaltedCream]); + SetDecorations(result, 6, forms[RubySwirl]); + SetDecorations(result, 7, forms[CaramelSwirl]); + SetDecorations(result, 8, forms[RainbowSwirl]); return result; + + static void SetDecorations(string[] result, int f, string baseName) + { + int start = f * deco; + var slice = result.AsSpan(start, deco); + for (int i = 0; i < slice.Length; i++) + slice[i] = $"{baseName} ({(AlcremieDecoration)i})"; + } } public static bool GetFormArgumentIsNamedIndex(ushort species) => species == (int)Alcremie; diff --git a/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs b/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs index 8f28eeb82..5f9fc3b49 100644 --- a/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs +++ b/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs @@ -25,7 +25,10 @@ public void Rejuvenate(PKM result, PKM original) // HOME transfers from PB8/PA8 => PK8 will sanitize Ball & Met/Egg Location. // Transferring back without a reference PB8/PA8, we need to guess the *original* values. if (original is not PK8 pk8) + { + ResetSideways(result); return; + } var ver = result.Version; if (ver is (int)GameVersion.BD or (int)GameVersion.SP) @@ -34,6 +37,21 @@ public void Rejuvenate(PKM result, PKM original) RejuvenatePLA(result, pk8); } + private static void ResetSideways(PKM result) + { + if (result is PA8 pa8) + { + // Won't work well for Alphas + if (pa8.RibbonMarkAlpha) + pa8.IsAlpha = true; + var la = new LegalityAnalysis(pa8); + var enc = la.EncounterMatch; + ResetDataPLA(la, enc, pa8); + if (pa8.LA) + ResetBallPLA(pa8, enc); + } + } + private static void RejuvenatePLA(PKM result, PK8 original) { var la = new LegalityAnalysis(original); @@ -51,18 +69,13 @@ private static void RejuvenatePLA(PKM result, PK8 original) la = new LegalityAnalysis(result); enc = la.EncounterOriginal; if (result is PA8 pa8) - { - Span relearn = stackalloc ushort[4]; - la.GetSuggestedRelearnMoves(relearn, enc); - if (relearn[0] != 0) - pa8.SetRelearnMoves(relearn); + ResetDataPLA(la, enc, pa8); - pa8.ClearMoveShopFlags(); - if (enc is IMasteryInitialMoveShop8 e) - e.SetInitialMastery(pa8); - pa8.SetMoveShopFlags(pa8); - } + ResetBallPLA(result, enc); + } + private static void ResetBallPLA(PKM result, IEncounterable enc) + { if (result.Ball is >= (int)Ball.LAPoke and <= (int)Ball.LAOrigin) return; if (enc is IFixedBall { FixedBall: not Ball.None } f) @@ -71,6 +84,19 @@ private static void RejuvenatePLA(PKM result, PK8 original) result.Ball = result.Species == (int)Species.Unown ? (int)Ball.LAJet : (int)Ball.LAPoke; } + private static void ResetDataPLA(LegalityAnalysis la, IEncounterable enc, PA8 pa8) + { + Span relearn = stackalloc ushort[4]; + la.GetSuggestedRelearnMoves(relearn, enc); + if (relearn[0] != 0) + pa8.SetRelearnMoves(relearn); + + pa8.ClearMoveShopFlags(); + if (enc is IMasteryInitialMoveShop8 e) + e.SetInitialMastery(pa8); + pa8.SetMoveShopFlags(pa8); + } + private static void RejuvenateBDSP(PKM result, PK8 original) { var la = new LegalityAnalysis(original); diff --git a/PKHeX.Core/PersonalInfo/Info/PersonalInfo1.cs b/PKHeX.Core/PersonalInfo/Info/PersonalInfo1.cs index a2392d086..59f228fef 100644 --- a/PKHeX.Core/PersonalInfo/Info/PersonalInfo1.cs +++ b/PKHeX.Core/PersonalInfo/Info/PersonalInfo1.cs @@ -31,7 +31,7 @@ public sealed class PersonalInfo1 : PersonalInfo, IPersonalInfoTM public byte Move4 { get => Data[0x12]; set => Data[0x12] = value; } public override byte EXPGrowth { get => Data[0x13]; set => Data[0x13] = value; } - // EV Yields are just aliases for base stats in Gen I + // EV Yields are just aliases for base stats in Gen1 public override int EV_HP { get => HP; set { } } public override int EV_ATK { get => ATK; set { } } public override int EV_DEF { get => DEF; set { } } @@ -89,4 +89,39 @@ public void SetAllLearnTM(Span result, ReadOnlySpan moves) result[moves[index]] = true; } } + + // 0-2 to indicate how many steps down to get the base species ID. + private static ReadOnlySpan EvoStages => new byte[] + { + 0, 0, 1, 2, 0, 1, 2, 0, 1, 2, + 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 2, 0, 1, 2, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 2, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, + 1, 2, 0, 1, 0, 1, 2, 0, 1, 0, + 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 2, 0, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, + 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, + 0, 1, 0, 0, 0, 0, 0, 0, 1, 2, + }; + + /// + /// Gets the amount of times a species has evolved from the base species. + /// + /// Current species + /// Baby species + public static int GetEvolutionStage(int species) + { + if ((uint)species >= EvoStages.Length) + return 0; + return EvoStages[species]; + } + + public (bool Match1, bool Match2) IsMatchType(IPersonalType other) => IsMatchType(other.Type1, other.Type2); + private (bool Match1, bool Match2) IsMatchType(byte type1, byte type2) => (type1 == Type1, type2 == Type2); } diff --git a/PKHeX.Core/PersonalInfo/Interfaces/IPersonalTable.cs b/PKHeX.Core/PersonalInfo/Interfaces/IPersonalTable.cs index 26b0b54db..4048e9ddb 100644 --- a/PKHeX.Core/PersonalInfo/Interfaces/IPersonalTable.cs +++ b/PKHeX.Core/PersonalInfo/Interfaces/IPersonalTable.cs @@ -8,7 +8,7 @@ public interface IPersonalTable /// /// Max Species ID (National Dex) that is stored in the table. /// - int MaxSpeciesID { get; } + ushort MaxSpeciesID { get; } /// /// Gets an index from the inner array. diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable1.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable1.cs index 78e73d412..d61f60f7d 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable1.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable1.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable1 : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_1; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable1(ReadOnlySpan data) { @@ -42,14 +42,26 @@ public PersonalTable1(ReadOnlySpan data) /// First type /// Second type /// Indication that the combination exists in the table. - public bool IsValidTypeCombination(byte type1, byte type2) + public int IsValidTypeCombination(byte type1, byte type2) { for (int i = 1; i <= MaxSpecies; i++) { var pi = Table[i]; if (pi.IsValidTypeCombination(type1, type2)) - return true; + return i; } - return false; + return -1; } + + /// + /// + /// + /// Type tuple to search for. + public int IsValidTypeCombination(IPersonalType other) => IsValidTypeCombination(other.Type1, other.Type2); + + /// + /// Checks if the type matches any of the type IDs extracted from the Personal Table used for R/G/B/Y games. + /// + /// Valid values: 0, 1, 2, 3, 4, 5, 7, 8, 20, 21, 22, 23, 24, 25, 26 + public static bool TypeIDExists(byte type) => type < 32 && (0b111111100000000000110111111 & (1 << type)) != 0; } diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable2.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable2.cs index 951e826f1..3abb4adcf 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable2.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable2.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable2 : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_2; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable2(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable3.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable3.cs index 87e9507f9..f666d0ee6 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable3.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable3.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable3 : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_3; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable3(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable4.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable4.cs index 67b387e46..3464a0ee8 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable4.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable4.cs @@ -10,8 +10,8 @@ public sealed class PersonalTable4 : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_4; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable4(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable5B2W2.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable5B2W2.cs index 2c0394737..169286a7b 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable5B2W2.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable5B2W2.cs @@ -10,8 +10,8 @@ public sealed class PersonalTable5B2W2 : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_5; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable5B2W2(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable5BW.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable5BW.cs index 04b8b622d..159864050 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable5BW.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable5BW.cs @@ -10,8 +10,8 @@ public sealed class PersonalTable5BW : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_5; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable5BW(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable6AO.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable6AO.cs index cdbb58bb7..753c7384c 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable6AO.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable6AO.cs @@ -11,7 +11,7 @@ public sealed class PersonalTable6AO : IPersonalTable, IPersonalTable MaxSpecies; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable6AO(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable6XY.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable6XY.cs index c1e57f8ce..c30a0cad4 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable6XY.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable6XY.cs @@ -10,8 +10,8 @@ public sealed class PersonalTable6XY : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_6; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable6XY(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable7.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable7.cs index a5db57be6..328a1bdcd 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable7.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable7.cs @@ -10,9 +10,9 @@ public sealed class PersonalTable7 : IPersonalTable, IPersonalTable data, int maxSpecies) + public PersonalTable7(ReadOnlySpan data, ushort maxSpecies) { MaxSpeciesID = maxSpecies; Table = new PersonalInfo7[data.Length / SIZE]; diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable7GG.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable7GG.cs index 7af82883b..9f736b94a 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable7GG.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable7GG.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable7GG : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_7b; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable7GG(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable8BDSP.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable8BDSP.cs index 8cb1f2877..d49e3eb06 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable8BDSP.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable8BDSP.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable8BDSP : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_8b; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable8BDSP(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable8LA.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable8LA.cs index 63bf1f486..bb29b8a80 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable8LA.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable8LA.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable8LA : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_8a; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable8LA(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable8SWSH.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable8SWSH.cs index b66008ba9..260fb436b 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable8SWSH.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable8SWSH.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable8SWSH : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_8_R2; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable8SWSH(ReadOnlySpan data) { diff --git a/PKHeX.Core/PersonalInfo/Table/PersonalTable9SV.cs b/PKHeX.Core/PersonalInfo/Table/PersonalTable9SV.cs index 0daed6e73..09833777b 100644 --- a/PKHeX.Core/PersonalInfo/Table/PersonalTable9SV.cs +++ b/PKHeX.Core/PersonalInfo/Table/PersonalTable9SV.cs @@ -9,8 +9,8 @@ public sealed class PersonalTable9SV : IPersonalTable, IPersonalTable MaxSpecies; + private const ushort MaxSpecies = Legal.MaxSpeciesID_9; + public ushort MaxSpeciesID => MaxSpecies; public PersonalTable9SV(ReadOnlySpan data) { diff --git a/PKHeX.Core/Resources/byte/evolve/evos_ao.pkl b/PKHeX.Core/Resources/byte/evolve/evos_ao.pkl index 626dbc17466be98fa35635847543b7b1be3fb6ee..1ac2bddd0c07410a2f92313c27ea43ccfb190495 100644 GIT binary patch literal 6160 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 K3Ord>;J*Qlx|JjV diff --git a/PKHeX.Core/Resources/byte/evolve/evos_bs.pkl b/PKHeX.Core/Resources/byte/evolve/evos_bs.pkl index 493f1d3990637db773b3d2af833fc95fb3aa2e67..a90a21c878c6276cef7c8becdd9380be72392fd6 100644 GIT binary patch delta 1294 zcmc)IJxIeq7zW@o{V~!X105Qy(pD5Z)-8*WG2P;pN$OU&j$I@j1A+u@$ym3HkuDuw z1i__l88c>*j9moZt6w36WD`0(yzh6{BwW%}Kj;Ui(t}fDEX~F2kp;4dkG&vch53zq zAlD>BM*LB_I%0LkWP!g*orZ>{gCX!QsdZzbSuiFOi^9%>G;0pY#)V^Gx34Vb9 z>nna=YT0WvA2HKFpyt*a_3IAsAp13pwVEa{F?Q<`5St zc(5QoGT@_f=R8CcA`&Bo1U6gzx%_R1P5dk+LKk94GB%jy<39>gB?DyE5NjIIkw(6| zq77!^0*j216Ea24$QNkB6J}p337|SR9hpaFnmcImMBP- z^pL^;hM=YB=ms&77XPVsxq4W)=a`)RvG@6KJloDcy@xmA7YMNaZ^XyTz1i|zev+0} TEs!es2e1oXX?`xlW_0ii4kpm+ delta 1251 zcmciAJ!ryE6b0aC{`!hPjablPK{Hh-1;Hg03hCxj(9JGIH%HyvVuwPP7U6bqOgD!P z0T)NdjOo;Fb#y9@jy(^b5Q0J{hmUjaeMoqTzBRM_bEPv%G8Q$)>`4>kK0eN7F>}m; z6e7O`dQvW2o?>jvL>CHBh6+@{21lwR8xbw^s5a)V)I$!WA@Vt9lwwpH%6veJ!6^p{;>s1bWqe-34mY60@*^bqqVfm81f)ELD+lHg=9w zM>cl5-gm%S_(5ChB74#RITYx|C?-6*H%fYdIiN=)PjAL4eHh$YW5di1j*K!foAhbq ztO{BcY;Yp#=naANqmny7+Zv(^&oDX+_fQYo^PJ27-_)ZHFS;|lmqXC-c@=z0kt+U; iB6Y^5KfJH#oHf<2>0NTF5YGhMU}wrh0*7H{k@y2as=;~y diff --git a/PKHeX.Core/Resources/byte/evolve/evos_gg.pkl b/PKHeX.Core/Resources/byte/evolve/evos_gg.pkl index 38b159f822785983b534a809d1205154d8ca6bf9..26afd9316c03512968a187d7eaa5da6b16ac6419 100644 GIT binary patch literal 7016 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 diff --git a/PKHeX.Core/Resources/byte/evolve/evos_ss.pkl b/PKHeX.Core/Resources/byte/evolve/evos_ss.pkl index b9fbb0dea9372313725ced5daaaa72282e92fed3..3e8d48527817468a88b283e02bbd95cb1f1d3687 100644 GIT binary patch literal 7504 zcmeI0YmA&_701u>yxZy9D(!UJ-JO=U%(hEAbW1zi(sr<1rWavKw9NK`1GaUrh+8PK zgxiQotG3hV==bYy}=Q+>iec$QcdTXvL*Da#tL4iy`i=+fClM1wYn$$2(sS}wm;*uaV zM8Z%dLZVP52I3$AQXNRcuZ)mUXfqku$`}|Y6VOSLgXY^%fUlJ)_>>YULo3rzMZZ!5 zbs`Hy8n|dT6NIgVS_s3AkSH_;;vhki&=g3M5om)^w3Q4PYa)wwGey|Vlwg-h1=@)! z`fBfjI>vTFdazf(1wjx3VGse)4#eO;RpN*}((=dkqYzyPC+c5#ZGD&h!B~J=arPzTI{N)xZuxq3amA=ln=nIk%H2fYD z!F*8?gHA^rF|8!vQ<6=j&{onXVFZ23D9J#_K(>i-v?s_UG)MAKr9h^jtrX!a{SQ+{ z?G;jmo{Sp$l{%2nJ8@wLNeHThn~0ztB{69HBqY$6Bq^wpZej%OQIdf+7(-jhHZhL2 zGC?MxN{-~AN`Xv4i=+fCw@`syZ9@${rA}la_J_DoB}hWha2q1h@q{-Ndl@Qry+&@G#P=8k_>e0BxKPy-i8VIlw1>ew3Pyxf+|H) zf+}TFfhtu{YeOAAMb7>gF50I;5OJXngyD~#1~H83L>zqyl7yy68ahHop-Kjfkt}qa zOhA=Ml7lLFQh+K`ph(Kl1{Jg$RMA%IL>6NYflGo=B?Q7G0*y{X2K~*9!EQ$u{!Wae zZ=wx3_|7DG#BC}oDE&7R5 z8?TgJ)uo7cWmwLkzn}Ip*y z&jw7Xx=Ezpm0hx#{TJgGqbqyl64(JbF5jg;jykY5*+P38#;d)Z_70S;`F7%3t0qOx z)$vE6YVSh7uJ`9CQuBYDKybgNg4f!SGZpXO<ARupH27liBxi4R&9^rmH zfFsblzJmEwAHy#mOeOf~=&fftbtN?qb>&7m#yY=^P#sqg=~n$A@+^?c%=7F&j`iw( zyiB`-_<(FTufWD#L?&4<3wi@|HuTS|<8Ib>5B2lZzftR~`yagDZ}WbmXzzJ2#=rj# z?JnA`@z*)m$Q<3rAnWS(?7cFdcAxRrwUF_Pj4YieH_5M|b5Q4U_~$|g*uR3h(ntg; z%SM~pkL<=GOpXoIU3)?{$Bb+u+_iN`u8qu{r73V)%Q+P zmLpE}T*gPNEYbM$tbgu<)<5?(*58+?m6h7Rp7sXozsELFFXTL%z5F-I#msvt>yKMG zPseYiy^Z$eoF`#r?M(E4l;f`Ce8a3~5A$C|J7r~s=DXU;km>>J-{&;)x^h^qWuAkK zyUt2f8H&10_#1-^L&x^OSE66 z{R-{hVV(NDdX@S9fOUnD_XJeyIAQ(u{fYbi7p|j*d#7=)GtXbG_h%@1!%Dx_Rkw1s z>ffz&_eh_-$^7_ec>7=-S*7=V20kJhH`np+Wzg~8N4<^=4#;Zhb7aE+zV0Dtk347w z7(eLv@88uNw}$pw+C#M0(Z0a(pYM&1|NYqH`1kBWM`r8%A7Wh>vHudsKmRz#ZFT(T zY`fz>f0r@taz~cAvcGEw>T+dY*A=YqBaZ)kUg^l9HON1V{A)cm-uibt`kg<^?DfX| zzUyNgceUfc7p}qnXkGhg?}x3waXy7OU-5$UY)i$)d3G0d1~ovPNp-#N_94!9GwRoQ z?r`}2b>yNb?7PuFU7<}!rC#@wd~M>qqNGXzqawm_1m zQCkl6R+|F>Nuz`kL_=DIKu``6B~TEgBnoX3(WI%Grc@1t^!xYm0D0`q?#`>-ckNe? zKhJn~HSh0z|1%nmMx)U+*Houer;7BDZY6Rnnh3WRISmzbQJf}nBN_;%i!4PM9V9bE zu0kCWi7Y`0ZNxK0E=CP~WZQ_`ffk}|MOL7S9@6c|M-$=pQM;RR? zG5M%NW|5B)+K6Y9j~e>O=8%sTqPgUwiXPIP$VU_5Jn~UN7sZ{)M*~4ZKFa7I*@b-6 zA-j@~655F8laCtu$aW(iEkwJMk1Bdd7m$x8!ac}G1zi-2M+IFJ_a`3>1P73hGCD{Wk&im$K=M&S z8}UKpqlP}RgULq=(IMociXPI8d^8arN~<^pIXaKAH$GBp(%YQM`zJG!VRte3a2aaxwX+LoOj7CA1M=NYLR2Cj zRrHYl5&38${A2P_K^Mh8As-C{cao1XI!Nv!A9cvxX5%CA0@OASI9>VePn+_K3a(WmV8vv zL;6wj(M0$$@=-w-#gCJZ27*tJk1{$)?j;{}NR@n)&_;Y8`KY0f?0)jmLi7OnsG^7T zljNg`@KfZYf-Z`GM?M+|K21K#=pd<)k2>TtsPmqrmqA!z=DtbttBp*$L zP4ZDe7saoTj|PITl8-VvNWMls>X5IKj}qF5pCTVM^pSmoe6$dKlYCUsL)s!AO@!Yf z9~E>_{5JV$Ab6U5l+i)*4Ed--{+WD~&_?_$`KY0f>^bt$LewT7RrHWPPd=ImUmzb9 zbW!{c`Dh^c7xGa?2g!@%qYn8l`6!`{_Oenvi8i2j>=RMA8FbMnzd_&?;Mf-Z`$ zkdFp}|0N$~bddDPM;-Ei&XP>1-QEI|ov#05_-Mh$&rQ#`o?Ekql6vI14~kPdlrE1C#5_T)5F z&_yxu$)X+z^8Tn`-+MIk;(L=fg`Dh{x$wviU z6t^TF4FprkM;RR?Tak}CWNY$KLL2ck@=-${*>v*JLNtSXRMA5kk&hK>4&_Jc@iY5G)}dWpt1nO+M<7 zW5`DdZN%>&A2sxm9ZNo1h>jy4RrHV^Pd=ImPaq!^bWvPNJ{ky?k&iMuNKPalb;xq^ zQ9>K>N#vu3KC+X^M+?y@u8 z8}ZrXqlP}RbI3;v(YfTKiXPGxd&D)Nf-&Md>!2SvZvRt;Io2#bgo)^bD za@{^}Co+Ak^P8Se`?G_A=Zip=$&RKwm!mx$*DOQhnw_H`rf1bU&o$j>j`jjMQRd~C z-}J0npTu;dIoi|pa90D5cinQ`m!ltRX4U@eCNgc!&tshiaaZ@agG6TL?NT5o z$-y)-Brob)Q4@aCab*VfTR65Mhfert-spgi>o5b4cOBQ^Z#3)vH=qjW@;Z9uOM?Lg zxMo*YKzX@738==hKBZ@ee(pe&qUpWpSg zQ(JCBve^5!fyc#=9OgZ3pdDN4vZf#UxOhKjDX#h1P2XAp-Cuc@<<9#~h38gIm&@(k zMIXPnt(=}HcQD;vwv|qwVaByf{yNyEybp7GdSbP*Z-%Z=$vRZtU%-sfA0j={d4SY^Y+j-rx>zr^TJvYs6Jxloiko z^Be=~^C)8nes_I*%`-4AZHLRfTJt&RffsI+0s7SS@F?eTcb~Wl==!{;Cvn~$Xghm( zdI>3aTU!ORKKpw1T-RuAeRRKjqXK$f+&$~zez>lOhgh_Ksr@7f6AHL?nn*I#QOrNio;0>qyMW^m&cLj9)xxmYRUA7@P!h6vz z?{zM0t@BrTOJ$vR7~vnZolANBK9%Fo-?iX%y!PjEL&qhq@bpsB?zSOSK-ZsZ4eW1d zJJ)%#mo2r^{@h^bIQEV1^^Prz)%C+wz*WFiz*WFiz(@sjKjBW#%Dn1%1tR9Fc=^AdXpHe2%xtF`5e$2A%#^V;f zf2#HQvVr&G-dc6{)2~-xx_nPw`t@Iq;b&c5U$bPDvaNd`x{3x94+_fh?6d7Clb2KRa2}pS;U^&HTFD z6Q8eJ&3V&ycJb}`oTm0?zQ6u`PF%;ez_;tXl%j9stHg4yfo5mdI`3^+ofrD~KgVjS zN?M=&EbGty7VZCNeHK~ue0rH2WYP1N*5?odua~u*%$Hr5SaH+-EH*T*!z>z?6~%p1 zC$!E-8F+rzc9!_E$D5jV{~bUDHm1$vEZhE^V9|byu7}Gk+J82+3h1~_vg~>J6pQMt z^*PO#d3o!t?VRq*o_X7G^ABGEt@Am)J=Yi2{;V*tpAg99a-L<|=kpCcE-vuxIqzMU z*F_e+ZW?}VANgPP`TY`$)}N90eE56n^19r&=ejGy_jcre)p1?v%lwhgG1z-*f3Egr zpTVY#f9pD~Ykk>w{FB$7sQtO#qW-MCmlO9}tRdV{khwhE%T06+quWU@#nn#aPzwgtdAAY3$%aK z!24*n)n*1A`UL0u&0ab6W=?efbURixupQImhxb`@{AWmR_aCsTKezdxvZz0Lfu&Dd zbbf-aho7r~QCCApIUa@rf1;ur=u#u zFn5mm+s`a|{?g_3b3gy}dxshFsQ#oE5`M+E=Q`3_pS~~Kj%pACytDS_7rso*8!=60 z$gA#oFmD-u`6a22qx0@LJfZ?4;Kv2Vc) zxJSC3pI?wLZ;ZCHn}Pj=y!}`+zxHQ=p>geD*|_#JG_JiY8`nMsdyfC;`m=8#|M3lN zXTL)J&#kXnuk%i|Kl>MCQxokRVA=760}U*%ymeVKzt;KSf?enHXO=y_78mkgk8w@C z^G)&-{ zbzCPDWE&<{+!oM_Wjycbg{YPlJpK5?gx<71%M12=5AQVD%sbhl$5kLdm~yH`{n6!h zdO_m6D~+~uhDGmRXn)Q!G_JEP8du)s2XZcP2nXLZ4bwB=#w%^x2soLwEsLQi#Q28t;_xTLjLPnX*)L* zF&vUQe2T|IAw5gAace>-51F5ZR)K$P$z*WFiz*WFiz|IQj{?7^n=O1c2f#+?V zca*be;2ew2JIy;DH@~ZZtH9tZpxd8&y@@~HYw#m=39bUJ0R(&dshKh0apQ60apQ60apQ60apQ60apQ60apQ60apQ60apQ6 z0apQ60apQ60apQ60apQ6fdN)v)%Qi_|D5KkcJjY3ys91d2I>G8xog^0Aa@1ygQqY1 z{>qyWM8n?Ka&~NhzgaI+c_{8!lp6TTBHzi(px3fiOcQ zqBuK_(u@^|Oo1(OiJ}-lG>Vm=WhNrXf>t9`NP)_Vw{*NrM5aVRj=wkGFJmUdj>^dB z_{YO@-g9}+`ObH~lP$Y;6zki6Y_>8H1JbI8;zyCDM*(176~T>-bTw6F@mgLa-7ZhY0GG zD2c&JoD9NBqKPD8Wf-KINF&aWESw_+xS3JZ6-fym`!^}0e}z2UGFKH`T1BnT@Z z5{4rr3M(-Zhn1lQ5-8V6qO1&)6kI2bvNF;{2C27 zUX&Fd@xw}h1Yspa!mttn(YGLmIwjt~AjKTk@GM**2RqI>XzfEkL#vQa@Y{!7SPxN~5)@*9)=tjNv>%n|p z*(meH*6Mp1FF>sIeKl{Ae#Sxe8^Cyj@%J)5PHekZw#c$tznFZR^~a0#>paWF&R4Gx zJ4d}zY`1!q*k1L?h`q8&R&(4Mv0Yx-Ebm6_kt6aQ)<>|uiRizU@j8st_%z1rxsDBt z&k)2vJkxxS6({&ul5wEhmB+q-yPnm3OKVniq9^LPsG0*q09t0p~~8aRl#4^B!jV7}xh}elKOj9y!fCkM^nPzXS75 zhA)Muz<=Pre4Kf&r9VNxMpwE2f8+kY$^91N-b2Q?_f8Ar35-3)y;r=(woSu)Q;eN7 z9i9g3`llNAKAKL?GV<;J~tPcU{s z^Pgzk@BSoX=jeI4mHpNr*7j4dKJ{AWUuWF=YCZFuVchTZOk%-G{R^0Hik zdXM~45^TSU^Y1eD1kHan_bthFe~SD3Y3A9D_ow|*T<>RiKK3xap6xdn_g+jJ_j|pG z=V347n=!B68>_jFeO$-qVf+(E?!Y?mPdvF3);MEqyUu$z_y5a0FJIxf`;6_@_xn}m z%Ng6R_tAF74{|;G@t)dcyF6U`ez(gt@NBtUu7oxJ0h|-{BgTd_-;Jo(brzWCQRaD! z@9778A5R)Pze8Ta4!e1N#P*7@voznIxIcep{3_#Pj9+8?7w-S-+}Ag_?{A`Be;)+* zt^OW5HE)&)HE$FTtn+vscb_IZ?s=Z-*tuFi-En{CW}u%wmyi8h9ryRQo$Vd0_aoME zU5KNkz+$TZabbw=e>mO zJ015syTY+cwf#!Ro~XXcu_viNLSMsvA9HL(>#ud}YV{t+y`MkJ_1?hvM#i6Gd=uln zcyBuH7RNn@w{lf2d=r(3lF+R-rdB!g~?mc$IvE2dWdx`t>3s`?2 zUdB9&;a_pS-{QF!VWmeM>FSqW*wns{-}8O{k|J8jzOAP;BcH^yLYJaz^5Hlx;_M?u| F^>;C_wjKZg literal 66376 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 data, uint key, int offset = return offset; case SCTypeCode.Object: // Cast raw bytes to Object - var length = ReadInt32LittleEndian(data[offset..]) ^ (int)xk.Next32(); + var length = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; return offset + length; case SCTypeCode.Array: // Cast raw bytes to SubType[] - var count = ReadInt32LittleEndian(data[offset..]) ^ (int)xk.Next32(); + var count = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; type = (SCTypeCode)(data[offset++] ^ xk.Next()); return offset + (type.GetTypeSize() * count); @@ -220,19 +220,19 @@ public static SCBlock ReadFromOffset(ReadOnlySpan data, uint key, ref int case SCTypeCode.Object: // Cast raw bytes to Object { - var num_bytes = ReadInt32LittleEndian(data[offset..]) ^ (int)xk.Next32(); + var num_bytes = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; var arr = data.Slice(offset, num_bytes).ToArray(); offset += num_bytes; for (int i = 0; i < arr.Length; i++) - arr[i] ^= (byte)xk.Next(); + arr[i] ^= xk.Next(); return new SCBlock(key, type, arr); } case SCTypeCode.Array: // Cast raw bytes to SubType[] { - var num_entries = ReadInt32LittleEndian(data[offset..]) ^ (int)xk.Next32(); + var num_entries = ReadInt32LittleEndian(data[offset..]) ^ xk.Next32(); offset += 4; var sub = (SCTypeCode)(data[offset++] ^ xk.Next()); @@ -240,7 +240,7 @@ public static SCBlock ReadFromOffset(ReadOnlySpan data, uint key, ref int var arr = data.Slice(offset, num_bytes).ToArray(); offset += num_bytes; for (int i = 0; i < arr.Length; i++) - arr[i] ^= (byte)xk.Next(); + arr[i] ^= xk.Next(); EnsureArrayIsSane(sub, arr); return new SCBlock(key, arr, sub); } @@ -251,7 +251,7 @@ public static SCBlock ReadFromOffset(ReadOnlySpan data, uint key, ref int var arr = data.Slice(offset, num_bytes).ToArray(); offset += num_bytes; for (int i = 0; i < arr.Length; i++) - arr[i] ^= (byte)xk.Next(); + arr[i] ^= xk.Next(); return new SCBlock(key, type, arr); } } diff --git a/PKHeX.Core/Saves/Encryption/SwishCrypto/SCXorShift32.cs b/PKHeX.Core/Saves/Encryption/SwishCrypto/SCXorShift32.cs index 451d8038e..1ac94be9e 100644 --- a/PKHeX.Core/Saves/Encryption/SwishCrypto/SCXorShift32.cs +++ b/PKHeX.Core/Saves/Encryption/SwishCrypto/SCXorShift32.cs @@ -11,28 +11,28 @@ namespace PKHeX.Core; public ref struct SCXorShift32 { private int Counter; - private uint Seed; + private uint State; - public SCXorShift32(uint seed) => Seed = GetInitialSeed(seed); + public SCXorShift32(uint seed) => State = GetInitialState(seed); - private static uint GetInitialSeed(uint seed) + private static uint GetInitialState(uint state) { - var pop_count = System.Numerics.BitOperations.PopCount(seed); + var pop_count = System.Numerics.BitOperations.PopCount(state); for (var i = 0; i < pop_count; i++) - seed = XorshiftAdvance(seed); - return seed; + state = XorshiftAdvance(state); + return state; } /// /// Gets a from the current state. /// - public uint Next() + public byte Next() { var c = Counter; - var result = (Seed >> (c << 3)) & 0xFF; + var result = (byte)(State >> (c << 3)); if (c == 3) { - Seed = XorshiftAdvance(Seed); + State = XorshiftAdvance(State); Counter = 0; } else @@ -43,19 +43,16 @@ public uint Next() } /// - /// Gets a from the current state. + /// Gets a from the current state. /// - public uint Next32() - { - return Next() | (Next() << 8) | (Next() << 16) | (Next() << 24); - } + public int Next32() => Next() | (Next() << 8) | (Next() << 16) | (Next() << 24); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint XorshiftAdvance(uint key) + private static uint XorshiftAdvance(uint state) { - key ^= key << 2; - key ^= key >> 15; - key ^= key << 13; - return key; + state ^= state << 2; + state ^= state >> 15; + state ^= state << 13; + return state; } } diff --git a/PKHeX.Core/Saves/Substructures/Gen5/FestaBlock5.cs b/PKHeX.Core/Saves/Substructures/Gen5/FestaBlock5.cs index 75673a6f5..0c3f31cf5 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/FestaBlock5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/FestaBlock5.cs @@ -236,25 +236,25 @@ public Funfest5Score(int total, int score, int level, bool isNew) : this(0) public int Total { - get => (int)(RawValue & 0x3FFFu); + readonly get => (int)(RawValue & 0x3FFFu); set => RawValue = (RawValue & ~0x3FFFu) | ((uint)value & 0x3FFFu); } public int Score { - get => (int)((RawValue >> 14) & 0x3FFFu); + readonly get => (int)((RawValue >> 14) & 0x3FFFu); set => RawValue = (RawValue & 0xF0003FFFu) | (((uint)value & 0x3FFFu) << 14); } public int Level { - get => (int)((RawValue >> 28) & 0x7u); + readonly get => (int)((RawValue >> 28) & 0x7u); set => RawValue = (RawValue & 0x8FFFFFFFu) | (((uint)value & 0x7u) << 28); } public bool IsNew { - get => RawValue >> 31 == 1; + readonly get => RawValue >> 31 == 1; set => RawValue = (RawValue & 0x7FFFFFFFu) | ((value ? 1u : 0) << 31); } } diff --git a/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs b/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs index bad22a6c1..75ad8b96f 100644 --- a/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs +++ b/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs @@ -764,6 +764,11 @@ private bool SetSuggestedMoves(bool random = false, bool silent = false) } Entity.SetMoves(moves); + if (Entity is ITechRecord tr) + { + tr.ClearRecordFlags(); + tr.SetRecordFlags(moves); + } Entity.HealPP(); FieldsLoaded = false; LoadMoves(Entity); diff --git a/PKHeX.WinForms/Controls/PKM Editor/StatEditor.cs b/PKHeX.WinForms/Controls/PKM Editor/StatEditor.cs index 6481768af..8038016b6 100644 --- a/PKHeX.WinForms/Controls/PKM Editor/StatEditor.cs +++ b/PKHeX.WinForms/Controls/PKM Editor/StatEditor.cs @@ -659,6 +659,12 @@ static void SetEVMaskSize(Size s, string Mask, MaskedTextBox[] arr) private void L_TeraTypeOriginal_Click(object sender, EventArgs e) { var pi = Entity.PersonalInfo; + if (!Entity.SV) + { + var expect = TeraTypeUtil.GetTeraTypeImport(pi.Type1, pi.Type2); + SetOriginalTeraType((byte)expect); + return; + } var current = WinFormsUtil.GetIndex(CB_TeraTypeOriginal); var update = pi.Type1 == current ? pi.Type2 : pi.Type1; SetOriginalTeraType(update); @@ -704,7 +710,7 @@ private void CHK_IsAlpha_CheckedChanged(object sender, EventArgs e) ((PKMEditor)MainEditor).UpdateSprite(); } - private void L_TeraTypeOverride_Click(object sender, EventArgs e) => CB_TeraTypeOverride.SelectedValue = (int)TeraOverrideNoneValue; + private void L_TeraTypeOverride_Click(object sender, EventArgs e) => CB_TeraTypeOverride.SelectedValue = Entity.SV ? (int)TeraOverrideNoneValue : CB_TeraTypeOriginal.SelectedValue; private void ChangeTeraType(object sender, EventArgs e) { diff --git a/PKHeX.WinForms/Subforms/PKM Editors/MoveShopEditor.cs b/PKHeX.WinForms/Subforms/PKM Editors/MoveShopEditor.cs index 0ac34207a..a081aa125 100644 --- a/PKHeX.WinForms/Subforms/PKM Editors/MoveShopEditor.cs +++ b/PKHeX.WinForms/Subforms/PKM Editors/MoveShopEditor.cs @@ -1,7 +1,9 @@ using System; using System.ComponentModel; +using System.Drawing; using System.Windows.Forms; using PKHeX.Core; +using PKHeX.Drawing.Misc; namespace PKHeX.WinForms; @@ -11,6 +13,13 @@ public partial class MoveShopEditor : Form private readonly IMoveShop8Mastery Master; private readonly PKM Entity; + private const int ColumnIndex = 0; + private const int ColumnTypeIcon = 1; + private const int ColumnType = 2; + private const int ColumnName = 3; + private const int ColumnPurchased = 4; + private const int ColumnMastered = 5; + public MoveShopEditor(IMoveShop8 s, IMoveShop8Mastery m, PKM pk) { Shop = s; @@ -32,17 +41,37 @@ private void Setup() var cIndex = new DataGridViewTextBoxColumn { HeaderText = "Index", - DisplayIndex = 0, + DisplayIndex = ColumnIndex, Width = 40, ReadOnly = true, SortMode = DataGridViewColumnSortMode.Automatic, }; + var cType = new DataGridViewImageColumn + { + HeaderText = "Type", + DisplayIndex = ColumnTypeIcon, + Width = 40, + ReadOnly = true, + SortMode = DataGridViewColumnSortMode.Automatic, + ImageLayout = DataGridViewImageCellLayout.Zoom, + }; + + var CTypeTextHidden = new DataGridViewTextBoxColumn + { + HeaderText = "Type", + DisplayIndex = ColumnType, + Width = 60, + ReadOnly = true, + SortMode = DataGridViewColumnSortMode.Automatic, + Visible = false, + }; + var cMove = new DataGridViewTextBoxColumn { HeaderText = "Move", - DisplayIndex = 1, - Width = 100, + DisplayIndex = ColumnName, + Width = 120, ReadOnly = true, SortMode = DataGridViewColumnSortMode.Automatic, }; @@ -50,7 +79,7 @@ private void Setup() var cPurchased = new DataGridViewCheckBoxColumn { HeaderText = "Purchased", - DisplayIndex = 2, + DisplayIndex = ColumnPurchased, Width = 70, SortMode = DataGridViewColumnSortMode.Automatic, }; @@ -58,7 +87,7 @@ private void Setup() var cMastered = new DataGridViewCheckBoxColumn { HeaderText = "Mastered", - DisplayIndex = 3, + DisplayIndex = ColumnMastered, Width = 70, SortMode = DataGridViewColumnSortMode.Automatic, }; @@ -70,6 +99,8 @@ private void Setup() cMastered.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; dgv.Columns.Add(cIndex); + dgv.Columns.Add(cType); + dgv.Columns.Add(CTypeTextHidden); dgv.Columns.Add(cMove); dgv.Columns.Add(cPurchased); dgv.Columns.Add(cMastered); @@ -78,6 +109,12 @@ private void Setup() // Inverted sort order for checkboxes, so that the first sort click has all True at top. private void ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) { + if (e.ColumnIndex == ColumnTypeIcon) + { + dgv.Sort(dgv.Columns[ColumnType], ListSortDirection.Ascending); + return; + } + var column = dgv.Columns[e.ColumnIndex]; if (column.SortMode != DataGridViewColumnSortMode.Programmatic) return; @@ -106,9 +143,24 @@ private void PopulateRecords() dgv.Rows.Add(indexes.Length); for (int i = 0; i < indexes.Length; i++) { + var isValid = Shop.Permit.IsRecordPermitted(i); var row = dgv.Rows[i]; - row.Cells[0].Value = $"{i + Bias:00}"; - row.Cells[1].Value = names[indexes[i]]; + var move = indexes[i]; + var type = MoveInfo.GetType(move, Entity.Context); + if (isValid) + { + var cell = row.Cells[ColumnPurchased]; + cell.Style.BackColor = cell.Style.SelectionBackColor = Color.LightGreen; + } + else + { + var cell = row.Cells[ColumnPurchased]; + cell.Style.SelectionBackColor = Color.Red; + } + row.Cells[ColumnIndex].Value = $"{i + Bias:00}"; + row.Cells[ColumnType].Value = type.ToString("00") + (isValid ? 0 : 1) + names[move]; // type -> valid -> name sorting + row.Cells[ColumnTypeIcon].Value = TypeSpriteUtil.GetTypeSpriteIcon(type); + row.Cells[ColumnName].Value = names[indexes[i]]; } } @@ -125,9 +177,9 @@ private void LoadRecords() for (int i = 0; i < dgv.Rows.Count; i++) { var row = dgv.Rows[i]; - var index = int.Parse((string)row.Cells[0].Value) - Bias; - var purchased = row.Cells[2]; - var mastered = row.Cells[3]; + var index = int.Parse((string)row.Cells[ColumnIndex].Value) - Bias; + var purchased = row.Cells[ColumnPurchased]; + var mastered = row.Cells[ColumnMastered]; purchased.Value = Shop.GetPurchasedRecordFlag(index); mastered.Value = Master.GetMasteredRecordFlag(index); } @@ -138,9 +190,9 @@ private void Save() for (int i = 0; i < dgv.Rows.Count; i++) { var row = dgv.Rows[i]; - var index = int.Parse((string)row.Cells[0].Value) - Bias; - var purchased = row.Cells[2]; - var mastered = row.Cells[3]; + var index = int.Parse((string)row.Cells[ColumnIndex].Value) - Bias; + var purchased = row.Cells[ColumnPurchased]; + var mastered = row.Cells[ColumnMastered]; Shop.SetPurchasedRecordFlag(index, (bool)purchased.Value); Master.SetMasteredRecordFlag(index, (bool)mastered.Value); } diff --git a/Tests/PKHeX.Core.Tests/General/MarshalTests.cs b/Tests/PKHeX.Core.Tests/General/MarshalTests.cs index 5530ff182..2f853d3ae 100644 --- a/Tests/PKHeX.Core.Tests/General/MarshalTests.cs +++ b/Tests/PKHeX.Core.Tests/General/MarshalTests.cs @@ -19,6 +19,7 @@ public class MarshalTests [InlineData( 8, typeof(IndividualValueSet))] [InlineData(16, typeof(DreamWorldEntry))] [InlineData(16, typeof(CheckResult))] + [InlineData(16, typeof(EvolutionLink))] [InlineData(24, typeof(GenerateParam9))] public void MarshalSizeLessThanEqual(int expect, Type t) => Marshal.SizeOf(t).Should().BeLessOrEqualTo(expect); } diff --git a/Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/Legal (has 7 out of 8 Battle Memory).pk7 b/Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/0248 - Tyranitar - DC689F6F123B (has 7 out of 8 Battle Memory).pk7 similarity index 63% rename from Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/Legal (has 7 out of 8 Battle Memory).pk7 rename to Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/0248 - Tyranitar - DC689F6F123B (has 7 out of 8 Battle Memory).pk7 index 605bae6deead11402dfbbd8e551da7a06b637d9b..24ea345aba511d214797961b1d22398ef8d4fff2 100644 GIT binary patch delta 20 bcmZo+YGGox7RsN`z>qPKU6-+8qUm`6G)x7u delta 20 bcmZo+YGGox7RsN`z;I$ByDsB{iKgcPIV=Wp diff --git a/Tests/PKHeX.Core.Tests/Legality/Legal/Generation 8/0083-01 - Egg - 219CD3317179 legal traded HT memory no HT.pk8 b/Tests/PKHeX.Core.Tests/Legality/Legal/Generation 8/0083-01 - Egg - 219CD3317179 legal traded HT memory no HT.pk8 new file mode 100644 index 0000000000000000000000000000000000000000..6a0ca7d44609285ce735150d83c2d5d9f054bd8f GIT binary patch literal 344 zcmb;{G`!5fFh?<%fq~%?(=<;Y#Sp;21fm$8-eR}seLI#FZ zh7yKkh71Ntg%qd(E(}Qw>lpeOMC7TfGQ%LVq{^{!O#T~f%pa8;1Hn8 kih*v!1W?}!^Y8!>L((gwQXwJ&fRTZRft`U33_;!k07N7iga7~l literal 0 HcmV?d00001 diff --git a/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs b/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs index 72b217f02..53ef7689d 100644 --- a/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs +++ b/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; using FluentAssertions; @@ -12,20 +10,16 @@ public class LegalityData [Fact] public void EvolutionsOrdered() // feebas, see issue #2394 { - var trees = typeof(EvolutionTree).GetFields(BindingFlags.Static | BindingFlags.NonPublic); - var fEntries = typeof(EvolutionTree).GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(z => z.Name == "Entries"); - foreach (var t in trees) - { - var gen = Convert.ToInt32(t.Name[7].ToString()); - if (gen <= 4) - continue; + int count = 0; + var trees = typeof(EvolutionTree) + .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .Select(z => z.GetValue(typeof(EvolutionTree))) + .OfType(); - if (t.GetValue(typeof(EvolutionTree)) is not EvolutionTree fTree) - throw new ArgumentException(nameof(fTree)); - if (fEntries.GetValue(fTree) is not IReadOnlyList entries) - throw new ArgumentException(nameof(entries)); - var feebas = entries[(int)Species.Feebas]; - if (feebas.Length == 0) + foreach (var tree in trees) + { + var feebas = tree.Forward.GetForward((int)Species.Feebas, 0).Span; + if (feebas.Length <= 1) continue; var t1 = feebas[0].Method; @@ -33,7 +27,11 @@ public class LegalityData t1.IsLevelUpRequired().Should().BeFalse(); t2.IsLevelUpRequired().Should().BeTrue(); + + count++; } + + count.Should().NotBe(0); } [Fact] @@ -41,10 +39,7 @@ public void EvolutionsOrderedSV() { // SV Crabrawler added a second, UseItem evolution method. Need to be sure it's before the more restrictive level-up method. var tree = EvolutionTree.Evolves9; - var fEntries = typeof(EvolutionTree).GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(z => z.Name == "Entries"); - if (fEntries.GetValue(tree) is not IReadOnlyList entries) - throw new ArgumentException(nameof(entries)); - var crab = entries[(int)Species.Crabrawler]; + var crab = tree.Forward.GetForward((int)Species.Crabrawler, 0).Span; var t1 = crab[0].Method; var t2 = crab[1].Method; diff --git a/Tests/PKHeX.Core.Tests/Legality/TempTests.cs b/Tests/PKHeX.Core.Tests/Legality/TempTests.cs deleted file mode 100644 index cc40dd7e5..000000000 --- a/Tests/PKHeX.Core.Tests/Legality/TempTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using FluentAssertions; -using Xunit; -using static PKHeX.Core.Species; -using static PKHeX.Core.Move; - -namespace PKHeX.Core.Tests.Legality; - -public static class TempTests -{ - // BD/SP has egg moves that cannot be obtained because no parents in the egg group can know the move. - [Theory] - [InlineData(Taillow, Boomburst)] - [InlineData(Plusle, TearfulLook)] [InlineData(Minun, TearfulLook)] - [InlineData(Luvdisc, HealPulse)] - [InlineData(Starly, Detect)] - [InlineData(Chatot, Boomburst)] [InlineData(Chatot, Encore)] - [InlineData(Spiritomb, FoulPlay)] - public static void CanLearnEggMoveBDSP(Species species, Move move) - { - LearnSource8BDSP.Instance.GetEggMoves((ushort)species, 0).Contains((ushort)move).Should().BeFalse(); - - var pb8 = new PB8 { Species = (ushort)species }; - var encs = EncounterMovesetGenerator.GenerateEncounters(pb8, new[] { (ushort)move }, GameVersion.BD); - - encs.Should().BeEmpty("HOME supports BD/SP, but does not make any disconnected moves available."); - } -}