mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-27 14:30:56 +00:00
cd19d1d700
Introduces an option to disable reading .bak / "backup" files; trickle the changes up to all surface users so that SAV_FolderList can correctly load all .bak saves into its Backup tab. The "ignore backups" was added in the latest feature push to ignore "backup" from being detected as the latest save.
839 lines
35 KiB
C#
839 lines
35 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using static System.Buffers.Binary.BinaryPrimitives;
|
||
using static PKHeX.Core.MessageStrings;
|
||
using static PKHeX.Core.GameVersion;
|
||
|
||
namespace PKHeX.Core
|
||
{
|
||
/// <summary>
|
||
/// Logic for <see cref="SaveFile"/> data loading and manipulation.
|
||
/// </summary>
|
||
public static class SaveUtil
|
||
{
|
||
public const int BEEF = 0x42454546;
|
||
|
||
public const int SIZE_G8LA = 0x136DDE;
|
||
|
||
public const int SIZE_G8BDSP = 0xE9828;
|
||
public const int SIZE_G8BDSP_1 = 0xEDC20;
|
||
|
||
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
|
||
|
||
public const int SIZE_G7GG = 0x100000;
|
||
public const int SIZE_G7USUM = 0x6CC00;
|
||
public const int SIZE_G7SM = 0x6BE00;
|
||
public const int SIZE_G6XY = 0x65600;
|
||
public const int SIZE_G6ORAS = 0x76000;
|
||
public const int SIZE_G6ORASDEMO = 0x5A00;
|
||
public const int SIZE_G5RAW = 0x80000;
|
||
public const int SIZE_G5BW = 0x24000;
|
||
public const int SIZE_G5B2W2 = 0x26000;
|
||
public const int SIZE_G4BR = 0x380000;
|
||
public const int SIZE_G4RAW = 0x80000;
|
||
public const int SIZE_G3BOX = 0x76000;
|
||
public const int SIZE_G3COLO = 0x60000;
|
||
public const int SIZE_G3XD = 0x56000;
|
||
public const int SIZE_G3RAW = 0x20000;
|
||
public const int SIZE_G3RAWHALF = 0x10000;
|
||
public const int SIZE_G2STAD = 0x20000; // same as G3RAW_U
|
||
public const int SIZE_G2RAW_U = 0x8000;
|
||
public const int SIZE_G2VC_U = 0x8010;
|
||
public const int SIZE_G2BAT_U = 0x802C;
|
||
public const int SIZE_G2EMU_U = 0x8030;
|
||
public const int SIZE_G2RAW_J = 0x10000;
|
||
public const int SIZE_G2VC_J = 0x10010;
|
||
public const int SIZE_G2BAT_J = 0x1002C;
|
||
public const int SIZE_G2EMU_J = 0x10030;
|
||
public const int SIZE_G1STAD = 0x20000; // same as G3RAW_U
|
||
public const int SIZE_G1STADJ = 0x8000; // same as G1RAW
|
||
public const int SIZE_G1RAW = 0x8000;
|
||
public const int SIZE_G1BAT = 0x802C;
|
||
|
||
// Bank Binaries
|
||
public const int SIZE_G7BANK = 0xACA48;
|
||
public const int SIZE_G4BANK = 0x405C4;
|
||
public const int SIZE_G4RANCH = 0x54000;
|
||
public const int SIZE_G4RANCH_PLAT = 0x7C000;
|
||
|
||
private static readonly SaveHandlerGCI DolphinHandler = new();
|
||
|
||
#if !EXCLUDE_HACKS
|
||
/// <summary>
|
||
/// Specialized readers for loading save files from non-standard games (e.g. hacks).
|
||
/// </summary>
|
||
// ReSharper disable once CollectionNeverUpdated.Global
|
||
public static readonly List<ISaveReader> CustomSaveReaders = new();
|
||
#endif
|
||
|
||
#if !EXCLUDE_EMULATOR_FORMATS
|
||
/// <summary>
|
||
/// Pre-formatters for loading save files from non-standard formats (e.g. emulators).
|
||
/// </summary>
|
||
public static readonly ICollection<ISaveHandler> Handlers = new List<ISaveHandler>
|
||
{
|
||
DolphinHandler,
|
||
new SaveHandlerDeSmuME(),
|
||
new SaveHandlerARDS(),
|
||
};
|
||
#endif
|
||
|
||
internal static readonly HashSet<int> SizesSWSH = new()
|
||
{
|
||
SIZE_G8SWSH, SIZE_G8SWSH_1, SIZE_G8SWSH_2, SIZE_G8SWSH_2B, SIZE_G8SWSH_3, SIZE_G8SWSH_3A, SIZE_G8SWSH_3B, SIZE_G8SWSH_3C,
|
||
};
|
||
|
||
private static readonly HashSet<int> SizesGen2 = new()
|
||
{
|
||
SIZE_G2RAW_U, SIZE_G2VC_U, SIZE_G2BAT_U, SIZE_G2EMU_U, SIZE_G2RAW_J, SIZE_G2BAT_J, SIZE_G2EMU_J, SIZE_G2VC_J,
|
||
};
|
||
|
||
private static readonly HashSet<int> Sizes = new(SizesGen2.Concat(SizesSWSH))
|
||
{
|
||
SIZE_G8LA, SIZE_G8BDSP, SIZE_G8BDSP_1,
|
||
// SizesSWSH covers gen8 sizes since there's so many
|
||
SIZE_G7SM, SIZE_G7USUM, SIZE_G7GG,
|
||
SIZE_G6XY, SIZE_G6ORAS, SIZE_G6ORASDEMO,
|
||
SIZE_G5RAW, SIZE_G5BW, SIZE_G5B2W2,
|
||
SIZE_G4BR, SIZE_G4RAW,
|
||
SIZE_G3BOX, SIZE_G3COLO, SIZE_G3XD, SIZE_G3RAW, SIZE_G3RAWHALF,
|
||
// SizesGen2 covers gen2 sizes since there's so many
|
||
SIZE_G1RAW, SIZE_G1BAT,
|
||
|
||
SIZE_G7BANK, SIZE_G4BANK, SIZE_G4RANCH, SIZE_G4RANCH_PLAT,
|
||
};
|
||
|
||
/// <summary>Determines the type of the provided save data.</summary>
|
||
/// <param name="data">Save data of which to determine the origins of</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetSAVType(byte[] data)
|
||
{
|
||
GameVersion ver;
|
||
if ((ver = GetIsG1SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG2SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG3SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG4SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG5SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG6SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG7SAV(data)) != Invalid)
|
||
return ver;
|
||
|
||
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;
|
||
|
||
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;
|
||
|
||
if ((ver = GetIsG8SAV(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG8SAV_BDSP(data)) != Invalid)
|
||
return ver;
|
||
if ((ver = GetIsG8SAV_LA(data)) != Invalid)
|
||
return ver;
|
||
|
||
return Invalid;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determines if a Gen1/2 Pokémon List is Invalid
|
||
/// </summary>
|
||
/// <param name="data">Save data</param>
|
||
/// <param name="offset">Offset the list starts at</param>
|
||
/// <param name="listCount">Max count of Pokémon in the list</param>
|
||
/// <returns>True if a valid list, False otherwise</returns>
|
||
private static bool IsG12ListValid(ReadOnlySpan<byte> data, int offset, int listCount)
|
||
{
|
||
byte num_entries = data[offset];
|
||
return num_entries <= listCount && data[offset + 1 + num_entries] == 0xFF;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen1 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
internal static GameVersion GetIsG1SAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length is not (SIZE_G1RAW or SIZE_G1BAT))
|
||
return Invalid;
|
||
|
||
// 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;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to an International Gen1 save</summary>
|
||
/// <param name="data">Save data of which to determine the region</param>
|
||
/// <returns>True if a valid International save, False otherwise.</returns>
|
||
private static bool GetIsG1SAVU(ReadOnlySpan<byte> data)
|
||
{
|
||
return IsG12ListValid(data, 0x2F2C, 20) && IsG12ListValid(data, 0x30C0, 20);
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Japanese Gen1 save</summary>
|
||
/// <param name="data">Save data of which to determine the region</param>
|
||
/// <returns>True if a valid Japanese save, False otherwise.</returns>
|
||
internal static bool GetIsG1SAVJ(ReadOnlySpan<byte> data)
|
||
{
|
||
return IsG12ListValid(data, 0x2ED5, 30) && IsG12ListValid(data, 0x302D, 30);
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen2 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
internal static GameVersion GetIsG2SAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (!SizesGen2.Contains(data.Length))
|
||
return Invalid;
|
||
|
||
// 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;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to an International (not Japanese or Korean) Gen2 save</summary>
|
||
/// <param name="data">Save data of which to determine the region</param>
|
||
/// <returns>True if a valid International save, False otherwise.</returns>
|
||
private static GameVersion GetIsG2SAVU(ReadOnlySpan<byte> 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;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Japanese Gen2 save</summary>
|
||
/// <param name="data">Save data of which to determine the region</param>
|
||
/// <returns>True if a valid Japanese save, False otherwise.</returns>
|
||
internal static GameVersion GetIsG2SAVJ(ReadOnlySpan<byte> data)
|
||
{
|
||
if (!IsG12ListValid(data, 0x2D10, 30))
|
||
return Invalid;
|
||
if (IsG12ListValid(data, 0x283E, 30))
|
||
return GS;
|
||
if (IsG12ListValid(data, 0x281A, 30))
|
||
return C;
|
||
return Invalid;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Korean Gen2 save</summary>
|
||
/// <param name="data">Save data of which to determine the region</param>
|
||
/// <returns>True if a valid Korean save, False otherwise.</returns>
|
||
internal static GameVersion GetIsG2SAVK(ReadOnlySpan<byte> data)
|
||
{
|
||
if (IsG12ListValid(data, 0x2DAE, 20) && IsG12ListValid(data, 0x28CC, 20))
|
||
return GS;
|
||
return Invalid;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen3 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG3SAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length is not (SIZE_G3RAW or SIZE_G3RAWHALF))
|
||
return Invalid;
|
||
|
||
// 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..]);
|
||
}
|
||
return Invalid;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks the input <see cref="data"/> to see which game is for this file.
|
||
/// </summary>
|
||
/// <param name="data">Data to check</param>
|
||
/// <returns>RS, E, or FR/LG.</returns>
|
||
private static GameVersion GetVersionG3SAV(ReadOnlySpan<byte> data)
|
||
{
|
||
// 0xAC
|
||
// RS: Battle Tower Data, which will never match the FR/LG fixed value.
|
||
// E: Encryption Key
|
||
// FR/LG @ 0xAC has a fixed value (01 00 00 00)
|
||
// RS has battle tower data (variable)
|
||
uint _0xAC = ReadUInt32LittleEndian(data[0xAC..]);
|
||
switch (_0xAC)
|
||
{
|
||
case 1: return FRLG; // fixed value
|
||
case 0: return RS; // save has no battle tower record data
|
||
default:
|
||
// RS data structure only extends 0x890 bytes; check if any data is present afterwards.
|
||
for (int i = 0x890; i < 0xF2C; i += 4)
|
||
{
|
||
if (ReadUInt64LittleEndian(data[i..]) != 0)
|
||
return E;
|
||
}
|
||
return RS;
|
||
}
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen3 Box RS save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG3BOXSAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length is not SIZE_G3BOX)
|
||
return Invalid;
|
||
|
||
// 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;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Colosseum save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG3COLOSAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length is not SIZE_G3COLO)
|
||
return Invalid;
|
||
|
||
// Check the intro bytes for each save slot
|
||
const int offset = 0x6000;
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var ofs = offset + (0x1E000 * i);
|
||
if (ReadUInt32LittleEndian(data[ofs..]) != 0x00000101)
|
||
return Invalid;
|
||
}
|
||
return COLO;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen3 XD save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG3XDSAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length is not SIZE_G3XD)
|
||
return Invalid;
|
||
|
||
// Check the intro bytes for each save slot
|
||
const int offset = 0x6000;
|
||
for (int i = 0; i < 2; i++)
|
||
{
|
||
var ofs = offset + (0x28000 * i);
|
||
if ((ReadUInt32LittleEndian(data[ofs..]) & 0xFFFE_FFFF) != 0x00000101)
|
||
return Invalid;
|
||
}
|
||
return XD;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen4 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG4SAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length != SIZE_G4RAW)
|
||
return Invalid;
|
||
|
||
// The block footers contain a u32 'size' followed by a u32 binary-coded-decimal timestamp(?)
|
||
// Korean saves have a different timestamp from other localizations.
|
||
static bool validSequence(ReadOnlySpan<byte> data, int offset)
|
||
{
|
||
var size = ReadUInt32LittleEndian(data[(offset - 0xC)..]);
|
||
if (size != (offset & 0xFFFF))
|
||
return false;
|
||
var sdk = ReadUInt32LittleEndian(data[(offset - 0x8)..]);
|
||
|
||
const int DATE_INT = 0x20060623;
|
||
const int DATE_KO = 0x20070903;
|
||
return sdk is DATE_INT or DATE_KO;
|
||
}
|
||
|
||
// 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.
|
||
if (validSequence(data, 0x4C100))
|
||
return DP;
|
||
if (validSequence(data, 0x4CF2C))
|
||
return Pt;
|
||
if (validSequence(data, 0x4F628))
|
||
return HGSS;
|
||
|
||
return Invalid;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen4 Battle Revolution save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG4BRSAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length != SIZE_G4BR)
|
||
return Invalid;
|
||
|
||
byte[] sav = SAV4BR.DecryptPBRSaveData(data);
|
||
return SAV4BR.IsChecksumsValid(sav) ? BATREV : Invalid;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen5 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG5SAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length != SIZE_G5RAW)
|
||
return Invalid;
|
||
|
||
// check the checksum block validity; nobody would normally modify this region
|
||
ushort chk1 = ReadUInt16LittleEndian(data[(SIZE_G5BW - 0x100 + 0x8C + 0xE)..]);
|
||
ushort actual1 = Checksums.CRC16_CCITT(data.Slice(SIZE_G5BW - 0x100, 0x8C));
|
||
if (chk1 == actual1)
|
||
return BW;
|
||
ushort chk2 = ReadUInt16LittleEndian(data[(SIZE_G5B2W2 - 0x100 + 0x94 + 0xE)..]);
|
||
ushort actual2 = Checksums.CRC16_CCITT(data.Slice(SIZE_G5B2W2 - 0x100, 0x94));
|
||
if (chk2 == actual2)
|
||
return B2W2;
|
||
return Invalid;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen6 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG6SAV(ReadOnlySpan<byte> 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
|
||
};
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen7 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG7SAV(ReadOnlySpan<byte> 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;
|
||
}
|
||
|
||
/// <summary>Determines if the input data belongs to a <see cref="SAV7b"/> save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsBelugaSAV(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length != SIZE_G7GG)
|
||
return Invalid;
|
||
|
||
const int actualLength = 0xB8800;
|
||
if (ReadUInt32LittleEndian(data[(actualLength - 0x1F0)..]) != BEEF) // beef table start
|
||
return Invalid;
|
||
if (ReadUInt16LittleEndian(data[(actualLength - 0x200 + 0xB0)..]) != 0x13) // check a block number to double check
|
||
return Invalid;
|
||
|
||
return GG;
|
||
}
|
||
|
||
/// <summary>Checks to see if the data belongs to a Gen7 save</summary>
|
||
/// <param name="data">Save data of which to determine the type</param>
|
||
/// <returns>Version Identifier or Invalid if type cannot be determined.</returns>
|
||
private static GameVersion GetIsG8SAV(byte[] data)
|
||
{
|
||
if (!SizesSWSH.Contains(data.Length))
|
||
return Invalid;
|
||
|
||
return SwishCrypto.GetIsHashValid(data) ? SWSH : Invalid;
|
||
}
|
||
|
||
private static GameVersion GetIsG8SAV_BDSP(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length is not SIZE_G8BDSP && data.Length is not SIZE_G8BDSP_1)
|
||
return Invalid;
|
||
|
||
return BDSP;
|
||
}
|
||
|
||
private static GameVersion GetIsG8SAV_LA(byte[] data)
|
||
{
|
||
if (data.Length is not SIZE_G8LA)
|
||
return Invalid;
|
||
|
||
return SwishCrypto.GetIsHashValidLA(data) ? PLA : Invalid;
|
||
}
|
||
|
||
private static bool GetIsBank7(ReadOnlySpan<byte> data) => data.Length == SIZE_G7BANK && data[0] != 0;
|
||
private static bool GetIsBank4(ReadOnlySpan<byte> data) => data.Length == SIZE_G4BANK && ReadUInt32LittleEndian(data[0x3FC00..]) != 0; // box name present
|
||
private static bool GetIsBank3(ReadOnlySpan<byte> data) => data.Length == SIZE_G4BANK && ReadUInt32LittleEndian(data[0x3FC00..]) == 0; // size collision with ^
|
||
private static bool GetIsRanchDP(ReadOnlySpan<byte> data) => data.Length == SIZE_G4RANCH && ReadUInt32BigEndian(data[0x22AC..]) != 0;
|
||
private static bool GetIsRanchPlat(ReadOnlySpan<byte> data) => data.Length == SIZE_G4RANCH_PLAT && ReadUInt32BigEndian(data[0x268C..]) != 0;
|
||
private static bool GetIsRanch4(ReadOnlySpan<byte> data) => GetIsRanchDP(data) || GetIsRanchPlat(data);
|
||
|
||
/// <summary>Creates an instance of a SaveFile using the given save data.</summary>
|
||
/// <param name="path">File location from which to create a SaveFile.</param>
|
||
/// <returns>An appropriate type of save file for the given data, or null if the save data is invalid.</returns>
|
||
public static SaveFile? GetVariantSAV(string path)
|
||
{
|
||
var data = File.ReadAllBytes(path);
|
||
var sav = GetVariantSAV(data, path);
|
||
if (sav == null)
|
||
return null;
|
||
sav.Metadata.SetExtraInfo(path);
|
||
return sav;
|
||
}
|
||
|
||
/// <summary>Creates an instance of a SaveFile using the given save data.</summary>
|
||
/// <param name="data">Save data from which to create a SaveFile.</param>
|
||
/// <param name="path">Optional save file path, may help initialize a non-standard save file format.</param>
|
||
/// <returns>An appropriate type of save file for the given data, or null if the save data is invalid.</returns>
|
||
public static SaveFile? GetVariantSAV(byte[] data, string? path = null)
|
||
{
|
||
#if !EXCLUDE_HACKS
|
||
foreach (var h in CustomSaveReaders)
|
||
{
|
||
if (!h.IsRecognized(data.Length))
|
||
continue;
|
||
|
||
var custom = h.ReadSaveFile(data, path);
|
||
if (custom != null)
|
||
return custom;
|
||
}
|
||
#endif
|
||
|
||
var sav = GetVariantSAVInternal(data);
|
||
if (sav != null)
|
||
return sav;
|
||
|
||
#if !EXCLUDE_EMULATOR_FORMATS
|
||
foreach (var h in Handlers)
|
||
{
|
||
if (!h.IsRecognized(data.Length))
|
||
continue;
|
||
|
||
var split = h.TrySplit(data);
|
||
if (split == null)
|
||
continue;
|
||
|
||
sav = GetVariantSAVInternal(split.Data);
|
||
if (sav == null)
|
||
continue;
|
||
|
||
var meta = sav.Metadata;
|
||
meta.SetExtraInfo(split.Header, split.Footer);
|
||
if (path is not null)
|
||
meta.SetExtraInfo(path);
|
||
return sav;
|
||
}
|
||
#endif
|
||
|
||
// unrecognized.
|
||
return null;
|
||
}
|
||
|
||
private static SaveFile? GetVariantSAVInternal(byte[] data)
|
||
{
|
||
var type = GetSAVType(data);
|
||
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),
|
||
|
||
// 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,
|
||
};
|
||
}
|
||
|
||
public static SaveFile? GetVariantSAV(SAV3GCMemoryCard memCard)
|
||
{
|
||
// Pre-check for header/footer signatures
|
||
byte[] data = memCard.ReadSaveGameData();
|
||
if (data.Length == 0)
|
||
return null;
|
||
|
||
var split = DolphinHandler.TrySplit(data);
|
||
if (split != null)
|
||
data = split.Data;
|
||
|
||
SaveFile sav;
|
||
switch (memCard.SelectedGameVersion)
|
||
{
|
||
// Side Games
|
||
case COLO: sav = new SAV3Colosseum(data, memCard); break;
|
||
case XD: sav = new SAV3XD(data, memCard); break;
|
||
case RSBOX: sav = new SAV3RSBox(data, memCard); break;
|
||
|
||
// No pattern matched
|
||
default: return null;
|
||
}
|
||
|
||
if (split != null)
|
||
sav.Metadata.SetExtraInfo(split.Header, split.Footer);
|
||
return sav;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns a <see cref="LanguageID"/> that feels best for the save file's language.
|
||
/// </summary>
|
||
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,
|
||
};
|
||
|
||
/// <summary>
|
||
/// Returns a Trainer Name that feels best for the save file's language.
|
||
/// </summary>
|
||
public static string GetSafeTrainerName(SaveFile? sav, LanguageID lang) => lang switch
|
||
{
|
||
LanguageID.Japanese => sav?.Generation >= 3 ? "PKHeX" : "1337",
|
||
_ => "PKHeX",
|
||
};
|
||
|
||
/// <summary>
|
||
/// Creates an instance of a SaveFile with a blank base.
|
||
/// </summary>
|
||
/// <param name="game">Version to create the save file for.</param>
|
||
/// <param name="trainerName">Trainer Name</param>
|
||
/// <param name="language">Language to initialize with</param>
|
||
/// <returns>Blank save file from the requested game, null if no game exists for that <see cref="GameVersion"/>.</returns>
|
||
public static SaveFile GetBlankSAV(GameVersion game, string trainerName, LanguageID language = LanguageID.English)
|
||
{
|
||
var sav = GetBlankSAV(game, language);
|
||
sav.Game = (int)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.
|
||
if (sav.Generation >= 7)
|
||
{
|
||
sav.TrainerID7 = 123456;
|
||
sav.TrainerSID7 = 1234;
|
||
}
|
||
else
|
||
{
|
||
sav.TID = 12345;
|
||
sav.SID = 54321;
|
||
}
|
||
sav.Language = (int)language;
|
||
|
||
// Only set geolocation data for 3DS titles
|
||
if (sav is IRegionOrigin o)
|
||
o.SetDefaultRegionOrigins();
|
||
|
||
return sav;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates an instance of a SaveFile with a blank base.
|
||
/// </summary>
|
||
/// <param name="game">Version to create the save file for.</param>
|
||
/// <param name="language">Save file language to initialize for</param>
|
||
/// <returns>Blank save file from the requested game, null if no game exists for that <see cref="GameVersion"/>.</returns>
|
||
private static SaveFile GetBlankSAV(GameVersion game, LanguageID language) => game switch
|
||
{
|
||
RD or BU or GN or YW or RBY => new SAV1(version: game, japanese: language == LanguageID.Japanese || game == BU),
|
||
StadiumJ => new SAV1StadiumJ(),
|
||
Stadium => new SAV1Stadium(language == LanguageID.Japanese),
|
||
|
||
GD or SV or GS => new SAV2(version: GS, lang: language),
|
||
C or GSC => new SAV2(version: C, lang: 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(),
|
||
|
||
_ => throw new ArgumentOutOfRangeException(nameof(game)),
|
||
};
|
||
|
||
/// <summary>
|
||
/// Creates an instance of a SaveFile with a blank base.
|
||
/// </summary>
|
||
/// <param name="generation">Generation of the Save File.</param>
|
||
/// <param name="trainerName">Trainer Name</param>
|
||
/// <param name="language">Save file language to initialize for</param>
|
||
/// <returns>Save File for that generation.</returns>
|
||
public static SaveFile GetBlankSAV(int generation, string trainerName, LanguageID language = LanguageID.English)
|
||
{
|
||
var ver = GameUtil.GetVersion(generation);
|
||
return GetBlankSAV(ver, trainerName, language);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves possible save file paths from the provided <see cref="folderPath"/>.
|
||
/// </summary>
|
||
/// <param name="folderPath">Folder to look within</param>
|
||
/// <param name="deep">Search all subfolders</param>
|
||
/// <param name="result">If this function returns true, full path of all <see cref="SaveFile"/> that match criteria. If this function returns false, the error message, or null if the directory could not be found</param>
|
||
/// <param name="ignoreBackups">Option to ignore files with backup names and extensions</param>
|
||
/// <returns>Boolean indicating whether or not operation was successful.</returns>
|
||
public static bool GetSavesFromFolder(string folderPath, bool deep, out IEnumerable<string> result, bool ignoreBackups = true)
|
||
{
|
||
if (!Directory.Exists(folderPath))
|
||
{
|
||
result = Array.Empty<string>();
|
||
return false;
|
||
}
|
||
try
|
||
{
|
||
var searchOption = deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||
// force evaluation so that an invalid path will throw before we return true/false.
|
||
// EnumerateFiles throws an exception while iterating, which won't be caught by the try-catch here.
|
||
var files = Directory.GetFiles(folderPath, "*", searchOption);
|
||
result = files.Where(f => !(ignoreBackups && IsBackup(f)) && IsSizeValid(FileUtil.GetFileSize(f)));
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result = new[]
|
||
{
|
||
MsgFileLoadFailAuto + Environment.NewLine + folderPath,
|
||
MsgFileLoadFailAutoAdvise + Environment.NewLine + MsgFileLoadFailAutoCause,
|
||
ex.Message,
|
||
};
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public static bool IsBackup(string path) => Path.GetFileName(path) is "backup" || Path.GetExtension(path) is ".bak";
|
||
|
||
/// <summary>
|
||
/// Determines whether the save data size is valid for automatically detecting saves.
|
||
/// </summary>
|
||
/// <param name="size">Size in bytes of the save data</param>
|
||
/// <returns>A boolean indicating whether or not the save data size is valid.</returns>
|
||
public static bool IsSizeValid(int size) => Sizes.Contains(size) || Handlers.Any(z => z.IsRecognized(size));
|
||
|
||
/// <summary>
|
||
/// Force loads the provided <see cref="sav"/> to the requested <see cref="version"/>.
|
||
/// </summary>
|
||
/// <param name="sav">SaveFile data to force</param>
|
||
/// <param name="version"> Version to retrieve for</param>
|
||
/// <returns>New <see cref="SaveFile"/> object.</returns>
|
||
public static SAV3 GetG3SaveOverride(SaveFile sav, GameVersion version) => version switch // Reset save file info
|
||
{
|
||
R or S or RS => new SAV3RS(sav.State.BAK),
|
||
E => new SAV3E(sav.State.BAK),
|
||
FR or LG or FRLG => new SAV3FRLG(sav.State.BAK),
|
||
_ => throw new ArgumentOutOfRangeException(nameof(version)),
|
||
};
|
||
}
|
||
}
|