using System; using System.Buffers; namespace PKHeX.Core; /// /// Encapsulates logic for HOME's Move Relearner feature. /// /// /// If the Entity knew a move at any point in its history, it can be relearned if the current format can learn it. /// 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 result, ReadOnlySpan 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 }; /// /// Scan the results and remove any that are not valid for the game game. /// /// True if all results are valid. private static bool CleanPurge(Span result, ReadOnlySpan current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan 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 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; } } } /// /// Try to add original moves from the encounter template. /// /// if any move is validated. private static bool TryAddOriginalMoves(Span result, ReadOnlySpan 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 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; } /// /// Adds all possible original moves from the encounter template. /// private static void AddOriginalMoves(Span result, PKM pk, IEncounterTemplate enc, MoveSourceType types, IHomeSource local, ReadOnlySpan evos, LearnOption option) { if (enc is EncounterSlot8GO { OriginFormat: PogoImportFormat.PK7 or PogoImportFormat.PB7 } g8) { Span 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 result, ReadOnlySpan 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 result, PKM pk) { if (pk.Species == (int)Species.Hoopa) result[pk.Form == 0 ? (int)Move.HyperspaceHole : (int)Move.HyperspaceFury] = true; } /// /// Get the current HOME source for the given context. /// /// 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 instance, Span result, PKM pk, EvolutionHistory history, IEncounterTemplate enc, MoveSourceType types, LearnOption option, ReadOnlySpan evos, IHomeSource local) where T : ILearnGroup { var length = instance.MaxMoveID + 1; var rent = ArrayPool.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.Shared.Return(rent); } /// /// For each move that is possible to learn in another game, check if it is possible to learn in the current game. /// /// Resulting array of moves that are possible to learn in the current game. /// Entity to check. /// Evolutions to check. /// Move source types to check. /// Destination game to check. /// Temporary array of moves that are possible to learn in the checked game. private static void LoopMerge(Span result, PKM pk, ReadOnlySpan evos, MoveSourceType types, IHomeSource dest, Span 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 result, PKM pk, ReadOnlySpan evos, MoveSourceType types, IHomeSource dest, ReadOnlySpan 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 result, ReadOnlySpan current, ReadOnlySpan 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 result, ReadOnlySpan current, ReadOnlySpan 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; } }