Misc tweaks

Add date validation for lgpe go park encounters (deferred, now finally remembered to implement?)
This commit is contained in:
Kurt 2025-04-21 00:57:23 -05:00
parent 154c370901
commit 87d2f10c7f
29 changed files with 103 additions and 43 deletions

View File

@ -27,9 +27,9 @@ public static PersonalColor GetColor(IEncounterTemplate enc)
public static ReadOnlySpan<Ball> GetPreferredByColor(IEncounterTemplate enc) => GetPreferredByColor(enc, GetColor(enc));
public static ReadOnlySpan<Ball> GetPreferredByColor<T>(T enc, PersonalColor color) where T : IVersion
public static ReadOnlySpan<Ball> GetPreferredByColor<T>(T enc, PersonalColor color) where T : IContext
{
if (enc.Version is GameVersion.PLA)
if (enc.Context is EntityContext.Gen8a)
return GetPreferredByColorLA(color);
return GetPreferredByColor(color);
}

View File

@ -1,7 +1,7 @@
namespace PKHeX.Core;
/// <summary>
/// <see cref="GameVersion.BATREV"/> Game Language IDs
/// <see cref="SAV4BR"/> Game Language IDs
/// </summary>
public enum LanguageBR : byte
{

View File

@ -24,6 +24,7 @@ public static class EncounterServerDate
WA8 wa8 => Result(wa8.IsWithinDistributionWindow(obtained)),
WB8 wb8 => Result(wb8.IsWithinDistributionWindow(obtained)),
WC9 wc9 => Result(wc9.IsWithinDistributionWindow(obtained)),
EncounterSlot7GO g7 => Result(g7.IsWithinDistributionWindow(obtained)),
EncounterSlot8GO g8 => Result(g8.IsWithinDistributionWindow(obtained)),
_ => throw new ArgumentOutOfRangeException(nameof(enc)),
};

View File

