Misc tweaks

no functional change
This commit is contained in:
Kurt 2025-06-18 16:23:31 -05:00
parent ec4cfc807f
commit fd72b6ea46
8 changed files with 101 additions and 41 deletions

View File

@ -6,8 +6,14 @@ namespace PKHeX.Core;
/// <summary>
/// Indicates the group of game(s) that the move was learned in.
/// </summary>
/// <remarks>
/// Each unique set of learnsets has a unique <see cref="LearnEnvironment"/> value.
/// </remarks>
public enum LearnEnvironment : byte
{
/// <summary>
/// Sentinel value indicating no environment specified/initial environment.
/// </summary>
None,
/* Gen1 */ RB, YW,
@ -27,7 +33,14 @@ public enum LearnEnvironment : byte
/// </summary>
public static class LearnEnvironmentExtensions
{
/// <summary>
/// Indicates whether the <see cref="LearnEnvironment"/> is specified (not <see cref="None"/>), and thus worth indicating.
/// </summary>
public static bool IsSpecified(this LearnEnvironment value) => value is not None;
/// <summary>
/// Gets the generation number (1-n) for the given <see cref="LearnEnvironment"/>.
/// </summary>
public static byte GetGeneration(this LearnEnvironment value) => value switch
{
RB or YW => 1,
@ -42,6 +55,9 @@ public static class LearnEnvironmentExtensions
_ => 0,
};
/// <summary>
/// Retrieves the evolution criteria for the given <see cref="LearnEnvironment"/> from the provided <see cref="EvolutionHistory"/>.
/// </summary>
public static ReadOnlySpan<EvoCriteria> GetEvolutions(this LearnEnvironment value, EvolutionHistory history) => value switch
{
RB or YW => history.Gen1,

View File

@ -13,12 +13,18 @@ namespace PKHeX.Core;
/// <param name="Argument">Conditions in which the move was learned</param>
public readonly record struct MoveLearnInfo(LearnMethod Method, LearnEnvironment Environment, byte Argument = 0)
{
/// <inheritdoc cref="Summarize(StringBuilder, ReadOnlySpan{char})"/>
public void Summarize(StringBuilder sb)
{
var localized = GetLocalizedMethod();
Summarize(sb, localized);
}
/// <summary>
/// Summarizes the move learn info into a human-readable format and appends it to the provided <see cref="StringBuilder"/>.
/// </summary>
/// <param name="sb">The <see cref="StringBuilder"/> to append the summary to.</param>
/// <param name="localizedMethod">The localized string representing the learning method.</param>
private void Summarize(StringBuilder sb, ReadOnlySpan<char> localizedMethod)
{
if (Environment.IsSpecified())

View File

@ -1,5 +1,4 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace PKHeX.Core;
@ -271,21 +270,3 @@ public static uint SeekInitialSeedForIVs(ReadOnlySpan<int> ivs, uint year, uint
return best;
}
}
/// <summary>
/// Stores the components of an initial seed from Generation 4.
/// </summary>
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;
}

View File

@ -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;
}
/// <summary>
/// Gets the starting PID for a male gender by rounding up to the next multiple of <see cref="NatureCount"/> (25).
/// </summary>
private static uint GetBufferedBasePID(byte genderRatio) => NatureCount * ((genderRatio / NatureCount) + 1);
/// <summary>
/// Gets the PID of a Female wild encounter when the player leads a Male Pokémon with Cute Charm.
/// </summary>
@ -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)
}
/// <summary>
/// Checks if the PID is within the range created by the buffered PID algorithm.
/// </summary>
/// <param name="pid">Obtained PID</param>
/// <returns>True if the PID is within the Azurill male range.</returns>
/// <remarks>
/// 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 <see cref="GetBufferedBasePID"/> with the 3F:1M gender ratio of Azurill (191).
/// </remarks>
private static bool IsAzurillMale(uint pid) => pid is >= 0xC8 and <= 0xE0;
/// <summary>

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace PKHeX.Core
{
/// <summary>
/// Stores the components of an initial seed from Generation 4.
/// </summary>
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;
}
}

View File

@ -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,
};

View File

@ -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)
/// <returns>True if stored ground tile value is permitted.</returns>
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);
}
}

View File

@ -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)