From 74ef6d7378a0f936d7e096c32b38c87f45bf41f4 Mon Sep 17 00:00:00 2001 From: abcboy101 <16735361+abcboy101@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:13:39 -0500 Subject: [PATCH] Validate Odd Egg OT when still an Egg (#4693) --- .../Templates/Gen2/EncounterStatic2.cs | 98 +++++++++++++++++-- .../Legality/Verifiers/LevelVerifier.cs | 2 +- .../Legality/Verifiers/TransferVerifier.cs | 2 +- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterStatic2.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterStatic2.cs index 92b582a29..cb7ab7315 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterStatic2.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen2/EncounterStatic2.cs @@ -1,3 +1,4 @@ +using System; using static PKHeX.Core.Species; using static PKHeX.Core.GameVersion; @@ -12,8 +13,8 @@ public sealed record EncounterStatic2(ushort Species, byte Level, GameVersion Ve public byte Generation => 2; public EntityContext Context => EntityContext.Gen2; public byte Form => 0; - public byte EggCycles => DizzyPunchEgg ? (byte)20 : (byte)0; - public bool DizzyPunchEgg => IsEgg && Moves.HasMoves; + public byte EggCycles => IsDizzyPunchEgg ? (byte)20 : (byte)0; + public bool IsDizzyPunchEgg => IsEgg && Moves.HasMoves; public Ball FixedBall => Ball.Poke; ushort ILocation.Location => Location; @@ -34,6 +35,9 @@ public sealed record EncounterStatic2(ushort Species, byte Level, GameVersion Ve public byte LevelMin => Level; public byte LevelMax => Level; + private const byte OddEggEXP = 125; + private const byte UnhatchedEggOTGender = 0; + #region Generating PKM IEncounterConvertible.ConvertToPKM(ITrainerInfo tr) => ConvertToPKM(tr); @@ -51,7 +55,6 @@ public PK2 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) CurrentLevel = LevelMin, TID16 = tr.TID16, - OriginalTrainerName = tr.OT, OriginalTrainerFriendship = pi.BaseFriendship, }; @@ -63,16 +66,39 @@ public PK2 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) if (IsEgg) { - if (DizzyPunchEgg) // Fixed EXP value instead of exactly Level 5 - pk.EXP = 125; + // Gender and location not set for regular eggs + if (IsDizzyPunchEgg) // Odd Egg: Fixed EXP value instead of exactly Level 5 + { + pk.EXP = OddEggEXP; + if (pk.IsEgg) + { + pk.OriginalTrainerName = GetOddEggTrainerName((LanguageID)language); + pk.OriginalTrainerFriendship = EggCycles; + } + else + { + pk.OriginalTrainerName = tr.OT; + } + } + else + { + pk.OriginalTrainerName = tr.OT; + if (pk.IsEgg) + pk.OriginalTrainerFriendship = EggCycles; + } } else if (Version == C || (Version == GSC && tr.Version == C)) { + pk.OriginalTrainerName = tr.OT; pk.OriginalTrainerGender = tr.Gender; pk.MetLevel = LevelMin; pk.MetLocation = Location; pk.MetTimeOfDay = GetRandomTime(); } + else + { + pk.OriginalTrainerName = tr.OT; + } if (Moves.HasMoves) pk.SetMoves(Moves); @@ -93,7 +119,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo) { if (Shiny == Shiny.Always && !pk.IsShiny) return false; - if (IsEgg && Moves.HasMoves) // Odd Egg + if (IsDizzyPunchEgg) // Odd Egg { if (pk.Format > 2) return false; // Can't be transferred to Gen7+ @@ -103,12 +129,19 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo) // EXP is a fixed starting value for eggs if (pk.IsEgg) { - if (pk.EXP != 125) + if (pk.EXP != OddEggEXP) + return false; + + // Check OT Details + if (pk.OriginalTrainerGender != UnhatchedEggOTGender) + return false; + if (!IsOddEggTrainerNameValid(pk)) return false; } else { - if (pk.EXP < 125) + // Once hatched, EXP can vary. Must be at least the starting value. + if (pk.EXP < OddEggEXP) return false; } } @@ -141,6 +174,55 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo) return true; } + private static LanguageID DetectOddEggLanguage(PKM pk) + { + // Only called when in Gen2 format, because they can only be transferred after hatching. + var span = pk.OriginalTrainerTrash; + Span name = stackalloc char[span.Length]; + var len = pk.LoadString(span, name); + name = name[..len]; + return DetectOddEggLanguage(name, pk.Japanese); + } + + private static string GetOddEggTrainerName(LanguageID language) => language switch + { + // Japanese language ID can be "なぞナゾ" or "なぞ", but we use the longer form here. + LanguageID.Japanese => "なぞナゾ", + + // Specific fixed names for other languages. + LanguageID.English => "ODD", + LanguageID.French => "BIZAR", + LanguageID.Italian => "Strano", + LanguageID.German => "Kurios", + LanguageID.Spanish => "Raro", + _ => throw new ArgumentOutOfRangeException(nameof(language), language, null), + }; + + private static LanguageID DetectOddEggLanguage(Span name, bool japanese) + { + // Japanese egg names can only be "なぞナゾ" or "なぞ" + // For the Japanese OT, it's initially set to なぞナゾ for the initial "post-trade" automatic save, + // but the last two characters are then removed so that any subsequent saves have なぞ instead. + // https://github.com/gb-mobile/pokecrystal-mobile-eng/blob/5ab6cd0617c4597400aeb963220747c8c778b1d6/mobile/mobile_45_stadium.asm#L133 + // Thus, both forms are valid for Japanese. + if (japanese) + return name is "なぞナゾ" or "なぞ" ? LanguageID.Japanese : LanguageID.None; + + // Other languages have fixed OT names, but can trade with different language games. + // Thus, we only check for the known valid names. + return name switch + { + "ODD" => LanguageID.English, + "BIZAR" => LanguageID.French, + "Strano" => LanguageID.Italian, + "Kurios" => LanguageID.German, + "Raro" => LanguageID.Spanish, + _ => LanguageID.None, + }; + } + + private static bool IsOddEggTrainerNameValid(PKM pk) => DetectOddEggLanguage(pk) != LanguageID.None; + private bool IsMatchEggLocation(PKM pk) { if (pk is not ICaughtData2 c2) diff --git a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs index b3f38bdd9..36c65c6ba 100644 --- a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs @@ -35,7 +35,7 @@ public override void Verify(LegalityAnalysis data) return; } - var reqEXP = enc is EncounterStatic2 { DizzyPunchEgg: true } + var reqEXP = enc is EncounterStatic2 { IsDizzyPunchEgg: true } ? 125 // Gen2 Dizzy Punch gifts always have 125 EXP, even if it's more than the Lv5 exp required. : Experience.GetEXP(enc.LevelMin, data.PersonalInfo.EXPGrowth); if (reqEXP != pk.EXP) diff --git a/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs b/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs index d325e22e0..45a31778b 100644 --- a/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs @@ -199,7 +199,7 @@ public void VerifyVCEncounter(PKM pk, IEncounterTemplate original, EncounterTran data.AddLine(GetInvalid(EggLocationNone)); // Flag Moves that cannot be transferred - if (original is EncounterStatic2 { DizzyPunchEgg: true}) // Dizzy Punch Gifts + if (original is EncounterStatic2 { IsDizzyPunchEgg: true }) // Dizzy Punch Gifts FlagIncompatibleTransferMove(pk, data.Info.Moves, 146, 2); // can't have Dizzy Punch at all bool checkShiny = pk.VC2 || original.Generation == 2 || MoveInfo.IsAnyFromGeneration(2, data.Info.Moves);