Evotree: Evolution Traversal Enhancements (#3936)

Like move validation, evolutions are the earliest thing we wish to traverse when determining what encounters may have originated the current Pokémon. To determine the permitted species-form-levels a Pokémon could originate with, we must devolve a Pokémon by traveling down-generation to origin. Once we have an encounter, we can then evolve it to the current species, traversing upwards from origin to the current format.
This commit is contained in:
Kurt 2023-07-05 21:14:09 -07:00 committed by GitHub
parent e02b33ef2c
commit dcc0e79435
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
175 changed files with 3663 additions and 2503 deletions

View file

@ -73,7 +73,7 @@ public static class BallApplicator
return pk.Ball = (int)next;
}
private static int ApplyFirstLegalBall(PKM pk, Span<Ball> balls)
private static int ApplyFirstLegalBall(PKM pk, ReadOnlySpan<Ball> balls)
{
foreach (var b in balls)
{

View file

@ -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<byte>.Shared.Rent(url.Length);
var result = Convert.TryFromBase64Chars(url, tmp, out int bytesWritten) ? tmp[..bytesWritten] : null;
ArrayPool<byte>.Shared.Return(tmp);
ArrayPool<byte>.Shared.Return(tmp, true);
return result;
}

View file

@ -1,3 +1,5 @@
using System;
namespace PKHeX.Core;
/// <summary>
@ -106,4 +108,47 @@ public static class LocationsHOME
SWSL when ver == (int)GameVersion.SW => true,
_ => false,
};
/// <summary>
/// Checks if the location is (potentially) remapped based on visitation options.
/// </summary>
/// <remarks>Relevant when a side data yields SW/SH side data with a higher priority than the original (by version) side data.</remarks>
/// <param name="original">Original context</param>
/// <param name="current">Current context</param>
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,
}

View file

@ -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<ushort> Pouch_Treasure_SV => new ushort[]

View file

@ -139,20 +139,6 @@ public static class Legal
internal static readonly ushort[] HeldItems_LA = Array.Empty<ushort>();
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
/// </summary>
public static bool GetIsFixedIVSequenceValidSkipRand(ReadOnlySpan<int> 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;

View file

@ -48,7 +48,7 @@ internal static class EncounterUtil
return temp;
}
internal static T? GetMinByLevel<T>(EvoCriteria[] chain, IEnumerable<T> possible) where T : class, IEncounterTemplate
internal static T? GetMinByLevel<T>(ReadOnlySpan<EvoCriteria> chain, IEnumerable<T> possible) where T : class, IEncounterTemplate
{
// MinBy grading: prefer species-form match, select lowest min level encounter.
// Minimum allocation :)

View file

@ -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;

View file

@ -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;
}

View file

@ -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)

View file

@ -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);

View file

@ -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;

View file

@ -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)

View file

@ -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)

View file

@ -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);

View file

@ -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;
}
}

View file

@ -13,6 +13,9 @@ public sealed class EncounterGenerator1 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable>();
return GetEncounters(pk, chain, game);
}

View file

@ -13,6 +13,9 @@ public sealed class EncounterGenerator2 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, GameVersion game)
{
var chain = EncounterOrigin.GetOriginChain12(pk, game);
if (chain.Length == 0)
return Array.Empty<IEncounterable>();
return GetEncounters(pk, chain, game);
}

View file

@ -14,6 +14,9 @@ public sealed class EncounterGenerator3 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 3);
return GetEncounters(pk, chain, info);
}
public IEnumerable<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
info.PIDIV = MethodFinder.Analyze(pk);
IEncounterable? partial = null;

View file

@ -13,6 +13,9 @@ public sealed class EncounterGenerator3GC : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 3);
return GetEncounters(pk, chain, info);
}
public IEnumerable<IEncounterable> 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))

View file

@ -17,12 +17,17 @@ public sealed class EncounterGenerator4 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable>();
return GetEncounters(pk, chain, info);
}
public IEnumerable<IEncounterable> 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)

View file

@ -13,12 +13,17 @@ public sealed class EncounterGenerator5 : IEncounterGenerator
public IEnumerable<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 5);
if (chain.Length == 0)
return Array.Empty<IEncounterable>();
return GetEncounters(pk, chain, info);
}
public IEnumerable<IEncounterable> GetPossible(PKM _, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups)
{
if (chain.Length == 0)
yield break;
if (groups.HasFlag(Mystery))
{
var table = EncounterEvent.MGDB_G5;

View file

@ -13,6 +13,9 @@ public sealed class EncounterGenerator6 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 6);
return GetEncounters(pk, chain, info);
}
public IEnumerable<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
var game = (GameVersion)pk.Version;
IEncounterable? deferred = null;

View file

@ -13,6 +13,9 @@ public sealed class EncounterGenerator7 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
var game = (GameVersion)pk.Version;
bool yielded = false;

View file

