Initial gen3 virtual console checks

Disables branching when virtual console is the current save file
This commit is contained in:
Kurt 2026-02-27 12:50:07 -06:00
parent 5c4d27f7e4
commit c037829b29
15 changed files with 149 additions and 19 deletions

View File

@ -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<byte> TradeContest_Cool => [ 30, 05, 05, 05, 05, 10 ];

View File

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

View File

@ -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<EvoCriteria> result, PKM pk, EvolutionOrigin enc) => EvolutionUtil.Discard(result, Personal);

View File

@ -94,6 +94,12 @@ private static void CheckEncounterMoves(Span<MoveResult> result, ReadOnlySpan<us
private static void Check(Span<MoveResult> result, ReadOnlySpan<ushort> 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<MoveResult> result, ReadOnlySpan<ushort> current,
}
}
private static void CheckNX(Span<MoveResult> result, ReadOnlySpan<ushort> 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<bool> 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<bool> result, PKM pk, EvolutionHistory history, IEn
private static void GetAllMoves(Span<bool> 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));

View File

@ -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.
/// </summary>
/// <remarks>If false, indicates to use Virtual Console rules (which are transferable to Gen7+)</remarks>
public static bool AllowGBCartEra { private get; set; }
public static bool AllowEraCartGB { private get; set; }
/// <summary>
/// Setting to specify if an analysis should permit data sourced from the physical cartridge era of Game Boy Advance games.
/// </summary>
public static bool AllowEraCartGBA { private get; set; }
/// <summary>
/// Setting to specify if an analysis should permit data sourced from the Nintendo Switch Virtual Console era of Game Boy Advance games.
/// </summary>
public static bool AllowEraSwitchGBA { private get; set; }
/// <summary>
/// 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
/// <returns>True if Crystal data is allowed</returns>
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;
/// <summary>
/// Initializes certain settings
/// </summary>
/// <param name="sav">Newly loaded save file</param>
/// <returns>Save file is Physical GB cartridge save file (not Virtual Console)</returns>
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;

View File

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

View File

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

View File

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

View File

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

View File

@ -324,6 +324,21 @@ public static bool GetValidRibbonStateNational(PKM pk, IEncounterTemplate enc)
return true;
}
/// <summary>
/// Checks if the input can receive the <see cref="IRibbonSetEvent3.RibbonEarth"/> ribbon.
/// </summary>
/// <remarks>
/// If returns true, can have the ribbon. If returns false, must not have the ribbon.
/// </remarks>
public static bool IsEarthRibbonAllowed(PKM pk, IEncounterTemplate enc)
{
if (enc.Generation != 3)
return false;
if (!ParseSettings.AllowGBACrossTransferXD(pk))
return false;
return true;
}
/// <summary>
/// Gets the max count values the input can receive for the <see cref="IRibbonSetMemory6.RibbonCountMemoryContest"/> and <see cref="IRibbonSetMemory6.RibbonCountMemoryBattle"/> ribbon counts.
/// </summary>
@ -360,9 +375,11 @@ public static (byte Contest, byte Battle) GetMaxMemoryCounts(EvolutionHistory ev
/// <summary>
/// Checks if the input evolution history could have participated in Generation 3 contests.
/// </summary>
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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