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)