@ -15,6 +15,9 @@ public sealed class EncounterGenerator7GG : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
bool yielded = false;
if (pk.FatefulEncounter)
{

View file

@ -12,6 +12,9 @@ public sealed class EncounterGenerator7GO : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
if (!CanBeWildEncounter(pk))
yield break;

View file

@ -12,6 +12,9 @@ public sealed class EncounterGenerator8 : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
if (pk.FatefulEncounter)
{
bool yielded = false;

View file

@ -12,6 +12,9 @@ public sealed class EncounterGenerator8GO : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> GetEncounters(PKM pk, EvoCriteria[] chain, LegalInfo info)
{
if (chain.Length == 0)
yield break;
if (!CanBeWildEncounter(pk))
yield break;

View file

@ -12,6 +12,9 @@ public sealed class EncounterGenerator8a : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> 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))

View file

@ -12,6 +12,9 @@ public sealed class EncounterGenerator8b : IEncounterGenerator
public IEnumerable<IEncounterable> 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<IEncounterable> 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)
{

View file

@ -13,7 +13,10 @@ public sealed class EncounterGenerator9 : IEncounterGenerator
public IEnumerable<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 9);
if (chain.Length == 0)
return System.Array.Empty<IEncounterable>();
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<IEncounterable> GetPossible(PKM pk, EvoCriteria[] chain, GameVersion game, EncounterTypeGroup groups)
{
if (chain.Length == 0)
yield break;
if (groups.HasFlag(Mystery))
{
var table = EncounterEvent.MGDB_G9;

View file

@ -15,7 +15,7 @@ public sealed class EncounterGenerator7X : IEncounterGenerator
public IEnumerable<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 7);
return GetEncounters(pk, chain, info);
}

View file

@ -17,7 +17,7 @@ public sealed class EncounterGenerator8X : IEncounterGenerator
public IEnumerable<IEncounterable> GetEncounters(PKM pk, LegalInfo info)
{
var chain = EncounterOrigin.GetOriginChain(pk);
var chain = EncounterOrigin.GetOriginChain(pk, 8);
return GetEncounters(pk, chain, info);
}

View file

