From 13e1debe3e36acbe619782fb30bfbc6f54e43758 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 13 Nov 2018 19:18:29 -0800 Subject: [PATCH] Add sav7b object & detection util --- PKHeX.Core/Saves/Blocks/BlockInfo3DS.cs | 22 +- PKHeX.Core/Saves/SAV7b.cs | 217 ++++++++++++++++++ .../Saves/Substructures/Gen7/ConfigSave7b.cs | 50 ++++ .../Saves/Substructures/Gen7/EventWork7b.cs | 183 +++++++++++++++ PKHeX.Core/Saves/Substructures/Gen7/Misc7b.cs | 27 +++ .../Saves/Substructures/Gen7/MyItem7b.cs | 43 ++++ .../Saves/Substructures/Gen7/MyStatus7b.cs | 67 ++++++ .../Saves/Substructures/Gen7/PlayTime7b.cs | 67 ++++++ .../Substructures/Gen7/PokeListHeader.cs | 150 ++++++++++++ PKHeX.Core/Saves/Util/SaveUtil.cs | 38 ++- 10 files changed, 859 insertions(+), 5 deletions(-) create mode 100644 PKHeX.Core/Saves/SAV7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/ConfigSave7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/EventWork7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/Misc7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/MyItem7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/MyStatus7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/PlayTime7b.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen7/PokeListHeader.cs 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) {