mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-27 14:30:56 +00:00
9166d0eb64
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
193 lines
8.6 KiB
C#
193 lines
8.6 KiB
C#
using System;
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
/// <summary>
|
|
/// Inheritance logic for bred eggs.
|
|
/// </summary>
|
|
/// <remarks>Refer to the associated Egg Source enums used by the associated child classes for inheritance ordering.</remarks>
|
|
public static class MoveBreed
|
|
{
|
|
/// <summary>
|
|
/// Verifies the input <see cref="moves"/> using the breeding rules of the associated <see cref="generation"/> and <see cref="version"/>.
|
|
/// </summary>
|
|
/// <param name="generation">Generation rule-set the egg was created in</param>
|
|
/// <param name="species">Entity species in the egg</param>
|
|
/// <param name="form">Entity form in the egg</param>
|
|
/// <param name="version">Version the egg was created in</param>
|
|
/// <param name="moves">Moves the egg supposedly originated with</param>
|
|
/// <param name="origins">Output buffer indicating the origin of each index within <see cref="moves"/></param>
|
|
/// <returns>True if the moves are ordered correctly, without missing moves.</returns>
|
|
public static bool Validate(int generation, int species, int form, GameVersion version, ReadOnlySpan<int> moves, Span<byte> origins) => generation switch
|
|
{
|
|
2 => MoveBreed2.Validate(species, version, moves, origins),
|
|
3 => MoveBreed3.Validate(species, version, moves, origins),
|
|
4 => MoveBreed4.Validate(species, version, moves, origins),
|
|
5 => MoveBreed5.Validate(species, version, moves, origins),
|
|
_ => MoveBreed6.Validate(generation, species, form, version, moves, origins),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Gets the expected moves the egg should come with, using an input of requested <see cref="moves"/> that are requested to be in the output.
|
|
/// </summary>
|
|
/// <param name="moves">Moves requested to be in the expected moves result</param>
|
|
/// <param name="enc">Encounter detail interface wrapper; should always be <see cref="EncounterEgg"/>.</param>
|
|
/// <param name="result">Result moves that are valid</param>
|
|
/// <remarks>Validates the requested moves first prior to trying a more expensive computation.</remarks>
|
|
/// <returns>True if the <see cref="result"/> is valid using the input <see cref="moves"/>. If not valid, the <see cref="result"/> will be base egg moves, probably valid.</returns>
|
|
public static bool GetExpectedMoves(ReadOnlySpan<int> moves, IEncounterTemplate enc, Span<int> result)
|
|
{
|
|
Span<byte> origins = stackalloc byte[moves.Length];
|
|
var valid = Validate(enc.Generation, enc.Species, enc.Form, enc.Version, moves, origins);
|
|
if (valid)
|
|
{
|
|
moves.CopyTo(result);
|
|
return true;
|
|
}
|
|
return GetExpectedMoves(enc.Generation, enc.Species, enc.Form, enc.Version, moves, origins, result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A more expensive method of getting the expected moves the egg should come with, using an input of requested <see cref="moves"/> that are requested to be in the output.
|
|
/// </summary>
|
|
/// <remarks>Uses inputs calculated from <see cref="Validate"/>. Don't call this directly unless already parsed the input as invalid.</remarks>
|
|
/// <returns>Expected moves for the encounter</returns>
|
|
/// <inheritdoc cref="Validate"/>
|
|
public static bool GetExpectedMoves(int generation, int species, int form, GameVersion version, ReadOnlySpan<int> moves, Span<byte> origins, Span<int> result)
|
|
{
|
|
// Try rearranging the order of the moves.
|
|
// Group and order moves by their possible origin flags.
|
|
Span<MoveOrder> expected = stackalloc MoveOrder[moves.Length];
|
|
GetSortedMoveOrder(generation, moves, origins, expected);
|
|
// Don't mutate the expected list any more.
|
|
|
|
// Temp buffer for the validation origin flags, unused in current scope but used inside the called method.
|
|
Span<byte> temp = stackalloc byte[moves.Length];
|
|
|
|
// Try checking if the rearranged order from above is valid.
|
|
for (int i = 0; i < moves.Length; i++)
|
|
result[i] = expected[i].Move;
|
|
var valid = Validate(generation, species, form, version, result, temp);
|
|
if (valid) // If true, the result buffer is now valid.
|
|
return true;
|
|
|
|
// Well, that didn't work; probably because one or more moves aren't valid.
|
|
// Let's remove all present base moves, and get a fresh set of base moves.
|
|
var learn = GameData.GetLearnsets(version);
|
|
var table = GameData.GetPersonal(version);
|
|
var index = table.GetFormIndex(species, form);
|
|
var learnset = learn[index];
|
|
var eggLevel = EggStateLegality.GetEggLevel(generation);
|
|
var baseMoves = learnset.GetBaseEggMoves(eggLevel);
|
|
|
|
RebuildMoves(baseMoves, expected, result);
|
|
|
|
// Check if that worked...
|
|
temp.Clear();
|
|
valid = Validate(generation, species, form, version, result, temp);
|
|
if (valid) // If true, the result buffer is now valid.
|
|
return true;
|
|
|
|
// Total failure; just return the base moves.
|
|
baseMoves.CopyTo(result);
|
|
for (int i = baseMoves.Length; i < result.Length; i++)
|
|
result[i] = 0;
|
|
return false;
|
|
}
|
|
|
|
private static void GetSortedMoveOrder(int generation, ReadOnlySpan<int> moves, Span<byte> origins, Span<MoveOrder> expected)
|
|
{
|
|
if (generation == 2)
|
|
{
|
|
GetSortedMoveOrder2(moves, origins, expected);
|
|
return;
|
|
}
|
|
|
|
int count = 0;
|
|
for (int i = moves.Length - 1; i >= 0; i--)
|
|
{
|
|
var origin = origins[i];
|
|
if (origin == 0) // invalid/empty
|
|
continue;
|
|
int insertIndex = GetInsertIndex(expected, origin, count);
|
|
if (insertIndex < count)
|
|
ShiftAllItems(expected, insertIndex);
|
|
expected[insertIndex] = new MoveOrder((ushort)moves[i], origin);
|
|
count++;
|
|
}
|
|
}
|
|
|
|
private static void ShiftAllItems(Span<MoveOrder> details, int insertIndex)
|
|
{
|
|
// Shifts all indexes starting at insertIndex to the right by one.
|
|
// Empty slot at the end is overwritten, but that slot was zero (unused).
|
|
for (int i = details.Length - 1; i > insertIndex; i--)
|
|
details[i] = details[i - 1];
|
|
}
|
|
|
|
private static void GetSortedMoveOrder2(ReadOnlySpan<int> moves, Span<byte> origins, Span<MoveOrder> expected)
|
|
{
|
|
// Base moves first, then non-base.
|
|
// Empty/invalid move slots are ignored -- default struct value is an empty move.
|
|
int baseMoves = origins.Count((byte)EggSource2.Base);
|
|
int ctrBase = 0;
|
|
int ctrNonBase = 0;
|
|
for (int i = 0; i < moves.Length; i++)
|
|
{
|
|
var origin = origins[i];
|
|
if (origin == 0) // invalid/empty
|
|
continue;
|
|
|
|
int index = origin == (byte)EggSource2.Base ? ctrBase++ : baseMoves + ctrNonBase++;
|
|
expected[index] = new MoveOrder((ushort)moves[i], origin);
|
|
}
|
|
}
|
|
|
|
private static int GetInsertIndex(ReadOnlySpan<MoveOrder> expected, byte origin, int count)
|
|
{
|
|
// Return the first index that has an origin lower than the entry present in the slot.
|
|
// If no such index exists, return the current count (insert at the end).
|
|
int i = 0;
|
|
for (; i < count; i++)
|
|
{
|
|
if (origin < expected[i].Origin)
|
|
break;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
private static void RebuildMoves(ReadOnlySpan<int> baseMoves, ReadOnlySpan<MoveOrder> expected, Span<int> result)
|
|
{
|
|
// Build a list of moves that are not present in the base moves list.
|
|
// Use the expected order (sorted by origin flags) when assembling the result.
|
|
Span<int> notBase = stackalloc int[expected.Length];
|
|
int notBaseCount = 0;
|
|
foreach (var (move, origin) in expected)
|
|
{
|
|
if (origin == 0) // invalid/empty
|
|
continue;
|
|
if (!baseMoves.Contains(move))
|
|
notBase[notBaseCount++] = move;
|
|
}
|
|
|
|
int baseCount = expected.Length - notBaseCount;
|
|
if (baseCount > baseMoves.Length)
|
|
baseCount = baseMoves.Length;
|
|
|
|
int ctr = 0;
|
|
// Start with base moves
|
|
for (; ctr < baseCount; ctr++)
|
|
result[ctr] = baseMoves[baseMoves.Length - baseCount + ctr];
|
|
// Then with non-base moves
|
|
for (var i = 0; i < notBaseCount; i++)
|
|
result[ctr++] = notBase[i];
|
|
// Then clear the remainder
|
|
for (int i = ctr; i < result.Length; i++)
|
|
result[i] = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple tuple to track the move and its possible origin flags.
|
|
/// </summary>
|
|
private readonly record struct MoveOrder(ushort Move, byte Origin);
|
|
}
|