@ -31,10 +31,10 @@ public static class EncounterMovesetGenerator
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
/// <param name="versions">Any specific version(s) to iterate for. If left blank, all will be checked.</param>
/// <returns>A consumable <see cref="PKM"/> list of possible results.</returns>
/// <remarks>When updating, update the sister <see cref="GenerateEncounters(PKM,ITrainerInfo,ushort[],GameVersion[])"/> method.</remarks>
public static IEnumerable<PKM> GeneratePKMs(PKM pk, ITrainerInfo info, ushort[] moves, params GameVersion[] versions)
/// <remarks>When updating, update the sister <see cref="GenerateEncounters(PKM,ITrainerInfo,ReadOnlyMemory{ushort},GameVersion[])"/> method.</remarks>
public static IEnumerable<PKM> GeneratePKMs(PKM pk, ITrainerInfo info, ReadOnlyMemory<ushort> 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
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
/// <param name="versions">Any specific version(s) to iterate for. If left blank, all will be checked.</param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible results.</returns>
/// <remarks>When updating, update the sister <see cref="GeneratePKMs(PKM,ITrainerInfo,ushort[],GameVersion[])"/> method.</remarks>
public static IEnumerable<IEncounterable> GenerateEncounters(PKM pk, ITrainerInfo info, ushort[] moves, params GameVersion[] versions)
/// <remarks>When updating, update the sister <see cref="GeneratePKMs(PKM,ITrainerInfo,ReadOnlyMemory{ushort},GameVersion[])"/> method.</remarks>
public static IEnumerable<IEncounterable> GenerateEncounters(PKM pk, ITrainerInfo info, ReadOnlyMemory<ushort> 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
/// <param name="info">Trainer information of the receiver.</param>
/// <param name="generation">Specific generation to iterate versions for.</param>
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
public static IEnumerable<PKM> GeneratePKMs(PKM pk, ITrainerInfo info, int generation, ushort[] moves)
public static IEnumerable<PKM> GeneratePKMs(PKM pk, ITrainerInfo info, int generation, ReadOnlyMemory<ushort> moves)
{
var vers = GameUtil.GetVersionsInGeneration(generation, pk.Version);
return GeneratePKMs(pk, info, moves, vers);
@ -112,7 +112,7 @@ public static class EncounterMovesetGenerator
/// <param name="generation">Specific generation to iterate versions for.</param>
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
public static IEnumerable<IEncounterable> GenerateEncounter(PKM pk, int generation, ushort[] moves)
public static IEnumerable<IEncounterable> GenerateEncounter(PKM pk, int generation, ReadOnlyMemory<ushort> moves)
{
var vers = GameUtil.GetVersionsInGeneration(generation, pk.Version);
return GenerateEncounters(pk, moves, vers);
@ -125,9 +125,9 @@ public static class EncounterMovesetGenerator
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
/// <param name="versions">Any specific version(s) to iterate for. If left blank, all will be checked.</param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
public static IEnumerable<IEncounterable> GenerateEncounters(PKM pk, ushort[] moves, params GameVersion[] versions)
public static IEnumerable<IEncounterable> GenerateEncounters(PKM pk, ReadOnlyMemory<ushort> 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
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
/// <param name="vers">Any specific version(s) to iterate for. If left blank, all will be checked.</param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
public static IEnumerable<IEncounterable> GenerateEncounters(PKM pk, ushort[] moves, IReadOnlyList<GameVersion> vers)
public static IEnumerable<IEncounterable> GenerateEncounters(PKM pk, ReadOnlyMemory<ushort> moves, IReadOnlyList<GameVersion> 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
/// <param name="moves">Moves that the resulting <see cref="IEncounterable"/> must be able to learn.</param>
/// <param name="version">Specific version to iterate for.</param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
private static IEnumerable<IEncounterable> GenerateVersionEncounters(PKM pk, ushort[] moves, GameVersion version)
private static IEnumerable<IEncounterable> GenerateVersionEncounters(PKM pk, ReadOnlyMemory<ushort> 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<ushort> 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<ushort> moves, GameVersion ver)
private static ushort[] GetNeededMoves(PKM pk, ReadOnlySpan<ushort> moves, GameVersion ver, int generation)
{
if (pk.Species == (int)Species.Smeargle)
return Array.Empty<ushort>();
// 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<bool>.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<bool>.Shared.Return(rent);
if (vcBump)
pk.Version = (int)ver;
if (ctr == 0)
return Array.Empty<ushort>();
return result[..ctr].ToArray();
}
private static IEnumerable<IEncounterable> GetPossibleOfType(PKM pk, ushort[] needs, GameVersion version, EncounterTypeGroup type, EvoCriteria[] chain, IEncounterGenerator generator)
private static IEnumerable<IEncounterable> GetPossibleOfType(PKM pk, ReadOnlyMemory<ushort> 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
/// <param name="version">Specific version to iterate for. Necessary for retrieving possible Egg Moves.</param>
/// <param name="generator"></param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
private static IEnumerable<IEncounterable> GetEggs(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator)
private static IEnumerable<IEncounterable> GetEggs(PKM pk, ReadOnlyMemory<ushort> 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
/// <param name="version">Specific version to iterate for.</param>
/// <param name="generator">Generator</param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
private static IEnumerable<IEncounterable> GetGifts(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator)
private static IEnumerable<IEncounterable> GetGifts(PKM pk, ReadOnlyMemory<ushort> 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
/// <param name="version">Specific version to iterate for.</param>
/// <param name="generator"></param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
private static IEnumerable<IEncounterable> GetStatic(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator)
private static IEnumerable<IEncounterable> GetStatic(PKM pk, ReadOnlyMemory<ushort> 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
/// <param name="version">Specific version to iterate for.</param>
/// <param name="generator"></param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
private static IEnumerable<IEncounterable> GetTrades(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator)
private static IEnumerable<IEncounterable> GetTrades(PKM pk, ReadOnlyMemory<ushort> 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
/// <param name="version">Origin version</param>
/// <param name="generator"></param>
/// <returns>A consumable <see cref="IEncounterable"/> list of possible encounters.</returns>
private static IEnumerable<IEncounterable> GetSlots(PKM pk, ushort[] needs, EvoCriteria[] chain, GameVersion version, IEncounterGenerator generator)
private static IEnumerable<IEncounterable> GetSlots(PKM pk, ReadOnlyMemory<ushort> 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;
}
}

View file

@ -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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> evos)
{
int max = 0;
foreach (var evo in evos)

View file

@ -8,6 +8,8 @@ namespace PKHeX.Core;
/// </summary>
public static class EvolutionVerifier
{
private static readonly CheckResult VALID = new(CheckIdentifier.Evolution);
/// <summary>
/// Verifies Evolution scenarios of an <see cref="IEncounterable"/> for an input <see cref="PKM"/> and relevant <see cref="LegalInfo"/>.
/// </summary>
@ -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);
/// <summary>
/// Checks if the Evolution from the source <see cref="IEncounterable"/> is valid.
/// </summary>
/// <param name="pk">Source data to verify</param>
/// <param name="info">Source supporting information to verify with</param>
/// <param name="history">Source supporting information to verify with</param>
/// <param name="enc">Matched encounter</param>
/// <returns>Evolution is valid or not</returns>
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;
}
}

View file

@ -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 <see cref="pk"/>
/// </summary>
/// <param name="pk">Current state of the Pokémon</param>
/// <param name="generation">Original Generation</param>
/// <returns>Possible origin species-form-levels to match against encounter data.</returns>
/// <remarks>Use <see cref="GetOriginChain12"/> if the <see cref="pk"/> originated from Generation 1 or 2.</remarks>
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);
}
/// <summary>
@ -31,150 +29,67 @@ public static class EncounterOrigin
/// <returns>Possible origin species-form-levels to match against encounter data.</returns>
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<EvoCriteria>();
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;
}
/// <summary>
/// Species introduced in Generation 2 that require a level up to evolve into from a specimen that originated in a previous generation.
/// </summary>
private static readonly HashSet<ushort> 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),
};
/// <summary>
/// Species introduced in Generation 4 that require a level up to evolve into from a specimen that originated in a previous generation.
/// </summary>
private static readonly HashSet<ushort> 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;
}
/// <summary>
/// Species introduced in Generation 4 that used to require a level up to evolve prior to Generation 8.
/// </summary>
private static readonly HashSet<ushort> 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;
}
/// <summary>
/// Species introduced in Generation 6+ that require a level up to evolve into from a specimen that originated in a previous generation.
/// </summary>
private static readonly Dictionary<ushort, byte> 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;
}
}

