Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
|
|
|
using System;
|
2018-06-20 00:14:22 +00:00
|
|
|
|
2022-06-18 18:04:24 +00:00
|
|
|
namespace PKHeX.Core;
|
|
|
|
|
2022-08-21 08:39:16 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Logic to create an <see cref="EvolutionHistory"/>.
|
|
|
|
/// </summary>
|
2023-01-29 03:23:43 +00:00
|
|
|
public static class EvolutionChain
|
2018-06-20 00:14:22 +00:00
|
|
|
{
|
2023-07-08 19:46:46 +00:00
|
|
|
/// <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>
|
2022-08-21 08:39:16 +00:00
|
|
|
public static EvolutionHistory GetEvolutionChainsAllGens(PKM pk, IEncounterTemplate enc)
|
2018-06-20 00:14:22 +00:00
|
|
|
{
|
2023-07-06 04:14:09 +00:00
|
|
|
var min = GetMinLevel(pk, enc);
|
|
|
|
var origin = new EvolutionOrigin(pk.Species, (byte)enc.Version, (byte)enc.Generation, min, (byte)pk.CurrentLevel);
|
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
|
|
|
if (!pk.IsEgg && enc is not EncounterInvalid)
|
2023-07-06 04:14:09 +00:00
|
|
|
return GetEvolutionChainsSearch(pk, origin, enc.Context, enc.Species);
|
2018-06-20 00:50:10 +00:00
|
|
|
|
2023-07-06 04:14:09 +00:00
|
|
|
return GetEvolutionChainsSearch(pk, origin, pk.Context, enc.Species);
|
2022-06-18 18:04:24 +00:00
|
|
|
}
|
2018-06-21 04:38:56 +00:00
|
|
|
|
2023-07-08 19:46:46 +00:00
|
|
|
/// <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);
|
|
|
|
}
|
|
|
|
|
2023-07-06 04:14:09 +00:00
|
|
|
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,
|
|
|
|
<= 4 when pk.Format != enc.Generation => enc.LevelMin,
|
|
|
|
_ => Math.Max((byte)pk.Met_Level, enc.LevelMin),
|
|
|
|
};
|
|
|
|
|
|
|
|
private static EvolutionHistory EvolutionChainsSearch(PKM pk, EvolutionOrigin enc, EntityContext context, ushort encSpecies, Span<EvoCriteria> chain)
|
|
|
|
{
|
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
|
|
|
var history = new EvolutionHistory();
|
2023-09-05 03:10:40 +00:00
|
|
|
var length = GetOriginChain(chain, pk, enc, encSpecies, enc.IsDiscardRequired(pk.Format));
|
2023-07-06 04:14:09 +00:00
|
|
|
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)
|
|
|
|
{
|
2023-08-20 07:31:54 +00:00
|
|
|
// Handle the evolution case for Gen2->Gen1
|
2023-07-06 04:14:09 +00:00
|
|
|
EvolutionGroup2.Instance.Evolve(chain, pk, enc, history);
|
|
|
|
EvolutionGroup1.Instance.Evolve(chain, pk, enc, history);
|
2023-08-20 07:31:54 +00:00
|
|
|
if (pk.Format > 2) // Skip forward to Gen7
|
2023-07-06 04:14:09 +00:00
|
|
|
context = EntityContext.Gen7;
|
2023-08-20 07:31:54 +00:00
|
|
|
else // no more possible contexts; done.
|
2023-07-06 04:14:09 +00:00
|
|
|
return history;
|
|
|
|
}
|
|
|
|
|
|
|
|
var group = EvolutionGroupUtil.GetGroup(context);
|
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
|
|
|
while (true)
|
2018-06-20 00:14:22 +00:00
|
|
|
{
|
2023-07-06 04:14:09 +00:00
|
|
|
group.Evolve(chain, pk, enc, history);
|
|
|
|
var previous = group.GetNext(pk, enc);
|
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
|
|
|
if (previous is null)
|
|
|
|
break;
|
|
|
|
group = previous;
|
2022-06-18 18:04:24 +00:00
|
|
|
}
|
Refactoring: Move Source (Legality) (#3560)
Rewrites a good amount of legality APIs pertaining to:
* Legal moves that can be learned
* Evolution chains & cross-generation paths
* Memory validation with forgotten moves
In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data.
The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space.
The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation.
* `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game.
* `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`).
* Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
|
|
|
return history;
|
2022-06-18 18:04:24 +00:00
|
|
|
}
|
2018-08-03 03:11:42 +00:00
|
|
|
|
2023-07-08 19:46:46 +00:00
|
|
|
/// <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>
|
2023-07-06 04:14:09 +00:00
|
|
|
public static EvoCriteria[] GetOriginChain(PKM pk, EvolutionOrigin enc, ushort encSpecies = 0, bool discard = true)
|
2022-06-18 18:04:24 +00:00
|
|
|
{
|
2023-07-06 04:14:09 +00:00
|
|
|
Span<EvoCriteria> result = stackalloc EvoCriteria[EvolutionTree.MaxEvolutions];
|
|
|
|
int count = GetOriginChain(result, pk, enc, encSpecies, discard);
|
|
|
|
if (count == 0)
|
2023-12-04 04:13:20 +00:00
|
|
|
return [];
|
2023-07-06 04:14:09 +00:00
|
|
|
|
|
|
|
var chain = result[..count];
|
|
|
|
return chain.ToArray();
|
|
|
|
}
|
|
|
|
|
2023-07-08 19:46:46 +00:00
|
|
|
/// <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>
|
2023-07-06 04:14:09 +00:00
|
|
|
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;
|
|
|
|
}
|
2018-06-20 00:14:22 +00:00
|
|
|
|
2023-07-06 04:14:09 +00:00
|
|
|
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)
|
2023-09-05 03:10:40 +00:00
|
|
|
group.DiscardForOrigin(result, pk, enc);
|
2023-07-06 04:14:09 +00:00
|
|
|
if (encSpecies != 0)
|
|
|
|
return EvolutionUtil.IndexOf(result, encSpecies) + 1;
|
|
|
|
return GetCount(result);
|
|
|
|
}
|
|
|
|
|
2023-07-08 19:46:46 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Gets the count of entries that are not empty (species == 0).
|
|
|
|
/// </summary>
|
|
|
|
private static int GetCount(in ReadOnlySpan<EvoCriteria> result)
|
2023-07-06 04:14:09 +00:00
|
|
|
{
|
|
|
|
int count = 0;
|
2023-07-08 19:46:46 +00:00
|
|
|
foreach (ref readonly var evo in result)
|
2023-07-06 04:14:09 +00:00
|
|
|
{
|
|
|
|
if (evo.Species == 0)
|
|
|
|
break;
|
|
|
|
count++;
|
|
|
|
}
|
2018-08-03 03:11:42 +00:00
|
|
|
|
2023-07-06 04:14:09 +00:00
|
|
|
return count;
|
2018-06-20 00:14:22 +00:00
|
|
|
}
|
|
|
|
}
|