More fixes/improvements

Improve cctor times for Breeding; direct calls for splitbreed checks; inlined binary searches via generated IL instead of hashset
Fix permitting alpha ribbon on non-alphas in Gen8/8a/8b, disallow Gen8a/8b ribbons for Gen7 Alolan Raichu
Improve some IL generation of EvoChain logic
Add xmldoc for new evotree additions
This commit is contained in:
Kurt 2023-07-08 12:46:46 -07:00
parent 79729de90c
commit 6d4cd60461
33 changed files with 300 additions and 249 deletions

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using static PKHeX.Core.GameVersion;
using static PKHeX.Core.Species;
@ -13,34 +12,35 @@ public static class Breeding
/// Checks if the game has a Daycare, and returns true if it does.
/// </summary>
/// <param name="game">Version ID to check for.</param>
public static bool CanGameGenerateEggs(GameVersion game) => GamesWithEggs.Contains(game);
private static readonly HashSet<GameVersion> GamesWithEggs = new()
public static bool CanGameGenerateEggs(GameVersion game) => game switch
{
GD, SI, C,
R, S, E, FR, LG,
D, P, Pt, HG, SS,
B, W, B2, W2,
X, Y, OR, AS,
SN, MN, US, UM,
SW, SH, BD, SP,
SL, VL,
R or S or E or FR or LG => true,
D or P or Pt or HG or SS => true,
B or W or B2 or W2 => true,
X or Y or OR or AS => true,
SN or MN or US or UM => true,
GD or SI or C => true,
SW or SH or BD or SP => true,
SL or VL => true,
GS,
GS => true,
_ => false,
};
/// <summary>
/// Species that have special handling for breeding.
/// </summary>
internal static readonly HashSet<ushort> MixedGenderBreeding = new()
private static bool IsMixedGenderBreed(ushort species) => species switch
{
(int)NidoranF,
(int)NidoranM,
(int)NidoranF => true,
(int)NidoranM => true,
(int)Volbeat,
(int)Illumise,
(int)Volbeat => true,
(int)Illumise => true,
(int)Indeedee, // male/female
(int)Indeedee => true, // male/female
_ => false,
};
/// <summary>
@ -53,39 +53,48 @@ public static class Breeding
var pi = PKX.Personal[species];
if (pi is { Genderless: false, OnlyMale: false })
return true;
if (MixedGenderBreeding.Contains(species))
if (IsMixedGenderBreed(species))
return true;
return false;
}
private static readonly HashSet<ushort> SplitBreed_3 = new()
{
// Incense
(int)Marill,
(int)Wobbuffet,
};
/// <summary>
/// Species that can yield a different baby species when bred.
/// </summary>
private static readonly HashSet<ushort> SplitBreed = new(SplitBreed_3)
public static bool IsSplitBreedNotBabySpecies(ushort species, int generation)
{
// Incense
(int)Chansey,
(int)MrMime,
(int)Snorlax,
(int)Sudowoodo,
(int)Mantine,
(int)Roselia,
(int)Chimecho,
};
internal static IReadOnlySet<ushort>? GetSplitBreedGeneration(int generation) => generation switch
{
3 => SplitBreed_3,
4 or 5 or 6 or 7 or 8 => SplitBreed,
if (generation == 3)
return IsSplitBreedNotBabySpecies3(species);
if (generation is 4 or 5 or 6 or 7 or 8)
return IsSplitBreedNotBabySpecies4(species);
// Gen9 does not have split-breed egg generation.
_ => null,
return false;
}
/// <summary>
/// Checks if the species can yield a different baby species when bred via incense in Generation 3.
/// </summary>
/// <remarks>
/// This is a special case for Marill and Wobbuffet, which can be bred with incense to yield Azurill and Wynaut respectively.
/// </remarks>
public static bool IsSplitBreedNotBabySpecies3(ushort species) => species is (ushort)Marill or (ushort)Wobbuffet;
/// <summary>
/// Checks if the species can yield a different baby species when bred via incense in Generation 4-8.
/// </summary>
public static bool IsSplitBreedNotBabySpecies4(ushort species) => species switch
{
(int)Marill => true,
(int)Wobbuffet => true,
(int)Chansey => true,
(int)MrMime => true,
(int)Snorlax => true,
(int)Sudowoodo => true,
(int)Mantine => true,
(int)Roselia => true,
(int)Chimecho => true,
_ => false,
};
/// <summary>
@ -93,7 +102,7 @@ public static class Breeding
/// </summary>
/// <remarks>Chained with the other 2 overloads for incremental checks with different parameters.</remarks>
/// <param name="species">Current species</param>
public static bool CanHatchAsEgg(ushort species) => !NoHatchFromEgg.Contains(species);
public static bool CanHatchAsEgg(ushort species) => IsAbleToHatchFromEgg(species);
/// <summary>
/// Checks if the <see cref="species"/>-<see cref="form"/> can exist as a hatched egg in the requested <see cref="context"/>.
@ -133,67 +142,69 @@ public static class Breeding
/// <summary>
/// Species that cannot hatch from an egg.
/// </summary>
private static readonly HashSet<ushort> NoHatchFromEgg = new()
private static bool IsAbleToHatchFromEgg(ushort species) => species switch
{
// Gen1
(int)Ditto,
(int)Articuno, (int)Zapdos, (int)Moltres,
(int)Mewtwo, (int)Mew,
(int)Ditto => false,
(int)Articuno or (int)Zapdos or (int)Moltres => false,
(int)Mewtwo or (int)Mew => false,
// Gen2
(int)Unown,
(int)Raikou, (int)Entei, (int)Suicune,
(int)Lugia, (int)HoOh, (int)Celebi,
(int)Unown => false,
(int)Raikou or (int)Entei or (int)Suicune => false,
(int)Lugia or (int)HoOh or (int)Celebi => false,
// Gen3
(int)Regirock, (int)Regice, (int)Registeel,
(int)Latias, (int)Latios,
(int)Kyogre, (int)Groudon, (int)Rayquaza,
(int)Jirachi, (int)Deoxys,
(int)Regirock or (int)Regice or (int)Registeel => false,
(int)Latias or (int)Latios => false,
(int)Kyogre or (int)Groudon or (int)Rayquaza => false,
(int)Jirachi or (int)Deoxys => false,
// Gen4
(int)Uxie, (int)Mesprit, (int)Azelf,
(int)Dialga, (int)Palkia, (int)Heatran,
(int)Regigigas, (int)Giratina, (int)Cresselia,
(int)Manaphy, (int)Darkrai, (int)Shaymin, (int)Arceus,
(int)Uxie or (int)Mesprit or (int)Azelf => false,
(int)Dialga or (int)Palkia or (int)Heatran => false,
(int)Regigigas or (int)Giratina or (int)Cresselia => false,
(int)Manaphy or (int)Darkrai or (int)Shaymin or (int)Arceus => false,
// Gen5
(int)Victini,
(int)Cobalion, (int)Terrakion, (int)Virizion,
(int)Tornadus, (int)Thundurus,
(int)Reshiram, (int)Zekrom,
(int)Landorus, (int)Kyurem,
(int)Keldeo, (int)Meloetta, (int)Genesect,
(int)Victini => false,
(int)Cobalion or (int)Terrakion or (int)Virizion => false,
(int)Tornadus or (int)Thundurus => false,
(int)Reshiram or (int)Zekrom => false,
(int)Landorus or (int)Kyurem => false,
(int)Keldeo or (int)Meloetta or (int)Genesect => false,
// Gen6
(int)Xerneas, (int)Yveltal, (int)Zygarde,
(int)Diancie, (int)Hoopa, (int)Volcanion,
(int)Xerneas or (int)Yveltal or (int)Zygarde => false,
(int)Diancie or (int)Hoopa or (int)Volcanion => false,
// Gen7
(int)TypeNull, (int)Silvally,
(int)TapuKoko, (int)TapuLele, (int)TapuBulu, (int)TapuFini,
(int)Cosmog, (int)Cosmoem, (int)Solgaleo, (int)Lunala,
(int)Nihilego, (int)Buzzwole, (int)Pheromosa, (int)Xurkitree, (int)Celesteela, (int)Kartana, (int)Guzzlord, (int)Necrozma,
(int)Magearna, (int)Marshadow,
(int)Poipole, (int)Naganadel, (int)Stakataka, (int)Blacephalon, (int)Zeraora,
(int)TypeNull or (int)Silvally => false,
(int)TapuKoko or (int)TapuLele or (int)TapuBulu or (int)TapuFini => false,
(int)Cosmog or (int)Cosmoem or (int)Solgaleo or (int)Lunala => false,
(int)Nihilego or (int)Buzzwole or (int)Pheromosa or (int)Xurkitree or (int)Celesteela or (int)Kartana or (int)Guzzlord or (int)Necrozma => false,
(int)Magearna or (int)Marshadow => false,
(int)Poipole or (int)Naganadel or (int)Stakataka or (int)Blacephalon or (int)Zeraora => false,
(int)Meltan, (int)Melmetal,
(int)Meltan or (int)Melmetal => false,
// Gen8
(int)Dracozolt, (int)Arctozolt, (int)Dracovish, (int)Arctovish,
(int)Zacian, (int)Zamazenta, (int)Eternatus,
(int)Kubfu, (int)Urshifu, (int)Zarude,
(int)Regieleki, (int)Regidrago,
(int)Glastrier, (int)Spectrier, (int)Calyrex,
(int)Enamorus,
(int)Dracozolt or (int)Arctozolt or (int)Dracovish or (int)Arctovish => false,
(int)Zacian or (int)Zamazenta or (int)Eternatus => false,
(int)Kubfu or (int)Urshifu or (int)Zarude => false,
(int)Regieleki or (int)Regidrago => false,
(int)Glastrier or (int)Spectrier or (int)Calyrex => false,
(int)Enamorus => false,
// Gen9
(int)GreatTusk, (int)ScreamTail, (int)BruteBonnet, (int)FlutterMane, (int)SlitherWing, (int)SandyShocks,
(int)IronTreads, (int)IronBundle, (int)IronHands, (int)IronJugulis, (int)IronMoth, (int)IronThorns,
(int)Gimmighoul, (int)Gholdengo,
(int)WoChien, (int)ChienPao, (int)TingLu, (int)ChiYu,
(int)RoaringMoon, (int)IronValiant,
(int)Koraidon, (int)Miraidon,
(int)WalkingWake, (int)IronLeaves,
(int)GreatTusk or (int)ScreamTail or (int)BruteBonnet or (int)FlutterMane or (int)SlitherWing or (int)SandyShocks => false,
(int)IronTreads or (int)IronBundle or (int)IronHands or (int)IronJugulis or (int)IronMoth or (int)IronThorns => false,
(int)Gimmighoul or (int)Gholdengo => false,
(int)WoChien or (int)ChienPao or (int)TingLu or (int)ChiYu => false,
(int)RoaringMoon or (int)IronValiant => false,
(int)Koraidon or (int)Miraidon => false,
(int)WalkingWake or (int)IronLeaves => false,
_ => true,
};
}

