diff --git a/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs b/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs index ca18a8a3b..0ccc41b88 100644 --- a/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs +++ b/PKHeX.Core/Legality/LearnSource/LearnEnvironment.cs @@ -6,8 +6,14 @@ namespace PKHeX.Core; /// /// Indicates the group of game(s) that the move was learned in. /// +/// +/// Each unique set of learnsets has a unique value. +/// public enum LearnEnvironment : byte { + /// + /// Sentinel value indicating no environment specified/initial environment. + /// None, /* Gen1 */ RB, YW, @@ -27,7 +33,14 @@ public enum LearnEnvironment : byte /// public static class LearnEnvironmentExtensions { + /// + /// Indicates whether the is specified (not ), and thus worth indicating. + /// public static bool IsSpecified(this LearnEnvironment value) => value is not None; + + /// + /// Gets the generation number (1-n) for the given . + /// public static byte GetGeneration(this LearnEnvironment value) => value switch { RB or YW => 1, @@ -42,6 +55,9 @@ public static class LearnEnvironmentExtensions _ => 0, }; + /// + /// Retrieves the evolution criteria for the given from the provided . + /// public static ReadOnlySpan GetEvolutions(this LearnEnvironment value, EvolutionHistory history) => value switch { RB or YW => history.Gen1, diff --git a/PKHeX.Core/Legality/LearnSource/MoveLearnInfo.cs b/PKHeX.Core/Legality/LearnSource/MoveLearnInfo.cs index e4644ee7f..9a0abf1cb 100644 --- a/PKHeX.Core/Legality/LearnSource/MoveLearnInfo.cs +++ b/PKHeX.Core/Legality/LearnSource/MoveLearnInfo.cs @@ -13,12 +13,18 @@ namespace PKHeX.Core; /// Conditions in which the move was learned public readonly record struct MoveLearnInfo(LearnMethod Method, LearnEnvironment Environment, byte Argument = 0) { + /// public void Summarize(StringBuilder sb) { var localized = GetLocalizedMethod(); Summarize(sb, localized); } + /// + /// Summarizes the move learn info into a human-readable format and appends it to the provided . + /// + /// The to append the summary to. + /// The localized string representing the learning method. private void Summarize(StringBuilder sb, ReadOnlySpan localizedMethod) { if (Environment.IsSpecified()) diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs b/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs index b2cacf712..8776303da 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/ClassicEraRNG.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel.DataAnnotations; namespace PKHeX.Core; @@ -271,21 +270,3 @@ public static uint SeekInitialSeedForIVs(ReadOnlySpan ivs, uint year, uint 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/Gen4/CuteCharm4.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/CuteCharm4.cs index 0f7680494..501989999 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/CuteCharm4.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/CuteCharm4.cs @@ -18,11 +18,16 @@ public static uint GetPIDMale(byte genderRatio, uint nature) { // pid >= ratio is male // get the lowest PID that will be male for Hardy (0) - var basePID = NatureCount * ((genderRatio / NatureCount) + 1); + var basePID = GetBufferedBasePID(genderRatio); // add the desired nature to the base PID return basePID + nature; } + /// + /// Gets the starting PID for a male gender by rounding up to the next multiple of (25). + /// + private static uint GetBufferedBasePID(byte genderRatio) => NatureCount * ((genderRatio / NatureCount) + 1); + /// /// Gets the PID of a Female wild encounter when the player leads a Male Pokémon with Cute Charm. /// @@ -39,6 +44,15 @@ private static byte GetGenderRatio(ushort species) : EntityGender.GetGenderRatio(species); // fallback (don't bother trying to devolve to Gen1-4 encounter species) } + /// + /// Checks if the PID is within the range created by the buffered PID algorithm. + /// + /// Obtained PID + /// True if the PID is within the Azurill male range. + /// + /// The game starts with a PID of 200, and then adds nature (0-24) to it, resulting in a range of 200-224. + /// 200 is via with the 3F:1M gender ratio of Azurill (191). + /// private static bool IsAzurillMale(uint pid) => pid is >= 0xC8 and <= 0xE0; /// diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/InitialSeedComponents4.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/InitialSeedComponents4.cs new file mode 100644 index 000000000..fdd2a757b --- /dev/null +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/InitialSeedComponents4.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace PKHeX.Core +{ + /// + /// 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/Verifiers/GenderVerifier.cs b/PKHeX.Core/Legality/Verifiers/GenderVerifier.cs index 4296bcb52..9241b82fb 100644 --- a/PKHeX.Core/Legality/Verifiers/GenderVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/GenderVerifier.cs @@ -12,8 +12,9 @@ public sealed class GenderVerifier : Verifier public override void Verify(LegalityAnalysis data) { var pk = data.Entity; - var pi = pk.PersonalInfo; - if (pi.Genderless != (pk.Gender == 2)) + var pi = data.PersonalInfo; + var gender = pk.Gender; + if (pi.Genderless != (gender == 2)) { // D/P/Pt & HG/SS Shedinja glitch -- only generation 4 spawns bool ignore = pk is { Format: 4, Species: (int)Species.Shedinja } && pk.MetLevel != pk.CurrentLevel; @@ -36,7 +37,7 @@ public override void Verify(LegalityAnalysis data) } // Check fixed gender cases - if ((pi.OnlyFemale && pk.Gender != 1) || (pi.OnlyMale && pk.Gender != 0)) + if ((pi.OnlyFemale && gender != 1) || (pi.OnlyMale && gender != 0)) data.AddLine(GetInvalid(LGenderInvalidNone)); } @@ -65,22 +66,22 @@ private static bool IsValidGenderPID(LegalityAnalysis data) return true; } - private static bool IsValidFixedGenderFromBiGender(PKM pk, ushort original) + private static bool IsValidFixedGenderFromBiGender(PKM pk, ushort originalSpecies) { var current = pk.Gender; if (current == 2) // shedinja, genderless return true; - var gender = EntityGender.GetFromPID(original, pk.EncryptionConstant); + var gender = EntityGender.GetFromPID(originalSpecies, pk.EncryptionConstant); return gender == current; } private static bool IsValidGenderMismatch(PKM pk) => pk.Species switch { // Shedinja evolution gender glitch, should match original Gender - (int) Species.Shedinja when pk.Format == 4 => pk.Gender == EntityGender.GetFromPIDAndRatio(pk.EncryptionConstant, 0x7F), // 50M-50F + (int) Species.Shedinja when pk.Format == 4 => pk.Gender == EntityGender.GetFromPIDAndRatio(pk.EncryptionConstant, EntityGender.HH), // 1:1 // Evolved from Azurill after transferring to keep gender - (int) Species.Marill or (int) Species.Azumarill when pk.Format >= 6 => pk.Gender == 1 && (pk.EncryptionConstant & 0xFF) > 0x3F, + (int) Species.Marill or (int) Species.Azumarill when pk.Format >= 6 => pk.Gender == 1 && (pk.EncryptionConstant & 0xFF) > EntityGender.MM, // 3:1 _ => false, }; diff --git a/PKHeX.Core/Legality/Verifiers/GroundTileVerifier.cs b/PKHeX.Core/Legality/Verifiers/GroundTileVerifier.cs index 4221efdff..0461d54e1 100644 --- a/PKHeX.Core/Legality/Verifiers/GroundTileVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/GroundTileVerifier.cs @@ -11,6 +11,9 @@ public sealed class GroundTileVerifier : Verifier public override void Verify(LegalityAnalysis data) { + // Only specific encounters in Generation 4 set a GroundTile value. + // This value is retained after transferring to Gen5, but not beyond. + // Gen3 and Gen5 encounters should never have a Tile value. if (data.Entity is not IGroundTile e) return; var enc = data.EncounterMatch; @@ -27,7 +30,12 @@ public override void Verify(LegalityAnalysis data) /// True if stored ground tile value is permitted. public static bool IsGroundTileValid(IEncounterTemplate enc, IGroundTile e) { - var type = enc is IGroundTypeTile t ? t.GroundTile : GroundTileAllowed.None; - return type.Contains(e.GroundTile); + if (enc is not IGroundTypeTile t) + return e.GroundTile is GroundTileType.None; + + var allow = t.GroundTile; + if (allow is GroundTileAllowed.None) + return e.GroundTile is GroundTileType.None; + return allow.Contains(e.GroundTile); } } diff --git a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs index 0f21f1bf3..898a6c336 100644 --- a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs @@ -21,26 +21,34 @@ public override void Verify(LegalityAnalysis data) { VerifyMiscEggCommon(data); + // No egg have contest stats from the encounter. if (pk is IContestStatsReadOnly s && s.HasContestStats()) data.AddLine(GetInvalid(LEggContest, Egg)); + // Cannot transfer eggs across contexts (must be hatched). + var e = data.EncounterOriginal; + if (e.Context != pk.Context) + data.AddLine(GetInvalid(LTransferEggVersion, Egg)); + switch (pk) { - case SK2 or CK3 or XK3 or BK4 or RK4 or PA8: // Side Game: No Eggs + // Side Game: No Eggs + case SK2 or CK3 or XK3 or BK4 or RK4 when e.Context == pk.Context: data.AddLine(GetInvalid(LTransferEggVersion, Egg)); break; - case PK5 pk5 when pk5.PokeStarFame != 0: - data.AddLine(GetInvalid(LEggShinyPokeStar, Egg)); + + // All Eggs are Japanese and flagged specially for localized string + case PK3 when pk.Language != 1: + data.AddLine(GetInvalid(string.Format(LOTLanguage, LanguageID.Japanese, (LanguageID)pk.Language), Egg)); break; + + // Cannot obtain Shiny Leaf or Pokeathlon Stats as Egg case PK4 pk4: if (pk4.ShinyLeaf != 0) data.AddLine(GetInvalid(LEggShinyLeaf, Egg)); if (pk4.PokeathlonStat != 0) data.AddLine(GetInvalid(LEggPokeathlon, Egg)); break; - case PK3 when pk.Language != 1: // All Eggs are Japanese and flagged specially for localized string - data.AddLine(GetInvalid(string.Format(LOTLanguage, LanguageID.Japanese, (LanguageID)pk.Language), Egg)); - break; } if (pk is IHomeTrack { HasTracker: true }) @@ -59,8 +67,8 @@ public override void Verify(LegalityAnalysis data) case PK9 pk9: VerifyStats9(data, pk9); break; } - if (pk.Format >= 6) - VerifyFullness(data, pk); + if (pk is IFullnessEnjoyment fe) // 6-8 + VerifyFullness(data, pk, fe); var enc = data.EncounterMatch; if (enc is IEncounterServerDate { IsDateRestricted: true } encounterDate) @@ -209,6 +217,12 @@ private void VerifyIsMovesetAllowed(LegalityAnalysis data, SK2 sk2) private static void VerifyStats5(LegalityAnalysis data, PK5 pk5) { var enc = data.EncounterMatch; + + // Cannot participate in Pokestar Studios as Egg + if (pk5.IsEgg && pk5.PokeStarFame != 0) + data.AddLine(GetInvalid(LEggShinyPokeStar, Egg)); + + // Ensure NSparkle is only present on N's encounters. if (enc is EncounterStatic5N) { if (!pk5.NSparkle) @@ -351,7 +365,7 @@ private static bool IsObedienceLevelValid(PKM pk, byte current, int expectObey) private void VerifyMiscPokerus(LegalityAnalysis data) { var pk = data.Entity; - if (pk.Format == 1) + if (pk.Format == 1) // not stored in Gen1 format return; var strain = pk.PokerusStrain; @@ -622,10 +636,8 @@ public void VerifyVersionEvolution(LegalityAnalysis data) } } - private static void VerifyFullness(LegalityAnalysis data, PKM pk) + private static void VerifyFullness(LegalityAnalysis data, PKM pk, IFullnessEnjoyment fe) { - if (pk is not IFullnessEnjoyment fe) - return; if (pk.IsEgg) { if (fe.Fullness != 0)