diff --git a/PKHeX.Core/Editing/Saves/Slots/Extensions.cs b/PKHeX.Core/Editing/Saves/Slots/Extensions.cs index 8ce778e52..cf8ee20fd 100644 --- a/PKHeX.Core/Editing/Saves/Slots/Extensions.cs +++ b/PKHeX.Core/Editing/Saves/Slots/Extensions.cs @@ -61,11 +61,11 @@ private static List GetExtraSlots2(SAV2 sav) private static List GetExtraSlots3(SAV3 sav) { - if (sav is not SAV3FRLG) + if (sav is not SAV3FRLG frlg) return None; return [ - new(sav.LargeBuffer[0x3C98..], 0) {Type = StorageSlotType.Daycare}, + new(frlg.LargeBlock.SingleDaycareRoute5, 0) {Type = StorageSlotType.Daycare}, ]; } diff --git a/PKHeX.Core/Editing/Saves/Slots/StorageSlotType.cs b/PKHeX.Core/Editing/Saves/Slots/StorageSlotType.cs index d4ce50293..346ee2b39 100644 --- a/PKHeX.Core/Editing/Saves/Slots/StorageSlotType.cs +++ b/PKHeX.Core/Editing/Saves/Slots/StorageSlotType.cs @@ -51,7 +51,7 @@ public enum StorageSlotType : byte /// Shiny Overworld Cache /// /// - /// + /// /// Shiny, diff --git a/PKHeX.Core/Items/Bags/PlayerBag3E.cs b/PKHeX.Core/Items/Bags/PlayerBag3E.cs index d96e156af..33d014268 100644 --- a/PKHeX.Core/Items/Bags/PlayerBag3E.cs +++ b/PKHeX.Core/Items/Bags/PlayerBag3E.cs @@ -6,8 +6,6 @@ namespace PKHeX.Core; public sealed class PlayerBag3E : PlayerBag, IPlayerBag3 { - private const int BaseOffset = 0x0498; - public override IReadOnlyList Pouches { get; } = GetPouches(ItemStorage3E.Instance); public override ItemStorage3E Info => ItemStorage3E.Instance; @@ -21,7 +19,7 @@ public sealed class PlayerBag3E : PlayerBag, IPlayerBag3 new(0x000, 50, 999, info, PCItems), ]; - public PlayerBag3E(SAV3E sav) : this(sav.Large[BaseOffset..], sav.SecurityKey) { } + public PlayerBag3E(SAV3E sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey) { } public PlayerBag3E(ReadOnlySpan data, uint security) { UpdateSecurityKey(security); @@ -29,7 +27,7 @@ public PlayerBag3E(ReadOnlySpan data, uint security) } public override void CopyTo(SaveFile sav) => CopyTo((SAV3E)sav); - public void CopyTo(SAV3E sav) => CopyTo(sav.Large[BaseOffset..]); + public void CopyTo(SAV3E sav) => CopyTo(sav.LargeBlock.Inventory); public void CopyTo(Span data) => Pouches.SaveAll(data); public override int GetMaxCount(InventoryType type, int itemIndex) diff --git a/PKHeX.Core/Items/Bags/PlayerBag3FRLG.cs b/PKHeX.Core/Items/Bags/PlayerBag3FRLG.cs index 2f9d94289..9e95bdb01 100644 --- a/PKHeX.Core/Items/Bags/PlayerBag3FRLG.cs +++ b/PKHeX.Core/Items/Bags/PlayerBag3FRLG.cs @@ -6,7 +6,6 @@ namespace PKHeX.Core; public sealed class PlayerBag3FRLG(bool VC) : PlayerBag, IPlayerBag3 { - private const int BaseOffset = 0x0298; public override IItemStorage Info => GetInfo(VC); private static IItemStorage GetInfo(bool vc) => vc ? ItemStorage3FRLG_VC.Instance : ItemStorage3FRLG.Instance; public override IReadOnlyList Pouches { get; } = GetPouches(GetInfo(VC)); @@ -21,7 +20,7 @@ public sealed class PlayerBag3FRLG(bool VC) : PlayerBag, IPlayerBag3 new(0x000, 30, 999, info, PCItems), ]; - public PlayerBag3FRLG(SAV3FRLG sav) : this(sav.Large[BaseOffset..], sav.SecurityKey, sav.IsVirtualConsole) { } + public PlayerBag3FRLG(SAV3FRLG sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey, sav.IsVirtualConsole) { } public PlayerBag3FRLG(ReadOnlySpan data, uint security, bool vc) : this(vc) { UpdateSecurityKey(security); @@ -29,7 +28,7 @@ public PlayerBag3FRLG(ReadOnlySpan data, uint security, bool vc) : this(vc } public override void CopyTo(SaveFile sav) => CopyTo((SAV3FRLG)sav); - public void CopyTo(SAV3FRLG sav) => CopyTo(sav.Large[BaseOffset..]); + public void CopyTo(SAV3FRLG sav) => CopyTo(sav.LargeBlock.Inventory); public void CopyTo(Span data) => Pouches.SaveAll(data); public override int GetMaxCount(InventoryType type, int itemIndex) diff --git a/PKHeX.Core/Items/Bags/PlayerBag3RS.cs b/PKHeX.Core/Items/Bags/PlayerBag3RS.cs index 4f5fe616a..caea7c81f 100644 --- a/PKHeX.Core/Items/Bags/PlayerBag3RS.cs +++ b/PKHeX.Core/Items/Bags/PlayerBag3RS.cs @@ -6,8 +6,6 @@ namespace PKHeX.Core; public sealed class PlayerBag3RS : PlayerBag { - private const int BaseOffset = 0x0498; - public override IReadOnlyList Pouches { get; } = GetPouches(ItemStorage3RS.Instance); public override ItemStorage3RS Info => ItemStorage3RS.Instance; @@ -21,11 +19,11 @@ public sealed class PlayerBag3RS : PlayerBag new(0x000, 50, 999, info, PCItems), ]; - public PlayerBag3RS(SAV3RS sav) : this(sav.Large[BaseOffset..]) { } + public PlayerBag3RS(SAV3RS sav) : this(sav.LargeBlock.Inventory) { } public PlayerBag3RS(ReadOnlySpan data) => Pouches.LoadAll(data); public override void CopyTo(SaveFile sav) => CopyTo((SAV3RS)sav); - public void CopyTo(SAV3RS sav) => CopyTo(sav.Large[BaseOffset..]); + public void CopyTo(SAV3RS sav) => CopyTo(sav.LargeBlock.Inventory); public void CopyTo(Span data) => Pouches.SaveAll(data); public override int GetMaxCount(InventoryType type, int itemIndex) diff --git a/PKHeX.Core/Legality/Legal.cs b/PKHeX.Core/Legality/Legal.cs index e8b8ab5d5..fe88d0429 100644 --- a/PKHeX.Core/Legality/Legal.cs +++ b/PKHeX.Core/Legality/Legal.cs @@ -20,7 +20,8 @@ public static class Legal internal const int MaxSpeciesID_3 = 386; internal const int MaxMoveID_3 = 354; - internal const int MaxItemID_3 = 374; + internal const int MaxItemID_3_RS = 346; + internal const int MaxItemID_3_FRLG = 374; internal const int MaxItemID_3_E = 376; internal const int MaxItemID_3_COLO = 547; internal const int MaxItemID_3_XD = 593; diff --git a/PKHeX.Core/Legality/Restrictions/ItemRestrictions.cs b/PKHeX.Core/Legality/Restrictions/ItemRestrictions.cs index 8d3984dde..dbb7cb499 100644 --- a/PKHeX.Core/Legality/Restrictions/ItemRestrictions.cs +++ b/PKHeX.Core/Legality/Restrictions/ItemRestrictions.cs @@ -51,7 +51,7 @@ public static bool IsHeldItemAllowed(int item, EntityContext context) // Combined bitflags for released held items across generations. private static readonly bool[] ReleasedHeldItems_2 = GetPermitList(MaxItemID_2, HeldItems_GSC); - private static readonly bool[] ReleasedHeldItems_3 = GetPermitList(MaxItemID_3, HeldItems_RS, ItemStorage3RS.Unreleased); // Safari Ball + private static readonly bool[] ReleasedHeldItems_3 = GetPermitList(MaxItemID_3_RS, HeldItems_RS, ItemStorage3RS.Unreleased); // Safari Ball private static readonly bool[] ReleasedHeldItems_4 = GetPermitList(MaxItemID_4_HGSS, HeldItems_HGSS, ItemStorage4.Unreleased); private static readonly bool[] ReleasedHeldItems_5 = GetPermitList(MaxItemID_5_B2W2, HeldItems_BW, ItemStorage5.Unreleased); private static readonly bool[] ReleasedHeldItems_6 = GetPermitList(MaxItemID_6_AO, HeldItems_AO, ItemStorage6XY.Unreleased); diff --git a/PKHeX.Core/PKM/Shared/G3PKM.cs b/PKHeX.Core/PKM/Shared/G3PKM.cs index 345523016..27922cae3 100644 --- a/PKHeX.Core/PKM/Shared/G3PKM.cs +++ b/PKHeX.Core/PKM/Shared/G3PKM.cs @@ -17,7 +17,7 @@ public abstract class G3PKM : PKM, IRibbonSetEvent3, IRibbonSetCommon3, IRibbonS public sealed override ushort MaxMoveID => Legal.MaxMoveID_3; public sealed override ushort MaxSpeciesID => Legal.MaxSpeciesID_3; public sealed override int MaxAbilityID => Legal.MaxAbilityID_3; - public sealed override int MaxItemID => Legal.MaxItemID_3; + public sealed override int MaxItemID => Legal.MaxItemID_3_E; public sealed override int MaxBallID => Legal.MaxBallID_3; public sealed override GameVersion MaxGameID => Legal.MaxGameID_3; public sealed override int MaxIV => 31; diff --git a/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3Large.cs b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3Large.cs new file mode 100644 index 000000000..0e9ca51b3 --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3Large.cs @@ -0,0 +1,43 @@ +using System; +using System.ComponentModel; + +namespace PKHeX.Core; + +[TypeConverter(typeof(ExpandableObjectConverter))] +public interface ISaveBlock3Large +{ + Memory Raw { get; } + Span Data { get; } + + ushort X { get; set; } + ushort Y { get; set; } + byte PartyCount { get; set; } + Span PartyBuffer { get; } + uint Money { get; set; } + ushort Coin { get; set; } + ushort RegisteredItem { get; set; } + Span EReaderBerry { get; } + Gen3MysteryData MysteryData { get; set; } + int DaycareOffset { get; } + int DaycareSlotSize { get; } + int BadgeFlagStart { get; } + int EventFlagCount { get; } + int EventWorkCount { get; } + int EggEventFlag { get; } + Memory RoamerData { get; } + uint GetRecord(int record); + void SetRecord(int record, uint value); + + Mail3 GetMail(int mailIndex); + void SetMail(int mailIndex, Mail3 value); + + bool GetEventFlag(int flagNumber); + void SetEventFlag(int flagNumber, bool value); + ushort GetWork(int index); + void SetWork(int index, ushort value); + + int SeenOffset2 { get; } + int ExternalEventData { get; } + int SeenOffset3 { get; } + Span GiftRibbons { get; } +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3LargeExpansion.cs b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3LargeExpansion.cs new file mode 100644 index 000000000..8b851b21e --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3LargeExpansion.cs @@ -0,0 +1,13 @@ +using System; + +namespace PKHeX.Core; + +public interface ISaveBlock3LargeExpansion : ISaveBlock3Large +{ + WonderNews3 GetWonderNews(bool isJapanese); + void SetWonderNews(bool isJapanese, ReadOnlySpan data); + WonderCard3 GetWonderCard(bool isJapanese); + void SetWonderCard(bool isJapanese, ReadOnlySpan data); + WonderCard3Extra GetWonderCardExtra(bool isJapanese); + void SetWonderCardExtra(bool isJapanese, ReadOnlySpan data); +} diff --git a/PKHeX.Core/Saves/Substructures/Gen3/IGen3Hoenn.cs b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3LargeHoenn.cs similarity index 61% rename from PKHeX.Core/Saves/Substructures/Gen3/IGen3Hoenn.cs rename to PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3LargeHoenn.cs index 09666240b..490a52329 100644 --- a/PKHeX.Core/Saves/Substructures/Gen3/IGen3Hoenn.cs +++ b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3LargeHoenn.cs @@ -3,13 +3,13 @@ namespace PKHeX.Core; /// -/// Properties common to RS & Emerald save files. +/// Large-block properties common to RS & Emerald save files. /// -public interface IGen3Hoenn +public interface ISaveBlock3LargeHoenn : ISaveBlock3Large { - RTC3 ClockInitial { get; set; } - RTC3 ClockElapsed { get; set; } PokeBlock3Case PokeBlocks { get; set; } + ushort GetBerryBlenderRPMRecord(int index); + void SetBerryBlenderRPMRecord(int index, ushort value); DecorationInventory3 Decorations { get; } Swarm3 Swarm { get; set; } @@ -19,6 +19,6 @@ public interface IGen3Hoenn RecordMixing3Gift RecordMixingGift { get; set; } SecretBaseManager3 SecretBases { get; } - Paintings3 GetPainting(int index); + Paintings3 GetPainting(int index, bool japanese); void SetPainting(int index, Paintings3 value); } diff --git a/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3Small.cs b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3Small.cs new file mode 100644 index 000000000..120b554fb --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3Small.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; + +namespace PKHeX.Core; + +[TypeConverter(typeof(ExpandableObjectConverter))] +public interface ISaveBlock3Small +{ + Memory Raw { get; } + Span Data { get; } + + Span OriginalTrainerTrash { get; } + byte Gender { get; set; } + uint ID32 { get; set; } + ushort TID16 { get; set; } + ushort SID16 { get; set; } + int PlayedHours { get; set; } + int PlayedMinutes { get; set; } + int PlayedSeconds { get; set; } + byte PlayedFrames { get; set; } + byte OptionsButtonMode { get; set; } + int TextSpeed { get; set; } + byte OptionWindowFrame { get; set; } + bool OptionSoundStereo { get; set; } + bool OptionBattleStyle { get; set; } + bool OptionBattleScene { get; set; } + bool OptionIsRegionMapZoom { get; set; } + byte PokedexSort { get; set; } + byte PokedexMode { get; set; } + byte PokedexNationalMagicRSE { get; set; } + byte PokedexNationalMagicFRLG { get; set; } + uint DexPIDUnown { get; set; } + uint DexPIDSpinda { get; set; } + Span EReaderTrainer { get; } + uint SecurityKey { get; } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen3/IGen3Joyful.cs b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3SmallExpansion.cs similarity index 54% rename from PKHeX.Core/Saves/Substructures/Gen3/IGen3Joyful.cs rename to PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3SmallExpansion.cs index 0f2f7a980..718787d2c 100644 --- a/PKHeX.Core/Saves/Substructures/Gen3/IGen3Joyful.cs +++ b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3SmallExpansion.cs @@ -1,6 +1,8 @@ +using System.ComponentModel.DataAnnotations; + namespace PKHeX.Core; -public interface IGen3Joyful +public interface ISaveBlock3SmallExpansion : ISaveBlock3Small { ushort JoyfulJumpInRow { get; set; } ushort JoyfulJump5InRow { get; set; } @@ -11,6 +13,9 @@ public interface IGen3Joyful ushort JoyfulBerriesInRow { get; set; } ushort JoyfulBerries5InRow { get; set; } + ushort GetBerryPressSpeed([Range(0, 3)] int index); + void SetBerryPressSpeed([Range(0, 3)] int index, ushort value); uint BerryPowder { get; set; } - uint SecurityKey { get; set; } + new uint SecurityKey { get; set; } + uint LinkFlags { get; set; } } diff --git a/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3SmallHoenn.cs b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3SmallHoenn.cs new file mode 100644 index 000000000..929c0d1f9 --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/ISaveBlock3SmallHoenn.cs @@ -0,0 +1,17 @@ +namespace PKHeX.Core; + +/// +/// Small-block properties common to RS & Emerald save files. +/// +public interface ISaveBlock3SmallHoenn : ISaveBlock3Small +{ + /// + /// localTimeOffset + /// + RTC3 ClockInitial { get; set; } + + /// + /// lastBerryTreeUpdate + /// + RTC3 ClockElapsed { get; set; } +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeE.cs b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeE.cs new file mode 100644 index 000000000..21b2da387 --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeE.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed record SaveBlock3LargeE(Memory Raw) : ISaveBlock3LargeExpansion, ISaveBlock3LargeHoenn, IRecordStatStorage +{ + public Span Data => Raw.Span; + public ushort X { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + public ushort Y { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } + + public byte PartyCount { get => Data[0x234]; set => Data[0x234] = value; } + public Span PartyBuffer => Data.Slice(0x238, 6 * PokeCrypto.SIZE_3PARTY); + + public uint Money { get => ReadUInt32LittleEndian(Data[0x0490..]); set => WriteUInt32LittleEndian(Data[0x0490..], value); } + public ushort Coin { get => ReadUInt16LittleEndian(Data[0x0494..]); set => WriteUInt16LittleEndian(Data[0x0494..], value); } + public ushort RegisteredItem { get => ReadUInt16LittleEndian(Data[0x0496..]); set => WriteUInt16LittleEndian(Data[0x0496..], value); } + public Span Inventory => Data.Slice(0x0498, 0x360); + private Span PokeBlockData => Data.Slice(0x848, PokeBlock3Case.SIZE); + public PokeBlock3Case PokeBlocks { get => new(PokeBlockData); set => value.Write(PokeBlockData); } + public int SeenOffset2 => 0x988; + + private const int OFS_BerryBlenderRecord = 0x9BC; + + /// + /// Max RPM for 2, 3 and 4 players. Each value unit represents 0.01 RPM. Value 0 if no record. + /// + /// 2 players: index 0, 3 players: index 1, 4 players: index 2 + public const int BerryBlenderRPMRecordCount = 3; + + private Span GetBlenderRPMSpan(int index) + { + if ((uint)index >= BerryBlenderRPMRecordCount) + throw new ArgumentOutOfRangeException(nameof(index)); + return Data[(OFS_BerryBlenderRecord + (index * 2))..]; + } + + public ushort GetBerryBlenderRPMRecord(int index) => ReadUInt16LittleEndian(GetBlenderRPMSpan(index)); + public void SetBerryBlenderRPMRecord(int index, ushort value) => WriteUInt16LittleEndian(GetBlenderRPMSpan(index), value); + + private const int EventFlag = 0x1270; + private const int EventWork = 0x139C; + public int EventFlagCount => 8 * 300; + public int EventWorkCount => 0x100; + public int EggEventFlag => 0x86; + public int BadgeFlagStart => 0x867; + + public bool GetEventFlag(int flagNumber) + { + if ((uint)flagNumber >= EventFlagCount) + throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount})."); + return FlagUtil.GetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7); + } + + public void SetEventFlag(int flagNumber, bool value) + { + if ((uint)flagNumber >= EventFlagCount) + throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount})."); + FlagUtil.SetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7, value); + } + + public ushort GetWork(int index) => ReadUInt16LittleEndian(Data[(EventWork + (index * 2))..]); + public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Data[EventWork..][(index * 2)..], value); + + private const int RecordOffset = 0x159C; + private static int GetRecordOffset(RecID3Emerald record) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)record, (uint)RecID3Emerald.NUM_GAME_STATS); + return RecordOffset + (sizeof(uint) * (int)record); + } + + public uint GetRecord(int record) => GetRecord((RecID3Emerald)record); + public void SetRecord(int record, uint value) => SetRecord((RecID3Emerald)record, value); + public uint GetRecord(RecID3Emerald record) => ReadUInt32LittleEndian(Data[GetRecordOffset(record)..]); + public void SetRecord(RecID3Emerald record, uint value) => WriteUInt32LittleEndian(Data[GetRecordOffset(record)..], value); + public void AddRecord(RecID3Emerald record, uint value) => SetRecord(record, GetRecord(record) + value); + + private Memory SecretBaseData => Raw.Slice(0x1A9C, SecretBaseManager3.BaseCount * SecretBase3.SIZE); + public SecretBaseManager3 SecretBases => new(SecretBaseData); + + public DecorationInventory3 Decorations => new(Data.Slice(0x2734, DecorationInventory3.SIZE)); + + private Span SwarmData => Data.Slice(0x2B90, Swarm3.SIZE); + public Swarm3 Swarm { get => new(SwarmData.ToArray()); set => value.Data.CopyTo(SwarmData); } + private void ClearSwarm() => SwarmData.Clear(); + public IReadOnlyList DefaultSwarms => Swarm3Details.Swarms_E; + + public int SwarmIndex + { + get + { + var map = Swarm.MapNum; + for (int i = 0; i < DefaultSwarms.Count; i++) + { + if (DefaultSwarms[i].MapNum == map) + return i; + } + return -1; + } + set + { + var arr = DefaultSwarms; + if ((uint)value >= arr.Count) + ClearSwarm(); + else + Swarm = arr[value]; + } + } + + private const int MailOffset = 0x2BE0; + private static int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset; + private Span GetMailSpan(int ofs) => Data.Slice(ofs, Mail3.SIZE); + + public Mail3 GetMail(int mailIndex) + { + var ofs = GetMailOffset(mailIndex); + var span = Data.Slice(ofs, Mail3.SIZE); + return new Mail3(span.ToArray(), ofs); + } + + public void SetMail(int mailIndex, Mail3 value) + { + var ofs = GetMailOffset(mailIndex); + value.CopyTo(GetMailSpan(ofs)); + } + + private const int OFS_TrendyWord = 0x2E20; + public bool GetTrendyWordUnlocked(TrendyWord3E word) => FlagUtil.GetFlag(Data, OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7); + public void SetTrendyWordUnlocked(TrendyWord3E word, bool value) => FlagUtil.SetFlag(Data, OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7, value); + + private const int Painting = 0x2F90; + private const int PaintingCount = 5; + private Span GetPaintingSpan(int index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, PaintingCount); + return Data.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * PaintingCount); + } + + public Paintings3 GetPainting(int index, bool japanese) => new(GetPaintingSpan(index).ToArray(), japanese); + public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index)); + + public int DaycareOffset => 0x3030; + public int DaycareSlotSize => PokeCrypto.SIZE_3STORED + 0x3C; // 0x38 mail + 4 exp + + public uint DaycareSeed + { + get => ReadUInt32LittleEndian(Data[0x3148..]); + set => WriteUInt32LittleEndian(Data[0x3148..], value); + } + + public Span GiftRibbons => Data.Slice(0x31B3, 11); + public int ExternalEventData => 0x31B3; + public Memory RoamerData => Raw.Slice(0x31DC, Roamer3.SIZE); + private const int OFFSET_EBERRY = 0x31F8; + private const int SIZE_EBERRY = 0x34; + public Span EReaderBerry => Data.Slice(OFFSET_EBERRY, SIZE_EBERRY); + + public const int WonderNewsOffset = 0x322C; + + // RAM Script + private Span MysterySpan => Data.Slice(0x3728, MysteryEvent3.SIZE); + public Gen3MysteryData MysteryData + { + get => new MysteryEvent3(MysterySpan.ToArray()); + set => value.Data.CopyTo(MysterySpan); + } + + private static int WonderCardOffset(bool isJapanese) => WonderNewsOffset + (isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); + private static int WonderCardExtraOffset(bool isJapanese) => WonderCardOffset(isJapanese) + (isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); + + private Span WonderNewsData(bool isJapanese) => Data.Slice(WonderNewsOffset, isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); + private Span WonderCardData(bool isJapanese) => Data.Slice(WonderCardOffset(isJapanese), isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); + private Span WonderCardExtraData(bool isJapanese) => Data.Slice(WonderCardExtraOffset(isJapanese), WonderCard3Extra.SIZE); + + public WonderNews3 GetWonderNews(bool isJapanese) => new(WonderNewsData(isJapanese).ToArray()); + public void SetWonderNews(bool isJapanese, ReadOnlySpan data) => data.CopyTo(WonderNewsData(isJapanese)); + public WonderCard3 GetWonderCard(bool isJapanese) => new(WonderCardData(isJapanese).ToArray()); + public void SetWonderCard(bool isJapanese, ReadOnlySpan data) => data.CopyTo(WonderCardData(isJapanese)); + public WonderCard3Extra GetWonderCardExtra(bool isJapanese) => new(WonderCardExtraData(isJapanese).ToArray()); + public void SetWonderCardExtra(bool isJapanese, ReadOnlySpan data) => data.CopyTo(WonderCardExtraData(isJapanese)); + + private const int OFS_TrainerHillRecord = 0x3718; + + /** Each value unit represents 1/60th of a second. Value 0 if no record. */ + public uint GetTrainerHillRecord(TrainerHillMode3E mode) => ReadUInt32LittleEndian(Data[(OFS_TrainerHillRecord + ((byte)mode * 4))..]); + public void SetTrainerHillRecord(TrainerHillMode3E mode, uint value) => WriteUInt32LittleEndian(Data[(OFS_TrainerHillRecord + ((byte)mode * 4))..], value); + + private Span RecordMixingData => Data.Slice(0x3B14, RecordMixing3Gift.SIZE); + public RecordMixing3Gift RecordMixingGift + { + get => new(RecordMixingData.ToArray()); + set => value.Data.CopyTo(RecordMixingData); + } + + public int SeenOffset3 => 0x3B24; + + private const int Walda = 0x3D70; + public ushort WaldaBackgroundColor { get => ReadUInt16LittleEndian(Data[(Walda + 0)..]); set => WriteUInt16LittleEndian(Data[(Walda + 0)..], value); } + public ushort WaldaForegroundColor { get => ReadUInt16LittleEndian(Data[(Walda + 2)..]); set => WriteUInt16LittleEndian(Data[(Walda + 2)..], value); } + public byte WaldaIconID { get => Data[Walda + 0x14]; set => Data[Walda + 0x14] = value; } + public byte WaldaPatternID { get => Data[Walda + 0x15]; set => Data[Walda + 0x15] = value; } + public bool WaldaUnlocked { get => Data[Walda + 0x16] != 0; set => Data[Walda + 0x16] = (byte)(value ? 1 : 0); } +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeFRLG.cs b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeFRLG.cs new file mode 100644 index 000000000..5d5bcec0a --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeFRLG.cs @@ -0,0 +1,121 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed record SaveBlock3LargeFRLG(Memory Raw) : ISaveBlock3LargeExpansion, IRecordStatStorage +{ + public Span Data => Raw.Span; + public ushort X { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + public ushort Y { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } + + public byte PartyCount { get => Data[0x034]; set => Data[0x034] = value; } + public Span PartyBuffer => Data.Slice(0x038, 6 * PokeCrypto.SIZE_3PARTY); + + public uint Money { get => ReadUInt32LittleEndian(Data[0x0290..]); set => WriteUInt32LittleEndian(Data[0x0290..], value); } + public ushort Coin { get => ReadUInt16LittleEndian(Data[0x0294..]); set => WriteUInt16LittleEndian(Data[0x0294..], value); } + public ushort RegisteredItem { get => ReadUInt16LittleEndian(Data[0x0296..]); set => WriteUInt16LittleEndian(Data[0x0296..], value); } + public Span Inventory => Data.Slice(0x0298, 0x360); + public int SeenOffset2 => 0x5F8; + + private const int EventFlag = 0xEE0; + private const int EventWork = 0x1000; + + public int EventFlagCount => 8 * 288; + public int EventWorkCount => 0x100; + public int EggEventFlag => 0x266; + public int BadgeFlagStart => 0x820; + + public bool GetEventFlag(int flagNumber) + { + if ((uint)flagNumber >= EventFlagCount) + throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount})."); + return FlagUtil.GetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7); + } + + public void SetEventFlag(int flagNumber, bool value) + { + if ((uint)flagNumber >= EventFlagCount) + throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount})."); + FlagUtil.SetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7, value); + } + + public ushort GetWork(int index) => ReadUInt16LittleEndian(Data[(EventWork + (index * 2))..]); + public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Data[EventWork..][(index * 2)..], value); + + private const int RecordOffset = 0x1200; + private static int GetRecordOffset(RecID3FRLG record) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)record, (uint)RecID3FRLG.NUM_GAME_STATS); + return RecordOffset + (sizeof(uint) * (int)record); + } + + public uint GetRecord(int record) => GetRecord((RecID3FRLG)record); + public void SetRecord(int record, uint value) => SetRecord((RecID3FRLG)record, value); + public uint GetRecord(RecID3FRLG record) => ReadUInt32LittleEndian(Data[GetRecordOffset(record)..]); + public void SetRecord(RecID3FRLG record, uint value) => WriteUInt32LittleEndian(Data[GetRecordOffset(record)..], value); + public void AddRecord(RecID3FRLG record, uint value) => SetRecord(record, GetRecord(record) + value); + + + private const int MailOffset = 0x2CD0; + private static int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset; + private Span GetMailSpan(int ofs) => Data.Slice(ofs, Mail3.SIZE); + + public Mail3 GetMail(int mailIndex) + { + var ofs = GetMailOffset(mailIndex); + var span = Data.Slice(ofs, Mail3.SIZE); + return new Mail3(span.ToArray(), ofs); + } + + public void SetMail(int mailIndex, Mail3 value) + { + var ofs = GetMailOffset(mailIndex); + value.CopyTo(GetMailSpan(ofs)); + } + + public int DaycareOffset => 0x2F80; + public int DaycareSlotSize => PokeCrypto.SIZE_3STORED + 0x3C; // 0x38 mail + 4 exp + + public ushort DaycareSeed + { + get => ReadUInt16LittleEndian(Data[0x3098..]); + set => WriteUInt16LittleEndian(Data[0x3098..], value); + } + + public Span GiftRibbons => Data.Slice(0x309C, 11); + public int ExternalEventData => 0x30A7; + public Memory RoamerData => Raw.Slice(0x30D0, Roamer3.SIZE); + + private const int OFFSET_EBERRY = 0x30EC; + private const int SIZE_EBERRY = 0x34; + public Span EReaderBerry => Data.Slice(OFFSET_EBERRY, SIZE_EBERRY); + + public const int WonderNewsOffset = 0x3120; + + private static int WonderCardOffset(bool isJapanese) => WonderNewsOffset + (isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); + private static int WonderCardExtraOffset(bool isJapanese) => WonderCardOffset(isJapanese) + (isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); + + private Span WonderNewsData(bool isJapanese) => Data.Slice(WonderNewsOffset, isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); + private Span WonderCardData(bool isJapanese) => Data.Slice(WonderCardOffset(isJapanese), isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); + private Span WonderCardExtraData(bool isJapanese) => Data.Slice(WonderCardExtraOffset(isJapanese), WonderCard3Extra.SIZE); + + public WonderNews3 GetWonderNews(bool isJapanese) => new(WonderNewsData(isJapanese).ToArray()); + public void SetWonderNews(bool isJapanese, ReadOnlySpan data) => data.CopyTo(WonderNewsData(isJapanese)); + public WonderCard3 GetWonderCard(bool isJapanese) => new(WonderCardData(isJapanese).ToArray()); + public void SetWonderCard(bool isJapanese, ReadOnlySpan data) => data.CopyTo(WonderCardData(isJapanese)); + public WonderCard3Extra GetWonderCardExtra(bool isJapanese) => new(WonderCardExtraData(isJapanese).ToArray()); + public void SetWonderCardExtra(bool isJapanese, ReadOnlySpan data) => data.CopyTo(WonderCardExtraData(isJapanese)); + + public Span RivalNameTrash => Data.Slice(0x3A4C, 8); + + private Span MysterySpan => Data.Slice(0x361C, MysteryEvent3.SIZE); + public Gen3MysteryData MysteryData + { + get => new MysteryEvent3(MysterySpan.ToArray()); + set => value.Data.CopyTo(MysterySpan); + } + + public int SeenOffset3 => 0x3A18; + public Memory SingleDaycareRoute5 => Raw.Slice(0x3C98, PokeCrypto.SIZE_3STORED); // 0x38 mail + 4 exp +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeRS.cs b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeRS.cs new file mode 100644 index 000000000..b1750ff2f --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3LargeRS.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed record SaveBlock3LargeRS(Memory Raw) : ISaveBlock3LargeHoenn, IRecordStatStorage +{ + public Span Data => Raw.Span; + public ushort X { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + public ushort Y { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } + + public byte PartyCount { get => Data[0x234]; set => Data[0x234] = value; } + public Span PartyBuffer => Data.Slice(0x238, 6 * PokeCrypto.SIZE_3PARTY); + + public uint Money { get => ReadUInt32LittleEndian(Data[0x0490..]); set => WriteUInt32LittleEndian(Data[0x0490..], value); } + public ushort Coin { get => ReadUInt16LittleEndian(Data[0x0494..]); set => WriteUInt16LittleEndian(Data[0x0494..], value); } + public ushort RegisteredItem { get => ReadUInt16LittleEndian(Data[0x0496..]); set => WriteUInt16LittleEndian(Data[0x0496..], value); } + public Span Inventory => Data.Slice(0x498, 0x3B0); + + private Span PokeBlockData => Data.Slice(0x7F8, PokeBlock3Case.SIZE); + public PokeBlock3Case PokeBlocks { get => new(PokeBlockData); set => value.Write(PokeBlockData); } + public int SeenOffset2 => 0x938; + + private const int OFS_BerryBlenderRecord = 0x96C; + + /// + /// Max RPM for 2, 3 and 4 players. Each value unit represents 0.01 RPM. Value 0 if no record. + /// + /// 2 players: index 0, 3 players: index 1, 4 players: index 2 + public const int BerryBlenderRPMRecordCount = 3; + + private Span GetBlenderRPMSpan(int index) + { + if ((uint)index >= BerryBlenderRPMRecordCount) + throw new ArgumentOutOfRangeException(nameof(index)); + return Data[(OFS_BerryBlenderRecord + (index * 2))..]; + } + + public ushort GetBerryBlenderRPMRecord(int index) => ReadUInt16LittleEndian(GetBlenderRPMSpan(index)); + public void SetBerryBlenderRPMRecord(int index, ushort value) => WriteUInt16LittleEndian(GetBlenderRPMSpan(index), value); + + private const int EventFlag = 0x1220; + private const int EventWork = 0x1340; + public int EventFlagCount => 8 * 288; + public int EventWorkCount => 0x100; + public int EggEventFlag => 0x86; + public int BadgeFlagStart => 0x807; + + public bool GetEventFlag(int flagNumber) + { + if ((uint)flagNumber >= EventFlagCount) + throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount})."); + return FlagUtil.GetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7); + } + + public void SetEventFlag(int flagNumber, bool value) + { + if ((uint)flagNumber >= EventFlagCount) + throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount})."); + FlagUtil.SetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7, value); + } + + public ushort GetWork(int index) => ReadUInt16LittleEndian(Data[(EventWork + (index * 2))..]); + public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Data[EventWork..][(index * 2)..], value); + + private const int RecordOffset = 0x1540; + private static int GetRecordOffset(RecID3RuSa record) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)record, (uint)RecID3RuSa.NUM_GAME_STATS); + return RecordOffset + (sizeof(uint) * (int)record); + } + + public uint GetRecord(int record) => GetRecord((RecID3RuSa)record); + public void SetRecord(int record, uint value) => SetRecord((RecID3RuSa)record, value); + public uint GetRecord(RecID3RuSa record) => ReadUInt32LittleEndian(Data[GetRecordOffset(record)..]); + public void SetRecord(RecID3RuSa record, uint value) => WriteUInt32LittleEndian(Data[GetRecordOffset(record)..], value); + public void AddRecord(RecID3RuSa record, uint value) => SetRecord(record, GetRecord(record) + value); + + private Memory SecretBaseData => Raw.Slice(0x1A08, SecretBaseManager3.BaseCount * SecretBase3.SIZE); + public SecretBaseManager3 SecretBases => new(SecretBaseData); + + public DecorationInventory3 Decorations => new(Data.Slice(0x26A0, DecorationInventory3.SIZE)); + + private Span SwarmData => Data.Slice(0x2AFC, Swarm3.SIZE); + public Swarm3 Swarm { get => new(SwarmData.ToArray()); set => value.Data.CopyTo(SwarmData); } + private void ClearSwarm() => SwarmData.Clear(); + public IReadOnlyList DefaultSwarms => Swarm3Details.Swarms_RS; + + public int SwarmIndex + { + get + { + var map = Swarm.MapNum; + for (int i = 0; i < DefaultSwarms.Count; i++) + { + if (DefaultSwarms[i].MapNum == map) + return i; + } + return -1; + } + set + { + var arr = DefaultSwarms; + if ((uint)value >= arr.Count) + ClearSwarm(); + else + Swarm = arr[value]; + } + } + + private const int MailOffset = 0x2B4C; + private static int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset; + private Span GetMailSpan(int ofs) => Data.Slice(ofs, Mail3.SIZE); + + public Mail3 GetMail(int mailIndex) + { + var ofs = GetMailOffset(mailIndex); + var span = Data.Slice(ofs, Mail3.SIZE); + return new Mail3(span.ToArray(), ofs); + } + + public void SetMail(int mailIndex, Mail3 value) + { + var ofs = GetMailOffset(mailIndex); + value.CopyTo(GetMailSpan(ofs)); + } + + + private const int Painting = 0x2EFC; + private const int PaintingCount = 5; + private Span GetPaintingSpan(int index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, PaintingCount); + return Data.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * PaintingCount); + } + + public Paintings3 GetPainting(int index, bool japanese) => new(GetPaintingSpan(index).ToArray(), japanese); + public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index)); + + public int DaycareOffset => 0x2F9C; + public int DaycareSlotSize => PokeCrypto.SIZE_3STORED; // mail stored separate from box mons + + public ushort DaycareSeed + { + get => ReadUInt16LittleEndian(Data[0x30B4..]); + set => WriteUInt16LittleEndian(Data[0x30B4..], value); + } + + public Span GiftRibbons => Data.Slice(ExternalEventData - 11, 11); + public int ExternalEventData => 0x311B; + public Memory RoamerData => Raw.Slice(0x3144, Roamer3.SIZE); + + private const int OFFSET_EBERRY = 0x3160; + private const int SIZE_EBERRY = 0x530; + public Span EReaderBerry => Data.Slice(OFFSET_EBERRY, SIZE_EBERRY); + + private Span MysterySpan => Data.Slice(0x3690, MysteryEvent3.SIZE); + public Gen3MysteryData MysteryData + { + get => new MysteryEvent3(MysterySpan.ToArray()); + set => value.Data.CopyTo(MysterySpan); + } + + private Span RecordSpan => Data.Slice(0x3A7C, RecordMixing3Gift.SIZE); + public RecordMixing3Gift RecordMixingGift + { + get => new(RecordSpan.ToArray()); + set => value.Data.CopyTo(RecordSpan); + } + + public int SeenOffset3 => 0x3A8C; +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallE.cs b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallE.cs new file mode 100644 index 000000000..bcb2b3cd1 --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallE.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed record SaveBlock3SmallE(Memory Raw) : ISaveBlock3SmallExpansion, ISaveBlock3SmallHoenn +{ + public Span Data => Raw.Span; + + public Span OriginalTrainerTrash => Data[..8]; + public byte Gender { get => Data[8]; set => Data[8] = value; } + public uint ID32 { get => ReadUInt32LittleEndian(Data[0x0A..]); set => WriteUInt32LittleEndian(Data[0x0A..], value); } + public ushort TID16 { get => ReadUInt16LittleEndian(Data[0xA..]); set => WriteUInt16LittleEndian(Data[0xA..], value); } + public ushort SID16 { get => ReadUInt16LittleEndian(Data[0xC..]); set => WriteUInt16LittleEndian(Data[0xC..], value); } + public int PlayedHours { get => ReadUInt16LittleEndian(Data[0xE..]); set => WriteUInt16LittleEndian(Data[0xE..], (ushort)value); } + public int PlayedMinutes { get => Data[0x10]; set => Data[0x10] = (byte)value; } + public int PlayedSeconds { get => Data[0x11]; set => Data[0x11] = (byte)value; } + public byte PlayedFrames { get => Data[0x12]; set => Data[0x12] = value; } + + public byte OptionsButtonMode { get => Data[0x13]; set => Data[0x13] = value; } + private uint OptionsConfig { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], value); } + public int TextSpeed { get => (int)(OptionsConfig & 0b11); set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); } + public byte OptionWindowFrame { get => (byte)((OptionsConfig >> 2) & 0b11111); set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); } + public bool OptionSoundStereo { get => (OptionsConfig & 0b100000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); } + public bool OptionBattleStyle { get => (OptionsConfig & 0b1000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); } + public bool OptionBattleScene { get => (OptionsConfig & 0b10000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); } + public bool OptionIsRegionMapZoom { get => (OptionsConfig & 0b100000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); } + + public byte PokedexSort { get => Data[0x18]; set => Data[0x18] = value; } + public byte PokedexMode { get => Data[0x19]; set => Data[0x19] = value; } + public byte PokedexNationalMagicRSE { get => Data[0x1A]; set => Data[0x1A] = value; } + public byte PokedexNationalMagicFRLG { get => Data[0x1B]; set => Data[0x1B] = value; } + public uint DexPIDUnown { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], value); } + public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], value); } + + private Span ClockInitialSpan => Data.Slice(0x098, RTC3.Size); + private Span ClockElapsedSpan => Data.Slice(0x0A0, RTC3.Size); + public RTC3 ClockInitial { get => new(ClockInitialSpan.ToArray()); set => value.Data.CopyTo(ClockInitialSpan); } + public RTC3 ClockElapsed { get => new(ClockElapsedSpan.ToArray()); set => value.Data.CopyTo(ClockElapsedSpan); } + + public uint LinkFlags { get => ReadUInt32LittleEndian(Data[0x0A8..]); set => WriteUInt32LittleEndian(Data[0x0A8..], value); } + public uint SecurityKey { get => ReadUInt32LittleEndian(Data[0x0AC..]); set => WriteUInt32LittleEndian(Data[0x0AC..], value); } + + // 0xB0: Player Apprentice + // 0xDC: Apprentices + + // 0x1EC: Berry Crush + public ushort GetBerryPressSpeed([Range(0, 3)] int index) => ReadUInt16LittleEndian(Data[(0x1EC + (index * 2))..]); + public void SetBerryPressSpeed([Range(0, 3)] int index, ushort value) => WriteUInt16LittleEndian(Data[(0x1EC + (index * 2))..], value); + public uint BerryPowder { get => ReadUInt32LittleEndian(Data[0x1F4..]) ^ SecurityKey; set => WriteUInt32LittleEndian(Data[0x1F4..], value ^ SecurityKey); } + public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Data[0x1FC..]); set => WriteUInt16LittleEndian(Data[0x1FC..], Math.Min((ushort)9999, value)); } + public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Data[0x200..]); set => WriteUInt16LittleEndian(Data[0x200..], Math.Min((ushort)9999, value)); } + public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Data[0x202..]); set => WriteUInt16LittleEndian(Data[0x202..], Math.Min((ushort)9999, value)); } + public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Data[0x208..]); set => WriteUInt32LittleEndian(Data[0x208..], Math.Min(99990, value)); } + public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Data[0x20C..]); set => WriteUInt32LittleEndian(Data[0x20C..], Math.Min(99990, value)); } + public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Data[0x210..]); set => WriteUInt16LittleEndian(Data[0x210..], Math.Min((ushort)9999, value)); } + public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Data[0x212..]); set => WriteUInt16LittleEndian(Data[0x212..], Math.Min((ushort)9999, value)); } + + // Battle Frontier: 0x64C + public Span EReaderTrainer => Data.Slice(0xBEC, 0xBC); + + public BattleFrontier3 BattleFrontier => new(Data.Slice(0xCDC, BattleFrontier3.SIZE)); + + public ushort BP { get => ReadUInt16LittleEndian(Data[0xEB8..]); set => WriteUInt16LittleEndian(Data[0xEB8..], Math.Min((ushort)9999, value)); } + public ushort BPEarned { get => ReadUInt16LittleEndian(Data[0xEBA..]); set => WriteUInt16LittleEndian(Data[0xEBA..], value); } +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallFRLG.cs b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallFRLG.cs new file mode 100644 index 000000000..3c5910559 --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallFRLG.cs @@ -0,0 +1,79 @@ +using System; +using System.ComponentModel.DataAnnotations; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed record SaveBlock3SmallFRLG(Memory Raw) : ISaveBlock3SmallExpansion +{ + public Span Data => Raw.Span; + + public Span OriginalTrainerTrash => Data[..8]; + public byte Gender { get => Data[8]; set => Data[8] = value; } + public uint ID32 { get => ReadUInt32LittleEndian(Data[0x0A..]); set => WriteUInt32LittleEndian(Data[0x0A..], value); } + public ushort TID16 { get => ReadUInt16LittleEndian(Data[0xA..]); set => WriteUInt16LittleEndian(Data[0xA..], value); } + public ushort SID16 { get => ReadUInt16LittleEndian(Data[0xC..]); set => WriteUInt16LittleEndian(Data[0xC..], value); } + public int PlayedHours { get => ReadUInt16LittleEndian(Data[0xE..]); set => WriteUInt16LittleEndian(Data[0xE..], (ushort)value); } + public int PlayedMinutes { get => Data[0x10]; set => Data[0x10] = (byte)value; } + public int PlayedSeconds { get => Data[0x11]; set => Data[0x11] = (byte)value; } + public byte PlayedFrames { get => Data[0x12]; set => Data[0x12] = value; } + + public byte OptionsButtonMode { get => Data[0x13]; set => Data[0x13] = value; } + private uint OptionsConfig { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], value); } + public int TextSpeed { get => (int)(OptionsConfig & 0b11); set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); } + public byte OptionWindowFrame { get => (byte)((OptionsConfig >> 2) & 0b11111); set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); } + public bool OptionSoundStereo { get => (OptionsConfig & 0b100000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); } + public bool OptionBattleStyle { get => (OptionsConfig & 0b1000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); } + public bool OptionBattleScene { get => (OptionsConfig & 0b10000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); } + public bool OptionIsRegionMapZoom { get => (OptionsConfig & 0b100000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); } + + public byte PokedexSort { get => Data[0x18]; set => Data[0x18] = value; } + public byte PokedexMode { get => Data[0x19]; set => Data[0x19] = value; } + public byte PokedexNationalMagicRSE { get => Data[0x1A]; set => Data[0x1A] = value; } + public byte PokedexNationalMagicFRLG { get => Data[0x1B]; set => Data[0x1B] = value; } + public uint DexPIDUnown { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], value); } + public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], value); } + + public uint LinkFlags { get => ReadUInt32LittleEndian(Data[0x0A8..]); set => WriteUInt32LittleEndian(Data[0x0A8..], value); } + + + public bool DummyFlagTrue + { + get => ReadUInt16LittleEndian(Data[0x0AC..]) != 0; + set => WriteUInt16LittleEndian(Data[0x0AC..], value ? (ushort)1 : (ushort)0); + } + + public bool DummyFlagFalse + { + get => ReadUInt16LittleEndian(Data[0x0AE..]) == 0; + set => WriteUInt16LittleEndian(Data[0x0AE..], value ? (ushort)0 : (ushort)1); + } + + public void FixDummyFlags() + { + DummyFlagTrue = true; + DummyFlagFalse = false; + } + + // Battle Tower: 0xB0 + + public Span EReaderTrainer => Data.Slice(0x4A0, 0xBC); + + // 0xAF0: Berry Crush + public ushort GetBerryPressSpeed([Range(0,3)] int index) => ReadUInt16LittleEndian(Data[(0xAF0 + (index * 2))..]); + public void SetBerryPressSpeed([Range(0, 3)] int index, ushort value) => WriteUInt16LittleEndian(Data[(0xAF0 + (index * 2))..], value); + public uint BerryPowder { get => ReadUInt32LittleEndian(Data[0xAF8..]) ^ SecurityKey; set => WriteUInt32LittleEndian(Data[0xAF8..], value ^ SecurityKey); } + public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Data[0xB00..]); set => WriteUInt16LittleEndian(Data[0xB00..], Math.Min((ushort)9999, value)); } + public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Data[0xB04..]); set => WriteUInt16LittleEndian(Data[0xB04..], Math.Min((ushort)9999, value)); } + public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Data[0xB06..]); set => WriteUInt16LittleEndian(Data[0xB06..], Math.Min((ushort)9999, value)); } + public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Data[0xB0C..]); set => WriteUInt32LittleEndian(Data[0xB0C..], Math.Min(99990, value)); } + public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Data[0xB10..]); set => WriteUInt32LittleEndian(Data[0xB10..], Math.Min(99990, value)); } + public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Data[0xB14..]); set => WriteUInt16LittleEndian(Data[0xB14..], Math.Min((ushort)9999, value)); } + public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Data[0xB16..]); set => WriteUInt16LittleEndian(Data[0xB16..], Math.Min((ushort)9999, value)); } + + public uint SecurityKey + { + get => ReadUInt32LittleEndian(Data[0xF20..]); + set => WriteUInt32LittleEndian(Data[0xF20..], value); + } +} diff --git a/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallRS.cs b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallRS.cs new file mode 100644 index 000000000..604a8153f --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/Gen3/SaveBlock3SmallRS.cs @@ -0,0 +1,45 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed record SaveBlock3SmallRS(Memory Raw) : ISaveBlock3SmallHoenn +{ + public Span Data => Raw.Span; + + public Span OriginalTrainerTrash => Data[..8]; + public byte Gender { get => Data[8]; set => Data[8] = value; } + public uint ID32 { get => ReadUInt32LittleEndian(Data[0x0A..]); set => WriteUInt32LittleEndian(Data[0x0A..], value); } + public ushort TID16 { get => ReadUInt16LittleEndian(Data[0xA..]); set => WriteUInt16LittleEndian(Data[0xA..], value); } + public ushort SID16 { get => ReadUInt16LittleEndian(Data[0xC..]); set => WriteUInt16LittleEndian(Data[0xC..], value); } + public int PlayedHours { get => ReadUInt16LittleEndian(Data[0xE..]); set => WriteUInt16LittleEndian(Data[0xE..], (ushort)value); } + public int PlayedMinutes { get => Data[0x10]; set => Data[0x10] = (byte)value; } + public int PlayedSeconds { get => Data[0x11]; set => Data[0x11] = (byte)value; } + public byte PlayedFrames { get => Data[0x12]; set => Data[0x12] = value; } + + public byte OptionsButtonMode { get => Data[0x13]; set => Data[0x13] = value; } + private uint OptionsConfig { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], value); } + public int TextSpeed { get => (int)(OptionsConfig & 0b11); set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); } + public byte OptionWindowFrame { get => (byte)((OptionsConfig >> 2) & 0b11111); set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); } + public bool OptionSoundStereo { get => (OptionsConfig & 0b100000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); } + public bool OptionBattleStyle { get => (OptionsConfig & 0b1000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); } + public bool OptionBattleScene { get => (OptionsConfig & 0b10000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); } + public bool OptionIsRegionMapZoom { get => (OptionsConfig & 0b100000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); } + + public byte PokedexSort { get => Data[0x18]; set => Data[0x18] = value; } + public byte PokedexMode { get => Data[0x19]; set => Data[0x19] = value; } + public byte PokedexNationalMagicRSE { get => Data[0x1A]; set => Data[0x1A] = value; } + public byte PokedexNationalMagicFRLG { get => Data[0x1B]; set => Data[0x1B] = value; } + public uint DexPIDUnown { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], value); } + public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], value); } + + private Span ClockInitialSpan => Data.Slice(0x098, RTC3.Size); + private Span ClockElapsedSpan => Data.Slice(0x0A0, RTC3.Size); + public RTC3 ClockInitial { get => new(ClockInitialSpan.ToArray()); set => value.Data.CopyTo(ClockInitialSpan); } + public RTC3 ClockElapsed { get => new(ClockElapsedSpan.ToArray()); set => value.Data.CopyTo(ClockElapsedSpan); } + + public uint SecurityKey => 0; + + // 0xA8: Battle Tower + public Span EReaderTrainer => Data.Slice(0x498, 0xBC); +} diff --git a/PKHeX.Core/Saves/SAV3.cs b/PKHeX.Core/Saves/SAV3.cs index c1d7107fb..b803655a4 100644 --- a/PKHeX.Core/Saves/SAV3.cs +++ b/PKHeX.Core/Saves/SAV3.cs @@ -18,8 +18,14 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai public bool Japanese { get; } public bool Korean => false; - public bool IsVirtualConsole => State.Exportable && Metadata.FileName is { } s && s.Contains(".sav") - && (s.StartsWith("FireRed_", StringComparison.Ordinal) || s.StartsWith("LeafGreen_")); // default to Mainline-Era for non-exportable + public bool IsVirtualConsole => State.Exportable && Metadata.FileName is { } s && IsVirtualConsoleFileName(s); // default to Mainline-Era for non-exportable + + public static bool IsVirtualConsoleFileName(string s) + { + if (!s.Contains(".sav")) + return false; + return s.StartsWith("FireRed_") || s.StartsWith("LeafGreen_"); + } // Similar to future games, the Generation 3 Mainline save files are comprised of separate objects: // Object 1 - Small, containing misc configuration data & the Pokédex. @@ -46,6 +52,8 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai public Span Small => SmallBuffer.Span; public Span Large => LargeBuffer.Span; public Span Storage => StorageBuffer.Span; + public abstract ISaveBlock3Small SmallBlock { get; } + public abstract ISaveBlock3Large LargeBlock { get; } private readonly int ActiveSlot; public sealed override int Language { get; set; } @@ -177,14 +185,11 @@ public void WriteBothSaveSlots(Span data) public sealed override ushort MaxMoveID => Legal.MaxMoveID_3; public sealed override ushort MaxSpeciesID => Legal.MaxSpeciesID_3; public sealed override int MaxAbilityID => Legal.MaxAbilityID_3; - public override int MaxItemID => Legal.MaxItemID_3; public sealed override int MaxBallID => Legal.MaxBallID_3; public sealed override GameVersion MaxGameID => Legal.MaxGameID_3; - public abstract int EventFlagCount { get; } - public abstract int EventWorkCount { get; } - protected abstract int EventFlag { get; } - protected abstract int EventWork { get; } + public int EventFlagCount => LargeBlock.EventFlagCount; + public int EventWorkCount => LargeBlock.EventWorkCount; /// /// Force loads a new object to the requested . @@ -210,13 +215,15 @@ public void WriteBothSaveSlots(Span data) public sealed override int MaxMoney => 999999; public sealed override bool HasParty => true; + public sealed override int PartyCount { get => LargeBlock.PartyCount; protected set => LargeBlock.PartyCount = (byte)value; } + public sealed override int GetPartyOffset(int slot) => SIZE_PARTY * slot; public sealed override bool IsPKMPresent(ReadOnlySpan data) => EntityDetection.IsPresentGBA(data); protected sealed override PK3 GetPKM(byte[] data) => new(data); protected sealed override byte[] DecryptPKM(byte[] data) => PokeCrypto.DecryptArray3(data); protected sealed override Span BoxBuffer => Storage; - protected sealed override Span PartyBuffer => Large; + protected sealed override Span PartyBuffer => LargeBlock.PartyBuffer; private const int COUNT_BOX = 14; private const int COUNT_SLOTSPERBOX = 30; @@ -328,7 +335,7 @@ public sealed override string ChecksumInfo public static bool IsMail(int itemID) => (uint)(itemID - 121) <= (132 - 121); - protected override void SetPartyValues(PKM pk, bool isParty) + protected sealed override void SetPartyValues(PKM pk, bool isParty) { if (pk is not PK3 p3) return; @@ -341,170 +348,82 @@ protected override void SetPartyValues(PKM pk, bool isParty) base.SetPartyValues(pk, isParty); } - public abstract uint SecurityKey { get; set; } - - public Span OriginalTrainerTrash => Small[..8]; - public sealed override string OT { get { int len = Japanese ? 5 : MaxStringLengthTrainer; - return GetString(OriginalTrainerTrash[..len]); + return GetString(SmallBlock.OriginalTrainerTrash[..len]); } set { int len = Japanese ? 5 : MaxStringLengthTrainer; - SetString(OriginalTrainerTrash[..len], value, len, StringConverterOption.ClearFF); // match the game-init FF terminating pattern + SetString(SmallBlock.OriginalTrainerTrash[..len], value, len, StringConverterOption.None); // Preserve original pattern } } public sealed override byte Gender { - get => Small[8]; - set => Small[8] = value; + get => SmallBlock.Gender; + set => SmallBlock.Gender = value; } public sealed override uint ID32 { - get => ReadUInt32LittleEndian(Small[0x0A..]); - set => WriteUInt32LittleEndian(Small[0x0A..], value); + get => SmallBlock.ID32; + set => SmallBlock.ID32 = value; } public sealed override ushort TID16 { - get => ReadUInt16LittleEndian(Small[0xA..]); - set => WriteUInt16LittleEndian(Small[0xA..], value); + get => SmallBlock.TID16; + set => SmallBlock.TID16 = value; } public sealed override ushort SID16 { - get => ReadUInt16LittleEndian(Small[0xC..]); - set => WriteUInt16LittleEndian(Small[0xC..], value); + get => SmallBlock.SID16; + set => SmallBlock.SID16 = value; } public sealed override int PlayedHours { - get => ReadUInt16LittleEndian(Small[0xE..]); - set => WriteUInt16LittleEndian(Small[0xE..], (ushort)value); + get => SmallBlock.PlayedHours; + set => SmallBlock.PlayedHours = value; } public sealed override int PlayedMinutes { - get => Small[0x10]; - set => Small[0x10] = (byte)value; + get => SmallBlock.PlayedMinutes; + set => SmallBlock.PlayedMinutes = value; } public sealed override int PlayedSeconds { - get => Small[0x11]; - set => Small[0x11] = (byte)value; - } - - public byte PlayedFrames - { - get => Small[0x12]; - set => Small[0x12] = value; - } - - public byte OptionsButtonMode - { - get => Small[0x13]; - set => Small[0x13] = value; - } - - private uint OptionsConfig - { - get => ReadUInt32LittleEndian(Small[0x14..]); - set => WriteUInt32LittleEndian(Small[0x14..], value); - } - - // 2 bits: Text Speed - // 5 bits: Window Frame - // 1 bit: sound - // 1 bit: battle style (shift vs set) - // 1 bit: battle scene off toggle (animations enabled/disabled) - // 1 bit: regionMapZoom (on/off) - // 4 bits unused - // 16 bits unused - public int TextSpeed - { - get => (int)(OptionsConfig & 0b11); - set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); - } - - public byte OptionWindowFrame - { - get => (byte)((OptionsConfig >> 2) & 0b11111); - set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); - } - - public bool OptionSoundStereo - { - get => (OptionsConfig & 0b100000) != 0; - set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); - } - - public bool OptionBattleStyle - { - get => (OptionsConfig & 0b1000000) != 0; - set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); - } - - public bool OptionBattleScene - { - get => (OptionsConfig & 0b10000000) != 0; - set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); - } - - public bool OptionIsRegionMapZoom - { - get => (OptionsConfig & 0b100000000) != 0; - set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); - } - - public ushort X - { - get => ReadUInt16LittleEndian(Large); - set => WriteUInt16LittleEndian(Large, value); - } - - public ushort Y - { - get => ReadUInt16LittleEndian(Large[2..]); - set => WriteUInt16LittleEndian(Large[2..], value); + get => SmallBlock.PlayedSeconds; + set => SmallBlock.PlayedSeconds = value; } #region Event Flag/Event Work - public bool GetEventFlag(int flagNumber) - { - if ((uint)flagNumber >= EventFlagCount) - throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount})."); - return GetFlag(EventFlag + (flagNumber >> 3), flagNumber & 7); - } + public bool GetEventFlag(int flagNumber) => LargeBlock.GetEventFlag(flagNumber); + public void SetEventFlag(int flagNumber, bool value) => LargeBlock.SetEventFlag(flagNumber, value); - public void SetEventFlag(int flagNumber, bool value) - { - if ((uint)flagNumber >= EventFlagCount) - throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount})."); - SetFlag(EventFlag + (flagNumber >> 3), flagNumber & 7, value); - } - - public ushort GetWork(int index) => ReadUInt16LittleEndian(Large[(EventWork + (index * 2))..]); - public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Large[EventWork..][(index * 2)..], value); + public ushort GetWork(int index) => LargeBlock.GetWork(index); + public void SetWork(int index, ushort value) => LargeBlock.SetWork(index, value); #endregion public sealed override bool GetFlag(int offset, int bitIndex) => GetFlag(Large, offset, bitIndex); public sealed override void SetFlag(int offset, int bitIndex, bool value) => SetFlag(Large, offset, bitIndex, value); + public uint GetRecord(int record) => LargeBlock.GetRecord(record) ^ SmallBlock.SecurityKey; + public void SetRecord(int record, uint value) => LargeBlock.SetRecord(record, value ^ SmallBlock.SecurityKey); - protected abstract int BadgeFlagStart { get; } public abstract uint Coin { get; set; } public int Badges { get { - int startFlag = BadgeFlagStart; + int startFlag = LargeBlock.BadgeFlagStart; int val = 0; for (int i = 0; i < 8; i++) { @@ -516,30 +435,27 @@ public int Badges } set { - int startFlag = BadgeFlagStart; + int startFlag = LargeBlock.BadgeFlagStart; for (int i = 0; i < 8; i++) SetEventFlag(startFlag + i, (value & (1 << i)) != 0); } } - protected abstract int PokeDex { get; } - public override bool HasPokeDex => true; + public sealed override bool HasPokeDex => true; public int DaycareSlotCount => 2; - protected abstract int DaycareSlotSize { get; } - protected abstract int DaycareOffset { get; } protected abstract int GetDaycareEXPOffset(int slot); - public Memory GetDaycareSlot(int slot) => LargeBuffer.Slice(GetDaycareSlotOffset(slot), DaycareSlotSize); + public Memory GetDaycareSlot(int slot) => LargeBuffer.Slice(GetDaycareSlotOffset(slot), LargeBlock.DaycareSlotSize); public uint GetDaycareEXP(int index) => ReadUInt32LittleEndian(Large[GetDaycareEXPOffset(index)..]); public void SetDaycareEXP(int index, uint value) => WriteUInt32LittleEndian(Large[GetDaycareEXPOffset(index)..], value); public bool IsDaycareOccupied(int slot) => IsPKMPresent(Large[GetDaycareSlotOffset(slot)..]); public void SetDaycareOccupied(int slot, bool occupied) { /* todo */ } - public int GetDaycareSlotOffset(int slot) => DaycareOffset + (slot * DaycareSlotSize); - protected abstract int EggEventFlag { get; } - public bool IsEggAvailable { get => GetEventFlag(EggEventFlag); set => SetEventFlag(EggEventFlag, value); } + public int GetDaycareSlotOffset(int slot) => LargeBlock.DaycareOffset + (slot * LargeBlock.DaycareSlotSize); + public bool IsEggAvailable { get => GetEventFlag(LargeBlock.EggEventFlag); set => SetEventFlag(LargeBlock.EggEventFlag, value); } #region Storage - public sealed override int GetBoxOffset(int box) => Box + 4 + (SIZE_STORED * box * COUNT_SLOTSPERBOX); + + public sealed override bool HasBox => true; public sealed override int CurrentBox { @@ -547,6 +463,8 @@ public sealed override int CurrentBox set => Storage[0] = (byte)value; } + public sealed override int GetBoxOffset(int box) => 4 + (SIZE_STORED * box * COUNT_SLOTSPERBOX); + public int GetBoxWallpaper(int box) { if (box >= COUNT_BOX) @@ -598,25 +516,24 @@ protected sealed override void SetDex(PKM pk) switch (species) { case (int)Species.Unown when !GetSeen(species): // Unown - DexPIDUnown = pk.PID; + SmallBlock.DexPIDUnown = pk.PID; break; case (int)Species.Spinda when !GetSeen(species): // Spinda - DexPIDSpinda = pk.PID; + SmallBlock.DexPIDSpinda = pk.PID; break; } SetCaught(species, true); SetSeen(species, true); } - public uint DexPIDUnown { get => ReadUInt32LittleEndian(Small[(PokeDex + 0x4)..]); set => WriteUInt32LittleEndian(Small[(PokeDex + 0x4)..], value); } - public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Small[(PokeDex + 0x8)..]); set => WriteUInt32LittleEndian(Small[(PokeDex + 0x8)..], value); } - public int DexUnownForm => EntityPID.GetUnownForm3(DexPIDUnown); + + private const int PokeDex = 0x18; // small public sealed override bool GetCaught(ushort species) { int bit = species - 1; int ofs = bit >> 3; - int caughtOffset = PokeDex + 0x10; + const int caughtOffset = PokeDex + 0x10; return FlagUtil.GetFlag(Small, caughtOffset + ofs, bit & 7); } @@ -624,7 +541,7 @@ public sealed override void SetCaught(ushort species, bool caught) { int bit = species - 1; int ofs = bit >> 3; - int caughtOffset = PokeDex + 0x10; + const int caughtOffset = PokeDex + 0x10; FlagUtil.SetFlag(Small, caughtOffset + ofs, bit & 7, caught); } @@ -632,46 +549,37 @@ public sealed override bool GetSeen(ushort species) { int bit = species - 1; int ofs = bit >> 3; - int seenOffset = PokeDex + 0x44; + const int seenOffset = PokeDex + 0x44; return FlagUtil.GetFlag(Small, seenOffset + ofs, bit & 7); } - protected abstract int SeenOffset2 { get; } - protected abstract int SeenOffset3 { get; } - public sealed override void SetSeen(ushort species, bool seen) { int bit = species - 1; int ofs = bit >> 3; - int seenOffset = PokeDex + 0x44; + const int seenOffset = PokeDex + 0x44; FlagUtil.SetFlag(Small, seenOffset + ofs, bit & 7, seen); - FlagUtil.SetFlag(Large, SeenOffset2 + ofs, bit & 7, seen); - FlagUtil.SetFlag(Large, SeenOffset3 + ofs, bit & 7, seen); + FlagUtil.SetFlag(Large, LargeBlock.SeenOffset2 + ofs, bit & 7, seen); + FlagUtil.SetFlag(Large, LargeBlock.SeenOffset3 + ofs, bit & 7, seen); } - public byte PokedexSort + /// + /// In Gen 3, the seen flags are stored in three different places. Mirror them to each other to ensure consistency. + /// + /// + /// Only really use this if you are allowing users to manually edit the seen flags in the first (normal) section; then trigger this on saving all. + /// + public void MirrorSeenFlags() { - get => Small[PokeDex + 0x01]; - set => Small[PokeDex + 0x01] = value; - } - - public byte PokedexMode - { - get => Small[PokeDex + 0x01]; - set => Small[PokeDex + 0x01] = value; - } - - public byte PokedexNationalMagicRSE - { - get => Small[PokeDex + 0x02]; - set => Small[PokeDex + 0x02] = value; - } - - public byte PokedexNationalMagicFRLG - { - get => Small[PokeDex + 0x03]; - set => Small[PokeDex + 0x03] = value; + for (ushort species = 1; species <= Legal.MaxSpeciesID_3; species++) + { + int bit = species - 1; + int ofs = bit >> 3; + bool seen = FlagUtil.GetFlag(Small, PokeDex + 0x44 + ofs, bit & 7); + FlagUtil.SetFlag(Large, LargeBlock.SeenOffset2 + ofs, bit & 7, seen); + FlagUtil.SetFlag(Large, LargeBlock.SeenOffset3 + ofs, bit & 7, seen); + } } protected const byte PokedexNationalUnlockRSE = 0xDA; @@ -684,32 +592,18 @@ public byte PokedexNationalMagicFRLG public sealed override string GetString(ReadOnlySpan data) => StringConverter3.GetString(data, Japanese); - public override int LoadString(ReadOnlySpan data, Span destBuffer) + public sealed override int LoadString(ReadOnlySpan data, Span destBuffer) => StringConverter3.LoadString(data, destBuffer, Japanese); public sealed override int SetString(Span destBuffer, ReadOnlySpan value, int maxLength, StringConverterOption option) => StringConverter3.SetString(destBuffer, value, maxLength, Japanese, option); - protected abstract int MailOffset { get; } - public int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset; + public string EBerryName => GetString(LargeBlock.EReaderBerry[..7]); + public bool IsEBerryEngima => LargeBlock.EReaderBerry[0] is 0 or 0xFF; - public MailDetail GetMail(int mailIndex) - { - var ofs = GetMailOffset(mailIndex); - var data = Large.Slice(ofs, Mail3.SIZE).ToArray(); - return new Mail3(data, ofs); - } - - #region eBerry - public abstract Span EReaderBerry(); - public string EBerryName => GetString(EReaderBerry()[..7]); - public bool IsEBerryEngima => EReaderBerry()[0] is 0 or 0xFF; - #endregion - - #region eTrainer - public abstract Span EReaderTrainer(); - #endregion - - public abstract Gen3MysteryData MysteryData { get; set; } + /// + /// Indicates if the extdata sections of the save file are available for get/set. + /// + public bool IsFullSaveFile => Data.Length >= SaveUtil.SIZE_G3RAW; /// /// Hall of Fame data is split across two sectors. @@ -741,14 +635,15 @@ public void SetHallOfFameData(ReadOnlySpan value) /// public Memory GetEReaderData() => Buffer.Slice(0x1E000, SIZE_SECTOR_USED); - /// Only used in Emerald. + /// Only used in Emerald for storing the Battle Video. public Memory GetFinalExternalData() => Buffer.Slice(0x1F000, SIZE_SECTOR_USED); public bool IsCorruptPokedexFF() => MemoryMarshal.Read(Small[0xAC..]) == ulong.MaxValue; - public override void CopyChangesFrom(SaveFile sav) + public sealed override void CopyChangesFrom(SaveFile sav) { - SetData(sav.Data, 0); + if (Data.Length != 0) + SetData(sav.Data, 0); var s3 = (SAV3)sav; SetData(Small, s3.Small); SetData(Large, s3.Large); @@ -757,7 +652,7 @@ public override void CopyChangesFrom(SaveFile sav) #region External Connections - public Span GiftRibbons => Large.Slice(ExternalEventData - 11, 11); + public Span GiftRibbons => LargeBlock.GiftRibbons; public void GiftRibbonsImport(ReadOnlySpan trade) { @@ -773,7 +668,7 @@ public void GiftRibbonsImport(ReadOnlySpan trade) } public void GiftRibbonsClear() => GiftRibbons.Clear(); - protected abstract int ExternalEventData { get; } + private int ExternalEventData => LargeBlock.ExternalEventData; protected int ExternalEventFlags => ExternalEventData + 0x14; public uint ColosseumRaw1 diff --git a/PKHeX.Core/Saves/SAV3E.cs b/PKHeX.Core/Saves/SAV3E.cs index 544c1e0f3..e116a2ff8 100644 --- a/PKHeX.Core/Saves/SAV3E.cs +++ b/PKHeX.Core/Saves/SAV3E.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -8,280 +7,70 @@ namespace PKHeX.Core; /// Generation 3 object for . /// /// -public sealed class SAV3E : SAV3, IGen3Hoenn, IGen3Joyful, IGen3Wonder, IDaycareRandomState +public sealed class SAV3E : SAV3, IDaycareRandomState { // Configuration protected override SAV3E CloneInternal() => new(GetFinalData()) { Language = Language }; + public override SaveBlock3SmallE SmallBlock { get; } + public override SaveBlock3LargeE LargeBlock { get; } public override GameVersion Version { get => GameVersion.E; set { } } public override PersonalTable3 Personal => PersonalTable.E; - public override int EventFlagCount => 8 * 300; - public override int EventWorkCount => 0x100; - protected override int DaycareSlotSize => SIZE_STORED + 0x3C; // 0x38 mail + 4 exp - protected override int EggEventFlag => 0x86; - protected override int BadgeFlagStart => 0x867; - - public SAV3E(Memory data) : base(data) => Initialize(); - public SAV3E(bool japanese = false) : base(japanese) => Initialize(); + public SAV3E(Memory data) : base(data) + { + SmallBlock = new SaveBlock3SmallE(SmallBuffer[..0xF2C]); + LargeBlock = new SaveBlock3LargeE(LargeBuffer[..0x3D88]); + } + public SAV3E(bool japanese = false) : base(japanese) + { + SmallBlock = new SaveBlock3SmallE(SmallBuffer[..0xF2C]); + LargeBlock = new SaveBlock3LargeE(LargeBuffer[..0x3D88]); + } public override PlayerBag3E Inventory => new(this); - protected override int EventFlag => 0x1270; - protected override int EventWork => 0x139C; public override int MaxItemID => Legal.MaxItemID_3_E; - protected override int PokeDex => 0x18; // small - protected override int DaycareOffset => 0x3030; // large - - // storage - private void Initialize() => Box = 0; - #region Small public override bool NationalDex { - get => PokedexNationalMagicRSE == PokedexNationalUnlockRSE; + get => SmallBlock.PokedexNationalMagicRSE == PokedexNationalUnlockRSE; set { - PokedexMode = value ? (byte)1 : (byte)0; // mode - PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; // magic + SmallBlock.PokedexMode = value ? (byte)1 : (byte)0; // mode + SmallBlock.PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; // magic SetEventFlag(0x896, value); SetWork(0x46, PokedexNationalUnlockWorkRSE); } } - public override uint SecurityKey - { - get => ReadUInt32LittleEndian(Small[0xAC..]); - set => WriteUInt32LittleEndian(Small[0xAC..], value); - } - - public RTC3 ClockInitial - { - get => new(Small.Slice(0x98, RTC3.Size).ToArray()); - set => SetData(Small[0x98..], value.Data); - } - - public RTC3 ClockElapsed - { - get => new(Small.Slice(0xA0, RTC3.Size).ToArray()); - set => SetData(Small[0xA0..], value.Data); - } - - public uint BerryPowder - { - get => ReadUInt32LittleEndian(Small[0x1F4..]) ^ SecurityKey; - set => WriteUInt32LittleEndian(Small[0x1F4..], value ^ SecurityKey); - } - - public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Small[0x1FC..]); set => WriteUInt16LittleEndian(Small[0x1FC..], Math.Min((ushort)9999, value)); } - // u16 field2; - public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Small[0x200..]); set => WriteUInt16LittleEndian(Small[0x200..], Math.Min((ushort)9999, value)); } - public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Small[0x202..]); set => WriteUInt16LittleEndian(Small[0x202..], Math.Min((ushort)9999, value)); } - // u32 field8; - public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Small[0x208..]); set => WriteUInt32LittleEndian(Small[0x208..], Math.Min(99990, value)); } - - public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Small[0x20C..]); set => WriteUInt32LittleEndian(Small[0x20C..], Math.Min(99990, value)); } - public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Small[0x210..]); set => WriteUInt16LittleEndian(Small[0x210..], Math.Min((ushort)9999, value)); } - public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Small[0x212..]); set => WriteUInt16LittleEndian(Small[0x212..], Math.Min((ushort)9999, value)); } - - public BattleFrontier3 BattleFrontier => new(Small.Slice(0xCDC, BattleFrontier3.SIZE)); - - public uint BP - { - get => ReadUInt16LittleEndian(Small[0xEB8..]); - set - { - if (value > 9999) - value = 9999; - WriteUInt16LittleEndian(Small[0xEB8..], (ushort)value); - } - } - - public uint BPEarned - { - get => ReadUInt16LittleEndian(Small[0xEBA..]); - set - { - if (value > 65535) - value = 65535; - WriteUInt16LittleEndian(Small[0xEBA..], (ushort)value); - } - } #endregion #region Large - public override int PartyCount { get => Large[0x234]; protected set => Large[0x234] = (byte)value; } - public override int GetPartyOffset(int slot) => 0x238 + (SIZE_PARTY * slot); public override uint Money { - get => ReadUInt32LittleEndian(Large[0x0490..]) ^ SecurityKey; - set => WriteUInt32LittleEndian(Large[0x0490..], value ^ SecurityKey); + get => LargeBlock.Money ^ SmallBlock.SecurityKey; + set => LargeBlock.Money = value ^ SmallBlock.SecurityKey; } public override uint Coin { - get => (ushort)(ReadUInt16LittleEndian(Large[0x0494..]) ^ SecurityKey); - set => WriteUInt16LittleEndian(Large[0x0494..], (ushort)(value ^ SecurityKey)); + get => (ushort)(LargeBlock.Coin ^ SmallBlock.SecurityKey); + set => LargeBlock.Coin = (ushort)(value ^ SmallBlock.SecurityKey); } - private const int OFS_BerryBlenderRecord = 0x9BC; - private const int OFS_TrendyWord = 0x2E20; - private const int OFS_TrainerHillRecord = 0x3718; - - private Span PokeBlockData => Large.Slice(0x848, PokeBlock3Case.SIZE); - - public PokeBlock3Case PokeBlocks - { - get => new(PokeBlockData); - set => value.Write(PokeBlockData); - } - - protected override int SeenOffset2 => 0x988; - - public DecorationInventory3 Decorations => new(Large.Slice(0x2734, DecorationInventory3.SIZE)); - - private Span SwarmSpan => Large.Slice(0x2B90, Swarm3.SIZE); - public Swarm3 Swarm - { - get => new(SwarmSpan.ToArray()); - set => SetData(SwarmSpan, value.Data); - } - - private void ClearSwarm() => SwarmSpan.Clear(); - - public IReadOnlyList DefaultSwarms => Swarm3Details.Swarms_E; - - public int SwarmIndex - { - get => Array.FindIndex(Swarm3Details.Swarms_E, z => z.MapNum == Swarm.MapNum); - set - { - var arr = DefaultSwarms; - if ((uint)value >= arr.Count) - ClearSwarm(); - else - Swarm = arr[value]; - } - } - - protected override int MailOffset => 0x2BE0; - protected override int GetDaycareEXPOffset(int slot) => GetDaycareSlotOffset(slot + 1) - 4; // @ end of each pk slot uint IDaycareRandomState.Seed // after the 2 slots, before the step counter { - get => ReadUInt32LittleEndian(Large[GetDaycareEXPOffset(2)..]); - set => WriteUInt32LittleEndian(Large[GetDaycareEXPOffset(2)..], value); + get => LargeBlock.DaycareSeed; + set => LargeBlock.DaycareSeed = value; } - - protected override int ExternalEventData => 0x31B3; - - /// - /// Max RPM for 2, 3 and 4 players. Each value unit represents 0.01 RPM. Value 0 if no record. - /// - /// 2 players: index 0, 3 players: index 1, 4 players: index 2 - public const int BerryBlenderRPMRecordCount = 3; - - private Span GetBlenderRPMSpan(int index) - { - if ((uint)index >= BerryBlenderRPMRecordCount) - throw new ArgumentOutOfRangeException(nameof(index)); - return Large[(OFS_BerryBlenderRecord + (index * 2))..]; - } - - public ushort GetBerryBlenderRPMRecord(int index) => ReadUInt16LittleEndian(GetBlenderRPMSpan(index)); - - public void SetBerryBlenderRPMRecord(int index, ushort value) - { - WriteUInt16LittleEndian(GetBlenderRPMSpan(index), value); - State.Edited = true; - } - - public bool GetTrendyWordUnlocked(TrendyWord3E word) - { - return GetFlag(OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7); - } - - public void SetTrendyWordUnlocked(TrendyWord3E word, bool value) - { - SetFlag(OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7, value); - State.Edited = true; - } - - /** Each value unit represents 1/60th of a second. Value 0 if no record. */ - public uint GetTrainerHillRecord(TrainerHillMode3E mode) - { - return ReadUInt32LittleEndian(Large[(OFS_TrainerHillRecord + ((byte)mode * 4))..]); - } - - public void SetTrainerHillRecord(TrainerHillMode3E mode, uint value) - { - WriteUInt32LittleEndian(Large[(OFS_TrainerHillRecord + ((byte)mode * 4))..], value); - State.Edited = true; - } - - #region eBerry - private const int OFFSET_EBERRY = 0x31F8; - private const int SIZE_EBERRY = 0x34; - - public override Span EReaderBerry() => Large.Slice(OFFSET_EBERRY, SIZE_EBERRY); - #endregion - - #region eTrainer - public override Span EReaderTrainer() => Small.Slice(0xBEC, 0xBC); - #endregion - - public int WonderOffset => WonderNewsOffset; - private const int WonderNewsOffset = 0x322C; - private int WonderCardOffset => WonderNewsOffset + (Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); - private int WonderCardExtraOffset => WonderCardOffset + (Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); - - private Span WonderNewsData => Large.Slice(WonderNewsOffset, Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); - private Span WonderCardData => Large.Slice(WonderCardOffset, Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); - private Span WonderCardExtraData => Large.Slice(WonderCardExtraOffset, WonderCard3Extra.SIZE); - - public WonderNews3 WonderNews { get => new(WonderNewsData.ToArray()); set => SetData(WonderNewsData, value.Data); } - public WonderCard3 WonderCard { get => new(WonderCardData.ToArray()); set => SetData(WonderCardData, value.Data); } - public WonderCard3Extra WonderCardExtra { get => new(WonderCardExtraData.ToArray()); set => SetData(WonderCardExtraData, value.Data); } - // 0x338: 4 easy chat words - // 0x340: news MENewsJisanStruct - // 0x344: uint[5], uint[5] tracking? - - private Span MysterySpan => Large.Slice(0x3728, MysteryEvent3.SIZE); - public override Gen3MysteryData MysteryData - { - get => new MysteryEvent3(MysterySpan.ToArray()); - set => SetData(MysterySpan, value.Data); - } - - private Span RecordMixingData => Large.Slice(0x3B14, RecordMixing3Gift.SIZE); - public RecordMixing3Gift RecordMixingGift { get => new(RecordMixingData.ToArray()); set => SetData(RecordMixingData, value.Data); } - - protected override int SeenOffset3 => 0x3B24; - - private const int Walda = 0x3D70; - public ushort WaldaBackgroundColor { get => ReadUInt16LittleEndian(Large[(Walda + 0)..]); set => WriteUInt16LittleEndian(Large[(Walda + 0)..], value); } - public ushort WaldaForegroundColor { get => ReadUInt16LittleEndian(Large[(Walda + 2)..]); set => WriteUInt16LittleEndian(Large[(Walda + 2)..], value); } - public byte WaldaIconID { get => Large[Walda + 0x14]; set => Large[Walda + 0x14] = value; } - public byte WaldaPatternID { get => Large[Walda + 0x15]; set => Large[Walda + 0x15] = value; } - public bool WaldaUnlocked { get => Large[Walda + 0x16] != 0; set => Large[Walda + 0x16] = (byte)(value ? 1 : 0); } - - private Memory SecretBaseData => LargeBuffer.Slice(0x1A9C, SecretBaseManager3.BaseCount * SecretBase3.SIZE); - public SecretBaseManager3 SecretBases => new(SecretBaseData); - - private const int Painting = 0x2F90; - private const int CountPaintings = 5; - private Span GetPaintingSpan(int index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, CountPaintings); - return Large.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * CountPaintings); - } - public Paintings3 GetPainting(int index) => new(GetPaintingSpan(index).ToArray(), Japanese); - public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index)); #endregion private const uint EXTRADATA_SENTINEL = 0x0000B39D; - public bool HasBattleVideo => Data.Length > SaveUtil.SIZE_G3RAWHALF && ReadUInt32LittleEndian(GetFinalExternalData().Span) == EXTRADATA_SENTINEL; + public bool HasBattleVideo => IsFullSaveFile && ReadUInt32LittleEndian(GetFinalExternalData().Span) == EXTRADATA_SENTINEL; + public void SetExtraDataSentinelBattleVideo() => WriteUInt32LittleEndian(GetFinalExternalData().Span, EXTRADATA_SENTINEL); public Memory BattleVideoData => GetFinalExternalData().Slice(4, BattleVideo3.SIZE); diff --git a/PKHeX.Core/Saves/SAV3FRLG.cs b/PKHeX.Core/Saves/SAV3FRLG.cs index 3d0ae4f46..359551ab6 100644 --- a/PKHeX.Core/Saves/SAV3FRLG.cs +++ b/PKHeX.Core/Saves/SAV3FRLG.cs @@ -1,5 +1,4 @@ using System; -using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -7,41 +6,39 @@ namespace PKHeX.Core; /// Generation 5 object for . /// /// -public sealed class SAV3FRLG : SAV3, IGen3Joyful, IGen3Wonder, IDaycareRandomState +public sealed class SAV3FRLG : SAV3, IDaycareRandomState { // Configuration protected override SAV3FRLG CloneInternal() => new(GetFinalData()) { Language = Language }; - public override GameVersion Version { get; set; } = GameVersion.FR; // allow mutation + public override SaveBlock3SmallFRLG SmallBlock { get; } + public override SaveBlock3LargeFRLG LargeBlock { get; } + public override GameVersion Version + { + get; + set => field = value is GameVersion.FR or GameVersion.LG ? value : GameVersion.FRLG; + } = GameVersion.FR; // allow mutation private PersonalTable3 _personal = PersonalTable.FR; public override PersonalTable3 Personal => _personal; + public override int MaxItemID => Legal.MaxItemID_3_FRLG; - public override int EventFlagCount => 8 * 288; - public override int EventWorkCount => 0x100; - protected override int DaycareSlotSize => SIZE_STORED + 0x3C; // 0x38 mail + 4 exp - protected override int EggEventFlag => 0x266; - protected override int BadgeFlagStart => 0x820; - - public SAV3FRLG(bool japanese = false) : base(japanese) => Initialize(); - - public override PlayerBag3FRLG Inventory => new(this); + public SAV3FRLG(bool japanese = false) : base(japanese) + { + SmallBlock = new SaveBlock3SmallFRLG(SmallBuffer[..0xF24]); + LargeBlock = new SaveBlock3LargeFRLG(LargeBuffer[..0x3D68]); + } public SAV3FRLG(Memory data) : base(data) { - Initialize(); + SmallBlock = new SaveBlock3SmallFRLG(SmallBuffer[..0xF24]); + LargeBlock = new SaveBlock3LargeFRLG(LargeBuffer[..0x3D68]); // Fix save files that have an overflow corruption with the Pokédex. // Future loads of this save file will cause it to be recognized as FR/LG correctly. if (IsCorruptPokedexFF()) - WriteUInt32LittleEndian(Small[0xAC..], 1); + SmallBlock.FixDummyFlags(); } - protected override int EventFlag => 0xEE0; - protected override int EventWork => 0x1000; - protected override int PokeDex => 0x18; // small - protected override int DaycareOffset => 0x2F80; // large - - // storage - private void Initialize() => Box = 0; + public override PlayerBag3FRLG Inventory => new(this); public bool ResetPersonal(GameVersion g) { @@ -55,105 +52,40 @@ public bool ResetPersonal(GameVersion g) #region Small public override bool NationalDex { - get => PokedexNationalMagicFRLG == PokedexNationalUnlockFRLG; + get => SmallBlock.PokedexNationalMagicFRLG == PokedexNationalUnlockFRLG; set { - PokedexNationalMagicFRLG = value ? PokedexNationalUnlockFRLG : (byte)0; // magic + SmallBlock.PokedexNationalMagicFRLG = value ? PokedexNationalUnlockFRLG : (byte)0; SetEventFlag(0x840, value); SetWork(0x4E, PokedexNationalUnlockWorkFRLG); } } - - public uint BerryPowder - { - get => ReadUInt32LittleEndian(Small[0xAF8..]) ^ SecurityKey; - set => WriteUInt32LittleEndian(Small[0xAF8..], value ^ SecurityKey); - } - - public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Small[0xB00..]); set => WriteUInt16LittleEndian(Small[0xB00..], Math.Min((ushort)9999, value)); } - // u16 field2; - public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Small[0xB04..]); set => WriteUInt16LittleEndian(Small[0xB04..], Math.Min((ushort)9999, value)); } - public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Small[0xB06..]); set => WriteUInt16LittleEndian(Small[0xB06..], Math.Min((ushort)9999, value)); } - // u32 field8; - public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Small[0xB0C..]); set => WriteUInt32LittleEndian(Small[0xB0C..], Math.Min(99990, value)); } - - public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Small[0xB10..]); set => WriteUInt32LittleEndian(Small[0xB10..], Math.Min(99990, value)); } - public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Small[0xB14..]); set => WriteUInt16LittleEndian(Small[0xB14..], Math.Min((ushort)9999, value)); } - public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Small[0xB16..]); set => WriteUInt16LittleEndian(Small[0xB16..], Math.Min((ushort)9999, value)); } - - public override uint SecurityKey - { - get => ReadUInt32LittleEndian(Small[0xF20..]); - set => WriteUInt32LittleEndian(Small[0xF20..], value); - } #endregion #region Large - public override int PartyCount { get => Large[0x034]; protected set => Large[0x034] = (byte)value; } - public override int GetPartyOffset(int slot) => 0x038 + (SIZE_PARTY * slot); - public override uint Money { - get => ReadUInt32LittleEndian(Large[0x0290..]) ^ SecurityKey; - set => WriteUInt32LittleEndian(Large[0x0290..], value ^ SecurityKey); + get => LargeBlock.Money ^ SmallBlock.SecurityKey; + set => LargeBlock.Money = value ^ SmallBlock.SecurityKey; } public override uint Coin { - get => (ushort)(ReadUInt16LittleEndian(Large[0x0294..]) ^ SecurityKey); - set => WriteUInt16LittleEndian(Large[0x0294..], (ushort)(value ^ SecurityKey)); + get => (ushort)(LargeBlock.Coin ^ SmallBlock.SecurityKey); + set => LargeBlock.Coin = (ushort)(value ^ SmallBlock.SecurityKey); } - protected override int SeenOffset2 => 0x5F8; - protected override int MailOffset => 0x2CD0; - protected override int GetDaycareEXPOffset(int slot) => GetDaycareSlotOffset(slot + 1) - 4; // @ end of each pk slot ushort IDaycareRandomState.Seed { - get => ReadUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..]); - set => WriteUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..], value); + get => LargeBlock.DaycareSeed; + set => LargeBlock.DaycareSeed = value; } - protected override int ExternalEventData => 0x30A7; - - #region eBerry - private const int OFFSET_EBERRY = 0x30EC; - private const int SIZE_EBERRY = 0x34; - - public override Span EReaderBerry() => Large.Slice(OFFSET_EBERRY, SIZE_EBERRY); - #endregion - - #region eTrainer - public override Span EReaderTrainer() => Small.Slice(0x4A0, 0xBC); - #endregion - - public int WonderOffset => WonderNewsOffset; - private const int WonderNewsOffset = 0x3120; - private int WonderCardOffset => WonderNewsOffset + (Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); - private int WonderCardExtraOffset => WonderCardOffset + (Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); - - private Span WonderNewsData => Large.Slice(WonderNewsOffset, Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE); - private Span WonderCardData => Large.Slice(WonderCardOffset, Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE); - private Span WonderCardExtraData => Large.Slice(WonderCardExtraOffset, WonderCard3Extra.SIZE); - - public WonderNews3 WonderNews { get => new(WonderNewsData.ToArray()); set => SetData(WonderNewsData, value.Data); } - public WonderCard3 WonderCard { get => new(WonderCardData.ToArray()); set => SetData(WonderCardData, value.Data); } - public WonderCard3Extra WonderCardExtra { get => new(WonderCardExtraData.ToArray()); set => SetData(WonderCardExtraData, value.Data); } - - // 0x338: 4 easy chat words - // 0x340: news MENewsJisanStruct - // 0x344: uint[5], uint[5] tracking? - - private Span MysterySpan => Large.Slice(0x361C, MysteryEvent3.SIZE); - public override Gen3MysteryData MysteryData { get => new MysteryEvent3(MysterySpan.ToArray()); set => SetData(MysterySpan, value.Data); } - - protected override int SeenOffset3 => 0x3A18; - public string RivalName { - get => GetString(Large.Slice(0x3A4C, 8)); - set => SetString(Large.Slice(0x3A4C, 8), value, 7, StringConverterOption.ClearZero); + get => GetString(LargeBlock.RivalNameTrash); + set => SetString(LargeBlock.RivalNameTrash, value, 7, StringConverterOption.ClearZero); } - #endregion } diff --git a/PKHeX.Core/Saves/SAV3RS.cs b/PKHeX.Core/Saves/SAV3RS.cs index cbf6053b9..4eecc00ff 100644 --- a/PKHeX.Core/Saves/SAV3RS.cs +++ b/PKHeX.Core/Saves/SAV3RS.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; @@ -8,160 +6,68 @@ namespace PKHeX.Core; /// Generation 3 object for . /// /// -public sealed class SAV3RS : SAV3, IGen3Hoenn, IDaycareRandomState +public sealed class SAV3RS : SAV3, IDaycareRandomState { // Configuration protected override SAV3RS CloneInternal() => new(GetFinalData()) { Language = Language }; + public override SaveBlock3SmallRS SmallBlock { get; } + public override SaveBlock3LargeRS LargeBlock { get; } public override GameVersion Version { get; - set => field = value is GameVersion.RS or GameVersion.R or GameVersion.S ? value : GameVersion.RS; + set => field = value is GameVersion.R or GameVersion.S ? value : GameVersion.RS; } = GameVersion.RS; public override PersonalTable3 Personal => PersonalTable.RS; + public override int MaxItemID => Legal.MaxItemID_3_RS; - public override int EventFlagCount => 8 * 288; - public override int EventWorkCount => 0x100; - protected override int DaycareSlotSize => SIZE_STORED; - protected override int EggEventFlag => 0x86; - protected override int BadgeFlagStart => 0x807; + public SAV3RS(Memory data) : base(data) + { + SmallBlock = new SaveBlock3SmallRS(SmallBuffer[..0x890]); + LargeBlock = new SaveBlock3LargeRS(LargeBuffer[..0x3AC0]); + } - public SAV3RS(Memory data) : base(data) => Initialize(); - public SAV3RS(bool japanese = false) : base(japanese) => Initialize(); + public SAV3RS(bool japanese = false) : base(japanese) + { + SmallBlock = new SaveBlock3SmallRS(SmallBuffer[..0x890]); + LargeBlock = new SaveBlock3LargeRS(LargeBuffer[..0x3AC0]); + } public override PlayerBag3RS Inventory => new(this); - protected override int EventFlag => 0x1220; - protected override int EventWork => 0x1340; - - protected override int PokeDex => 0x18; // small - protected override int DaycareOffset => 0x2F9C; // large - - // storage - private void Initialize() => Box = 0; - #region Small public override bool NationalDex { - get => PokedexNationalMagicRSE == PokedexNationalUnlockRSE; + get => SmallBlock.PokedexNationalMagicRSE == PokedexNationalUnlockRSE; set { - PokedexMode = value ? (byte)1 : (byte)0; // mode - PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; // magic + SmallBlock.PokedexMode = value ? (byte)1 : (byte)0; + SmallBlock.PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; SetEventFlag(0x836, value); SetWork(0x46, PokedexNationalUnlockWorkRSE); } } - - public override uint SecurityKey { get => 0; set { } } - - public RTC3 ClockInitial - { - get => new(Small.Slice(0x98, RTC3.Size).ToArray()); - set => SetData(Small.Slice(0x98, RTC3.Size), value.Data); - } - - public RTC3 ClockElapsed - { - get => new(Small.Slice(0xA0, RTC3.Size).ToArray()); - set => SetData(Small.Slice(0xA0, RTC3.Size), value.Data); - } #endregion #region Large - public override int PartyCount { get => Large[0x234]; protected set => Large[0x234] = (byte)value; } - public override int GetPartyOffset(int slot) => 0x238 + (SIZE_PARTY * slot); - public override uint Money { - get => ReadUInt32LittleEndian(Large[0x0490..]); - set => WriteUInt32LittleEndian(Large[0x0490..], value); + get => LargeBlock.Money; + set => LargeBlock.Money = value; } public override uint Coin { - get => ReadUInt16LittleEndian(Large[0x0494..]); - set => WriteUInt16LittleEndian(Large[0x0494..], (ushort)(value)); + get => LargeBlock.Coin; + set => LargeBlock.Coin = (ushort)value; } - private Span PokeBlockData => Large.Slice(0x7F8, PokeBlock3Case.SIZE); - - public PokeBlock3Case PokeBlocks - { - get => new(PokeBlockData); - set => value.Write(PokeBlockData); - } - - protected override int SeenOffset2 => 0x938; - - public DecorationInventory3 Decorations => new(Large.Slice(0x26A0, DecorationInventory3.SIZE)); - - private Span SwarmData => Large.Slice(0x2AFC, Swarm3.SIZE); - public Swarm3 Swarm - { - get => new(SwarmData.ToArray()); - set => SetData(SwarmData, value.Data); - } - - private void ClearSwarm() => Large.Slice(0x2AFC, Swarm3.SIZE).Clear(); - - public IReadOnlyList DefaultSwarms => Swarm3Details.Swarms_RS; - - public int SwarmIndex - { - get => Array.FindIndex(Swarm3Details.Swarms_RS, z => z.MapNum == Swarm.MapNum); - set - { - var arr = DefaultSwarms; - if ((uint)value >= arr.Count) - ClearSwarm(); - else - Swarm = arr[value]; - } - } - - protected override int MailOffset => 0x2B4C; - protected override int GetDaycareEXPOffset(int slot) => GetDaycareSlotOffset(2) + (2 * 0x38) + (4 * slot); // consecutive vals, after both consecutive slots & 2 mail ushort IDaycareRandomState.Seed { - get => ReadUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..]); - set => WriteUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..], value); + get => LargeBlock.DaycareSeed; + set => LargeBlock.DaycareSeed = value; } - - protected override int ExternalEventData => 0x311B; - - #region eBerry - private const int OFFSET_EBERRY = 0x3160; - private const int SIZE_EBERRY = 0x530; - - public override Span EReaderBerry() => Large.Slice(OFFSET_EBERRY, SIZE_EBERRY); - #endregion - - #region eTrainer - public override Span EReaderTrainer() => Small.Slice(0x498, 0xBC); - #endregion - - private Span MysterySpan => Large.Slice(0x3690, MysteryEvent3.SIZE); - public override Gen3MysteryData MysteryData { get => new MysteryEvent3(MysterySpan.ToArray()); set => SetData(MysterySpan, value.Data); } - - private Span RecordSpan => Large.Slice(0x3A7C, RecordMixing3Gift.SIZE); - public RecordMixing3Gift RecordMixingGift { get => new(RecordSpan.ToArray()); set => SetData(RecordSpan, value.Data); } - - protected override int SeenOffset3 => 0x3A8C; - - private Memory SecretBaseData => LargeBuffer.Slice(0x1A08, SecretBaseManager3.BaseCount * SecretBase3.SIZE); - public SecretBaseManager3 SecretBases => new(SecretBaseData); - - private const int Painting = 0x2EFC; - private const int CountPaintings = 5; - private Span GetPaintingSpan(int index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, CountPaintings); - return Large.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * CountPaintings); - } - public Paintings3 GetPainting(int index) => new(GetPaintingSpan(index).ToArray(), Japanese); - public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index)); #endregion } diff --git a/PKHeX.Core/Saves/SAV3RSBox.cs b/PKHeX.Core/Saves/SAV3RSBox.cs index 3b47daec8..58364fadc 100644 --- a/PKHeX.Core/Saves/SAV3RSBox.cs +++ b/PKHeX.Core/Saves/SAV3RSBox.cs @@ -133,7 +133,7 @@ public override void CopyChangesFrom(SaveFile sav) public override ushort MaxMoveID => Legal.MaxMoveID_3; public override ushort MaxSpeciesID => Legal.MaxSpeciesID_3; public override int MaxAbilityID => Legal.MaxAbilityID_3; - public override int MaxItemID => Legal.MaxItemID_3; + public override int MaxItemID => Legal.MaxItemID_3_RS; public override int MaxBallID => Legal.MaxBallID_3; public override GameVersion MaxGameID => Legal.MaxGameID_3; diff --git a/PKHeX.Core/Saves/Substructures/Gen3/IGen3Wonder.cs b/PKHeX.Core/Saves/Substructures/Gen3/IGen3Wonder.cs deleted file mode 100644 index 7cd2fe33c..000000000 --- a/PKHeX.Core/Saves/Substructures/Gen3/IGen3Wonder.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace PKHeX.Core; - -public interface IGen3Wonder -{ - int WonderOffset { get; } - WonderNews3 WonderNews { get; set; } - WonderCard3 WonderCard { get; set; } - WonderCard3Extra WonderCardExtra { get; set; } -} diff --git a/PKHeX.Core/Saves/Substructures/Gen3/Record3.cs b/PKHeX.Core/Saves/Substructures/Gen3/Record3.cs index 8f18b89de..4822fd968 100644 --- a/PKHeX.Core/Saves/Substructures/Gen3/Record3.cs +++ b/PKHeX.Core/Saves/Substructures/Gen3/Record3.cs @@ -1,29 +1,10 @@ using System; using System.Collections.Generic; -using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; -public sealed class Record3(SAV3 SAV) +public static class Record3 { - public uint GetRecord(int record) => ReadUInt32LittleEndian(SAV.Large[GetRecordOffset(record)..]) ^ SAV.SecurityKey; - public void SetRecord(int record, uint value) => WriteUInt32LittleEndian(SAV.Large[GetRecordOffset(record)..], value ^ SAV.SecurityKey); - - private int GetRecordOffset(int record) - { - var baseOffset = GetOffset(SAV.Version); - var offset = baseOffset + (4 * record); - return offset; - } - - public static int GetOffset(GameVersion version) => version switch - { - GameVersion.RS or GameVersion.R or GameVersion.S => 0x1540, - GameVersion.E => 0x159C, - GameVersion.FRLG or GameVersion.FR or GameVersion.LG => 0x1200, - _ => throw new ArgumentOutOfRangeException(nameof(version), version, null), - }; - private static Type GetEnumType(GameVersion version) => version switch { GameVersion.RS or GameVersion.R or GameVersion.S => typeof(RecID3RuSa), @@ -41,7 +22,7 @@ public static IList GetItems(SAV3 sav) var names = GetEnumNames(version); var values = GetEnumValues(version); - var result = new ComboItem[values.Length]; + var result = new ComboItem[values.Length - 1]; // exclude NUM_GAME_STATS for (int i = 0; i < result.Length; i++) { var replaced = names[i].Replace('_', ' '); @@ -110,7 +91,7 @@ public enum RecID3RuSa USED_DAYCARE = 47, RODE_CABLE_CAR = 48, ENTERED_HOT_SPRINGS = 49, - // NUM_GAME_STATS = 50 + NUM_GAME_STATS = 50, } /// @@ -175,7 +156,7 @@ public enum RecID3Emerald BERRY_CRUSH_WITH_FRIENDS = 51, // NUM_USED_GAME_STATS = 52, - // NUM_GAME_STATS = 64 + NUM_GAME_STATS = 64, } /// @@ -239,5 +220,5 @@ public enum RecID3FRLG UNION_WITH_FRIENDS = 50, BERRY_CRUSH_WITH_FRIENDS = 51, - // NUM_GAME_STATS = 64, + NUM_GAME_STATS = 64, } diff --git a/PKHeX.Core/Saves/Substructures/Gen3/Roamer3.cs b/PKHeX.Core/Saves/Substructures/Gen3/Roamer3.cs index 11c37efc9..cb5a410a7 100644 --- a/PKHeX.Core/Saves/Substructures/Gen3/Roamer3.cs +++ b/PKHeX.Core/Saves/Substructures/Gen3/Roamer3.cs @@ -3,24 +3,13 @@ namespace PKHeX.Core; -public sealed class Roamer3 : IContestStats +public sealed record Roamer3(Memory Raw, bool IsGlitched) : IContestStats { - public const int SIZE = 0x14; - public bool IsGlitched { get; } - private readonly Memory Raw; + public const int SIZE = 0x14; // +8 bytes of unused private Span Data => Raw.Span; - public Roamer3(SAV3 sav) + public Roamer3(ISaveBlock3Large large) : this(large.RoamerData, large is not SaveBlock3LargeE) { - var offset = sav switch - { - SAV3RS => 0x3144, - SAV3E => 0x31DC, - _ => 0x30D0, // FRLG - }; - var buffer = sav.LargeBuffer; - Raw = buffer.Slice(offset, SIZE); - IsGlitched = sav is not SAV3E; } public uint IV32 @@ -53,7 +42,7 @@ public byte CurrentLevel set => Data[12] = value; } - public int Status { get => Data[0x0D]; set => Data[0x0D] = (byte)value; } + public byte Status { get => Data[0x0D]; set => Data[0x0D] = (byte)value; } public byte ContestCool { get => Data[0x0E]; set => Data[0x0E] = value; } public byte ContestBeauty { get => Data[0x0F]; set => Data[0x0F] = value; } diff --git a/PKHeX.Core/Saves/Substructures/Mail/Mail3.cs b/PKHeX.Core/Saves/Substructures/Mail/Mail3.cs index aabaa60d6..87daa03b4 100644 --- a/PKHeX.Core/Saves/Substructures/Mail/Mail3.cs +++ b/PKHeX.Core/Saves/Substructures/Mail/Mail3.cs @@ -29,6 +29,7 @@ private void ResetData() public override ushort GetMessage(int index1, int index2) => ReadUInt16LittleEndian(Data[(((index1 * 3) + index2) * 2)..]); public override void SetMessage(int index1, int index2, ushort value) => WriteUInt16LittleEndian(Data[(((index1 * 3) + index2) * 2)..], value); public override void CopyTo(SaveFile sav) => sav.SetData(((SAV3)sav).Large[DataOffset..], Data); + public void CopyTo(Span dest) => Data.CopyTo(dest); public override string AuthorName { diff --git a/PKHeX.Core/Saves/Substructures/Misc/IRecordStatStorage.cs b/PKHeX.Core/Saves/Substructures/Misc/IRecordStatStorage.cs index 324296657..c1a0b533f 100644 --- a/PKHeX.Core/Saves/Substructures/Misc/IRecordStatStorage.cs +++ b/PKHeX.Core/Saves/Substructures/Misc/IRecordStatStorage.cs @@ -1,4 +1,6 @@ -namespace PKHeX.Core; +using System; + +namespace PKHeX.Core; /// /// Provides a minimal API for mutating stat records. @@ -9,3 +11,10 @@ public interface IRecordStatStorage void SetRecord(int recordID, int value); void AddRecord(int recordID, int count = 1); } + +public interface IRecordStatStorage where TType : Enum where TValue : struct +{ + TValue GetRecord(TType recordID); + void SetRecord(TType recordID, TValue value); + void AddRecord(TType recordID, TValue count = default); +} diff --git a/PKHeX.WinForms/Controls/SAV Editor/BoxEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/BoxEditor.cs index 860065eaf..cea92ad59 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/BoxEditor.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/BoxEditor.cs @@ -358,7 +358,7 @@ public bool InitializeFromSAV(SaveFile sav) public void ApplySearchFilter(Func? searchFilter, bool reload = true) { _searchFilter = searchFilter; - _lastSearchResult = null; + _lastSearchResult = null; if (!reload) return; ResetSlots(); diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs index 1dbb4d96c..2ac9228d5 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs @@ -1285,7 +1285,7 @@ private void ToggleViewSubEditors(SaveFile sav) GB_Daycare.Visible = sav is IDaycareStorage or IDaycareMulti; B_ConvertKorean.Visible = sav is SAV4; B_OpenPokeblocks.Visible = sav is SAV6AO; - B_OpenSecretBase.Visible = sav is SAV6AO or IGen3Hoenn; + B_OpenSecretBase.Visible = sav is SAV6AO or SAV3 { LargeBlock: ISaveBlock3LargeHoenn }; B_OpenPokepuffs.Visible = sav is ISaveBlock6Main; B_JPEG.Visible = B_OpenLinkInfo.Visible = B_OpenSuperTraining.Visible = B_OUTPasserby.Visible = sav is ISaveBlock6Main; B_OpenBoxLayout.Visible = sav is IBoxDetailName; @@ -1313,7 +1313,7 @@ private void ToggleViewSubEditors(SaveFile sav) B_OpenBattlePass.Visible = B_OpenGear.Visible = sav is SAV4BR; B_OpenSealStickers.Visible = B_Poffins.Visible = sav is SAV8BS; B_OpenApricorn.Visible = sav is SAV4HGSS; - B_OpenRTCEditor.Visible = (sav.Generation == 2 && sav is not SAV2Stadium) || sav is IGen3Hoenn; + B_OpenRTCEditor.Visible = (sav.Generation == 2 && sav is not SAV2Stadium) || sav is SAV3 { SmallBlock: ISaveBlock3SmallHoenn }; B_MailBox.Visible = sav is SAV2 or SAV2Stadium or SAV3 or SAV4 or SAV5; B_Raids.Visible = sav is SAV8SWSH or SAV9SV; diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen3/PokeBlock3CaseEditor.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen3/PokeBlock3CaseEditor.cs index e8e838070..ceb9c6bdf 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen3/PokeBlock3CaseEditor.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen3/PokeBlock3CaseEditor.cs @@ -13,7 +13,7 @@ public partial class Pokeblock3CaseEditor : UserControl private readonly string[] ItemNames = Util.GetStringList("pokeblock3", Main.CurrentLanguage); - public void Initialize(IGen3Hoenn sav) + public void Initialize(ISaveBlock3LargeHoenn sav) { Case = sav.PokeBlocks; @@ -42,7 +42,7 @@ private string GetPokeblockName(PokeBlock3Color color) private string GetPokeblockText(int index) => $"{index + 1:00} - {GetPokeblockName(Case.Blocks[index].Color)}"; - public void Save(IGen3Hoenn sav) + public void Save(ISaveBlock3LargeHoenn sav) { SaveIndex(CurrentIndex); sav.PokeBlocks = Case; diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Misc3.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Misc3.cs index 88115bdf4..ede8e11f1 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Misc3.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Misc3.cs @@ -21,7 +21,7 @@ public SAV_Misc3(SAV3 sav) LoadRecords(); - if (SAV is IGen3Hoenn h) + if (SAV.LargeBlock is ISaveBlock3LargeHoenn h) { pokeblock3CaseEditor1.Initialize(h); ReadDecorations(h); @@ -37,7 +37,7 @@ public SAV_Misc3(SAV3 sav) TC_Misc.Controls.Remove(Tab_Paintings); } - if (SAV is IGen3Joyful j) + if (SAV.SmallBlock is ISaveBlock3SmallExpansion j) ReadJoyful(j); else TC_Misc.Controls.Remove(TAB_Joyful); @@ -78,13 +78,13 @@ public SAV_Misc3(SAV3 sav) private void B_Save_Click(object sender, EventArgs e) { - if (SAV is IGen3Hoenn h) + if (SAV.LargeBlock is ISaveBlock3LargeHoenn h) { pokeblock3CaseEditor1.Save(h); SaveDecorations(h); SavePaintings(); } - if (TC_Misc.Controls.Contains(TAB_Joyful) && SAV is IGen3Joyful j) + if (TC_Misc.Controls.Contains(TAB_Joyful) && SAV.SmallBlock is ISaveBlock3SmallExpansion j) SaveJoyful(j); if (TC_Misc.Controls.Contains(TAB_Ferry)) SaveFerry(); @@ -103,7 +103,7 @@ private void B_Save_Click(object sender, EventArgs e) } if (SAV is SAV3E se) - se.BP = (ushort)NUD_BP.Value; + se.SmallBlock.BP = (ushort)NUD_BP.Value; SAV.Coin = (ushort)NUD_Coins.Value; Origin.CopyChangesFrom(SAV); @@ -113,7 +113,7 @@ private void B_Save_Click(object sender, EventArgs e) private void B_Cancel_Click(object sender, EventArgs e) => Close(); #region Joyful - private void ReadJoyful(IGen3Joyful j) + private void ReadJoyful(ISaveBlock3SmallExpansion j) { TB_J1.Text = Math.Min((ushort)9999, j.JoyfulJumpInRow).ToString(); TB_J2.Text = Math.Min(99990, j.JoyfulJumpScore).ToString(); @@ -125,7 +125,7 @@ private void ReadJoyful(IGen3Joyful j) TB_BerryPowder.Text = Math.Min(99999u, j.BerryPowder).ToString(); } - private void SaveJoyful(IGen3Joyful j) + private void SaveJoyful(ISaveBlock3SmallExpansion j) { j.JoyfulJumpInRow = (ushort)Util.ToUInt32(TB_J1.Text); j.JoyfulJumpScore = (ushort)Util.ToUInt32(TB_J2.Text); @@ -350,7 +350,7 @@ private void LoadStatsFromSave() if (recordIndex < 0) return; - var bf = ((SAV3E)SAV).BattleFrontier; + var bf = ((SAV3E)SAV).SmallBlock.BattleFrontier; var mode = (BattleFrontierBattleMode3)modeIndex; var record = (BattleFrontierRecordType3)recordIndex; @@ -402,7 +402,7 @@ private void SaveStatToSave(int statIndex) if (recordIndex < 0) return; - var bf = ((SAV3E)SAV).BattleFrontier; + var bf = ((SAV3E)SAV).SmallBlock.BattleFrontier; var mode = (BattleFrontierBattleMode3)modeIndex; var record = (BattleFrontierRecordType3)recordIndex; @@ -443,7 +443,7 @@ private void SaveContinueFlag() if (recordIndex < 0) return; - var bf = ((SAV3E)SAV).BattleFrontier; + var bf = ((SAV3E)SAV).SmallBlock.BattleFrontier; var mode = (BattleFrontierBattleMode3)modeIndex; var record = (BattleFrontierRecordType3)recordIndex; @@ -511,7 +511,6 @@ private void BTN_Symbol_Click(object sender, EventArgs e) private void LoadRecords() { - var records = new Record3(SAV); var items = Record3.GetItems(SAV); CB_Record.InitializeBinding(); CB_Record.DataSource = items; @@ -537,16 +536,17 @@ private void LoadRecords() var index = WinFormsUtil.GetIndex(CB_Record); var value = (uint)NUD_RecordValue.Value; - records.SetRecord(index, value); + SAV.SetRecord(index, value); if (index == 1) LoadFame(value); }; if (SAV is SAV3E em) { - NUD_BP.Value = Math.Min(NUD_BP.Maximum, em.BP); - NUD_BPEarned.Value = em.BPEarned; - NUD_BPEarned.ValueChanged += (_, _) => em.BPEarned = (uint)NUD_BPEarned.Value; + var small = em.SmallBlock; + NUD_BP.Value = Math.Min(NUD_BP.Maximum, small.BP); + NUD_BPEarned.Value = small.BPEarned; + NUD_BPEarned.ValueChanged += (_, _) => small.BPEarned = (ushort)NUD_BPEarned.Value; } else { @@ -554,12 +554,13 @@ private void LoadRecords() NUD_BPEarned.Visible = L_BPEarned.Visible = false; } - NUD_FameH.ValueChanged += (_, _) => ChangeFame(records); - NUD_FameM.ValueChanged += (_, _) => ChangeFame(records); - NUD_FameS.ValueChanged += (_, _) => ChangeFame(records); + NUD_FameH.ValueChanged += (_, _) => ChangeFame(); + NUD_FameM.ValueChanged += (_, _) => ChangeFame(); + NUD_FameS.ValueChanged += (_, _) => ChangeFame(); + return; - void ChangeFame(Record3 r3) => r3.SetRecord(1, (uint)(NUD_RecordValue.Value = GetFameTime())); - void LoadRecordID(int index) => NUD_RecordValue.Value = records.GetRecord(index); + void ChangeFame() => SAV.SetRecord(1, (uint)(NUD_RecordValue.Value = GetFameTime())); + void LoadRecordID(int index) => NUD_RecordValue.Value = SAV.GetRecord(index); void LoadFame(uint val) => SetFameTime(val); } @@ -580,7 +581,7 @@ public void SetFameTime(uint time) } #region Decorations - private void ReadDecorations(IGen3Hoenn h) + private void ReadDecorations(ISaveBlock3LargeHoenn h) { DataGridViewComboBoxColumn[] columns = [ @@ -632,7 +633,7 @@ private static void ReadDecorationCategory(ReadOnlySpan data, DataG dgv.Rows[i].Cells[0].Value = (int)data[i]; } - private void SaveDecorations(IGen3Hoenn h) + private void SaveDecorations(ISaveBlock3LargeHoenn h) { SaveDecorationCategory(h.Decorations.Desk, DGV_Desk); SaveDecorationCategory(h.Decorations.Chair, DGV_Chair); @@ -681,8 +682,9 @@ private void LoadPainting(int index) { if ((uint)index >= 5) return; - var gallery = (IGen3Hoenn)SAV; - var painting = gallery.GetPainting(index); + if (SAV.LargeBlock is not ISaveBlock3LargeHoenn gallery) + return; + var painting = gallery.GetPainting(index, SAV.Japanese); GB_Painting.Visible = CHK_EnablePaint.Checked = SAV.GetEventFlag(Paintings3.GetFlagIndexContestStat(index)); @@ -703,8 +705,9 @@ private void SavePainting(int index) { if ((uint)index >= 5) return; - var gallery = (IGen3Hoenn)SAV; - var painting = gallery.GetPainting(index); + if (SAV.LargeBlock is not ISaveBlock3LargeHoenn gallery) + return; + var painting = gallery.GetPainting(index, SAV.Japanese); var enabled = CHK_EnablePaint.Checked; SAV.SetEventFlag(Paintings3.GetFlagIndexContestStat(index), enabled); diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_RTC3.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_RTC3.cs index 94cceabc0..649af5b6e 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_RTC3.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_RTC3.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Windows.Forms; using PKHeX.Core; @@ -7,16 +7,18 @@ namespace PKHeX.WinForms; public partial class SAV_RTC3 : Form { private readonly SaveFile Origin; - private readonly IGen3Hoenn SAV; + private readonly SAV3 SAV; + private readonly ISaveBlock3SmallHoenn Small; public SAV_RTC3(SaveFile sav) { InitializeComponent(); WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); - SAV = (IGen3Hoenn)(Origin = sav).Clone(); + SAV = (SAV3)(Origin = sav).Clone(); + Small = (ISaveBlock3SmallHoenn)SAV.SmallBlock; - ClockInitial = SAV.ClockInitial; - ClockElapsed = SAV.ClockElapsed; + ClockInitial = Small.ClockInitial; + ClockElapsed = Small.ClockElapsed; LoadData(); } @@ -53,10 +55,10 @@ private void B_Save_Click(object sender, EventArgs e) { SaveData(); - SAV.ClockInitial = ClockInitial; - SAV.ClockElapsed = ClockElapsed; + Small.ClockInitial = ClockInitial; + Small.ClockElapsed = ClockElapsed; - Origin.CopyChangesFrom((SaveFile)SAV); + Origin.CopyChangesFrom(SAV); Close(); } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Roamer3.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Roamer3.cs index 7934c4ed6..e19dce3cb 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Roamer3.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_Roamer3.cs @@ -13,7 +13,7 @@ public SAV_Roamer3(SAV3 sav) { InitializeComponent(); WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); - Reader = new Roamer3(sav); + Reader = new Roamer3(sav.LargeBlock.RoamerData, sav is not SAV3E); SAV = sav; CB_Species.InitializeBinding(); diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_SecretBase3.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_SecretBase3.cs index 00e8a2876..133f6bf45 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_SecretBase3.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen3/SAV_SecretBase3.cs @@ -17,6 +17,8 @@ public SAV_SecretBase3(SAV3 sav) InitializeComponent(); //WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); SAV = (SAV3)(Origin = sav).Clone(); + var large = (ISaveBlock3LargeHoenn)SAV.LargeBlock; + Manager = large.SecretBases; TB_Name.MaxLength = 7; TB_SID.MaxLength = 5; @@ -73,8 +75,6 @@ public SAV_SecretBase3(SAV3 sav) if (!TB_PID.Text.All(c => "0123456789abcdefABCDEF\n".Contains(c))) TB_PID.Text = uint.MaxValue.ToString("X8"); }; - - Manager = ((IGen3Hoenn)SAV).SecretBases; LB_Bases.InitializeBinding(); LB_Bases.DataSource = Manager.Bases; LB_Bases.DisplayMember = "OriginalTrainerName"; diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_Inventory.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_Inventory.cs index 4da105b51..baffc08b1 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_Inventory.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_Inventory.cs @@ -12,7 +12,6 @@ namespace PKHeX.WinForms; public sealed partial class SAV_Inventory : Form { private readonly SaveFile Origin; - private readonly SaveFile SAV; private static readonly ImageList IL_Pouch = InventoryTypeImageUtil.GetImageList(); @@ -21,8 +20,8 @@ public SAV_Inventory(SaveFile sav) InitializeComponent(); tabControl1.ImageList = IL_Pouch; WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); - SAV = (Origin = sav).Clone(); - itemlist = [.. GameInfo.Strings.GetItemStrings(SAV.Context, SAV.Version)]; // copy + Origin = sav; + itemlist = [.. GameInfo.Strings.GetItemStrings(sav.Context, sav.Version)]; // copy for (int i = 0; i < itemlist.Length; i++) { @@ -30,8 +29,8 @@ public SAV_Inventory(SaveFile sav) itemlist[i] = $"(Item #{i:000})"; } - Bag = SAV.Inventory; - ItemColumnReadOnly = SAV is SAV9ZA or SAV9SV; + Bag = sav.Inventory; + ItemColumnReadOnly = sav is SAV9ZA or SAV9SV; var item0 = Bag.Pouches[0].Items[0]; HasFreeSpace = item0 is IItemFreeSpace; HasFreeSpaceIndex = item0 is IItemFreeSpaceIndex; @@ -90,8 +89,7 @@ public SAV_Inventory(SaveFile sav) private void B_Save_Click(object sender, EventArgs e) { SetBags(); - Bag.CopyTo(SAV); - Origin.CopyChangesFrom(SAV); + Bag.CopyTo(Origin); Close(); } @@ -336,7 +334,7 @@ private void ChangeViewedPouch(int index) var pouch = Bag.Pouches[index]; NUD_Count.Maximum = pouch.MaxCount; - bool disable = pouch.Type is InventoryType.PCItems or InventoryType.FreeSpace && SAV is not SAV8LA; + bool disable = pouch.Type is InventoryType.PCItems or InventoryType.FreeSpace && Origin is not SAV8LA; NUD_Count.Visible = L_Count.Visible = B_GiveAll.Visible = !disable; if (disable && !Main.HaX) { diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs index 20b834d09..4cc4b91c8 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs @@ -95,7 +95,7 @@ public SAV_MailBox(SaveFile sav) case SAV3 sav3: m = new Mail3[6 + 10]; for (int i = 0; i < m.Length; i++) - m[i] = sav3.GetMail(i); + m[i] = sav3.LargeBlock.GetMail(i); MailItemID = [121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132]; PartyBoxCount = 6; diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_SimplePokedex.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_SimplePokedex.cs index 49278d9c8..0e272466b 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_SimplePokedex.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_SimplePokedex.cs @@ -73,6 +73,8 @@ private void SanityCheck() SAV.SetCaught(species, false); } } + if (SAV is SAV3 s3) + s3.MirrorSeenFlags(); } private void B_Cancel_Click(object sender, EventArgs e) => Close(); diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs index 3258e3778..bc15f2daa 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs @@ -102,6 +102,7 @@ public SAV_SimpleTrainer(SaveFile sav) if (SAV is SAV3 sav3) { + var small = sav3.SmallBlock; GB_Map.Visible = false; badgeval = sav3.Badges; @@ -112,14 +113,14 @@ public SAV_SimpleTrainer(SaveFile sav) GB_Options.Visible = true; CB_BattleStyle.Items.AddRange("Switch", "Set"); CB_SoundType.Items.AddRange("Mono", "Stereo"); - CB_TextSpeed.Items.AddRange("0 (Slow)", "1 (Mid)", "2 (Fast)", "3 (Instant)"); + CB_TextSpeed.Items.AddRange("0 (Slow)", "1 (Mid)", "2 (Fast)", "3 (Invalid)"); - CB_TextSpeed.SelectedIndex = sav3.TextSpeed; - CB_BattleStyle.SelectedIndex = sav3.OptionBattleStyle ? 1 : 0; - CB_SoundType.SelectedIndex = sav3.OptionSoundStereo ? 0 : 1; - CHK_BattleEffects.Checked = sav3.OptionBattleScene; + CB_TextSpeed.SelectedIndex = small.TextSpeed; + CB_BattleStyle.SelectedIndex = small.OptionBattleStyle ? 1 : 0; + CB_SoundType.SelectedIndex = small.OptionSoundStereo ? 0 : 1; + CHK_BattleEffects.Checked = small.OptionBattleScene; - TB_OTName.Click += (_, _) => ClickOT(sav3.OriginalTrainerTrash, TB_OTName); + TB_OTName.Click += (_, _) => ClickOT(small.OriginalTrainerTrash, TB_OTName); } if (SAV is SAV3Colosseum or SAV3XD) { @@ -273,11 +274,12 @@ private void B_Save_Click(object sender, EventArgs e) if (SAV is SAV3 sav3) { + var small = sav3.SmallBlock; sav3.Badges = badgeval & 0xFF; - sav3.OptionBattleStyle = CB_BattleStyle.SelectedIndex == 1; - sav3.OptionSoundStereo = CB_SoundType.SelectedIndex == 0; - sav3.TextSpeed = CB_TextSpeed.SelectedIndex; - sav3.OptionBattleScene = CHK_BattleEffects.Checked; + small.OptionBattleStyle = CB_BattleStyle.SelectedIndex == 1; + small.OptionSoundStereo = CB_SoundType.SelectedIndex == 0; + small.TextSpeed = CB_TextSpeed.SelectedIndex; + small.OptionBattleScene = CHK_BattleEffects.Checked; } if (SAV is SAV4 sav4)