View file

@ -349,7 +349,7 @@ public sealed class EncounterGenerator3 : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies3(devolved.Species))
yield break; // no split-breed.
// Sanity Check 1
@ -372,10 +372,7 @@ public sealed class EncounterGenerator3 : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies3(devolved.Species))
yield break;
species = devolved.Species;
@ -383,12 +380,6 @@ public sealed class EncounterGenerator3 : IEncounterGenerator
yield return CreateEggEncounter(species, form, version);
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version)
{
if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Castform)

View file

@ -345,7 +345,7 @@ public sealed class EncounterGenerator4 : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break; // not a split-breed.
// Sanity Check 1
@ -368,10 +368,7 @@ public sealed class EncounterGenerator4 : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break;
species = devolved.Species;
@ -379,12 +376,6 @@ public sealed class EncounterGenerator4 : IEncounterGenerator
yield return CreateEggEncounter(species, form, version);
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version)
{
if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform)

View file

@ -276,7 +276,7 @@ public sealed class EncounterGenerator5 : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break; // not a split-breed.
// Sanity Check 1
@ -300,10 +300,7 @@ public sealed class EncounterGenerator5 : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break;
species = devolved.Species;
@ -311,12 +308,6 @@ public sealed class EncounterGenerator5 : IEncounterGenerator
yield return CreateEggEncounter(species, form, version);
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version)
{
if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform)

