diff --git a/PKHeX.Core/Editing/Applicators/MoveSetApplicator.cs b/PKHeX.Core/Editing/Applicators/MoveSetApplicator.cs index dcf83b6f2..e77604d55 100644 --- a/PKHeX.Core/Editing/Applicators/MoveSetApplicator.cs +++ b/PKHeX.Core/Editing/Applicators/MoveSetApplicator.cs @@ -85,7 +85,7 @@ public static void GetSuggestedRelearnMoves(this LegalityAnalysis legal, Span GetEncounters(PKM pk, EvoCriteria[] c yield return enc.Encounter; } - private const byte Generation = 2; - private const EntityContext Context = EntityContext.Gen2; - private const byte EggLevel = 5; + private const byte EggLevel = EncounterEgg2.Level; - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) - { - if (FormInfo.IsBattleOnlyForm(species, form, Generation)) - form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); - } + private static EncounterEgg2 CreateEggEncounter(ushort species, GameVersion version) => new(species, version); private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) { return EvolutionTree.Evolves2.GetBaseSpeciesForm(lowest.Species, lowest.Form); } - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg2? result) { result = null; var devolved = chain[^1]; @@ -73,13 +66,13 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio if (!PersonalTable.C.IsPresentInGame(species, form)) return false; - result = CreateEggEncounter(species, form, version); + result = CreateEggEncounter(species, version); return true; } // Depending on the game it was hatched (GS vs C), met data will be present. // Since met data can't be used to infer which game it was created on, we yield both if possible. - public static bool TryGetEggCrystal(PKM pk, EncounterEgg egg, [NotNullWhen(true)] out EncounterEgg? crystal) + public static bool TryGetEggCrystal(PKM pk, EncounterEgg2 egg, [NotNullWhen(true)] out EncounterEgg2? crystal) { if (!ParseSettings.AllowGen2Crystal(pk)) { diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs index 7c504d842..a1ad9a13f 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -24,9 +25,12 @@ public IEnumerable GetEncounters(PKM pk, LegalInfo info) private enum DeferralType { + // Legal None, + PIDIVDefer, + + // Illegal PIDIV, - Tile, Ball, SlotNumber, } @@ -64,9 +68,13 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le foreach (var enc in iterator) { var e = enc.Encounter; - if (!IsTypeCompatible(e, pk, ref info.GetPIDIVRef())) + var typeCheck = IsTypeCompatible(e, pk, ref info.GetPIDIVRef()); + if (typeCheck is not Match) { - defer.Update(DeferralType.PIDIV, e); + var rating = typeCheck == NotIdeal + ? DeferralType.PIDIVDefer + : DeferralType.PIDIV; + defer.Update(rating, e); continue; } if (!IsBallCompatible(e, pk)) @@ -102,7 +110,7 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le // Errors will be flagged later for those not manually handled below. if (defer.Encounter is not { } lastResort) yield break; - if (defer.Type is DeferralType.PIDIV && !(lastResort is EncounterEgg && ParseSettings.Settings.FramePattern.EggRandomAnyType3)) + if (defer.Type is DeferralType.PIDIV) info.ManualFlag = EncounterYieldFlag.InvalidPIDIV; else if (defer.Type is DeferralType.SlotNumber) info.ManualFlag = EncounterYieldFlag.InvalidFrame; @@ -115,33 +123,27 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le _ => pk.Ball is not (byte)Ball.Safari, }; - private static bool IsTypeCompatible(IEncounterTemplate enc, PKM pk, ref PIDIV pidiv) + private static RandomCorrelationRating IsTypeCompatible(IEncounterTemplate enc, PKM pk, ref PIDIV pidiv) { if (enc is IRandomCorrelationEvent3 revise) return revise.IsCompatibleReviseReset(ref pidiv, pk); var type = pidiv.Type; if (enc is IRandomCorrelation r) return r.IsCompatible(type, pk); - return type == PIDType.None; + return type is PIDType.None ? Match : Mismatch; } - private const byte Generation = 3; private const EntityContext Context = EntityContext.Gen3; - private const byte EggLevel = 5; + private const byte EggLevel = EncounterEgg3.Level; - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) - { - if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Castform) - form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); - } + private static EncounterEgg3 CreateEggEncounter(ushort species, GameVersion version) => new(species, version); private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) { return EvolutionTree.Evolves3.GetBaseSpeciesForm(lowest.Species, lowest.Form); } - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg3? result) { result = null; var devolved = chain[^1]; @@ -163,13 +165,13 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio if (!PersonalTable.E.IsPresentInGame(species, form)) return false; - result = CreateEggEncounter(species, form, version); + result = CreateEggEncounter(species, version); return true; } // Version is not updated when hatching an Egg in Gen3. Version is a clear indicator of the game it originated on. - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg3 other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg3? result) { result = null; // Check for split-breed @@ -183,7 +185,7 @@ public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan cha if (!Breeding.IsSplitBreedNotBabySpecies3(devolved.Species)) return false; - result = other with { Species = devolved.Species, Form = devolved.Form }; + result = other with { Species = devolved.Species }; return true; } } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs index e57fd1946..a7eb992cb 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs @@ -48,9 +48,10 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le } static bool IsTypeCompatible(IEncounterTemplate enc, PKM pk, PIDType type) { + // boolean results only from this set of games (no correlation confusion to be concerned with) if (enc is IRandomCorrelation r) - return r.IsCompatible(type, pk); - return type == PIDType.None; + return r.IsCompatible(type, pk) == RandomCorrelationRating.Match; + return type is PIDType.None; } if (IsTypeCompatible(z, pk, info.PIDIV.Type)) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs index d68da0437..360874b06 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator4.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -29,7 +30,11 @@ public IEnumerable GetPossible(PKM pk, EvoCriteria[] chain, Game private enum DeferralType { + // Legal None, + PIDIVDefer, + + // Illegal PIDIV, Tile, Ball, @@ -66,9 +71,14 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le defer.Update(DeferralType.Tile, e); continue; } - if (!IsTypeCompatible(e, pk, info.PIDIV.Type)) + + var typeCheck = IsTypeCompatible(e, pk, info.PIDIV.Type); + if (typeCheck is not Match) { - defer.Update(DeferralType.PIDIV, e); + var rating = typeCheck == NotIdeal + ? DeferralType.PIDIVDefer + : DeferralType.PIDIV; + defer.Update(rating, e); continue; } if (!IsBallCompatible(e, pk)) @@ -106,7 +116,7 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le // Errors will be flagged later for those not manually handled below. if (defer.Encounter is not { } lastResort) yield break; - if (defer.Type is DeferralType.PIDIV && !(lastResort is EncounterEgg && ParseSettings.Settings.FramePattern.EggRandomAnyType4)) + if (defer.Type is DeferralType.PIDIV) info.ManualFlag = EncounterYieldFlag.InvalidPIDIV; else if (defer.Type is DeferralType.SlotNumber) info.ManualFlag = EncounterYieldFlag.InvalidFrame; @@ -129,30 +139,24 @@ private static bool IsTileCompatible(IEncounterTemplate enc, PKM pk) return t.GroundTile.Contains(e.GroundTile); } - private static bool IsTypeCompatible(IEncounterTemplate enc, PKM pk, PIDType type) + private static RandomCorrelationRating IsTypeCompatible(IEncounterTemplate enc, PKM pk, PIDType type) { if (enc is IRandomCorrelation r) return r.IsCompatible(type, pk); - return type == PIDType.None; + return type is PIDType.None ? Match : Mismatch; } - private const byte Generation = 4; private const EntityContext Context = EntityContext.Gen4; - private const byte EggLevel = 1; + private const byte EggLevel = EncounterEgg4.Level; - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) - { - if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) - form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); - } + private static EncounterEgg4 CreateEggEncounter(ushort species, GameVersion version) => new(species, version); private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) { return EvolutionTree.Evolves4.GetBaseSpeciesForm(lowest.Species, lowest.Form); } - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg4? result) { result = null; var devolved = chain[^1]; @@ -174,13 +178,13 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio if (!PersonalTable.HGSS.IsPresentInGame(species, form)) return false; - result = CreateEggEncounter(species, form, version); + result = CreateEggEncounter(species, version); return true; } // Version is not updated when hatching an Egg in Gen4. Version is a clear indicator of the game it originated on. - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg4 other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg4? result) { result = null; // Check for split-breed @@ -194,7 +198,7 @@ public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan cha if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species)) return false; - result = other with { Species = devolved.Species, Form = devolved.Form }; + result = other with { Species = devolved.Species }; return true; } } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs index 2500faf38..baf07cdd6 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator5.cs @@ -31,23 +31,17 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le yield return enc.Encounter; } - private const byte Generation = 5; private const EntityContext Context = EntityContext.Gen5; private const byte EggLevel = EggStateLegality.EggMetLevel; - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) - { - if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) - form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); - } + private static EncounterEgg5 CreateEggEncounter(ushort species, GameVersion version) => new(species, version); private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) { return EvolutionTree.Evolves5.GetBaseSpeciesForm(lowest.Species, lowest.Form); } - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg5? result) { result = null; var devolved = chain[^1]; @@ -69,14 +63,14 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio if (!PersonalTable.B2W2.IsPresentInGame(species, form)) return false; - result = CreateEggEncounter(species, form, version); + result = CreateEggEncounter(species, version); return true; } // Both B/W and B2/W2 have the same egg move sets, so there is no point generating other-game pair encounters for traded eggs. // When hatched, the entity's Version is updated to the OT's. - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg5 other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg5? result) { result = null; // Check for split-breed @@ -90,7 +84,7 @@ public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan cha if (!Breeding.IsSplitBreedNotBabySpecies4(devolved.Species)) return false; - result = other with { Species = devolved.Species, Form = devolved.Form }; + result = other with { Species = devolved.Species }; return true; } } diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs index b5ccc09d9..b3609378b 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator6.cs @@ -31,7 +31,7 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le private const byte Generation = 6; private const EntityContext Context = EntityContext.Gen6; - private const byte EggLevel = EggStateLegality.EggMetLevel; + private const byte EggLevel = EncounterEgg6.Level; private static GameVersion GetOtherGamePair(GameVersion version) { @@ -43,11 +43,11 @@ private static GameVersion GetOtherGamePair(GameVersion version) return version ^ (GameVersion)2; } - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) + private static EncounterEgg6 CreateEggEncounter(ushort species, byte form, GameVersion version) { if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); + return new EncounterEgg6(species, form, version); } private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) @@ -55,7 +55,7 @@ private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) return EvolutionTree.Evolves6.GetBaseSpeciesForm(lowest.Species, lowest.Form); } - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg6? result) { result = null; var devolved = chain[^1]; @@ -81,9 +81,9 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio return true; } - public static EncounterEgg MutateEggTrade(EncounterEgg egg) => egg with { Version = GetOtherGamePair(egg.Version) }; + public static EncounterEgg6 MutateEggTrade(EncounterEgg6 egg) => egg with { Version = GetOtherGamePair(egg.Version) }; - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg6 other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg6? result) { result = null; // Check for split-breed diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs index 3f52c9d58..870b79ffc 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator7.cs @@ -66,7 +66,7 @@ private static ushort GetVCSpecies(ReadOnlySpan chain, PKM pk, usho private const EntityContext Context = EntityContext.Gen7; private const byte EggLevel = EggStateLegality.EggMetLevel; - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg7? result) { result = null; var devolved = chain[^1]; @@ -92,9 +92,9 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio return true; } - public static EncounterEgg MutateEggTrade(EncounterEgg egg) => egg with { Version = GetOtherGamePair(egg.Version) }; + public static EncounterEgg7 MutateEggTrade(EncounterEgg7 egg) => egg with { Version = GetOtherGamePair(egg.Version) }; - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg7 other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg7? result) { result = null; // Check for split-breed @@ -124,11 +124,11 @@ private static GameVersion GetOtherGamePair(GameVersion version) #pragma warning restore RCS1130 // Bitwise operation on enum without Flags attribute. } - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) + private static EncounterEgg7 CreateEggEncounter(ushort species, byte form, GameVersion version) { if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); + return new EncounterEgg7(species, form, version); } private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs index 86a84ae6b..af9baf42f 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8.cs @@ -27,11 +27,11 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le private const EntityContext Context = EntityContext.Gen8; private const byte EggLevel = 1; - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) + private static EncounterEgg8 CreateEggEncounter(ushort species, byte form, GameVersion version) { if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); + return new EncounterEgg8(species, form, version); } private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) @@ -43,7 +43,7 @@ private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) return (lowest.Species, lowest.Form); } - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg8? result) { result = null; var devolved = chain[^1]; @@ -69,7 +69,7 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio return true; } - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg8 other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg8? result) { result = null; // Check for split-breed diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs index 1de24a392..aa95316ac 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator8b.cs @@ -35,7 +35,7 @@ public IEnumerable GetEncountersSWSH(PKM pk, EvoCriteria[] chain private const EntityContext Context = EntityContext.Gen8b; private const byte EggLevel = 1; - public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(ReadOnlySpan chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg8b? result) { result = null; var devolved = chain[^1]; @@ -61,7 +61,7 @@ public static bool TryGetEgg(ReadOnlySpan chain, GameVersion versio return true; } - public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetSplit(EncounterEgg8b other, ReadOnlySpan chain, [NotNullWhen(true)] out EncounterEgg8b? result) { result = null; // Check for split-breed @@ -79,11 +79,11 @@ public static bool TryGetSplit(EncounterEgg other, ReadOnlySpan cha return true; } - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) + private static EncounterEgg8b CreateEggEncounter(ushort species, byte form, GameVersion version) { if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); + return new EncounterEgg8b(species, form, version); } private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs index 6b887d4b1..2f0a4d216 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs @@ -48,14 +48,14 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le private const EntityContext Context = EntityContext.Gen9; private const byte EggLevel = 1; - public static bool TryGetEgg(PKM pk, EvoCriteria[] chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(PKM pk, EvoCriteria[] chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg9? result) { if (version == 0 && pk.IsEgg) version = SL; return TryGetEgg(chain, version, out result); } - public static bool TryGetEgg(EvoCriteria[] chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg? result) + public static bool TryGetEgg(EvoCriteria[] chain, GameVersion version, [NotNullWhen(true)] out EncounterEgg9? result) { result = null; var devolved = chain[^1]; @@ -82,13 +82,13 @@ public static bool TryGetEgg(EvoCriteria[] chain, GameVersion version, [NotNullW return true; } - private static EncounterEgg CreateEggEncounter(ushort species, byte form, GameVersion version) + private static EncounterEgg9 CreateEggEncounter(ushort species, byte form, GameVersion version) { if (species == (int)Species.Scatterbug) form = Vivillon3DS.FancyFormID; // Fancy else if (FormInfo.IsBattleOnlyForm(species, form, Generation) || species is (int)Species.Rotom or (int)Species.Castform) form = FormInfo.GetOutOfBattleForm(species, form, Generation); - return new EncounterEgg(species, form, EggLevel, Generation, version, Context); + return new EncounterEgg9(species, form, version); } private static (ushort Species, byte Form) GetBaby(EvoCriteria lowest) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator12.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator12.cs index 611079546..04c76952a 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator12.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/Lump/EncounterGenerator12.cs @@ -93,7 +93,7 @@ private static PeekEnumerator PickPreferredIterator(PKM pk, Peek EncounterTrade1 => GBEncounterPriority.TradeEncounterG1, EncounterTrade2 => GBEncounterPriority.TradeEncounterG2, EncounterSlot1 or EncounterSlot2 => GBEncounterPriority.WildEncounter, - EncounterEgg => GBEncounterPriority.EggEncounter, + EncounterEgg2 => GBEncounterPriority.EggEncounter, _ => GBEncounterPriority.StaticEncounter, }; diff --git a/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs b/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs index b7afa06a2..d9a9f2fbc 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Moveset/EncounterMovesetGenerator.cs @@ -275,7 +275,7 @@ private static IEnumerable GetEggs(PKM pk, ReadOnlyMemory needs, IEncounterTemplat return Moveset.BitOverlap(moves, needs); } - private static int GetMoveMaskEgg(ReadOnlySpan needs, IEncounterTemplate egg) + private static int GetMoveMaskEgg(ReadOnlySpan needs, IEncounterEgg egg) { - var source = GameData.GetLearnSource(egg.Version); + var source = egg.Learn; var eggMoves = source.GetEggMoves(egg.Species, egg.Form); int flags = Moveset.BitOverlap(eggMoves, needs); var vt = needs.IndexOf((ushort)Move.VoltTackle); - if (vt != -1 && egg is EncounterEgg { CanHaveVoltTackle: true }) + if (vt != -1 && egg.CanHaveVoltTackle) flags |= 1 << vt; else if (egg.Generation <= 2) flags |= GetMoveMaskGen2(needs, egg); @@ -440,7 +440,7 @@ private static bool HasAllNeededMovesSlot(ReadOnlySpan needs, IEncounter return false; } - private static bool HasAllNeededMovesEgg(ReadOnlySpan needs, IEncounterTemplate egg) + private static bool HasAllNeededMovesEgg(ReadOnlySpan needs, IEncounterEgg egg) { int flags = GetMoveMaskEgg(needs, egg); return flags == (1 << needs.Length) - 1; diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible3.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible3.cs index 854464c76..08f32cdb7 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible3.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible3.cs @@ -78,7 +78,7 @@ public bool MoveNext() State = YieldState.BredSplit; return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator3.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (!EncounterGenerator3.TryGetSplit((EncounterEgg3)Current, Chain, out egg)) goto case YieldState.EventStart; State = YieldState.EventStart; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible4.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible4.cs index b0bb1cae2..f4aba06ed 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible4.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible4.cs @@ -72,7 +72,7 @@ public bool MoveNext() return SetCurrent(egg); case YieldState.BredSplit: State = YieldState.EventStart; - if (EncounterGenerator4.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (EncounterGenerator4.TryGetSplit((EncounterEgg4)Current, Chain, out egg)) return SetCurrent(egg); goto case YieldState.EventStart; diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible5.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible5.cs index 5fac21831..c9a6dd385 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible5.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible5.cs @@ -75,7 +75,7 @@ public bool MoveNext() State = YieldState.BredSplit; return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator5.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (!EncounterGenerator5.TryGetSplit((EncounterEgg5)Current, Chain, out egg)) goto case YieldState.EventStart; State = YieldState.EventStart; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible6.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible6.cs index 510764a73..12a6bf788 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible6.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible6.cs @@ -69,16 +69,16 @@ public bool MoveNext() return SetCurrent(egg); case YieldState.BredTrade: State = YieldState.BredSplit; - egg = EncounterGenerator6.MutateEggTrade((EncounterEgg)Current); + egg = EncounterGenerator6.MutateEggTrade((EncounterEgg6)Current); return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator6.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (!EncounterGenerator6.TryGetSplit((EncounterEgg6)Current, Chain, out egg)) goto case YieldState.EventStart; State = YieldState.BredSplitTrade; return SetCurrent(egg); case YieldState.BredSplitTrade: State = YieldState.EventStart; - egg = EncounterGenerator6.MutateEggTrade((EncounterEgg)Current); + egg = EncounterGenerator6.MutateEggTrade((EncounterEgg6)Current); return SetCurrent(egg); case YieldState.EventStart: diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible7.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible7.cs index 7b98e8ed4..67f3aac83 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible7.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible7.cs @@ -71,16 +71,16 @@ public bool MoveNext() return SetCurrent(egg); case YieldState.BredTrade: State = YieldState.BredSplit; - egg = EncounterGenerator7.MutateEggTrade((EncounterEgg)Current); + egg = EncounterGenerator7.MutateEggTrade((EncounterEgg7)Current); return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator7.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (!EncounterGenerator7.TryGetSplit((EncounterEgg7)Current, Chain, out egg)) goto case YieldState.EventStart; State = YieldState.BredSplitTrade; return SetCurrent(egg); case YieldState.BredSplitTrade: State = YieldState.EventStart; - egg = EncounterGenerator7.MutateEggTrade((EncounterEgg)Current); + egg = EncounterGenerator7.MutateEggTrade((EncounterEgg7)Current); return SetCurrent(egg); case YieldState.EventStart: @@ -97,6 +97,8 @@ public bool MoveNext() Index = 0; goto case YieldState.TradeStart; case YieldState.TradeStart: + if (!Flags.HasFlag(EncounterTypeGroup.Trade)) + goto case YieldState.StartCaptures; if (Version is GameVersion.SN or GameVersion.MN) { State = YieldState.TradeSM; goto case YieldState.TradeSM; } if (Version is GameVersion.US or GameVersion.UM) @@ -115,6 +117,8 @@ public bool MoveNext() goto case YieldState.StaticStart; case YieldState.StaticStart: + if (!Flags.HasFlag(EncounterTypeGroup.Static)) + goto case YieldState.SlotStart; if (Version == GameVersion.US) { State = YieldState.StaticUS; goto case YieldState.StaticUS; } if (Version == GameVersion.UM) @@ -152,6 +156,8 @@ public bool MoveNext() Index = 0; goto case YieldState.SlotStart; case YieldState.SlotStart: + if (!Flags.HasFlag(EncounterTypeGroup.Slot)) + goto case YieldState.SlotEnd; if (Version == GameVersion.US) { State = YieldState.SlotUS; goto case YieldState.SlotUS; } if (Version == GameVersion.UM) diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8.cs index 02e8925bc..701ec9369 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8.cs @@ -67,7 +67,7 @@ public bool MoveNext() State = YieldState.BredSplit; return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator8.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (!EncounterGenerator8.TryGetSplit((EncounterEgg8)Current, Chain, out egg)) goto case YieldState.EventStart; State = YieldState.EventStart; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8b.cs b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8b.cs index d0090c5ce..131d9d8fa 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8b.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Possible/EncounterPossible8b.cs @@ -60,7 +60,7 @@ public bool MoveNext() State = YieldState.BredSplit; return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator8b.TryGetSplit((EncounterEgg)Current, Chain, out egg)) + if (!EncounterGenerator8b.TryGetSplit((EncounterEgg8b)Current, Chain, out egg)) goto case YieldState.EventStart; State = YieldState.EventStart; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/Dirtied/EncounterEnumerator8bSWSH.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/Dirtied/EncounterEnumerator8bSWSH.cs index f4e060560..f36ab4d3f 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/Dirtied/EncounterEnumerator8bSWSH.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/Dirtied/EncounterEnumerator8bSWSH.cs @@ -79,7 +79,7 @@ public bool MoveNext() State = YieldState.BredSplit; return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator8b.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (!EncounterGenerator8b.TryGetSplit((EncounterEgg8b)Current.Encounter, Chain, out egg)) goto case YieldState.TradeStart; State = YieldState.End; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator2.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator2.cs index 974e49077..dbe8fda64 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator2.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator2.cs @@ -103,7 +103,7 @@ public bool MoveNext() goto case YieldState.StaticStart; case YieldState.BredCrystal: State = YieldState.StaticStart; - if (EncounterGenerator2.TryGetEggCrystal(Entity, (EncounterEgg)Current.Encounter, out egg)) + if (EncounterGenerator2.TryGetEggCrystal(Entity, (EncounterEgg2)Current.Encounter, out egg)) return SetCurrent(egg); goto case YieldState.StaticStart; diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator3.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator3.cs index e4e1763c8..6e375f042 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator3.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator3.cs @@ -242,7 +242,7 @@ public bool MoveNext() return SetCurrent(egg); case YieldState.BredSplit: State = YieldState.Fallback; - if (!EncounterGenerator3.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (!EncounterGenerator3.TryGetSplit((EncounterEgg3)Current.Encounter, Chain, out egg)) goto case YieldState.Fallback; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator4.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator4.cs index 84a4e43aa..45a2d4ee2 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator4.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator4.cs @@ -105,7 +105,7 @@ public bool MoveNext() State = YieldState.BredSplit; return SetCurrent(egg); case YieldState.BredSplit: - if (!EncounterGenerator4.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (!EncounterGenerator4.TryGetSplit((EncounterEgg4)Current.Encounter, Chain, out egg)) goto case YieldState.TradeStart; State = YieldState.TradeStart; return SetCurrent(egg); diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator5.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator5.cs index 00513a4d7..636b51981 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator5.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator5.cs @@ -100,7 +100,7 @@ public bool MoveNext() case YieldState.BredSplit: bool daycare = Entity.EggLocation == Locations.Daycare5; State = daycare ? YieldState.End : YieldState.StartCaptures; - if (EncounterGenerator5.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (EncounterGenerator5.TryGetSplit((EncounterEgg5)Current.Encounter, Chain, out egg)) return SetCurrent(egg); if (daycare) break; // no other encounters diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator6.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator6.cs index a0f2c6efc..a8a8129d5 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator6.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator6.cs @@ -95,20 +95,20 @@ public bool MoveNext() State = YieldState.BredSplit; if (Entity.EggLocation != Locations.LinkTrade6) goto case YieldState.BredSplit; - egg = EncounterGenerator6.MutateEggTrade((EncounterEgg)Current.Encounter); + egg = EncounterGenerator6.MutateEggTrade((EncounterEgg6)Current.Encounter); return SetCurrent(egg); case YieldState.BredSplit: if (Chain[^1].Species is (int)Species.Togepi or (int)Species.Wynaut) goto case YieldState.StartCaptures; State = YieldState.BredSplitTrade; - if (!EncounterGenerator6.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (!EncounterGenerator6.TryGetSplit((EncounterEgg6)Current.Encounter, Chain, out egg)) break; return SetCurrent(egg); case YieldState.BredSplitTrade: State = YieldState.StartCaptures; if (Entity.EggLocation != Locations.LinkTrade6) goto case YieldState.StartCaptures; - egg = EncounterGenerator6.MutateEggTrade((EncounterEgg)Current.Encounter); + egg = EncounterGenerator6.MutateEggTrade((EncounterEgg6)Current.Encounter); return SetCurrent(egg); case YieldState.TradeStart: diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator7.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator7.cs index cc3228646..2006836f6 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator7.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator7.cs @@ -94,20 +94,20 @@ public bool MoveNext() State = YieldState.BredSplit; if (Entity.EggLocation != Locations.LinkTrade6) goto case YieldState.BredSplit; - egg = EncounterGenerator7.MutateEggTrade((EncounterEgg)Current.Encounter); + egg = EncounterGenerator7.MutateEggTrade((EncounterEgg7)Current.Encounter); return SetCurrent(egg); case YieldState.BredSplit: if (Chain[^1].Species == (int)Species.Eevee) { State = YieldState.StaticSharedUSUM; goto case YieldState.StaticSharedUSUM; } State = YieldState.BredSplitTrade; - if (!EncounterGenerator7.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (!EncounterGenerator7.TryGetSplit((EncounterEgg7)Current.Encounter, Chain, out egg)) break; return SetCurrent(egg); case YieldState.BredSplitTrade: State = YieldState.End; if (Entity.EggLocation != Locations.LinkTrade6) break; - egg = EncounterGenerator7.MutateEggTrade((EncounterEgg)Current.Encounter); + egg = EncounterGenerator7.MutateEggTrade((EncounterEgg7)Current.Encounter); return SetCurrent(egg); case YieldState.TradeStart: diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8.cs index f06b046bb..068610a4f 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8.cs @@ -91,7 +91,7 @@ public bool MoveNext() return SetCurrent(egg); case YieldState.BredSplit: State = YieldState.End; - if (EncounterGenerator8.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (EncounterGenerator8.TryGetSplit((EncounterEgg8)Current.Encounter, Chain, out egg)) return SetCurrent(egg); break; diff --git a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8b.cs b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8b.cs index 4bb63ee77..e51b1e2cc 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8b.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/Search/EncounterEnumerator8b.cs @@ -83,7 +83,7 @@ public bool MoveNext() return SetCurrent(egg); case YieldState.BredSplit: State = Entity.EggLocation == Locations.Daycare8b ? YieldState.End : YieldState.StartCaptures; - if (EncounterGenerator8b.TryGetSplit((EncounterEgg)Current.Encounter, Chain, out egg)) + if (EncounterGenerator8b.TryGetSplit((EncounterEgg8b)Current.Encounter, Chain, out egg)) return SetCurrent(egg); break; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterEgg2.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterEgg2.cs new file mode 100644 index 000000000..161adf1ec --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterEgg2.cs @@ -0,0 +1,78 @@ +namespace PKHeX.Core; + +/// +/// Egg Encounter Data +/// +public sealed record EncounterEgg2(ushort Species, GameVersion Version) : IEncounterEgg +{ + public byte Form => 0; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 5; + public bool CanHaveVoltTackle => false; + + public byte Generation => 2; + public EntityContext Context => EntityContext.Gen2; + public bool IsEgg => true; + public byte LevelMin => Level; + public byte LevelMax => Level; + public bool IsShiny => false; + public ushort Location => 0; + public ushort EggLocation => 0; + public Ball FixedBall => Generation <= 5 ? Ball.Poke : Ball.None; + public Shiny Shiny => Shiny.Random; + public AbilityPermission Ability => AbilityPermission.Any12H; + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK2 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK2 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var rnd = Util.Rand; + + var pk = new PK2(language == (int)LanguageID.Japanese) + { + Species = Species, + CurrentLevel = Level, + TID16 = tr.TID16, + + // Force Hatch + OriginalTrainerName = tr.OT, + OriginalTrainerFriendship = 120, + + DV16 = criteria.IsSpecifiedIVsAll() ? criteria.GetCombinedDVs() + : EncounterUtil.GetRandomDVs(rnd, criteria.Shiny.IsShiny(), criteria.HiddenPowerType) + }; + pk.SetNotNicknamed(language); + + if (Version == GameVersion.C) + { + // Set met data for Crystal hatch. + pk.MetLocation = Locations.HatchLocationC; + pk.MetLevel = 1; + pk.MetTimeOfDay = rnd.Next(1, 4); // Morning | Day | Night + pk.OriginalTrainerGender = (byte)(tr.Gender & 1); + } + + SetEncounterMoves(pk); + pk.HealPP(); + + return pk; + } + + ILearnSource IEncounterEgg.Learn => Learn; + public ILearnSource Learn => Version is GameVersion.C + ? LearnSource2C.Instance + : LearnSource2GS.Instance; + + private void SetEncounterMoves(PK2 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterSlot2.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterSlot2.cs index 8e55b2e4c..993d11ffb 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterSlot2.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterSlot2.cs @@ -83,6 +83,8 @@ public PK2 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language); var isJapanese = language == (int)LanguageID.Japanese; var pi = PersonalTable.C[Species]; + var rnd = Util.Rand; + var pk = new PK2(isJapanese) { Species = Species, @@ -90,15 +92,12 @@ public PK2 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) CurrentLevel = LevelMin, OriginalTrainerFriendship = pi.BaseFriendship, DV16 = criteria.IsSpecifiedIVsAll() ? criteria.GetCombinedDVs() - : EncounterUtil.GetRandomDVs(Util.Rand, criteria.Shiny.IsShiny(), criteria.HiddenPowerType), + : EncounterUtil.GetRandomDVs(rnd, criteria.Shiny.IsShiny(), criteria.HiddenPowerType), - Language = language, OriginalTrainerName = tr.OT, TID16 = tr.TID16, }; pk.SetNotNicknamed(language); - if (criteria.Shiny.IsShiny()) - pk.SetShiny(); if (Version == GameVersion.C) { @@ -115,7 +114,7 @@ public PK2 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) if (!IsTreeAvailable(id)) { // Get a random TID that satisfies this slot. - do { id = (ushort)Util.Rand.Next(); } + do { id = (ushort)rnd.Next(); } while (!IsTreeAvailable(id)); pk.TID16 = id; } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterGift3Colo.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterGift3Colo.cs index d4c505a2c..64eca1fec 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterGift3Colo.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterGift3Colo.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -155,7 +156,7 @@ private bool IsMatchPartial(PKM pk) } #endregion - public bool IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD or PIDType.CXDAnti; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD or PIDType.CXDAnti ? Match : Mismatch; public PIDType GetSuggestedCorrelation() => PIDType.CXD; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterShadow3Colo.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterShadow3Colo.cs index 14c4ad592..1b19e901c 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterShadow3Colo.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterShadow3Colo.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -180,11 +181,11 @@ private bool IsMatchLocation(PKM pk) #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { if (IsEReader) - return true; - return type is PIDType.CXD; + return Match; + return type is PIDType.CXD ? Match : Mismatch; } public PIDType GetSuggestedCorrelation() diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterStarter3Colo.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterStarter3Colo.cs index 2301df940..60882e152 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterStarter3Colo.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Colo/EncounterStarter3Colo.cs @@ -1,3 +1,5 @@ +using static PKHeX.Core.RandomCorrelationRating; + namespace PKHeX.Core; /// @@ -151,7 +153,7 @@ private bool IsMatchPartial(PKM pk) } #endregion - public bool IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD_ColoStarter; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD_ColoStarter ? Match : Mismatch; public PIDType GetSuggestedCorrelation() => PIDType.CXD_ColoStarter; } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterEgg3.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterEgg3.cs new file mode 100644 index 000000000..3df7cfec3 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterEgg3.cs @@ -0,0 +1,129 @@ +using System; +using static PKHeX.Core.RandomCorrelationRating; + +namespace PKHeX.Core; + +public sealed record EncounterEgg3(ushort Species, GameVersion Version) : IEncounterEgg, IRandomCorrelation +{ + private byte Location => Version is GameVersion.FR or GameVersion.LG + ? Locations.HatchLocationFRLG + : Locations.HatchLocationRSE; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 5; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu && Version is GameVersion.E; + + public byte Form => 0; // No forms in Gen3 + public byte Generation => 3; + public EntityContext Context => EntityContext.Gen3; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => 0; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityPermission.Any12; + public Ball FixedBall => Ball.Poke; + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + // Generation 3 has PID/IV correlations and RNG abuse; assume none. + public PIDType GetSuggestedCorrelation() => PIDType.None; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) + { + if (type is PIDType.None) + return Match; + if (ParseSettings.Settings.FramePattern.EggRandomAnyType3) + return NotIdeal; + return Mismatch; + } + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK3 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK3 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + + var pk = new PK3 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)FixedBall, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + OriginalTrainerName = tr.OT, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerFriendship = 120, + MetLevel = 0, + MetLocation = Location, + }; + + SetEncounterMoves(pk); + pk.HealPP(); + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + // Get a random PID that matches gender/nature/ability criteria + var pi = PersonalTable.E[Species]; + var gr = pi.Gender; + var pid = GetRandomPID(criteria, gr); + pk.PID = pid; + pk.RefreshAbility((int)(pid % 2)); + + return pk; + } + + private uint GetRandomPID(in EncounterCriteria criteria, byte gr) + { + var seed = Util.Rand32(); + while (true) + { + // LCRNG is sufficiently random, especially with the nature of vBlanks potentially (super rarely) disjointing rand calls. + seed = LCRNG.Next(seed); + var pid = seed; + var gender = EntityGender.GetFromPIDAndRatio(pid, gr); + if (criteria.IsSpecifiedGender() && !criteria.IsSatisfiedGender(gender)) + continue; + if (criteria.IsSpecifiedNature() && !criteria.IsSatisfiedNature((Nature)(pid % 25))) + continue; + if (criteria.IsSpecifiedAbility() && !criteria.IsSatisfiedAbility((byte)(pid % 2))) + continue; + + // For Nidoran and Volbeat/Illumise, match the bit correlation to be most permissive with move inheritance. + if (Breeding.IsGenderSpeciesDetermination(Species) && !Breeding.IsValidSpeciesBit34(pid, gender)) + continue; // 50/50 chance! + + if (!Daycare3.IsValidProcPID(pid, Version)) + continue; // 0-value PID is invalid + + return pid; + } + } + + ILearnSource IEncounterEgg.Learn => Learn; + public ILearnSource Learn => Version switch + { + GameVersion.R or GameVersion.S => LearnSource3RS.Instance, + GameVersion.E => LearnSource3RS.Instance, + GameVersion.FR => LearnSource3FR.Instance, + GameVersion.LG => LearnSource3FR.Instance, + _ => throw new ArgumentOutOfRangeException(nameof(Version), Version, null), + }; + + private void SetEncounterMoves(PK3 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs index 81f1dbbfb..25ce262f8 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs @@ -1,5 +1,6 @@ using static PKHeX.Core.PIDType; using static PKHeX.Core.SlotType3; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -130,11 +131,13 @@ public EncounterMatchRating GetMatchRating(PKM pk) private bool IsDeferredSafari3(bool IsSafariBall) => IsSafariBall != Locations.IsSafariZoneLocation3(Location); #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { - if (Species != (int)Core.Species.Unown) - return type is (Method_1 or Method_2 or Method_3 or Method_4); - return type is (Method_1_Unown or Method_2_Unown or Method_3_Unown or Method_4_Unown); + var match = Species != (int)Core.Species.Unown + ? type is Method_1 or Method_2 or Method_3 or Method_4 + : type is Method_1_Unown or Method_2_Unown or Method_3_Unown or Method_4_Unown; + + return match ? Match : Mismatch; } public PIDType GetSuggestedCorrelation() => Species == (int)Core.Species.Unown ? Method_1_Unown : Method_1; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterStatic3.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterStatic3.cs index 56d2c877b..ca0135d05 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterStatic3.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterStatic3.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -246,22 +247,22 @@ private bool IsMatchPartial(PKM pk) } #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { var version = pk.Version; if (version is GameVersion.E) - return type is PIDType.Method_1; + return type is PIDType.Method_1 ? Match : Mismatch; if (IsRoaming) // Glitched IVs - return IsRoamerPIDIV(type, pk); + return IsRoamerPIDIV(type, pk) ? Match : Mismatch; if (type is PIDType.Method_1) - return true; + return Match; // RS: Only Method 1, but RSBox s/w emulation can yield Method 4. if (version is GameVersion.R or GameVersion.S) - return type is PIDType.Method_4; + return type is PIDType.Method_4 ? NotIdeal : Mismatch; // FR/LG: Only Method 1, but Togepi gift can be Method 4 via PID modulo VBlank abuse - return type is PIDType.Method_4 && Species is (ushort)Core.Species.Togepi; + return type is PIDType.Method_4 && Species is (ushort)Core.Species.Togepi ? NotIdeal : Mismatch; } private static bool IsRoamerPIDIV(PIDType val, PKM pk) diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3.cs index 77f483381..09ac21322 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3.cs @@ -2,6 +2,7 @@ using static PKHeX.Core.PIDType; using static PKHeX.Core.CommonEvent3; using static PKHeX.Core.CommonEvent3Checker; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -420,20 +421,20 @@ public EncounterMatchRating GetMatchRating(PKM pk) public bool IsTrainerMatch(PKM pk, ReadOnlySpan trainer, int language) => true; // checked in explicit match - public bool IsCompatible(PIDType type, PKM pk) => type == Method; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type == Method ? Match : Mismatch; - public bool IsCompatibleReviseReset(ref PIDIV value, PKM pk) + public RandomCorrelationRating IsCompatibleReviseReset(ref PIDIV value, PKM pk) { var prev = value.Mutated; // if previously revised, use that instead. var type = prev is 0 ? value.Type : prev; if (type is BACD_EA or BACD_ES && !IsEgg) - return false; + return Mismatch; if (OriginalTrainerGender is not (GiftGender3.RandAlgo or GiftGender3.Recipient) && (!IsEgg || pk.IsEgg) && !IsMatchGender(pk, value.OriginSeed)) - return false; + return Mismatch; - return Method switch + bool result = Method switch { BACD_U => type is BACD, BACD_R => IsRestrictedSimple(ref value, type), @@ -448,6 +449,10 @@ public bool IsCompatibleReviseReset(ref PIDIV value, PKM pk) Method_2 => type is Method_2 or (Method_1 or Method_4), // via PID modulo VBlank abuse _ => false, }; + + if (result) + return Match; + return Mismatch; } private bool IsMatchGender(PKM pk, uint seed) diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3JPN.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3JPN.cs index 05d227da2..488f7323e 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3JPN.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3JPN.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -143,22 +144,22 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo) return true; } - public bool IsCompatible(PIDType type, PKM pk) => type is Method; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is Method ? Match : Mismatch; - public bool IsCompatibleReviseReset(ref PIDIV value, PKM pk) + public RandomCorrelationRating IsCompatibleReviseReset(ref PIDIV value, PKM pk) { var prev = value.Mutated; // if previously revised, use that instead. var type = prev is 0 ? value.Type : prev; if (type is not PIDType.BACD_AX) - return false; + return Mismatch; var seed = value.OriginSeed; var rand5 = LCRNG.Next5(seed) >> 16; var expect = GetGender(rand5); if (pk.OriginalTrainerGender != expect) - return false; + return Mismatch; - return true; // Table weight -> gift selection is a separate RNG, nothing to check! + return Match; // Table weight -> gift selection is a separate RNG, nothing to check! } private static uint GetGender(uint rand16) => CommonEvent3.GetGenderBit7(rand16); diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3NY.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3NY.cs index 12ccfcfc0..34afc8bf1 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3NY.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/Gifts/EncounterGift3NY.cs @@ -1,5 +1,6 @@ using System; using static System.Buffers.Binary.BinaryPrimitives; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -158,15 +159,15 @@ internal static EncounterGift3NY[] GetArray(ReadOnlySpan readOnlySpan) return result; } - public bool IsCompatible(PIDType type, PKM pk) => type is Method; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is Method ? Match : Mismatch; - public bool IsCompatibleReviseReset(ref PIDIV value, PKM pk) + public RandomCorrelationRating IsCompatibleReviseReset(ref PIDIV value, PKM pk) { var prev = value.Mutated; // if previously revised, use that instead. var type = prev is 0 ? value.Type : prev; if (type is not PIDType.BACD_AX) - return false; + return Mismatch; - return true; // Table weight -> gift selection is a separate RNG, nothing to check! + return Match; // Table weight -> gift selection is a separate RNG, nothing to check! } } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterShadow3XD.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterShadow3XD.cs index 9783b1a20..72e4da81b 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterShadow3XD.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterShadow3XD.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -151,6 +152,7 @@ private bool IsMatchLocation(PKM pk) #endregion - public bool IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD or PIDType.CXDAnti; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD or PIDType.CXDAnti ? Match : Mismatch; + public PIDType GetSuggestedCorrelation() => PIDType.CXD; } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs index b6ee3a2b7..ab3c76fbc 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs @@ -1,3 +1,5 @@ +using static PKHeX.Core.RandomCorrelationRating; + namespace PKHeX.Core; /// @@ -72,6 +74,6 @@ private void SetPINGA(XK3 pk, EncounterCriteria criteria, PersonalInfo3 pi) public EncounterMatchRating GetMatchRating(PKM pk) => EncounterMatchRating.Match; #endregion - public bool IsCompatible(PIDType type, PKM pk) => type == PIDType.PokeSpot; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is PIDType.PokeSpot ? Match : Mismatch; public PIDType GetSuggestedCorrelation() => PIDType.PokeSpot; } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterStatic3XD.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterStatic3XD.cs index b08537534..ba42e318b 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterStatic3XD.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterStatic3XD.cs @@ -1,3 +1,5 @@ +using static PKHeX.Core.RandomCorrelationRating; + namespace PKHeX.Core; /// @@ -138,11 +140,13 @@ private bool IsMatchPartial(PKM pk) } #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { if (type is PIDType.CXD) - return true; - return type is PIDType.CXDAnti && FatefulEncounter; + return Match; + if (type is PIDType.CXDAnti && FatefulEncounter) + return Match; + return Mismatch; } public PIDType GetSuggestedCorrelation() => PIDType.CXD; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterTrade3XD.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterTrade3XD.cs index 0ede929e4..0fc0723c8 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterTrade3XD.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterTrade3XD.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -156,8 +157,9 @@ private bool IsMatchPartial(PKM pk) } #endregion - public bool IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is PIDType.CXD ? Match : Mismatch; public PIDType GetSuggestedCorrelation() => PIDType.CXD; + public bool IsTrainerMatch(PKM pk, ReadOnlySpan trainer, int language) { if ((uint)language >= TrainerNames.Length) diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterEgg4.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterEgg4.cs new file mode 100644 index 000000000..5563edd17 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterEgg4.cs @@ -0,0 +1,138 @@ +using System; +using static PKHeX.Core.RandomCorrelationRating; + +namespace PKHeX.Core; + +public sealed record EncounterEgg4(ushort Species, GameVersion Version) : IEncounterEgg, IRandomCorrelation +{ + private ushort Location => GetHatchLocation(Version); + + private static ushort GetHatchLocation(GameVersion version) + { + return version is GameVersion.HG or GameVersion.SS + ? Locations.HatchLocationHGSS + : Locations.HatchLocationDPPt; + } + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Form => 0; // No forms in Gen3 + public byte Generation => 4; + public EntityContext Context => EntityContext.Gen4; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Daycare4; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityPermission.Any12; + public Ball FixedBall => Ball.Poke; + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + // Generation 4 has PID/IV correlations and RNG abuse; assume none. + public PIDType GetSuggestedCorrelation() => PIDType.None; + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) + { + if (type is PIDType.None) + return Match; + if (ParseSettings.Settings.FramePattern.EggRandomAnyType4) + return NotIdeal; + return Mismatch; + } + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK4 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK4 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDateNDS(); + + var pk = new PK4 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)FixedBall, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + OriginalTrainerName = tr.OT, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerFriendship = 120, + MetLevel = 0, + MetDate = date, + MetLocation = GetHatchLocation(tr.Version), + EggMetDate = date, + EggLocation = tr.Version == Version ? Locations.Daycare4 : Locations.LinkTrade4, + }; + + SetEncounterMoves(pk); + pk.HealPP(); + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + // Get a random PID that matches gender/nature/ability criteria + var pi = PersonalTable.HGSS[Species]; + var gr = pi.Gender; + var pid = GetRandomPID(criteria, gr, out var gender); + pk.PID = pid; + pk.Gender = gender; + pk.RefreshAbility((int)(pid & 1)); + + return pk; + } + + private uint GetRandomPID(in EncounterCriteria criteria, byte gr, out byte gender) + { + var seed = Util.Rand32(); + while (true) + { + seed = LCRNG.Next(seed); + var pid = seed; + gender = EntityGender.GetFromPIDAndRatio(pid, gr); + if (criteria.IsSpecifiedGender() && !criteria.IsSatisfiedGender(gender)) + continue; + if (criteria.IsSpecifiedNature() && !criteria.IsSatisfiedNature((Nature)(pid % 25))) + continue; + if (criteria.IsSpecifiedAbility() && !criteria.IsSatisfiedAbility((byte)(pid % 2))) + continue; + + // For Nidoran and Volbeat/Illumise, match the bit correlation to be most permissive with move inheritance. + if (Breeding.IsGenderSpeciesDetermination(Species) && !Breeding.IsValidSpeciesBit34(pid, gender)) + continue; // 50/50 chance! + + // A 0-value PID is possible via Masuda Method even though a 0-value saved indicates "no egg available". + // PID is rolled forward upon picking up the egg. + // Not worth skipping 0-value PIDs. Too rare to be worth trying again, since it can be a valid PID. + + return pid; + } + } + + ILearnSource IEncounterEgg.Learn => Learn; + public ILearnSource Learn => Version switch + { + GameVersion.D or GameVersion.P => LearnSource4DP.Instance, + GameVersion.Pt => LearnSource4DP.Instance, + GameVersion.HG or GameVersion.SS => LearnSource4HGSS.Instance, + _ => throw new ArgumentOutOfRangeException(nameof(Version), Version, null), + }; + + private void SetEncounterMoves(PK4 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs index 6ab898637..d2c0cdf60 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs @@ -1,4 +1,5 @@ using static PKHeX.Core.SlotType4; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -173,17 +174,17 @@ public EncounterMatchRating GetMatchRating(PKM pk) private bool IsDeferredWurmple(PKM pk) => Species == (int)Core.Species.Wurmple && pk.Species != (int)Core.Species.Wurmple && !WurmpleUtil.IsWurmpleEvoValid(pk); #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { if (type is PIDType.Method_1) - return true; + return Match; // Chain shiny with Poké Radar is only possible in D/P/Pt, in grass. // Safari Zone does not allow using the Poké Radar if (type is PIDType.ChainShiny) - return pk.IsShiny && CanUseRadar; + return pk.IsShiny && CanUseRadar ? Match : Mismatch; if (type is PIDType.CuteCharm) - return CuteCharm4.IsValid(this, pk); - return false; + return CuteCharm4.IsValid(this, pk) ? Match : Mismatch; + return Mismatch; } public PIDType GetSuggestedCorrelation() => PIDType.Method_1; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs index d00f5faa1..6ad2fbea0 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using static PKHeX.Core.GroundTileAllowed; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -383,17 +384,17 @@ public static bool IsMatchRoamerLocation([ConstantExpected] uint permit, ushort #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { if (Species == (int)Core.Species.Pichu) - return type == PIDType.Pokewalker; + return type is PIDType.Pokewalker ? Match : Mismatch; if (Shiny == Shiny.Always) - return type == PIDType.ChainShiny; + return type is PIDType.ChainShiny ? Match : Mismatch; if (type is PIDType.Method_1) - return true; + return Match; if (type is PIDType.CuteCharm) - return CuteCharm4.IsValid(this, pk); - return false; + return CuteCharm4.IsValid(this, pk) ? Match : Mismatch; + return Mismatch; } public PIDType GetSuggestedCorrelation() diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs index 41028789f..496800071 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs @@ -1,5 +1,6 @@ using System; using static System.Buffers.Binary.BinaryPrimitives; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -194,15 +195,15 @@ public EncounterMatchRating GetMatchRating(PKM pk) private static bool IsMatchPartial(PKM pk) => pk.Ball != (byte)Ball.Poke || !IsMatchSeed(pk); #endregion - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { if (type is PIDType.Pokewalker) - return true; + return Match; // Pokewalker can sometimes be confused with CuteCharm due to the PID creation routine. Double check if it is okay. if (type is PIDType.CuteCharm) - return CuteCharm4.IsCuteCharm(pk, pk.EncryptionConstant) && CuteCharm4.IsValid(this, pk); - return false; + return CuteCharm4.IsCuteCharm(pk, pk.EncryptionConstant) && CuteCharm4.IsValid(this, pk) ? Match : Mismatch; + return Mismatch; } public PIDType GetSuggestedCorrelation() => PIDType.Pokewalker; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen5/EncounterEgg5.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen5/EncounterEgg5.cs new file mode 100644 index 000000000..794ccd12b --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen5/EncounterEgg5.cs @@ -0,0 +1,112 @@ +using System; +using static PKHeX.Core.RandomCorrelationRating; + +namespace PKHeX.Core; + +public sealed record EncounterEgg5(ushort Species, GameVersion Version) : IEncounterEgg, IRandomCorrelation +{ + private const ushort Location = Locations.HatchLocation5; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Form => 0; + public byte Generation => 5; + public EntityContext Context => EntityContext.Gen5; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Daycare5; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityBreedLegality.IsHiddenPossible5(Species) ? AbilityPermission.Any12H : AbilityPermission.Any12; + public Ball FixedBall => Ball.Poke; + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + // Generation 5 has PID/IV correlations and RNG abuse; assume none. + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => type is PIDType.None ? Match : Mismatch; + public PIDType GetSuggestedCorrelation() => PIDType.None; + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK5 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK5 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDateNDS(); + + var pk = new PK5 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)FixedBall, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + OriginalTrainerName = tr.OT, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerFriendship = 120, + MetLevel = 1, + MetDate = date, + MetLocation = Location, + EggMetDate = date, + EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade5, + + Nature = criteria.GetNature(), + }; + + SetEncounterMoves(pk); + pk.HealPP(); + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + // Get a random PID that matches gender/nature/ability criteria + var pi = PersonalTable.B2W2[Species]; + var gr = pi.Gender; + var ability = criteria.GetAbilityFromNumber(Ability); + var pid = GetRandomPID(criteria, gr, out var gender); + pid = pid & 0xFFFEFFFF | (uint)(ability & 1) << 16; // 0x00000000 or 0x00010000 + pk.PID = pid; + pk.Gender = gender; + pk.RefreshAbility(ability); + + return pk; + } + + private static uint GetRandomPID(in EncounterCriteria criteria, byte gr, out byte gender) + { + var seed = Util.Rand32(); + while (true) + { + seed = LCRNG.Next(seed); + var pid = seed; + gender = EntityGender.GetFromPIDAndRatio(pid, gr); + if (criteria.IsSpecifiedGender() && !criteria.IsSatisfiedGender(gender)) + continue; + return pid; + } + } + + public ILearnSource Learn => Version switch + { + GameVersion.B or GameVersion.W => LearnSource5BW.Instance, + GameVersion.B2 or GameVersion.W2 => LearnSource5B2W2.Instance, + _ => throw new ArgumentOutOfRangeException(nameof(Version), Version, null), + }; + + private void SetEncounterMoves(PK5 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen6/EncounterEgg6.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen6/EncounterEgg6.cs new file mode 100644 index 000000000..10d41cb07 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen6/EncounterEgg6.cs @@ -0,0 +1,107 @@ +using System; + +namespace PKHeX.Core; + +public sealed record EncounterEgg6(ushort Species, byte Form, GameVersion Version) : IEncounterEgg +{ + private ushort Location => Version is GameVersion.AS or GameVersion.OR + ? Locations.HatchLocation6AO + : Locations.HatchLocation6XY; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Generation => 6; + public EntityContext Context => EntityContext.Gen6; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Daycare5; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityBreedLegality.IsHiddenPossible6(Species, Form) ? AbilityPermission.Any12H : AbilityPermission.Any12; + public Ball FixedBall => Ball.None; // Inheritance allowed. + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK6 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK6 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDate3DS(); + var pi = PersonalTable.AO[Species, Form]; + var rnd = Util.Rand; + var geo = tr.GetRegionOrigin(language); + + var pk = new PK6 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)Ball.Poke, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerName = tr.OT, + OriginalTrainerFriendship = 120, + MetLevel = 1, + MetDate = date, + MetLocation = Location, + EggMetDate = date, + EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6, + + EncryptionConstant = rnd.Rand32(), + PID = rnd.Rand32(), + Nature = criteria.GetNature(), + Gender = criteria.GetGender(pi), + + ConsoleRegion = geo.ConsoleRegion, + Country = geo.Country, + Region = geo.Region, + }; + pk.StatNature = pk.Nature; + pk.SetHatchMemory6(); + + if (Species is (int)Core.Species.Scatterbug) + pk.Form = Vivillon3DS.GetPattern(pk.Country, pk.Region); + + SetEncounterMoves(pk); + pk.HealPP(); + pk.RelearnMove1 = pk.Move1; + pk.RelearnMove2 = pk.Move2; + pk.RelearnMove3 = pk.Move3; + pk.RelearnMove4 = pk.Move4; + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + var ability = criteria.GetAbilityFromNumber(Ability); + pk.RefreshAbility(ability); + + return pk; + } + + public ILearnSource Learn => Version switch + { + GameVersion.X or GameVersion.Y => LearnSource6XY.Instance, + GameVersion.AS or GameVersion.OR => LearnSource6AO.Instance, + _ => throw new ArgumentOutOfRangeException(nameof(Version), Version, null), + }; + + private void SetEncounterMoves(PK6 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen7/EncounterEgg7.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen7/EncounterEgg7.cs new file mode 100644 index 000000000..3d4124f85 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen7/EncounterEgg7.cs @@ -0,0 +1,104 @@ +using System; + +namespace PKHeX.Core; + +public sealed record EncounterEgg7(ushort Species, byte Form, GameVersion Version) : IEncounterEgg +{ + private const ushort Location = Locations.HatchLocation7; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Generation => 7; + public EntityContext Context => EntityContext.Gen7; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Daycare5; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityBreedLegality.IsHiddenPossible7(Species, Form) ? AbilityPermission.Any12H : AbilityPermission.Any12; + public Ball FixedBall => Ball.None; // Inheritance allowed. + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK7 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDate3DS(); + var pi = PersonalTable.USUM[Species, Form]; + var rnd = Util.Rand; + var geo = tr.GetRegionOrigin(language); + + var pk = new PK7 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)Ball.Poke, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerName = tr.OT, + OriginalTrainerFriendship = 120, + MetLevel = 1, + MetDate = date, + MetLocation = Location, + EggMetDate = date, + EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6, + + EncryptionConstant = rnd.Rand32(), + PID = rnd.Rand32(), + Nature = criteria.GetNature(), + Gender = criteria.GetGender(pi), + + ConsoleRegion = geo.ConsoleRegion, + Country = geo.Country, + Region = geo.Region, + }; + pk.StatNature = pk.Nature; + + if (Species is (int)Core.Species.Scatterbug) + pk.Form = Vivillon3DS.GetPattern(pk.Country, pk.Region); + + SetEncounterMoves(pk); + pk.HealPP(); + pk.RelearnMove1 = pk.Move1; + pk.RelearnMove2 = pk.Move2; + pk.RelearnMove3 = pk.Move3; + pk.RelearnMove4 = pk.Move4; + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + var ability = criteria.GetAbilityFromNumber(Ability); + pk.RefreshAbility(ability); + + return pk; + } + + public ILearnSource Learn => Version switch + { + GameVersion.SN or GameVersion.MN => LearnSource7SM.Instance, + GameVersion.US or GameVersion.UM => LearnSource7USUM.Instance, + _ => throw new ArgumentOutOfRangeException(nameof(Version), Version, null), + }; + + private void SetEncounterMoves(PK7 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen8/EncounterEgg8.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen8/EncounterEgg8.cs new file mode 100644 index 000000000..b10bb6cf3 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen8/EncounterEgg8.cs @@ -0,0 +1,93 @@ +namespace PKHeX.Core; + +public sealed record EncounterEgg8(ushort Species, byte Form, GameVersion Version) : IEncounterEgg +{ + private const ushort Location = Locations.HatchLocation8; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Generation => 8; + public EntityContext Context => EntityContext.Gen8; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Daycare5; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityBreedLegality.IsHiddenPossibleHOME(Species) ? AbilityPermission.Any12H : AbilityPermission.Any12; + public Ball FixedBall => Ball.None; // Inheritance allowed. + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK8 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK8 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDateSwitch(); + var pi = PersonalTable.SWSH[Species, Form]; + var rnd = Util.Rand; + + var pk = new PK8 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)Ball.Poke, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerName = tr.OT, + OriginalTrainerFriendship = 100, // previously 120 in Gen2-7 + MetLevel = 1, + MetDate = date, + MetLocation = Location, + EggMetDate = date, + EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6, + + EncryptionConstant = rnd.Rand32(), + PID = rnd.Rand32(), + Nature = criteria.GetNature(), + Gender = criteria.GetGender(pi), + }; + pk.StatNature = pk.Nature; + + SetEncounterMoves(pk); + pk.HealPP(); + pk.RelearnMove1 = pk.Move1; + pk.RelearnMove2 = pk.Move2; + pk.RelearnMove3 = pk.Move3; + pk.RelearnMove4 = pk.Move4; + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + pk.HeightScalar = PokeSizeUtil.GetRandomScalar(rnd); + pk.WeightScalar = PokeSizeUtil.GetRandomScalar(rnd); + + var ability = criteria.GetAbilityFromNumber(Ability); + pk.RefreshAbility(ability); + + return pk; + } + + ILearnSource IEncounterEgg.Learn => Learn; + public ILearnSource Learn => LearnSource8SWSH.Instance; + + private void SetEncounterMoves(PK8 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen8b/EncounterEgg8b.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen8b/EncounterEgg8b.cs new file mode 100644 index 000000000..5602b32ba --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen8b/EncounterEgg8b.cs @@ -0,0 +1,95 @@ +namespace PKHeX.Core; + +public sealed record EncounterEgg8b(ushort Species, byte Form, GameVersion Version) : IEncounterEgg +{ + private const ushort Location = Locations.HatchLocation8b; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Generation => 8; + public EntityContext Context => EntityContext.Gen8b; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Daycare8b; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityBreedLegality.IsHiddenPossibleHOME(Species) ? AbilityPermission.Any12H : AbilityPermission.Any12; + public Ball FixedBall => Ball.None; // Inheritance allowed. + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PB8 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PB8 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDateSwitch(); + var pi = PersonalTable.BDSP[Species, Form]; + var rnd = Util.Rand; + + var pk = new PB8 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)Ball.Poke, + TID16 = tr.TID16, + SID16 = tr.SID16, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + OriginalTrainerName = tr.OT, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerFriendship = 100, + MetLevel = 1, + MetLocation = Location, + EggLocation = tr.Version == Version ? Locations.Daycare8b : Locations.LinkTrade6NPC, + + MetDate = date, + EggMetDate = date, + + EncryptionConstant = rnd.Rand32(), + PID = rnd.Rand32(), + Nature = criteria.GetNature(), + Gender = criteria.GetGender(pi), + }; + pk.StatNature = pk.Nature; + + SetEncounterMoves(pk); + pk.HealPP(); + pk.RelearnMove1 = pk.Move1; + pk.RelearnMove2 = pk.Move2; + pk.RelearnMove3 = pk.Move3; + pk.RelearnMove4 = pk.Move4; + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + pk.HeightScalar = PokeSizeUtil.GetRandomScalar(rnd); + pk.WeightScalar = PokeSizeUtil.GetRandomScalar(rnd); + + var ability = criteria.GetAbilityFromNumber(Ability); + pk.RefreshAbility(ability); + + return pk; + } + + ILearnSource IEncounterEgg.Learn => Learn; + public ILearnSource Learn => LearnSource8BDSP.Instance; + + private void SetEncounterMoves(PB8 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen9/EncounterEgg9.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen9/EncounterEgg9.cs new file mode 100644 index 000000000..5e281efb0 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen9/EncounterEgg9.cs @@ -0,0 +1,96 @@ +namespace PKHeX.Core; + +public sealed record EncounterEgg9(ushort Species, byte Form, GameVersion Version) : IEncounterEgg +{ + private const ushort Location = Locations.HatchLocation9; + + public string Name => "Egg"; + public string LongName => Name; + + public const byte Level = 1; + public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu; + + public byte Generation => 9; + public EntityContext Context => EntityContext.Gen9; + public bool IsShiny => false; + public byte LevelMin => Level; + public byte LevelMax => Level; + ushort ILocation.EggLocation => Locations.Picnic9; + ushort ILocation.Location => Location; + public AbilityPermission Ability => AbilityBreedLegality.IsHiddenPossibleHOME(Species) ? AbilityPermission.Any12H : AbilityPermission.Any12; + public Ball FixedBall => Ball.None; // Inheritance allowed. + public Shiny Shiny => Shiny.Random; + public bool IsEgg => true; + + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) => ConvertToPKM(tr, criteria); + PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); + public PK9 ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); + + public PK9 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) + { + int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, Version); + var date = EncounterDate.GetDateSwitch(); + var pi = PersonalTable.SV[Species, Form]; + var rnd = Util.Rand; + + var pk = new PK9 + { + Species = Species, + CurrentLevel = Level, + Version = Version, + Ball = (byte)Ball.Poke, + ID32 = tr.ID32, + OriginalTrainerGender = tr.Gender, + + // Force Hatch + Language = language, + Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, Generation), + OriginalTrainerName = tr.OT, + OriginalTrainerFriendship = 100, + MetLevel = 1, + MetDate = date, + MetLocation = Location, + EggMetDate = date, + EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6, + + EncryptionConstant = rnd.Rand32(), + PID = rnd.Rand32(), + Nature = criteria.GetNature(), + Gender = criteria.GetGender(pi), + }; + pk.StatNature = pk.Nature; + + SetEncounterMoves(pk); + pk.HealPP(); + pk.RelearnMove1 = pk.Move1; + pk.RelearnMove2 = pk.Move2; + pk.RelearnMove3 = pk.Move3; + pk.RelearnMove4 = pk.Move4; + + if (criteria.IsSpecifiedIVsAny(out _)) + criteria.SetRandomIVs(pk); + else + criteria.SetRandomIVs(pk, 3); + + pk.HeightScalar = PokeSizeUtil.GetRandomScalar(rnd); + pk.WeightScalar = PokeSizeUtil.GetRandomScalar(rnd); + pk.Scale = PokeSizeUtil.GetRandomScalar(rnd); + var type = Tera9RNG.GetTeraTypeFromPersonal(Species, Form, rnd.Rand64()); + pk.TeraTypeOriginal = (MoveType)type; + + var ability = criteria.GetAbilityFromNumber(Ability); + pk.RefreshAbility(ability); + + return pk; + } + + ILearnSource IEncounterEgg.Learn => Learn; + public ILearnSource Learn => LearnSource9SV.Instance; + + private void SetEncounterMoves(PK9 pk) + { + var learn = Learn.GetLearnset(Species, Form); + var initial = learn.GetBaseEggMoves(LevelMin); + pk.SetMoves(initial); + } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Interfaces/IEncounterEgg.cs b/PKHeX.Core/Legality/Encounters/Templates/Interfaces/IEncounterEgg.cs new file mode 100644 index 000000000..a72b742f7 --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/Templates/Interfaces/IEncounterEgg.cs @@ -0,0 +1,10 @@ +namespace PKHeX.Core; + +/// +/// Breeding Egg Encounter Properties. +/// +public interface IEncounterEgg : IEncounterable +{ + ILearnSource Learn { get; } + bool CanHaveVoltTackle { get; } +} diff --git a/PKHeX.Core/Legality/Encounters/Templates/Interfaces/Properties/IRandomCorrelation.cs b/PKHeX.Core/Legality/Encounters/Templates/Interfaces/Properties/IRandomCorrelation.cs index 1c15c08d5..b232d7e05 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Interfaces/Properties/IRandomCorrelation.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Interfaces/Properties/IRandomCorrelation.cs @@ -11,7 +11,7 @@ public interface IRandomCorrelation /// Observed of the /// Entity to compare against for other details /// True if all details are compatible - bool IsCompatible(PIDType type, PKM pk); + RandomCorrelationRating IsCompatible(PIDType type, PKM pk); /// /// Gets the suggested for the encounter. @@ -30,5 +30,16 @@ public interface IRandomCorrelationEvent3 : IRandomCorrelation /// Value to check and mutate /// Entity to compare against /// True if Compatible. Revision of the ref value is not returned. - bool IsCompatibleReviseReset(ref PIDIV value, PKM pk); + RandomCorrelationRating IsCompatibleReviseReset(ref PIDIV value, PKM pk); +} + +public enum RandomCorrelationRating +{ + // Clear match, no issues. + Match, + // Weak match, could be better matched to another encounter. + NotIdeal, + + // Invalid + Mismatch, } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Shared/EncounterEgg.cs b/PKHeX.Core/Legality/Encounters/Templates/Shared/EncounterEgg.cs deleted file mode 100644 index eaf75fe94..000000000 --- a/PKHeX.Core/Legality/Encounters/Templates/Shared/EncounterEgg.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; - -namespace PKHeX.Core; - -/// -/// Egg Encounter Data -/// -public sealed record EncounterEgg(ushort Species, byte Form, byte Level, byte Generation, GameVersion Version, EntityContext Context) : IEncounterable -{ - public string Name => "Egg"; - public string LongName => "Egg"; - - public bool IsEgg => true; - public byte LevelMin => Level; - public byte LevelMax => Level; - public bool IsShiny => false; - public ushort Location => 0; - public ushort EggLocation => Locations.GetDaycareLocation(Generation, Version); - public Ball FixedBall => Generation <= 5 ? Ball.Poke : Ball.None; - public Shiny Shiny => Shiny.Random; - public AbilityPermission Ability => AbilityPermission.Any12H; - - public bool CanHaveVoltTackle => Species is (int)Core.Species.Pichu && (Generation > 3 || Version is GameVersion.E); - - public PKM ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr, EncounterCriteria.Unrestricted); - - public PKM ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) - { - var generation = Generation; - var version = Version; - var pk = EntityBlank.GetBlank(generation, version); - - tr.ApplyTo(pk); - - int language = (int)Language.GetSafeLanguage(Generation, (LanguageID)tr.Language, version); - pk.Species = Species; - pk.Form = Form; - pk.Language = language; - pk.Nickname = SpeciesName.GetSpeciesNameGeneration(Species, language, generation); - pk.CurrentLevel = Level; - pk.Version = version; - - var ball = FixedBall; - pk.Ball = ball is Ball.None ? (byte)Ball.Poke : (byte)ball; - pk.OriginalTrainerFriendship = EggStateLegality.GetEggHatchFriendship(Context); - - SetEncounterMoves(pk, version); - pk.HealPP(); - var rnd = Util.Rand; - SetPINGA(pk, criteria); - - if (generation <= 2) - { - var pk2 = (PK2)pk; - if (version == GameVersion.C) - { - // Set met data for Crystal hatch. - pk2.MetLocation = Locations.HatchLocationC; - pk2.MetLevel = 1; - pk2.MetTimeOfDay = rnd.Next(1, 4); // Morning | Day | Night - } - else // G/S - { - // G/S can't set any data for Trainer Gender. - pk2.OriginalTrainerGender = 0; - } - - // No other revisions needed. - return pk2; - } - - SetMetData(pk); - - if (generation >= 4) - pk.SetEggMetData(version, tr.Version); - - if (generation < 6) - return pk; - if (pk is PK6 pk6) - pk6.SetHatchMemory6(); - - SetForm(pk, tr); - - pk.SetRandomEC(); - pk.RelearnMove1 = pk.Move1; - pk.RelearnMove2 = pk.Move2; - pk.RelearnMove3 = pk.Move3; - pk.RelearnMove4 = pk.Move4; - if (pk is IScaledSize s) - { - s.HeightScalar = PokeSizeUtil.GetRandomScalar(rnd); - s.WeightScalar = PokeSizeUtil.GetRandomScalar(rnd); - if (pk is IScaledSize3 s3) - s3.Scale = PokeSizeUtil.GetRandomScalar(rnd); - } - - if (pk is ITeraType tera) - { - var type = Tera9RNG.GetTeraTypeFromPersonal(Species, Form, rnd.Rand64()); - tera.TeraTypeOriginal = (MoveType)type; - } - - return pk; - } - - private void SetForm(PKM pk, ITrainerInfo sav) - { - switch (Species) - { - case (int)Core.Species.Minior: - pk.Form = (byte)Util.Rand.Next(7, 14); - break; - case (int)Core.Species.Scatterbug or (int)Core.Species.Spewpa or (int)Core.Species.Vivillon: - if (sav.Generation is 6 or 7 && sav is IRegionOriginReadOnly o) - pk.Form = Vivillon3DS.GetPattern(o.Country, o.Region); - // else keep original value - break; - } - } - - private static void SetPINGA(PKM pk, EncounterCriteria criteria) - { - if (pk is PK2 pk2) - { - pk2.DV16 = criteria.IsSpecifiedIVsAll() - ? criteria.GetCombinedDVs() - : EncounterUtil.GetRandomDVs(Util.Rand, criteria.Shiny.IsShiny(), criteria.HiddenPowerType); - return; - } - if (criteria.IsSpecifiedIVsAny(out _)) - criteria.SetRandomIVs(pk); - else - criteria.SetRandomIVs(pk, 3); - - var gender = criteria.GetGender(pk.PersonalInfo); - var nature = criteria.GetNature(); - - if (pk.Format <= 5) - { - pk.SetPIDGender(gender); - pk.Gender = gender; - pk.SetPIDNature(nature); - if (pk.Format is 3 or 4 && Breeding.IsGenderSpeciesDetermination(pk.Species)) - { - while (!Breeding.IsValidSpeciesBit34(pk.EncryptionConstant, gender)) - { - // Try again. - pk.SetPIDGender(gender); - pk.SetPIDNature(nature); - } - } - pk.RefreshAbility(pk.PIDAbility); - } - else - { - pk.PID = Util.Rand32(); - pk.Nature = pk.StatNature = nature; - pk.Gender = gender; - pk.RefreshAbility(Util.Rand.Next(2)); - } - } - - private void SetMetData(PKM pk) - { - pk.MetLevel = EggStateLegality.GetEggLevelMet(Version, Generation); - pk.MetLocation = Math.Max((ushort)0, EggStateLegality.GetEggHatchLocation(Version, Generation)); - - if (pk is IObedienceLevel l) - l.ObedienceLevel = pk.MetLevel; - } - - private void SetEncounterMoves(PKM pk, GameVersion version) - { - var ls = GameData.GetLearnSource(version); - var learn = ls.GetLearnset(Species, Form); - var initial = learn.GetBaseEggMoves(LevelMin); - pk.SetMoves(initial); - } -} diff --git a/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs b/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs index b82ef1ce5..fefec9882 100644 --- a/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs +++ b/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs @@ -21,13 +21,13 @@ public static class EncounterVerifier private static CheckResult VerifyEncounter(PKM pk, IEncounterTemplate enc) => enc switch { - EncounterEgg e => VerifyEncounterEgg(pk, e.Generation), EncounterShadow3Colo { IsEReader: true } when pk.Language != (int)LanguageID.Japanese => GetInvalid(LG3EReader), EncounterStatic3 { Species: (int)Species.Mew } when pk.Language != (int)LanguageID.Japanese => GetInvalid(LEncUnreleasedEMewJP), EncounterStatic3 { Species: (int)Species.Deoxys, Location: 200 } when pk.Language == (int)LanguageID.Japanese => GetInvalid(LEncUnreleased), EncounterStatic4 { IsRoaming: true } when pk is G4PKM { MetLocation: 193, GroundTile: GroundTileType.Water } => GetInvalid(LG4InvalidTileR45Surf), MysteryGift g => VerifyEncounterEvent(pk, g), - { IsEgg: true } when !pk.IsEgg => VerifyEncounterEgg(pk, enc.Generation), + IEncounterEgg e when pk.IsEgg => VerifyEncounterEggUnhatched(pk, e.Context), + { IsEgg: true } when !pk.IsEgg => VerifyEncounterEggHatched(pk, enc.Context), EncounterInvalid => GetInvalid(LEncInvalid), _ => GetValid(string.Empty), // todo: refactor }; @@ -35,7 +35,7 @@ public static class EncounterVerifier private static CheckResult VerifyEncounterG12(PKM pk, IEncounterTemplate enc) { if (enc.IsEgg) - return VerifyEncounterEgg(pk, 2); + return pk.IsEgg ? VerifyUnhatchedEgg2(pk) : VerifyEncounterEgg2(pk); return enc switch { @@ -57,17 +57,31 @@ private static CheckResult VerifyEncounterG12(PKM pk, IEncounterTemplate enc) }; // Eggs - private static CheckResult VerifyEncounterEgg(PKM pk, byte generation) => generation switch + private static CheckResult VerifyEncounterEggUnhatched(PKM pk, EntityContext context) => context switch { - 2 => pk.IsEgg ? VerifyUnhatchedEgg2(pk) : VerifyEncounterEgg2(pk), - 3 => pk.IsEgg ? VerifyUnhatchedEgg3(pk) : VerifyEncounterEgg3(pk), - 4 => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade4) : VerifyEncounterEgg4(pk), - 5 => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade5) : VerifyEncounterEgg5(pk), - 6 => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade6) : VerifyEncounterEgg6(pk), - 7 => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade6) : VerifyEncounterEgg7(pk), - 8 when GameVersion.BDSP.Contains(pk.Version) => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade6NPC, Locations.Default8bNone) : VerifyEncounterEgg8BDSP(pk), - 8 => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade6) : VerifyEncounterEgg8(pk), - 9 => pk.IsEgg ? VerifyUnhatchedEgg(pk, Locations.LinkTrade6) : VerifyEncounterEgg9(pk), + EntityContext.Gen2 => VerifyUnhatchedEgg2(pk), + EntityContext.Gen3 => VerifyUnhatchedEgg3(pk), + EntityContext.Gen4 => VerifyUnhatchedEgg(pk, Locations.LinkTrade4), + EntityContext.Gen5 => VerifyUnhatchedEgg(pk, Locations.LinkTrade5), + EntityContext.Gen6 => VerifyUnhatchedEgg(pk, Locations.LinkTrade6), + EntityContext.Gen7 => VerifyUnhatchedEgg(pk, Locations.LinkTrade6), + EntityContext.Gen8b=> VerifyUnhatchedEgg(pk, Locations.LinkTrade6NPC, Locations.Default8bNone), + EntityContext.Gen8 => VerifyUnhatchedEgg(pk, Locations.LinkTrade6), + EntityContext.Gen9 => VerifyUnhatchedEgg(pk, Locations.LinkTrade6), + _ => GetInvalid(LEggLocationInvalid), + }; + + private static CheckResult VerifyEncounterEggHatched(PKM pk, EntityContext context) => context switch + { + EntityContext.Gen2 => VerifyEncounterEgg2(pk), + EntityContext.Gen3 => VerifyEncounterEgg3(pk), + EntityContext.Gen4 => VerifyEncounterEgg4(pk), + EntityContext.Gen5 => VerifyEncounterEgg5(pk), + EntityContext.Gen6 => VerifyEncounterEgg6(pk), + EntityContext.Gen7 => VerifyEncounterEgg7(pk), + EntityContext.Gen8b=> VerifyEncounterEgg8BDSP(pk), + EntityContext.Gen8 => VerifyEncounterEgg8(pk), + EntityContext.Gen9 => VerifyEncounterEgg9(pk), _ => GetInvalid(LEggLocationInvalid), }; @@ -335,7 +349,7 @@ private static CheckResult VerifyEncounterEvent(PKM pk, MysteryGift gift) } if (!pk.IsEgg && gift.IsEgg) // hatched { - var hatchCheck = VerifyEncounterEgg(pk, gift.Generation); + var hatchCheck = VerifyEncounterEggHatched(pk, gift.Context); if (!hatchCheck.Valid) return hatchCheck; } diff --git a/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs b/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs index b948f4c2d..bdaa74645 100644 --- a/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs +++ b/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs @@ -124,17 +124,50 @@ private static void AddEncounterInfoPIDIV(List lines, LegalInfo info) lines.Add(msgType); if (pidiv.NoSeed) { - if (type is PIDType.Pokewalker) + if (enc is EncounterStatic4Pokewalker) { + if (type is not PIDType.Pokewalker) + return; var line = GetLinePokewalkerSeed(info); lines.Add(line); } - else if (enc is PCD { Gift.PK.PID: <= 1 }) // tick rand + else if (enc is PCD pcd) { - var ticks = ARNG.Prev(info.Entity.EncryptionConstant); - var line = string.Format(L_FOriginSeed_0, ticks.ToString("X8")); - line += $" [{ticks / 524_288f:F2}]"; // seconds? - lines.Add(line); + var gift = pcd.Gift; + if (gift is { HasPID: false }) // tick rand + { + var ticks = ARNG.Prev(info.Entity.EncryptionConstant); + var line = string.Format(L_FOriginSeed_0, ticks.ToString("X8")); + line += $" [{ticks / 524_288f:F2}]"; // seconds? + lines.Add(line); + } + if (gift is { HasIVs: false }) + { + var pk = info.Entity; + Span ivs = stackalloc int[6]; + pk.GetIVs(ivs); + + var date = pk.MetDate ?? new DateOnly(2000, 1, 1); + var initial = ClassicEraRNG.SeekInitialSeedForIVs(ivs, (uint)date.Year, (uint)date.Month, (uint)date.Day, out var origin); + var components = ClassicEraRNG.DecomposeSeed(initial, (uint)date.Year, (uint)date.Month, (uint)date.Day); + + AppendInitialDateTime(lines, initial, origin, components); + if (components.IsInvalid()) + lines.Add("INVALID"); + } + } + else if (enc is EncounterEgg3) + { + if (Daycare3.TryGetOriginSeed(info.Entity, out var day3)) + { + var line = string.Format(L_FOriginSeed_0, day3.Origin.ToString("X8")); + lines.Add(line); + + lines.Add($"Initial: 0x{day3.Initial:X8}, Frame: {day3.Advances + 1}"); // frames are 1-indexed + var sb = new StringBuilder(); + AppendFrameTimeStamp(day3.Advances, sb); + lines.Add($"Time: {sb}"); + } } return; } @@ -160,6 +193,8 @@ private static void AddEncounterInfoPIDIV(List lines, LegalInfo info) } if (enc is EncounterSlot3 or EncounterStatic3) AppendDetailsFrame3(info, lines); + else if (enc is EncounterSlot4 or EncounterStatic4) + AppendDetailsDate4(info, lines); } private static string GetLinePokewalkerSeed(LegalInfo info) @@ -195,17 +230,47 @@ private static string GetLineSlot34(LegalInfo info, PIDIV pidiv, IEncounterSlot3 return line; } + private static void AppendDetailsDate4(LegalInfo info, List lines) + { + var pidiv = info.PIDIV; + if (pidiv.Type is not (PIDType.Method_1 or PIDType.ChainShiny)) + return; + + // Try to determine date/time + var enc = info.EncounterOriginal; + var seed = enc is EncounterSlot4 && info.FrameMatches ? pidiv.EncounterSeed : pidiv.OriginSeed; + + // Assume the met date is the same as the encounter date. + var entity = info.Entity; + var date = entity.MetDate ?? new DateOnly(2000, 1, 1); + var initialSeed = ClassicEraRNG.SeekInitialSeed((uint)date.Year, (uint)date.Month, (uint)date.Day, seed); + AppendInitialDateTime(lines, initialSeed, seed, date); + } + + private static void AppendInitialDateTime(List lines, uint initialSeed, uint origin, DateOnly date) + { + var decompose = ClassicEraRNG.DecomposeSeed(initialSeed, (uint)date.Year, (uint)date.Month, (uint)date.Day); + AppendInitialDateTime(lines, initialSeed, origin, decompose); + } + + private static void AppendInitialDateTime(List lines, uint initialSeed, uint origin, InitialSeedComponents4 decompose) + { + var advances = LCRNG.GetDistance(initialSeed, origin); + lines.Add($"{decompose.Year+2000:0000}-{decompose.Month:00}-{decompose.Day:00} @ {decompose.Hour:00}:{decompose.Minute:00}:{decompose.Second:00} - {decompose.Delay}"); + lines.Add($"Initial: 0x{initialSeed:X8}, Frame: {advances + 1}"); // frames are 1-indexed + } + private static void AppendDetailsFrame3(LegalInfo info, List lines) { var pidiv = info.PIDIV; var pk = info.Entity; var enc = info.EncounterOriginal; var seed = enc is EncounterSlot3 && info.FrameMatches ? pidiv.EncounterSeed : pidiv.OriginSeed; - var (initialSeed, frame) = GetInitialSeed(seed, pk.Version); - lines.Add($"Initial: 0x{initialSeed:X8}, Frame: {frame + 1}"); // frames are 1-indexed + var (initialSeed, advances) = GetInitialSeed3(seed, pk.Version); + lines.Add($"Initial: 0x{initialSeed:X8}, Frame: {advances + 1}"); // frames are 1-indexed var sb = new StringBuilder(); - AppendFrameTimeStamp(frame, sb); + AppendFrameTimeStamp(advances, sb); lines.Add($"Time: {sb}"); // Try appending the TID frame if it originates from Emerald. @@ -213,12 +278,12 @@ private static void AppendDetailsFrame3(LegalInfo info, List lines) return; // don't bother ignoring postgame-only. E4 resets the seed, but it's annoying to check. var tidSeed = pk.TID16; // start of game - var tidFrame = LCRNG.GetDistance(tidSeed, seed); - if (tidFrame >= frame) + var tidAdvances = LCRNG.GetDistance(tidSeed, seed); + if (tidAdvances >= advances) return; // only show if it makes sense to - lines.Add($"New Game: 0x{tidSeed:X8}, Frame: {tidFrame + 1}"); // frames are 1-indexed + lines.Add($"New Game: 0x{tidSeed:X8}, Frame: {tidAdvances + 1}"); // frames are 1-indexed sb.Clear(); - AppendFrameTimeStamp(tidFrame, sb); + AppendFrameTimeStamp(tidAdvances, sb); lines.Add($"Time: {sb}"); } @@ -236,7 +301,7 @@ private static void AppendFrameTimeStamp(uint frame, StringBuilder sb) sb.Append($" (days: {(int)time.TotalDays})"); } - private static (uint Seed, uint Frame) GetInitialSeed(uint seed, GameVersion game) + private static (uint Seed, uint Advances) GetInitialSeed3(uint seed, GameVersion game) { if (game is GameVersion.E) // Always 0 seed. return (0, LCRNG.GetDistance(0, seed)); @@ -251,9 +316,9 @@ private static (uint Seed, uint Frame) GetInitialSeed(uint seed, GameVersion gam if (game is GameVersion.R or GameVersion.S) { const uint drySeed = 0x05A0; - var dryFrame = LCRNG.GetDistance(drySeed, seed); - if (dryFrame < ushort.MaxValue << 2) - return (drySeed, dryFrame); + var advances = LCRNG.GetDistance(drySeed, seed); + if (advances < ushort.MaxValue << 2) + return (drySeed, advances); } return (nearest16, ctr); } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs index 17577f08d..cc3d991ba 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup2.cs @@ -35,7 +35,7 @@ public sealed class LearnGroup2 : ILearnGroup Check(result, current, pk, evos[i], i, option, types); } - if (enc is EncounterEgg { Generation: Generation } egg) + if (enc is EncounterEgg2 egg) CheckEncounterMoves(result, current, egg); bool vc1 = pk.VC1; @@ -82,7 +82,7 @@ private static void GetEncounterMoves(PKM pk, IEncounterTemplate enc, Span result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg2 egg) { ILearnSource inst = egg.Version == GameVersion.C ? LearnSource2C.Instance : LearnSource2GS.Instance; var eggMoves = inst.GetEggMoves(egg.Species, egg.Form); diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs index 663bc9577..ad71e64ef 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs @@ -21,7 +21,7 @@ public sealed class LearnGroup3 : ILearnGroup for (var i = 0; i < evos.Length; i++) Check(result, current, pk, evos[i], i, types); - if (types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg { Generation: Generation } egg) + if (types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg3 { Generation: Generation } egg) CheckEncounterMoves(result, current, egg); if (types.HasFlag(MoveSourceType.LevelUp) && enc.Species is (int)Species.Nincada && evos is [{ Species: (int)Species.Shedinja }, _]) @@ -65,7 +65,7 @@ private static void CheckNincadaMoves(Span result, ReadOnlySpan result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg3 egg) { ILearnSource inst = egg.Version switch { diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs index 1ee2b89f8..c0307d34e 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup4.cs @@ -21,7 +21,7 @@ public sealed class LearnGroup4 : ILearnGroup for (var i = 0; i < evos.Length; i++) Check(result, current, pk, evos[i], i); - if (types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg { Generation: Generation } egg) + if (types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg4 egg) CheckEncounterMoves(result, current, egg); if (types.HasFlag(MoveSourceType.LevelUp) && enc.Species is (int)Species.Nincada && evos is [{ Species: (int)Species.Shedinja }, _]) @@ -57,7 +57,7 @@ private static void CheckNincadaMoves(Span result, ReadOnlySpan result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg4 egg) { ILearnSource inst = egg.Version switch { diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs index 2b6b73f1a..3c394b8ac 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup5.cs @@ -21,13 +21,13 @@ public sealed class LearnGroup5 : ILearnGroup for (var i = 0; i < evos.Length; i++) Check(result, current, pk, evos[i], i, types, option); - if (types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg { Generation: Generation } egg) + if (types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg5 egg) CheckEncounterMoves(result, current, egg); return MoveResult.AllParsed(result); } - private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg5 egg) { ILearnSource inst = egg.Version > GameVersion.B ? LearnSource5B2W2.Instance : LearnSource5BW.Instance; var eggMoves = inst.GetEggMoves(egg.Species, egg.Form); diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs index e9f15009e..a822aa300 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup6.cs @@ -24,7 +24,7 @@ public sealed class LearnGroup6 : ILearnGroup if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter)) { - if (enc is EncounterEgg { Generation: Generation } egg) + if (enc is EncounterEgg6 egg) CheckEncounterMoves(result, current, egg); else if (enc is EncounterSlot6AO { CanDexNav: true } dexnav && pk.IsOriginalMovesetDeleted()) CheckDexNavMoves(result, current, dexnav); @@ -33,7 +33,7 @@ public sealed class LearnGroup6 : ILearnGroup return MoveResult.AllParsed(result); } - private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg6 egg) { ILearnSource inst = egg.Version > GameVersion.Y ? LearnSource6AO.Instance : LearnSource6XY.Instance; var eggMoves = inst.GetEggMoves(egg.Species, egg.Form); diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs index 6167b0705..a4df815cc 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs @@ -29,13 +29,13 @@ public sealed class LearnGroup7 : ILearnGroup for (var i = 0; i < evos.Length; i++) Check(result, current, pk, evos[i], i, types, option, mode); - if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg { Generation: Generation } egg) + if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter) && enc is EncounterEgg7 egg) CheckEncounterMoves(result, current, egg); return MoveResult.AllParsed(result); } - private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg7 egg) { ILearnSource inst = egg.Version > GameVersion.MN ? LearnSource7USUM.Instance : LearnSource7SM.Instance; var eggMoves = inst.GetEggMoves(egg.Species, egg.Form); diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs index 8983300ec..4048fecaa 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs @@ -46,7 +46,7 @@ public sealed class LearnGroup8 : ILearnGroup CheckSharedMoves(result, current, evos[0]); - if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter) && pk.IsOriginalMovesetDeleted() && enc is EncounterEgg { Generation: Generation } egg) + if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter) && pk.IsOriginalMovesetDeleted() && enc is EncounterEgg8 egg) CheckEncounterMoves(result, current, egg); if (MoveResult.AllParsed(result)) @@ -76,7 +76,7 @@ private static void CheckSharedMoves(Span result, ReadOnlySpan result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg8 egg) { var game = LearnSource8SWSH.Instance; var eggMoves = game.GetEggMoves(egg.Species, egg.Form); diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs index 67d9c468d..bc2333fd6 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs @@ -27,7 +27,7 @@ public sealed class LearnGroup9 : ILearnGroup CheckSharedMoves(result, current, evos[0]); - if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter) && pk.IsOriginalMovesetDeleted() && enc is EncounterEgg { Generation: Generation } egg) + if (option.IsPast() && types.HasFlag(MoveSourceType.Encounter) && pk.IsOriginalMovesetDeleted() && enc is EncounterEgg9 egg) CheckEncounterMoves(result, current, egg); if (MoveResult.AllParsed(result)) @@ -57,7 +57,7 @@ private static void CheckSharedMoves(Span result, ReadOnlySpan result, ReadOnlySpan current, EncounterEgg egg) + private static void CheckEncounterMoves(Span result, ReadOnlySpan current, EncounterEgg9 egg) { var game = LearnSource9SV.Instance; var eggMoves = game.GetEggMoves(egg.Species, egg.Form); diff --git a/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierEgg.cs b/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierEgg.cs index 2184dee38..6024092be 100644 --- a/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierEgg.cs +++ b/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierEgg.cs @@ -17,7 +17,7 @@ public static void Verify(Span result, ReadOnlySpan current, private static void VerifyPre3DS(Span result, ReadOnlySpan current, IEncounterTemplate enc) { - if (enc is EncounterEgg e) + if (enc is IEncounterEgg e) LearnVerifierRelearn.VerifyEggMoveset(e, result, current); else VerifyFromEncounter(result, current, enc); @@ -74,7 +74,7 @@ private static void VerifyMovesInitial(Span result, ReadOnlySpan result, ReadOnlySpan current, IEncounterTemplate enc, PKM pk) { - if (enc is EncounterEgg) + if (enc is IEncounterEgg) VerifyMatchesRelearn(result, current, pk); else if (enc is IMoveset { Moves: { HasMoves: true } x }) VerifyMovesInitial(result, current, x, GameData.GetLearnSource(enc.Version).Environment); diff --git a/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierRelearn.cs b/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierRelearn.cs index 5b9c7dce6..c5dd4f1c9 100644 --- a/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierRelearn.cs +++ b/PKHeX.Core/Legality/LearnSource/Verify/LearnVerifierRelearn.cs @@ -14,7 +14,7 @@ public static void Verify(Span result, IEncounterTemplate enc, PKM p VerifyRelearnNone(pk, result); else if (enc is IRelearn {Relearn: {HasMoves: true} x}) VerifyRelearnSpecifiedMoveset(pk, x, result); - else if (enc is EncounterEgg e) + else if (enc is IEncounterEgg e) VerifyEggMoveset(e, result, pk); else if (enc is EncounterSlot6AO { CanDexNav: true } z && pk.RelearnMove1 != 0) VerifyRelearnDexNav(pk, result, z); @@ -75,14 +75,14 @@ private static void VerifyRelearnNone(PKM pk, Span result) result[0] = ParseExpect(pk.RelearnMove1); } - private static void VerifyEggMoveset(EncounterEgg e, Span result, PKM pk) + private static void VerifyEggMoveset(IEncounterEgg e, Span result, PKM pk) { Span moves = stackalloc ushort[4]; pk.GetRelearnMoves(moves); VerifyEggMoveset(e, result, moves); } - internal static void VerifyEggMoveset(EncounterEgg e, Span result, ReadOnlySpan moves) + internal static void VerifyEggMoveset(IEncounterEgg e, Span result, ReadOnlySpan moves) { var gen = e.Generation; Span origins = stackalloc byte[moves.Length]; @@ -94,7 +94,7 @@ internal static void VerifyEggMoveset(EncounterEgg e, Span result, R if (moves[i] == 0) result[i] = MoveResult.Empty; else - result[i] = new(EggSourceUtil.GetSource(origins[i], gen), GameData.GetLearnSource(e.Version).Environment); + result[i] = new(EggSourceUtil.GetSource(origins[i], gen), e.Learn.Environment); } } else @@ -111,7 +111,7 @@ internal static void VerifyEggMoveset(EncounterEgg e, Span result, R else if (current == 0) result[i] = MoveResult.Empty; else - result[i] = new(EggSourceUtil.GetSource(origins[i], gen), GameData.GetLearnSource(e.Version).Environment); + result[i] = new(EggSourceUtil.GetSource(origins[i], gen), e.Learn.Environment); } } diff --git a/PKHeX.Core/Legality/MoveListSuggest.cs b/PKHeX.Core/Legality/MoveListSuggest.cs index 66300537f..fdb8da4db 100644 --- a/PKHeX.Core/Legality/MoveListSuggest.cs +++ b/PKHeX.Core/Legality/MoveListSuggest.cs @@ -132,7 +132,7 @@ private static void GetSuggestedRelearnInternal(this IEncounterTemplate enc, PKM { if (enc is IRelearn { Relearn: { HasMoves: true } r }) r.CopyTo(moves); - else if (enc is EncounterEgg or EncounterInvalid { IsEgg: true }) + else if (enc is IEncounterEgg or EncounterInvalid { IsEgg: true }) GetSuggestedRelearnEgg(enc, pk, moves); } @@ -157,7 +157,7 @@ public static void GetSuggestedRelearnMovesFromEncounter(this LegalityAnalysis a if (LearnVerifierRelearn.ShouldNotHaveRelearnMoves(enc, pk)) return; - if (enc is EncounterEgg or EncounterInvalid {IsEgg: true}) + if (enc is IEncounterEgg or EncounterInvalid {IsEgg: true}) enc.GetSuggestedRelearnEgg(info.Moves, pk, moves); else enc.GetSuggestedRelearnInternal(pk, moves); diff --git a/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs b/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs index f22a8e7b1..7f808dae5 100644 --- a/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs +++ b/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs @@ -31,7 +31,7 @@ public static class MoveBreed /// Gets the expected moves the egg should come with, using an input of requested that are requested to be in the output. /// /// Moves requested to be in the expected moves result - /// Encounter detail interface wrapper; should always be . + /// Encounter detail interface wrapper; should always be or . /// Result moves that are valid /// Validates the requested moves first prior to trying a more expensive computation. /// True if the is valid using the input . If not valid, the will be base egg moves, probably valid. diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs b/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs index f91b11a4f..c98e7ac27 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs @@ -1,3 +1,6 @@ +using System; +using System.ComponentModel.DataAnnotations; + namespace PKHeX.Core; /// @@ -89,4 +92,208 @@ public static uint GetSequentialIVs(ref uint seed) var rand2 = LCRNG.Next15(ref seed); return (rand2 << 15) | rand1; } + + /// + /// Creates an initial seed from the given components. + /// + /// Year component (2000-2099). + /// Month component (1-12). + /// Day component (1-31). + /// Hour component (0-23). + /// Minute component (0-59). + /// Seconds component (0-59). + /// Delay timer component. + /// + /// No sanity checking if the Month/Day/Year are valid. + /// + public static uint GetInitialSeed(uint year, uint month, uint day, uint hour, uint minute, uint second, uint delay) + { + byte ab = (byte)(month * day + minute + second); + byte cd = (byte)hour; + + return (uint)(((ab << 24) | (cd << 16))) + delay + year - 2000u; + } + + /// + /// Finds the initial seed for a given date and time. + /// + /// Year component (2000-2099). + /// Month component (1-12). + /// Day component (1-31). + /// Origin seed to look backwards for the initial seed. + /// + /// No sanity checking if the Month/Day/Year are valid. + /// + public static uint SeekInitialSeed(uint year, uint month, uint day, uint seed) + { + while (true) + { + if (IsInitialSeed(year, month, day, seed)) + break; + seed = LCRNG.Prev(seed); + } + var decompose = DecomposeSeed(seed, year, month, day); + // Check one step previous, just in case that delay is better. + var prevSeed = LCRNG.Prev(seed); + while (true) + { + if (IsInitialSeed(year, month, day, prevSeed)) + break; + prevSeed = LCRNG.Prev(prevSeed); + } + + var distance = LCRNG.GetDistance(prevSeed, seed); + if (distance > 5000) // arbitrary limit, most won't need this many advances to RNG. + return seed; // don't go too far back + + var prevDecompose = DecomposeSeed(prevSeed, year, month, day); + // Check if the previous seed has a better delay + if (prevDecompose.Delay < decompose.Delay) + return prevSeed; + return seed; + } + + /// + /// Checks if a seed is an initial seed for the given date and time. + /// + /// Year component (2000-2099). + /// Month component (1-12). + /// Day component (1-31). + /// Initial seed to check. + /// true if the seed is an initial seed for the given date and time; otherwise, false. + public static bool IsInitialSeed(uint year, uint month, uint day, uint seed) + { + // Check component: hour + var hour = (byte)(seed >> 16 & 0xFF); + if (hour > 23) + return false; + + // Check component: everything else but delay/year using modular arithmetic to handle overflow + const uint maxBonusMinSec = 59 + 59; // min + sec + var top = (byte)(seed >> 24); + var topMin = (byte)(month * day); + // Calculate the difference modulo 256. If it exceeds maxBonusMinSec, it's out of range. + if ((byte)(top - topMin) > maxBonusMinSec) + return false; + + // Check component: delay/year + // Should be a plausible delay; even though delay can overflow, it would take at least half an hour of waiting to launch the game to do so. + const uint baseDelay = 400; // hg/ss + var minDelay = baseDelay + (year - 2000u); + const uint maxDelay = 6000; + + var delayComponent = (ushort)(seed - minDelay); + // Check if the delay is within the plausible range + if (delayComponent > maxDelay) + return false; + + return true; + } + + /// + /// Decomposes a seed into its datetime initial seed components. + /// + /// Initial seed to decompose. + /// Year of the initial seed. + /// Month of the initial seed. + /// Day of the initial seed. + /// if any component is out of range and is not an Initial Seed. + public static InitialSeedComponents4 DecomposeSeed(uint seed, uint year, uint month, uint day) + { + // Check component: hour + var hour = (byte)(seed >> 16 & 0xFF); + ArgumentOutOfRangeException.ThrowIfGreaterThan(hour, 23, nameof(hour)); + + // Check component: everything else but delay/year using modular arithmetic to handle overflow + const uint maxBonusMinSec = 59 + 59; // min + sec + var top = (byte)(seed >> 24); + var topMin = (byte)(month * day); + // Calculate the difference modulo 256. If it exceeds maxBonusMinSec, it's out of range. + var delta = (byte)(top - topMin); + ArgumentOutOfRangeException.ThrowIfGreaterThan(delta, maxBonusMinSec, nameof(top)); + + var yearComponent = (byte)(year - 2000u); + var delay = (ushort)((ushort)seed - yearComponent); + + // Minute and seconds: prefer a seconds value at least 7, at most 15 if possible. + const byte minSec = 7; + const byte maxSec = 15; + byte min; byte sec; + if (delta < 59 + maxSec) + { + sec = delta >= minSec ? (byte)(delta - minSec) : delta; + min = (byte)(delta - sec); + } + else + { + // need a higher seconds + min = 59; + sec = (byte)(delta - 59); + } + + return new InitialSeedComponents4 + { + Year = yearComponent, + Month = (byte)month, + Day = (byte)day, + Hour = hour, + Delay = delay, + + Minute = min, + Second = sec, + }; + } + + /// + /// Finds the initial seed for a given set of IVs in Generation 4. + /// + /// IVs to use for the search. + /// Year of the initial seed. + /// Month of the initial seed. + /// Day of the initial seed. + /// Seed that originated the IVs. + /// Initial datetime seed. + public static uint SeekInitialSeedForIVs(ReadOnlySpan ivs, uint year, uint month, uint day, out uint origin) + { + origin = 0; + uint bestDistance = uint.MaxValue; + + Span seeds = stackalloc uint[LCRNG.MaxCountSeedsIV]; + var count = LCRNGReversal.GetSeedsIVs(seeds, (uint)ivs[0], (uint)ivs[1], (uint)ivs[2], (uint)ivs[4], (uint)ivs[5], (uint)ivs[3]); + if (count == 0) + return 0; // shouldn't happen; IVs should always find seeds. + + seeds = seeds[..count]; + + uint best = 0; + foreach (var seed in seeds) + { + var init = SeekInitialSeed(year, month, day, seed); + var distance = LCRNG.GetDistance(init, seed); + if (distance > bestDistance) + continue; + bestDistance = distance; + best = init; + origin = seed; + } + return best; + } +} + +/// +/// Stores the components of an initial seed from Generation 4. +/// +public readonly record struct InitialSeedComponents4 +{ + [Range(0, 99)] public required byte Year { get; init; } + [Range(1, 12)] public required byte Month { get; init; } + [Range(1, 31)] public required byte Day { get; init; } + [Range(0, 23)] public required byte Hour { get; init; } + [Range(0, 59)] public required byte Minute { get; init; } + [Range(0, 59)] public required byte Second { get; init; } + + public required ushort Delay { get; init; } // essentially XXX-65535, but can overflow. Not that anyone waits the 30+ minutes to do that since other initial seeds are more efficient. + + public uint ToSeed() => ClassicEraRNG.GetInitialSeed(Year, Month, Day, Hour, Minute, Second, Delay); + public bool IsInvalid() => Month == 0; } diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/Daycare3.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/Daycare3.cs new file mode 100644 index 000000000..d8a3a04f6 --- /dev/null +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/Daycare3.cs @@ -0,0 +1,279 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Generation 3 Daycare RNG correlation logic. +/// +public static class Daycare3 +{ + /// + /// Checks if the PID is possible to obtain, in isolation. + /// + /// Obtained PID. + /// Version the egg was obtained in. + /// true if the PID is valid, false otherwise. + public static bool IsValidProcPID(uint pid, GameVersion version) + { + // Gen3 Eggs don't have a zero-value pending PID value. + + // For R/S/FR/LG (not Emerald) + // LoveCheck: Rand() * 100 / 65535 < compatibility* + // PID: The game stores the lower 16 bits via (Rand() & 0xFFFE) + 1 + // 0-value for lower 16 bits is never set by egg proc + if (version is not GameVersion.E) + return (pid & 0xFFFF) != 0; + + // For Emerald, the PID is generated according to the following pattern: + // LoveCheck: Rand() * 100 / 65535 < compatibility* + // PID: The game stores the full 32-bit value to lock in nature; previous games only store the lower 16 bits. + // Everstone (inherit nature): 2400 attempts with rand() << 16 | rand() && pid != 0 -- assume never needing 2400 attempts even with vBlank in the mix. + // Otherwise (random nature): rand() << 16 | (rand() & 0xFFFE) + 1 + + // The PID will never be 0. + if (pid == 0) + return false; + + // The LoveCheck is not always immediately before the PID generation (Everstone). + // Considering vBlank interrupts, it's essentially random enough to get any PID (besides 0). + return true; + } + + /// + public static bool TryGetOriginSeed(PKM pk, out Daycare3Origin origin) + { + Span actual = stackalloc int[6]; + pk.GetIVs(actual); + if (pk.Version is GameVersion.E) + return TryGetOriginSeedEmerald(actual, out origin); + return TryGetOriginSeed(actual, pk.EncryptionConstant, out origin); + } + + /// + /// Searches for a state that the player picked up the egg. + /// + /// IVs to check for. + /// Obtained PID; only high 16 bits needed. + /// Version of the game. + /// Origin info when receiving the egg + /// true if a valid origin was found, false otherwise. + public static bool TryGetOriginSeed(ReadOnlySpan ivs, uint pid, GameVersion version, out Daycare3Origin origin) + { + if (version is GameVersion.E) + return TryGetOriginSeedEmerald(ivs, out origin); + return TryGetOriginSeed(ivs, pid, out origin); + } + + /// + public static bool TryGetOriginSeedEmerald(ReadOnlySpan ivs, out Daycare3Origin origin) + { + // Frame pattern: + // PID is already decided. Simply IVs. + // IVs low (overwritten by inheritance later) + // IVs high (overwritten by inheritance later) + // **vBlank + // Inheritance + + Span tmp = stackalloc int[6]; + + // Search forward from Emerald's initial 0 seed. + // Once we find a result, that's the lowest (best) result possible. + uint frame = 0xBE34A09C; // 60 frames after initial seed (0x0000_0000); arbitrary minimum frame. + + // Assume the most-frequent vBlank placement in the calculation sequence. + // (after IVs) + while (true) + { + var seed = frame = LCRNG.Next(frame); + + var iv1 = LCRNG.Next16(ref seed); + var iv2 = LCRNG.Next16(ref seed); + Fill(tmp, iv1, iv2); + + var countMatching = GetCountMatch(ivs, tmp); + if (countMatching < 3) + continue; + + _ = LCRNG.Next16(ref seed); // vBlank, setting IVs is slow + + // Determine inherited IVs + ApplyInheritanceEmerald(seed, tmp); + // Don't care which parent passes the IVs. + // Done. + + // Check if the IVs are valid + var count = GetCountMatchInherit(ivs, tmp); + if (count != 6) + continue; + + frame = LCRNG.Prev4(frame); // unroll once, and account for interaction lag (vBlank) + var advances = LCRNG.GetDistance(0, frame); + origin = new Daycare3Origin(frame, advances, 0); + + return true; + } + } + + /// + /// Usable by all versions (R/S/FR/LG), excluding Emerald. + /// + /// + public static bool TryGetOriginSeed(ReadOnlySpan ivs, uint pid, out Daycare3Origin origin) + { + // Frame pattern: + // PID high + // **vBlank + // IVs low (overwritten by inheritance later) + // IVs high (overwritten by inheritance later) + // **vBlank + // Inheritance + + Span tmp = stackalloc int[6]; + origin = default; + + // PID is the first Rand() call. We don't know the lower bits of the seed, so try all. + // The seed that produces the IV pattern with the lowest amount of advances from a 16-bit seed is our result. + // For Ruby Sapphire RNG abused eggs, ideally we discover 0x05A0 is the initial seed. + var high = pid & 0xFFFF0000; + + // Assume the most-frequent vBlank placement in the calculation sequence. + // (after PID, after IVs) + for (uint i = 0; i < 0x10000; i++) + { + var seed = high | i; // PID high; unknown lower portion of seed. + + _ = LCRNG.Next16(ref seed); // vBlank, set PID and all misc PKM properties before IVs + var iv1 = LCRNG.Next16(ref seed); + var iv2 = LCRNG.Next16(ref seed); + Fill(tmp, iv1, iv2); + + var countMatching = GetCountMatch(ivs, tmp); + if (countMatching < 3) + continue; + + _ = LCRNG.Next16(ref seed); // vBlank, setting IVs is slow + + // Determine inherited IVs + ApplyInheritance(seed, tmp); + // Don't care which parent passes the IVs. + // Done. + + // Check if the IVs are valid + var count = GetCountMatchInherit(ivs, tmp); + if (count != 6) + continue; + + var generate = LCRNG.Prev4(high | i); // unroll once, and account for interaction lag (vBlank) + UpdateIfBetter(ref origin, generate); + } + + return origin.Pattern != Daycare3Correlation.None; + } + + private static void ApplyInheritance(uint seed, Span tmp) + { + Span statIndexes = stackalloc int[6]; + for (int i = 0; i < 6; i++) + statIndexes[i] = i; + + for (int i = 0; i < 3; i++) + { + var index = (int)LCRNG.Next16(ref seed) % (6 - i); + var inherit = statIndexes[index]; + for (int j = index + 1; j < 6; j++) + statIndexes[j - 1] = statIndexes[j]; + + tmp[inherit] = -1; // Inherit this stat + } + } + + private static void ApplyInheritanceEmerald(uint seed, Span tmp) + { + // Game Bug: Instead of removing the IV that was just picked, this + // removes position 0 (HP) then position 1 (DEF), then position 2. + Span statIndexes = stackalloc int[6]; + for (int i = 0; i < 6; i++) + statIndexes[i] = i; + + for (int i = 0; i < 3; i++) + { + var index = (int)LCRNG.Next16(ref seed) % (6 - i); + var inherit = statIndexes[index]; + for (int j = /* index */ i + 1; j < 6; j++) + statIndexes[j - 1] = statIndexes[j]; + + tmp[inherit] = -1; // Inherit this stat + } + } + + private static void Fill(Span tmp, uint iv1, uint iv2) + { + tmp[0] = (byte)(iv1 & 0x1F); + tmp[1] = (byte)((iv1 >> 5) & 0x1F); + tmp[2] = (byte)((iv1 >> 10) & 0x1F); + tmp[3] = (byte)(iv2 & 0x1F); + tmp[4] = (byte)((iv2 >> 5) & 0x1F); + tmp[5] = (byte)((iv2 >> 10) & 0x1F); + } + + private static int GetCountMatch(ReadOnlySpan actual, ReadOnlySpan tmp) + { + int count = 0; + for (int i = 0; i < actual.Length; i++) + { + if (actual[i] == tmp[i]) + count++; + } + return count; + } + + private static int GetCountMatchInherit(ReadOnlySpan actual, ReadOnlySpan tmp) + { + int count = 0; + for (int i = 0; i < actual.Length; i++) + { + var iv = tmp[i]; + if (iv == -1 || actual[i] == iv) + count++; + } + return count; + } + + private static void UpdateIfBetter(ref Daycare3Origin best, uint origin, Daycare3Correlation pattern = Daycare3Correlation.Regular) + { + // Determine initial seed + var seed = LCRNG.Prev9(origin); // arbitrary un-hittable frames + while (seed >> 16 != 0) + seed = LCRNG.Prev(seed); + + var advances = LCRNG.GetDistance(seed, origin); + + // Check if the seed is better + if (best.Advances >= advances || best.Pattern is Daycare3Correlation.None) + best = new Daycare3Origin(origin, advances, (ushort)seed, pattern); + } +} + +/// +/// Stores initial seed information for Gen3 daycare seeds. +/// +/// Seed that originates the egg on pickup from Daycare. +/// Advances from the initial seed to the origin seed. +/// Initial seed from the start of the game. +/// Generation pattern of the egg. +public readonly record struct Daycare3Origin(uint Origin, uint Advances, ushort Initial, Daycare3Correlation Pattern = Daycare3Correlation.Regular); + +public enum Daycare3Correlation +{ + /// + /// None detected, usually just a sentinel value. + /// + None, + + /// + /// Standard vBlank pattern. + /// + Regular, + + // Other patterns may be added in the future if other examples necessitate it. +} diff --git a/PKHeX.Core/Legality/Restrictions/Memories/MemoryPermissions.cs b/PKHeX.Core/Legality/Restrictions/Memories/MemoryPermissions.cs index 46f16e4b2..b7512c472 100644 --- a/PKHeX.Core/Legality/Restrictions/Memories/MemoryPermissions.cs +++ b/PKHeX.Core/Legality/Restrictions/Memories/MemoryPermissions.cs @@ -123,7 +123,7 @@ private static bool IsSpecialEncounterMoveEggDeleted(PKM pk, IEncounterTemplate { if (pk.IsOriginalMovesetDeleted()) return true; - return enc is EncounterEgg { Generation: < 6 }; // egg moves that are no longer in the movepool + return enc is IEncounterEgg { Generation: < 6 }; // egg moves that are no longer in the movepool } public static bool GetCanRelearnMove(PKM pk, ushort move, EntityContext context, EvolutionHistory history, IEncounterTemplate enc) diff --git a/PKHeX.Core/Legality/Verifiers/Ability/AbilityVerifier.cs b/PKHeX.Core/Legality/Verifiers/Ability/AbilityVerifier.cs index 4044d5093..324d09b35 100644 --- a/PKHeX.Core/Legality/Verifiers/Ability/AbilityVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/Ability/AbilityVerifier.cs @@ -322,7 +322,7 @@ private CheckResult VerifyAbility5(LegalityAnalysis data, IEncounterTemplate enc // Eggs and Encounter Slots are not yet checked for Hidden Ability potential. return enc switch { - EncounterEgg e when pk.AbilityNumber == 4 && !AbilityBreedLegality.IsHiddenPossible5(e.Species) => GetInvalid(LAbilityHiddenUnavailable), + EncounterEgg5 egg when pk.AbilityNumber == 4 && !egg.Ability.CanBeHidden() => GetInvalid(LAbilityHiddenUnavailable), _ => CheckMatch(data.Entity, abilities, 5, pk.Format == 5 ? AbilityState.MustMatch : AbilityState.CanMismatch, enc), }; } @@ -335,7 +335,7 @@ private CheckResult VerifyAbility6(LegalityAnalysis data, IEncounterTemplate enc return enc switch { - EncounterEgg egg when !AbilityBreedLegality.IsHiddenPossible6(egg.Species, egg.Form) => GetInvalid(LAbilityHiddenUnavailable), + EncounterEgg6 egg when !egg.Ability.CanBeHidden() => GetInvalid(LAbilityHiddenUnavailable), _ => VALID, }; } @@ -348,7 +348,7 @@ private CheckResult VerifyAbility7(LegalityAnalysis data, IEncounterTemplate enc return enc switch { - EncounterEgg egg when !AbilityBreedLegality.IsHiddenPossible7(egg.Species, egg.Form) => GetInvalid(LAbilityHiddenUnavailable), + EncounterEgg7 egg when !egg.Ability.CanBeHidden() => GetInvalid(LAbilityHiddenUnavailable), _ => VALID, }; } @@ -361,7 +361,7 @@ private CheckResult VerifyAbility8BDSP(LegalityAnalysis data, IEncounterTemplate return enc switch { - EncounterEgg egg when !AbilityBreedLegality.IsHiddenPossibleHOME(egg.Species) => GetInvalid(LAbilityHiddenUnavailable), + EncounterEgg8b egg when !egg.Ability.CanBeHidden() => GetInvalid(LAbilityHiddenUnavailable), _ => VALID, }; } diff --git a/PKHeX.Core/Legality/Verifiers/Ball/BallVerifier.cs b/PKHeX.Core/Legality/Verifiers/Ball/BallVerifier.cs index c04c9b16c..8d571b15b 100644 --- a/PKHeX.Core/Legality/Verifiers/Ball/BallVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/Ball/BallVerifier.cs @@ -75,7 +75,7 @@ public static BallVerificationResult VerifyBall(IEncounterTemplate enc, Ball cur return VerifyBallEquals(current, ball); // Capturing with Heavy Ball is impossible in Sun/Moon for specific species. - if (current is Heavy && enc is not EncounterEgg && pk is { SM: true } && BallUseLegality.IsAlolanCaptureNoHeavyBall(enc.Species)) + if (current is Heavy && enc is not EncounterEgg7 && pk is { SM: true } && BallUseLegality.IsAlolanCaptureNoHeavyBall(enc.Species)) return BadCaptureHeavy; // Heavy Ball, can inherit if from egg (US/UM fixed catch rate calc) return enc switch @@ -86,13 +86,13 @@ public static BallVerificationResult VerifyBall(IEncounterTemplate enc, Ball cur EncounterSlot8 when pk is IRibbonSetMark8 { RibbonMarkCurry: true } or IRibbonSetAffixed { AffixedRibbon: (sbyte)RibbonIndex.MarkCurry } => GetResult(current is Poke or Great or Ultra), - EncounterEgg => VerifyBallEgg(enc, current, pk), // Inheritance rules can vary. + IEncounterEgg egg => VerifyBallEgg(egg, current, pk), // Inheritance rules can vary. EncounterStatic5Entree => VerifyBallEquals(current, BallUseLegality.DreamWorldBalls), _ => VerifyBallEquals(current, BallUseLegality.GetWildBalls(enc.Generation, enc.Version)), }; } - private static BallVerificationResult VerifyBallEgg(IEncounterTemplate enc, Ball ball, PKM pk) + private static BallVerificationResult VerifyBallEgg(IEncounterEgg enc, Ball ball, PKM pk) { if (enc.Generation < 6) // No inheriting Balls return VerifyBallEquals(ball, Poke); // Must be Poké Ball -- no ball inheritance. @@ -105,17 +105,17 @@ private static BallVerificationResult VerifyBallEgg(IEncounterTemplate enc, Ball }; } - private static BallVerificationResult VerifyBallInherited(IEncounterTemplate enc, Ball ball, PKM pk) => enc.Context switch + private static BallVerificationResult VerifyBallInherited(IEncounterEgg egg, Ball ball, PKM pk) => egg switch { - EntityContext.Gen6 => VerifyBallEggGen6(enc, ball, pk), // Gen6 Inheritance Rules - EntityContext.Gen7 => VerifyBallEggGen7(enc, ball, pk), // Gen7 Inheritance Rules - EntityContext.Gen8 => VerifyBallEggGen8(enc, ball), - EntityContext.Gen8b => VerifyBallEggGen8BDSP(enc, ball), - EntityContext.Gen9 => VerifyBallEggGen9(enc, ball), + EncounterEgg6 e6 => VerifyBallEggGen6(e6, ball, pk), // Gen6 Inheritance Rules + EncounterEgg7 e7 => VerifyBallEggGen7(e7, ball, pk), // Gen7 Inheritance Rules + EncounterEgg8 e8 => VerifyBallEggGen8(e8, ball), + EncounterEgg8b b => VerifyBallEggGen8BDSP(b, ball), + EncounterEgg9 e9 => VerifyBallEggGen9(e9, ball), _ => BadEncounter, }; - private static BallVerificationResult VerifyBallEggGen6(IEncounterTemplate enc, Ball ball, PKM pk) + private static BallVerificationResult VerifyBallEggGen6(EncounterEgg6 enc, Ball ball, PKM pk) { if (ball > Dream) return BadOutOfRange; @@ -124,7 +124,7 @@ private static BallVerificationResult VerifyBallEggGen6(IEncounterTemplate enc, return GetResult(result); } - private static BallVerificationResult VerifyBallEggGen7(IEncounterTemplate enc, Ball ball, PKM pk) + private static BallVerificationResult VerifyBallEggGen7(EncounterEgg7 enc, Ball ball, PKM pk) { if (ball > Beast) return BadOutOfRange; @@ -133,7 +133,7 @@ private static BallVerificationResult VerifyBallEggGen7(IEncounterTemplate enc, return GetResult(result); } - private static BallVerificationResult VerifyBallEggGen8BDSP(IEncounterTemplate enc, Ball ball) + private static BallVerificationResult VerifyBallEggGen8BDSP(EncounterEgg8b enc, Ball ball) { if (ball > Beast) return BadOutOfRange; @@ -146,7 +146,7 @@ private static BallVerificationResult VerifyBallEggGen8BDSP(IEncounterTemplate e return GetResult(result); } - private static BallVerificationResult VerifyBallEggGen8(IEncounterTemplate enc, Ball ball) + private static BallVerificationResult VerifyBallEggGen8(EncounterEgg8 enc, Ball ball) { if (ball > Beast) return BadOutOfRange; @@ -155,7 +155,7 @@ private static BallVerificationResult VerifyBallEggGen8(IEncounterTemplate enc, return GetResult(result); } - private static BallVerificationResult VerifyBallEggGen9(IEncounterTemplate enc, Ball ball) + private static BallVerificationResult VerifyBallEggGen9(EncounterEgg9 enc, Ball ball) { if (ball > Beast) return BadOutOfRange; diff --git a/PKHeX.Core/Legality/Verifiers/FormVerifier.cs b/PKHeX.Core/Legality/Verifiers/FormVerifier.cs index 5f75a932a..28c64af98 100644 --- a/PKHeX.Core/Legality/Verifiers/FormVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/FormVerifier.cs @@ -62,7 +62,7 @@ private CheckResult VerifyForm(LegalityAnalysis data) return GetInvalid(LFormBattle); case Pikachu when enc.Generation >= 7: // Cap - var expectForm = enc is EncounterInvalid or EncounterEgg ? 0 : enc.Form; + var expectForm = enc is EncounterInvalid or IEncounterEgg ? 0 : enc.Form; if (form != expectForm) { bool gift = enc is MysteryGift g && g.Form != form; @@ -108,7 +108,7 @@ private CheckResult VerifyForm(LegalityAnalysis data) case Scatterbug or Spewpa or Vivillon when enc.Context is EntityContext.Gen9: if (form > 18 && enc.Form != form) // Pokéball return GetInvalid(LFormVivillonEventPre); - if (form != 18 && enc is EncounterEgg) // Fancy + if (form != 18 && enc is IEncounterEgg) // Fancy return GetInvalid(LFormVivillonNonNative); break; case Scatterbug or Spewpa: diff --git a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs index 1f2f59256..a9af63d00 100644 --- a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs @@ -253,7 +253,7 @@ private void VerifySVStats(LegalityAnalysis data, PK9 pk9) } var enc = data.EncounterOriginal; - if (enc is EncounterEgg { Context: EntityContext.Gen9 } g) + if (enc is EncounterEgg9 g) { if (!Tera9RNG.IsMatchTeraTypePersonalEgg(g.Species, g.Form, (byte)pk9.TeraTypeOriginal)) data.AddLine(GetInvalid(LTeraTypeMismatch)); @@ -279,9 +279,9 @@ private void VerifySVStats(LegalityAnalysis data, PK9 pk9) if (!Locations9.IsAccessiblePreDLC(pk9.MetLocation)) { - if (enc is { Species: (int)Species.Larvesta, Form: 0 } and not EncounterEgg) + if (enc is { Species: (int)Species.Larvesta, Form: 0 } and not EncounterEgg9) DisallowLevelUpMove(24, (ushort)Move.BugBite, pk9, data); - else if (enc is { Species: (int)Species.Zorua, Form: 1 } and not EncounterEgg) + else if (enc is { Species: (int)Species.Zorua, Form: 1 } and not EncounterEgg9) DisallowLevelUpMove(28, (ushort)Move.Spite, pk9, data); else return; @@ -500,7 +500,7 @@ private static void VerifyMiscEggCommon(LegalityAnalysis data) if (!EggStateLegality.GetIsEggHatchCyclesValid(pk, enc)) data.AddLine(GetInvalid(LEggHatchCycles, Egg)); - if (pk.Format >= 6 && enc is EncounterEgg && !MovesMatchRelearn(pk)) + if (pk.Format >= 6 && enc is IEncounterEgg && !MovesMatchRelearn(pk)) { const int moveCount = 4; var sb = new StringBuilder(64); diff --git a/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs b/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs index cdabb165a..0dab4f1ca 100644 --- a/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs @@ -26,15 +26,15 @@ public override void Verify(LegalityAnalysis data) data.AddLine(Get(LPIDZero, Severity.Fishy)); if (!pk.Nature.IsFixed()) // out of range data.AddLine(GetInvalid(LPIDNatureMismatch)); - if (data.Info.EncounterMatch is EncounterEgg egg) + if (data.Info.EncounterMatch is IEncounterEgg egg) VerifyEggPID(data, pk, egg); VerifyShiny(data); } - private static void VerifyEggPID(LegalityAnalysis data, PKM pk, EncounterEgg egg) + private static void VerifyEggPID(LegalityAnalysis data, PKM pk, IEncounterEgg egg) { - if (egg.Generation is 4 && pk.EncryptionConstant == 0) + if (egg is EncounterEgg4) { // Gen4 Eggs are "egg available" based on the stored PID value in the save file. // If this value is 0 or is generated as 0 (possible), the game will see "false" and no egg is available. @@ -42,19 +42,30 @@ private static void VerifyEggPID(LegalityAnalysis data, PKM pk, EncounterEgg egg // However, With Masuda Method, the egg PID is re-rolled with the ARNG (until shiny, at most 4 times) upon receipt. // None of the un-rolled states share the same shiny-xor as PID=0, you can re-roll into an all-zero PID. // Flag it as fishy, because more often than not, it is hacked rather than a legitimately obtained egg. - data.AddLine(Get(LPIDEncryptZero, Severity.Fishy, CheckIdentifier.EC)); - return; - } + if (pk.EncryptionConstant == 0) + data.AddLine(Get(LPIDEncryptZero, Severity.Fishy, CheckIdentifier.EC)); - if (egg.Generation is 3 or 4 && Breeding.IsGenderSpeciesDetermination(egg.Species)) - { - var gender = pk.Gender; - if (!Breeding.IsValidSpeciesBit34(pk.EncryptionConstant, gender)) // 50/50 chance! - { - if (gender == 1 || IsEggBitRequiredMale34(data.Info.Moves)) - data.AddLine(GetInvalid(LPIDGenderMismatch, CheckIdentifier.EC)); - } + if (Breeding.IsGenderSpeciesDetermination(egg.Species)) + VerifyEggGender8000(data, pk); } + else if (egg is EncounterEgg3) + { + if (!Daycare3.IsValidProcPID(pk.EncryptionConstant, egg.Version)) + data.AddLine(Get(LPIDEncryptZero, Severity.Invalid, CheckIdentifier.EC)); + + if (Breeding.IsGenderSpeciesDetermination(egg.Species)) + VerifyEggGender8000(data, pk); + // PID and IVs+Inheritance randomness is sufficiently random; any permutation of vBlank correlations is possible. + } + } + + private static void VerifyEggGender8000(LegalityAnalysis data, PKM pk) + { + var gender = pk.Gender; + if (Breeding.IsValidSpeciesBit34(pk.EncryptionConstant, gender)) + return; // 50/50 chance! + if (gender == 1 || IsEggBitRequiredMale34(data.Info.Moves)) + data.AddLine(GetInvalid(LPIDGenderMismatch, CheckIdentifier.EC)); } private void VerifyShiny(LegalityAnalysis data) diff --git a/PKHeX.Core/Legality/Verifiers/TrashByteVerifier.cs b/PKHeX.Core/Legality/Verifiers/TrashByteVerifier.cs index 2ddec5326..9ef097b3c 100644 --- a/PKHeX.Core/Legality/Verifiers/TrashByteVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/TrashByteVerifier.cs @@ -102,7 +102,7 @@ private void VerifyTrashBytesHOME(LegalityAnalysis data, PKM pk) VerifyTrashNickname(data, pk.NicknameTrash); var enc = data.Info.EncounterMatch; - if (enc is EncounterEgg && pk.WasTradedEgg) + if (enc is IEncounterEgg && pk.WasTradedEgg) { // Allow Traded eggs to have a single layer of OT trash bytes. VerifyTrashSingle(data, pk.OriginalTrainerTrash, OriginalTrainer); diff --git a/PKHeX.Core/MysteryGifts/PCD.cs b/PKHeX.Core/MysteryGifts/PCD.cs index 57004ea4f..3431aa0b3 100644 --- a/PKHeX.Core/MysteryGifts/PCD.cs +++ b/PKHeX.Core/MysteryGifts/PCD.cs @@ -101,7 +101,7 @@ public override string CardTitle public override ushort Location { get => (ushort)(IsEgg ? 0 : Gift.EggLocation + 3000); set { } } public override ushort EggLocation { get => (ushort)(IsEgg ? Gift.EggLocation + 3000 : 0); set { } } - public bool IsCompatible(PIDType type, PKM pk) => Gift.IsCompatible(type, pk); + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) => Gift.IsCompatible(type, pk); public PIDType GetSuggestedCorrelation() => Gift.GetSuggestedCorrelation(); public bool GiftEquals(PGT pgt) diff --git a/PKHeX.Core/MysteryGifts/PGT.cs b/PKHeX.Core/MysteryGifts/PGT.cs index c9f9d9133..585200395 100644 --- a/PKHeX.Core/MysteryGifts/PGT.cs +++ b/PKHeX.Core/MysteryGifts/PGT.cs @@ -1,6 +1,7 @@ using System; using static System.Buffers.Binary.BinaryPrimitives; using static PKHeX.Core.GiftType4; +using static PKHeX.Core.RandomCorrelationRating; namespace PKHeX.Core; @@ -317,6 +318,9 @@ private static void SetDefaultManaphyEggDetails(PK4 pk4, ITrainerInfo trainer) pk4.EggMetDate = pk4.MetDate = EncounterDate.GetDateNDS(); } + public bool HasPID => PK.PID > 1; // 0=Random, 1=Random (Anti-Shiny). 0 was never used in any Gen4 gift (all non-shiny). + public bool HasIVs => (PK.IV32 & 0x3FFF_FFFFu) != 0; // ignore Nickname/Egg flag bits + private static void SetPINGA(PK4 pk4, PersonalInfo4 pi, EncounterCriteria criteria) { // Ability is forced already, can't force anything @@ -449,13 +453,13 @@ public static bool IsRangerManaphy(PKM pk) public bool RibbonChampionWorld { get => PK.RibbonChampionWorld; set => PK.RibbonChampionWorld = value; } public bool RibbonSouvenir { get => PK.RibbonSouvenir; set => PK.RibbonSouvenir = value; } - public bool IsCompatible(PIDType type, PKM pk) + public RandomCorrelationRating IsCompatible(PIDType type, PKM pk) { if (IsManaphyEgg) - return IsG4ManaphyPIDValid(type, pk); + return IsG4ManaphyPIDValid(type, pk) ? Match : Mismatch; if (PK.PID != 1 && type == PIDType.G5MGShiny) - return true; - return type == PIDType.None; + return Match; + return type is PIDType.None ? Match : Mismatch; } public PIDType GetSuggestedCorrelation() diff --git a/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs b/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs index f3ce18b48..961bae785 100644 --- a/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs +++ b/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs @@ -32,7 +32,7 @@ public void SimulatorGetEncounters() var first = encounters.FirstOrDefault(); Assert.NotNull(first); - var egg = (EncounterEgg)first; + var egg = (EncounterEgg7)first; var info = new SimpleTrainerInfo(GameVersion.SN); var pk = egg.ConvertToPKM(info); Assert.True(pk.Species != set.Species);