PKHeX/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs
Kurt 9792455f34 Refactor to use Context over Generation
Generation was always more weak; am I paranoid about potential VC3? maybe
Better indicates the move source for LGPE exclusive moves, etc.
2026-02-15 02:15:50 -06:00

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;
}
}