View file

@ -332,7 +332,7 @@ public sealed class EncounterGenerator6 : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break; // not a split-breed.
// Sanity Check 1
@ -360,10 +360,7 @@ public sealed class EncounterGenerator6 : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break;
species = devolved.Species;
@ -374,12 +371,6 @@ public sealed class EncounterGenerator6 : IEncounterGenerator
yield return egg with { Version = GetOtherGamePair(version) };
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static GameVersion GetOtherGamePair(GameVersion version)
{
// 24 -> 26 ( X -> AS)

View file

@ -318,7 +318,7 @@ public sealed class EncounterGenerator7 : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break; // not a split-breed.
// Sanity Check 1
@ -346,10 +346,7 @@ public sealed class EncounterGenerator7 : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break;
species = devolved.Species;
@ -360,12 +357,6 @@ public sealed class EncounterGenerator7 : IEncounterGenerator
yield return egg with { Version = GetOtherGamePair(version) };
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static GameVersion GetOtherGamePair(GameVersion version)
{
// 30 -> 32 (SN -> US)

View file

@ -258,7 +258,7 @@ public sealed class EncounterGenerator8 : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break; // not a split-breed.
// Sanity Check 1
@ -280,21 +280,12 @@ public sealed class EncounterGenerator8 : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break;
yield return CreateEggEncounter(devolved.Species, devolved.Form, version);
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version)
{
if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform)

View file

@ -394,7 +394,7 @@ public sealed class EncounterGenerator8b : IEncounterGenerator
// Ensure most devolved species is the same as the egg species.
var (species, form) = GetBaby(devolved);
if (species != devolved.Species && !IsValidBabySpecies(devolved.Species))
if (species != devolved.Species && !Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break; // not a split-breed.
// Sanity Check 1
@ -416,21 +416,12 @@ public sealed class EncounterGenerator8b : IEncounterGenerator
yield break; // no split-breed
devolved = chain[^2];
}
var splitSet = Breeding.GetSplitBreedGeneration(Generation);
if (splitSet is null)
yield break; // Shouldn't happen.
if (!splitSet.Contains(devolved.Species))
if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species))
yield break;
yield return CreateEggEncounter(devolved.Species, devolved.Form, version);
}
private static bool IsValidBabySpecies(ushort species)
{
var split = Breeding.GetSplitBreedGeneration(Generation);
return split is not null && split.Contains(species);
}
private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version)
{
if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform)