View file

@ -1,7 +1,5 @@
using System;
using static PKHeX.Core.Legal;
namespace PKHeX.Core;
/// <summary>
@ -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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> result = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions];
int count = GetOriginChain(result, pk, enc, encSpecies, discard);
if (count == 0)
return Array.Empty<EvoCriteria>();
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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> result)
{
// return the count of species != 0
int count = 0;
foreach (var evo in result)
{
if (evo.Species == 0)
break;
count++;
}
return count;
}
}

View file

@ -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<EvoCriteria> chain, EvolutionOrigin enc)
public void DiscardForOrigin(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc)
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc)
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc)
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc)
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc)
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> chain, EvolutionOrigin enc)
public void DiscardForOrigin(Span<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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,
};
}

View file

@ -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<EvoCriteria> result, PKM pk) => EvolutionUtil.Discard(result, Personal);
public bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc)
public int Devolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> 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<EvoCriteria>();
if (pk is not PA8)
history.Gen8a = Array.Empty<EvoCriteria>();
if (pk is not PB8)
history.Gen8b = Array.Empty<EvoCriteria>();
}
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<EvoCriteria>() : evos.AsSpan(0, evos.Length - 1).ToArray();
}
}
private static ReadOnlySpan<EvoCriteria> 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<EvoCriteria> local, ReadOnlySpan<EvoCriteria> other1, ReadOnlySpan<EvoCriteria> other2)
{
for (int i = 0; i < local.Length; i++)
{
ReplaceIfBetter(local, other1, i);
ReplaceIfBetter(local, other2, i);
}
}
private static void ReplaceIfBetter(Span<EvoCriteria> local, ReadOnlySpan<EvoCriteria> 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<EvoCriteria> 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<T>(PKM pk, ReadOnlySpan<EvoCriteria> 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<EvoCriteria>();
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>(T pt, ReadOnlySpan<EvoCriteria> 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;
}
}

View file

@ -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<EvoCriteria> 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<T>(PKM pk, ReadOnlySpan<EvoCriteria> 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<EvoCriteria>();
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>(T pt, ReadOnlySpan<EvoCriteria> 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;
}
}

View file

@ -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<EvoCriteria> 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.
}
/// <summary>
/// Checks if we should check all adjacent evolution sources in addition to the current one.
/// </summary>
/// <returns>True if we should check all adjacent evolution sources.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CheckAllAdjacent(PKM pk, EvolutionOrigin enc) => enc.SkipChecks || pk is IHomeTrack { HasTracker: true } || !ParseSettings.IgnoreTransferIfNoTracker;
public int Devolve(Span<EvoCriteria> result, PKM pk, EvolutionOrigin enc)
{
if (CheckAllAdjacent(pk, enc))
return DevolveMulti(result, pk, enc);
return DevolveSingle(result, pk, enc);
}
public int Evolve(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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);
}

View file

@ -1,4 +1,3 @@
using System;
using static PKHeX.Core.EntityContext;
namespace PKHeX.Core;
@ -8,15 +7,10 @@ namespace PKHeX.Core;
/// </summary>
public static class EvolutionGroupUtil
{
/// <summary>
/// Gets the <see cref="IEvolutionGroup"/> for the <see cref="pk"/>.
/// </summary>
public static IEvolutionGroup GetCurrentGroup(PKM pk) => GetCurrentGroup(pk.Context);
/// <summary>
/// Gets the <see cref="IEvolutionGroup"/> for the <see cref="EntityContext"/>.
/// </summary>
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,
};
}

View file

@ -0,0 +1,12 @@
namespace PKHeX.Core;
/// <summary>
/// Details about the original encounter.
/// </summary>
/// <param name="Species">Species the encounter originated as</param>
/// <param name="Version">Version the encounter originated on</param>
/// <param name="Generation">Generation the encounter originated in</param>
/// <param name="LevelMin">Minimum level the encounter originated at</param>
/// <param name="LevelMax">Maximum level in final state</param>
/// <param name="SkipChecks">Skip enforcement of legality for evolution criteria</param>
public readonly record struct EvolutionOrigin(ushort Species, byte Version, byte Generation, byte LevelMin, byte LevelMax, bool SkipChecks = false);

View file

