From 275f5fb5df3de901bd0b8a62b951d26dc3b70d9f Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 7 May 2025 23:06:40 -0500 Subject: [PATCH] Misc tweaks Seal some classes Use derived pkm class for template->pk* moves (pk3/pa8) Revise inventory9 to better handle empty slots Cache legality for Summary report grid (does this make it faster?? seems to open instantly); add shift-quit to skip prompt Fix handling for level range on encountercriteria passing level range for gen3/4 encounter slot method1/etc lead checks --- PKHeX.Core/Editing/PKM/EntitySummary.cs | 6 +- .../Templates/Gen3/EncounterSlot3.cs | 2 +- .../Templates/Gen3/EncounterSlot3Swarm.cs | 2 +- .../Templates/Gen8a/EncounterSlot8a.cs | 11 ++- .../Legality/RNG/ClassicEra/Gen3/MethodH.cs | 2 +- .../Legality/RNG/ClassicEra/Gen4/MethodJ.cs | 2 +- .../Legality/RNG/ClassicEra/Gen4/MethodK.cs | 2 +- .../Saves/Substructures/Gen5/BattleTest5.cs | 2 +- .../Substructures/Gen5/PokestarMovie5.cs | 2 +- .../Saves/Substructures/Gen5/Record5.cs | 2 +- .../Substructures/Gen5/WorldTournament5.cs | 2 +- .../Saves/Substructures/Gen9/MyItem9.cs | 31 +++++++- .../Inventory/Item/InventoryItem9.cs | 39 +++++----- .../Inventory/Pouch/InventoryPouch9.cs | 78 ++++++++++++------- PKHeX.WinForms/Subforms/ReportGrid.cs | 2 + 15 files changed, 117 insertions(+), 68 deletions(-) diff --git a/PKHeX.Core/Editing/PKM/EntitySummary.cs b/PKHeX.Core/Editing/PKM/EntitySummary.cs index 5c3b68a60..547b26538 100644 --- a/PKHeX.Core/Editing/PKM/EntitySummary.cs +++ b/PKHeX.Core/Editing/PKM/EntitySummary.cs @@ -41,7 +41,10 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh public string OT => Entity.OriginalTrainerName; public string Version => Get(Strings.gamelist, (int)Entity.Version); public string OTLang => ((LanguageID)Entity.Language).ToString(); - public string Legal { get { var la = new LegalityAnalysis(Entity); return la.Parsed ? la.Valid.ToString() : "-"; } } + public string Legal => Legality.Parsed ? Legality.Valid.ToString() : "-"; + public string EncounterType => Legality.EncounterMatch.LongName; + + private LegalityAnalysis Legality { get; } #region Extraneous public string EC => Entity.EncryptionConstant.ToString("X8"); @@ -113,6 +116,7 @@ protected EntitySummary(PKM pk, GameStrings strings) Entity = pk; Strings = strings; Stats = Entity.GetStats(Entity.PersonalInfo); + Legality = new LegalityAnalysis(Entity); } /// diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs index 9c294d946..81f1dbbfb 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3.cs @@ -92,7 +92,7 @@ private void SetPINGA(PK3 pk, EncounterCriteria criteria, PersonalInfo3 pi) } } - protected virtual void SetEncounterMoves(PKM pk) => EncounterUtil.SetEncounterMoves(pk, Version, LevelMin); + protected virtual void SetEncounterMoves(PK3 pk) => EncounterUtil.SetEncounterMoves(pk, Version, LevelMin); #endregion #region Matching diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3Swarm.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3Swarm.cs index 9314fb48c..2ceb6127b 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3Swarm.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/EncounterSlot3Swarm.cs @@ -9,5 +9,5 @@ namespace PKHeX.Core; internal sealed record EncounterSlot3Swarm(EncounterArea3 Parent, ushort Species, byte LevelMin, byte LevelMax, byte SlotNumber, Moveset Moves) : EncounterSlot3(Parent, Species, 0, LevelMin, LevelMax, SlotNumber, 0, 0, 0, 0), IMoveset { - protected override void SetEncounterMoves(PKM pk) => pk.SetMoves(Moves); + protected override void SetEncounterMoves(PK3 pk) => pk.SetMoves(Moves); } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen8a/EncounterSlot8a.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen8a/EncounterSlot8a.cs index 6585972df..3523d0baa 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen8a/EncounterSlot8a.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen8a/EncounterSlot8a.cs @@ -117,16 +117,15 @@ private OverworldParam8a GetParams(PersonalInfo8LA pi) => new() _ => 0, }); - private void SetEncounterMoves(PKM pk, int level) + private void SetEncounterMoves(PA8 pk, int level) { - var pa8 = (PA8)pk; Span moves = stackalloc ushort[4]; var (learn, mastery) = GetLevelUpInfo(); - LoadInitialMoveset(pa8, moves, learn, level); + LoadInitialMoveset(pk, moves, learn, level); pk.SetMoves(moves); - pa8.SetEncounterMasteryFlags(moves, mastery, level); - if (pa8.AlphaMove != 0) - pa8.SetMasteryFlagMove(pa8.AlphaMove); + pk.SetEncounterMasteryFlags(moves, mastery, level); + if (pk.AlphaMove != 0) + pk.SetMasteryFlagMove(pk.AlphaMove); } public void LoadInitialMoveset(PA8 pa8, Span moves, Learnset learn, int level) diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/MethodH.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/MethodH.cs index 4c97f5a82..6fb3d7f80 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/MethodH.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/MethodH.cs @@ -604,7 +604,7 @@ private static bool IsSlotValid(T enc, uint u16SlotRand) private static bool IsOriginalLevelValid(byte min, byte max, byte format, uint level) { - if (format == Format) + if (format == Format && min > 1) return level == min; // Met Level matches return LevelRangeExtensions.IsLevelWithinRange((int)level, min, max); } diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodJ.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodJ.cs index d1a555be7..755b4d54a 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodJ.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodJ.cs @@ -472,7 +472,7 @@ private static bool IsSlotValid(T enc, uint u16SlotRand) private static bool IsOriginalLevelValid(byte min, byte max, byte format, uint level) { - if (format == Format) + if (format == Format && min > 1) return level == min; // Met Level matches return LevelRangeExtensions.IsLevelWithinRange((int)level, min, max); } diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodK.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodK.cs index d23f1a753..4e71a929f 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodK.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/MethodK.cs @@ -499,7 +499,7 @@ private static bool IsSlotValid(T enc, uint u16SlotRand) private static bool IsOriginalLevelValid(byte min, byte max, byte format, uint level) { - if (format == Format) + if (format == Format && min > 1) return level == min; // Met Level matches return LevelRangeExtensions.IsLevelWithinRange((int)level, min, max); } diff --git a/PKHeX.Core/Saves/Substructures/Gen5/BattleTest5.cs b/PKHeX.Core/Saves/Substructures/Gen5/BattleTest5.cs index 41ce0ae22..9af17837b 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/BattleTest5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/BattleTest5.cs @@ -3,7 +3,7 @@ namespace PKHeX.Core; -public class BattleTest5(Memory Raw) +public sealed class BattleTest5(Memory Raw) { public const int SIZE = 0x5C8; public const string Extension = "bt5"; diff --git a/PKHeX.Core/Saves/Substructures/Gen5/PokestarMovie5.cs b/PKHeX.Core/Saves/Substructures/Gen5/PokestarMovie5.cs index 2af66496d..9ec214d7f 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/PokestarMovie5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/PokestarMovie5.cs @@ -3,7 +3,7 @@ namespace PKHeX.Core; -public class PokestarMovie5(Memory Raw) +public sealed class PokestarMovie5(Memory Raw) { public const int SIZE = 0x4B4; public const string Extension = "psm5"; diff --git a/PKHeX.Core/Saves/Substructures/Gen5/Record5.cs b/PKHeX.Core/Saves/Substructures/Gen5/Record5.cs index 432e1cac2..d07a7762f 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/Record5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/Record5.cs @@ -3,7 +3,7 @@ namespace PKHeX.Core; -public class Record5(SAV5 SAV, Memory raw) : SaveBlock(SAV, raw) +public sealed class Record5(SAV5 SAV, Memory raw) : SaveBlock(SAV, raw) { public const int Max32 = 999_999_999; public const ushort Max16 = 65535; diff --git a/PKHeX.Core/Saves/Substructures/Gen5/WorldTournament5.cs b/PKHeX.Core/Saves/Substructures/Gen5/WorldTournament5.cs index 9e6c6345d..b398c37d2 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/WorldTournament5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/WorldTournament5.cs @@ -3,7 +3,7 @@ namespace PKHeX.Core; -public class WorldTournament5(Memory Raw) +public sealed class WorldTournament5(Memory Raw) { public const int SIZE = 0x1214; public const string Extension = "pwt"; diff --git a/PKHeX.Core/Saves/Substructures/Gen9/MyItem9.cs b/PKHeX.Core/Saves/Substructures/Gen9/MyItem9.cs index 239ac9c10..fe52d5cf9 100644 --- a/PKHeX.Core/Saves/Substructures/Gen9/MyItem9.cs +++ b/PKHeX.Core/Saves/Substructures/Gen9/MyItem9.cs @@ -13,7 +13,17 @@ public sealed class MyItem9(SAV9SV sav, SCBlock block) : MyItem(sav, block.Raw) private Span GetItemSpan(ushort itemIndex) => InventoryPouch9.GetItemSpan(Data, itemIndex); - public void DeleteItem(ushort itemIndex) => InventoryItem9.Clear(GetItemSpan(itemIndex)); + public uint DefaultInitPouch => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Data); // Item 0 + + /// + /// Deletes the item at the requested . + /// + /// + /// Copies item 0 to the requested item index, effectively deleting it. + /// Item 0 should always be un-tarnished, so this is a safe operation. + /// for remarks on Pouch type quirks. This aims to retain consistency within the block. + /// + public void DeleteItem(ushort itemIndex) => GetItemSpan(0).CopyTo(GetItemSpan(itemIndex)); public InventoryItem9 GetItem(ushort itemIndex) => InventoryItem9.Read(itemIndex, GetItemSpan(itemIndex)); public uint GetItemQuantity(ushort itemIndex) => InventoryItem9.GetItemCount(GetItemSpan(itemIndex)); @@ -21,7 +31,7 @@ public sealed class MyItem9(SAV9SV sav, SCBlock block) : MyItem(sav, block.Raw) public void SetItemQuantity(ushort itemIndex, int quantity) { var pouch = GetPouchIndex(GetType(itemIndex)); - if (pouch == InventoryItem9.PouchNone) + if (pouch == InventoryItem9.PouchInvalid) { DeleteItem(itemIndex); // don't allow setting items that don't exist return; @@ -30,6 +40,7 @@ public void SetItemQuantity(ushort itemIndex, int quantity) var item = InventoryItem9.Read(itemIndex, span); item.Count = quantity; item.Pouch = GetPouchIndex(GetType(itemIndex)); + item.IsObtained = true; item.Write(span); } @@ -79,6 +90,20 @@ private void CleanIllegalSlots() } } + public void ResetToDefault() + { + var block = Data; + var defaultPouch = DefaultInitPouch; + ResetToDefault(block, defaultPouch); + } + + public static void ResetToDefault(Span block, uint defaultPouch) + { + block.Clear(); + for (int i = 0; i < block.Length; i += InventoryItem9.SIZE) + System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(block[i..], defaultPouch); + } + private static InventoryPouch9 MakePouch(InventoryType type) { var info = ItemStorage9SV.Instance; @@ -98,6 +123,6 @@ private static InventoryPouch9 MakePouch(InventoryType type) InventoryType.Treasure => InventoryItem9.PouchTreasure, InventoryType.Ingredients => InventoryItem9.PouchPicnic, InventoryType.Candy => InventoryItem9.PouchMaterial, - _ => InventoryItem9.PouchNone, + _ => InventoryItem9.PouchInvalid, }; } diff --git a/PKHeX.Core/Saves/Substructures/Inventory/Item/InventoryItem9.cs b/PKHeX.Core/Saves/Substructures/Inventory/Item/InventoryItem9.cs index 49b595798..a41c40348 100644 --- a/PKHeX.Core/Saves/Substructures/Inventory/Item/InventoryItem9.cs +++ b/PKHeX.Core/Saves/Substructures/Inventory/Item/InventoryItem9.cs @@ -7,7 +7,6 @@ public sealed record InventoryItem9 : InventoryItem, IItemFavorite, IItemNewFlag { public const int SIZE = 0x10; - public const uint PouchNone = 0xFFFFFFFF; public const uint PouchMedicine = 0; public const uint PouchBall = 1; public const uint PouchBattle = 2; @@ -20,31 +19,38 @@ public sealed record InventoryItem9 : InventoryItem, IItemFavorite, IItemNewFlag public const uint PouchMaterial = 9; public const uint PouchRecipe = 10; + /// + /// Invalid pouch ID. + /// + /// + /// Only used internally to this program, not stored in the data. + /// The game did have behavior that initialized to this value on early patches, but was later changed to initialize to 0. + /// Therefore, we cannot use this value to set. Technically it can be used to determine if the game was started on an early patch. + /// + public const uint PouchInvalid = 0xFFFFFFFF; + public uint Pouch { get; set; } public uint Flags { get; set; } public uint Padding { get; set; } public bool IsNew { get => (Flags & 0x1) != 0; set => Flags = (Flags & ~0x1u) | (value ? 0x1u : 0x0u); } // red dot public bool IsFavorite { get => (Flags & 0x2) != 0; set => Flags = (Flags & ~0x2u) | (value ? 0x2u : 0x0u); } - public bool IsUpdated { get => (Flags & 0x4) != 0; set => Flags = (Flags & ~0x4u) | (value ? 0x4u : 0x0u); } // always true if pouch is set + public bool IsObtained { get => (Flags & 0x4) != 0; set => Flags = (Flags & ~0x4u) | (value ? 0x4u : 0x0u); } // Has Been Obtained At Least Once - public override string ToString() => $"{Index:000} x{Count}{(IsNew ? "*" : "")}{(IsFavorite ? "F" : "")} - {Flags:X8}"; + public override string ToString() + { + if (IsObtained) + return $"{Index:0000} Empty"; + return $"{Index:0000} x{Count}{(IsNew ? "*" : "")}{(IsFavorite ? "F" : "")} - {Flags:X8}"; + } public override void Clear() { - Index = Count = 0; + Pouch = 0; + Count = 0; Flags = Padding = 0; - IsFavorite = false; - IsUpdated = false; - IsNew = false; - Pouch = PouchNone; } - /// - /// Indicates if the item has been acquired by the player. - /// - public bool IsValidPouch => Pouch != PouchNone; - public static InventoryItem9 Read(ushort index, ReadOnlySpan data) => new() { Index = index, @@ -56,8 +62,6 @@ public static InventoryItem9 Read(ushort index, ReadOnlySpan data) => new( public void Write(Span data) { - IsUpdated = Pouch != PouchNone; - // Index is not saved. WriteUInt32LittleEndian(data, Pouch); WriteUInt32LittleEndian(data[4..], (uint)Count); @@ -65,16 +69,13 @@ public void Write(Span data) WriteUInt32LittleEndian(data[12..], Padding); } - public static void Clear(Span data) => WriteUInt128LittleEndian(data, PouchNone); public static uint GetItemCount(Span data) => ReadUInt32LittleEndian(data[4..]); public override void SetNewDetails(int count) { base.SetNewDetails(count); - if (IsValidPouch) - return; IsNew = true; - IsUpdated = true; + IsObtained = true; IsFavorite = false; } diff --git a/PKHeX.Core/Saves/Substructures/Inventory/Pouch/InventoryPouch9.cs b/PKHeX.Core/Saves/Substructures/Inventory/Pouch/InventoryPouch9.cs index 090f9463e..163f7e08e 100644 --- a/PKHeX.Core/Saves/Substructures/Inventory/Pouch/InventoryPouch9.cs +++ b/PKHeX.Core/Saves/Substructures/Inventory/Pouch/InventoryPouch9.cs @@ -11,6 +11,8 @@ public sealed class InventoryPouch9(InventoryType type, IItemStorage info, int m public uint PouchIndex { get; set; } = pouch; public override InventoryItem9 GetEmpty(int itemID = 0, int count = 0) => new() { Index = itemID, Count = count, IsNew = true }; + public static int GetItemOffset(ushort index) => InventoryItem9.SIZE * index; + public static Span GetItemSpan(Span block, ushort index) => block[GetItemOffset(index)..]; public override void GetPouch(ReadOnlySpan data) { @@ -19,71 +21,87 @@ public override void GetPouch(ReadOnlySpan data) int ctr = 0; foreach (var index in LegalItems) - { - var item = GetItem(data, index); - if (!item.IsValidPouch) - continue; - items[ctr++] = item; - } - while (ctr != LegalItems.Length) - items[ctr++] = new InventoryItem9(); + items[ctr++] = GetItem(data, index); Items = items; } - public static InventoryItem9 GetItem(ReadOnlySpan data, ushort itemID) + public static InventoryItem9 GetItem(ReadOnlySpan block, ushort itemID) { var ofs = GetItemOffset(itemID); - return InventoryItem9.Read(itemID, data[ofs..]); + return InventoryItem9.Read(itemID, block[ofs..]); } - public override void SetPouch(Span data) + public override void SetPouch(Span block) { - HashSet processed = []; - // Write all the item slots still present in the pouch. Keep track of the item IDs processed. var items = (InventoryItem9[])Items; + var processed = new HashSet(items.Length); - var LegalItems = Info.GetItems(Type); + var legal = Info.GetItems(Type); foreach (var item in items) { var index = (ushort)item.Index; if (index == 0) continue; - if (!LegalItems.Contains(index)) + if (!legal.Contains(index)) { Debug.WriteLine($"Invalid Item ID returned within this pouch: {index}"); continue; } - if (SetNew && item.Index != 0) - { - var original = GetItem(data, (ushort)item.Index); - item.IsNew |= !original.IsValidPouch; - } - - item.Pouch = PouchIndex; - - var ofs = GetItemOffset(index); - item.Write(data[ofs..]); + var span = GetItemSpan(block, index); + var original = InventoryItem9.Read(index, span); + EnsureFlagsConsistent(item, original); // In the event of duplicates, we just overwrite what was previously written by a prior duplicate. // Don't care if we've already processed this item, just write it again. + item.Write(span); processed.Add(index); } // For all the items that were not present in the pouch, clear the data for them. - foreach (var index in LegalItems) + foreach (var index in legal) { if (processed.Contains(index)) continue; - ClearItem(data, index); + SetQuantityZero(block, index); } } - public static int GetItemOffset(ushort index) => InventoryItem9.SIZE * index; - public static Span GetItemSpan(Span data, ushort index) => data[GetItemOffset(index)..]; - public static void ClearItem(Span data, ushort index) => InventoryItem9.Clear(GetItemSpan(data, index)); + private void EnsureFlagsConsistent(InventoryItem9 item, InventoryItem9 original) + { + if (item.Count != 0) + { + // Ensure the flag is set; 0->X and Y->Z + item.IsObtained = true; + if (!original.IsObtained && SetNew) + item.IsNew = true; + } + else + { + if (!item.IsObtained) + { + item.IsNew = item.IsFavorite = false; + if (item.Pouch is not (0 or uint.MaxValue)) + item.Pouch = 0; + } + } + + if (item.IsObtained) + item.Pouch = PouchIndex; // ensure the pouch is set + } + + public static void SetQuantityZero(Span block, ushort index) + { + var span = GetItemSpan(block, index); + var exist = InventoryItem9.Read(index, span); + if (exist.Count == 0) + return; + exist.Count = 0; + exist.IsObtained = true; + exist.Write(span); + } public static int GetSuggestedCount(InventoryType t, int item, int requestVal) { diff --git a/PKHeX.WinForms/Subforms/ReportGrid.cs b/PKHeX.WinForms/Subforms/ReportGrid.cs index 068ab10cf..a837fb7b5 100644 --- a/PKHeX.WinForms/Subforms/ReportGrid.cs +++ b/PKHeX.WinForms/Subforms/ReportGrid.cs @@ -161,6 +161,8 @@ private void Data_Sorted(object sender, EventArgs e) private void PromptSaveCSV(object sender, FormClosingEventArgs e) { + if (ModifierKeys.HasFlag(Keys.Shift)) + return; if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgReportExportCSV) != DialogResult.Yes) return; using var savecsv = new SaveFileDialog();