diff --git a/PKHeX.Core/Legality/LegalityAnalysis.cs b/PKHeX.Core/Legality/LegalityAnalysis.cs index 9b958d5bf..3b6990e6c 100644 --- a/PKHeX.Core/Legality/LegalityAnalysis.cs +++ b/PKHeX.Core/Legality/LegalityAnalysis.cs @@ -189,7 +189,7 @@ public sealed class LegalityAnalysis Nickname.Verify(this); Level.Verify(this); Level.VerifyG1(this); - Trainer.VerifyOTG1(this); + Trainer.VerifyOTGB(this); MiscValues.VerifyMiscG1(this); if (Entity.Format == 2) Item.Verify(this); diff --git a/PKHeX.Core/Legality/RNG/Util/ShinyUtil.cs b/PKHeX.Core/Legality/RNG/Util/ShinyUtil.cs index b7c393a74..3ac160cda 100644 --- a/PKHeX.Core/Legality/RNG/Util/ShinyUtil.cs +++ b/PKHeX.Core/Legality/RNG/Util/ShinyUtil.cs @@ -12,7 +12,7 @@ public static class ShinyUtil } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetIsShiny(in uint id32, in uint pid, [ConstantExpected] uint cmp = 16) => GetShinyXor(id32, pid) < cmp; + public static bool GetIsShiny(in uint id32, in uint pid, [ConstantExpected(Max = 16, Min = 8)] uint cmp = 16) => GetShinyXor(id32, pid) < cmp; public static uint GetShinyXor(in uint pid, in uint id32) { diff --git a/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs b/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs index 4e6b74fff..bc2bda03d 100644 --- a/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs +++ b/PKHeX.Core/Legality/Restrictions/GBRestrictions.cs @@ -93,25 +93,25 @@ internal static class GBRestrictions return rate == PersonalTable.RB[species].CatchRate; } + private static bool RateMatchesEither(byte catch_rate, ushort rate) + { + return catch_rate == PersonalTable.RB[rate].CatchRate || catch_rate == PersonalTable.Y[rate].CatchRate; + } + private static bool GetCatchRateMatchesPreEvolution(PK1 pk, byte catch_rate) { // For species catch rate, discard any species that has no valid encounters and a different catch rate than their pre-evolutions var head = new EvoCriteria { Species = pk.Species, Form = pk.Form, LevelMax = (byte)pk.CurrentLevel }; // as struct to avoid boxing - while (true) + do { var s = head.Species; - if (!IsSpeciesNotAvailableCatchRate((byte)s)) - { - if (catch_rate == PersonalTable.RB[s].CatchRate || catch_rate == PersonalTable.Y[s].CatchRate) - return true; - } - - if (!EvolutionGroup1.Instance.TryDevolve(head, pk, head.LevelMax, 2, false, out head)) - break; + if (!IsSpeciesNotAvailableCatchRate((byte)s) && RateMatchesEither(catch_rate, s)) + return true; } + while (EvolutionGroup1.Instance.TryDevolve(head, pk, head.LevelMax, 2, false, out head)); // Account for oddities via special catch rate encounters - if (catch_rate is 167 or 168 && IsStadiumGiftSpecies((byte)pk.Species)) + if (catch_rate is 167 or 168 && IsStadiumGiftSpecies((byte)head.Species)) return true; return false; } diff --git a/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs b/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs index 2c788e3ac..b2e518fe9 100644 --- a/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs @@ -34,7 +34,7 @@ public sealed class TrainerNameVerifier : Verifier if (pk.VC) { - VerifyOTG1(data); + VerifyOTGB(data); } else if (ot.Length > Legal.GetMaxLengthOT(data.Info.Generation, (LanguageID)pk.Language)) { @@ -65,11 +65,11 @@ public sealed class TrainerNameVerifier : Verifier _ => true, }; - public static bool IsEdgeCaseLength(PKM pk, IEncounterTemplate e, string ot) + public static bool IsEdgeCaseLength(PKM pk, IEncounterTemplate e, ReadOnlySpan ot) { if (e.EggEncounter) { - if (e is WC3 wc3 && pk.IsEgg && wc3.OT_Name == ot) + if (e is WC3 wc3 && pk.IsEgg && ot.SequenceEqual(wc3.OT_Name)) return true; // Fixed OT Mystery Gift Egg bool eggEdge = pk.IsEgg ? pk.IsTradedEgg || pk.Format == 3 : pk.WasTradedEgg; if (!eggEdge) @@ -86,11 +86,22 @@ public sealed class TrainerNameVerifier : Verifier return false; } - public void VerifyOTG1(LegalityAnalysis data) + public void VerifyOTGB(LegalityAnalysis data) { var pk = data.Entity; - string tr = pk.OT_Name; + var enc = data.EncounterOriginal; + if (pk.OT_Gender == 1) + { + // Transferring from RBY->Gen7 won't have OT Gender in PK1, nor will PK1 originated encounters. + // GSC Trades already checked for OT Gender matching. + if (pk is { Format: > 2, VC1: true } || enc is { Generation: 1 } or EncounterGift2 { IsGift: true }) + data.AddLine(GetInvalid(LG1OTGender)); + } + if (enc is IFixedTrainer { IsFixedTrainer: true }) + return; // already verified + + string tr = pk.OT_Name; if (tr.Length == 0) { if (pk is SK2 {TID16: 0, IsRental: true}) @@ -103,50 +114,46 @@ public sealed class TrainerNameVerifier : Verifier return; } } - - VerifyG1OTWithinBounds(data, tr); - - if (pk.OT_Gender == 1) - { - if (pk is ICaughtData2 {CaughtData:0} or { Format: > 2, VC1: true } || data is {EncounterOriginal: {Generation:1} or EncounterGift2 {IsGift:true}}) - data.AddLine(GetInvalid(LG1OTGender)); - } + VerifyGBOTWithinBounds(data, tr); } - private void VerifyG1OTWithinBounds(LegalityAnalysis data, ReadOnlySpan str) + private void VerifyGBOTWithinBounds(LegalityAnalysis data, ReadOnlySpan str) { - if (StringConverter12.GetIsG1English(str)) - { - if (str.Length > 7 && data.EncounterOriginal is not IFixedTrainer { IsFixedTrainer: true }) // OT already verified; GER shuckle has 8 chars - data.AddLine(GetInvalid(LOTLong)); - } - else if (StringConverter12.GetIsG1Japanese(str)) + var pk = data.Entity; + if (pk.Japanese) { if (str.Length > 5) data.AddLine(GetInvalid(LOTLong)); + if (!StringConverter12.GetIsG1Japanese(str)) + data.AddLine(GetInvalid(LG1CharOT)); } - else if (data.Entity.Korean && StringConverter2KOR.GetIsG2Korean(str)) + else if (pk.Korean) { if (str.Length > 5) data.AddLine(GetInvalid(LOTLong)); + if (!StringConverter2KOR.GetIsG2Korean(str)) + data.AddLine(GetInvalid(LG1CharOT)); } - else if (data.EncounterOriginal is not EncounterTrade2) // OT already verified; SPA Shuckle/Voltorb transferred from French can yield 2 inaccessible chars + else { - data.AddLine(GetInvalid(LG1CharOT)); + if (str.Length > 7) + data.AddLine(GetInvalid(LOTLong)); + if (!StringConverter12.GetIsG1English(str)) + data.AddLine(GetInvalid(LG1CharOT)); } } - private static bool IsOTNameSuspicious(string name) + private static bool IsOTNameSuspicious(ReadOnlySpan name) { foreach (var s in SuspiciousOTNames) { - if (s.StartsWith(name, StringComparison.InvariantCultureIgnoreCase)) + if (name.StartsWith(s, StringComparison.OrdinalIgnoreCase)) return true; } return false; } - public static bool ContainsTooManyNumbers(string str, int originalGeneration) + public static bool ContainsTooManyNumbers(ReadOnlySpan str, int originalGeneration) { if (originalGeneration <= 3) return false; // no limit from these generations @@ -157,11 +164,11 @@ public sealed class TrainerNameVerifier : Verifier return count > max; } - private static int GetNumberCount(string str) + private static int GetNumberCount(ReadOnlySpan str) { static bool IsNumber(char c) { - if ('0' <= c) + if (c >= '0') return c <= '9'; return (uint)(c - '0') <= 9; } diff --git a/PKHeX.Core/PKM/Util/EntityFileExtension.cs b/PKHeX.Core/PKM/Util/EntityFileExtension.cs index 2d9575096..123f7598c 100644 --- a/PKHeX.Core/PKM/Util/EntityFileExtension.cs +++ b/PKHeX.Core/PKM/Util/EntityFileExtension.cs @@ -5,9 +5,16 @@ namespace PKHeX.Core; public static class EntityFileExtension { + // All side-game formats that don't follow the usual pk* format + private const string ExtensionSK2 = "sk2"; + private const string ExtensionCK3 = "ck3"; + private const string ExtensionXK3 = "xk3"; + private const string ExtensionBK4 = "bk4"; + private const string ExtensionRK4 = "rk4"; private const string ExtensionPB7 = "pb7"; private const string ExtensionPB8 = "pb8"; private const string ExtensionPA8 = "pa8"; + private const int CountExtra = 8; public static IReadOnlyList Extensions7b => new[] { ExtensionPB7 }; @@ -19,20 +26,22 @@ public static class EntityFileExtension public static string[] GetExtensions(int maxGeneration = PKX.Generation) { int min = maxGeneration is <= 2 or >= 7 ? 1 : 3; - int size = maxGeneration - min + 1 + 6; + int size = maxGeneration - min + 1 + CountExtra; var result = new List(size); for (int i = min; i <= maxGeneration; i++) result.Add($"pk{i}"); + if (min < 3) + result.Add(ExtensionSK2); // stadium if (maxGeneration >= 3) { - result.Add("ck3"); // colosseum - result.Add("xk3"); // xd + result.Add(ExtensionCK3); // colosseum + result.Add(ExtensionXK3); // xd } if (maxGeneration >= 4) { - result.Add("bk4"); // battle revolution - result.Add("rk4"); // My Pokemon Ranch + result.Add(ExtensionBK4); // battle revolution + result.Add(ExtensionRK4); // My Pokemon Ranch } if (maxGeneration >= 7) result.Add(ExtensionPB7); // let's go @@ -56,6 +65,7 @@ public static class EntityFileExtension return prefer; static bool Is(ReadOnlySpan ext, ReadOnlySpan str) => ext.EndsWith(str, StringComparison.InvariantCultureIgnoreCase); + if (Is(ext, "a8")) return EntityContext.Gen8a; if (Is(ext, "b8")) return EntityContext.Gen8b; if (Is(ext, "k8")) return EntityContext.Gen8; if (Is(ext, "b7")) return EntityContext.Gen7b; diff --git a/PKHeX.Core/Saves/Substructures/Battle Videos/BV6.cs b/PKHeX.Core/Saves/Substructures/Battle Videos/BV6.cs index 21afbe1b9..196e0df38 100644 --- a/PKHeX.Core/Saves/Substructures/Battle Videos/BV6.cs +++ b/PKHeX.Core/Saves/Substructures/Battle Videos/BV6.cs @@ -8,7 +8,7 @@ namespace PKHeX.Core; public sealed class BV6 : BattleVideo { - internal const int SIZE = 0x2E60; + public const int SIZE = 0x2E60; private const string NPC = "NPC"; private readonly byte[] Data; private const int PlayerCount = 4; diff --git a/PKHeX.Core/Saves/Substructures/Battle Videos/BV7.cs b/PKHeX.Core/Saves/Substructures/Battle Videos/BV7.cs index 1570d5bbd..5402551d0 100644 --- a/PKHeX.Core/Saves/Substructures/Battle Videos/BV7.cs +++ b/PKHeX.Core/Saves/Substructures/Battle Videos/BV7.cs @@ -7,7 +7,7 @@ namespace PKHeX.Core; public sealed class BV7 : BattleVideo { - internal const int SIZE = 0x2BC0; + public const int SIZE = 0x2BC0; private const string NPC = "NPC"; private const int PlayerCount = 4; diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs index 10c3c30d5..3283f412a 100644 --- a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs +++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs @@ -11,20 +11,7 @@ public sealed class SaveHandlerDeSmuME : ISaveHandler private const int RealSize = SaveUtil.SIZE_G4RAW; private const int ExpectedSize = RealSize + sizeFooter; - private const string SignatureDSV = "|-DESMUME SAVE-|"; - - private static bool GetHasFooter(ReadOnlySpan input) - { - var start = input.Length - SignatureDSV.Length; - var footer = input[start..]; - for (int i = SignatureDSV.Length - 1; i >= 0; i--) - { - byte c = (byte)SignatureDSV[i]; - if (footer[i] != c) - return false; - } - return true; - } + private static bool GetHasFooter(ReadOnlySpan input) => input.EndsWith("|-DESMUME SAVE-|"u8); public bool IsRecognized(long size) => size is ExpectedSize; diff --git a/PKHeX.Core/Saves/Util/SaveExtensions.cs b/PKHeX.Core/Saves/Util/SaveExtensions.cs index 5531200d7..083668bac 100644 --- a/PKHeX.Core/Saves/Util/SaveExtensions.cs +++ b/PKHeX.Core/Saves/Util/SaveExtensions.cs @@ -193,7 +193,7 @@ public static class SaveExtensions /// Template if it exists, or a blank from the public static PKM LoadTemplate(this SaveFile sav, string? templatePath = null) { - if (templatePath == null || !Directory.Exists(templatePath)) + if (!Directory.Exists(templatePath)) return LoadTemplateInternal(sav); var di = new DirectoryInfo(templatePath);