@ -0,0 +1,172 @@
using System;
namespace PKHeX.Core;
internal static class EvolutionUtil
{
public static EvoCriteria[] SetHistory<T>(Span<EvoCriteria> 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<EvoCriteria> result)
{
if (result.Length == 0)
return Array.Empty<EvoCriteria>();
var array = result.ToArray();
var length = CleanEvolve(array);
if (length != array.Length)
Array.Resize(ref array, length);
return array;
}
public static void Discard<T>(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> result, ushort species)
{
foreach (ref readonly var z in result)
{
if (z.Species == species)
return true;
}
return false;
}
/// <summary>
/// Revises the <see cref="result"/> to account for a new maximum <see cref="level"/>.
/// </summary>
public static void UpdateCeiling(Span<EvoCriteria> result, int level)
{
foreach (ref var evo in result)
{
if (evo.Species == 0)
break;
evo = evo with { LevelMax = (byte)Math.Min(evo.LevelMax, level) };
}
}
/// <summary>
/// Revises the <see cref="result"/> to account for a new minimum <see cref="level"/>.
/// </summary>
public static void UpdateFloor(Span<EvoCriteria> result, int level)
{
foreach (ref var evo in result)
{
if (evo.Species == 0)
break;
evo = evo with { LevelMin = (byte)Math.Max(evo.LevelMin, level) };
}
}
/// <summary>
/// Mutates the result to leave placeholder data for the species that the <see cref="encSpecies"/> evolves into.
/// </summary>
/// <param name="result">All evolutions for the species.</param>
/// <param name="encSpecies">Species encountered as.</param>
public static void ConditionBaseChainForward(Span<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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;
}
}

View file

@ -17,18 +17,45 @@ public interface IEvolutionGroup
/// </summary>
IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc);
bool Append(PKM pk, EvolutionHistory history, ref ReadOnlySpan<EvoCriteria> chain, EvolutionOrigin enc);
/// <summary>
/// Walks down the evolution chain to find previous evolutions.
/// </summary>
/// <param name="result">Array to store results in.</param>
/// <param name="pk">PKM to check.</param>
/// <param name="enc">Cached metadata about the PKM.</param>
/// <returns>Number of results found.</returns>
int Devolve(Span<EvoCriteria> result, PKM pk, EvolutionOrigin enc);
EvoCriteria[] GetInitialChain(PKM pk, EvolutionOrigin enc, ushort species, byte form);
/// <summary>
/// Walks up the evolution chain to find the evolution path.
/// </summary>
/// <param name="result">Array to store best results in.</param>
/// <param name="pk">PKM to check.</param>
/// <param name="enc">Cached metadata about the PKM.</param>
/// <param name="history">History of evolutions to cache arrays for individual contexts.</param>
/// <returns>Number of results found.</returns>
int Evolve(Span<EvoCriteria> result, PKM pk, EvolutionOrigin enc, EvolutionHistory history);
/// <summary>
/// Discards all entries that do not exist in the group.
/// </summary>
void DiscardForOrigin(Span<EvoCriteria> result, PKM pk);
}
/// <summary>
/// Details about the original encounter.
/// Provides information about how to evolve to the next evolution stage.
/// </summary>
/// <param name="Species">Species the encounter originated as</param>
/// <param name="Version">Version the encounter originated on</param>
/// <param name="Generation">Generation the encounter originated in</param>
/// <param name="LevelMin">Minimum level the encounter originated at</param>
/// <param name="LevelMax">Maximum level in final state</param>
/// <param name="SkipChecks">Skip enforcement of legality for evolution criteria</param>
public readonly record struct EvolutionOrigin(ushort Species, byte Version, byte Generation, byte LevelMin, byte LevelMax, bool SkipChecks = false);
public interface IEvolutionEnvironment
{
/// <summary>
/// Attempts to devolve the provided <see cref="head"/> to the previous evolution.
/// </summary>
/// <returns>True if the de-evolution was successful and <see cref="result"/> should be used.</returns>
bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result);
/// <summary>
/// Attempts to evolve the provided <see cref="head"/> to the next evolution.
/// </summary>
/// <returns>True if the evolution was successful and <see cref="result"/> should be used.</returns>
bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result);
}

View file

@ -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<EvoCriteria> 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<EvoCriteria> 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;
}
}

View file

@ -1,35 +0,0 @@
using System.Collections.Generic;
namespace PKHeX.Core;
internal static class EvolutionLegality
{
internal static readonly HashSet<ushort> 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,
};
}

View file

@ -1,160 +0,0 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Evolution Reversal logic
/// </summary>
public static class EvolutionReversal
{
/// <summary>
/// Reverses from current state to see what evolutions the <see cref="pk"/> may have existed as.
/// </summary>
/// <param name="lineage">Evolution Method lineage reversal object</param>
/// <param name="species">Species to devolve from</param>
/// <param name="form">Form to devolve from</param>
/// <param name="pk">Entity reference to sanity check evolutions with</param>
/// <param name="levelMin">Minimum level the entity may exist as</param>
/// <param name="levelMax">Maximum the entity may exist as</param>
/// <param name="maxSpeciesID">Maximum species ID that may exist</param>
/// <param name="skipChecks">Option to bypass some evolution criteria</param>
/// <param name="stopSpecies">Species ID that should be the last node, if at all.</param>
/// <returns>Reversed evolution lineage, with the lowest index being the current species-form.</returns>
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<EvoCriteria> 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<EvoCriteria> 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<EvoCriteria> 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 };
}
}