View file

@ -7,6 +7,11 @@ namespace PKHeX.Core;
/// </summary>
public static class EvolutionChain
{
/// <summary>
/// Build an <see cref="EvolutionHistory"/> for the given <paramref name="pk"/> and <paramref name="enc"/>.
/// </summary>
/// <param name="pk">Entity to search for.</param>
/// <param name="enc">Evolution details.</param>
public static EvolutionHistory GetEvolutionChainsAllGens(PKM pk, IEncounterTemplate enc)
{
var min = GetMinLevel(pk, enc);
@ -17,6 +22,19 @@ public static class EvolutionChain
return GetEvolutionChainsSearch(pk, origin, pk.Context, enc.Species);
}
/// <summary>
/// Build an <see cref="EvolutionHistory"/> for the given <paramref name="pk"/> and <paramref name="enc"/>.
/// </summary>
/// <param name="pk">Entity to search for.</param>
/// <param name="enc">Evolution details.</param>
/// <param name="context">Starting (original) context of the <paramref name="pk"/>.</param>
/// <param name="encSpecies">Encountered as species. If not known (search for all), set to 0.</param>
public static EvolutionHistory GetEvolutionChainsSearch(PKM pk, EvolutionOrigin enc, EntityContext context, ushort encSpecies = 0)
{
Span<EvoCriteria> chain = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions];
return EvolutionChainsSearch(pk, enc, context, encSpecies, chain);
}
private static byte GetMinLevel(PKM pk, IEncounterTemplate enc) => enc.Generation switch
{
2 => pk is ICaughtData2 c2 ? Math.Max((byte)c2.Met_Level, enc.LevelMin) : enc.LevelMin,
@ -24,12 +42,6 @@ public static class EvolutionChain
_ => 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();
@ -63,6 +75,13 @@ public static class EvolutionChain
return history;
}
/// <summary>
/// Gets a list of <see cref="EvoCriteria"/> that represent the possible original states of the <paramref name="pk"/>.
/// </summary>
/// <param name="pk">Entity to search for.</param>
/// <param name="enc">Evolution details.</param>
/// <param name="encSpecies">Encountered as species. If not known (search for all), set to 0.</param>
/// <param name="discard">Discard evolutions that are not possible for the original context. Pass false to keep all evolutions.</param>
public static EvoCriteria[] GetOriginChain(PKM pk, EvolutionOrigin enc, ushort encSpecies = 0, bool discard = true)
{
Span<EvoCriteria> result = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions];
@ -74,6 +93,15 @@ public static class EvolutionChain
return chain.ToArray();
}
/// <summary>
/// Gets a list of <see cref="EvoCriteria"/> that represent the possible original states of the <paramref name="pk"/>.
/// </summary>
/// <param name="result">Span to write results to.</param>
/// <param name="pk">Entity to search for.</param>
/// <param name="enc">Evolution details.</param>
/// <param name="encSpecies">Encountered as species. If not known (search for all), set to 0.</param>
/// <param name="discard">Discard evolutions that are not possible for the original context. Pass false to keep all evolutions.</param>
/// <returns>Number of valid evolutions found.</returns>
public static int GetOriginChain(Span<EvoCriteria> result, PKM pk, EvolutionOrigin enc, ushort encSpecies = 0, bool discard = true)
{
ushort species = enc.Species;
@ -111,11 +139,13 @@ public static class EvolutionChain
return GetCount(result);
}
private static int GetCount(Span<EvoCriteria> result)
/// <summary>
/// Gets the count of entries that are not empty (species == 0).
/// </summary>
private static int GetCount(in ReadOnlySpan<EvoCriteria> result)
{
// return the count of species != 0
int count = 0;
foreach (var evo in result)
foreach (ref readonly var evo in result)
{
if (evo.Species == 0)
break;

View file

@ -35,6 +35,8 @@ public sealed class EvolutionGroupHOME : IEvolutionGroup
Discard(result, PersonalTable.BDSP);
else if (pk.SWSH)
Discard(result, PersonalTable.SWSH);
else if (pk.GO && result.Length >= 2 && result[1].Species == (ushort)Species.Gimmighoul)
result[1] = result[1] with { Form = 1 }; // Roaming, exclusive GO form
// GO can be otherwise, don't discard any.
}
@ -128,6 +130,14 @@ public sealed class EvolutionGroupHOME : IEvolutionGroup
history.Gen8b = SetHistory(result, PersonalTable.BDSP);
history.Gen9 = SetHistory(result, PersonalTable.SV);
if (history.HasVisitedGen7)
{
// 0->X Alolan forms can't evolve after Gen7 (yet).
if (pk is { Species: (int)Species.Raichu, Form: 1 })
history.Gen8b = history.Gen8a = Array.Empty<EvoCriteria>();
// All others can't enter otherwise (not in game).
}
return present;
}

