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 class BallApplicator 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 class QRMessageUtil // 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 class LocationsHOME 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 class Legal /// 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 @@ public sealed record EncounterFixed9 : EncounterStatic, IGemType 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 @@ public sealed record EncounterMight9 : EncounterStatic, ITeraRaid9 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 sealed record EncounterStatic8a(GameVersion Version) : EncounterStatic(Ve 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 sealed record EncounterStatic8a(GameVersion Version) : EncounterStatic(Ve 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 sealed record EncounterStatic8b : EncounterStatic, IStaticCorrelation8b 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 @@ public sealed record EncounterTera9 : EncounterStatic, ITeraRaid9 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 sealed record EncounterTrade8b : EncounterTrade, IContestStatsReadOnly, I 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 sealed record EncounterTrade9 : EncounterTrade, IGemType { 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 sealed class EncounterGenerator1 : IEncounterGenerator // 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 sealed class EncounterGenerator2 : IEncounterGenerator 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 @@ public sealed class EncounterGenerator3 : IEncounterGenerator 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 @@ public sealed class EncounterGenerator3GC : IEncounterGenerator 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 @@ public sealed class EncounterGenerator6 : IEncounterGenerator 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 @@ public sealed class EncounterGenerator7 : IEncounterGenerator 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 @@ public sealed class EncounterGenerator7GG : IEncounterGenerator 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 @@ public sealed class EncounterGenerator7GO : IEncounterGenerator 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 @@ public sealed class EncounterGenerator8 : IEncounterGenerator 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 @@ public sealed class EncounterGenerator8GO : IEncounterGenerator 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 @@ public sealed class EncounterGenerator8a : IEncounterGenerator 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 sealed class EncounterGenerator8a : IEncounterGenerator // 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 @@ public sealed class EncounterGenerator8b : IEncounterGenerator 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 sealed class EncounterGenerator8b : IEncounterGenerator 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 sealed class EncounterGenerator9 : 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_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 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 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 class EncounterMovesetGenerator /// 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 class EncounterMovesetGenerator /// 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 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 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 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 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 class EncounterMovesetGenerator /// 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 @@ public static class EncounterMovesetGenerator 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 @@ public static class EncounterMovesetGenerator 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 @@ public static class EncounterMovesetGenerator /// 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 @@ public static class EncounterMovesetGenerator 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 @@ public static class EncounterMovesetGenerator /// 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 @@ public static class EncounterMovesetGenerator { 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 @@ public static class EncounterMovesetGenerator /// 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 @@ public static class EncounterMovesetGenerator { 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 @@ public static class EncounterMovesetGenerator /// 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 @@ public static class EncounterMovesetGenerator { 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 @@ public static class EncounterMovesetGenerator /// 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 @@ public static class EncounterMovesetGenerator { 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 class EncounterSuggestion 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 class EvolutionVerifier 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 class EncounterOrigin /// 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 class EvolutionChain 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 @@ using System; 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 sealed class EvolutionHistory } 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 @@ public static class EvolutionSet1 : 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 @@ public static class EvolutionSet3 } } - 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 @@ public static class EvolutionSet4 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 @@ public static class EvolutionSet5 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 @@ public static class EvolutionSet6 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 @@ public sealed class 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 @@ public readonly record struct EvoCriteria : ISpeciesForm 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 readonly record struct EvolutionMethod(EvolutionType Method, ushort Speci /// /// 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 struct EvolutionNode 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 @@ public sealed class LearnGroup7 : ILearnGroup } // 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 @@ public sealed class LearnGroup9 : ILearnGroup } // 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 sealed class LearnSource4HGSS : ILearnSource, IEggSource 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 sealed class LearnSource4Pt : LearnSource4, ILearnSource, 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 sealed class LearnSource5B2W2 : LearnSource5, ILearnSource 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 sealed class LearnSource5BW : LearnSource5, ILearnSource (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 sealed class LearnSource6AO : ILearnSource, IEggSource (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 sealed class LearnSource6XY : ILearnSource, IEggSource (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 sealed class LearnSource7SM : ILearnSource, IEggSource (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 sealed class LearnSource7USUM : ILearnSource, IEggSource (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 sealed class LearnSource8BDSP : ILearnSource, IEggSour 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 sealed class LearnSource8BDSP : ILearnSource, IEggSour (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 sealed class LearnSource8LA : ILearnSource 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 sealed class LearnSource8LA : ILearnSource 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 sealed class LearnSource8SWSH : ILearnSource, IEggSour { (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 @@ public sealed class LearnSource8SWSH : ILearnSource, IEggSour 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 sealed class LearnSource8SWSH : ILearnSource, IEggSour 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 @@ public sealed class LearnSource9SV : ILearnSource, IEggSource, 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 sealed class LearnSource9SV : ILearnSource, IEggSource, 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 sealed class Learnset { // 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 @@ public static class MoveListSuggest } // 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 @@ public static class MoveListSuggest 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 @@ public static class MoveListSuggest // 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 ref struct XorShift128 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 @@ public ref struct Xoroshiro128Plus 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 @@ public ref struct Xoroshiro128Plus8b 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 @@ internal static class GBRestrictions 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 @@ internal static class GBRestrictions 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 @@ internal static class GBRestrictions 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 sealed class AwakenedValueVerifier : Verifier 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 sealed class AwakenedValueVerifier : Verifier // 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 sealed class FormVerifier : Verifier 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 @@ public sealed class FormVerifier : Verifier 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 @@ public sealed class FormVerifier : Verifier 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 @@ public sealed class FormVerifier : Verifier case Silvally: { - int silvally = GetSilvallyFormFromHeldItem(pk.HeldItem); + var silvally = GetSilvallyFormFromHeldItem(pk.HeldItem); return silvally != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem); } @@ -220,183 +220,4 @@ public sealed class FormVerifier : Verifier 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 @@ public sealed class HistoryVerifier : Verifier 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 @@ public sealed class HistoryVerifier : Verifier 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 @@ public sealed class HyperTrainingVerifier : Verifier { // 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 @@ public sealed class MarkVerifier : Verifier 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 @@ public sealed class MemoryVerifier : Verifier 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 @@ public sealed class MemoryVerifier : Verifier // 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 @@ public sealed class MemoryVerifier : Verifier 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 @@ public sealed class MemoryVerifier : Verifier 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 @@ public sealed class MemoryVerifier : Verifier // 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 @@ public sealed class MemoryVerifier : Verifier 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 @@ public sealed class MemoryVerifier : Verifier return; } - var context = Info.EncounterOriginal.Context; var memory = mem.OT_Memory; if (pk.IsEgg) @@ -241,17 +339,17 @@ public sealed class MemoryVerifier : Verifier { // 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 @@ public sealed class MemoryVerifier : Verifier // {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 @@ public sealed class MemoryVerifier : Verifier }; } - 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 @@ public sealed class MemoryVerifier : Verifier 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 @@ public sealed class MemoryVerifier : Verifier { // 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 @@ public sealed class MemoryVerifier : Verifier 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 sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier { 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 @@ public sealed class MiscVerifier : Verifier } } - 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 sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier 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 sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier 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 @@ public sealed class MiscVerifier : Verifier 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 class MarkRules /// /// 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 class RibbonRules /// /// 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 class RibbonRules 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 @@ public static class RibbonRules 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 class RibbonVerifierCommon3 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 class RibbonVerifierCommon6 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 class RibbonVerifierCommon8 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 sealed class WA8 : DataMysteryGift, ILangNick, INature, IGigantamax, IDyn 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 sealed class WB8 : DataMysteryGift, ILangNick, INature, IRibbonIndex, ICo { 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 @@ public sealed class WB8 : DataMysteryGift, ILangNick, INature, IRibbonIndex, ICo 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 sealed class WC3 : MysteryGift, IRibbonSetEvent3, ILangNicknamedTemplate 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 sealed class WC3 : MysteryGift, IRibbonSetEvent3, ILangNicknamedTemplate 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 sealed class WC9 : DataMysteryGift, ILangNick, INature, ITeraType, IRibbo 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 sealed class WC9 : DataMysteryGift, ILangNick, INature, ITeraType, IRibbo { 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 sealed class WC9 : DataMysteryGift, ILangNick, INature, ITeraType, IRibbo 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 class GameDataPA8 : HomeOptional1, IGameDataSide, IScaledSizeAbsolut 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 @@ public class GameDataPA8 : HomeOptional1, IGameDataSide, IScaledSizeAbsolut 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 sealed class GameDataPB8 : HomeOptional1, IGameDataSide, IGameDataSp 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 sealed class GameDataPK9 : HomeOptional1, IGameDataSide, IScaledSize 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 @@ public sealed class GameDataPK9 : HomeOptional1, IGameDataSide, IScaledSize 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 sealed class PKH : PKM, IHandlerLanguage, IFormArgument, IHomeTrack, IBat 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 sealed class PKH : PKM, IHandlerLanguage, IFormArgument, IHomeTrack, IBat 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 sealed class PKH : PKM, IHandlerLanguage, IFormArgument, IHomeTrack, IBat 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 sealed class PK1 : GBPKML, IPersonalType 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 @@ public sealed class PK1 : GBPKML, IPersonalType 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 sealed class PK6 : G6PKM, IRibbonSetEvent3, IRibbonSetEvent4, IRibbonSetC 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 @@ public sealed class PK9 : PKM, ISanityChecksum, ITeraType, ITechRecord, IObedien } 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 @@ public static class FormConverter "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", - // "!", "?", not in Gen II + // "!", "?", not in Gen2 }, _ => new[] { @@ -878,31 +878,28 @@ public static class FormConverter 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 sealed class LegalityRejuvenator : IEntityRejuvenator // 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 sealed class LegalityRejuvenator : IEntityRejuvenator 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 @@ public sealed class LegalityRejuvenator : IEntityRejuvenator 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 @@ public sealed class LegalityRejuvenator : IEntityRejuvenator 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 sealed class PersonalInfo1 : PersonalInfo, IPersonalInfoTM 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 sealed class PersonalTable1 : IPersonalTable, IPersonalTableFirst 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 626dbc174..1ac2bddd0 100644 Binary files a/PKHeX.Core/Resources/byte/evolve/evos_ao.pkl and b/PKHeX.Core/Resources/byte/evolve/evos_ao.pkl differ diff --git a/PKHeX.Core/Resources/byte/evolve/evos_bs.pkl b/PKHeX.Core/Resources/byte/evolve/evos_bs.pkl index 493f1d399..a90a21c87 100644 Binary files a/PKHeX.Core/Resources/byte/evolve/evos_bs.pkl and b/PKHeX.Core/Resources/byte/evolve/evos_bs.pkl differ diff --git a/PKHeX.Core/Resources/byte/evolve/evos_gg.pkl b/PKHeX.Core/Resources/byte/evolve/evos_gg.pkl index 38b159f82..26afd9316 100644 Binary files a/PKHeX.Core/Resources/byte/evolve/evos_gg.pkl and b/PKHeX.Core/Resources/byte/evolve/evos_gg.pkl differ diff --git a/PKHeX.Core/Resources/byte/evolve/evos_ss.pkl b/PKHeX.Core/Resources/byte/evolve/evos_ss.pkl index b9fbb0dea..3e8d48527 100644 Binary files a/PKHeX.Core/Resources/byte/evolve/evos_ss.pkl and b/PKHeX.Core/Resources/byte/evolve/evos_ss.pkl differ diff --git a/PKHeX.Core/Resources/byte/evolve/evos_uu.pkl b/PKHeX.Core/Resources/byte/evolve/evos_uu.pkl index 29cc2473f..73e12a28b 100644 Binary files a/PKHeX.Core/Resources/byte/evolve/evos_uu.pkl and b/PKHeX.Core/Resources/byte/evolve/evos_uu.pkl differ diff --git a/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlock.cs b/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlock.cs index 27ea375b8..8b59f0eb4 100644 --- a/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlock.cs +++ b/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlock.cs @@ -127,11 +127,11 @@ public sealed class SCBlock if (Type == SCTypeCode.Object) { - bw.Write((uint)Data.Length ^ xk.Next32()); + bw.Write(Data.Length ^ xk.Next32()); } else if (Type == SCTypeCode.Array) { - var entries = (uint)(Data.Length / SubType.GetTypeSize()); + var entries = Data.Length / SubType.GetTypeSize(); bw.Write(entries ^ xk.Next32()); bw.Write((byte)((byte)SubType ^ xk.Next())); } @@ -170,12 +170,12 @@ public sealed class SCBlock 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 sealed class SCBlock 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 sealed class SCBlock 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 sealed class SCBlock 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 ref struct SCXorShift32 } /// - /// 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 record struct Funfest5Score(uint RawValue) 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 @@ public sealed partial class PKMEditor : UserControl, IMainEditor } 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 @@ public partial class StatEditor : UserControl 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 @@ public partial class StatEditor : UserControl ((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 @@ public partial class MoveShopEditor : Form 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 @@ public partial class MoveShopEditor : Form var cPurchased = new DataGridViewCheckBoxColumn { HeaderText = "Purchased", - DisplayIndex = 2, + DisplayIndex = ColumnPurchased, Width = 70, SortMode = DataGridViewColumnSortMode.Automatic, }; @@ -58,7 +87,7 @@ public partial class MoveShopEditor : Form var cMastered = new DataGridViewCheckBoxColumn { HeaderText = "Mastered", - DisplayIndex = 3, + DisplayIndex = ColumnMastered, Width = 70, SortMode = DataGridViewColumnSortMode.Automatic, }; @@ -70,6 +99,8 @@ public partial class MoveShopEditor : Form 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 @@ public partial class MoveShopEditor : Form // 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 @@ public partial class MoveShopEditor : Form 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 @@ public partial class MoveShopEditor : Form 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 @@ public partial class MoveShopEditor : Form 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 605bae6de..24ea345ab 100644 Binary files a/Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/Legal (has 7 out of 8 Battle Memory).pk7 and b/Tests/PKHeX.Core.Tests/Legality/Legal/General/Ribbons/0248 - Tyranitar - DC689F6F123B (has 7 out of 8 Battle Memory).pk7 differ 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 000000000..6a0ca7d44 Binary files /dev/null and b/Tests/PKHeX.Core.Tests/Legality/Legal/Generation 8/0083-01 - Egg - 219CD3317179 legal traded HT memory no HT.pk8 differ 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 class LegalityData { // 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."); - } -}