From ff0f4727dde0e3c27c5c3fa26a34466d8091b6a7 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sat, 9 Aug 2025 21:55:55 -0500 Subject: [PATCH] Extract logic from SaveUtil BlankSaveFile -> creation of blank save files SaveFileType -> listing of all savefile types Blank save file arg passing is now clearer Instead of SaveFile? return, use TryGet pattern with nullable annotations to indicate success --- .../Editing/Program/StartupArguments.cs | 28 +- .../Editors/EventWork/Diff/EventWorkDiff.cs | 30 +- .../Editors/EventWork/Diff/EventWorkDiff7b.cs | 16 +- .../Editors/EventWork/Diff/EventWorkDiff8b.cs | 14 +- .../Saves/MemoryCard/SAV3GCMemoryCard.cs | 22 +- PKHeX.Core/Saves/SAV1.cs | 40 +- PKHeX.Core/Saves/SAV2.cs | 15 +- PKHeX.Core/Saves/SAV8BS.cs | 2 +- PKHeX.Core/Saves/SAV_STADIUM.cs | 4 +- .../Substructures/Gen8/BS/Gem8Version.cs | 2 +- PKHeX.Core/Saves/Util/BlankSaveFile.cs | 131 +++ PKHeX.Core/Saves/Util/SaveFileType.cs | 106 +++ PKHeX.Core/Saves/Util/SaveFinder.cs | 13 +- PKHeX.Core/Saves/Util/SaveUtil.cs | 826 +++++++----------- PKHeX.Core/Util/FileUtil.cs | 14 +- PKHeX.WinForms/MainWindow/Main.cs | 39 +- PKHeX.WinForms/Subforms/SAV_Database.cs | 24 +- PKHeX.WinForms/Subforms/SAV_FolderList.cs | 4 +- .../Save Editors/Gen8/SAV_BlockDump8.cs | 6 +- .../Subforms/Save Editors/SAV_GameSelect.cs | 4 +- .../Saves/MemeCrypto/SwishCryptoTests.cs | 2 +- 21 files changed, 695 insertions(+), 647 deletions(-) create mode 100644 PKHeX.Core/Saves/Util/BlankSaveFile.cs create mode 100644 PKHeX.Core/Saves/Util/SaveFileType.cs diff --git a/PKHeX.Core/Editing/Program/StartupArguments.cs b/PKHeX.Core/Editing/Program/StartupArguments.cs index c58b8bb06..1f0996341 100644 --- a/PKHeX.Core/Editing/Program/StartupArguments.cs +++ b/PKHeX.Core/Editing/Program/StartupArguments.cs @@ -27,18 +27,11 @@ public void ReadArguments(IEnumerable args) { var other = FileUtil.GetSupportedFile(path, SAV); if (other is SaveFile s) - { - s.Metadata.SetExtraInfo(path); - SAV = s; - } + (SAV = s).Metadata.SetExtraInfo(path); else if (other is PKM pk) - { Entity = pk; - } else if (other is not null) - { Extra.Add(other); - } } } @@ -52,7 +45,7 @@ public void ReadSettings(IStartupSettings startup) if (Entity is { } x) SAV = ReadSettingsDefinedPKM(startup, x) ?? GetBlank(x); - else if (Extra.OfType().FirstOrDefault() is { } mc && SaveUtil.GetVariantSAV(mc) is { } mcSav) + else if (Extra.OfType().FirstOrDefault() is { } mc && SaveUtil.TryGetSaveFile(mc, out var mcSav)) SAV = mcSav; else SAV = ReadSettingsAnyPKM(startup) ?? GetBlankSaveFile(startup.DefaultSaveVersion, SAV); @@ -96,19 +89,19 @@ private static SaveFile GetBlank(PKM pk) if (pk is { Format: 1, Japanese: true }) version = GameVersion.BU; - return SaveUtil.GetBlankSAV(version, pk.OriginalTrainerName, (LanguageID)pk.Language); + return BlankSaveFile.Get(version, pk.OriginalTrainerName, (LanguageID)pk.Language); } private static SaveFile GetBlankSaveFile(GameVersion version, SaveFile? current) { - var lang = SaveUtil.GetSafeLanguage(current); - var tr = SaveUtil.GetSafeTrainerName(current, lang); - var sav = SaveUtil.GetBlankSAV(version, tr, lang); + var lang = BlankSaveFile.GetSafeLanguage(current); + var tr = BlankSaveFile.GetSafeTrainerName(current, lang); + var sav = BlankSaveFile.Get(version, tr, lang); if (sav.Version == GameVersion.Invalid) // will fail to load { var max = GameInfo.Sources.VersionDataSource.MaxBy(z => z.Value)!; var maxVer = (GameVersion)max.Value; - sav = SaveUtil.GetBlankSAV(maxVer, tr, lang); + sav = BlankSaveFile.Get(maxVer, tr, lang); } return sav; } @@ -120,11 +113,8 @@ private static IEnumerable GetMostRecentlyLoaded(IEnumerable p if (!File.Exists(path)) continue; - var sav = SaveUtil.GetVariantSAV(path); - if (sav is null) - continue; - - yield return sav; + if (SaveUtil.TryGetSaveFile(path, out var sav)) + yield return sav; } } #endregion diff --git a/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff.cs b/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff.cs index 15aaed3b5..f0580f25b 100644 --- a/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff.cs +++ b/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using static PKHeX.Core.EventWorkDiffCompatibility; using static PKHeX.Core.EventWorkDiffCompatibilityExtensions; @@ -21,6 +22,17 @@ public sealed class EventBlockDiff : IEventWorkDiff private const int MAX_SAVEFILE_SIZE = 0x10_0000; // 1 MB + private static bool TryGetSaveFile(string path, [NotNullWhen(true)] out TSave? sav, out GameVersion version) + { + version = default; + sav = null; + if (!SaveUtil.TryGetSaveFile(path, out var s) || s is not TSave b) + return false; + sav = b; + version = s.Version; + return true; + } + public EventBlockDiff(TSave s1, TSave s2) => Diff(s1, s2); public EventBlockDiff(string f1, string f2) @@ -28,29 +40,19 @@ public EventBlockDiff(string f1, string f2) Message = SanityCheckFiles(f1, f2, MAX_SAVEFILE_SIZE); if (Message != Valid) return; - var s1 = SaveUtil.GetVariantSAV(f1); - var s2 = SaveUtil.GetVariantSAV(f2); - if (s1 is null || s2 is null || s1.GetType() != s2.GetType() || GetBlock(s1) is not { } t1 || GetBlock(s2) is not { } t2) + + if (!TryGetSaveFile(f1, out var s1, out var v1) || !TryGetSaveFile(f2, out var s2, out var v2)) { Message = DifferentGameGroup; return; } - if (s1.Version != s2.Version) + if (v1 != v2) { Message = DifferentVersion; return; } - Diff(t1, t2); - } - - private static TSave? GetBlock(SaveFile s1) - { - if (s1 is TSave t1) - return t1; - if (s1 is IEventFlagProvider37 p1) - return p1.EventWork as TSave; - return null; + Diff(s1, s2); } private static EventWorkDiffCompatibility SanityCheckSaveInfo(TSave s1, TSave s2) diff --git a/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff7b.cs b/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff7b.cs index 78525b84c..3bd7ba5e7 100644 --- a/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff7b.cs +++ b/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff7b.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using static PKHeX.Core.EventWorkUtil; using static PKHeX.Core.EventWorkDiffCompatibility; @@ -19,17 +20,24 @@ public sealed class EventWorkDiff7b : IEventWorkDiff public EventWorkDiff7b(SAV7b s1, SAV7b s2) => Diff(s1, s2); + private static bool TryGetSaveFile(string path, [NotNullWhen(true)] out SAV7b? sav) + { + sav = null; + if (!SaveUtil.TryGetSaveFile(path, out var s) || s is not SAV7b b) + return false; + sav = b; + return true; + } + public EventWorkDiff7b(string f1, string f2) { Message = SanityCheckFiles(f1, f2, MAX_SAVEFILE_SIZE); if (Message != Valid) return; - var s1 = SaveUtil.GetVariantSAV(f1); - var s2 = SaveUtil.GetVariantSAV(f2); - if (s1 is not SAV7b b1 || s2 is not SAV7b b2) + if (!TryGetSaveFile(f1, out var b1) || !TryGetSaveFile(f2, out var b2)) { - Message = DifferentVersion; + Message = DifferentGameGroup; return; } diff --git a/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff8b.cs b/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff8b.cs index 577cb7da5..f467ee0e9 100644 --- a/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff8b.cs +++ b/PKHeX.Core/Editing/Saves/Editors/EventWork/Diff/EventWorkDiff8b.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using static PKHeX.Core.EventWorkUtil; using static PKHeX.Core.EventWorkDiffCompatibility; @@ -19,6 +20,15 @@ public sealed class EventWorkDiff8b : IEventWorkDiff private const int MAX_SAVEFILE_SIZE = 0x10_0000; // 1 MB + private static bool TryGetSaveFile(string path, [NotNullWhen(true)] out SAV8BS? sav) + { + sav = null; + if (!SaveUtil.TryGetSaveFile(path, out var s) || s is not SAV8BS b) + return false; + sav = b; + return true; + } + public EventWorkDiff8b(SAV8BS s1, SAV8BS s2) => Diff(s1, s2); public EventWorkDiff8b(string f1, string f2) @@ -26,9 +36,7 @@ public EventWorkDiff8b(string f1, string f2) Message = SanityCheckFiles(f1, f2, MAX_SAVEFILE_SIZE); if (Message != Valid) return; - var s1 = SaveUtil.GetVariantSAV(f1); - var s2 = SaveUtil.GetVariantSAV(f2); - if (s1 is not SAV8BS b1 || s2 is not SAV8BS b2) + if (!TryGetSaveFile(f1, out var b1) || !TryGetSaveFile(f2, out var b2)) { Message = DifferentGameGroup; return; diff --git a/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs b/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs index abfa17de7..095840f9a 100644 --- a/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs +++ b/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs @@ -257,31 +257,31 @@ private MemoryCardSaveStatus AutoInferState() return MemoryCardSaveStatus.SaveGameRSBOX; } - public bool IsNoGameSelected => SelectedGameVersion == GameVersion.Any; + public bool IsNoGameSelected => SelectedGameVersion == default; - public GameVersion SelectedGameVersion + public SaveFileType SelectedGameVersion { get { if (EntrySelected < 0) - return GameVersion.Any; + return SaveFileType.None; if (EntrySelected == EntryCOLO) - return GameVersion.COLO; + return SaveFileType.Colosseum; if (EntrySelected == EntryXD) - return GameVersion.XD; + return SaveFileType.XD; if (EntrySelected == EntryRSBOX) - return GameVersion.RSBOX; - return GameVersion.Any; //Default for no game selected + return SaveFileType.RSBox; + return default; //Default for no game selected } } - public void SelectSaveGame(GameVersion Game) + public void SelectSaveGame(SaveFileType Game) { switch (Game) { - case GameVersion.COLO: if (HasCOLO) EntrySelected = EntryCOLO; break; - case GameVersion.XD: if (HasXD) EntrySelected = EntryXD; break; - case GameVersion.RSBOX: if (HasRSBOX) EntrySelected = EntryRSBOX; break; + case SaveFileType.Colosseum: if (HasCOLO) EntrySelected = EntryCOLO; break; + case SaveFileType.XD: if (HasXD) EntrySelected = EntryXD; break; + case SaveFileType.RSBox: if (HasRSBOX) EntrySelected = EntryRSBOX; break; } } diff --git a/PKHeX.Core/Saves/SAV1.cs b/PKHeX.Core/Saves/SAV1.cs index 6d6826220..7608afb29 100644 --- a/PKHeX.Core/Saves/SAV1.cs +++ b/PKHeX.Core/Saves/SAV1.cs @@ -35,35 +35,33 @@ public sealed class SAV1 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWo public override IReadOnlyList PKMExtensions => EntityFileExtension.GetExtensionsAtOrBelow(2); - public SAV1(GameVersion version = GameVersion.RBY, LanguageID language = LanguageID.English) : base(SaveUtil.SIZE_G1RAW) + public SAV1(LanguageID language = LanguageID.English, GameVersion version = default) : base(SaveUtil.SIZE_G1RAW) { - Version = version; + Version = version == default ? GameVersion.RBY : version; Japanese = language == LanguageID.Japanese; Language = (int)language; Offsets = Japanese ? SAV1Offsets.JPN : SAV1Offsets.INT; - Personal = version == GameVersion.YW ? PersonalTable.Y : PersonalTable.RB; - Initialize(version); + Personal = Version == GameVersion.YW ? PersonalTable.Y : PersonalTable.RB; + + Initialize(); ClearBoxes(); } - public SAV1(Memory data, GameVersion versionOverride = GameVersion.Any, LanguageID language = LanguageID.English) : base(data) + public SAV1(Memory data, LanguageID language, GameVersion version = default) : base(data) { - Japanese = SaveUtil.GetIsG1SAVJ(Data); + Version = version == default ? GameVersion.RBY : version; + Japanese = language == LanguageID.Japanese; Language = (int)language; Offsets = Japanese ? SAV1Offsets.JPN : SAV1Offsets.INT; - - Version = versionOverride != GameVersion.Any ? versionOverride : SaveUtil.GetIsG1SAV(Data); Personal = Version == GameVersion.YW ? PersonalTable.Y : PersonalTable.RB; - if (Version == GameVersion.Invalid) - return; - Initialize(versionOverride); + Initialize(); } - private void Initialize(GameVersion versionOverride) + private void Initialize() { - // see if RBY can be differentiated - if (versionOverride is not (GameVersion.RB or GameVersion.YW)) + // See if RBY can be differentiated + if (Version is not (GameVersion.RB or GameVersion.YW)) { if (Starter != 0) // Pikachu Version = Starter == 0x54 ? GameVersion.YW : GameVersion.RB; @@ -217,7 +215,7 @@ private int GetBoxRawDataOffset(int box) } // Configuration - protected override SAV1 CloneInternal() => new(GetFinalData(), Version) { Language = Language }; + protected override SAV1 CloneInternal() => new(GetFinalData(), (LanguageID)Language, Version); protected override int SIZE_STORED => Japanese ? PokeCrypto.SIZE_1JLIST : PokeCrypto.SIZE_1ULIST; protected override int SIZE_PARTY => SIZE_STORED; @@ -591,4 +589,16 @@ public override int LoadString(ReadOnlySpan data, Span destBuffer) => StringConverter1.LoadString(data, destBuffer, Japanese); public override int SetString(Span destBuffer, ReadOnlySpan value, int maxLength, StringConverterOption option) => StringConverter1.SetString(destBuffer, value, maxLength, Japanese, option); + + + public static bool IsYellow(ReadOnlySpan data, bool japanese) => japanese ? IsYellowJPN(data) : IsYellowINT(data); + public static bool IsYellowINT(ReadOnlySpan data) => IsYellow(data[0x29C3], data[0x271C]); + public static bool IsYellowJPN(ReadOnlySpan data) => IsYellow(data[0x29B9], data[0x2712]); + + private static bool IsYellow(byte starter, byte friendship) + { + if (starter != 0) + return starter == 0x54; // Pikachu + return friendship != 0; // Initial Pikachu friendship is non-zero + } } diff --git a/PKHeX.Core/Saves/SAV2.cs b/PKHeX.Core/Saves/SAV2.cs index ed3c5cba5..5dad37e24 100644 --- a/PKHeX.Core/Saves/SAV2.cs +++ b/PKHeX.Core/Saves/SAV2.cs @@ -37,7 +37,7 @@ public sealed class SAV2 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWo public override IReadOnlyList PKMExtensions => Korean ? ["pk2"] : EntityFileExtension.GetExtensionsAtOrBelow(2); - public SAV2(GameVersion version = GameVersion.C, LanguageID language = LanguageID.English) : base(SaveUtil.SIZE_G2RAW_J) + public SAV2(LanguageID language = LanguageID.English, GameVersion version = GameVersion.C) : base(SaveUtil.SIZE_G2RAW_J) { Version = version; switch (language) @@ -60,13 +60,12 @@ public SAV2(GameVersion version = GameVersion.C, LanguageID language = LanguageI ClearBoxes(); } - public SAV2(Memory data, GameVersion versionOverride = GameVersion.Any) : base(data) + public SAV2(Memory data, LanguageID language, GameVersion version) : base(data) { - Version = versionOverride != GameVersion.Any ? versionOverride : SaveUtil.GetIsG2SAV(Data); - Japanese = SaveUtil.GetIsG2SAVJ(Data) != GameVersion.Invalid; - if (Version != GameVersion.C && !Japanese) - Korean = SaveUtil.GetIsG2SAVK(Data) != GameVersion.Invalid; - Language = Japanese ? 1 : Korean ? (int)LanguageID.Korean : -1; + Version = version == GameVersion.C ? GameVersion.C : GameVersion.GS; + Japanese = language == LanguageID.Japanese; + Korean = language == LanguageID.Korean; + Language = (int)language; Offsets = new SAV2Offsets(this); Personal = Version == GameVersion.C ? PersonalTable.C : PersonalTable.GS; @@ -243,7 +242,7 @@ private ushort GetKoreanChecksum() } // Configuration - protected override SAV2 CloneInternal() => new(GetFinalData(), Version) { Language = Language }; + protected override SAV2 CloneInternal() => new(GetFinalData(), (LanguageID)Language, Version); protected override int SIZE_STORED => Japanese ? PokeCrypto.SIZE_2JLIST : PokeCrypto.SIZE_2ULIST; protected override int SIZE_PARTY => SIZE_STORED; diff --git a/PKHeX.Core/Saves/SAV8BS.cs b/PKHeX.Core/Saves/SAV8BS.cs index f5df96eb0..91a0a71a0 100644 --- a/PKHeX.Core/Saves/SAV8BS.cs +++ b/PKHeX.Core/Saves/SAV8BS.cs @@ -166,7 +166,7 @@ public override StorageSlotSource GetBoxSlotFlags(int index) #region Checksums private const int HashLength = MD5.HashSizeInBytes; - private const int HashOffset = SaveUtil.SIZE_G8BDSP - HashLength; + private const int HashOffset = SaveUtil.SIZE_G8BDSP_0 - HashLength; private Span CurrentHash => Data.Slice(HashOffset, HashLength); // Checksum is stored in the middle of the save file, and is zeroed before computing. diff --git a/PKHeX.Core/Saves/SAV_STADIUM.cs b/PKHeX.Core/Saves/SAV_STADIUM.cs index 9b071939f..3710f7ea7 100644 --- a/PKHeX.Core/Saves/SAV_STADIUM.cs +++ b/PKHeX.Core/Saves/SAV_STADIUM.cs @@ -33,7 +33,7 @@ public abstract class SAV_STADIUM : SaveFile, ILangDeviantSave protected SAV_STADIUM(Memory data, bool japanese, bool swap) : base(data) { Japanese = japanese; - OT = SaveUtil.GetSafeTrainerName(this, (LanguageID)Language); + OT = BlankSaveFile.GetSafeTrainerName(this, (LanguageID)Language); if (!swap) return; @@ -44,7 +44,7 @@ protected SAV_STADIUM(Memory data, bool japanese, bool swap) : base(data) protected SAV_STADIUM(bool japanese, [ConstantExpected] int size) : base(size) { Japanese = japanese; - OT = SaveUtil.GetSafeTrainerName(this, (LanguageID)Language); + OT = BlankSaveFile.GetSafeTrainerName(this, (LanguageID)Language); } protected sealed override byte[] DecryptPKM(byte[] data) => data; diff --git a/PKHeX.Core/Saves/Substructures/Gen8/BS/Gem8Version.cs b/PKHeX.Core/Saves/Substructures/Gen8/BS/Gem8Version.cs index 27f661e4b..bd790131c 100644 --- a/PKHeX.Core/Saves/Substructures/Gen8/BS/Gem8Version.cs +++ b/PKHeX.Core/Saves/Substructures/Gen8/BS/Gem8Version.cs @@ -13,7 +13,7 @@ public enum Gem8Version /// /// Initial cartridge version shipped. /// - /// + /// V1_0 = 0x25, // 37 /// diff --git a/PKHeX.Core/Saves/Util/BlankSaveFile.cs b/PKHeX.Core/Saves/Util/BlankSaveFile.cs new file mode 100644 index 000000000..c8a10ee7a --- /dev/null +++ b/PKHeX.Core/Saves/Util/BlankSaveFile.cs @@ -0,0 +1,131 @@ +using System; +using static PKHeX.Core.LanguageID; +using static PKHeX.Core.SaveFileType; + +namespace PKHeX.Core; + +/// +/// Logic for blank save files or default values if not exposed via data. +/// +public static class BlankSaveFile +{ + private const string DefaultTrainer = TrainerName.ProgramINT; + private const LanguageID DefaultLanguage = English; + + /// + /// Returns a that feels best for the save file's language. + /// + public static LanguageID GetSafeLanguage(SaveFile? sav) => sav switch + { + null => English, + ILangDeviantSave s => s.Japanese ? Japanese : s.Korean ? Korean : English, + _ => (uint)sav.Language <= Legal.GetMaxLanguageID(sav.Generation) ? (LanguageID)sav.Language : English, + }; + + /// + /// Returns a Trainer Name that feels best for the save file's language. + /// + public static string GetSafeTrainerName(SaveFile? sav, LanguageID lang) => lang switch + { + Japanese => sav?.Generation >= 3 ? TrainerName.ProgramJPN : TrainerName.GameFreakJPN, + _ => TrainerName.ProgramINT, + }; + + /// + public static SaveFile Get(GameVersion game, string trainerName = DefaultTrainer, LanguageID language = DefaultLanguage) + { + var type = game.GetSaveFileType(); + return Get(type, game, trainerName, language); + } + + /// + /// Creates an instance of a SaveFile with a blank base. + /// + /// Context of the Save File. + /// Trainer Name + /// Save file language to initialize for + /// Save File for that generation. + public static SaveFile Get(EntityContext context, string trainerName = DefaultTrainer, LanguageID language = DefaultLanguage) + { + var version = context.GetSingleGameVersion(); + var type = version.GetSaveFileType(); + return Get(type, version, trainerName, language); + } + + /// + /// Creates an instance of a SaveFile with a blank base. + /// + /// Requested save file type. + /// Version to create the save file for. + /// Trainer Name + /// Language to initialize with + /// Blank save file from the requested game, null if no game exists for that . + public static SaveFile Get(SaveFileType type, GameVersion game, string trainerName = DefaultTrainer, LanguageID language = DefaultLanguage) + { + var sav = Get(type, language, game); + sav.Version = game; + sav.OT = trainerName; + if (sav.Generation >= 4) + sav.Language = (int)language; + + // Secondary Properties may not be used but can be filled in as template. + (uint tid, uint sid) = sav.Generation >= 7 ? (123456u, 1234u) : (12345u, 54321u); + sav.SetDisplayID(tid, sid); + sav.Language = (int)language; + + // Only set geolocation data for 3DS titles + if (sav is IRegionOrigin o) + o.SetDefaultRegionOrigins((int)language); + + return sav; + } + + /// + /// Creates an instance of a SaveFile with a blank base. + /// + /// Requested save file type. + /// Save file language to initialize for + /// Version to create the save file for, if a specific version is requested within the . + /// Blank save file from the requested game, null if no game exists for that . + private static SaveFile Get(SaveFileType type, LanguageID language, GameVersion game = default) => type switch + { + RBY => new SAV1(game == GameVersion.BU ? Japanese : language, version: game), + Stadium1J => new SAV1StadiumJ(), + Stadium1 => new SAV1Stadium(language == Japanese), + + GSC => new SAV2(language, language == Korean ? GameVersion.GS : game), + Stadium2 => new SAV2Stadium(language == Japanese), + + RS => new SAV3RS(language == Japanese), + Emerald => new SAV3E(language == Japanese), + FRLG => new SAV3FRLG(language == Japanese), + + Colosseum => new SAV3Colosseum(), + XD => new SAV3XD(), + RSBox => new SAV3RSBox(), + + DP => new SAV4DP(), + Pt => new SAV4Pt(), + HGSS => new SAV4HGSS(), + BattleRevolution => new SAV4BR(), + + BW => new SAV5BW(), + B2W2 => new SAV5B2W2(), + + XY => new SAV6XY(), + AODemo => new SAV6AODemo(), + AO => new SAV6AO(), + + SM => new SAV7SM(), + USUM => new SAV7USUM(), + LGPE => new SAV7b(), + + SWSH => new SAV8SWSH(), + BDSP => new SAV8BS(), + LA => new SAV8LA(), + + SV => new SAV9SV(), + + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; +} diff --git a/PKHeX.Core/Saves/Util/SaveFileType.cs b/PKHeX.Core/Saves/Util/SaveFileType.cs new file mode 100644 index 000000000..eec9c020d --- /dev/null +++ b/PKHeX.Core/Saves/Util/SaveFileType.cs @@ -0,0 +1,106 @@ +using System; +using static PKHeX.Core.SaveFileType; + +namespace PKHeX.Core; + +/// +/// Enumerates the possible save file types that can be detected and loaded. +/// +public enum SaveFileType : byte +{ + /// + /// Invalid or unrecognized save file type. + /// + None = 0, + + // Main Games + RBY, + GSC, + RS, + Emerald, + FRLG, + DP, + Pt, + HGSS, + BW, + B2W2, + XY, + AO, + AODemo, + SM, + USUM, + LGPE, + SWSH, + BDSP, + LA, + SV, + + // Side Games + Colosseum, + XD, + RSBox, + BattleRevolution, + Ranch, + Stadium1J, + Stadium1, + Stadium2, + + // Bulk Storage + Bulk3, + Bulk4, + Bulk7, +} + +public static class SaveFileTypeExtensions +{ + /// + /// Maps a stored to a . + /// + /// Actual game version to map + /// Corresponding for the given . + public static SaveFileType GetSaveFileType(this GameVersion version) => version switch + { + > GameVersion.VL => 0, + GameVersion.RD or GameVersion.GN or GameVersion.YW or GameVersion.BU => RBY, + GameVersion.GD or GameVersion.SI or GameVersion.C => GSC, + GameVersion.R or GameVersion.S => RS, + GameVersion.E => Emerald, + GameVersion.FR or GameVersion.LG => FRLG, + GameVersion.CXD => XD, + GameVersion.D or GameVersion.P => DP, + GameVersion.Pt => Pt, + GameVersion.HG or GameVersion.SS => HGSS, + GameVersion.B or GameVersion.W => BW, + GameVersion.B2 or GameVersion.W2 => B2W2, + GameVersion.X or GameVersion.Y => XY, + GameVersion.AS or GameVersion.OR => AO, + GameVersion.SN or GameVersion.MN => SM, + GameVersion.US or GameVersion.UM => USUM, + GameVersion.GP or GameVersion.GE => LGPE, + GameVersion.SW or GameVersion.SH => SWSH, + GameVersion.BD or GameVersion.SP => BDSP, + GameVersion.PLA => LA, + GameVersion.SL or GameVersion.VL => SV, + _ => None, + }; + + public static byte GetGeneration(this SaveFileType type) => type switch + { + 0 => 0, + RBY or Stadium1J or Stadium1 => 1, + GSC or Stadium2 => 2, + RS or Emerald or FRLG => 3, + DP or Pt or HGSS => 4, + BW or B2W2 => 5, + XY or AO or AODemo => 6, + SM or USUM or LGPE => 7, + SWSH or BDSP or LA => 8, + SV => 9, + Colosseum or XD or RSBox => 3, + Ranch or BattleRevolution => 4, + Bulk3 => 3, + Bulk4 => 4, + Bulk7 => 7, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; +} diff --git a/PKHeX.Core/Saves/Util/SaveFinder.cs b/PKHeX.Core/Saves/Util/SaveFinder.cs index c3d219537..8d8ece247 100644 --- a/PKHeX.Core/Saves/Util/SaveFinder.cs +++ b/PKHeX.Core/Saves/Util/SaveFinder.cs @@ -99,7 +99,7 @@ public static IEnumerable GetSwitchBackupPaths(string root) // return newest save file path that is valid var byMostRecent = possiblePaths.OrderByDescending(File.GetLastWriteTimeUtc); - var saves = byMostRecent.Select(SaveUtil.GetVariantSAV); + var saves = byMostRecent.Select(SaveUtil.GetSaveFile); return saves.FirstOrDefault(z => z?.ChecksumsValid == true); } @@ -122,8 +122,7 @@ public static IEnumerable GetSaveFiles(IReadOnlyList drives, b var byMostRecent = possiblePaths.OrderByDescending(File.GetLastWriteTimeUtc); foreach (var s in byMostRecent) { - var sav = SaveUtil.GetVariantSAV(s); - if (sav is not null) + if (SaveUtil.TryGetSaveFile(s, out var sav)) yield return sav; } } @@ -176,13 +175,13 @@ private static bool GetSaveFilePathsFromFolders(IEnumerable foldersToChe /// True if a valid save file was found, false otherwise. /// /// - public static bool TryDetectSaveFile(CancellationToken token, [NotNullWhen(true)] out SaveFile? sav) => TryDetectSaveFile(DriveList, token, out sav); + public static bool TryDetectSaveFile(CancellationToken token, [NotNullWhen(true)] out SaveFile? result) => TryDetectSaveFile(DriveList, token, out result); /// - public static bool TryDetectSaveFile(IReadOnlyList drives, CancellationToken token, [NotNullWhen(true)] out SaveFile? sav) + public static bool TryDetectSaveFile(IReadOnlyList drives, CancellationToken token, [NotNullWhen(true)] out SaveFile? result) { - sav = FindMostRecentSaveFile(drives, CustomBackupPaths, token); - var path = sav?.Metadata.FilePath; + result = FindMostRecentSaveFile(drives, CustomBackupPaths, token); + var path = result?.Metadata.FilePath; return File.Exists(path); } diff --git a/PKHeX.Core/Saves/Util/SaveUtil.cs b/PKHeX.Core/Saves/Util/SaveUtil.cs index 4777892e4..74a19b61c 100644 --- a/PKHeX.Core/Saves/Util/SaveUtil.cs +++ b/PKHeX.Core/Saves/Util/SaveUtil.cs @@ -5,7 +5,7 @@ using System.Threading; using static System.Buffers.Binary.BinaryPrimitives; using static PKHeX.Core.MessageStrings; -using static PKHeX.Core.GameVersion; +using static PKHeX.Core.SaveFileType; namespace PKHeX.Core; @@ -14,29 +14,27 @@ namespace PKHeX.Core; /// public static class SaveUtil { - public const int BEEF = 0x42454546; - - public const int SIZE_G9_0 = 0x31626F; // 1.0.0 fresh - public const int SIZE_G9_0a = 0x31627C; // 1.0.0 after multiplayer - public const int SIZE_G9_1 = 0x319DB3; // 1.0.1 fresh - public const int SIZE_G9_1a = 0x319DC0; // 1.0.1 after multiplayer - public const int SIZE_G9_3 = 0x319DC3; // 1.1.0 fresh - public const int SIZE_G9_1Ba = 0x319DD0; // 1.0.1 -> 1.1.0 - public const int SIZE_G9_1A = 0x31A2C0; // 1.0.0 -> 1.0.1 - public const int SIZE_G9_1Aa = 0x31A2CD; // 1.0.0 -> 1.0.1 -> 1.0.1 after multiplayer - public const int SIZE_G9_1Ab = 0x31A2DD; // 1.0.0 -> 1.0.1 -> 1.0.1 after multiplayer -> 1.1.0 - public const int SIZE_G9_2 = 0x31A2D0; // 1.0.0 -> 1.1.0 + private const int SIZE_G9_0 = 0x31626F; // 1.0.0 fresh + private const int SIZE_G9_0a = 0x31627C; // 1.0.0 after multiplayer + private const int SIZE_G9_1 = 0x319DB3; // 1.0.1 fresh + private const int SIZE_G9_1a = 0x319DC0; // 1.0.1 after multiplayer + private const int SIZE_G9_3 = 0x319DC3; // 1.1.0 fresh + private const int SIZE_G9_1Ba = 0x319DD0; // 1.0.1 -> 1.1.0 + private const int SIZE_G9_1A = 0x31A2C0; // 1.0.0 -> 1.0.1 + private const int SIZE_G9_1Aa = 0x31A2CD; // 1.0.0 -> 1.0.1 -> 1.0.1 after multiplayer + private const int SIZE_G9_1Ab = 0x31A2DD; // 1.0.0 -> 1.0.1 -> 1.0.1 after multiplayer -> 1.1.0 + private const int SIZE_G9_2 = 0x31A2D0; // 1.0.0 -> 1.1.0 // 1.2.0: add 0x2C9F; clean upgrade (1.1.0->1.2.0 is same as *1.2.0) - public const int SIZE_G9_3B1 = SIZE_G9_3A1 - 0xD; // BM - public const int SIZE_G9_3P1 = SIZE_G9_3B1 + 0x5; // GO (before Multiplayer) - public const int SIZE_G9_3A1 = 0x31CA6F; // 1.0.1 -> 1.1.0 -> 1.2.0 AM - public const int SIZE_G9_3G1 = SIZE_G9_3A1 + 0x5; // GO + private const int SIZE_G9_3B1 = SIZE_G9_3A1 - 0xD; // BM + private const int SIZE_G9_3P1 = SIZE_G9_3B1 + 0x5; // GO (before Multiplayer) + private const int SIZE_G9_3A1 = 0x31CA6F; // 1.0.1 -> 1.1.0 -> 1.2.0 AM + private const int SIZE_G9_3G1 = SIZE_G9_3A1 + 0x5; // GO - public const int SIZE_G9_3B0 = SIZE_G9_3A0 - 0xD; // BM - public const int SIZE_G9_3P0 = SIZE_G9_3B0 + 0x5; // GO (before Multiplayer) - public const int SIZE_G9_3A0 = 0x31CF7C; // 1.0.0 -> 1.0.1 -> 1.1.0 -> 1.2.0 AM - public const int SIZE_G9_3G0 = SIZE_G9_3A0 + 0x5; // GO + private const int SIZE_G9_3B0 = SIZE_G9_3A0 - 0xD; // BM + private const int SIZE_G9_3P0 = SIZE_G9_3B0 + 0x5; // GO (before Multiplayer) + private const int SIZE_G9_3A0 = 0x31CF7C; // 1.0.0 -> 1.0.1 -> 1.1.0 -> 1.2.0 AM + private const int SIZE_G9_3G0 = SIZE_G9_3A0 + 0x5; // GO // 2.0.1 (2.0.0 skipped): Teal Mask // 3.0.0: The Indigo Disk @@ -48,22 +46,22 @@ public static class SaveUtil private const int SIZE_G9_202 = 0xC8E; // Add 2 blocks (1 obj 0xC80, 1 bool) = 4{key}1{obj}4{len} + 4{key}1{boolT/boolF} private const int SIZE_G9_300 = 0x83AD; - public const int SIZE_G8LA = 0x136DDE; - public const int SIZE_G8LA_1 = 0x13AD06; + private const int SIZE_G8LA = 0x136DDE; + private const int SIZE_G8LA_1 = 0x13AD06; - public const int SIZE_G8BDSP = 0xE9828; + public const int SIZE_G8BDSP_0 = 0xE9828; public const int SIZE_G8BDSP_1 = 0xEDC20; public const int SIZE_G8BDSP_2 = 0xEED8C; public const int SIZE_G8BDSP_3 = 0xEF0A4; - public const int SIZE_G8SWSH = 0x1716B3; // 1.0 - public const int SIZE_G8SWSH_1 = 0x17195E; // 1.0 -> 1.1 - public const int SIZE_G8SWSH_2 = 0x180B19; // 1.0 -> 1.1 -> 1.2 - public const int SIZE_G8SWSH_2B = 0x180AD0; // 1.0 -> 1.2 - public const int SIZE_G8SWSH_3 = 0x1876B1; // 1.0 -> 1.1 -> 1.2 -> 1.3 - public const int SIZE_G8SWSH_3A = 0x187693; // 1.0 -> 1.1 -> 1.3 - public const int SIZE_G8SWSH_3B = 0x187668; // 1.0 -> 1.2 -> 1.3 - public const int SIZE_G8SWSH_3C = 0x18764A; // 1.0 -> 1.3 + private const int SIZE_G8SWSH = 0x1716B3; // 1.0 + private const int SIZE_G8SWSH_1 = 0x17195E; // 1.0 -> 1.1 + private const int SIZE_G8SWSH_2 = 0x180B19; // 1.0 -> 1.1 -> 1.2 + private const int SIZE_G8SWSH_2B = 0x180AD0; // 1.0 -> 1.2 + private const int SIZE_G8SWSH_3 = 0x1876B1; // 1.0 -> 1.1 -> 1.2 -> 1.3 + private const int SIZE_G8SWSH_3A = 0x187693; // 1.0 -> 1.1 -> 1.3 + private const int SIZE_G8SWSH_3B = 0x187668; // 1.0 -> 1.2 -> 1.3 + private const int SIZE_G8SWSH_3C = 0x18764A; // 1.0 -> 1.3 public const int SIZE_G7GG = 0x100000; public const int SIZE_G7USUM = 0x6CC00; @@ -145,7 +143,7 @@ or SIZE_G8SWSH_1 private static bool IsSizeCommonFixed(int length) => length is SIZE_G8LA or SIZE_G8LA_1 - or SIZE_G8BDSP or SIZE_G8BDSP_1 or SIZE_G8BDSP_2 or SIZE_G8BDSP_3 + or SIZE_G8BDSP_0 or SIZE_G8BDSP_1 or SIZE_G8BDSP_2 or SIZE_G8BDSP_3 or SIZE_G7SM or SIZE_G7USUM or SIZE_G7GG or SIZE_G6XY or SIZE_G6ORAS or SIZE_G6ORASDEMO or SIZE_G5RAW or SIZE_G5BW or SIZE_G5B2W2 @@ -156,63 +154,57 @@ or SIZE_G2RAW_U /// Determines the type of the provided save data. /// Save data of which to determine the origins of - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetSAVType(ReadOnlySpan data) + /// Save file type information including sub-version details, or Invalid if type cannot be determined. + private static SaveTypeInfo GetTypeInfo(ReadOnlySpan data) { - GameVersion version; - if ((version = GetIsG1SAV(data)) != Invalid) - return version; - if ((version = GetIsG2SAV(data)) != Invalid) - return version; - if ((version = GetIsG3SAV(data)) != Invalid) - return version; - if ((version = GetIsG4SAV(data)) != Invalid) - return version; - if ((version = GetIsG5SAV(data)) != Invalid) - return version; - if ((version = GetIsG6SAV(data)) != Invalid) - return version; - if ((version = GetIsG7SAV(data)) != Invalid) - return version; + // Mainline + if (IsG1(data, out var info)) return info; + if (IsG2(data, out info)) return info; + if (IsG3(data, out var smallOffset)) return GetVersionG3SAV(data[smallOffset..]); + if (IsG4DP(data)) return DP; + if (IsG4Pt(data)) return Pt; + if (IsG4HGSS(data)) return HGSS; + if (IsG5BW(data)) return BW; + if (IsG5B2W2(data)) return B2W2; + if (IsG6XY(data)) return XY; + if (IsG6AO(data)) return AO; + if (IsG6AODemo(data)) return AODemo; + if (IsG7SM(data)) return SM; + if (IsG7USUM(data)) return USUM; + if (IsG7LGPE(data)) return LGPE; + if (IsG8SWSH(data)) return SWSH; + if (IsG8BDSP(data)) return BDSP; + if (IsG8LA(data)) return LA; + if (IsG9SV(data)) return SV; - if (GetIsBelugaSAV(data) != Invalid) - return GG; - if (GetIsG3COLOSAV(data) != Invalid) - return COLO; - if (GetIsG3XDSAV(data) != Invalid) - return XD; - if (GetIsG3BOXSAV(data) != Invalid) - return RSBOX; - if (GetIsG4BRSAV(data) != Invalid) - return BATREV; + // Side-game + if (IsG3Colosseum(data)) return Colosseum; + if (IsG3XD(data)) return XD; + if (IsG3RSBox(data)) return RSBox; + if (IsG4BR(data)) return BattleRevolution; - if (GetIsBank7(data)) // pokebank - return Gen7; - if (GetIsBank4(data)) // pokestock - return Gen4; - if (GetIsBank3(data)) // pokestock - return Gen3; - if (GetIsRanch4(data)) // ranch - return DPPt; - if (SAV2Stadium.IsStadium(data)) - return Stadium2; - if (SAV1Stadium.IsStadium(data)) - return Stadium; - if (SAV1StadiumJ.IsStadium(data)) - return StadiumJ; + // Adjacent/misc. + if (IsBank7(data)) return Bulk7; // pokebank + if (IsBank4(data)) return Bulk4; // pokestock + if (IsBank3(data)) return Bulk3; // pokestock + if (IsRanch4(data)) return Ranch; + if (SAV2Stadium.IsStadium(data)) return Stadium2; + if (SAV1Stadium.IsStadium(data)) return Stadium1; + if (SAV1StadiumJ.IsStadium(data)) return Stadium1J; - if ((version = GetIsG8SAV(data)) != Invalid) - return version; - if ((version = GetIsG8SAV_BDSP(data)) != Invalid) - return version; - if ((version = GetIsG8SAV_LA(data)) != Invalid) - return version; - if ((version = GetIsG9SAV(data)) != Invalid) - return version; - - return Invalid; + return SaveTypeInfo.Invalid; } + private static bool IsG1INT(ReadOnlySpan data) => HasListAt(data, 0x2F2C, 0x30C0, 20); + private static bool IsG1JPN(ReadOnlySpan data) => HasListAt(data, 0x2ED5, 0x302D, 30); + private static bool IsG2GSINT(ReadOnlySpan data) => HasListAt(data, 0x288A, 0x2D6C, 20); + private static bool IsG2GSJPN(ReadOnlySpan data) => HasListAt(data, 0x2D10, 0x283E, 30); + private static bool IsG2GSKOR(ReadOnlySpan data) => HasListAt(data, 0x2DAE, 0x28CC, 20); + private static bool IsG2CrystalINT(ReadOnlySpan data) => HasListAt(data, 0x2865, 0x2D10, 20); + private static bool IsG2CrystalJPN(ReadOnlySpan data) => HasListAt(data, 0x283E, 0x281A, 30); + private static bool HasListAt(ReadOnlySpan data, [ConstantExpected] int offset1, [ConstantExpected] int offset2, [ConstantExpected] byte maxCount) => + IsListValidG12(data, offset1, maxCount) && IsListValidG12(data, offset2, maxCount); + /// /// Determines if a Gen1/2 Pokémon List is Invalid /// @@ -220,7 +212,7 @@ private static GameVersion GetSAVType(ReadOnlySpan data) /// Offset the list starts at /// Max count of Pokémon in the list /// True if a valid list, False otherwise - private static bool IsG12ListValid(ReadOnlySpan data, int offset, [ConstantExpected] byte maxCount) + private static bool IsListValidG12(ReadOnlySpan data, int offset, [ConstantExpected] byte maxCount) { byte count = data[offset]; return count <= maxCount && data[offset + 1 + count] == 0xFF; @@ -228,110 +220,70 @@ private static bool IsG12ListValid(ReadOnlySpan data, int offset, [Constan /// Checks to see if the data belongs to a Gen1 save /// Save data of which to determine the type + /// Recognized save file type, if any. /// Version Identifier or Invalid if type cannot be determined. - internal static GameVersion GetIsG1SAV(ReadOnlySpan data) + private static bool IsG1(ReadOnlySpan data, out SaveTypeInfo info) { + info = default; if (data.Length is not SIZE_G1RAW) - return Invalid; + return false; - // Check if it's not an american save or a japanese save - if (!(GetIsG1SAVU(data) || GetIsG1SAVJ(data))) - return Invalid; - // I can't actually detect which game version, because it's not stored anywhere. - // If you can think of anything to do here, please implement :) - return RBY; - } + // Check if it's not an international save or a Japanese save + if (IsG1JPN(data)) + { info = new SaveTypeInfo(RBY, SAV1.IsYellowJPN(data) ? GameVersion.YW : default, LanguageID.Japanese); return true; } + if (IsG1INT(data)) + { info = new SaveTypeInfo(RBY, SAV1.IsYellowINT(data) ? GameVersion.YW : default); return true; } - /// Checks to see if the data belongs to an International Gen1 save - /// Save data of which to determine the region - /// True if a valid International save, False otherwise. - private static bool GetIsG1SAVU(ReadOnlySpan data) - { - return IsG12ListValid(data, 0x2F2C, 20) && IsG12ListValid(data, 0x30C0, 20); - } - - /// Checks to see if the data belongs to a Japanese Gen1 save - /// Save data of which to determine the region - /// True if a valid Japanese save, False otherwise. - internal static bool GetIsG1SAVJ(ReadOnlySpan data) - { - return IsG12ListValid(data, 0x2ED5, 30) && IsG12ListValid(data, 0x302D, 30); + return false; } /// Checks to see if the data belongs to a Gen2 save /// Save data of which to determine the type + /// Recognized save file type, if any. /// Version Identifier or Invalid if type cannot be determined. - internal static GameVersion GetIsG2SAV(ReadOnlySpan data) + private static bool IsG2(ReadOnlySpan data, out SaveTypeInfo info) { + info = default; if (!IsSizeGen2(data.Length)) - return Invalid; + return false; // Check if it's not an International, Japanese, or Korean save file - GameVersion result; - if ((result = GetIsG2SAVU(data)) != Invalid) - return result; - if ((result = GetIsG2SAVJ(data)) != Invalid) - return result; - if ((result = GetIsG2SAVK(data)) != Invalid) - return result; - return Invalid; - } + // International + if (IsG2GSINT(data)) + { info = new SaveTypeInfo(GSC, GameVersion.GS); return true; } + if (IsG2CrystalINT(data)) + { info = new SaveTypeInfo(GSC, GameVersion.C); return true; } - /// Checks to see if the data belongs to an International (not Japanese or Korean) Gen2 save - /// Save data of which to determine the region - /// True if a valid International save, False otherwise. - private static GameVersion GetIsG2SAVU(ReadOnlySpan data) - { - if (IsG12ListValid(data, 0x288A, 20) && IsG12ListValid(data, 0x2D6C, 20)) - return GS; - if (IsG12ListValid(data, 0x2865, 20) && IsG12ListValid(data, 0x2D10, 20)) - return C; - return Invalid; - } + // Japanese + if (IsG2GSJPN(data)) + { info = new SaveTypeInfo(GSC, GameVersion.GS, LanguageID.Japanese); return true; } + if (IsG2CrystalJPN(data)) + { info = new SaveTypeInfo(GSC, GameVersion.C, LanguageID.Japanese); return true; } - /// Checks to see if the data belongs to a Japanese Gen2 save - /// Save data of which to determine the region - /// True if a valid Japanese save, False otherwise. - internal static GameVersion GetIsG2SAVJ(ReadOnlySpan data) - { - if (!IsG12ListValid(data, 0x2D10, 30)) - return Invalid; - if (IsG12ListValid(data, 0x283E, 30)) - return GS; - if (IsG12ListValid(data, 0x281A, 30)) - return C; - return Invalid; - } - - /// Checks to see if the data belongs to a Korean Gen2 save - /// Save data of which to determine the region - /// True if a valid Korean save, False otherwise. - internal static GameVersion GetIsG2SAVK(ReadOnlySpan data) - { - if (IsG12ListValid(data, 0x2DAE, 20) && IsG12ListValid(data, 0x28CC, 20)) - return GS; - return Invalid; + // Korean + if (IsG2GSKOR(data)) + { info = new SaveTypeInfo(GSC, GameVersion.GS, LanguageID.Korean); return true; } + return false; } /// Checks to see if the data belongs to a Gen3 save /// Save data of which to determine the type + /// Offset to the small sector of the save file, if applicable. /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG3SAV(ReadOnlySpan data) + private static bool IsG3(ReadOnlySpan data, out int smallOffset) { + smallOffset = 0; if (data.Length is not SIZE_G3RAW) - return Invalid; + return false; // check the save file(s) int count = data.Length/SIZE_G3RAWHALF; for (int slot = 0; slot < count; slot++) { - if (!SAV3.IsAllMainSectorsPresent(data, slot, out var smallOffset)) - continue; - - // Detect RS/E/FRLG - return GetVersionG3SAV(data[smallOffset..]); + if (SAV3.IsAllMainSectorsPresent(data, slot, out smallOffset)) + return true; } - return Invalid; + return false; } /// @@ -339,7 +291,7 @@ private static GameVersion GetIsG3SAV(ReadOnlySpan data) /// /// Data to check /// RS, E, or FR/LG. - private static GameVersion GetVersionG3SAV(ReadOnlySpan data) + private static SaveFileType GetVersionG3SAV(ReadOnlySpan data) { // 0xAC // RS: Battle Tower Data, which will never match the FR/LG fixed value. @@ -355,7 +307,7 @@ private static GameVersion GetVersionG3SAV(ReadOnlySpan data) // RS data structure only extends 0x890 bytes; check if any data is present afterward. var remainder = data[0x890..0xF2C]; if (remainder.ContainsAnyExcept(0)) - return E; + return Emerald; return RS; } } @@ -363,26 +315,26 @@ private static GameVersion GetVersionG3SAV(ReadOnlySpan data) /// Checks to see if the data belongs to a Gen3 Box RS save /// Save data of which to determine the type /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG3BOXSAV(ReadOnlySpan data) + private static bool IsG3RSBox(ReadOnlySpan data) { if (data.Length is not SIZE_G3BOX) - return Invalid; + return false; // Verify first checksum const int offset = 0x2000; var span = data.Slice(offset, 0x1FFC); var actual = ReadUInt32BigEndian(span); var chk = Checksums.CheckSum16BigInvert(span[4..]); - return chk == actual ? RSBOX : Invalid; + return chk == actual; } /// Checks to see if the data belongs to a Colosseum save /// Save data of which to determine the type /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG3COLOSAV(ReadOnlySpan data) + private static bool IsG3Colosseum(ReadOnlySpan data) { if (data.Length is not SIZE_G3COLO) - return Invalid; + return false; // Check the intro bytes for each save slot const int offset = 0x6000; @@ -390,18 +342,18 @@ private static GameVersion GetIsG3COLOSAV(ReadOnlySpan data) { var ofs = offset + (0x1E000 * i); if (ReadUInt32LittleEndian(data[ofs..]) != 0x00000101) - return Invalid; + return false; } - return COLO; + return true; } /// Checks to see if the data belongs to a Gen3 XD save /// Save data of which to determine the type /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG3XDSAV(ReadOnlySpan data) + private static bool IsG3XD(ReadOnlySpan data) { if (data.Length is not SIZE_G3XD) - return Invalid; + return false; // Check the intro bytes for each save slot const int offset = 0x6000; @@ -409,225 +361,165 @@ private static GameVersion GetIsG3XDSAV(ReadOnlySpan data) { var ofs = offset + (0x28000 * i); if ((ReadUInt32LittleEndian(data[ofs..]) & 0xFFFE_FFFF) != 0x00000101) - return Invalid; + return false; } - return XD; + return true; } - /// Checks to see if the data belongs to a Gen4 save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG4SAV(ReadOnlySpan data) - { - if (data.Length != SIZE_G4RAW) - return Invalid; + private static bool IsG4DP(ReadOnlySpan data) => data.Length == SIZE_G4RAW && IsValidGeneralFooter2(data, SAV4DP.GeneralSize); + private static bool IsG4Pt(ReadOnlySpan data) => data.Length == SIZE_G4RAW && IsValidGeneralFooter2(data, SAV4Pt.GeneralSize); + private static bool IsG4HGSS(ReadOnlySpan data) => data.Length == SIZE_G4RAW && IsValidGeneralFooter2(data, SAV4HGSS.GeneralSize); + private static bool IsG4BR(ReadOnlySpan data) => data.Length == SIZE_G4BR && SAV4BR.IsValidSaveFile(data); + private static bool IsG5BW(ReadOnlySpan data) => data.Length == SIZE_G5BW && IsValidFooter5(data, SIZE_G5BW, 0x8C); + private static bool IsG5B2W2(ReadOnlySpan data) => data.Length == SIZE_G5B2W2 && IsValidFooter5(data, SIZE_G5B2W2, 0x94); + private static bool IsG6XY(ReadOnlySpan data) => data.Length == SIZE_G6XY && HasSaveFooterBEEF(data); + private static bool IsG6AO(ReadOnlySpan data) => data.Length == SIZE_G6ORAS && HasSaveFooterBEEF(data); + private static bool IsG6AODemo(ReadOnlySpan data) => data.Length == SIZE_G6ORASDEMO && HasSaveFooterBEEF(data); + private static bool IsG7SM(ReadOnlySpan data) => data.Length is (SIZE_G7SM) && HasSaveFooterBEEF(data); + private static bool IsG7USUM(ReadOnlySpan data) => data.Length is (SIZE_G7USUM) && HasSaveFooterBEEF(data); + private static bool IsValidGeneralFooter2(ReadOnlySpan data, [ConstantExpected] int length) + { // Check the other save -- first save is done to the latter half of the binary. // The second save should be all that is needed to check. const int generalOffset = 0x40000; - if (IsValidGeneralFooter(data.Slice(generalOffset, SAV4DP.GeneralSize))) - return DP; - if (IsValidGeneralFooter(data.Slice(generalOffset, SAV4Pt.GeneralSize))) - return Pt; - if (IsValidGeneralFooter(data.Slice(generalOffset, SAV4HGSS.GeneralSize))) - return HGSS; - - return Invalid; + var general = data.Slice(generalOffset, length); // The block footers contain a 32-bit 'size' followed by a 32-bit binary-coded-decimal timestamp // Korean saves have a different timestamp from other localizations. - static bool IsValidGeneralFooter(ReadOnlySpan general) - { - var size = ReadUInt32LittleEndian(general[^0xC..]); - if (size != general.Length) - return false; - var sdk = ReadUInt32LittleEndian(general[^0x8..]); - return sdk is SAV4.MAGIC_JAPAN_INTL or SAV4.MAGIC_KOREAN; - } + var size = ReadUInt32LittleEndian(general[^0xC..]); + if (size != general.Length) + return false; + var sdk = ReadUInt32LittleEndian(general[^0x8..]); + return sdk is SAV4.MAGIC_JAPAN_INTL or SAV4.MAGIC_KOREAN; } - /// Checks to see if the data belongs to a Gen4 Battle Revolution save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG4BRSAV(ReadOnlySpan data) + private static bool IsValidFooter5(ReadOnlySpan data, int mainSize, int infoLength) { - if (data.Length != SIZE_G4BR) - return Invalid; - - return SAV4BR.IsValidSaveFile(data) ? BATREV : Invalid; + var footer = data.Slice(mainSize - 0x100, infoLength + 0x10); + var stored = ReadUInt16LittleEndian(footer[^2..]); + var actual = Checksums.CRC16_CCITT(footer[..infoLength]); + return stored == actual; } - /// Checks to see if the data belongs to a Gen5 save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG5SAV(ReadOnlySpan data) - { - if (data.Length != SIZE_G5RAW) - return Invalid; - - // check the checksum footer block validity; nobody would normally modify this region - if (IsValidFooter(data, SIZE_G5BW, 0x8C)) - return BW; - if (IsValidFooter(data, SIZE_G5B2W2, 0x94)) - return B2W2; - return Invalid; - - static bool IsValidFooter(ReadOnlySpan data, int mainSize, int infoLength) - { - var footer = data.Slice(mainSize - 0x100, infoLength + 0x10); - ushort stored = ReadUInt16LittleEndian(footer[^2..]); - ushort actual = Checksums.CRC16_CCITT(footer[..infoLength]); - return stored == actual; - } - } - - /// Checks to see if the data belongs to a Gen6 save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG6SAV(ReadOnlySpan data) - { - if (data.Length is not (SIZE_G6XY or SIZE_G6ORAS or SIZE_G6ORASDEMO)) - return Invalid; - - if (ReadUInt32LittleEndian(data[^0x1F0..]) != BEEF) - return Invalid; - - return data.Length switch - { - SIZE_G6XY => XY, - SIZE_G6ORAS => ORAS, - _ => ORASDEMO, // least likely - }; - } - - /// Checks to see if the data belongs to a Gen7 save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG7SAV(ReadOnlySpan data) - { - if (data.Length is not (SIZE_G7SM or SIZE_G7USUM)) - return Invalid; - - if (ReadUInt32LittleEndian(data[^0x1F0..]) != BEEF) - return Invalid; - - return data.Length == SIZE_G7SM ? SM : USUM; - } + private static bool HasSaveFooterBEEF(ReadOnlySpan data) => ReadUInt32LittleEndian(data[^0x1F0..]) == 0x42454546; // BEEF /// Determines if the input data belongs to a save /// Save data of which to determine the type /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsBelugaSAV(ReadOnlySpan data) + private static bool IsG7LGPE(ReadOnlySpan data) { if (data.Length != SIZE_G7GG) - return Invalid; + return false; + data = data[..0xB8800]; const int actualLength = 0xB8800; - if (ReadUInt32LittleEndian(data[(actualLength - 0x1F0)..]) != BEEF) // beef table start - return Invalid; + if (!HasSaveFooterBEEF(data)) + return false; if (ReadUInt16LittleEndian(data[(actualLength - 0x200 + 0xB0)..]) != 0x13) // check a block number to double-check - return Invalid; + return false; - return GG; + return true; } - /// Checks to see if the data belongs to a Gen8 save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG8SAV(ReadOnlySpan data) + private static bool IsG8BDSP(ReadOnlySpan data) => data.Length switch { - if (!IsSizeGen8SWSH(data.Length)) - return Invalid; + SIZE_G8BDSP_0 => (Gem8Version)ReadUInt32LittleEndian(data) == Gem8Version.V1_0, + SIZE_G8BDSP_1 => (Gem8Version)ReadUInt32LittleEndian(data) == Gem8Version.V1_1, + SIZE_G8BDSP_2 => (Gem8Version)ReadUInt32LittleEndian(data) == Gem8Version.V1_2, + SIZE_G8BDSP_3 => (Gem8Version)ReadUInt32LittleEndian(data) == Gem8Version.V1_3, + _ => false + }; - return SwishCrypto.GetIsHashValid(data) ? SWSH : Invalid; - } + private static bool IsG8LA(ReadOnlySpan data) => data.Length is SIZE_G8LA or SIZE_G8LA_1 && SwishCrypto.GetIsHashValid(data); + private static bool IsG8SWSH(ReadOnlySpan data) => IsSizeGen8SWSH(data.Length) && SwishCrypto.GetIsHashValid(data); + private static bool IsG9SV(ReadOnlySpan data) => IsSizeGen9SV(data.Length) && SwishCrypto.GetIsHashValid(data); - private static GameVersion GetIsG8SAV_BDSP(ReadOnlySpan data) - { - if (data.Length is not (SIZE_G8BDSP or SIZE_G8BDSP_1 or SIZE_G8BDSP_2 or SIZE_G8BDSP_3)) - return Invalid; - - var version = (Gem8Version)ReadUInt32LittleEndian(data); - if (version is not (Gem8Version.V1_0 or Gem8Version.V1_1 or Gem8Version.V1_2 or Gem8Version.V1_3)) - return Invalid; - - return BDSP; - } - - private static GameVersion GetIsG8SAV_LA(ReadOnlySpan data) - { - if (data.Length is not (SIZE_G8LA or SIZE_G8LA_1)) - return Invalid; - - return SwishCrypto.GetIsHashValid(data) ? PLA : Invalid; - } - - /// Checks to see if the data belongs to a Gen8 save - /// Save data of which to determine the type - /// Version Identifier or Invalid if type cannot be determined. - private static GameVersion GetIsG9SAV(ReadOnlySpan data) - { - if (!IsSizeGen9SV(data.Length)) - return Invalid; - - return SwishCrypto.GetIsHashValid(data) ? SV : Invalid; - } - - private static bool GetIsBank7(ReadOnlySpan data) => data.Length == SIZE_G7BANK && data[0] != 0; - private static bool GetIsBank4(ReadOnlySpan data) => data.Length == SIZE_G4BANK && ReadUInt32LittleEndian(data[0x3FC00..]) != 0; // box name present - private static bool GetIsBank3(ReadOnlySpan data) => data.Length == SIZE_G4BANK && ReadUInt32LittleEndian(data[0x3FC00..]) == 0; // size collision with ^ - private static bool GetIsRanchDP(ReadOnlySpan data) => data.Length == SIZE_G4RANCH && ReadUInt32BigEndian(data[0x22AC..]) != 0; - private static bool GetIsRanchPlat(ReadOnlySpan data) => data.Length == SIZE_G4RANCH_PLAT && ReadUInt32BigEndian(data[0x268C..]) != 0; - private static bool GetIsRanch4(ReadOnlySpan data) => GetIsRanchDP(data) || GetIsRanchPlat(data); + private static bool IsBank7(ReadOnlySpan data) => data.Length == SIZE_G7BANK && data[0] != 0; + private static bool IsBank4(ReadOnlySpan data) => data.Length == SIZE_G4BANK && ReadUInt32LittleEndian(data[0x3FC00..]) != 0; // box name present + private static bool IsBank3(ReadOnlySpan data) => data.Length == SIZE_G4BANK && ReadUInt32LittleEndian(data[0x3FC00..]) == 0; // size collision with ^ + private static bool IsRanchDP(ReadOnlySpan data) => data.Length == SIZE_G4RANCH && ReadUInt32BigEndian(data[0x22AC..]) != 0; + private static bool IsRanchPlat(ReadOnlySpan data) => data.Length == SIZE_G4RANCH_PLAT && ReadUInt32BigEndian(data[0x268C..]) != 0; + private static bool IsRanch4(ReadOnlySpan data) => IsRanchDP(data) || IsRanchPlat(data); /// Creates an instance of a SaveFile using the given save data. /// File location from which to create a SaveFile. - /// An appropriate type of save file for the given data, or null if the save data is invalid. - public static SaveFile? GetVariantSAV(string path) + /// Save file loaded from the given path, or null if loading failed. + public static bool TryGetSaveFile(string path, [NotNullWhen(true)] out SaveFile? result) { // Many things can go wrong with loading save data (file no longer present toc-tou, or bad save layout). try { var data = File.ReadAllBytes(path); - var sav = GetVariantSAV(data, path); - if (sav is null) - return null; + if (!TryGetSaveFile(data, out result, path)) + return false; - sav.Metadata.SetExtraInfo(path); - if (sav.Generation <= 3) - SaveLanguage.TryRevise(sav); - return sav; + result.Metadata.SetExtraInfo(path); + if (result.Generation <= 3) + SaveLanguage.TryRevise(result); + return true; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); - return null; + result = null; + return false; } } + /// Creates an instance of a SaveFile using the given save data. + /// File location from which to create a SaveFile. + /// An appropriate type of save file for the given data, or null if the save data is invalid. + public static SaveFile? GetSaveFile(string path) => TryGetSaveFile(path, out var result) ? result : null; + /// Creates an instance of a SaveFile using the given save data. /// Save data from which to create a SaveFile. + /// File path, may help initialize a non-standard save file format. + /// An appropriate type of save file for the given data, or null if the save data is invalid. + public static SaveFile? GetSaveFile(Memory data, string? path = null) => TryGetSaveFile(data, out var result, path) ? result : null; + + /// Creates an instance of a SaveFile using the given save data. + /// Save data from which to create a SaveFile. + /// Save file loaded from the given data, or null if loading failed. /// Optional save file path, may help initialize a non-standard save file format. /// An appropriate type of save file for the given data, or null if the save data is invalid. - public static SaveFile? GetVariantSAV(Memory data, string? path = null) + public static bool TryGetSaveFile(Memory data, [NotNullWhen(true)] out SaveFile? result, string? path = null) { #if !EXCLUDE_HACKS + if (TryGetSaveFileCustom(data, out result, path)) + return true; +#endif + + result = GetSaveFileInternal(data); + if (result is not null) + return true; + +#if !EXCLUDE_EMULATOR_FORMATS + if (TryGetSaveFileHandler(data, out result, path)) + return true; +#endif + + // unrecognized. + return false; + } + + private static bool TryGetSaveFileCustom(Memory data, [NotNullWhen(true)] out SaveFile? result, string? path) + { foreach (var h in CustomSaveReaders) { if (!h.IsRecognized(data.Length)) continue; - var custom = h.ReadSaveFile(data, path); - if (custom is not null) - return custom; + result = h.ReadSaveFile(data, path); + if (result is not null) + return true; } -#endif + result = null; + return false; + } - var sav = GetVariantSAVInternal(data); - if (sav is not null) - return sav; - -#if !EXCLUDE_EMULATOR_FORMATS + private static bool TryGetSaveFileHandler(Memory data, [NotNullWhen(true)] out SaveFile? result, string? path) + { foreach (var h in Handlers) { if (!h.IsRecognized(data.Length)) @@ -637,85 +529,88 @@ private static GameVersion GetIsG9SAV(ReadOnlySpan data) if (split is null) continue; - sav = GetVariantSAVInternal(split.Data); - if (sav is null) + result = GetSaveFileInternal(split.Data); + if (result is null) continue; - var meta = sav.Metadata; + var meta = result.Metadata; meta.SetExtraInfo(split.Header, split.Footer, split.Handler); if (path is not null) meta.SetExtraInfo(path); - return sav; + return true; } -#endif - - // unrecognized. - return null; + result = null; + return false; } - private static SaveFile? GetVariantSAVInternal(Memory data) + private static SaveFile? GetSaveFileInternal(Memory data) { - var type = GetSAVType(data.Span); - return type switch - { - // Main Games - RBY => new SAV1(data, type), - GS or C => new SAV2(data, type), - - RS => new SAV3RS(data), - E => new SAV3E(data), - FRLG => new SAV3FRLG(data), - - DP => new SAV4DP(data), - Pt => new SAV4Pt(data), - HGSS => new SAV4HGSS(data), - - BW => new SAV5BW(data), - B2W2 => new SAV5B2W2(data), - - XY => new SAV6XY(data), - ORAS => new SAV6AO(data), - ORASDEMO => new SAV6AODemo(data), - - SM => new SAV7SM(data), - USUM => new SAV7USUM(data), - GG => new SAV7b(data), - - SWSH => new SAV8SWSH(data), - BDSP => new SAV8BS(data), - PLA => new SAV8LA(data), - - SV => new SAV9SV(data), - - // Side Games - COLO => new SAV3Colosseum(data), - XD => new SAV3XD(data), - RSBOX => new SAV3RSBox(data), - BATREV => new SAV4BR(data), - Stadium2 => new SAV2Stadium(data), - Stadium => new SAV1Stadium(data), - StadiumJ => new SAV1StadiumJ(data), - - // Bulk Storage - Gen3 => new Bank3(data), - DPPt => new SAV4Ranch(data), - Gen4 => new Bank4(data), - Gen7 => Bank7.GetBank7(data), - - // No pattern matched - _ => null, - }; + var typeInfo = GetTypeInfo(data.Span); + return GetSaveFileInternal(data, typeInfo); } - public static SaveFile? GetVariantSAV(SAV3GCMemoryCard memCard) + private static SaveFile? GetSaveFileInternal(Memory data, SaveTypeInfo typeInfo) => typeInfo.Type switch + { + // Main Games + RBY => new SAV1(data, typeInfo.Language, typeInfo.SubVersion), + GSC => new SAV2(data, typeInfo.Language, typeInfo.SubVersion), + + RS => new SAV3RS(data), + Emerald => new SAV3E(data), + FRLG => new SAV3FRLG(data), + + DP => new SAV4DP(data), + Pt => new SAV4Pt(data), + HGSS => new SAV4HGSS(data), + + BW => new SAV5BW(data), + B2W2 => new SAV5B2W2(data), + + XY => new SAV6XY(data), + AO => new SAV6AO(data), + AODemo => new SAV6AODemo(data), + + SM => new SAV7SM(data), + USUM => new SAV7USUM(data), + LGPE => new SAV7b(data), + + SWSH => new SAV8SWSH(data), + BDSP => new SAV8BS(data), + LA => new SAV8LA(data), + + SV => new SAV9SV(data), + + // Side Games + Colosseum => new SAV3Colosseum(data), + XD => new SAV3XD(data), + RSBox => new SAV3RSBox(data), + BattleRevolution => new SAV4BR(data), + Stadium2 => new SAV2Stadium(data), + Stadium1 => new SAV1Stadium(data), + Stadium1J => new SAV1StadiumJ(data), + + // Bulk Storage + Ranch => new SAV4Ranch(data), + Bulk3 => new Bank3(data), + Bulk4 => new Bank4(data), + Bulk7 => Bank7.GetBank7(data), + + // No pattern matched + _ => null, + }; + + public static bool TryGetSaveFile(SAV3GCMemoryCard memCard, [NotNullWhen(true)] out SaveFile? result) { // Pre-check for header/footer signatures if (memCard.IsNoGameSelected) memCard.GetMemoryCardState(); - var memory = memCard.ReadSaveGameData().ToArray(); - if (memory.Length == 0) - return null; + result = null; + var peek = memCard.ReadSaveGameData(); + if (peek.Length == 0) + return false; + + var memory = peek.ToArray(); var split = DolphinHandler.TrySplit(memory); var data = split?.Data ?? memory; @@ -723,125 +618,18 @@ private static GameVersion GetIsG9SAV(ReadOnlySpan data) switch (memCard.SelectedGameVersion) { // Side Games - case COLO: sav = new SAV3Colosseum(data) { MemoryCard = memCard }; break; + case Colosseum: sav = new SAV3Colosseum(data) { MemoryCard = memCard }; break; case XD: sav = new SAV3XD(data) { MemoryCard = memCard }; break; - case RSBOX: sav = new SAV3RSBox(data) { MemoryCard = memCard }; break; + case RSBox: sav = new SAV3RSBox(data) { MemoryCard = memCard }; break; // No pattern matched - default: return null; + default: return false; } if (split is not null) sav.Metadata.SetExtraInfo(split.Header, split.Footer, split.Handler); - return sav; - } - - /// - /// Returns a that feels best for the save file's language. - /// - public static LanguageID GetSafeLanguage(SaveFile? sav) => sav switch - { - null => LanguageID.English, - ILangDeviantSave s => s.Japanese ? LanguageID.Japanese : s.Korean ? LanguageID.Korean : LanguageID.English, - _ => (uint)sav.Language <= Legal.GetMaxLanguageID(sav.Generation) ? (LanguageID)sav.Language : LanguageID.English, - }; - - /// - /// Returns a Trainer Name that feels best for the save file's language. - /// - public static string GetSafeTrainerName(SaveFile? sav, LanguageID lang) => lang switch - { - LanguageID.Japanese => sav?.Generation >= 3 ? TrainerName.ProgramJPN : TrainerName.GameFreakJPN, - _ => TrainerName.ProgramINT, - }; - - /// - /// Creates an instance of a SaveFile with a blank base. - /// - /// Version to create the save file for. - /// Trainer Name - /// Language to initialize with - /// Blank save file from the requested game, null if no game exists for that . - public static SaveFile GetBlankSAV(GameVersion game, string trainerName, LanguageID language = LanguageID.English) - { - var sav = GetBlankSAV(game, language); - sav.Version = game; - sav.OT = trainerName; - if (sav.Generation >= 4) - sav.Language = (int)language; - - // Secondary Properties may not be used but can be filled in as template. - (uint tid, uint sid) = sav.Generation >= 7 ? (123456u, 1234u) : (12345u, 54321u); - sav.SetDisplayID(tid, sid); - sav.Language = (int)language; - - // Only set geolocation data for 3DS titles - if (sav is IRegionOrigin o) - o.SetDefaultRegionOrigins((int)language); - - return sav; - } - - /// - /// Creates an instance of a SaveFile with a blank base. - /// - /// Version to create the save file for. - /// Save file language to initialize for - /// Blank save file from the requested game, null if no game exists for that . - private static SaveFile GetBlankSAV(GameVersion game, LanguageID language) => game switch - { - RD or BU or GN or YW or RBY => new SAV1(version: game, game == BU ? LanguageID.Japanese : language), - StadiumJ => new SAV1StadiumJ(), - Stadium => new SAV1Stadium(language == LanguageID.Japanese), - - GD or SI or GS => new SAV2(version: GS, language: language), - C or GSC => new SAV2(version: C, language: language), - Stadium2 => new SAV2Stadium(language == LanguageID.Japanese), - - R or S or RS => new SAV3RS(language == LanguageID.Japanese), - E or RSE => new SAV3E(language == LanguageID.Japanese), - FR or LG or FRLG => new SAV3FRLG(language == LanguageID.Japanese), - - CXD or COLO => new SAV3Colosseum(), - XD => new SAV3XD(), - RSBOX => new SAV3RSBox(), - - D or P or DP => new SAV4DP(), - Pt or DPPt => new SAV4Pt(), - HG or SS or HGSS => new SAV4HGSS(), - BATREV => new SAV4BR(), - - B or W or BW => new SAV5BW(), - B2 or W2 or B2W2 => new SAV5B2W2(), - - X or Y or XY => new SAV6XY(), - ORASDEMO => new SAV6AODemo(), - OR or AS or ORAS => new SAV6AO(), - - SN or MN or SM => new SAV7SM(), - US or UM or USUM => new SAV7USUM(), - GP or GE or GG or GO => new SAV7b(), - - SW or SH or SWSH => new SAV8SWSH(), - BD or SP or BDSP => new SAV8BS(), - PLA => new SAV8LA(), - - SL or VL or SV => new SAV9SV(), - - _ => throw new ArgumentOutOfRangeException(nameof(game)), - }; - - /// - /// Creates an instance of a SaveFile with a blank base. - /// - /// Context of the Save File. - /// Trainer Name - /// Save file language to initialize for - /// Save File for that generation. - public static SaveFile GetBlankSAV(EntityContext context, string trainerName, LanguageID language = LanguageID.English) - { - var version = context.GetSingleGameVersion(); - return GetBlankSAV(version, trainerName, language); + result = sav; + return true; } /// @@ -887,7 +675,7 @@ private static IEnumerable FilterSaveFiles(bool ignoreBackups, IEnumerab if (token.IsCancellationRequested) yield break; - if (ignoreBackups && IsBackup(file)) + if (ignoreBackups && IsBackupFilePath(file)) continue; var size = FileUtil.GetFileSize(file); @@ -898,12 +686,14 @@ private static IEnumerable FilterSaveFiles(bool ignoreBackups, IEnumerab } } - public static bool IsBackup(ReadOnlySpan path) + public static bool IsBackupFilePath(ReadOnlySpan path) { + // Gen8+ store main,backup,poke_trade var fn = Path.GetFileNameWithoutExtension(path); if (fn is "backup") return true; + // Programs storing backups via .bak extension var ext = Path.GetExtension(path); return ext is ".bak"; } @@ -938,4 +728,24 @@ public static bool IsSizeValidNoHandler(long size) return true; return false; } + + /// + /// Stores the result from a save detection. + /// + /// The save file type detected + /// Specific game version within the type, or Any if not distinguished + private readonly record struct SaveTypeInfo(SaveFileType Type, GameVersion SubVersion = default, LanguageID Language = default) + { + /// + /// Implicit conversion from SaveTypeInfo to SaveFileType for convenience. + /// + public static implicit operator SaveFileType(SaveTypeInfo info) => info.Type; + + public static implicit operator SaveTypeInfo(SaveFileType type) => new(type); + + /// + /// Returns Invalid save type info. + /// + public static SaveTypeInfo Invalid => default; + } } diff --git a/PKHeX.Core/Util/FileUtil.cs b/PKHeX.Core/Util/FileUtil.cs index 600fba3f3..98910274b 100644 --- a/PKHeX.Core/Util/FileUtil.cs +++ b/PKHeX.Core/Util/FileUtil.cs @@ -48,7 +48,7 @@ public static class FileUtil /// Supported file object reference, null if none found. public static object? GetSupportedFile(Memory data, ReadOnlySpan ext, SaveFile? reference = null) { - if (TryGetSAV(data, out var sav)) + if (SaveUtil.TryGetSaveFile(data, out var sav)) return sav; if (TryGetMemoryCard(data, out var mc)) return mc; @@ -179,18 +179,6 @@ public static bool IsFileTooBig(long length) /// File size public static bool IsFileTooSmall(long length) => length < 0x20; // bigger than PK1 - /// - /// Tries to get a object from the input parameters. - /// - /// Binary data - /// Output result - /// True if file object reference is valid, false if none found. - public static bool TryGetSAV(Memory data, [NotNullWhen(true)] out SaveFile? sav) - { - sav = SaveUtil.GetVariantSAV(data); - return sav is not null; - } - /// /// Tries to get a object from the input parameters. /// diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index c7b6c7457..6809decbc 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -182,14 +182,14 @@ private void FormLoadInitialFiles(StartupArguments args) private void LoadBlankSaveFile(GameVersion version) { var current = C_SAV?.SAV; - var lang = SaveUtil.GetSafeLanguage(current); - var tr = SaveUtil.GetSafeTrainerName(current, lang); - var sav = SaveUtil.GetBlankSAV(version, tr, lang); + var lang = BlankSaveFile.GetSafeLanguage(current); + var tr = BlankSaveFile.GetSafeTrainerName(current, lang); + var sav = BlankSaveFile.Get(version, tr, lang); if (sav.Version == GameVersion.Invalid) // will fail to load { var max = GameInfo.Sources.VersionDataSource.MaxBy(z => z.Value) ?? throw new Exception(); version = (GameVersion)max.Value; - sav = SaveUtil.GetBlankSAV(version, tr, lang); + sav = BlankSaveFile.Get(version, tr, lang); } OpenSAV(sav, string.Empty); C_SAV!.SAV.State.Edited = false; // Prevents form close warning from showing until changes are made @@ -656,8 +656,7 @@ private bool LoadFile(object? input, string path) case SAV3GCMemoryCard gc: if (!CheckGCMemoryCard(gc, path)) return true; - var mcsav = SaveUtil.GetVariantSAV(gc); - if (mcsav is null) + if (!SaveUtil.TryGetSaveFile(gc, out var mcsav)) return false; mcsav.Metadata.SetExtraInfo(path); return OpenSAV(mcsav, path); @@ -729,7 +728,7 @@ private bool OpenPCBoxBin(ConcatenatedEntitySet pkms) return true; } - private static GameVersion SelectMemoryCardSaveGame(SAV3GCMemoryCard memCard) + private static SaveFileType SelectMemoryCardSaveGame(SAV3GCMemoryCard memCard) { if (memCard.SaveGameCount == 1) return memCard.SelectedGameVersion; @@ -737,15 +736,15 @@ private static GameVersion SelectMemoryCardSaveGame(SAV3GCMemoryCard memCard) var games = GetMemoryCardGameSelectionList(memCard); var dialog = new SAV_GameSelect(games, MsgFileLoadSaveMultiple, MsgFileLoadSaveSelectGame); dialog.ShowDialog(); - return dialog.Result; + return (SaveFileType)dialog.Result; } private static List GetMemoryCardGameSelectionList(SAV3GCMemoryCard memCard) { var games = new List(); - if (memCard.HasCOLO) games.Add(new ComboItem(MsgGameColosseum, (int)GameVersion.COLO)); - if (memCard.HasXD) games.Add(new ComboItem(MsgGameXD, (int)GameVersion.XD)); - if (memCard.HasRSBOX) games.Add(new ComboItem(MsgGameRSBOX, (int)GameVersion.RSBOX)); + if (memCard.HasCOLO) games.Add(new ComboItem(MsgGameColosseum, (int)SaveFileType.Colosseum)); + if (memCard.HasXD) games.Add(new ComboItem(MsgGameXD, (int)SaveFileType.XD)); + if (memCard.HasRSBOX) games.Add(new ComboItem(MsgGameRSBOX, (int)SaveFileType.RSBox)); return games; } @@ -766,14 +765,14 @@ private static bool CheckGCMemoryCard(SAV3GCMemoryCard memCard, string path) case MemoryCardSaveStatus.MultipleSaveGame: var game = SelectMemoryCardSaveGame(memCard); - if (game == GameVersion.Invalid) //Cancel + if (game == 0) // Cancel return false; memCard.SelectSaveGame(game); break; - case MemoryCardSaveStatus.SaveGameCOLO: memCard.SelectSaveGame(GameVersion.COLO); break; - case MemoryCardSaveStatus.SaveGameXD: memCard.SelectSaveGame(GameVersion.XD); break; - case MemoryCardSaveStatus.SaveGameRSBOX: memCard.SelectSaveGame(GameVersion.RSBOX); break; + case MemoryCardSaveStatus.SaveGameCOLO: memCard.SelectSaveGame(SaveFileType.Colosseum); break; + case MemoryCardSaveStatus.SaveGameXD: memCard.SelectSaveGame(SaveFileType.XD); break; + case MemoryCardSaveStatus.SaveGameRSBOX: memCard.SelectSaveGame(SaveFileType.RSBox); break; default: WinFormsUtil.Error(!SAV3GCMemoryCard.IsMemoryCardSize(memCard.Data.Length) ? MsgFileGameCubeBad : GetHintInvalidFile(memCard.Data, path), path); @@ -964,13 +963,14 @@ private static bool SanityCheckSAV(ref SaveFile sav) var msg = string.Format(MsgFileLoadVersionDetect, $"3 ({s3.Version})"); using var dialog = new SAV_GameSelect(games, msg, MsgFileLoadSaveSelectVersion); dialog.ShowDialog(); - if (dialog.Result is GameVersion.Invalid) + if (dialog.Result is 0) return false; - var s = s3.ForceLoad(dialog.Result); + var game = (GameVersion)dialog.Result; + var s = s3.ForceLoad(game); if (s is SAV3FRLG frlg) { - bool result = frlg.ResetPersonal(dialog.Result); + bool result = frlg.ResetPersonal(game); if (!result) return false; } @@ -988,7 +988,8 @@ private static bool SanityCheckSAV(ref SaveFile sav) var msg = string.Format(dual, "3", fr, lg); using var dialog = new SAV_GameSelect(games, msg, MsgFileLoadSaveSelectVersion); dialog.ShowDialog(); - bool result = frlg.ResetPersonal(dialog.Result); + var game = (GameVersion)dialog.Result; + bool result = frlg.ResetPersonal(game); if (!result) return false; } diff --git a/PKHeX.WinForms/Subforms/SAV_Database.cs b/PKHeX.WinForms/Subforms/SAV_Database.cs index 206feea74..541f117d1 100644 --- a/PKHeX.WinForms/Subforms/SAV_Database.cs +++ b/PKHeX.WinForms/Subforms/SAV_Database.cs @@ -455,17 +455,16 @@ private static List LoadEntitiesFromFolder(string databaseFolder, Sav private static void TryAddPKMsFromSaveFilePath(ConcurrentBag dbTemp, string file) { - var sav = SaveUtil.GetVariantSAV(file); - if (sav is null) + if (SaveUtil.TryGetSaveFile(file, out var sav)) { - if (FileUtil.TryGetMemoryCard(file, out var mc)) - TryAddPKMsFromMemoryCard(dbTemp, mc, file); - else - Debug.WriteLine($"Unable to load SaveFile: {file}"); + SlotInfoLoader.AddFromSaveFile(sav, dbTemp); return; } - SlotInfoLoader.AddFromSaveFile(sav, dbTemp); + if (FileUtil.TryGetMemoryCard(file, out var mc)) + TryAddPKMsFromMemoryCard(dbTemp, mc, file); + else + Debug.WriteLine($"Unable to load SaveFile: {file}"); } private static void TryAddPKMsFromMemoryCard(ConcurrentBag dbTemp, SAV3GCMemoryCard mc, string file) @@ -475,17 +474,16 @@ private static void TryAddPKMsFromMemoryCard(ConcurrentBag dbTemp, SA return; if (mc.HasCOLO) - TryAdd(dbTemp, mc, file, GameVersion.COLO); + TryAdd(dbTemp, mc, file, SaveFileType.Colosseum); if (mc.HasXD) - TryAdd(dbTemp, mc, file, GameVersion.XD); + TryAdd(dbTemp, mc, file, SaveFileType.XD); if (mc.HasRSBOX) - TryAdd(dbTemp, mc, file, GameVersion.RSBOX); + TryAdd(dbTemp, mc, file, SaveFileType.RSBox); - static void TryAdd(ConcurrentBag dbTemp, SAV3GCMemoryCard mc, string path, GameVersion game) + static void TryAdd(ConcurrentBag dbTemp, SAV3GCMemoryCard mc, string path, SaveFileType game) { mc.SelectSaveGame(game); - var sav = SaveUtil.GetVariantSAV(mc); - if (sav is null) + if (!SaveUtil.TryGetSaveFile(mc, out var sav)) return; sav.Metadata.SetExtraInfo(path); SlotInfoLoader.AddFromSaveFile(sav, dbTemp); diff --git a/PKHeX.WinForms/Subforms/SAV_FolderList.cs b/PKHeX.WinForms/Subforms/SAV_FolderList.cs index 4f77fb7cd..f98ae2707 100644 --- a/PKHeX.WinForms/Subforms/SAV_FolderList.cs +++ b/PKHeX.WinForms/Subforms/SAV_FolderList.cs @@ -42,7 +42,7 @@ public SAV_FolderList(Action openSaveFile) var recent = SaveFinder.GetSaveFiles(drives, false, extra, true, token).ToList(); var loaded = Main.Settings.Startup.RecentlyLoaded .Where(z => recent.All(x => x.Metadata.FilePath != z)) - .Where(File.Exists).Select(SaveUtil.GetVariantSAV).OfType(); + .Where(File.Exists).Select(SaveUtil.GetSaveFile).OfType(); Recent = PopulateData(dgDataRecent, loaded.Concat(recent)); Backup = PopulateData(dgDataBackup, backup); @@ -276,7 +276,7 @@ public static void CleanBackups(string path, bool deleteNotSaves) foreach (var file in files) { var fi = new FileInfo(file); - if (!SaveUtil.IsSizeValid(fi.Length) || SaveUtil.GetVariantSAV(file) is not { } sav) + if (!SaveUtil.IsSizeValid(fi.Length) || !SaveUtil.TryGetSaveFile(file, out var sav)) { if (deleteNotSaves) File.Delete(file); diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_BlockDump8.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_BlockDump8.cs index ec972dd8b..1fc1d22c5 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_BlockDump8.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_BlockDump8.cs @@ -234,11 +234,9 @@ private void CompareSaves() if (!SaveUtil.IsSizeValid((int)f2.Length)) return; - var s1 = SaveUtil.GetVariantSAV(p1); - if (s1 is not ISCBlockArray w1) + if (!SaveUtil.TryGetSaveFile(p1, out var s1) || s1 is not ISCBlockArray w1) return; - var s2 = SaveUtil.GetVariantSAV(p2); - if (s2 is not ISCBlockArray w2) + if (!SaveUtil.TryGetSaveFile(p2, out var s2) || s2 is not ISCBlockArray w2) return; // Get an external source of names if available. diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_GameSelect.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_GameSelect.cs index 7bef0db92..007996ce4 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_GameSelect.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_GameSelect.cs @@ -8,7 +8,7 @@ namespace PKHeX.WinForms; public partial class SAV_GameSelect : Form { - public GameVersion Result = GameVersion.Invalid; + public int Result { get; private set; } public SAV_GameSelect(IEnumerable items, params ReadOnlySpan lines) { @@ -25,7 +25,7 @@ public SAV_GameSelect(IEnumerable items, params ReadOnlySpan private void B_OK_Click(object sender, EventArgs e) { - Result = (GameVersion)WinFormsUtil.GetIndex(CB_Game); + Result = WinFormsUtil.GetIndex(CB_Game); Close(); } diff --git a/Tests/PKHeX.Core.Tests/Saves/MemeCrypto/SwishCryptoTests.cs b/Tests/PKHeX.Core.Tests/Saves/MemeCrypto/SwishCryptoTests.cs index ddb243e52..b40dcc30a 100644 --- a/Tests/PKHeX.Core.Tests/Saves/MemeCrypto/SwishCryptoTests.cs +++ b/Tests/PKHeX.Core.Tests/Saves/MemeCrypto/SwishCryptoTests.cs @@ -14,7 +14,7 @@ public void SizeCheck() [Fact] public void CanMakeBlankSAV8() { - var sav = SaveUtil.GetBlankSAV(GameVersion.SW, TrainerName.ProgramINT); + var sav = BlankSaveFile.Get(SaveFileType.SWSH, GameVersion.SW); sav.Should().NotBeNull(); } }