View file

@ -55,9 +55,9 @@ public sealed class EvolutionHistory
_ => throw new ArgumentOutOfRangeException(nameof(context), context, null),
};
public static bool HasVisited(ReadOnlySpan<EvoCriteria> evos, ushort species)
public static bool HasVisited(in ReadOnlySpan<EvoCriteria> evos, ushort species)
{
foreach (var evo in evos)
foreach (ref readonly var evo in evos)
{
if (evo.Species == species)
return true;

View file

@ -42,6 +42,11 @@ public sealed class EvolutionTree : EvolutionNetwork
return new EvolutionTree(forward, reverse);
}
/// <summary>
/// Get the <see cref="EvolutionTree"/> for the given <see cref="EntityContext"/>.
/// </summary>
/// <param name="context">Context to get the <see cref="EvolutionTree"/> for.</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static EvolutionTree GetEvolutionTree(EntityContext context) => context switch
{
EntityContext.Gen1 => Evolves1,

View file

@ -3,6 +3,9 @@ using System.Collections.Generic;
namespace PKHeX.Core;
/// <summary>
/// Provides forward evolution pathways with reliance on personal table data for form branched evolutions.
/// </summary>
public sealed class EvolutionForwardPersonal : IEvolutionForward
{
private readonly IPersonalTable Personal;

View file

@ -3,6 +3,9 @@ using System.Collections.Generic;
namespace PKHeX.Core;
/// <summary>
/// Provides forward evolution pathways based only on species.
/// </summary>
public sealed class EvolutionForwardSpecies : IEvolutionForward
{
private readonly EvolutionMethod[][] Entries;

View file

@ -3,8 +3,14 @@ using System.Collections.Generic;
namespace PKHeX.Core;
/// <summary>
/// Object storing forward paths for evolution nodes.
/// </summary>
public interface IEvolutionForward
{
/// <summary>
/// Gets the forward evolution paths for the given species and form.
/// </summary>
ReadOnlyMemory<EvolutionMethod> GetForward(ushort species, byte form);
/// <summary>
@ -15,5 +21,16 @@ public interface IEvolutionForward
/// <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);
/// <summary>
/// Tries to evolve the given <see cref="pk"/> to the next evolution stage.
/// </summary>
/// <param name="head">Current species and form to try evolving</param>
/// <param name="next">Expected species and form after evolution</param>
/// <param name="pk">Entity to evolve</param>
/// <param name="currentMaxLevel">Maximum allowed level for the result</param>
/// <param name="levelMin">Minimum level for the result</param>
/// <param name="skipChecks">Skip evolution exclusion checks</param>
/// <param name="result">Resulting evolution criteria</param>
/// <returns>True if the evolution is possible and <see cref="result"/> is valid.</returns>
bool TryEvolve(ISpeciesForm head, ISpeciesForm next, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result);
}

View file

@ -1,11 +1,20 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
/// <summary>
/// Exposes abstractions for reverse and forward evolution lookups.
/// </summary>
public interface IEvolutionNetwork
{
/// <summary>
/// Provides interactions to look forward in an evolution tree.
/// </summary>
IEvolutionForward Forward { get; }
/// <summary>
/// Provides interactions to look backward in an evolution tree.
/// </summary>
IEvolutionReverse Reverse { get; }
}
@ -38,7 +47,10 @@ public abstract class EvolutionNetwork : IEvolutionNetwork
yield return s;
}
public bool IsSpeciesDerivedFrom(ushort species, byte form, int otherSpecies, int otherForm, bool ignoreForm = true)
/// <summary>
/// Checks if the requested <see cref="species"/>-<see cref="form"/> is can provide any common evolutions with the <see cref="otherSpecies"/>-<see cref="otherForm"/>.
/// </summary>
public bool IsSpeciesDerivedFrom(ushort species, byte form, ushort otherSpecies, byte otherForm, bool ignoreForm = true)
{
var evos = GetEvolutionsAndPreEvolutions(species, form);
foreach (var (s, f) in evos)
@ -52,6 +64,9 @@ public abstract class EvolutionNetwork : IEvolutionNetwork
return false;
}
/// <summary>
/// Gets the base (baby) species and form of the given <see cref="species"/>-<see cref="form"/> pair.
/// </summary>
public (ushort Species, byte Form) GetBaseSpeciesForm(ushort species, byte form)
{
var chain = Reverse.GetPreEvolutions(species, form);
@ -59,10 +74,4 @@ public abstract class EvolutionNetwork : IEvolutionNetwork
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

@ -9,6 +9,7 @@ public struct EvolutionNode
{
/// <summary> First reverse evolution in the node. </summary>
public EvolutionLink First;
/// <summary> Second reverse evolution in the node. Often empty. </summary>
public EvolutionLink Second;

View file

@ -38,7 +38,7 @@ public static class EvolutionReversal
int ctr = 1; // count in the 'evos' span
while (head.Species != stopSpecies)
{
var node = lineage[head.Species, head.Form];
ref readonly var node = ref lineage[head.Species, head.Form];
if (!node.TryDevolve(pk, currentMaxLevel, levelMin, skipChecks, out var x))
return ctr;
@ -52,16 +52,30 @@ public static class EvolutionReversal
{
// 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;
ref readonly var link = ref node.First;
if (link.IsEmpty)
break;
{
result = default;
return false;
}
var chk = link.Method.Check(pk, currentMaxLevel, levelMin, skipChecks);
if (chk != EvolutionCheckResult.Valid)
continue;
if (chk == EvolutionCheckResult.Valid)
{
result = Create(link, currentMaxLevel);
return true;
}
link = ref node.Second;
if (link.IsEmpty)
{
result = default;
return false;
}
chk = link.Method.Check(pk, currentMaxLevel, levelMin, skipChecks);
if (chk == EvolutionCheckResult.Valid)
{
result = Create(link, currentMaxLevel);
return true;
}

View file

@ -18,9 +18,9 @@ public sealed class EvolutionReverseLookup : IEvolutionLookup
MaxSpecies = maxSpecies;
}
private void Register(EvolutionLink link, ushort species)
private void Register(EvolutionLink link, int index)
{
ref var node = ref Nodes[species];
ref var node = ref Nodes[index];
node.Add(link);
}
@ -32,15 +32,19 @@ public sealed class EvolutionReverseLookup : IEvolutionLookup
return;
}
int key = GetKey(species, form);
if (!KeyLookup.TryGetValue(key, out var index))
{
index = Nodes.Length - KeyLookup.Count - 1;
KeyLookup.Add(key, index);
int index = GetOrAppendIndex(species, form);
Register(link, index);
}
ref var node = ref Nodes[index];
node.Add(link);
private int GetOrAppendIndex(ushort species, byte form)
{
int key = GetKey(species, form);
if (KeyLookup.TryGetValue(key, out var index))
return index;
index = Nodes.Length - KeyLookup.Count - 1;
KeyLookup.Add(key, index);
return index;
}
private int GetIndex(ushort species, byte form)
@ -54,10 +58,5 @@ public sealed class EvolutionReverseLookup : IEvolutionLookup
}
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[ushort species, byte form] { get; }
public ref readonly EvolutionNode this[ushort species, byte form] => ref Nodes[GetIndex(species, form)];
}

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -6,11 +5,8 @@ namespace PKHeX.Core;
public sealed class EvolutionReversePersonal : IEvolutionReverse
{
public IEvolutionLookup Lineage { get; }
public EvolutionReversePersonal(EvolutionMethod[][] entries, IPersonalTable t)
{
Lineage = GetLineage(t, entries);
}
public EvolutionReversePersonal(EvolutionMethod[][] entries, IPersonalTable t) => Lineage = GetLineage(t, entries);
public ref readonly EvolutionNode GetReverse(ushort species, byte form) => ref Lineage[species, form];
private static EvolutionReverseLookup GetLineage(IPersonalTable t, EvolutionMethod[][] entries)
{
@ -37,8 +33,6 @@ public sealed class EvolutionReversePersonal : IEvolutionReverse
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];
@ -54,15 +48,9 @@ public sealed class EvolutionReversePersonal : IEvolutionReverse
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];
ref readonly var node = ref Lineage[head.Species, head.Form];
return node.TryDevolve(pk, currentMaxLevel, levelMin, skipChecks, out result);
}
}

View file

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -6,11 +5,8 @@ namespace PKHeX.Core;
public sealed class EvolutionReverseSpecies : IEvolutionReverse
{
public EvolutionReverseLookup Lineage { get; }
public EvolutionReverseSpecies(EvolutionMethod[][] entries, IPersonalTable t)
{
Lineage = GetLineage(t, entries);
}
public EvolutionReverseSpecies(EvolutionMethod[][] entries, IPersonalTable t) => Lineage = GetLineage(t, entries);
public ref readonly EvolutionNode GetReverse(ushort species, byte form) => ref Lineage[species, form];
private static EvolutionReverseLookup GetLineage(IPersonalTable t, EvolutionMethod[][] entries)
{
@ -39,8 +35,6 @@ public sealed class EvolutionReverseSpecies : IEvolutionReverse
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];
@ -56,15 +50,9 @@ public sealed class EvolutionReverseSpecies : IEvolutionReverse
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];
ref readonly var node = ref Lineage[head.Species, head.Form];
return node.TryDevolve(pk, currentMaxLevel, levelMin, skipChecks, out result);
}
}

