diff --git a/PKHeX.Core/Saves/Blocks/BlockInfo3DS.cs b/PKHeX.Core/Saves/Blocks/BlockInfo3DS.cs
index 5b88af3cd..aeeb04b4b 100644
--- a/PKHeX.Core/Saves/Blocks/BlockInfo3DS.cs
+++ b/PKHeX.Core/Saves/Blocks/BlockInfo3DS.cs
@@ -39,7 +39,27 @@ public static BlockInfo[] GetBlockInfoData(byte[] data, out int blockInfoOffset,
blockInfoOffset = data.Length - 0x200 + 0x10;
if (BitConverter.ToUInt32(data, blockInfoOffset) != SaveUtil.BEEF)
blockInfoOffset -= 0x200; // No savegames have more than 0x3D blocks, maybe in the future?
- int count = (data.Length - blockInfoOffset - 0x8) / 8;
+ int len = data.Length;
+ return GetBlockInfo(data, ref blockInfoOffset, CheckFunc, len);
+ }
+
+ ///
+ /// Gets the table for the input .
+ ///
+ /// Complete data array
+ /// Offset the starts at.
+ /// Checksum method for validating each block.
+ ///
+ public static BlockInfo[] GetBlockInfoData(byte[] data, ref int blockInfoOffset, Func CheckFunc, int dataLength)
+ {
+ if (BitConverter.ToUInt32(data, blockInfoOffset) != SaveUtil.BEEF)
+ blockInfoOffset -= 0x200; // No savegames have more than 0x3D blocks, maybe in the future?
+ return GetBlockInfo(data, ref blockInfoOffset, CheckFunc, dataLength);
+ }
+
+ private static BlockInfo[] GetBlockInfo(byte[] data, ref int blockInfoOffset, Func CheckFunc, int dataLength)
+ {
+ int count = (dataLength - blockInfoOffset - 0x8) / 8;
blockInfoOffset += 4;
var Blocks = new BlockInfo[count];
diff --git a/PKHeX.Core/Saves/SAV7b.cs b/PKHeX.Core/Saves/SAV7b.cs
new file mode 100644
index 000000000..0bd4304bc
--- /dev/null
+++ b/PKHeX.Core/Saves/SAV7b.cs
@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace PKHeX.Core
+{
+ public sealed class SAV7b : SaveFile
+ {
+ protected override string BAKText => $"{OT} ({Version}) - {Played.LastSavedTime}";
+ public override string Filter => "Main SAV|*.*";
+ public override string Extension => string.Empty;
+ public override string[] PKMExtensions => PKM.Extensions.Where(f => f[0] == 'B' && 7 == f[f.Length - 1] - 0x30).ToArray();
+
+ public override Type PKMType => typeof(PB7);
+ public override PKM BlankPKM => new PB7();
+ public override int SIZE_STORED => SIZE_PARTY;
+ protected override int SIZE_PARTY => 260;
+
+ public override SaveFile Clone() => new SAV7b(Data);
+
+ public SAV7b() : this(new byte[SaveUtil.SIZE_G7GG]) { }
+
+ public SAV7b(byte[] data)
+ {
+ Data = data;
+ BAK = (byte[])Data.Clone();
+ Exportable = !IsRangeEmpty(0, Data.Length);
+
+ // Load Info
+ const int len = 0xB8800; // 1mb always allocated
+ BlockInfoOffset = len - 0x1F0;
+ Blocks = BlockInfo3DS.GetBlockInfoData(Data, ref BlockInfoOffset, SaveUtil.CRC16NoInvert, len);
+ Personal = PersonalTable.GG;
+
+ Box = GetBlockOffset(BelugaBlockIndex.PokeListPokemon);
+ Party = GetBlockOffset(BelugaBlockIndex.PokeListPokemon);
+ EventFlag = GetBlockOffset(BelugaBlockIndex.EventWork);
+ PokeDex = GetBlockOffset(BelugaBlockIndex.Zukan);
+ Zukan = new Zukan7b(this, PokeDex, 0x550);
+ Config = new ConfigSave7b(this);
+ Items = new MyItem7b(this);
+ Storage = new PokeListHeader(this);
+ Status = new MyStatus7b(this);
+ Played = new PlayTime7b(this);
+ Misc = new Misc7b(this);
+ EventWork = new EventWork7b(this);
+
+ HeldItems = Legal.HeldItems_GG;
+
+ if (Exportable)
+ CanReadChecksums();
+ else
+ ClearBoxes();
+ }
+
+ // Save Block accessors
+ public readonly MyItem Items;
+ public readonly Misc7b Misc;
+ public readonly Zukan7b Zukan;
+ public readonly MyStatus7b Status;
+ public readonly PlayTime7b Played;
+ public readonly ConfigSave7b Config;
+ public readonly EventWork7b EventWork;
+ public readonly PokeListHeader Storage;
+
+ public override InventoryPouch[] Inventory { get => Items.Inventory; set => Items.Inventory = value; }
+
+ // Feature Overrides
+ public override int Generation => 7;
+ public override int MaxMoveID => Legal.MaxMoveID_7b;
+ public override int MaxSpeciesID => Legal.MaxSpeciesID_7b;
+ public override int MaxItemID => Legal.MaxItemID_7b;
+ public override int MaxBallID => Legal.MaxBallID_7b;
+ public override int MaxGameID => Legal.MaxGameID_7b;
+ public override int MaxAbilityID => Legal.MaxAbilityID_7b;
+
+ public override int MaxIV => 31;
+ public override int MaxEV => 252;
+ public override int OTLength => 12;
+ public override int NickLength => 12;
+ protected override int GiftCountMax => 48;
+ protected override int GiftFlagMax => 0x100 * 8;
+ protected override int EventFlagMax => 4160; // 0xDC0 (true max may be up to 0x7F less. 23A8 starts u64 hashvals)
+ protected override int EventConstMax => 1000;
+
+ public override bool HasParty => false; // handled via team slots
+ public override bool HasEvents => true; // advanced!
+
+ // BoxSlotCount => 1000 -- why couldn't this be a multiple of 30...
+ public override int BoxSlotCount => 25;
+ public override int BoxCount => 40; // 1000/25
+
+ // Blocks & Offsets
+ private readonly int BlockInfoOffset;
+ private readonly BlockInfo[] Blocks;
+ public override bool ChecksumsValid => CanReadChecksums() && Blocks.GetChecksumsValid(Data);
+ public override string ChecksumInfo => CanReadChecksums() ? Blocks.GetChecksumInfo(Data) : string.Empty;
+
+ public BlockInfo GetBlock(BelugaBlockIndex index) => Blocks[(int)index];
+ public int GetBlockOffset(BelugaBlockIndex index) => Blocks[(int)index].Offset;
+
+ private bool CanReadChecksums()
+ {
+ if (Blocks.Length <= 3)
+ { Debug.WriteLine($"Not enough blocks ({Blocks.Length}), aborting {nameof(CanReadChecksums)}"); return false; }
+ return true;
+ }
+
+ protected override void SetChecksums()
+ {
+ if (!CanReadChecksums())
+ return;
+ Blocks.SetChecksums(Data);
+ }
+
+ public bool FixPreWrite() => Storage.CompressStorage();
+
+ protected override void SetPKM(PKM pkm)
+ {
+ var pk = (PB7)pkm;
+ // Apply to this Save File
+ int CT = pk.CurrentHandler;
+ var Date = DateTime.Now;
+ pk.Trade(OT, TID, SID, Country, SubRegion, Gender, false, Date.Day, Date.Month, Date.Year);
+ if (CT != pk.CurrentHandler) // Logic updated Friendship
+ {
+ // Copy over the Friendship Value only under certain circumstances
+ if (pk.Moves.Contains(216)) // Return
+ pk.CurrentFriendship = pk.OppositeFriendship;
+ else if (pk.Moves.Contains(218)) // Frustration
+ pk.CurrentFriendship = pk.OppositeFriendship;
+ }
+ pk.RefreshChecksum();
+ }
+
+ protected override void SetDex(PKM pkm) => Zukan.SetDex(pkm);
+ public override bool GetCaught(int species) => Zukan.GetCaught(species);
+ public override bool GetSeen(int species) => Zukan.GetSeen(species);
+
+ public override PKM GetPKM(byte[] data) => new PB7(data);
+ public override byte[] DecryptPKM(byte[] data) => PKX.DecryptArray(data);
+ public override int GetBoxOffset(int box) => Box + (box * BoxSlotCount * SIZE_STORED);
+ protected override IList[] SlotPointers => new[] { Storage.PokeListInfo };
+
+ public override int GetPartyOffset(int slot) => Storage.GetPartyOffset(slot);
+ public override int PartyCount { get => Storage.PartyCount; protected set => Storage.PartyCount = value; }
+ public override bool IsSlotInBattleTeam(int box, int slot) => Storage.IsSlotInBattleTeam(box, slot);
+ public override bool IsSlotLocked(int box, int slot) => Storage.IsSlotLocked(box, slot);
+ protected override bool IsSlotOverwriteProtected(int box, int slot) => false;
+
+ public override string GetBoxName(int box) => $"Box {box + 1}";
+ public override void SetBoxName(int box, string value) { }
+
+ public override string GetString(int Offset, int Length) => StringConverter.GetString7(Data, Offset, Length);
+
+ public override byte[] SetString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0)
+ {
+ if (PadToSize == 0)
+ PadToSize = maxLength + 1;
+ return StringConverter.SetString7(value, maxLength, Language, PadToSize, PadWith);
+ }
+
+ public override ulong? Secure1
+ {
+ get => BitConverter.ToUInt64(Data, BlockInfoOffset - 0x14);
+ set => BitConverter.GetBytes(value ?? 0).CopyTo(Data, BlockInfoOffset - 0x14);
+ }
+
+ public override ulong? Secure2
+ {
+ get => BitConverter.ToUInt64(Data, BlockInfoOffset - 0xC);
+ set => BitConverter.GetBytes(value ?? 0).CopyTo(Data, BlockInfoOffset - 0xC);
+ }
+
+ public override GameVersion Version
+ {
+ get
+ {
+ switch (Game)
+ {
+ case (int)GameVersion.GP: return GameVersion.GP;
+ case (int)GameVersion.GE: return GameVersion.GE;
+ default: return GameVersion.Invalid;
+ }
+ }
+ }
+
+ // Player Information
+ public override int TID { get => Status.TID; set => Status.TID = value; }
+ public override int SID { get => Status.SID; set => Status.SID = value; }
+ public override int Game { get => Status.Game; set => Status.Game = value; }
+ public override int Gender { get => Status.Gender; set => Status.Gender = value; }
+ public override int Language { get => Status.Language; set => Status.Language = value; }
+ public override string OT { get => Status.OT; set => Status.OT = value; }
+ public override uint Money { get => Misc.Money; set => Misc.Money = value; }
+
+ public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = value; }
+ public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = value; }
+ public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = value; }
+
+ ///
+ /// Gets the status of a desired Event Flag
+ ///
+ /// Event Flag to check
+ /// Flag is Set (true) or not Set (false)
+ public override bool GetEventFlag(int flagNumber) => EventWork.GetFlag(flagNumber);
+
+ ///
+ /// Sets the status of a desired Event Flag
+ ///
+ /// Event Flag to check
+ /// Event Flag status to set
+ /// Flag is Set (true) or not Set (false)
+ public override void SetEventFlag(int flagNumber, bool value) => EventWork.SetFlag(flagNumber, value);
+ }
+}
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/ConfigSave7b.cs b/PKHeX.Core/Saves/Substructures/Gen7/ConfigSave7b.cs
new file mode 100644
index 000000000..039d488e8
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/ConfigSave7b.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace PKHeX.Core
+{
+ public sealed class ConfigSave7b : SaveBlock
+ {
+ public ConfigSave7b(SAV7b sav) : base(sav)
+ {
+ Offset = sav.GetBlockOffset(BelugaBlockIndex.ConfigSave);
+ }
+
+ public int ConfigValue
+ {
+ get => BitConverter.ToInt32(Data, Offset);
+ set => BitConverter.GetBytes(value).CopyTo(Data, Offset);
+ }
+
+ public int TalkingSpeed
+ {
+ get => ConfigValue & 3;
+ set => ConfigValue = (ConfigValue & ~3) | (value & 3);
+ }
+
+ public BattleAnimationSetting BattleAnimation
+ {
+ // Effects OFF = 1, Effects ON = 0
+ get => (BattleAnimationSetting)((ConfigValue >> 2) & 1);
+ set => ConfigValue = (ConfigValue & ~(1 << 2)) | ((byte)value << 2);
+ }
+
+ public BattleStyleSetting BattleStyle
+ {
+ // SET = 1, SWITCH = 0
+ get => (BattleStyleSetting)((ConfigValue >> 3) & 1);
+ set => ConfigValue = (ConfigValue & ~(1 << 3)) | ((byte)value << 3);
+ }
+
+ public enum BattleAnimationSetting
+ {
+ EffectsON,
+ EffectsOFF,
+ }
+
+ public enum BattleStyleSetting
+ {
+ SET,
+ SWITCH,
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/EventWork7b.cs b/PKHeX.Core/Saves/Substructures/Gen7/EventWork7b.cs
new file mode 100644
index 000000000..a4b060230
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/EventWork7b.cs
@@ -0,0 +1,183 @@
+using System;
+
+namespace PKHeX.Core
+{
+ public sealed class EventWork7b : SaveBlock, IEventWork
+ {
+ public EventWork7b(SAV7b sav) : base(sav)
+ {
+ Offset = sav.GetBlockOffset(BelugaBlockIndex.EventWork);
+ // Zone @ 0x21A0 - 0x21AF (128 flags)
+ // System @ 0x21B0 - 0x21EF (512 flags) -- is this really 256 instead, with another 256 region after for the small vanish?
+ // Vanish @ 0x21F0 - 0x22AF (1536 flags)
+ // Event @ 0x22B0 - 0x23A7 (rest of the flags) (512) -- I think trainer flags are afterwards.... For now, this is a catch-all
+
+ // time flags (39 used flags of 42) = 6 bytes 0x22F0-0x22F5
+ // trainer flags (???) = 0x22F6 - end?
+
+ }
+
+ // Overall Layout
+ private const int WorkCount = 1000;
+ private const int WorkSize = sizeof(int);
+ private const int FlagStart = WorkCount * WorkSize;
+ private const int FlagCount = EventFlagStart + EventFlagCount;
+
+ // Breakdown!
+ private const int ZoneWorkCount = 0x20; // 32
+ private const int SystemWorkCount = 0x80; // 128
+ private const int SceneWorkCount = 0x200; // 512
+ private const int EventWorkCount = 0x100; // 256
+ // private const int UnusedWorkCount = 72;
+
+ private const int ZoneWorkStart = 0;
+ private const int SystemWorkStart = ZoneWorkStart + ZoneWorkCount;
+ private const int SceneWorkStart = SystemWorkStart + SystemWorkCount;
+ private const int EventWorkStart = SceneWorkStart + SceneWorkCount;
+
+ private const int ZoneFlagCount = 0x80; // 128
+ private const int SystemFlagCount = 0x200; // 512
+ private const int VanishFlagCount = 0x600; // 1536
+ private const int EventFlagCount = 0x7C0; // 1984
+
+ private const int ZoneFlagStart = 0;
+ private const int SystemFlagStart = ZoneFlagStart + ZoneFlagCount;
+ private const int VanishFlagStart = SystemFlagStart + SystemFlagCount;
+ private const int EventFlagStart = VanishFlagStart + VanishFlagCount;
+
+ public int MaxFlag => FlagCount;
+ public int MaxWork => WorkCount;
+
+ public int GetWork(int index) => BitConverter.ToInt32(Data, Offset + (index * WorkSize));
+ public void SetWork(int index, int value) => BitConverter.GetBytes(value).CopyTo(Data, Offset + (index * WorkSize));
+ public int GetWork(EventVarType type, int index) => GetWork(GetWorkRawIndex(type, index));
+ public void SetWork(EventVarType type, int index, int value) => SetWork(GetWorkRawIndex(type, index), value);
+ public bool GetFlag(EventVarType type, int index) => GetFlag(GetFlagRawIndex(type, index));
+ public void SetFlag(EventVarType type, int index, bool value = true) => SetFlag(GetFlagRawIndex(type, index), value);
+
+ public int GetFlagRawIndex(EventVarType type, int index)
+ {
+ int max = GetFlagCount(type);
+ if ((uint)index > max)
+ throw new ArgumentException(nameof(index));
+ var start = GetFlagStart(type);
+ return start + index;
+ }
+
+ public int GetWorkRawIndex(EventVarType type, int index)
+ {
+ int max = GetWorkCount(type);
+ if ((uint)index > max)
+ throw new ArgumentException(nameof(index));
+ var start = GetWorkStart(type);
+ return start + index;
+ }
+
+ public bool GetFlag(int index)
+ {
+ var offset = Offset + FlagStart + (index >> 3);
+ var current = Data[offset];
+ return (current & (1 << (index & 7))) != 0;
+ }
+
+ public void SetFlag(int index, bool value = true)
+ {
+ var offset = Offset + FlagStart + (index >> 3);
+ var bit = 1 << (index & 7);
+ if (value)
+ Data[offset] |= (byte)bit;
+ else
+ Data[offset] &= (byte)~bit;
+ }
+
+ public EventVarType GetFlagType(int index, out int subIndex)
+ {
+ subIndex = index;
+ if (index < ZoneFlagCount)
+ return EventVarType.Zone;
+ subIndex -= ZoneFlagCount;
+
+ if (subIndex < SystemFlagCount)
+ return EventVarType.System;
+ subIndex -= SystemFlagCount;
+
+ if (subIndex < VanishFlagCount)
+ return EventVarType.Vanish;
+ subIndex -= VanishFlagCount;
+
+ if (subIndex < EventFlagCount)
+ return EventVarType.Event;
+
+ throw new ArgumentException(nameof(index));
+ }
+
+ public EventVarType GetWorkType(int index, out int subIndex)
+ {
+ subIndex = index;
+ if (subIndex < ZoneWorkCount)
+ return EventVarType.Zone;
+ subIndex -= ZoneWorkCount;
+
+ if (subIndex < SystemWorkCount)
+ return EventVarType.System;
+ subIndex -= SystemWorkCount;
+
+ if (subIndex < SceneWorkCount)
+ return EventVarType.Scene;
+ subIndex -= SceneWorkCount;
+
+ if (subIndex < EventWorkCount)
+ return EventVarType.Event;
+
+ throw new ArgumentException(nameof(index));
+ }
+
+ private int GetFlagStart(EventVarType type)
+ {
+ switch (type)
+ {
+ case EventVarType.Zone: return ZoneFlagStart;
+ case EventVarType.System: return SystemFlagStart;
+ case EventVarType.Vanish: return VanishFlagStart;
+ case EventVarType.Event: return EventFlagStart;
+ }
+ throw new ArgumentException(nameof(type));
+ }
+
+ private int GetWorkStart(EventVarType type)
+ {
+ switch (type)
+ {
+ case EventVarType.Zone: return ZoneWorkStart;
+ case EventVarType.System: return SystemWorkStart;
+ case EventVarType.Scene: return SceneWorkStart;
+ case EventVarType.Event: return EventWorkStart;
+ }
+ throw new ArgumentException(nameof(type));
+ }
+
+ private int GetFlagCount(EventVarType type)
+ {
+ switch (type)
+ {
+ case EventVarType.Zone: return ZoneFlagCount;
+ case EventVarType.System: return SystemFlagCount;
+ case EventVarType.Vanish: return VanishFlagCount;
+ case EventVarType.Event: return EventFlagCount;
+ }
+ throw new ArgumentException(nameof(type));
+ }
+
+ private int GetWorkCount(EventVarType type)
+ {
+ switch (type)
+ {
+ case EventVarType.Zone: return ZoneWorkCount;
+ case EventVarType.System: return SystemWorkCount;
+ case EventVarType.Scene: return SceneWorkCount;
+ case EventVarType.Event: return EventWorkCount;
+ }
+ throw new ArgumentException(nameof(type));
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/Misc7b.cs b/PKHeX.Core/Saves/Substructures/Gen7/Misc7b.cs
new file mode 100644
index 000000000..1d745a1c2
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/Misc7b.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace PKHeX.Core
+{
+ public sealed class Misc7b : SaveBlock
+ {
+ private readonly SaveFile SAV;
+
+ public Misc7b(SaveFile sav) : base(sav)
+ {
+ SAV = sav;
+ Offset = ((SAV7b)sav).GetBlockOffset(BelugaBlockIndex.Misc);
+ }
+
+ public uint Money
+ {
+ get => BitConverter.ToUInt32(Data, Offset + 4);
+ set => BitConverter.GetBytes(value).CopyTo(Data, Offset + 4);
+ }
+
+ public string Rival
+ {
+ get => SAV.GetString(Offset + 0x200, 0x1A);
+ set => SAV.SetString(value, SAV.OTLength).CopyTo(Data, Offset + 0x200);
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/MyItem7b.cs b/PKHeX.Core/Saves/Substructures/Gen7/MyItem7b.cs
new file mode 100644
index 000000000..e22d03e5f
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/MyItem7b.cs
@@ -0,0 +1,43 @@
+namespace PKHeX.Core
+{
+ public sealed class MyItem7b : MyItem
+ {
+ private const int Medicine = 0x0000; // 0
+ private const int TM = 0x00F0; // 1
+ private const int Candy = 0x02A0; // 2
+ private const int PowerUp = 0x05C0; // 3
+ private const int Catching = 0x0818; // 4
+ private const int Battle = 0x08E0; // 5
+ private const int Key = 0x0B38; // 6
+
+ public MyItem7b(SaveFile SAV) : base(SAV) { }
+
+ public override InventoryPouch[] Inventory
+ {
+ get
+ {
+ var pouch = new InventoryPouch[]
+ {
+ new InventoryPouch7b(InventoryType.Medicine, Legal.Pouch_Medicine_GG, 999, Medicine, PouchSize7b.Medicine),
+ new InventoryPouch7b(InventoryType.TMHMs, Legal.Pouch_TM_GG, 1, TM, PouchSize7b.TM),
+ new InventoryPouch7b(InventoryType.Balls, Legal.Pouch_Catching_GG, 999, Catching, PouchSize7b.Catching),
+ new InventoryPouch7b(InventoryType.Items, Legal.Pouch_Regular_GG, 999, Key, PouchSize7b.Items),
+ new InventoryPouch7b(InventoryType.BattleItems, Legal.Pouch_Battle_GG, 999, Battle, PouchSize7b.Battle),
+ new InventoryPouch7b(InventoryType.ZCrystals, Legal.Pouch_PowerUp_GG, 999, PowerUp, PouchSize7b.PowerUp),
+ new InventoryPouch7b(InventoryType.FreeSpace, Legal.Pouch_Candy_GG, 999, Candy, PouchSize7b.Candy),
+ };
+ foreach (var p in pouch)
+ p.GetPouch(Data);
+ return pouch;
+ }
+ set
+ {
+ foreach (var p in value)
+ {
+ ((InventoryPouch7b)p).SanitizeCounts();
+ p.SetPouch(Data);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/MyStatus7b.cs b/PKHeX.Core/Saves/Substructures/Gen7/MyStatus7b.cs
new file mode 100644
index 000000000..1caa8f62a
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/MyStatus7b.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace PKHeX.Core
+{
+ public sealed class MyStatus7b : SaveBlock
+ {
+ private readonly SaveFile SAV;
+
+ public MyStatus7b(SaveFile sav) : base(sav)
+ {
+ SAV = sav;
+ Offset = ((SAV7b)sav).GetBlockOffset(BelugaBlockIndex.MyStatus);
+ }
+
+ // Player Information
+
+ // idb uint8 offset: 0x58
+
+ public int TID
+ {
+ get => BitConverter.ToUInt16(Data, Offset + 0);
+ set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Offset + 0);
+ }
+
+ public int SID
+ {
+ get => BitConverter.ToUInt16(Data, Offset + 2);
+ set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Offset + 2);
+ }
+
+ public int Game
+ {
+ get => Data[Offset + 4];
+ set => Data[Offset + 4] = (byte)value;
+ }
+
+ public int Gender
+ {
+ get => Data[Offset + 5];
+ set => Data[Offset + 5] = (byte)value;
+ }
+
+ public int Language
+ {
+ get => Data[Offset + 0x35];
+ set => Data[Offset + 0x35] = (byte)value;
+ }
+
+ public string OT
+ {
+ get => SAV.GetString(Offset + 0x38, 0x1A);
+ set => SAV.SetString(value, SAV.OTLength).CopyTo(Data, Offset + 0x38);
+ }
+
+ public bool MegaUnlocked
+ {
+ get => (Data[Offset + 0x6D] & 0x01) != 0;
+ set => Data[Offset + 0x6D] = (byte)((Data[Offset + 0x6D] & 0xFE) | (value ? 1 : 0)); // in battle
+ }
+
+ public bool ZMoveUnlocked
+ {
+ get => (Data[Offset + 0x6D] & 2) != 0;
+ set => Data[Offset + 0x6D] = (byte)((Data[Offset + 0x6D] & ~2) | (value ? 2 : 0)); // in battle
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/PlayTime7b.cs b/PKHeX.Core/Saves/Substructures/Gen7/PlayTime7b.cs
new file mode 100644
index 000000000..fe26e7764
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/PlayTime7b.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace PKHeX.Core
+{
+ public sealed class PlayTime7b : SaveBlock
+ {
+ public PlayTime7b(SaveFile sav) : base(sav)
+ {
+ Offset = ((SAV7b)sav).GetBlockOffset(BelugaBlockIndex.PlayTime);
+ }
+
+ public int PlayedHours
+ {
+ get => BitConverter.ToUInt16(Data, Offset);
+ set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Offset);
+ }
+
+ public int PlayedMinutes
+ {
+ get => Data[Offset + 2];
+ set => Data[Offset + 2] = (byte)value;
+ }
+
+ public int PlayedSeconds
+ {
+ get => Data[Offset + 3];
+ set => Data[Offset + 3] = (byte)value;
+ }
+
+ private uint LastSaved { get => BitConverter.ToUInt32(Data, Offset + 0x4); set => BitConverter.GetBytes(value).CopyTo(Data, Offset + 0x4); }
+ private int LastSavedYear { get => (int)(LastSaved & 0xFFF); set => LastSaved = (LastSaved & 0xFFFFF000) | (uint)value; }
+ private int LastSavedMonth { get => (int)(LastSaved >> 12 & 0xF); set => LastSaved = (LastSaved & 0xFFFF0FFF) | ((uint)value & 0xF) << 12; }
+ private int LastSavedDay { get => (int)(LastSaved >> 16 & 0x1F); set => LastSaved = (LastSaved & 0xFFE0FFFF) | ((uint)value & 0x1F) << 16; }
+ private int LastSavedHour { get => (int)(LastSaved >> 21 & 0x1F); set => LastSaved = (LastSaved & 0xFC1FFFFF) | ((uint)value & 0x1F) << 21; }
+ private int LastSavedMinute { get => (int)(LastSaved >> 26 & 0x3F); set => LastSaved = (LastSaved & 0x03FFFFFF) | ((uint)value & 0x3F) << 26; }
+ public string LastSavedTime => $"{LastSavedYear:0000}{LastSavedMonth:00}{LastSavedDay:00}{LastSavedHour:00}{LastSavedMinute:00}";
+
+ public DateTime? LastSavedDate
+ {
+ get => !Util.IsDateValid(LastSavedYear, LastSavedMonth, LastSavedDay)
+ ? (DateTime?)null
+ : new DateTime(LastSavedYear, LastSavedMonth, LastSavedDay, LastSavedHour, LastSavedMinute, 0);
+ set
+ {
+ // Only update the properties if a value is provided.
+ if (value.HasValue)
+ {
+ var dt = value.Value;
+ LastSavedYear = dt.Year;
+ LastSavedMonth = dt.Month;
+ LastSavedDay = dt.Day;
+ LastSavedHour = dt.Hour;
+ LastSavedMinute = dt.Minute;
+ }
+ else // Clear the date.
+ {
+ // If code tries to access MetDate again, null will be returned.
+ LastSavedYear = 0;
+ LastSavedMonth = 0;
+ LastSavedDay = 0;
+ LastSavedHour = 0;
+ LastSavedMinute = 0;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Substructures/Gen7/PokeListHeader.cs b/PKHeX.Core/Saves/Substructures/Gen7/PokeListHeader.cs
new file mode 100644
index 000000000..f6580881f
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen7/PokeListHeader.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace PKHeX.Core
+{
+ ///
+ /// Header information for the stored data.
+ ///
+ ///
+ /// This block simply contains the following:
+ /// u16 Party Pointers * 6: Indicates which index occupies this slot. if nothing in slot.
+ /// u16 Follower Pointer: Indicates which index is following the player. if nothing following.
+ /// u16 List Count: Points to the next empty slot, and indicates how many slots are stored in the list.
+ ///
+ public sealed class PokeListHeader : SaveBlock
+ {
+ private readonly SaveFile SAV;
+
+ ///
+ /// Raw representation of data, casted to ushort[].
+ ///
+ internal readonly int[] PokeListInfo;
+
+ private const int FOLLOW = 6;
+ private const int COUNT = 7;
+ private const int MAX_SLOTS = 1000;
+ private const int SLOT_EMPTY = 1001;
+
+ public PokeListHeader(SaveFile sav) : base(sav)
+ {
+ SAV = sav;
+ Offset = ((SAV7b)sav).GetBlockOffset(BelugaBlockIndex.PokeListHeader);
+ PokeListInfo = LoadPointerData();
+ PartyCount = PokeListInfo.Take(6).Count(z => z < MAX_SLOTS);
+ }
+
+ private int _partyCount;
+
+ public int PartyCount
+ {
+ get => _partyCount;
+ set
+ {
+ if (_partyCount > value)
+ {
+ for (int i = _partyCount; i < value; i++)
+ ClearPartySlot(i);
+ }
+ _partyCount = value;
+ }
+ }
+
+ public bool ClearPartySlot(int slot)
+ {
+ if (slot >= 6 || PartyCount <= 1)
+ return false;
+
+ if (slot > PartyCount)
+ {
+ slot = PartyCount;
+ }
+ else if (slot != PartyCount - 1)
+ {
+ int countShiftDown = PartyCount - 1 - slot;
+ Array.Copy(PokeListInfo, slot + 1, PokeListInfo, slot, countShiftDown);
+ slot = PartyCount - 1;
+ }
+ PokeListInfo[slot] = SLOT_EMPTY;
+ PartyCount--;
+ return true;
+ }
+
+ public void RemoveFollower() => FollowerIndex = SLOT_EMPTY;
+
+ public int FollowerIndex
+ {
+ get => PokeListInfo[FOLLOW];
+ set
+ {
+ if ((ushort)value > 1000)
+ throw new ArgumentException(nameof(value));
+ PokeListInfo[FOLLOW] = (ushort)value;
+ }
+ }
+
+ public int Count
+ {
+ get => BitConverter.ToUInt16(Data, Offset + (COUNT * 2));
+ set => BitConverter.GetBytes((ushort) value).CopyTo(Data, Offset + (COUNT * 2));
+ }
+
+ private int[] LoadPointerData()
+ {
+ var list = new int[7];
+ for (int i = 0; i < list.Length; i++)
+ list[i] = BitConverter.ToUInt16(Data, Offset + (i * 2));
+ return list;
+ }
+
+ private void SetPointerData(IList vals)
+ {
+ for (int i = 0; i < vals.Count; i++)
+ BitConverter.GetBytes((ushort)vals[i]).CopyTo(Data, Offset + (i * 2));
+ vals.CopyTo(PokeListInfo);
+ }
+
+ public int GetPartyOffset(int slot)
+ {
+ if ((uint)slot >= 6)
+ throw new ArgumentException(nameof(slot) + " expected to be < 6.");
+ int position = PokeListInfo[slot];
+ return SAV.GetBoxSlotOffset(position);
+ }
+
+ public bool IsSlotInBattleTeam(int box, int slot)
+ {
+ if ((uint)slot >= SAV.SlotCount || (uint)box >= SAV.BoxCount)
+ return false;
+
+ int slotIndex = slot + (SAV.BoxSlotCount * box);
+ return PokeListInfo.Take(6).Any(s => s == slotIndex) || FollowerIndex == slotIndex;
+ }
+
+ public bool IsSlotLocked(int box, int slot)
+ {
+ if ((uint)slot >= SAV.SlotCount || (uint)box >= SAV.BoxCount)
+ return false;
+ return false;
+ }
+
+ public bool CompressStorage()
+ {
+ // Box Data is stored as a list, instead of an array. Empty interstitials are not legal.
+ // Fix stored slots!
+ var arr = PokeListInfo.Take(7).ToArray();
+ var result = SAV.CompressStorage(out int count, arr);
+ Debug.Assert(count <= MAX_SLOTS);
+ arr.CopyTo(PokeListInfo);
+ Count = count;
+ if (FollowerIndex > count && FollowerIndex != SLOT_EMPTY)
+ RemoveFollower();
+
+ if (result)
+ SetPointerData(PokeListInfo);
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/PKHeX.Core/Saves/Util/SaveUtil.cs b/PKHeX.Core/Saves/Util/SaveUtil.cs
index 4c6ccc7a1..134183c27 100644
--- a/PKHeX.Core/Saves/Util/SaveUtil.cs
+++ b/PKHeX.Core/Saves/Util/SaveUtil.cs
@@ -15,6 +15,7 @@ public static class SaveUtil
{
public const int BEEF = 0x42454546;
+ public const int SIZE_G7GG = 0x100000;
public const int SIZE_G7USUM = 0x6CC00;
public const int SIZE_G7SM = 0x6BE00;
public const int SIZE_G6XY = 0x65600;
@@ -54,7 +55,7 @@ public static class SaveUtil
private static readonly HashSet SIZES = new HashSet(SIZES_2)
{
- SIZE_G7SM, SIZE_G7USUM,
+ SIZE_G7SM, SIZE_G7USUM, SIZE_G7GG,
SIZE_G6XY, SIZE_G6ORAS, SIZE_G6ORASDEMO,
SIZE_G5RAW, SIZE_G5BW, SIZE_G5B2W2,
SIZE_G4BR, SIZE_G4RAW,
@@ -92,6 +93,8 @@ private static GameVersion GetSAVGeneration(byte[] data)
if (GetIsG7SAV(data) != GameVersion.Invalid)
return GameVersion.Gen7;
+ if (GetIsBelugaSAV(data) != GameVersion.Invalid)
+ return GameVersion.GG;
if (GetIsG3COLOSAV(data) != GameVersion.Invalid)
return GameVersion.COLO;
if (GetIsG3XDSAV(data) != GameVersion.Invalid)
@@ -430,6 +433,23 @@ private static GameVersion GetIsG7SAV(byte[] data)
return GameVersion.Invalid;
}
+ /// Determines if the input data belongs to a save
+ /// Save data of which to determine the type
+ /// Version Identifier or Invalid if type cannot be determined.
+ private static GameVersion GetIsBelugaSAV(byte[] data)
+ {
+ if (data.Length != SIZE_G7GG)
+ return GameVersion.Invalid;
+
+ const int actualLength = 0xB8800;
+ if (BitConverter.ToUInt32(data, actualLength - 0x1F0) != BEEF) // beef table start
+ return GameVersion.Invalid;
+ if (BitConverter.ToUInt16(data, actualLength - 0x200 + 0xB0) != 0x13) // check a block number to double check
+ return GameVersion.Invalid;
+
+ return GameVersion.GG;
+ }
+
private static bool GetIsBank7(byte[] data) => data.Length == SIZE_G7BANK && data[0] != 0;
/// Creates an instance of a SaveFile using the given save data.
@@ -477,6 +497,7 @@ private static SaveFile GetVariantSAVInternal(byte[] data)
case GameVersion.XD: return new SAV3XD(data);
case GameVersion.RSBOX: return new SAV3RSBox(data);
case GameVersion.BATREV: return new SAV4BR(data);
+ case GameVersion.GG: return new SAV7b(data);
// Bulk Storage
case GameVersion.USUM: return Bank7.GetBank7(data);
@@ -583,6 +604,8 @@ private static SaveFile GetBlankSAV(GameVersion Game)
return new SAV7(new byte[SIZE_G7SM]);
case GameVersion.US: case GameVersion.UM: case GameVersion.USUM:
return new SAV7(new byte[SIZE_G7USUM]);
+ case GameVersion.GP: case GameVersion.GE: case GameVersion.GG:
+ return new SAV7b(new byte[SIZE_G7GG]);
default:
return null;
@@ -711,10 +734,10 @@ public static ushort CRC16_CCITT(byte[] data, int start, int length)
/// Checksum
public static ushort CRC16(byte[] data, int start, int length, ushort initial)
{
- ushort chk = (ushort)~initial;
+ ushort chk = initial;
for (var i = start; i < start + length; i++)
chk = (ushort) (crc16[(data[i] ^ chk) & 0xFF] ^ chk >> 8);
- return (ushort)~chk;
+ return chk;
}
/// Calculates the 16bit checksum over an input byte array.
@@ -722,7 +745,14 @@ public static ushort CRC16(byte[] data, int start, int length, ushort initial)
/// Offset to start checksum at
/// Length of array to checksum
/// Checksum
- public static ushort CRC16(byte[] data, int start, int length) => CRC16(data, start, length, 0);
+ public static ushort CRC16(byte[] data, int start, int length) => (ushort)~CRC16(data, start, length, unchecked((ushort)~0));
+
+ /// Calculates the 16bit checksum over an input byte array.
+ /// Input byte array
+ /// Offset to start checksum at
+ /// Length of array to checksum
+ /// Checksum
+ public static ushort CRC16NoInvert(byte[] data, int start, int length) => CRC16(data, start, length, 0);
public static byte[] Resign7(byte[] sav7)
{