View file

@ -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<EvolutionMethod[]> GetArray(ReadOnlySpan<byte> data, int maxSpecies)
public static EvolutionMethod[][] GetArray(ReadOnlySpan<byte> 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<byte> data, ref int offset)
{
var count = data[offset++];
if (count == 0)
return Array.Empty<EvolutionMethod>();
const int bpe = 3;
for (int i = 0; i < evos.Length; i++)
{
int count = data[ofs];
ofs++;
if (count == 0)
{
evos[i] = Array.Empty<EvolutionMethod>();
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;
}
}

View file

@ -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<EvolutionMethod[]> GetArray(ReadOnlySpan<byte> data)
public static EvolutionMethod[][] GetArray(ReadOnlySpan<byte> data)
{
var evos = new EvolutionMethod[Legal.MaxSpeciesID_3 + 1][];
evos[0] = Array.Empty<EvolutionMethod>();
var result = new EvolutionMethod[Legal.MaxSpeciesID_3 + 1][];
result[0] = Array.Empty<EvolutionMethod>();
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<EvolutionMethod>();
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<byte> data, ushort index)
{
var span = data.Slice(index * (maxCount * size), maxCount * size);
int count = ScanCountEvolutions(span);
if (count == 0)
return Array.Empty<EvolutionMethod>();
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<byte> span)
{
for (int count = 0; count < maxCount; count++)
{
if (span[count * size] == 0)
return count;
}
return maxCount;
}
}

View file

@ -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<EvolutionMethod[]> GetArray(ReadOnlySpan<byte> 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<byte> 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<byte> data)
{
int count = ScanCountEvolutions(data);
if (count == 0)
return Array.Empty<EvolutionMethod>();
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<byte> 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<EvolutionMethod>();
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;
}
}

View file

@ -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<EvolutionMethod[]> GetArray(ReadOnlySpan<byte> data)
public static EvolutionMethod[][] GetArray(ReadOnlySpan<byte> 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<EvolutionMethod>();
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<byte> data)
{
var count = ScanCountEvolutions(data);
if (count == 0)
return Array.Empty<EvolutionMethod>();
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<byte> data)

View file

@ -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<byte> 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<byte> 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<byte> 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<EvolutionMethod[]> GetArray(BinLinkerAccessor data)
{
var evos = new EvolutionMethod[data.Length][];
for (int i = 0; i < evos.Length; i++)
evos[i] = GetMethods(data[i]);
return evos;
}
}

View file

@ -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<byte> 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<byte> data, byte levelUp)
{
if (data.Length == 0)
return Array.Empty<EvolutionMethod>();
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<byte> entry, bool levelUpBypass)
private static EvolutionMethod GetMethod(ReadOnlySpan<byte> 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<EvolutionMethod[]> 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;
}
}

View file

@ -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;
/// <remarks>
/// Used to determine if a <see cref="PKM.Species"/> can evolve from prior steps in its evolution branch.
/// </remarks>
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<byte> 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<EvolutionMethod[]> 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<byte> 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<EvolutionMethod[]> GetEntries(ReadOnlySpan<byte> 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<EvolutionMethod[]> 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<PKM, bool> func)
{
var key = GetLookupKey(species, form);
ref var node = ref Lineage[key];
node.Ban(func);
}
#endregion
/// <summary>
/// Gets a list of evolutions for the input <see cref="PKM"/> by checking each evolution in the chain.
/// </summary>
/// <param name="pk">Pokémon data to check with.</param>
/// <param name="levelMax">Maximum level to permit before the chain breaks.</param>
/// <param name="maxSpeciesOrigin">Maximum species ID to permit within the chain.</param>
/// <param name="skipChecks">Ignores an evolution's criteria, causing the returned list to have all possible evolutions.</param>
/// <param name="levelMin">Minimum level to permit before the chain breaks.</param>
/// <param name="stopSpecies">Final species to stop at, if known</param>
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;
}
/// <summary>
/// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve to &amp; from, yielded in order of increasing evolution stage.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Form ID</param>
/// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
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);
}
/// <summary>
/// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve from, yielded in order of increasing evolution stage.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Form ID</param>
/// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
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;
}
}
/// <summary>
/// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve to, yielded in order of increasing evolution stage.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Form ID</param>
/// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
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;
}
}
/// <summary>
/// Generates the reverse evolution path for the input <see cref="pk"/>.
/// </summary>
/// <param name="species">Entity Species to begin the chain</param>
/// <param name="form">Entity Form to begin the chain</param>
/// <param name="pk">Entity data</param>
/// <param name="levelMin">Minimum level</param>
/// <param name="levelMax">Maximum level</param>
/// <param name="maxSpeciesID">Clamp for maximum species ID</param>
/// <param name="skipChecks">Skip the secondary checks that validate the evolution</param>
/// <param name="stopSpecies">Final species to stop at, if known</param>
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);
}
}

