2022-06-25 18:14:00 +00:00
using System ;
2016-08-05 04:12:16 +00:00
using System.Collections.Generic ;
2016-08-05 03:48:52 +00:00
using System.IO ;
2016-06-20 04:22:43 +00:00
using System.Linq ;
2022-01-03 05:35:59 +00:00
using static System . Buffers . Binary . BinaryPrimitives ;
2018-09-07 03:35:55 +00:00
using static PKHeX . Core . MessageStrings ;
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
using static PKHeX . Core . GameVersion ;
2018-09-07 03:35:55 +00:00
2022-06-18 18:04:24 +00:00
namespace PKHeX.Core ;
/// <summary>
/// Logic for <see cref="SaveFile"/> data loading and manipulation.
/// </summary>
public static class SaveUtil
2016-06-20 04:22:43 +00:00
{
2022-06-18 18:04:24 +00:00
public const int BEEF = 0x42454546 ;
2022-11-25 01:42:17 +00:00
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
2022-12-02 06:40:11 +00:00
public const int SIZE_G9_3 = 0x319DC3 ; // 1.1.0 fresh
public const int SIZE_G9_1Ba = 0x319DD0 ; // 1.0.1 -> 1.1.0
2022-11-25 01:42:17 +00:00
public const int SIZE_G9_1A = 0x31A2C0 ; // 1.0.0 -> 1.0.1
2022-12-02 06:40:11 +00:00
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
2022-11-25 01:42:17 +00:00
2023-02-28 03:12:27 +00:00
// 1.2.0: add 0x2C9F; clean upgrade (1.1.0->1.2.0 is same as *1.2.0)
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_3A1 = 0x31CA6F ; // 1.0.1 -> 1.1.0 -> 1.2.0 AM
public const int SIZE_G9_3B0 = SIZE_G9_3A0 - 0xD ; // BM
public const int SIZE_G9_3B1 = SIZE_G9_3A1 - 0xD ; // BM
public const int SIZE_G9_3G0 = SIZE_G9_3A0 + 0x5 ; // GO
public const int SIZE_G9_3G1 = SIZE_G9_3A1 + 0x5 ; // GO
2023-03-01 04:03:46 +00:00
public const int SIZE_G9_3P0 = SIZE_G9_3B0 + 0x5 ; // GO (before Multiplayer)
public const int SIZE_G9_3P1 = SIZE_G9_3B1 + 0x5 ; // GO (before Multiplayer)
2023-02-28 03:12:27 +00:00
2023-09-16 16:40:09 +00:00
// 2.0.1 (2.0.0 skipped): Teal Mask
public const int SIZE_G9_DLC1_B = 0x4329A0 ; // +9
public const int SIZE_G9_DLC1_0 = 0x4329A9 ; // +4
public const int SIZE_G9_DLC1_E = 0x4329AD ; // +1
public const int SIZE_G9_DLC1_1 = 0x4329AE ; // +1
public const int SIZE_G9_DLC1_D = 0x4329B2 ; // +4
public const int SIZE_G9_DLC1_F = 0x4329B3 ; // +1
public const int SIZE_G9_DLC1_A = 0x4329B6 ; // +3
2023-09-16 06:36:21 +00:00
public const int SIZE_G9_DLC1_2 = 0x4329BB ; // +5
public const int SIZE_G9_DLC1_3 = 0x4329C0 ; // +5
public const int SIZE_G9_DLC1_4 = 0x432EB6 ;
2023-09-16 16:40:09 +00:00
public const int SIZE_G9_DLC1_C = 0x432EBB ; // +5
public const int SIZE_G9_DLC1_5 = 0x432EBF ; // +4
2023-09-16 06:36:21 +00:00
public const int SIZE_G9_DLC1_6 = 0x432EC3 ; // +4
public const int SIZE_G9_DLC1_7 = 0x432EC8 ; // +5
public const int SIZE_G9_DLC1_8 = 0x432ECD ; // +5
2022-06-18 18:04:24 +00:00
public const int SIZE_G8LA = 0x136DDE ;
public const int SIZE_G8LA_1 = 0x13AD06 ;
public const int SIZE_G8BDSP = 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
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 ;
2022-10-14 14:29:52 +00:00
public const int SIZE_G3EMU = 0x20010 ;
2022-06-18 18:04:24 +00:00
public const int SIZE_G3RAWHALF = 0x10000 ;
2022-10-14 14:29:52 +00:00
public const int SIZE_G2STAD = 0x20000 ; // same as G3RAW
2022-06-18 18:04:24 +00:00
public const int SIZE_G2STADF = 0x1FF00 ;
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 ;
2022-10-14 14:29:52 +00:00
public const int SIZE_G1STAD = 0x20000 ; // same as G3RAW
2022-06-18 18:04:24 +00:00
public const int SIZE_G1STADF = 0x1FF00 ;
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 ( ) ;
2021-01-06 23:46:43 +00:00
2021-02-13 09:53:38 +00:00
#if ! EXCLUDE_HACKS
2022-06-18 18:04:24 +00:00
/// <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 ( ) ;
2021-02-13 09:53:38 +00:00
#endif
#if ! EXCLUDE_EMULATOR_FORMATS
2022-06-18 18:04:24 +00:00
/// <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 ( ) ,
2022-06-25 18:14:00 +00:00
new SaveHandlerBizHawk ( ) ,
2022-06-18 18:04:24 +00:00
new SaveHandlerARDS ( ) ,
} ;
2021-02-13 09:53:38 +00:00
#endif
2021-01-06 23:46:43 +00:00
2023-05-01 23:51:17 +00:00
private static readonly HashSet < long > SizesSV = new ( )
2022-11-25 01:42:17 +00:00
{
SIZE_G9_0 , SIZE_G9_0a ,
SIZE_G9_1 , SIZE_G9_1a ,
SIZE_G9_1A , SIZE_G9_1Aa ,
2022-12-02 06:40:11 +00:00
SIZE_G9_1Ba , SIZE_G9_1Ab ,
SIZE_G9_2 , SIZE_G9_3 ,
2023-02-28 03:12:27 +00:00
SIZE_G9_3A0 , SIZE_G9_3A1 ,
SIZE_G9_3B0 , SIZE_G9_3B1 ,
SIZE_G9_3G0 , SIZE_G9_3G1 ,
2023-03-01 04:03:46 +00:00
SIZE_G9_3P0 , SIZE_G9_3P1 ,
2023-09-16 06:36:21 +00:00
SIZE_G9_DLC1_0 ,
SIZE_G9_DLC1_1 ,
SIZE_G9_DLC1_2 ,
SIZE_G9_DLC1_3 ,
SIZE_G9_DLC1_4 ,
SIZE_G9_DLC1_5 ,
SIZE_G9_DLC1_6 ,
SIZE_G9_DLC1_7 ,
SIZE_G9_DLC1_8 ,
2023-09-16 07:47:47 +00:00
SIZE_G9_DLC1_A ,
2023-09-16 16:40:09 +00:00
SIZE_G9_DLC1_B ,
SIZE_G9_DLC1_C ,
SIZE_G9_DLC1_D ,
SIZE_G9_DLC1_E ,
2022-11-25 01:42:17 +00:00
} ;
2023-05-01 23:51:17 +00:00
private static readonly HashSet < long > SizesSWSH = new ( )
2022-06-18 18:04:24 +00:00
{
SIZE_G8SWSH , SIZE_G8SWSH_1 , SIZE_G8SWSH_2 , SIZE_G8SWSH_2B , SIZE_G8SWSH_3 , SIZE_G8SWSH_3A , SIZE_G8SWSH_3B , SIZE_G8SWSH_3C ,
} ;
2020-10-10 20:14:32 +00:00
2023-05-01 23:51:17 +00:00
private static readonly HashSet < long > SizesGen2 = new ( )
2022-06-18 18:04:24 +00:00
{
SIZE_G2RAW_U , SIZE_G2VC_U , SIZE_G2BAT_U , SIZE_G2EMU_U , SIZE_G2RAW_J , SIZE_G2BAT_J , SIZE_G2EMU_J , SIZE_G2VC_J ,
} ;
2018-07-29 20:27:48 +00:00
2023-05-01 23:51:17 +00:00
private static readonly HashSet < long > Sizes = new ( SizesGen2 . Concat ( SizesSWSH ) . Concat ( SizesSV ) )
2022-06-18 18:04:24 +00:00
{
SIZE_G8LA , SIZE_G8LA_1 , SIZE_G8BDSP , SIZE_G8BDSP_1 , SIZE_G8BDSP_2 , SIZE_G8BDSP_3 ,
// 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 ,
2022-10-14 14:29:52 +00:00
SIZE_G3BOX , SIZE_G3COLO , SIZE_G3XD , SIZE_G3RAW , SIZE_G3EMU , SIZE_G3RAWHALF ,
2022-06-18 18:04:24 +00:00
// 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>
2023-01-22 04:02:33 +00:00
private static GameVersion GetSAVType ( ReadOnlySpan < byte > data )
2022-06-18 18:04:24 +00:00
{
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 ;
2022-11-25 01:42:17 +00:00
if ( ( ver = GetIsG9SAV ( data ) ) ! = Invalid )
return ver ;
2022-06-18 18:04:24 +00:00
return Invalid ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
}
2019-11-16 01:34:18 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
// 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 ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) ;
}
2016-08-27 11:33:21 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
// 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 ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
}
2016-09-02 21:20:39 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2022-06-18 18:04:24 +00:00
if ( IsG12ListValid ( data , 0x283E , 30 ) )
return GS ;
if ( IsG12ListValid ( data , 0x281A , 30 ) )
return C ;
return Invalid ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 )
{
2022-10-14 14:29:52 +00:00
if ( data . Length is not ( SIZE_G3RAW or SIZE_G3EMU or SIZE_G3RAWHALF ) )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
// check the save file(s)
int count = data . Length / SIZE_G3RAWHALF ;
for ( int slot = 0 ; slot < count ; slot + + )
2016-09-02 21:20:39 +00:00
{
2022-06-18 18:04:24 +00:00
if ( ! SAV3 . IsAllMainSectorsPresent ( data , slot , out var smallOffset ) )
continue ;
// Detect RS/E/FRLG
return GetVersionG3SAV ( data [ smallOffset . . ] ) ;
2016-09-02 21:20:39 +00:00
}
2022-06-18 18:04:24 +00:00
return Invalid ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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.
2023-04-23 22:51:48 +00:00
var remainder = data [ 0x890 . . 0xF2C ] ;
if ( remainder . IndexOfAnyExcept < byte > ( 0 ) ! = - 1 )
return E ;
2022-06-18 18:04:24 +00:00
return RS ;
2017-09-11 02:56:21 +00:00
}
2022-06-18 18:04:24 +00:00
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
2016-06-26 21:23:41 +00:00
2022-06-18 18:04:24 +00:00
// 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 ;
}
2016-06-26 21:23:41 +00:00
2022-06-18 18:04:24 +00:00
/// <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 )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2021-03-16 06:51:58 +00:00
2022-06-18 18:04:24 +00:00
// Check the intro bytes for each save slot
const int offset = 0x6000 ;
for ( int i = 0 ; i < 3 ; i + + )
2016-09-19 05:47:31 +00:00
{
2022-06-18 18:04:24 +00:00
var ofs = offset + ( 0x1E000 * i ) ;
if ( ReadUInt32LittleEndian ( data [ ofs . . ] ) ! = 0x00000101 )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2016-09-19 05:47:31 +00:00
}
2022-06-18 18:04:24 +00:00
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 ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
// Check the intro bytes for each save slot
const int offset = 0x6000 ;
for ( int i = 0 ; i < 2 ; i + + )
2016-09-26 23:15:40 +00:00
{
2022-06-18 18:04:24 +00:00
var ofs = offset + ( 0x28000 * i ) ;
if ( ( ReadUInt32LittleEndian ( data [ ofs . . ] ) & 0xFFFE _FFFF ) ! = 0x00000101 )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2016-09-27 06:07:17 +00:00
}
2022-06-18 18:04:24 +00:00
return XD ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 )
2016-09-27 06:07:17 +00:00
{
2022-06-18 18:04:24 +00:00
var size = ReadUInt32LittleEndian ( data [ ( offset - 0xC ) . . ] ) ;
if ( size ! = ( offset & 0xFFFF ) )
return false ;
var sdk = ReadUInt32LittleEndian ( data [ ( offset - 0x8 ) . . ] ) ;
2016-09-27 06:07:17 +00:00
2022-06-18 18:04:24 +00:00
const int DATE_INT = 0x20060623 ;
const int DATE_KO = 0x20070903 ;
return sdk is DATE_INT or DATE_KO ;
2016-09-26 23:15:40 +00:00
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
// 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 ;
2018-05-12 15:13:39 +00:00
2022-06-18 18:04:24 +00:00
return Invalid ;
}
2016-09-13 01:01:26 +00:00
2022-06-18 18:04:24 +00:00
/// <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 )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
byte [ ] sav = SAV4BR . DecryptPBRSaveData ( data ) ;
return SAV4BR . IsChecksumsValid ( sav ) ? BATREV : Invalid ;
}
2016-09-26 23:14:11 +00:00
2022-06-18 18:04:24 +00:00
/// <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 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 ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
static bool IsValidFooter ( ReadOnlySpan < byte > data , int mainSize , int infoLength )
2016-06-20 04:22:43 +00:00
{
2022-06-18 18:04:24 +00:00
var footer = data . Slice ( mainSize - 0x100 , infoLength + 0x10 ) ;
ushort stored = ReadUInt16LittleEndian ( footer [ ^ 2. . ] ) ;
ushort actual = Checksums . CRC16_CCITT ( footer [ . . infoLength ] ) ;
return stored = = actual ;
}
}
2016-06-20 04:22:43 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) )
PKHeX.Core Nullable cleanup (#2401)
* Handle some nullable cases
Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data)
Make some classes have explicit constructors instead of { } initialization
* Handle bits more obviously without null
* Make SaveFile.BAK explicitly readonly again
* merge constructor methods to have readonly fields
* Inline some properties
* More nullable handling
* Rearrange box actions
define straightforward classes to not have any null properties
* Make extrabyte reference array immutable
* Move tooltip creation to designer
* Rearrange some logic to reduce nesting
* Cache generated fonts
* Split mystery gift album purpose
* Handle more tooltips
* Disallow null setters
* Don't capture RNG object, only type enum
* Unify learnset objects
Now have readonly properties which are never null
don't new() empty learnsets (>800 Learnset objects no longer created,
total of 2400 objects since we also new() a move & level array)
optimize g1/2 reader for early abort case
* Access rewrite
Initialize blocks in a separate object, and get via that object
removes a couple hundred "might be null" warnings since blocks are now readonly getters
some block references have been relocated, but interfaces should expose all that's needed
put HoF6 controls in a groupbox, and disable
* Readonly personal data
* IVs non nullable for mystery gift
* Explicitly initialize forced encounter moves
* Make shadow objects readonly & non-null
Put murkrow fix in binary data resource, instead of on startup
* Assign dex form fetch on constructor
Fixes legality parsing edge cases
also handle cxd parse for valid; exit before exception is thrown in FrameGenerator
* Remove unnecessary null checks
* Keep empty value until init
SetPouch sets the value to an actual one during load, but whatever
* Readonly team lock data
* Readonly locks
Put locked encounters at bottom (favor unlocked)
* Mail readonly data / offset
Rearrange some call flow and pass defaults
Add fake classes for SaveDataEditor mocking
Always party size, no need to check twice in stat editor
use a fake save file as initial data for savedata editor, and for
gamedata (wow i found a usage)
constrain eventwork editor to struct variable types (uint, int, etc),
thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
return Invalid ;
2022-03-06 01:14:25 +00:00
2022-06-18 18:04:24 +00:00
if ( ReadUInt32LittleEndian ( data [ ^ 0x1F0 . . ] ) ! = BEEF )
return Invalid ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
return data . Length switch
2016-06-20 04:22:43 +00:00
{
2022-06-18 18:04:24 +00:00
SIZE_G6XY = > XY ,
SIZE_G6ORAS = > ORAS ,
_ = > ORASDEMO , // least likely
} ;
}
2016-06-20 04:22:43 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
2016-06-20 04:22:43 +00:00
2022-06-18 18:04:24 +00:00
if ( ReadUInt32LittleEndian ( data [ ^ 0x1F0 . . ] ) ! = BEEF )
return Invalid ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
return data . Length = = SIZE_G7SM ? SM : USUM ;
}
2017-10-18 06:19:34 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
2017-10-18 06:19:34 +00:00
2022-06-18 18:04:24 +00:00
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 ;
2016-06-26 18:46:08 +00:00
2022-06-18 18:04:24 +00:00
return GG ;
}
2018-11-14 03:18:29 +00:00
2022-06-18 18:04:24 +00:00
/// <summary>Checks to see if the data belongs to a Gen8 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>
2023-01-22 04:02:33 +00:00
private static GameVersion GetIsG8SAV ( ReadOnlySpan < byte > data )
2022-06-18 18:04:24 +00:00
{
if ( ! SizesSWSH . Contains ( data . Length ) )
return Invalid ;
2018-11-14 03:18:29 +00:00
2022-06-18 18:04:24 +00:00
return SwishCrypto . GetIsHashValid ( data ) ? SWSH : Invalid ;
}
2018-11-14 03:18:29 +00:00
2022-06-18 18:04:24 +00:00
private static GameVersion GetIsG8SAV_BDSP ( ReadOnlySpan < byte > data )
{
if ( data . Length is not ( SIZE_G8BDSP or SIZE_G8BDSP_1 or SIZE_G8BDSP_2 or SIZE_G8BDSP_3 ) )
return Invalid ;
2019-09-23 23:56:47 +00:00
2022-06-18 18:04:24 +00:00
var ver = ( Gem8Version ) ReadUInt32LittleEndian ( data ) ;
if ( ver is not ( Gem8Version . V1_0 or Gem8Version . V1_1 or Gem8Version . V1_2 or Gem8Version . V1_3 ) )
return Invalid ;
2019-09-23 23:56:47 +00:00
2022-06-18 18:04:24 +00:00
return BDSP ;
}
2021-11-20 02:23:49 +00:00
2023-01-22 04:02:33 +00:00
private static GameVersion GetIsG8SAV_LA ( ReadOnlySpan < byte > data )
2022-06-18 18:04:24 +00:00
{
if ( data . Length is not ( SIZE_G8LA or SIZE_G8LA_1 ) )
return Invalid ;
2022-03-26 02:24:37 +00:00
2022-06-18 18:04:24 +00:00
return SwishCrypto . GetIsHashValid ( data ) ? PLA : Invalid ;
}
2021-11-20 02:23:49 +00:00
2022-11-25 01:42:17 +00:00
/// <summary>Checks to see if the data belongs to a Gen8 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>
2023-01-22 04:02:33 +00:00
private static GameVersion GetIsG9SAV ( ReadOnlySpan < byte > data )
2022-11-25 01:42:17 +00:00
{
if ( ! SizesSV . Contains ( data . Length ) )
return Invalid ;
return SwishCrypto . GetIsHashValid ( data ) ? SV : Invalid ;
}
2022-06-18 18:04:24 +00:00
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 )
{
// Many things can go wrong with loading save data (file no longer present toc-tou, or bad save layout).
try
2022-02-05 01:31:20 +00:00
{
2022-06-18 18:04:24 +00:00
var data = File . ReadAllBytes ( path ) ;
var sav = GetVariantSAV ( data , path ) ;
sav ? . Metadata . SetExtraInfo ( path ) ;
return sav ;
2022-02-05 01:31:20 +00:00
}
2022-06-18 18:04:24 +00:00
catch ( Exception ex )
2018-07-15 20:35:58 +00:00
{
2022-06-18 18:04:24 +00:00
System . Diagnostics . Debug . WriteLine ( ex ) ;
return null ;
2018-07-15 20:35:58 +00:00
}
2022-06-18 18:04:24 +00:00
}
2018-07-15 20:35:58 +00:00
2022-06-18 18:04:24 +00:00
/// <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 )
{
2021-02-13 09:53:38 +00:00
#if ! EXCLUDE_HACKS
2022-06-18 18:04:24 +00:00
foreach ( var h in CustomSaveReaders )
{
if ( ! h . IsRecognized ( data . Length ) )
continue ;
2021-02-13 09:53:38 +00:00
2022-06-18 18:04:24 +00:00
var custom = h . ReadSaveFile ( data , path ) ;
if ( custom ! = null )
return custom ;
}
2021-02-13 09:53:38 +00:00
#endif
2022-06-18 18:04:24 +00:00
var sav = GetVariantSAVInternal ( data ) ;
if ( sav ! = null )
return sav ;
2020-12-05 13:36:23 +00:00
2021-02-13 09:53:38 +00:00
#if ! EXCLUDE_EMULATOR_FORMATS
2022-06-18 18:04:24 +00:00
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 ;
}
2021-02-13 09:53:38 +00:00
#endif
2021-01-06 23:46:43 +00:00
2022-06-18 18:04:24 +00:00
// 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 ) ,
2022-11-25 01:42:17 +00:00
SV = > new SAV9SV ( data ) ,
2022-06-18 18:04:24 +00:00
// 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
2023-01-22 04:02:33 +00:00
var memory = memCard . ReadSaveGameData ( ) ;
if ( memory . Length = = 0 )
2021-01-06 23:46:43 +00:00
return null ;
2016-10-12 02:11:24 +00:00
2023-01-22 04:02:33 +00:00
var split = DolphinHandler . TrySplit ( memory . Span ) ;
var data = split ! = null ? split . Data : memory . ToArray ( ) ;
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
SaveFile sav ;
switch ( memCard . SelectedGameVersion )
2017-04-02 14:53:46 +00:00
{
2022-06-18 18:04:24 +00:00
// Side Games
case COLO : sav = new SAV3Colosseum ( data ) { MemoryCard = memCard } ; break ;
case XD : sav = new SAV3XD ( data ) { MemoryCard = memCard } ; break ;
case RSBOX : sav = new SAV3RSBox ( data , memCard ) { MemoryCard = memCard } ; break ;
2021-01-06 23:46:43 +00:00
2022-06-18 18:04:24 +00:00
// No pattern matched
default : return null ;
}
2017-04-02 14:53:46 +00:00
2022-06-18 18:04:24 +00:00
if ( split ! = null )
sav . Metadata . SetExtraInfo ( split . Header , split . Footer ) ;
return sav ;
}
2017-04-02 14:53:46 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ,
} ;
2020-12-05 13:36:23 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ? "P K H e X " : "1337" ,
_ = > "PKHeX" ,
} ;
2016-06-26 18:46:08 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ;
2020-10-03 18:22:06 +00:00
2022-06-18 18:04:24 +00:00
// Secondary Properties may not be used but can be filled in as template.
2023-01-22 04:02:33 +00:00
( uint tid , uint sid ) = sav . Generation > = 7 ? ( 123456 u , 1234 u ) : ( 12345 u , 54321 u ) ;
sav . SetDisplayID ( tid , sid ) ;
2022-06-18 18:04:24 +00:00
sav . Language = ( int ) language ;
2019-12-04 22:00:53 +00:00
2022-06-18 18:04:24 +00:00
// Only set geolocation data for 3DS titles
if ( sav is IRegionOrigin o )
2023-08-12 23:01:16 +00:00
o . SetDefaultRegionOrigins ( ( int ) language ) ;
2017-01-14 21:10:36 +00:00
2022-06-18 18:04:24 +00:00
return sav ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
GD or SI or GS = > new SAV2 ( version : GS , lang : language ) ,
C or GSC = > new SAV2 ( version : C , lang : language ) ,
Stadium2 = > new SAV2Stadium ( language = = LanguageID . Japanese ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
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 ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
CXD or COLO = > new SAV3Colosseum ( ) ,
XD = > new SAV3XD ( ) ,
RSBOX = > new SAV3RSBox ( ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
D or P or DP = > new SAV4DP ( ) ,
Pt or DPPt = > new SAV4Pt ( ) ,
HG or SS or HGSS = > new SAV4HGSS ( ) ,
BATREV = > new SAV4BR ( ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
B or W or BW = > new SAV5BW ( ) ,
B2 or W2 or B2W2 = > new SAV5B2W2 ( ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
X or Y or XY = > new SAV6XY ( ) ,
ORASDEMO = > new SAV6AODemo ( ) ,
OR or AS or ORAS = > new SAV6AO ( ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
SN or MN or SM = > new SAV7SM ( ) ,
US or UM or USUM = > new SAV7USUM ( ) ,
GP or GE or GG or GO = > new SAV7b ( ) ,
2020-12-24 01:14:38 +00:00
2022-06-18 18:04:24 +00:00
SW or SH or SWSH = > new SAV8SWSH ( ) ,
BD or SP or BDSP = > new SAV8BS ( ) ,
PLA = > new SAV8LA ( ) ,
2020-12-24 01:14:38 +00:00
2022-11-25 01:42:17 +00:00
SL or VL or SV = > new SAV9SV ( ) ,
2022-06-18 18:04:24 +00:00
_ = > throw new ArgumentOutOfRangeException ( nameof ( game ) ) ,
} ;
/// <summary>
/// Creates an instance of a SaveFile with a blank base.
/// </summary>
/// <param name="context">Context 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 ( EntityContext context , string trainerName , LanguageID language = LanguageID . English )
{
var ver = context . GetSingleGameVersion ( ) ;
return GetBlankSAV ( ver , trainerName , language ) ;
}
2018-07-29 20:27:48 +00:00
2022-06-18 18:04:24 +00:00
/// <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 ) )
2017-01-14 21:10:36 +00:00
{
2022-06-18 18:04:24 +00:00
result = Array . Empty < string > ( ) ;
return false ;
2017-01-14 21:10:36 +00:00
}
2022-06-18 18:04:24 +00:00
try
{
var searchOption = deep ? SearchOption . AllDirectories : SearchOption . TopDirectoryOnly ;
2023-05-01 23:51:17 +00:00
var files = Directory . EnumerateFiles ( folderPath , "*" , searchOption )
. IterateSafe ( log : z = > System . Diagnostics . Debug . WriteLine ( z ) ) ;
result = FilterSaveFiles ( ignoreBackups , files ) ;
2022-06-18 18:04:24 +00:00
return true ;
}
catch ( Exception ex )
2016-08-05 03:48:52 +00:00
{
2022-06-18 18:04:24 +00:00
result = new [ ]
2016-12-03 17:32:18 +00:00
{
2022-06-18 18:04:24 +00:00
MsgFileLoadFailAuto + Environment . NewLine + folderPath ,
MsgFileLoadFailAutoAdvise + Environment . NewLine + MsgFileLoadFailAutoCause ,
ex . Message ,
} ;
return false ;
2016-08-05 03:48:52 +00:00
}
2022-06-18 18:04:24 +00:00
}
2016-08-05 03:48:52 +00:00
2023-05-01 23:51:17 +00:00
private static IEnumerable < string > FilterSaveFiles ( bool ignoreBackups , IEnumerable < string > files )
{
foreach ( string file in files )
{
if ( ignoreBackups & & IsBackup ( file ) )
continue ;
var size = FileUtil . GetFileSize ( file ) ;
if ( ! IsSizeValid ( size ) )
continue ;
yield return file ;
}
}
2023-07-14 05:18:34 +00:00
public static bool IsBackup ( ReadOnlySpan < char > path )
2023-05-01 23:51:17 +00:00
{
var fn = Path . GetFileNameWithoutExtension ( path ) ;
2023-07-14 05:18:34 +00:00
if ( fn is "backup" )
2023-05-01 23:51:17 +00:00
return true ;
var ext = Path . GetExtension ( path ) ;
2023-07-14 05:18:34 +00:00
return ext is ".bak" ;
2023-05-01 23:51:17 +00:00
}
2022-02-05 01:31:20 +00:00
2022-06-18 18:04:24 +00:00
/// <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>
2023-05-01 23:51:17 +00:00
public static bool IsSizeValid ( long size ) = > IsSizeValidNoHandler ( size ) | | IsSizeValidHandler ( size ) | | SAV3GCMemoryCard . IsMemoryCardSize ( size ) ;
2022-06-25 18:14:00 +00:00
/// <summary>
/// Determines whether the save data size is valid for automatically detecting saves.
/// </summary>
/// <remarks>Only checks the <see cref="Handlers"/> list.</remarks>
2023-05-01 23:51:17 +00:00
public static bool IsSizeValidHandler ( long size ) = > Handlers . Any ( z = > z . IsRecognized ( size ) ) ;
2022-06-25 18:14:00 +00:00
/// <summary>
/// Determines whether the save data size is valid for automatically detecting saves.
/// </summary>
/// <remarks>Does not check the <see cref="Handlers"/> list.</remarks>
2023-05-01 23:51:17 +00:00
public static bool IsSizeValidNoHandler ( long size ) = > Sizes . Contains ( size ) ;
2016-06-20 04:22:43 +00:00
}