View file

@ -0,0 +1,6 @@
namespace PKHeX.Core;
public interface IEvolutionLookup
{
ref readonly EvolutionNode this[ushort species, byte form] { get; }
}

View file

@ -1,11 +1,16 @@
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
/// <summary>
/// Object storing reversal paths for evolution nodes.
/// </summary>
public interface IEvolutionReverse
{
EvolutionNode GetReverse(ushort species, byte form);
/// <summary>
/// Gets the reverse evolution pathways for the given species and form.
/// </summary>
ref readonly 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.
@ -15,7 +20,15 @@ public interface IEvolutionReverse
/// <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);
/// <summary>
/// Tries to devolve the given <see cref="pk"/> to the next evolution stage.
/// </summary>
/// <param name="head">Current species and form to try devolving</param>
/// <param name="pk">Entity to devolve</param>
/// <param name="currentMaxLevel">Maximum allowed level for the result</param>
/// <param name="levelMin">Minimum level for the result</param>
/// <param name="skipChecks">Skip evolution exclusion checks</param>
/// <param name="result">Resulting evolution criteria</param>
/// <returns>True if the de-evolution is possible and <see cref="result"/> is valid.</returns>
bool TryDevolve(ISpeciesForm head, PKM pk, byte currentMaxLevel, byte levelMin, bool skipChecks, out EvoCriteria result);
}

