mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-04-16 01:59:15 -05:00
Generation was always more weak; am I paranoid about potential VC3? maybe Better indicates the move source for LGPE exclusive moves, etc.
435 lines
19 KiB
C#
435 lines
19 KiB
C#
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 sealed class LearnGroupHOME : ILearnGroup
|
|
{
|
|
public static readonly LearnGroupHOME Instance = new();
|
|
private const LearnOption Option = LearnOption.HOME;
|
|
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 = Option)
|
|
{
|
|
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, option))
|
|
return true;
|
|
}
|
|
if (history.HasVisitedZA && pk is not PA9)
|
|
{
|
|
var instance = LearnGroup9a.Instance;
|
|
instance.Check(result, current, pk, history, enc, types, Option);
|
|
if (CleanPurge(result, current, pk, types, local, evos, option))
|
|
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, option))
|
|
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, option))
|
|
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, option))
|
|
return true;
|
|
}
|
|
|
|
// Ignore Battle Version generally; can be transferred back to SW/SH and wiped after the moves have been shared from HOME.
|
|
// Battle Version is only relevant while in PK8 format, as a wiped moveset can no longer harbor external moves for that format.
|
|
// SW/SH is the only game that can ever harbor external moves, and is the only game that uses Battle Version.
|
|
if (TryAddOriginalMoves(result, current, pk, enc, option))
|
|
{
|
|
if (CleanPurge(result, current, pk, types, local, evos, option))
|
|
return true;
|
|
}
|
|
|
|
// HOME is silly and allows form exclusive moves to be transferred without ever knowing the move.
|
|
if (TryAddExclusiveMoves(result, current, pk))
|
|
{
|
|
if (CleanPurge(result, current, pk, types, local, evos, option))
|
|
return true;
|
|
}
|
|
|
|
if (history.HasVisitedLGPE)
|
|
{
|
|
// PK8 w/ Battle Version can be ignored, as LGP/E has separate HOME data.
|
|
var instance = LearnGroup7b.Instance;
|
|
instance.Check(result, current, pk, history, enc, types, Option);
|
|
if (CleanPurge(result, current, pk, types, local, evos, option))
|
|
return true;
|
|
}
|
|
else if (history.HasVisitedGen7)
|
|
{
|
|
if (IsWipedPK8(pk))
|
|
return false; // Battle Version wiped Gen7 and below moves.
|
|
ILearnGroup instance = LearnGroup7.Instance;
|
|
while (true)
|
|
{
|
|
instance.Check(result, current, pk, history, enc, types, Option);
|
|
if (CleanPurge(result, current, pk, types, local, evos, option))
|
|
return true;
|
|
var prev = instance.GetPrevious(pk, history, enc, option);
|
|
if (prev is null)
|
|
break;
|
|
instance = prev;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool IsWipedPK8(PKM pk) => pk is PK8 { BattleVersion: GameVersion.SW or GameVersion.SH };
|
|
|
|
/// <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, LearnOption option)
|
|
{
|
|
if (option == LearnOption.AtAnyTime)
|
|
return MoveResult.AllParsed(result);
|
|
|
|
// The logic used to update the results did not check if the move could 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.Context == 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;
|
|
}
|
|
|
|
// HOME has special handling to allow Volt Tackle outside learnset possibilities.
|
|
// Most games do not have a Learn Source for Volt Tackle besides it being specially inserted for Egg Encounters.
|
|
if (!valid && move is not (ushort)Move.VoltTackle)
|
|
{
|
|
if (r.Context.IsEraHOME || local is not LearnSource8SWSH)
|
|
r = default;
|
|
}
|
|
}
|
|
|
|
return MoveResult.AllParsed(result);
|
|
}
|
|
|
|
public void GetAllMoves(Span<bool> result, PKM pk, EvolutionHistory history, IEncounterTemplate enc,
|
|
MoveSourceType types = MoveSourceType.All, LearnOption 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.HasVisitedZA && pk is not PA9)
|
|
RentLoopGetAll(LearnGroup9a.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);
|
|
AddOriginalMoves(result, pk, enc, types, local, evos, option);
|
|
AddExclusiveMoves(result, pk);
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to add original moves from the encounter template.
|
|
/// </summary>
|
|
/// <returns><see langword="true"/> if any move is validated.</returns>
|
|
private static bool TryAddOriginalMoves(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, IEncounterTemplate enc, LearnOption option)
|
|
{
|
|
if (enc is EncounterSlot8GO { OriginFormat: PogoImportFormat.PK7 or PogoImportFormat.PB7 } g8)
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && g8 is { OriginFormat: PogoImportFormat.PK7 })
|
|
return false; // Battle Version wiped Gen7 and below moves.
|
|
Span<ushort> moves = stackalloc ushort[4];
|
|
g8.GetInitialMoves(pk.MetLevel, moves);
|
|
return AddOriginalMoves(result, current, moves, g8.OriginFormat == PogoImportFormat.PK7 ? LearnEnvironment.USUM : LearnEnvironment.GG);
|
|
}
|
|
if (enc is IEncounterEgg egg)
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && egg is { Generation: <= 7 })
|
|
return false; // Battle Version wiped Gen7 and below moves.
|
|
var ls = egg.Learn;
|
|
if (AddOriginalMoves(result, current, ls.GetInheritMoves(egg.Species, egg.Form), ls.Environment))
|
|
return true;
|
|
if (AddOriginalMoves(result, current, ls.GetEggMoves(egg.Species, egg.Form), ls.Environment))
|
|
return true;
|
|
}
|
|
if (enc is IMoveset { Moves: { HasMoves: true } x })
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && enc is { Generation: <= 7, Context: not EntityContext.Gen7b })
|
|
return false; // Battle Version wiped Gen7 and below moves.
|
|
var ls = GameData.GetLearnSource(enc.Version);
|
|
if (AddOriginalMoves(result, current, x, ls.Environment))
|
|
return true;
|
|
// fall through
|
|
}
|
|
if (enc is IRelearn { Relearn: { HasMoves: true } r })
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && enc is { Generation: <= 7, Context: not EntityContext.Gen7b })
|
|
return false; // Battle Version wiped Gen7 and below moves.
|
|
var ls = GameData.GetLearnSource(enc.Version);
|
|
if (AddOriginalMoves(result, current, r, ls.Environment))
|
|
return true;
|
|
// fall through
|
|
}
|
|
if (enc is ISingleMoveBonus { IsMoveBonusPossible: true } bonus)
|
|
{
|
|
// Can only have one, but we're looking for an "all possible".
|
|
var ls = bonus.GetMoveBonusPossible();
|
|
var learnSource = GameData.GetLearnSource(enc.Version);
|
|
if (AddOriginalMovesSingle(result, current, ls, learnSource.Environment))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds all possible original moves from the encounter template.
|
|
/// </summary>
|
|
private static void AddOriginalMoves(Span<bool> result, PKM pk, IEncounterTemplate enc, MoveSourceType types, IHomeSource local, ReadOnlySpan<EvoCriteria> evos, LearnOption option)
|
|
{
|
|
if (enc is EncounterSlot8GO { OriginFormat: PogoImportFormat.PK7 or PogoImportFormat.PB7 } g8)
|
|
{
|
|
Span<ushort> moves = stackalloc ushort[4];
|
|
g8.GetInitialMoves(pk.MetLevel, moves);
|
|
AddOriginalMoves(result, pk, evos, types, local, moves);
|
|
return;
|
|
}
|
|
if (enc is IEncounterEgg egg)
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && egg is { Generation: <= 7 })
|
|
return; // Battle Version wiped Gen7 and below moves.
|
|
var ls = egg.Learn;
|
|
AddOriginalMoves(result, pk, evos, types, local, ls.GetInheritMoves(egg.Species, egg.Form));
|
|
AddOriginalMoves(result, pk, evos, types, local, ls.GetEggMoves(egg.Species, egg.Form));
|
|
return;
|
|
}
|
|
if (enc is IMoveset { Moves: { HasMoves: true } x })
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && enc is { Generation: <= 7, Context: not EntityContext.Gen7b })
|
|
return; // Battle Version wiped Gen7 and below moves.
|
|
AddOriginalMoves(result, pk, evos, types, local, x);
|
|
// fall through
|
|
}
|
|
if (enc is IRelearn { Relearn: { HasMoves: true } r })
|
|
{
|
|
if (option == LearnOption.Current && IsWipedPK8(pk) && enc is { Generation: <= 7, Context: not EntityContext.Gen7b })
|
|
return; // Battle Version wiped Gen7 and below moves.
|
|
AddOriginalMoves(result, pk, evos, types, local, r);
|
|
// fall through
|
|
}
|
|
if (enc is ISingleMoveBonus { IsMoveBonusPossible: true } bonus)
|
|
{
|
|
// Can only have one, but we're looking for an "all possible".
|
|
var ls = bonus.GetMoveBonusPossible();
|
|
AddOriginalMoves(result, pk, evos, types, local, ls);
|
|
}
|
|
}
|
|
|
|
private static bool TryAddExclusiveMoves(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk)
|
|
{
|
|
if (pk.Species is (int)Species.Hoopa)
|
|
{
|
|
var move = pk.Form == 0 ? (ushort)Move.HyperspaceHole : (ushort)Move.HyperspaceFury;
|
|
var index = current.IndexOf(move);
|
|
if (index < 0)
|
|
return false;
|
|
ref var exist = ref result[index];
|
|
if (exist.Valid)
|
|
return false;
|
|
exist = new MoveResult(new MoveLearnInfo(LearnMethod.HOME, LearnEnvironment.HOME));
|
|
return true;
|
|
}
|
|
// Kyurem as Fused cannot move into HOME and trigger move sharing.
|
|
return false;
|
|
}
|
|
|
|
private static void AddExclusiveMoves(Span<bool> result, PKM pk)
|
|
{
|
|
if (pk.Species == (int)Species.Hoopa)
|
|
result[pk.Form == 0 ? (int)Move.HyperspaceHole : (int)Move.HyperspaceFury] = true;
|
|
}
|
|
|
|
/// <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,
|
|
EntityContext.Gen9a => LearnSource9ZA.Instance,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(context), context, null),
|
|
};
|
|
|
|
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 + 1;
|
|
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);
|
|
// HOME has special handling to allow Volt Tackle outside learnset possibilities.
|
|
// Most games do not have a Learn Source for Volt Tackle besides it being specially inserted for Egg Encounters.
|
|
if (chk.Method == LearnMethod.None && move is not (int)Move.VoltTackle)
|
|
continue;
|
|
result[move] = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AddOriginalMoves(Span<bool> result, PKM pk, ReadOnlySpan<EvoCriteria> evos, MoveSourceType types, IHomeSource dest, ReadOnlySpan<ushort> moves)
|
|
{
|
|
foreach (var move in moves)
|
|
{
|
|
if (move == 0)
|
|
break;
|
|
if (move >= result.Length)
|
|
continue;
|
|
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 AddOriginalMoves(Span<MoveResult> result, ReadOnlySpan<ushort> current, ReadOnlySpan<ushort> moves, LearnEnvironment game)
|
|
{
|
|
bool addedAny = false;
|
|
foreach (var move in moves)
|
|
{
|
|
if (move == 0)
|
|
break;
|
|
var index = current.IndexOf(move);
|
|
if (index == -1)
|
|
continue;
|
|
if (result[index].Valid)
|
|
continue;
|
|
|
|
result[index] = MoveResult.Initial(game);
|
|
addedAny = true;
|
|
}
|
|
return addedAny;
|
|
}
|
|
|
|
private static bool AddOriginalMovesSingle(Span<MoveResult> result, ReadOnlySpan<ushort> current, ReadOnlySpan<ushort> moves, LearnEnvironment game)
|
|
{
|
|
// Only one move to add -- I'm sure there will be issues with this naive approach (in the event that multiple moves are needed, provided another environment is "better" providing.)
|
|
foreach (var move in moves)
|
|
{
|
|
if (move == 0)
|
|
break;
|
|
var index = current.IndexOf(move);
|
|
if (index == -1)
|
|
continue;
|
|
if (result[index].Valid)
|
|
continue;
|
|
|
|
result[index] = MoveResult.Initial(game);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|