@ -7,8 +7,9 @@ namespace PKHeX.Core;
/// <inheritdoc cref="PogoSlotExtensions" />
/// </summary>
public sealed record EncounterSlot7GO(int StartDate, int EndDate, ushort Species, byte Form, byte LevelMin, byte LevelMax, Shiny Shiny, Gender Gender, PogoType Type)
: IEncounterable, IEncounterMatch, IPogoSlot, IEncounterConvertible<PB7>
: IEncounterable, IEncounterMatch, IPogoSlot, IEncounterConvertible<PB7>, IEncounterServerDate
{
public bool IsDateRestricted => true;
public byte Generation => 7;
public EntityContext Context => EntityContext.Gen7b;
public Ball FixedBall => Ball.None; // GO Park can override the ball; obey capture rules for LGP/E
@ -137,6 +138,18 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return true;
}
public bool IsWithinDistributionWindow(PKM pk)
{
var date = new DateOnly(pk.MetYear + 2000, pk.MetMonth, pk.MetDay);
return IsWithinDistributionWindow(date);
}
public bool IsWithinDistributionWindow(DateOnly date)
{
var stamp = PogoDateRangeExtensions.GetTimeStamp(date.Year, date.Month, date.Day);
return this.IsWithinStartEnd(stamp);
}
public EncounterMatchRating GetMatchRating(PKM pk)
{
var stamp = PogoDateRangeExtensions.GetTimeStamp(pk.MetYear + 2000, pk.MetMonth, pk.MetDay);

View File

@ -3,13 +3,8 @@ namespace PKHeX.Core;
/// <summary>
/// Represents all details that an entity may be encountered with.
/// </summary>
public interface IEncounterTemplate : ISpeciesForm, IVersion, IGeneration, IShiny, ILevelRange, ILocation, IFixedAbilityNumber, IFixedBall, IShinyPotential
public interface IEncounterTemplate : ISpeciesForm, IVersion, IGeneration, IShiny, ILevelRange, ILocation, IFixedAbilityNumber, IFixedBall, IShinyPotential, IContext
{
/// <summary>
/// Original Context
/// </summary>
EntityContext Context { get; }
/// <summary>
/// Indicates if the encounter originated as an egg.
/// </summary>

View File

@ -79,18 +79,18 @@ public override void Verify(LegalityAnalysis data)
VerifyFullness(data, pk);
var enc = data.EncounterMatch;
if (enc is IEncounterServerDate { IsDateRestricted: true } serverGift)
if (enc is IEncounterServerDate { IsDateRestricted: true } encounterDate)
{
var date = new DateOnly(pk.MetYear + 2000, pk.MetMonth, pk.MetDay);
var actualDay = new DateOnly(pk.MetYear + 2000, pk.MetMonth, pk.MetDay);
// HOME Gifts for Sinnoh/Hisui starters were forced JPN until May 20, 2022 (UTC).
if (enc is WB8 { IsDateLockJapanese: true } or WA8 { IsDateLockJapanese: true })
{
if (date < new DateOnly(2022, 5, 20) && pk.Language != (int)LanguageID.Japanese)
if (actualDay < new DateOnly(2022, 5, 20) && pk.Language != (int)LanguageID.Japanese)
data.AddLine(GetInvalid(LDateOutsideDistributionWindow));
}
var result = serverGift.IsWithinDistributionWindow(date);
var result = encounterDate.IsWithinDistributionWindow(actualDay);
if (result == EncounterServerDateCheck.Invalid)
data.AddLine(GetInvalid(LDateOutsideDistributionWindow));
}

View File

@ -106,11 +106,11 @@ public static bool IsRibbonValidMasterRank(PKM pk, IEncounterTemplate enc, Evolu
private static bool IsRibbonValidMasterRankSWSH(PKM pk, IEncounterTemplate enc)
{
// Transfers from prior games, as well as from GO, require the battle-ready symbol in order to participate in Ranked.
if ((enc.Generation < 8 || enc.Version == GameVersion.GO) && pk is IBattleVersion { BattleVersion: 0 })
if ((enc.Generation < 8 || enc.Context is EntityContext.Gen7b) && pk is IBattleVersion { BattleVersion: 0 })
return false;
// GO transfers: Capture date is global time, and not console changeable.
bool hasRealDate = enc.Version == GameVersion.GO || enc is IEncounterServerDate { IsDateRestricted: true };
// GO transfers and server gifts: Capture date is global time, and not console changeable.
bool hasRealDate = enc is IEncounterServerDate { IsDateRestricted: true };
if (hasRealDate)
{
// Ranked is still ongoing, but the use of Mythicals was restricted to Series 13 only.

View File

@ -122,7 +122,7 @@ public override string CardTitle
get
{
// Check to see if date is valid
if (!DateUtil.IsDateValid(Year, Month, Day))
if (!DateUtil.IsValidDate(Year, Month, Day))
return null;
return new DateOnly(Year, Month, Day);

View File

@ -83,7 +83,7 @@ private uint Day
get
{
// Check to see if date is valid
if (!DateUtil.IsDateValid(Year, Month, Day))
if (!DateUtil.IsValidDate(Year, Month, Day))
return null;
return new DateOnly((int)Year, (int)Month, (int)Day);

View File

@ -80,7 +80,7 @@ private uint Day
get
{
// Check to see if date is valid
if (!DateUtil.IsDateValid(Year, Month, Day))
if (!DateUtil.IsValidDate(Year, Month, Day))
return null;
return new DateOnly((int)Year, (int)Month, (int)Day);

View File

@ -80,7 +80,7 @@ private uint Day
get
{
// Check to see if date is valid
if (!DateUtil.IsDateValid(Year, Month, Day))
if (!DateUtil.IsValidDate(Year, Month, Day))
return null;
return new DateOnly((int)Year, (int)Month, (int)Day);

View File

@ -0,0 +1,9 @@
namespace PKHeX.Core;
public interface IContext
{
/// <summary>
/// The Context the data originated in.
/// </summary>
EntityContext Context { get; }
}

View File

@ -342,7 +342,7 @@ public override string OriginalTrainerName
{
get
{
if (!DateUtil.IsDateValid(2000 + ReceivedYear, ReceivedMonth, ReceivedDay))
if (!DateUtil.IsValidDate(2000 + ReceivedYear, ReceivedMonth, ReceivedDay))
return null;
return new DateOnly(ReceivedYear + 2000, ReceivedMonth, ReceivedDay);
}
@ -370,7 +370,7 @@ public override string OriginalTrainerName
{
get
{
if (!DateUtil.IsTimeValid(ReceivedHour, ReceivedMinute, ReceivedSecond))
if (!DateUtil.IsValidTime(ReceivedHour, ReceivedMinute, ReceivedSecond))
return null;
return new TimeOnly(ReceivedHour, ReceivedMinute, ReceivedSecond);
}

View File

@ -164,7 +164,7 @@ private byte[] Write()
get
{
// Check to see if date is valid
if (!DateUtil.IsDateValid(2000 + MetYear, MetMonth, MetDay))
if (!DateUtil.IsValidDate(2000 + MetYear, MetMonth, MetDay))
return null;
return new DateOnly(2000 + MetYear, MetMonth, MetDay);
}
@ -207,7 +207,7 @@ private byte[] Write()
get
{
// Check to see if date is valid
if (!DateUtil.IsDateValid(2000 + EggYear, EggMonth, EggDay))
if (!DateUtil.IsValidDate(2000 + EggYear, EggMonth, EggDay))
return null;
return new DateOnly(2000 + EggYear, EggMonth, EggDay);
}

View File

@ -4,6 +4,9 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for detecting a <see cref="PKM"/> entity from a byte array or length.
/// </summary>
public static class EntityDetection
{
/// <summary>

View File

@ -3,6 +3,9 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for interacting with Entity file extensions.
/// </summary>
public static class EntityFileExtension
{
// All side-game formats that don't follow the usual pk* format

View File

@ -4,6 +4,9 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for creating file names for <see cref="PKM"/> data.
/// </summary>
public static class EntityFileNamer
{
/// <summary>

View File

@ -5,6 +5,9 @@
namespace PKHeX.Core;
/// <summary>
/// Utility class for detecting the type format of a Pokémon entity.
/// </summary>
public static class EntityFormat
{
/// <summary>
@ -210,7 +213,7 @@ private static EntityFormatDetected IsFormatReally8b(PK8 pk)
public enum EntityFormatDetected
{
None = -1,
None,
FormatPK1,
FormatPK2, FormatSK2,

View File

@ -2,6 +2,9 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for creating a PID, mostly for originating in Generations 3-5.
/// </summary>
public static class EntityPID
{
/// <summary>
@ -25,7 +28,7 @@ public static uint GetRandomPID(Random rnd, ushort species, byte gender, GameVer
// Below logic handles Gen3-5.
// No need to get form specific entry, as Gen3-5 do not have that feature.
var gt = PersonalTable.B2W2[species].Gender;
bool g34 = origin <= GameVersion.CXD;
bool g34 = origin.IsGen3();
uint abilBitVal = g34 ? oldPID & 0x0000_0001 : oldPID & 0x0001_0000;
bool g3unown = origin is GameVersion.FR or GameVersion.LG && species == (int)Species.Unown;

View File

@ -36,6 +36,8 @@ public static class RecentTrainerCache
public static void SetRecentTrainer(ITrainerInfo trainer)
{
Trainer = trainer;
// Update Gen6/7 trainer reference if applicable, otherwise retain whatever was there.
if (trainer is IRegionOriginReadOnly g67)
Trainer67 = g67;
}

View File

@ -3,7 +3,7 @@ namespace PKHeX.Core;
/// <summary>
/// Simple record containing trainer data
/// </summary>
public sealed record SimpleTrainerInfo : ITrainerInfo, IRegionOriginReadOnly
public sealed record SimpleTrainerInfo : ITrainerInfo, IRegionOriginReadOnly, ITrainerID
{
public string OT { get; init; } = TrainerName.ProgramINT;
public ushort TID16 { get; init; } = 12345;

View File

@ -165,7 +165,8 @@ private static bool IsChecksumValid(Span<byte> sav, int offset)
public override int Language
{
get => (int)(BRLanguage == LanguageBR.JapaneseOrEnglish && Japanese ? LanguageID.Japanese : BRLanguage.ToLanguageID());
set {
set
{
Japanese = value == (int)LanguageID.Japanese;
BRLanguage = ((LanguageID)value).ToLanguageBR();
}

View File

@ -137,7 +137,7 @@ public void SetTeam(IReadOnlyList<PK6> team, int t)
{
get
{
if (!DateUtil.IsDateValid(MatchYear, MatchMonth, MatchDay))
if (!DateUtil.IsValidDate(MatchYear, MatchMonth, MatchDay))
return null;
return new DateTime(MatchYear, MatchMonth, MatchDay, MatchHour, MatchMinute, MatchSecond);
}
@ -163,7 +163,7 @@ public void SetTeam(IReadOnlyList<PK6> team, int t)
{
get
{
if (!DateUtil.IsDateValid(UploadYear, UploadMonth, UploadDay))
if (!DateUtil.IsValidDate(UploadYear, UploadMonth, UploadDay))
return null;
return new DateTime(UploadYear, UploadMonth, UploadDay, UploadHour, UploadMinute, UploadSecond);
}

View File

@ -95,7 +95,7 @@ public void SetPlayerNames(IReadOnlyList<string> value)
{
get
{
if (!DateUtil.IsDateValid(MatchYear, MatchMonth, MatchDay))
if (!DateUtil.IsValidDate(MatchYear, MatchMonth, MatchDay))
return null;
return new DateTime(MatchYear, MatchMonth, MatchDay, MatchHour, MatchMinute, MatchSecond);
}

View File

@ -44,7 +44,7 @@ public abstract class PlayTimeLastSaved<TSave, TEpoch>(TSave sav, Memory<byte> r
public DateTime? LastSavedDate
{
get => !DateUtil.IsDateValid(LastSaved.Year, LastSaved.Month, LastSaved.Day)
get => !DateUtil.IsValidDate(LastSaved.Year, LastSaved.Month, LastSaved.Day)
? null
: LastSaved.Timestamp;
set

View File

@ -73,7 +73,7 @@ public void SetFestaPhraseUnlocked(int index, bool value)
public DateTime? FestaDate
{
get => FestaYear >= 0 && FestaMonth > 0 && FestaDay > 0 && FestaHour >= 0 && FestaMinute >= 0 && FestaSecond >= 0 && DateUtil.IsDateValid(FestaYear, FestaMonth, FestaDay)
get => FestaYear >= 0 && FestaMonth > 0 && FestaDay > 0 && FestaHour >= 0 && FestaMinute >= 0 && FestaSecond >= 0 && DateUtil.IsValidDate(FestaYear, FestaMonth, FestaDay)
? new DateTime(FestaYear, FestaMonth, FestaDay, FestaHour, FestaMinute, FestaSecond)
: null;
set

View File

@ -11,7 +11,7 @@ public static class DateUtil
/// <param name="month">The month of the date of which to check the validity.</param>
/// <param name="day">The day of the date of which to check the validity.</param>
/// <returns>A boolean indicating if the date is valid.</returns>
public static bool IsDateValid(int year, int month, int day)
public static bool IsValidDate(int year, int month, int day)
{
if (year is <= 0 or > 9999)
return false;
@ -30,14 +30,20 @@ public static bool IsDateValid(int year, int month, int day)
/// <param name="month">The month of the date of which to check the validity.</param>
/// <param name="day">The day of the date of which to check the validity.</param>
/// <returns>A boolean indicating if the date is valid.</returns>
public static bool IsDateValid(uint year, uint month, uint day)
public static bool IsValidDate(uint year, uint month, uint day)
{
return year < int.MaxValue && month < int.MaxValue && day < int.MaxValue && IsDateValid((int)year, (int)month, (int)day);
return year < int.MaxValue && month < int.MaxValue && day < int.MaxValue && IsValidDate((int)year, (int)month, (int)day);
}
private static readonly DateTime Epoch2000 = new(2000, 1, 1);
private const int SecondsPerDay = 60*60*24; // 86400
/// <summary>
/// Combines the date and time components into a seconds-elapsed value, relative to the epoch 2000.
/// </summary>
/// <param name="date">Date component</param>
/// <param name="time">Time component</param>
/// <returns>Seconds elapsed since epoch 2000</returns>
public static int GetSecondsFrom2000(DateTime date, DateTime time)
{
int seconds = (int)(date - Epoch2000).TotalSeconds;
@ -46,12 +52,24 @@ public static int GetSecondsFrom2000(DateTime date, DateTime time)
return seconds;
}
/// <summary>
/// Converts a seconds-elapsed value to the Date and Time components, relative to the epoch 2000.
/// </summary>
/// <param name="seconds">Seconds elapsed since epoch 2000</param>
/// <param name="date">Date component</param>
/// <param name="time">Time component</param>
public static void GetDateTime2000(uint seconds, out DateTime date, out DateTime time)
{
date = Epoch2000.AddSeconds(seconds);
time = Epoch2000.AddSeconds(seconds % SecondsPerDay);
}
/// <summary>
/// Converts a seconds-elapsed value to a string.
/// </summary>
/// <param name="value">Seconds elapsed</param>
/// <param name="secondsBias">If provided, treat as epoch 2000 + secondsBias</param>
/// <returns>String representation of the date/time value</returns>
public static string ConvertDateValueToString(int value, int secondsBias = -1)
{
var sb = new System.Text.StringBuilder();
@ -81,7 +99,14 @@ public static DateOnly GetRandomDateWithin(DateOnly start, DateOnly end, Random
/// <inheritdoc cref="GetRandomDateWithin(DateOnly,DateOnly,Random)"/>
public static DateOnly GetRandomDateWithin(DateOnly start, DateOnly end) => GetRandomDateWithin(start, end, Util.Rand);
public static bool IsTimeValid(byte receivedHour, byte receivedMinute, byte receivedSecond)
/// <summary>
/// Checks if the given time components represent a valid time.
/// </summary>
/// <param name="receivedHour"></param>
/// <param name="receivedMinute"></param>
/// <param name="receivedSecond"></param>
/// <returns></returns>
public static bool IsValidTime(byte receivedHour, byte receivedMinute, byte receivedSecond)
{
return receivedHour < 24u && receivedMinute < 60u && receivedSecond < 60u;
}

View File

@ -179,7 +179,6 @@ public static string ToTitleCase(ReadOnlySpan<char> span)
/// <inheritdoc cref="ToTitleCase(ReadOnlySpan{char})"/>
public static void ToTitleCase(ReadOnlySpan<char> span, Span<char> result)
{
// Add each word to the string builder. Continue from the first index that isn't a space.
// Add the first character as uppercase, then add each successive character as lowercase.
bool first = true;
for (var i = 0; i < span.Length; i++)

View File

@ -11,7 +11,7 @@ public class DateUtilTests
[InlineData(2001, 1, 31)]
public void RecognizesCorrectDates(int year, int month, int day)
{
Assert.True(DateUtil.IsDateValid(year, month, day), $"Failed to recognize {year}/{month}/{day}");
Assert.True(DateUtil.IsValidDate(year, month, day), $"Failed to recognize {year}/{month}/{day}");
}
[Theory]
@ -29,13 +29,13 @@ public void RecognizesCorrectDates(int year, int month, int day)
[InlineData(2016, 12, 31)]
public void RecognizesValidMonthBoundaries(int year, int month, int day)
{
Assert.True(DateUtil.IsDateValid(year, month, day), $"Incorrect month boundary for {year}/{month}/{day}");
Assert.True(DateUtil.IsValidDate(year, month, day), $"Incorrect month boundary for {year}/{month}/{day}");
}
[Fact]
public void RecognizeCorrectLeapYear()
{
Assert.True(DateUtil.IsDateValid(2004, 2, 29));
Assert.True(DateUtil.IsValidDate(2004, 2, 29));
}
[Theory]
@ -51,7 +51,7 @@ public void RecognizeCorrectLeapYear()
[InlineData(uint.MaxValue, uint.MaxValue, uint.MaxValue, false, "Failed with uint.MaxValue, negative")]
public void CheckDate(uint year, uint month, uint day, bool cmp, string because)
{
var result = DateUtil.IsDateValid(year, month, day);
var result = DateUtil.IsValidDate(year, month, day);
result.Should().Be(cmp, because);
}