View file

@ -9,6 +9,9 @@ namespace PKHeX.Core;
/// </summary>
public sealed class BaseLegalityFormatter : ILegalityFormatter
{
/// <summary>
/// Gets a minimal report string for the analysis.
/// </summary>
public string GetReport(LegalityAnalysis l)
{
if (l.Valid)
@ -20,6 +23,9 @@ public sealed class BaseLegalityFormatter : ILegalityFormatter
return string.Join(Environment.NewLine, lines);
}
/// <summary>
/// Gets a verbose report string for the analysis.
/// </summary>
public string GetReportVerbose(LegalityAnalysis l)
{
if (!l.Parsed)

View file

@ -9,7 +9,7 @@ namespace PKHeX.Core;
/// <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 sealed class LearnGroupHOME : ILearnGroup
{
public static readonly LearnGroupHOME Instance = new();
public ushort MaxMoveID => 0;

View file

@ -8,8 +8,16 @@ namespace PKHeX.Core;
/// </summary>
public static class LearnGroupUtil
{
/// <summary>
/// Get the <see cref="ILearnGroup"/> for the given <see cref="PKM"/>.
/// </summary>
public static ILearnGroup GetCurrentGroup(PKM pk) => GetCurrentGroup(pk.Context);
/// <summary>
/// Get the <see cref="ILearnGroup"/> for the given <see cref="EntityContext"/>.
/// </summary>
/// <param name="context">Context to get the <see cref="ILearnGroup"/> for.</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static ILearnGroup GetCurrentGroup(EntityContext context) => context switch
{
Gen1 => LearnGroup1.Instance,

View file

@ -172,8 +172,7 @@ public static class MoveListSuggest
// Split-breed species like Budew & Roselia may be legal for one, and not the other.
// If we're not a split-breed or are already legal, return.
var split = Breeding.GetSplitBreedGeneration(generation);
if (split?.Contains(enc.Species) != true)
if (!Breeding.IsSplitBreedNotBabySpecies(enc.Species, generation))
return;
var tmp = pk.Clone();

View file

@ -159,7 +159,7 @@ public sealed class MiscVerifier : Verifier
var enc = data.EncounterOriginal;
if (pk9 is { HeightScalar: 0, WeightScalar: 0 })
{
if (enc.Context.Generation() < 9 && enc is not EncounterSlot8GO && !data.Info.EvoChainsAllGens.HasVisitedPLA) // <=Gen8 rerolls height/weight, never zero.
if (enc.Context.Generation() < 9 && enc is not EncounterSlotGO && !data.Info.EvoChainsAllGens.HasVisitedPLA) // <=Gen8 rerolls height/weight, never zero.
data.AddLine(Get(LStatInvalidHeightWeight, Severity.Invalid, Encounter));
else if (CheckHeightWeightOdds(enc) && ParseSettings.ZeroHeightWeight != Severity.Valid)
data.AddLine(Get(LStatInvalidHeightWeight, ParseSettings.ZeroHeightWeight, Encounter));

View file

@ -125,7 +125,7 @@ public static class MarkRules
return true;
// Before HOME 3.0.0, this mark was never set.
return pk is PK8 or PB8 or PA8; // Not yet touched HOME 3.0.0
return wasAlpha && pk is PK8 or PB8 or PA8; // Not yet touched HOME 3.0.0
}
/// <summary>

View file

@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Side game data for <see cref="PA8"/> data transferred into HOME.
/// </summary>
public class GameDataPA8 : HomeOptional1, IGameDataSide<PA8>, IScaledSizeAbsolute, IScaledSize3
public sealed class GameDataPA8 : HomeOptional1, IGameDataSide<PA8>, IScaledSizeAbsolute, IScaledSize3, IGameDataSplitAbility
{
private const HomeGameDataFormat ExpectFormat = HomeGameDataFormat.PA8;
private const int SIZE = HomeCrypto.SIZE_2GAME_PA8;

View file

@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Side game data for <see cref="PB7"/> data transferred into HOME.
/// </summary>
public sealed class GameDataPB7 : HomeOptional1, IGameDataSide<PB7>, IScaledSizeAbsolute, IMemoryOT
public sealed class GameDataPB7 : HomeOptional1, IGameDataSide<PB7>, IScaledSizeAbsolute, IMemoryOT, IGameDataSplitAbility
{
private const HomeGameDataFormat ExpectFormat = HomeGameDataFormat.PB7;
private const int SIZE = HomeCrypto.SIZE_2GAME_PB7;

View file

@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Record Mixing Data for Generation 3 <see cref="SAV3"/> games.
/// </summary>
public class RecordMixing3Gift
public sealed class RecordMixing3Gift
{
/// <summary>
/// 0x8: Total Size of this object

View file

@ -12,6 +12,10 @@ public class MarshalTests
[InlineData(8, typeof(MoveResult))]
[InlineData(8, typeof(EvolutionMethod))]
[InlineData(8, typeof(Moveset))]
[InlineData(8, typeof(SCXorShift32))]
[InlineData(16, typeof(Xoroshiro128Plus))]
[InlineData(16, typeof(Xoroshiro128Plus8b))]
[InlineData(16, typeof(XorShift128))]
public void MarshalSizeExact(int expect, Type t) => Marshal.SizeOf(t).Should().Be(expect);
[Theory]