PKHeX/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierRelearn.cs
Kurt 9166d0eb64
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 16:15:27 -07:00

136 lines
4.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace PKHeX.Core;
/// <summary>
/// Logic to verify the current <see cref="PKM.RelearnMoves"/>.
/// </summary>
public static class LearnVerifierRelearn
{
public static void Verify(Span<MoveResult> result, IEncounterTemplate enc, PKM pk)
{
if (ShouldNotHaveRelearnMoves(enc, pk))
VerifyRelearnNone(pk, result);
else if (enc is IRelearn {Relearn: int[] {Length: not 0} x})
VerifyRelearnSpecifiedMoveset(pk, x, result);
else if (enc is EncounterEgg e)
VerifyEggMoveset(e, result, pk.RelearnMoves);
else if (enc is EncounterSlot6AO { CanDexNav: true } z && pk.RelearnMove1 != 0)
VerifyRelearnDexNav(pk, result, z);
else if (enc is EncounterSlot8b { IsUnderground: true } u)
VerifyRelearnUnderground(pk, result, u);
else
VerifyRelearnNone(pk, result);
}
public static bool ShouldNotHaveRelearnMoves(IGeneration enc, PKM pk) => enc.Generation < 6 || pk.IsOriginalMovesetDeleted();
private static void VerifyRelearnSpecifiedMoveset(PKM pk, IReadOnlyList<int> required, Span<MoveResult> result)
{
for (int i = result.Length - 1; i >= 0; i--)
{
var current = pk.GetRelearnMove(i);
var expect = required[i];
result[i] = ParseExpect(current, expect);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static MoveResult ParseExpect(int move, int expect = 0)
{
if (move != expect)
return MoveResult.Unobtainable(expect);
if (move == 0)
return MoveResult.Empty;
return MoveResult.Relearn;
}
private static void VerifyRelearnDexNav(PKM pk, Span<MoveResult> result, EncounterSlot6AO slot)
{
// All other relearn moves must be empty.
result[3] = ParseExpect(pk.RelearnMove4);
result[2] = ParseExpect(pk.RelearnMove3);
result[1] = ParseExpect(pk.RelearnMove2);
// DexNav Pokémon can have 1 random egg move as a relearn move.
result[0] = slot.CanBeDexNavMove(pk.RelearnMove1) ? MoveResult.Relearn : MoveResult.Unobtainable(); // DexNav
}
private static void VerifyRelearnUnderground(PKM pk, Span<MoveResult> result, EncounterSlot8b slot)
{
// All other relearn moves must be empty.
result[3] = ParseExpect(pk.RelearnMove4);
result[2] = ParseExpect(pk.RelearnMove3);
result[1] = ParseExpect(pk.RelearnMove2);
// Underground Pokémon can have 1 random egg move as a relearn move.
result[0] = slot.CanBeUndergroundMove(pk.RelearnMove1) ? MoveResult.Relearn : MoveResult.Unobtainable(); // Underground
}
private static void VerifyRelearnNone(PKM pk, Span<MoveResult> result)
{
// No relearn moves should be present.
result[3] = ParseExpect(pk.RelearnMove4);
result[2] = ParseExpect(pk.RelearnMove3);
result[1] = ParseExpect(pk.RelearnMove2);
result[0] = ParseExpect(pk.RelearnMove1);
}
internal static void VerifyEggMoveset(EncounterEgg e, Span<MoveResult> result, ReadOnlySpan<int> moves)
{
int gen = e.Generation;
Span<byte> origins = stackalloc byte[moves.Length];
var valid = MoveBreed.Validate(gen, e.Species, e.Form, e.Version, moves, origins);
if (valid)
{
for (int i = result.Length - 1; i >= 0; i--)
{
if (moves[i] == 0)
result[i] = MoveResult.Empty;
else
result[i] = new(EggSourceUtil.GetSource(origins[i], gen));
}
}
else
{
Span<int> expected = stackalloc int[moves.Length];
_ = MoveBreed.GetExpectedMoves(moves, e, expected);
_ = MoveBreed.Validate(gen, e.Species, e.Form, e.Version, expected, origins);
for (int i = moves.Length - 1; i >= 0; i--)
{
var current = moves[i];
var expect = expected[i];
if (current != expect)
result[i] = MoveResult.Unobtainable(expect);
else if (current == 0)
result[i] = MoveResult.Empty;
else
result[i] = new(EggSourceUtil.GetSource(origins[i], gen));
}
}
var dupe = IsAnyMoveDuplicate(moves);
if (dupe != NO_DUPE)
result[dupe] = MoveResult.Duplicate;
}
private const int NO_DUPE = -1;
private static int IsAnyMoveDuplicate(ReadOnlySpan<int> move)
{
int m1 = move[0];
int m2 = move[1];
if (m1 != 0 && m1 == m2)
return 1;
int m3 = move[2];
if (m3 != 0 && (m1 == m3 || m2 == m3))
return 2;
int m4 = move[3];
if (m4 != 0 && (m1 == m4 || m2 == m4 || m3 == m4))
return 3;
return NO_DUPE;
}
}