diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterTrade4PID.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterTrade4PID.cs index 55fbd2b1b..25087596f 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterTrade4PID.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterTrade4PID.cs @@ -216,6 +216,14 @@ private bool IsMatchEggLocation(PKM pk) #endregion + /// + /// Language obtained by the trainer will be of a foreign language ID. + /// + /// + /// Does NOT indicate for bugged D/P English origin, which is Japanese. + /// + public bool IsLanguageSwap => Species is (ushort)Core.Species.Magikarp or (ushort)Core.Species.Pikachu; + public int DetectOriginalLanguage(PKM pk) { int lang = pk.Language; diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs index 869166a82..4853874f9 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionOrigin.cs @@ -24,12 +24,22 @@ namespace PKHeX.Core; public bool IsDiscardRequired(byte format) => format <= 2 && Options.HasFlag(OriginOptions.ForceDiscard); } +/// +/// Options for to modify logic checks based on the source of the constructor input. +/// [Flags] public enum OriginOptions : byte { None = 0, + + /// SkipChecks = 1 << 0, + + /// ForceDiscard = 1 << 1, + /// + /// Options relevant when checking for an encounter template, which bypasses logic checks against an Entity. + /// EncounterTemplate = SkipChecks | ForceDiscard, } diff --git a/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs b/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs index 6f2ed36b6..95a659b34 100644 --- a/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs @@ -18,10 +18,13 @@ public override void Verify(LegalityAnalysis data) if (pk.OriginalTrainerGender == 1) data.AddLine(GetInvalid(LG3OTGender, CheckIdentifier.Trainer)); + + // Trainer ID is checked in another verifier. Don't duplicate it here. } private static void VerifyStarterXD(LegalityAnalysis data) { + // The starter in XD must have the correct PIDIV type. var info = data.Info.PIDIV; if (info.Type is not (PIDType.CXD or PIDType.CXD_ColoStarter)) return; // already flagged as invalid diff --git a/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs b/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs index 5e57fa80c..dcf899090 100644 --- a/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs @@ -17,17 +17,19 @@ public override void Verify(LegalityAnalysis data) data.AddLine(GetInvalid(LItemEgg)); if (!ItemRestrictions.IsHeldItemAllowed(item, context: pk.Context)) - { data.AddLine(GetInvalid(LItemUnreleased)); - } - else if (pk.Format == 3 && item == 175) // Enigma Berry - { - // A Pokémon holding this Berry cannot be traded to Pokémon Colosseum or Pokémon XD: Gale of Darkness, nor can it be stored in Pokémon Box Ruby & Sapphire. - if (pk is CK3 or XK3) - data.AddLine(GetInvalid(LItemUnreleased)); - else - VerifyEReaderBerry(data); - } + else if (item == 175 && pk is G3PKM g3) // Enigma Berry + VerifyEnigmaGen3(data, g3); + } + + private void VerifyEnigmaGen3(LegalityAnalysis data, G3PKM g3) + { + // A Pokémon holding this Berry cannot be traded to Pokémon Colosseum or Pokémon XD: Gale of Darkness, + // nor can it be stored in Pokémon Box Ruby & Sapphire. + if (g3 is CK3 or XK3 || ParseSettings.ActiveTrainer is SAV3RSBox) + data.AddLine(GetInvalid(LItemUnreleased)); + else + VerifyEReaderBerry(data); } private void VerifyEReaderBerry(LegalityAnalysis data) diff --git a/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs b/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs index a3c8e187b..9080775cc 100644 --- a/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs @@ -1,4 +1,6 @@ using static PKHeX.Core.LegalityCheckStrings; +using static PKHeX.Core.GameVersion; +using static PKHeX.Core.LanguageID; namespace PKHeX.Core; @@ -13,21 +15,21 @@ public override void Verify(LegalityAnalysis data) { var pk = data.Entity; var originalGeneration = data.Info.Generation; - var currentLanguage = pk.Language; - var maxLanguageID = Legal.GetMaxLanguageID(originalGeneration); + var currentLanguage = (LanguageID)pk.Language; + var maxLanguageID = (LanguageID)Legal.GetMaxLanguageID(originalGeneration); var enc = data.EncounterMatch; if (!IsValidLanguageID(currentLanguage, maxLanguageID, pk, enc)) { - data.AddLine(GetInvalid(string.Format(LOTLanguage, $"<={(LanguageID)maxLanguageID}", (LanguageID)currentLanguage))); + data.AddLine(GetInvalid(string.Format(LOTLanguage, $"<={maxLanguageID}", currentLanguage))); return; } // Korean Gen4 games can not trade with other Gen4 languages, but can use Pal Park with any Gen3 game/language. if (pk.Format == 4 && enc.Generation == 4 && !IsValidGen4Korean(currentLanguage) - && enc is not EncounterTrade4PID {Species: (int)Species.Pikachu or (int)Species.Magikarp} // ger magikarp / eng pikachu + && enc is not EncounterTrade4PID { IsLanguageSwap: true } // ger magikarp / eng pikachu ) { - bool kor = currentLanguage == (int)LanguageID.Korean; + bool kor = currentLanguage == Korean; var msgpkm = kor ? L_XKorean : L_XKoreanNon; var msgsav = kor ? L_XKoreanNon : L_XKorean; data.AddLine(GetInvalid(string.Format(LTransferOriginFInvalid0_1, msgpkm, msgsav))); @@ -37,24 +39,24 @@ public override void Verify(LegalityAnalysis data) if (originalGeneration <= 2) { // Korean Crystal does not exist, neither do Korean VC1 - if (pk is { Korean: true, Version: not (GameVersion.GD or GameVersion.SI) }) - data.AddLine(GetInvalid(string.Format(LOTLanguage, $"!={(LanguageID)currentLanguage}", (LanguageID)currentLanguage))); + if (pk is { Korean: true, Version: not (GD or SI) }) + data.AddLine(GetInvalid(string.Format(LOTLanguage, $"!={currentLanguage}", currentLanguage))); // Japanese VC is language locked; cannot obtain Japanese-Blue version as other languages. - if (pk is { Japanese: false, Version: GameVersion.BU }) - data.AddLine(GetInvalid(string.Format(LOTLanguage, nameof(LanguageID.Japanese), (LanguageID)currentLanguage))); + if (pk is { Japanese: false, Version: BU }) + data.AddLine(GetInvalid(string.Format(LOTLanguage, nameof(Japanese), currentLanguage))); } } - public static bool IsValidLanguageID(int currentLanguage, int maxLanguageID, PKM pk, IEncounterTemplate enc) + public static bool IsValidLanguageID(LanguageID currentLanguage, LanguageID maxLanguageID, PKM pk, IEncounterTemplate enc) { - if (currentLanguage == (int)LanguageID.UNUSED_6) + if (currentLanguage == UNUSED_6) return false; // Language ID 6 is unused. if (currentLanguage > maxLanguageID) return false; // Language not available (yet) - if (currentLanguage <= (int)LanguageID.None && !(enc is EncounterTrade5BW && EncounterTrade5BW.IsValidMissingLanguage(pk))) + if (currentLanguage == 0 && !(enc is EncounterTrade5BW && EncounterTrade5BW.IsValidMissingLanguage(pk))) return false; // Missing Language value is not obtainable return true; // Language is possible @@ -67,18 +69,18 @@ public static bool IsValidLanguageID(int currentLanguage, int maxLanguageID, PKM /// Korean Gen4 games can not trade with other Gen4 languages, but can use Pal Park with any Gen3 game/language. /// Anything with Gen4 origin cannot exist in the other language save file. /// - public static bool IsValidGen4Korean(int pkmLanguage) + public static bool IsValidGen4Korean(LanguageID pkmLanguage) { if (ParseSettings.ActiveTrainer is not SAV4 tr) return true; // ignore return IsValidGen4Korean(pkmLanguage, tr); } - /// - public static bool IsValidGen4Korean(int pkmLanguage, SAV4 tr) + /// + public static bool IsValidGen4Korean(LanguageID pkmLanguage, SAV4 tr) { - bool savKOR = tr.Language == (int)LanguageID.Korean; - bool pkmKOR = pkmLanguage == (int)LanguageID.Korean; + bool savKOR = (LanguageID)tr.Language == Korean; + bool pkmKOR = pkmLanguage == Korean; return savKOR == pkmKOR; } } diff --git a/PKHeX.Core/Legality/Verifiers/LegendsArceusVerifier.cs b/PKHeX.Core/Legality/Verifiers/LegendsArceusVerifier.cs index 780486772..0bc60aa40 100644 --- a/PKHeX.Core/Legality/Verifiers/LegendsArceusVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/LegendsArceusVerifier.cs @@ -59,7 +59,7 @@ private void CheckScalars(LegalityAnalysis data, PA8 pa) private static void CheckLearnset(LegalityAnalysis data, PA8 pa) { - var moveCount = GetMoveCount(pa); + var moveCount = pa.MoveCount; if (moveCount == 4) return; @@ -166,16 +166,6 @@ private static int AddMasteredMissing(PA8 pa, Span current, int ctr, Lea return ctr; } - private static int GetMoveCount(PA8 pa) - { - var count = 0; - if (pa.Move1 != 0) count++; - if (pa.Move2 != 0) count++; - if (pa.Move3 != 0) count++; - if (pa.Move4 != 0) count++; - return count; - } - private void CheckMastery(LegalityAnalysis data, PA8 pa) { var permit = pa.Permit; diff --git a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs index 421262be8..e41784174 100644 --- a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs @@ -37,7 +37,7 @@ public override void Verify(LegalityAnalysis data) var reqEXP = enc is EncounterStatic2 { DizzyPunchEgg: true } ? 125 // Gen2 Dizzy Punch gifts always have 125 EXP, even if it's more than the Lv5 exp required. - : Experience.GetEXP(enc.LevelMin, pk.PersonalInfo.EXPGrowth); + : Experience.GetEXP(enc.LevelMin, data.PersonalInfo.EXPGrowth); if (reqEXP != pk.EXP) data.AddLine(GetInvalid(LEggEXP)); return; @@ -46,14 +46,14 @@ public override void Verify(LegalityAnalysis data) var lvl = pk.CurrentLevel; if (lvl >= 100) { - var expect = Experience.GetEXP(100, pk.PersonalInfo.EXPGrowth); + var expect = Experience.GetEXP(100, data.PersonalInfo.EXPGrowth); if (pk.EXP != expect) data.AddLine(GetInvalid(LLevelEXPTooHigh)); } if (lvl < pk.MetLevel) data.AddLine(GetInvalid(LLevelMetBelow)); - else if (!enc.IsWithinEncounterRange(pk) && lvl != 100 && pk.EXP == Experience.GetEXP(lvl, pk.PersonalInfo.EXPGrowth)) + else if (!enc.IsWithinEncounterRange(pk) && lvl != 100 && pk.EXP == Experience.GetEXP(lvl, data.PersonalInfo.EXPGrowth)) data.AddLine(Get(LLevelEXPThreshold, Severity.Fishy)); else data.AddLine(GetValid(LLevelMetSane)); diff --git a/PKHeX.Core/Legality/Verifiers/SlotTypeVerifier.cs b/PKHeX.Core/Legality/Verifiers/SlotTypeVerifier.cs index 6939a99c9..4cf38ff0d 100644 --- a/PKHeX.Core/Legality/Verifiers/SlotTypeVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/SlotTypeVerifier.cs @@ -1,4 +1,6 @@ using static PKHeX.Core.LegalityCheckStrings; +using static PKHeX.Core.StorageSlotType; +using static PKHeX.Core.Species; namespace PKHeX.Core; @@ -27,12 +29,12 @@ public override void Verify(LegalityAnalysis data) public static bool IsSourceValid(PKM pk, StorageSlotType source) => source switch { - StorageSlotType.FusedKyurem => pk.Species is (int)Species.Reshiram or (int)Species.Zekrom, - StorageSlotType.FusedNecrozmaS => pk.Species is (int)Species.Solgaleo, - StorageSlotType.FusedNecrozmaM => pk.Species is (int)Species.Lunala, - StorageSlotType.FusedCalyrex => pk.Species is (int)Species.Glastrier or (int)Species.Spectrier, + FusedKyurem => pk.Species is (int)Reshiram or (int)Zekrom, + FusedNecrozmaS => pk.Species is (int)Solgaleo, + FusedNecrozmaM => pk.Species is (int)Lunala, + FusedCalyrex => pk.Species is (int)Glastrier or (int)Spectrier, - StorageSlotType.Ride => pk.Species is (int)Species.Koraidon or (int)Species.Miraidon + Ride => pk.Species is (int)Koraidon or (int)Miraidon && pk is PK9 {FormArgument: EncounterStatic9.RideLegendFormArg }, _ => true, }; @@ -40,9 +42,9 @@ public override void Verify(LegalityAnalysis data) public static bool IsSourceValidEgg(PKM pk, StorageSlotType source) => source switch { // Eggs should normally only be in Box or Party. - StorageSlotType.Box or StorageSlotType.Party => true, - StorageSlotType.Resort => true, // Poké Pelago can incubate eggs - StorageSlotType.Daycare when pk.Format == 2 => true, // ignore the "current egg" slot + Box or Party => true, + Resort => true, // Poké Pelago can incubate eggs + Daycare when pk.Format == 2 => true, // ignore the "current egg" slot _ => false, }; } diff --git a/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs b/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs index b6fde1171..bbeb031c2 100644 --- a/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs @@ -44,7 +44,7 @@ private void VerifyVCNatureEXP(LegalityAnalysis data) } if (met <= 2) // Not enough EXP to have every nature -- check for exclusions! { - var pi = pk.PersonalInfo; + var pi = data.PersonalInfo; var growth = pi.EXPGrowth; var nature = pk.Nature; bool valid = VerifyVCNature(growth, nature); diff --git a/PKHeX.Core/PKM/PKM.cs b/PKHeX.Core/PKM/PKM.cs index cb4e76e86..2b796314b 100644 --- a/PKHeX.Core/PKM/PKM.cs +++ b/PKHeX.Core/PKM/PKM.cs @@ -408,6 +408,7 @@ public void SetIVs(uint iv32) } /// + /// Returns the combined 30-bit representation commonly used as IV32. public uint GetIVs() { uint iv32 = 0; @@ -466,6 +467,12 @@ public ushort[] Moves set => SetMoves(value); } + /// + /// Tries to add a move to the moveset of the PKM. + /// + /// Move ID to add. + /// If the current moveset is full, whether to push out the oldest move (index 0) to add the new one. + /// public bool AddMove(ushort move, bool pushOut = true) { if (move == 0 || move >= MaxMoveID || HasMove(move)) @@ -483,6 +490,9 @@ public bool AddMove(ushort move, bool pushOut = true) return true; } + /// + /// Count of non-zero moves in the moveset. + /// public int MoveCount => Convert.ToInt32(Move1 != 0) + Convert.ToInt32(Move2 != 0) + Convert.ToInt32(Move3 != 0) + Convert.ToInt32(Move4 != 0); public void GetMoves(Span value) diff --git a/PKHeX.Core/Saves/Substructures/Gen3/CXD/ShadowInfoEntryXD.cs b/PKHeX.Core/Saves/Substructures/Gen3/CXD/ShadowInfoEntryXD.cs index 08e87563e..0e63d95b0 100644 --- a/PKHeX.Core/Saves/Substructures/Gen3/CXD/ShadowInfoEntryXD.cs +++ b/PKHeX.Core/Saves/Substructures/Gen3/CXD/ShadowInfoEntryXD.cs @@ -36,24 +36,22 @@ public uint EXP public override string ToString() => $"{(Species) Species} 0x{PID:X8} {Purification}"; } -public sealed class ShadowInfoEntry3J : ShadowInfoEntryXD +public sealed class ShadowInfoEntry3J(Memory data) : ShadowInfoEntryXD(data) { internal const int SIZE_ENTRY = 60; // -12 from U - public ShadowInfoEntry3J() : base(new byte[SIZE_ENTRY]) { } - public ShadowInfoEntry3J(byte[] data) : base(data) { } + public ShadowInfoEntry3J() : this(new byte[SIZE_ENTRY]) { } public override byte Index { get => Data[0x35]; set => Data[0x35] = value; } public override string ToString() => $"{(Species)Species} 0x{PID:X8} {Purification}"; } -public sealed class ShadowInfoEntry3U : ShadowInfoEntryXD +public sealed class ShadowInfoEntry3U(Memory data) : ShadowInfoEntryXD(data) { internal const int SIZE_ENTRY = 72; // -12 from U - public ShadowInfoEntry3U() : base(new byte[SIZE_ENTRY]) { } - public ShadowInfoEntry3U(byte[] data) : base(data) { } + public ShadowInfoEntry3U() : this(new byte[SIZE_ENTRY]) { } public override byte Index { get => Data[0x3F]; set => Data[0x3F] = value; }