View file

@ -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<EvolutionMethod> 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<EvolutionMethod> 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,
};
}

View file

@ -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<EvolutionMethod> GetForward(ushort species, byte form)
{
var arr = Entries;
if (species >= arr.Length)
return Array.Empty<EvolutionMethod>();
return arr[species];
}
private IEnumerable<(ushort Species, byte Form)> GetEvolutions(ReadOnlyMemory<EvolutionMethod> 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,
};
}

View file

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
public interface IEvolutionForward
{
ReadOnlyMemory<EvolutionMethod> GetForward(ushort species, byte form);
/// <summary>
/// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve to, yielded in order of increasing evolution stage.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Form ID</param>
/// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
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);
}

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
public interface IEvolutionNetwork
{
IEvolutionForward Forward { get; }
IEvolutionReverse Reverse { get; }
}
/// <summary>
/// Base abstraction for <see cref="EvolutionTree"/>
/// </summary>
public abstract class EvolutionNetwork : IEvolutionNetwork
{
public IEvolutionForward Forward { get; }
public IEvolutionReverse Reverse { get; }
protected EvolutionNetwork(IEvolutionForward forward, IEvolutionReverse reverse)
{
Forward = forward;
Reverse = reverse;
}
/// <summary>
/// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve to &amp; from, yielded in order of increasing evolution stage.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Form ID</param>
/// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
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<EvoCriteria> 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);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,13 @@
namespace PKHeX.Core;
public enum EvolutionCheckResult
{
Valid = 0,
InsufficientLevel,
BadGender,
BadForm,
WrongEC,
VisitVersion,
LowContestStat,
Untraded,
}

View file

@ -1,4 +1,5 @@
using static PKHeX.Core.EvolutionType;
using static PKHeX.Core.EvolutionCheckResult;
namespace PKHeX.Core;
@ -11,7 +12,7 @@ namespace PKHeX.Core;
/// <param name="Argument">Conditional Argument (different from <see cref="Level"/>)</param>
/// <param name="Level">Conditional Argument (different from <see cref="Argument"/>)</param>
/// <param name="LevelUp">Indicates if a level up is required to trigger evolution.</param>
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
{
/// <summary>Evolve to Species</summary>
public ushort Species { get; } = Species;
@ -56,88 +57,61 @@ public readonly record struct EvolutionMethod(EvolutionType Method, ushort Speci
/// </summary>
/// <param name="pk">Entity to check</param>
/// <param name="lvl">Current level</param>
/// <param name="levelMin">Minimum level to sanity check with</param>
/// <param name="skipChecks">Option to skip some comparisons to return a 'possible' evolution.</param>
/// <returns>True if a evolution criteria is valid.</returns>
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)

View file

@ -78,6 +78,11 @@ public enum EvolutionType : byte
UseItemFullMoon = 90, // Ursaluna
UseMoveAgileStyle = 91, // Wyrdeer
UseMoveStrongStyle = 92, // Overqwil
/// <summary>
/// Used as a placeholder sentinel for internal logic.
/// </summary>
Invalid = unchecked((byte)-1),
}
/// <summary>

View file

@ -1,34 +1,9 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Links a <see cref="EvolutionMethod"/> to the source <see cref="Species"/> and <see cref="Form"/> that the method can be triggered from.
/// </summary>
public struct EvolutionLink
public readonly record struct EvolutionLink(EvolutionMethod Method, ushort Species, byte Form)
{
private Func<PKM, bool>? 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<PKM, bool> check) => IsBanned = check;
/// <summary>
/// Checks if the <see cref="Method"/> is allowed.
/// </summary>
/// <param name="pk">Entity to check</param>
/// <returns>True if banned, false if allowed.</returns>
public bool IsEvolutionBanned(PKM pk) => IsBanned != null && IsBanned(pk);
}

View file

@ -26,20 +26,4 @@ public struct EvolutionNode
else
throw new InvalidOperationException($"{nameof(EvolutionNode)} already has two links.");
}
/// <summary>
/// Registers a function that disallows the reverse evolution link from being valid if the <see cref="func"/> is not satisfied.
/// </summary>
/// <param name="func">Function that checks if the link should be allowed as an evolution path.</param>
public void Ban(Func<PKM, bool> 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);
}
}

View file

@ -0,0 +1,84 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Evolution Reversal logic
/// </summary>
public static class EvolutionReversal
{
/// <summary>
/// Reverses from current state to see what evolutions the <see cref="pk"/> may have existed as.
/// </summary>
/// <param name="lineage">Evolution Method lineage reversal object</param>
/// <param name="result">Result storage</param>
/// <param name="species">Species to devolve from</param>
/// <param name="form">Form to devolve from</param>
/// <param name="pk">Entity reference to sanity check evolutions with</param>
/// <param name="levelMin">Minimum level the entity may exist as</param>
/// <param name="levelMax">Maximum the entity may exist as</param>
/// <param name="stopSpecies">Species ID that should be the last node, if at all. Provide 0 to fully devolve.</param>
/// <param name="skipChecks">Option to bypass some evolution criteria</param>
/// <returns>Reversed evolution lineage, with the lowest index being the current species-form.</returns>
public static int Devolve(this IEvolutionLookup lineage, Span<EvoCriteria> 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<EvoCriteria> 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,
};
}

