diff --git a/PKHeX.Core/Legality/Encounters/Data/Gen3/Encounters3FRLG.cs b/PKHeX.Core/Legality/Encounters/Data/Gen3/Encounters3FRLG.cs index 9969d7212..1be7487ae 100644 --- a/PKHeX.Core/Legality/Encounters/Data/Gen3/Encounters3FRLG.cs +++ b/PKHeX.Core/Legality/Encounters/Data/Gen3/Encounters3FRLG.cs @@ -71,7 +71,7 @@ private static EncounterArea3[] GetRegular([ConstantExpected] string resource, [ new(147, 18, FR) { FixedBall = Ball.Poke, Location = 94 }, // Dratini new(137, 26, FR) { FixedBall = Ball.Poke, Location = 94 }, // Porygon - new(386, 30, FR ) { Location = 187, FatefulEncounter = true, Form = 1 }, // Deoxys @ Birth Island + new(386, 30, FR) { Location = 187, FatefulEncounter = true, Form = 1 }, // Deoxys @ Birth Island ]; public static readonly EncounterStatic3[] StaticLG = @@ -88,7 +88,8 @@ private static EncounterArea3[] GetRegular([ConstantExpected] string resource, [ new(127, 18, LG) { FixedBall = Ball.Poke, Location = 94 }, // Pinsir new(147, 24, LG) { FixedBall = Ball.Poke, Location = 94 }, // Dratini new(137, 18, LG) { FixedBall = Ball.Poke, Location = 94 }, // Porygon - new(386, 30, LG) { Location = 187, FatefulEncounter = true, Form = 2 }, // Deoxys @ Birth Island + + new(386, 30, LG) { Location = 187, FatefulEncounter = true, Form = 2 }, // Deoxys @ Birth Island ]; private static ReadOnlySpan TradeContest_Cool => [ 30, 05, 05, 05, 05, 10 ]; diff --git a/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs b/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs index 3dcb30913..14102a55e 100644 --- a/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs +++ b/PKHeX.Core/Legality/Encounters/Verifiers/EncounterVerifier.cs @@ -22,8 +22,14 @@ public static class EncounterVerifier private static CheckResult VerifyEncounter(PKM pk, IEncounterTemplate enc) => enc switch { EncounterShadow3Colo { IsEReader: true } when pk.Language != (int)LanguageID.Japanese => GetInvalid(G3EReader), - EncounterStatic3 { Species: (int)Species.Mew } when pk.Language != (int)LanguageID.Japanese => GetInvalid(EncUnreleasedEMewJP), - EncounterStatic3 { Species: (int)Species.Deoxys, Location: 200 } when pk.Language == (int)LanguageID.Japanese => GetInvalid(EncUnreleased), + + // Mew @ Faraway Island (Emerald) + EncounterStatic3 { Species: (int)Species.Mew } when pk.Language != (int)LanguageID.Japanese + => GetInvalid(EncUnreleasedEMewJP), + // Deoxys @ Birth Island (FireRed/LeafGreen) - Never distributed in Japan during GBA Cart era. NX virtual console added for all. + EncounterStatic3 { Species: (int)Species.Deoxys, Location: 200 } when pk.Language == (int)LanguageID.Japanese && !ParseSettings.AllowGen3EventTicketsAll(pk) + => GetInvalid(EncUnreleased), + EncounterStatic4 { Species: (int)Species.Shaymin } when pk.Language == (int)LanguageID.Korean => GetInvalid(EncUnreleased), EncounterStatic4 { IsRoaming: true } when pk is G4PKM { MetLocation: 193, GroundTile: GroundTileType.Water } => GetInvalid(G4InvalidTileR45Surf), MysteryGift g => VerifyEncounterEvent(pk, g), diff --git a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs index 7f6eb55b1..8576b8006 100644 --- a/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs +++ b/PKHeX.Core/Legality/Evolutions/EvolutionGroup/EvolutionGroup3.cs @@ -10,7 +10,7 @@ public sealed class EvolutionGroup3 : IEvolutionGroup private static PersonalTable3 Personal => PersonalTable.E; private static EvolutionRuleTweak Tweak => EvolutionRuleTweak.Default; - public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup4.Instance : null; + public IEvolutionGroup? GetNext(PKM pk, EvolutionOrigin enc) => pk.Format > Generation ? EvolutionGroup4.Instance : null; // TODO HOME FR/LG public IEvolutionGroup? GetPrevious(PKM pk, EvolutionOrigin enc) => null; public void DiscardForOrigin(Span result, PKM pk, EvolutionOrigin enc) => EvolutionUtil.Discard(result, Personal); diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs index 310c8f68f..af822a2db 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup3.cs @@ -94,6 +94,12 @@ private static void CheckEncounterMoves(Span result, ReadOnlySpan result, ReadOnlySpan current, PKM pk, EvoCriteria evo, int stage, MoveSourceType types) { + if (!ParseSettings.AllowGBACrossTransferRSE(pk)) + { + CheckNX(result, current, pk, evo, stage, types); + return; + } + var rs = LearnSource3RS.Instance; var species = evo.Species; if (!rs.TryGetPersonal(species, evo.Form, out var rp)) @@ -137,6 +143,34 @@ private static void Check(Span result, ReadOnlySpan current, } } + private static void CheckNX(Span result, ReadOnlySpan current, PKM pk, EvoCriteria evo, int stage, MoveSourceType types) + { + var species = evo.Species; + var fr = LearnSource3FR.Instance; + if (!fr.TryGetPersonal(species, evo.Form, out var fp)) + return; // should never happen. + var lg = LearnSource3LG.Instance; + var lp = lg[species]; + + for (int i = result.Length - 1; i >= 0; i--) + { + if (result[i].Valid) + continue; + + // Level Up moves are different for each game, but TM/HM is shared (use Emerald). + var move = current[i]; + var chk = fr.GetCanLearn(pk, fp, evo, move, types & (MoveSourceType.LevelUp | MoveSourceType.AllTutors)); + if (chk != default) + { + result[i] = new(chk, (byte)stage, Context); + continue; + } + chk = lg.GetCanLearn(pk, lp, evo, move, types & MoveSourceType.LevelUp); // Tutors same as FR + if (chk != default) + result[i] = new(chk, (byte)stage, Context); + } + } + public void GetAllMoves(Span result, PKM pk, EvolutionHistory history, IEncounterTemplate enc, MoveSourceType types = MoveSourceType.All, LearnOption option = LearnOption.Current) { if (types.HasFlag(MoveSourceType.Encounter) && enc.Context == Context) @@ -158,6 +192,12 @@ public void GetAllMoves(Span result, PKM pk, EvolutionHistory history, IEn private static void GetAllMoves(Span result, PKM pk, EvoCriteria evo, MoveSourceType types) { + if (!ParseSettings.AllowGBACrossTransferRSE(pk)) // NX + { + LearnSource3FR.Instance.GetAllMoves(result, pk, evo, types); + LearnSource3LG.Instance.GetAllMoves(result, pk, evo, types & (MoveSourceType.LevelUp)); + return; + } LearnSource3E.Instance.GetAllMoves(result, pk, evo, types); LearnSource3RS.Instance.GetAllMoves(result, pk, evo, types & (MoveSourceType.LevelUp | MoveSourceType.AllTutors)); LearnSource3FR.Instance.GetAllMoves(result, pk, evo, types & (MoveSourceType.LevelUp | MoveSourceType.AllTutors)); diff --git a/PKHeX.Core/Legality/Settings/ParseSettings.cs b/PKHeX.Core/Legality/Settings/ParseSettings.cs index 303a0f40d..f829aa6f6 100644 --- a/PKHeX.Core/Legality/Settings/ParseSettings.cs +++ b/PKHeX.Core/Legality/Settings/ParseSettings.cs @@ -31,7 +31,17 @@ public static class ParseSettings /// Setting to specify if an analysis should permit data sourced from the physical cartridge era of Game Boy games. /// /// If false, indicates to use Virtual Console rules (which are transferable to Gen7+) - public static bool AllowGBCartEra { private get; set; } + public static bool AllowEraCartGB { private get; set; } + + /// + /// Setting to specify if an analysis should permit data sourced from the physical cartridge era of Game Boy Advance games. + /// + public static bool AllowEraCartGBA { private get; set; } + + /// + /// Setting to specify if an analysis should permit data sourced from the Nintendo Switch Virtual Console era of Game Boy Advance games. + /// + public static bool AllowEraSwitchGBA { private get; set; } /// /// Setting to specify if an analysis should permit trading a Generation 1 origin file to Generation 2, then back. Useful for checking RBY Metagame rules. @@ -55,27 +65,35 @@ public static class ParseSettings /// True if Crystal data is allowed public static bool AllowGen2MoveReminder(PKM pk) => !pk.Korean && AllowGBStadium2; - public static bool AllowGen2OddEgg(PKM pk) => !pk.Japanese || AllowGBCartEra; + public static bool AllowGen2OddEgg(PKM pk) => !pk.Japanese || AllowEraCartGB; - public static bool AllowGBVirtualConsole3DS => !AllowGBCartEra; - public static bool AllowGBEraEvents => AllowGBCartEra; - public static bool AllowGBStadium2 => AllowGBCartEra; + public static bool AllowGBVirtualConsole3DS => !AllowEraCartGB; + public static bool AllowGBEraEvents => AllowEraCartGB; + public static bool AllowGBStadium2 => AllowEraCartGB; + + // This logic will likely need to change (format check): TODO HOME FR/LG + public static bool AllowGBACrossTransferXD(PKM pk) => AllowEraCartGBA; + public static bool AllowGBACrossTransferRSE(PKM pk) => AllowEraCartGBA; + public static bool AllowGen3EventTicketsAll(PKM pk) => AllowEraSwitchGBA; /// /// Initializes certain settings /// /// Newly loaded save file /// Save file is Physical GB cartridge save file (not Virtual Console) - public static bool InitFromSaveFileData(SaveFile sav) + public static void InitFromSaveFileData(SaveFile sav) { ActiveTrainer = sav; - return AllowGBCartEra = sav switch + AllowEraCartGB = sav switch { SAV1 { IsVirtualConsole: true } => false, SAV2 { IsVirtualConsole: true } => false, { Generation: 1 or 2 } => true, _ => false, }; + var isVirtual3 = sav is SAV3 { IsVirtualConsole: true }; + AllowEraSwitchGBA = isVirtual3; + AllowEraCartGBA = !isVirtual3; // sav.Generation >= 8; TODO HOME FR/LG } internal static bool IgnoreTransferIfNoTracker => Settings.HOMETransfer.HOMETransferTrackerNotPresent == Severity.Invalid; diff --git a/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs b/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs index 3f8026d67..f35a271c9 100644 --- a/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs +++ b/PKHeX.Core/Legality/Verifiers/Misc/ContestStatInfo.cs @@ -61,6 +61,7 @@ public void SetMaxContestStats(IEncounterTemplate enc, EvolutionHistory h) public static ContestStatGranting GetContestStatRestriction(PKM pk, byte origin, EvolutionHistory h) => origin switch { + 3 when pk.Format == 3 && !ParseSettings.AllowGBACrossTransferRSE(pk) => None, 3 => pk.Format < 6 ? CorrelateSheen : Mixed, 4 => pk.Format < 6 ? CorrelateSheen : Mixed, diff --git a/PKHeX.Core/Legality/Verifiers/Misc/MiscPokerusVerifier.cs b/PKHeX.Core/Legality/Verifiers/Misc/MiscPokerusVerifier.cs index cfcb220b4..35330d44d 100644 --- a/PKHeX.Core/Legality/Verifiers/Misc/MiscPokerusVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/Misc/MiscPokerusVerifier.cs @@ -14,6 +14,12 @@ internal void Verify(LegalityAnalysis data, PKM pk) if (pk.Format == 1) // not stored in Gen1 format return; + if (pk.Format == 3 && !ParseSettings.AllowGBACrossTransferRSE(pk)) + { + VerifyNone(data, pk); + return; + } + var strain = pk.PokerusStrain; var days = pk.PokerusDays; var enc = data.Info.EncounterMatch; @@ -22,4 +28,14 @@ internal void Verify(LegalityAnalysis data, PKM pk) if (!Pokerus.IsDurationValid(strain, days, out var max)) data.AddLine(GetInvalid(PokerusDaysLEQ_0, (ushort)max)); } + + private void VerifyNone(LegalityAnalysis data, PKM pk) + { + var strain = pk.PokerusStrain; + var days = pk.PokerusDays; + if (strain != 0) + data.AddLine(GetInvalid(PokerusStrainUnobtainable_0, (ushort)strain)); + if (days != 0) + data.AddLine(GetInvalid(PokerusDaysLEQ_0, 0)); + } } diff --git a/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs b/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs new file mode 100644 index 000000000..41b8d882c --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs @@ -0,0 +1,25 @@ +using static PKHeX.Core.LegalityCheckResultCode; +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core; + +internal sealed class MiscVerifierG3 : Verifier +{ + protected override CheckIdentifier Identifier => Misc; + + public override void Verify(LegalityAnalysis data) + { + if (data.Entity is G3PKM pk) + Verify(data, pk); + } + + internal void Verify(LegalityAnalysis data, G3PKM pk) + { + if (ParseSettings.AllowGBACrossTransferRSE(pk)) + return; + + // Only FR/LG are released. Only can originate from FR/LG. + if (pk.Version is not (GameVersion.FR or GameVersion.LG)) + data.AddLine(GetInvalid(EncUnreleased)); + } +} diff --git a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs index 0e8beac78..eb444d982 100644 --- a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs @@ -19,6 +19,7 @@ public sealed class MiscVerifier : Verifier private static readonly MiscG1Verifier Gen1 = new(); private static readonly MiscEvolutionVerifier Evolution = new(); private static readonly MiscVerifierSK2 Stadium2 = new(); + private static readonly MiscVerifierG3 Gen3 = new(); private static readonly MiscVerifierG4 Gen4 = new(); private static readonly MiscVerifierPK6 Gen6 = new(); private static readonly MiscVerifierPK5 Gen5 = new(); @@ -41,6 +42,7 @@ public override void Verify(LegalityAnalysis data) // Verify gimmick data switch (pk) { + case G3PKM pk3: Gen3.Verify(data, pk3); break; case G4PKM pk4: Gen4.Verify(data, pk4); break; case PK5 pk5: Gen5.Verify(data, pk5); break; case PK6 pk6: Gen6.Verify(data, pk6); break; diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs index 8b94b6ed7..aaf8eeaa6 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonRules.cs @@ -324,6 +324,21 @@ public static bool GetValidRibbonStateNational(PKM pk, IEncounterTemplate enc) return true; } + /// + /// Checks if the input can receive the ribbon. + /// + /// + /// If returns true, can have the ribbon. If returns false, must not have the ribbon. + /// + public static bool IsEarthRibbonAllowed(PKM pk, IEncounterTemplate enc) + { + if (enc.Generation != 3) + return false; + if (!ParseSettings.AllowGBACrossTransferXD(pk)) + return false; + return true; + } + /// /// Gets the max count values the input can receive for the and ribbon counts. /// @@ -360,9 +375,11 @@ public static (byte Contest, byte Battle) GetMaxMemoryCounts(EvolutionHistory ev /// /// Checks if the input evolution history could have participated in Generation 3 contests. /// - public static bool IsAllowedContest3(EvolutionHistory evos) + public static bool IsAllowedContest3(EvolutionHistory evos, PKM pk) { // Any species can enter contests in Gen3. + if (!ParseSettings.AllowGBACrossTransferRSE(pk)) + return false; return evos.HasVisitedGen3; } diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierEvent3.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierEvent3.cs index 13071d721..600968d44 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierEvent3.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierEvent3.cs @@ -21,7 +21,7 @@ public void Parse(in RibbonVerifierArguments args, ref RibbonResultList list) if (!r.RibbonEarth) list.Add(Earth, true); } - else if (r.RibbonEarth && enc.Generation != 3) + else if (r.RibbonEarth && !RibbonRules.IsEarthRibbonAllowed(args.Entity, enc)) { list.Add(Earth); } @@ -41,7 +41,7 @@ public void Parse(in RibbonVerifierArguments args, ref RibbonResultList list) { // The Earth Ribbon is a ribbon exclusive to Pokémon Colosseum and Pokémon XD: Gale of Darkness // Awarded to all Pokémon on the player's team when they complete the Mt. Battle challenge without switching the team at any point. - if (r.RibbonEarth && enc.Generation != 3) + if (r.RibbonEarth && !RibbonRules.IsEarthRibbonAllowed(args.Entity, enc)) list.Add(Earth); var nationalRequired = RibbonRules.GetValidRibbonStateNational(args.Entity, enc); diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierOnly3.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierOnly3.cs index 8cc840dd1..2a3f60eb4 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierOnly3.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierOnly3.cs @@ -12,7 +12,7 @@ public static void Parse(this IRibbonSetOnly3 r, in RibbonVerifierArguments args if (r.RibbonWorld) list.Add(RibbonIndex.World); - if (!RibbonRules.IsAllowedContest3(args.History)) + if (!RibbonRules.IsAllowedContest3(args.History, args.Entity)) FlagContestAny(r, ref list); else FlagContest(r, ref list); diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierUnique4.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierUnique4.cs index de34cbde0..090de89ea 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierUnique4.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/RibbonVerifierUnique4.cs @@ -13,7 +13,7 @@ public void Parse(in RibbonVerifierArguments args, ref RibbonResultList list) if (!RibbonRules.IsAllowedBattleFrontier4(evos)) FlagAnyAbility(r, ref list); - if (RibbonRules.IsAllowedContest3(evos)) + if (RibbonRules.IsAllowedContest3(evos, args.Entity)) AddMissingContest3(r, ref list); else FlagAnyContest3(r, ref list); diff --git a/PKHeX.Core/Saves/SAV3.cs b/PKHeX.Core/Saves/SAV3.cs index 7ec43d954..f4d0a6a28 100644 --- a/PKHeX.Core/Saves/SAV3.cs +++ b/PKHeX.Core/Saves/SAV3.cs @@ -14,10 +14,13 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai public sealed override string Extension => ".sav"; public int SaveRevision => Japanese ? 0 : 1; - public string SaveRevisionString => Japanese ? "J" : "U"; + public string SaveRevisionString => (Japanese ? "J" : "U") + (IsVirtualConsole ? "VC" : "GBA"); public bool Japanese { get; } public bool Korean => false; + public bool IsVirtualConsole => State.Exportable && Metadata.FileName is { } s && s.Contains(".sav") + && (s.StartsWith("FireRed_", StringComparison.Ordinal) || s.StartsWith("LeafGreen_")); // default to Mainline-Era for non-exportable + // Similar to future games, the Generation 3 Mainline save files are comprised of separate objects: // Object 1 - Small, containing misc configuration data & the Pokédex. // Object 2 - Large, containing everything else that isn't PC Storage system data. diff --git a/Tests/PKHeX.Core.Tests/Legality/LegalityTests.cs b/Tests/PKHeX.Core.Tests/Legality/LegalityTests.cs index 54e0f1f86..530d1b86c 100644 --- a/Tests/PKHeX.Core.Tests/Legality/LegalityTests.cs +++ b/Tests/PKHeX.Core.Tests/Legality/LegalityTests.cs @@ -130,7 +130,8 @@ private static void VerifyAll(string folder, string subFolder, bool isValid, boo prefer.IsValid.Should().BeTrue("filename is expected to have a valid extension"); var dn = fi.DirectoryName ?? string.Empty; - ParseSettings.AllowGBCartEra = dn.Contains("GBCartEra"); + ParseSettings.AllowEraCartGB = dn.Contains("GBCartEra"); + ParseSettings.AllowEraCartGBA = !dn.Contains("GBAVCEra"); ParseSettings.Settings.Tradeback.AllowGen1Tradeback = dn.Contains("1 Tradeback"); var pk = EntityFormat.GetFromBytes(data, prefer); pk.Should().NotBeNull($"the PKM '{new FileInfo(file).Name}' should have been loaded");