View file

@ -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<int, int> 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<int, int>(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; }
}

View file

@ -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<EvoCriteria> 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);
}
}

View file

@ -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<EvoCriteria> 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);
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
public interface IEvolutionReverse
{
EvolutionNode GetReverse(ushort species, byte form);
/// <summary>
/// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve from, yielded in order of increasing evolution stage.
/// </summary>
/// <param name="species">Species ID</param>
/// <param name="form">Form ID</param>
/// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
IEnumerable<(ushort Species, byte Form)> GetPreEvolutions(ushort species, byte form);
int Devolve(Span<EvoCriteria> 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);
}

View file

@ -7,6 +7,11 @@ namespace PKHeX.Core;
/// </summary>
public interface ILearnGroup
{
/// <summary>
/// Gets the maximum move ID that this group can learn.
/// </summary>
ushort MaxMoveID { get; }
/// <summary>
/// Gets the next group to traverse to continue checking moves.
/// </summary>

View file

@ -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
{

View file

@ -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
{

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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<MoveResult> result, ReadOnlySpan<ushort> current, EvoCriteria evo)

View file

@ -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<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, EvoCriteria evo, int stage)

View file

@ -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<MoveResult> result, ReadOnlySpan<ushort> current, EvoCriteria evo)

View file

@ -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<MoveResult> result, ReadOnlySpan<ushort> 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<MoveResult> result, ReadOnlySpan<ushort> 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;

View file

@ -0,0 +1,239 @@
using System;
using System.Buffers;
namespace PKHeX.Core;
/// <summary>
/// Encapsulates logic for HOME's Move Relearner feature.
/// </summary>
/// <remarks>
/// If the Entity knew a move at any point in its history, it can be relearned if the current format can learn it.
/// </remarks>
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<MoveResult> result, ReadOnlySpan<ushort> 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;
}
/// <summary>
/// Scan the results and remove any that are not valid for the game <see cref="local"/> game.
/// </summary>
/// <returns>True if all results are valid.</returns>
private static bool CleanPurge(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan<EvoCriteria> 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);
}
/// <summary>
/// Get the current HOME source for the given context.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
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<bool> 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>(T instance, Span<bool> result, PKM pk, EvolutionHistory history,
IEncounterTemplate enc,
MoveSourceType types, LearnOption option, ReadOnlySpan<EvoCriteria> evos, IHomeSource local) where T : ILearnGroup
{
var length = instance.MaxMoveID;
var rent = ArrayPool<bool>.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<bool>.Shared.Return(rent);
}
/// <summary>
/// For each move that is possible to learn in another game, check if it is possible to learn in the current game.
/// </summary>
/// <param name="result">Resulting array of moves that are possible to learn in the current game.</param>
/// <param name="pk">Entity to check.</param>
/// <param name="evos">Evolutions to check.</param>
/// <param name="types">Move source types to check.</param>
/// <param name="dest">Destination game to check.</param>
/// <param name="temp">Temporary array of moves that are possible to learn in the checked game.</param>
private static void LoopMerge(Span<bool> result, PKM pk, ReadOnlySpan<EvoCriteria> evos, MoveSourceType types, IHomeSource dest, Span<bool> 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<MoveResult> result, ReadOnlySpan<ushort> 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<bool> 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;
}

View file

@ -19,6 +19,7 @@ public enum LearnEnvironment : byte
/* Gen7 */ SM, USUM, GG,
/* Gen8 */ SWSH, BDSP, PLA,
/* Gen9 */ SV,
HOME,
}
/// <summary>

View file

@ -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);
}

View file

@ -77,11 +77,11 @@ public sealed class LearnSource4HGSS : ILearnSource<PersonalInfo4>, 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,
};

View file

@ -75,11 +75,11 @@ public sealed class LearnSource4Pt : LearnSource4, ILearnSource<PersonalInfo4>,
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,
};

View file

@ -73,11 +73,11 @@ public sealed class LearnSource5B2W2 : LearnSource5, ILearnSource<PersonalInfo5B
(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,

View file

@ -70,11 +70,11 @@ public sealed class LearnSource5BW : LearnSource5, ILearnSource<PersonalInfo5BW>
(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,

View file

@ -74,11 +74,11 @@ public sealed class LearnSource6AO : ILearnSource<PersonalInfo6AO>, 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,

View file

@ -71,11 +71,11 @@ public sealed class LearnSource6XY : ILearnSource<PersonalInfo6XY>, 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,

View file

@ -72,19 +72,19 @@ public sealed class LearnSource7SM : ILearnSource<PersonalInfo7>, 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,

Some files were not shown because too many files have changed in this diff Show more