From 7d05e12c7f1f5e42d503344cb7d2879215fe1ea8 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sat, 1 Jun 2024 17:44:32 -0500 Subject: [PATCH] Misc tweaks Fix pk6/7 pkmeditor export, now retaining status condition instead of wiping it Abstract b2w2 key system to a save block; better documentation on its odd mechanics Allow gen1-3 filename language/ver detect to work if the filename is ` `->`_` (discord attachments changing spaces to underscores); also revise canadian French emerald filename pattern others: negligible perf/using standard library functions --- .../Editing/Bulk/StringInstructionSet.cs | 2 +- PKHeX.Core/Game/GameStrings/GameDataSource.cs | 24 +- .../Legality/Moves/Breeding/MoveBreed.cs | 3 +- .../Legality/Verifiers/MedalVerifier.cs | 3 +- PKHeX.Core/PKM/Shared/GBPKML.cs | 6 +- .../PKM/Util/Conversion/EntityConverter.cs | 6 +- PKHeX.Core/Saves/Access/ISaveBlock5B2W2.cs | 3 +- .../Saves/Access/SaveBlockAccessor5B2W2.cs | 1 + .../Encryption/SwishCrypto/SCBlockMetadata.cs | 4 +- PKHeX.Core/Saves/SAV5B2W2.cs | 1 + PKHeX.Core/Saves/Storage/SlotPointerUtil.cs | 6 +- .../Saves/Substructures/Gen5/BoxLayout5.cs | 4 +- .../Saves/Substructures/Gen5/KeySystem5.cs | 216 ++++++++++++++++++ PKHeX.Core/Saves/Substructures/Gen5/Misc5.cs | 16 ++ .../Saves/Substructures/Gen6/BoxLayout6.cs | 2 +- .../Saves/Substructures/Gen7/FashionBlock7.cs | 2 + .../Gen8/BS/BattleTrainerStatus8b.cs | 2 +- PKHeX.Core/Saves/Util/SaveLanguage.cs | 24 +- PKHeX.WinForms/Controls/PKM Editor/EditPK6.cs | 10 +- PKHeX.WinForms/Controls/PKM Editor/EditPK7.cs | 10 +- .../Controls/PKM Editor/PKMEditor.cs | 2 +- PKHeX.WinForms/MainWindow/Main.cs | 12 +- .../Subforms/PKM Editors/MemoryAmie.cs | 16 +- PKHeX.WinForms/Subforms/ReportGrid.cs | 2 +- PKHeX.WinForms/Subforms/SAV_FolderList.cs | 8 +- .../Subforms/Save Editors/Gen5/SAV_Misc5.cs | 60 ++--- .../Save Editors/Gen7/SAV_FestivalPlaza.cs | 2 +- .../Subforms/Save Editors/Gen8/SAV_Misc8b.cs | 2 + .../Gen9/SAV_PokedexSVKitakami.cs | 3 +- PKHeX.WinForms/Util/WinFormsUtil.cs | 2 +- .../Simulator/ShowdownSetTests.cs | 3 +- 31 files changed, 345 insertions(+), 112 deletions(-) create mode 100644 PKHeX.Core/Saves/Substructures/Gen5/KeySystem5.cs diff --git a/PKHeX.Core/Editing/Bulk/StringInstructionSet.cs b/PKHeX.Core/Editing/Bulk/StringInstructionSet.cs index 416d4dd1a..1ad9f3f46 100644 --- a/PKHeX.Core/Editing/Bulk/StringInstructionSet.cs +++ b/PKHeX.Core/Editing/Bulk/StringInstructionSet.cs @@ -153,7 +153,7 @@ public sealed class StringInstructionSet while (start < lines.Length) { var line = lines[start++]; - if (line.Length != 0 && line[0] == SetSeparatorChar) + if (line.StartsWith(SetSeparatorChar)) return start; } return start; diff --git a/PKHeX.Core/Game/GameStrings/GameDataSource.cs b/PKHeX.Core/Game/GameStrings/GameDataSource.cs index daa02d0b3..134717bb4 100644 --- a/PKHeX.Core/Game/GameStrings/GameDataSource.cs +++ b/PKHeX.Core/Game/GameStrings/GameDataSource.cs @@ -24,7 +24,7 @@ public sealed class GameDataSource /// /// List of values to display. /// - private static readonly List LanguageList = + private static readonly ComboItem[] LanguageList = [ new ComboItem("JPN (日本語)", (int)LanguageID.Japanese), new ComboItem("ENG (English)", (int)LanguageID.English), @@ -37,6 +37,18 @@ public sealed class GameDataSource new ComboItem("CHT (繁體中文)", (int)LanguageID.ChineseT), ]; + /// + /// Gets a list of languages to display based on the generation. + /// + /// Generation to get the language list for. + /// List of languages to display. + public static IReadOnlyList LanguageDataSource(byte generation) => generation switch + { + 3 => LanguageList[..6], // No Korean+ + < 7 => LanguageList[..7], // No Chinese+ + _ => [.. LanguageList], + }; + public GameDataSource(GameStrings s) { Strings = s; @@ -130,14 +142,4 @@ public sealed class GameDataSource var items = Strings.GetItemStrings(context, game); return HaX ? Util.GetCBList(items) : Util.GetCBList(items, allowed); } - - public static IReadOnlyList LanguageDataSource(byte generation) - { - var languages = new List(LanguageList); - if (generation == 3) - languages.RemoveAll(static l => l.Value >= (int)LanguageID.Korean); - else if (generation < 7) - languages.RemoveAll(static l => l.Value > (int)LanguageID.Korean); - return languages; - } } diff --git a/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs b/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs index 9704e377c..f22a8e7b1 100644 --- a/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs +++ b/PKHeX.Core/Legality/Moves/Breeding/MoveBreed.cs @@ -181,8 +181,7 @@ public static class MoveBreed for (var i = 0; i < notBaseCount; i++) result[ctr++] = notBase[i]; // Then clear the remainder - for (int i = ctr; i < result.Length; i++) - result[i] = 0; + result[ctr..].Clear(); } /// diff --git a/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs b/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs index ef24786b1..3ac3d06ba 100644 --- a/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs @@ -1,4 +1,3 @@ -using System; using static PKHeX.Core.LegalityCheckStrings; namespace PKHeX.Core; @@ -21,7 +20,7 @@ public sealed class MedalVerifier : Verifier var pk = data.Entity; var train = (ISuperTrain)pk; var Info = data.Info; - uint value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(data.Entity.Data.AsSpan(0x2C)); + uint value = train.SuperTrainBitFlags; if ((value & 3) != 0) // 2 unused flags data.AddLine(GetInvalid(LSuperUnused)); int TrainCount = train.SuperTrainingMedalCount(); diff --git a/PKHeX.Core/PKM/Shared/GBPKML.cs b/PKHeX.Core/PKM/Shared/GBPKML.cs index 9f9843c76..51e87f0f5 100644 --- a/PKHeX.Core/PKM/Shared/GBPKML.cs +++ b/PKHeX.Core/PKM/Shared/GBPKML.cs @@ -56,11 +56,7 @@ public abstract class GBPKML : GBPKM return; // Decimal point<->period fix - foreach (ref var c in data) - { - if (c == 0xF2) - c = 0xE8; - } + data.Replace(0xF2, 0xE8); } public sealed override string Nickname diff --git a/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs b/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs index c2c81f683..b85816104 100644 --- a/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs +++ b/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs @@ -146,8 +146,8 @@ public static class EntityConverter private static PKM? IntermediaryConvert(PKM pk, Type destType, ref EntityConverterResult result) => pk switch { // Non-sequential - PK1 pk1 when destType.Name[^1] - '0' > 2 => pk1.ConvertToPK7(), - PK2 pk2 when destType.Name[^1] - '0' > 2 => pk2.ConvertToPK7(), + PK1 pk1 when destType.Name[^1] - '0' is not (1 or 2) => pk1.ConvertToPK7(), + PK2 pk2 when destType.Name[^1] - '0' is not (1 or 2) => pk2.ConvertToPK7(), PK2 pk2 when destType == typeof(SK2) => pk2.ConvertToSK2(), PK3 pk3 when destType == typeof(CK3) => pk3.ConvertToCK3(), PK3 pk3 when destType == typeof(XK3) => pk3.ConvertToXK3(), @@ -224,7 +224,7 @@ public static class EntityConverter }; } - if (destType.Name[^1] == '1' && pk.Species > Legal.MaxSpeciesID_1) + if (destType.Name.EndsWith('1') && pk.Species > Legal.MaxSpeciesID_1) return IncompatibleSpecies; return Success; diff --git a/PKHeX.Core/Saves/Access/ISaveBlock5B2W2.cs b/PKHeX.Core/Saves/Access/ISaveBlock5B2W2.cs index 22a9736e4..e4e9c4528 100644 --- a/PKHeX.Core/Saves/Access/ISaveBlock5B2W2.cs +++ b/PKHeX.Core/Saves/Access/ISaveBlock5B2W2.cs @@ -1,4 +1,4 @@ -namespace PKHeX.Core; +namespace PKHeX.Core; /// /// Interface for Accessing named blocks within a Generation 5 save file. @@ -6,5 +6,6 @@ public interface ISaveBlock5B2W2 { PWTBlock5 PWT { get; } + KeySystem5 Keys { get; } FestaBlock5 Festa { get; } } diff --git a/PKHeX.Core/Saves/Access/SaveBlockAccessor5B2W2.cs b/PKHeX.Core/Saves/Access/SaveBlockAccessor5B2W2.cs index 34a1ce172..088f6e823 100644 --- a/PKHeX.Core/Saves/Access/SaveBlockAccessor5B2W2.cs +++ b/PKHeX.Core/Saves/Access/SaveBlockAccessor5B2W2.cs @@ -112,6 +112,7 @@ public sealed class SaveBlockAccessor5B2W2(SAV5B2W2 sav) public EntreeForest EntreeForest { get; } = new(sav, Block(sav, 60)); public PWTBlock5 PWT { get; } = new(sav, Block(sav, 63)); public MedalList5 Medals { get; } = new(sav, Block(sav, 68)); + public KeySystem5 Keys { get; } = new(sav, Block(sav, 69)); public FestaBlock5 Festa { get; } = new(sav, Block(sav, 70)); EventWork5 ISaveBlock5BW.EventWork => EventWork; Encount5 ISaveBlock5BW.Encount => Encount; diff --git a/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlockMetadata.cs b/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlockMetadata.cs index 4732492e3..5df833e82 100644 --- a/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlockMetadata.cs +++ b/PKHeX.Core/Saves/Encryption/SwishCrypto/SCBlockMetadata.cs @@ -35,7 +35,7 @@ public sealed class SCBlockMetadata /// public IEnumerable GetSortedBlockKeyList() => Accessor.BlockInfo .Select((z, i) => new ComboItem(GetBlockHint(z, i), (int)z.Key)) - .OrderBy(z => !(z.Text.Length != 0 && z.Text[0] == '*')) + .OrderBy(z => !z.Text.StartsWith('*')) .ThenBy(z => GetSortKey(z)); /// @@ -63,7 +63,7 @@ public sealed class SCBlockMetadata private static string GetSortKey(in ComboItem item) { var text = item.Text; - if (text.Length != 0 && text[0] == '*') + if (text.StartsWith('*')) return text; // key:X8, " - ", "####", " ", type return text[(8 + 3 + 4 + 1)..]; diff --git a/PKHeX.Core/Saves/SAV5B2W2.cs b/PKHeX.Core/Saves/SAV5B2W2.cs index 321e0b4cf..e78f598bd 100644 --- a/PKHeX.Core/Saves/SAV5B2W2.cs +++ b/PKHeX.Core/Saves/SAV5B2W2.cs @@ -61,6 +61,7 @@ public sealed class SAV5B2W2 : SAV5, ISaveBlock5B2W2 public FestaBlock5 Festa => Blocks.Festa; public PWTBlock5 PWT => Blocks.PWT; public MedalList5 Medals => Blocks.Medals; + public KeySystem5 Keys => Blocks.Keys; public string Rival { diff --git a/PKHeX.Core/Saves/Storage/SlotPointerUtil.cs b/PKHeX.Core/Saves/Storage/SlotPointerUtil.cs index aaeb436c8..bec6274b9 100644 --- a/PKHeX.Core/Saves/Storage/SlotPointerUtil.cs +++ b/PKHeX.Core/Saves/Storage/SlotPointerUtil.cs @@ -58,11 +58,7 @@ public static class SlotPointerUtil public static void UpdateRepointFrom(int newIndex, int oldIndex, Span slotPointers) { // Don't return on first match; assume multiple pointers can point to the same slot - foreach (ref var ptr in slotPointers) - { - if (ptr == oldIndex) - ptr = newIndex; - } + slotPointers.Replace(oldIndex, newIndex); } public static void UpdateMove(int bMove, int cMove, int slotsPerBox, params IList[] ptrset) diff --git a/PKHeX.Core/Saves/Substructures/Gen5/BoxLayout5.cs b/PKHeX.Core/Saves/Substructures/Gen5/BoxLayout5.cs index 0e2d12250..3a2d52a38 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/BoxLayout5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/BoxLayout5.cs @@ -5,8 +5,8 @@ namespace PKHeX.Core; public sealed class BoxLayout5(SAV5 sav, Memory raw) : SaveBlock(sav, raw) { public int CurrentBox { get => Data[0]; set => Data[0] = (byte)value; } - public int GetBoxNameOffset(int box) => (0x28 * box) + 4; - public int GetBoxWallpaperOffset(int box) => 0x3C4 + box; + private static int GetBoxNameOffset(int box) => (0x28 * box) + 4; + private static int GetBoxWallpaperOffset(int box) => 0x3C4 + box; public int GetBoxWallpaper(int box) { diff --git a/PKHeX.Core/Saves/Substructures/Gen5/KeySystem5.cs b/PKHeX.Core/Saves/Substructures/Gen5/KeySystem5.cs new file mode 100644 index 000000000..d269e9525 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/KeySystem5.cs @@ -0,0 +1,216 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public sealed class KeySystem5(SAV5B2W2 SAV, Memory raw) : SaveBlock(SAV, raw) +{ + // 0x00-0x27: Unknown + private const int OffsetKeysObtained = 0x28; // 5 * sizeof(uint) + private const int OffsetKeysUnlocked = 0x3C; // 5 * sizeof(uint) + // 3x selections (Difficulty, City, Chamber) - 3 * sizeof(uint) + private const int OffsetCrypto = 0x5C; + + // The game uses a simple XOR encryption and hardcoded magic numbers to indicate selections and key status. + // If the value is not zero, it is encrypted with the XOR key. Compare to the associated magic number. + // Magic numbers (game code), found in ram: 0x0208FA24 + // Selections - Magic Numbers + private const uint MagicCityWhiteForest = 0x34525; + private const uint MagicCityBlackCity = 0x11963; + private const uint MagicDifficultyEasy = 0x31239; + private const uint MagicDifficultyNormal = 0x15657; + private const uint MagicDifficultyChallenge = 0x49589; + private const uint MagicMysteryDoorRock = 0x94525; + private const uint MagicMysteryDoorIron = 0x81963; + private const uint MagicMysteryDoorIceberg = 0x38569; + + // magic numbers, immediately after ^ in ram. same as below set + private static ReadOnlySpan MagicKeyObtained => + [ + 0x35691, // 0x28 Obtained Key (EasyMode) + 0x18256, // 0x2C Obtained Key (Challenge) + 0x59389, // 0x30 Obtained Key (City) + 0x48292, // 0x34 Obtained Key (Iron) + 0x09892, // 0x38 Obtained Key (Iceberg) + ]; + + private static ReadOnlySpan MagicKeyUnlocked => + [ + 0x93389, // 0x3C Unlocked (EasyMode) + 0x22843, // 0x40 Unlocked (Challenge) + 0x34771, // 0x44 Unlocked (City) + 0xAB031, // 0x48 Unlocked (Iron) + 0xB3818, // 0x4C Unlocked (Iceberg) + ]; + + public bool GetIsKeyObtained(KeyType5 key) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)key, (uint)KeyType5.Iceberg); + var offset = OffsetKeysObtained + (sizeof(uint) * (int)key); + var expect = MagicKeyObtained[(int)key] ^ Crypto; + var value = ReadUInt32LittleEndian(Data[offset..]); + return value == expect; + } + + public void SetIsKeyObtained(KeyType5 key, bool value) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)key, (uint)KeyType5.Iceberg); + var offset = OffsetKeysObtained + (sizeof(uint) * (int)key); + var expect = MagicKeyObtained[(int)key] ^ Crypto; + WriteUInt32LittleEndian(Data[offset..], value ? expect : 0); + } + + public bool GetIsKeyUnlocked(KeyType5 key) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)key, (uint)KeyType5.Iceberg); + var offset = OffsetKeysUnlocked + (sizeof(uint) * (int)key); + var expect = MagicKeyUnlocked[(int)key + 5] ^ Crypto; + var value = ReadUInt32LittleEndian(Data[offset..]); + return value == expect; + } + + public void SetIsKeyUnlocked(KeyType5 key, bool value) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)key, (uint)KeyType5.Iceberg); + var offset = OffsetKeysUnlocked + (sizeof(uint) * (int)key); + var expect = MagicKeyUnlocked[(int)key + 5] ^ Crypto; + WriteUInt32LittleEndian(Data[offset..], value ? expect : 0); + } + + // 0x50 - Difficulty Selected (uses selection's magic number) - 0 if default + public Difficulty5 ActiveDifficulty + { + get + { + uint value = ReadUInt32LittleEndian(Data[0x50..]); + if (value is 0) + return Difficulty5.Normal; + + var xor = value ^ Crypto; + return xor switch + { + MagicDifficultyEasy => Difficulty5.Easy, + MagicDifficultyNormal => Difficulty5.Normal, + MagicDifficultyChallenge => Difficulty5.Challenge, + _ => Difficulty5.Normal, // default + }; + } + set + { + if (value is Difficulty5.Normal) + { + WriteUInt32LittleEndian(Data[0x50..], 0); + return; + } + uint write = value switch + { + Difficulty5.Easy => MagicDifficultyEasy, + Difficulty5.Challenge => MagicDifficultyChallenge, + _ => MagicDifficultyNormal, // default + }; + WriteUInt32LittleEndian(Data[0x50..], write ^ Crypto); + } + } + + // 0x54 - City Selected (uses selection's magic number) - 0 if default + public City5 ActiveCity + { + get + { + var initial = SAV.Version is GameVersion.W2 ? City5.WhiteForest : City5.BlackCity; + uint value = ReadUInt32LittleEndian(Data[0x54..]); + if (value is 0) + return initial; + + var xor = value ^ Crypto; + return xor switch + { + MagicCityWhiteForest => City5.WhiteForest, + MagicCityBlackCity => City5.BlackCity, + _ => initial, // default + }; + } + set + { + var initial = SAV.Version is GameVersion.W2 ? City5.WhiteForest : City5.BlackCity; + if (value == initial) + { + WriteUInt32LittleEndian(Data[0x54..], 0); + return; + } + uint write = value == City5.WhiteForest ? MagicCityWhiteForest : MagicCityBlackCity; + WriteUInt32LittleEndian(Data[0x54..], write ^ Crypto); + } + } + + // 0x58 - Chamber Selected (uses selection's magic number) - 0 if default + public Chamber5 ActiveChamber + { + get + { + uint value = ReadUInt32LittleEndian(Data[0x58..]); + if (value is 0) + return Chamber5.Rock; + + var xor = value ^ Crypto; + return xor switch + { + MagicMysteryDoorRock => Chamber5.Rock, + MagicMysteryDoorIron => Chamber5.Iron, + MagicMysteryDoorIceberg => Chamber5.Iceberg, + _ => Chamber5.Rock, // default + }; + } + set + { + if (value is Chamber5.Rock) + { + WriteUInt32LittleEndian(Data[0x58..], 0); + return; + } + uint write = value switch + { + Chamber5.Iron => MagicMysteryDoorIron, + Chamber5.Iceberg => MagicMysteryDoorIceberg, + _ => MagicMysteryDoorRock, // default + }; + WriteUInt32LittleEndian(Data[0x58..], write ^ Crypto); + } + } + + // This value should be > 0xFFFFF to ensure the magic numbers aren't visible in savedata. + public uint Crypto + { + get => ReadUInt32LittleEndian(Data[OffsetCrypto..]); + set => WriteUInt32LittleEndian(Data[OffsetCrypto..], value); + } +} + +public enum Difficulty5 +{ + Easy = 0, + Normal = 1, + Challenge = 2, +} + +public enum City5 +{ + WhiteForest = 0, + BlackCity = 1, +} + +public enum Chamber5 +{ + Rock = 0, + Iron = 1, + Iceberg = 2, +} + +public enum KeyType5 +{ + Easy = 0, + Challenge = 1, + City = 2, + Iron = 3, + Iceberg = 4, +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/Misc5.cs b/PKHeX.Core/Saves/Substructures/Gen5/Misc5.cs index 121470fce..ea67725f9 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/Misc5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/Misc5.cs @@ -51,6 +51,22 @@ public sealed class Misc5BW(SAV5BW sav, Memory raw) : Misc5(sav, raw) { protected override int TransferMinigameScoreOffset => 0x14; protected override int BadgeVictoryOffset => 0x58; // thru 0xB7 + + public const uint LibertyTicketMagic = 2010_04_06; // 0x132B536 + + public uint LibertyTicketState + { + get => ReadUInt32LittleEndian(Data[0xBC..]); + set => WriteUInt32LittleEndian(Data[0xBC..], value); + } + + public uint LibertyTicketExpectValue => LibertyTicketMagic ^ sav.ID32; + + public bool IsLibertyTicketActivated + { + get => LibertyTicketState == LibertyTicketExpectValue; + set => LibertyTicketState = value ? LibertyTicketExpectValue : 0; + } } public sealed class Misc5B2W2(SAV5B2W2 sav, Memory raw) : Misc5(sav, raw) diff --git a/PKHeX.Core/Saves/Substructures/Gen6/BoxLayout6.cs b/PKHeX.Core/Saves/Substructures/Gen6/BoxLayout6.cs index 2f4f02d71..db0bb29e2 100644 --- a/PKHeX.Core/Saves/Substructures/Gen6/BoxLayout6.cs +++ b/PKHeX.Core/Saves/Substructures/Gen6/BoxLayout6.cs @@ -22,7 +22,7 @@ public sealed class BoxLayout6 : SaveBlock, IBoxDetailName, IBoxDetailWall public BoxLayout6(SAV6XY sav, Memory raw) : base(sav, raw) { } public BoxLayout6(SAV6AO sav, Memory raw) : base(sav, raw) { } - public int GetBoxWallpaperOffset(int box) => PCBackgrounds + box; + private static int GetBoxWallpaperOffset(int box) => PCBackgrounds + box; public int GetBoxWallpaper(int box) { diff --git a/PKHeX.Core/Saves/Substructures/Gen7/FashionBlock7.cs b/PKHeX.Core/Saves/Substructures/Gen7/FashionBlock7.cs index 736d76bd1..fc8145406 100644 --- a/PKHeX.Core/Saves/Substructures/Gen7/FashionBlock7.cs +++ b/PKHeX.Core/Saves/Substructures/Gen7/FashionBlock7.cs @@ -50,6 +50,8 @@ public sealed class FashionBlock7(SAV7 sav, Memory raw) : SaveBlock( private static ReadOnlySpan DefaultFashionOffsetUU_F => [ 0x05E, 0x208, 0x264, 0x395, 0x3B4, 0x4F9, 0x5A8 ]; public void ImportPayload(ReadOnlySpan data) => SAV.SetData(Data[..FashionLength], data); + + public void GiveAgentSunglasses() => Data[0xD0] = 3; } // Every fashion item is 2 bits, New Flag (high) & Owned Flag (low) diff --git a/PKHeX.Core/Saves/Substructures/Gen8/BS/BattleTrainerStatus8b.cs b/PKHeX.Core/Saves/Substructures/Gen8/BS/BattleTrainerStatus8b.cs index 3adb2aabb..f1df51d2a 100644 --- a/PKHeX.Core/Saves/Substructures/Gen8/BS/BattleTrainerStatus8b.cs +++ b/PKHeX.Core/Saves/Substructures/Gen8/BS/BattleTrainerStatus8b.cs @@ -42,7 +42,7 @@ public sealed class BattleTrainerStatus8b(SAV8BS sav, Memory raw) : SaveBl } } - private int GetTrainerOffset(int trainer) + private static int GetTrainerOffset(int trainer) { if ((uint)trainer >= COUNT_TRAINER) throw new ArgumentOutOfRangeException(nameof(trainer)); diff --git a/PKHeX.Core/Saves/Util/SaveLanguage.cs b/PKHeX.Core/Saves/Util/SaveLanguage.cs index d793ff03b..69b139293 100644 --- a/PKHeX.Core/Saves/Util/SaveLanguage.cs +++ b/PKHeX.Core/Saves/Util/SaveLanguage.cs @@ -138,6 +138,18 @@ public static class SaveLanguage private static bool Contains(ReadOnlySpan span, ReadOnlySpan value) => span.Contains(value, StringComparison.OrdinalIgnoreCase); + private static bool ContainsSpU(ReadOnlySpan span, ReadOnlySpan value) + { + if (Contains(span, value)) + return true; + + // Check for underscores too; replace the input w/ spaces to underscore + Span tmp = stackalloc char[value.Length]; + value.CopyTo(tmp); + tmp.Replace(' ', '_'); + return Contains(span, tmp); + } + /// public static SaveLanguageResult InferFrom1(ReadOnlySpan name, GameVersion hint = Any) { @@ -191,9 +203,9 @@ public static class SaveLanguage if (MaybeGD(hint)) { if (Contains(name, "golde")) return (German, GD); if (Contains(name, "gold")) return (English, GD); - if (Contains(name, "e oro")) return (Italian, GD); + if (ContainsSpU(name, "e oro")) return (Italian, GD); if (Contains(name, "oro")) return (Spanish, GD); - if (Contains(name, "n or")) return (French, GD); + if (ContainsSpU(name, "n or")) return (French, GD); if (Contains(name, "金")) return (Japanese, GD); if (Contains(name, "금")) return (Korean, GD); @@ -204,7 +216,7 @@ public static class SaveLanguage if (Contains(name, "silv")) return (English, SI); if (Contains(name, "silb")) return (German, SI); if (Contains(name, "plat")) return (Spanish, SI); - if (Contains(name, "e arg")) return (Italian, SI); + if (ContainsSpU(name, "e arg")) return (Italian, SI); if (Contains(name, "arge")) return (French, SI); if (Contains(name, "銀")) return (Japanese, SI); if (Contains(name, "은")) return (Korean, SI); @@ -216,8 +228,8 @@ public static class SaveLanguage if (Contains(name, "cry")) return (English, C); if (Contains(name, "kri")) return (German, C); if (Contains(name, "cristall")) return (Italian, C); - if (Contains(name, "on cri")) return (French, C); - if (Contains(name, "ón crist")) return (Spanish, C); + if (ContainsSpU(name, "on cri")) return (French, C); + if (ContainsSpU(name, "ón crist")) return (Spanish, C); if (Contains(name, "クリ")) return (Japanese, C); } @@ -270,7 +282,7 @@ public static class SaveLanguage if (Contains(name, "esm")) return (Spanish, E); if (Contains(name, "smar")) return (German, E); if (Contains(name, "smer")) return (Italian, E); - if (Contains(name, "éme") || Contains(name, "n eme")) return (French, E); + if (Contains(name, "merau") || ContainsSpU(name, "ion emerald de")) return (French, E); if (Contains(name, "emer")) return (English, E); if (Contains(name, "エメ")) return (Japanese, E); } diff --git a/PKHeX.WinForms/Controls/PKM Editor/EditPK6.cs b/PKHeX.WinForms/Controls/PKM Editor/EditPK6.cs index 74aefd73a..abc444b6d 100644 --- a/PKHeX.WinForms/Controls/PKM Editor/EditPK6.cs +++ b/PKHeX.WinForms/Controls/PKM Editor/EditPK6.cs @@ -1,4 +1,4 @@ -using System; +using System; using PKHeX.Core; namespace PKHeX.WinForms.Controls; @@ -39,11 +39,9 @@ public partial class PKMEditor // Toss in Party Stats SavePartyStats(pk6); - // Unneeded Party Stats (Status, Flags, Unused) - pk6.Data[0xE8] = pk6.Data[0xE9] = pk6.Data[0xEA] = pk6.Data[0xEB] = - pk6.Data[0xEF] = - pk6.Data[0xFE] = pk6.Data[0xFF] = pk6.Data[0x100] = - pk6.Data[0x101] = pk6.Data[0x102] = pk6.Data[0x103] = 0; + // Ensure party stats are essentially clean. + pk6.Data.AsSpan(0xFE).Clear(); + // Status Condition is allowed to be mutated to pre-set conditions like Burn for Guts. pk6.FixMoves(); pk6.FixRelearn(); diff --git a/PKHeX.WinForms/Controls/PKM Editor/EditPK7.cs b/PKHeX.WinForms/Controls/PKM Editor/EditPK7.cs index 1956949bd..2be62d796 100644 --- a/PKHeX.WinForms/Controls/PKM Editor/EditPK7.cs +++ b/PKHeX.WinForms/Controls/PKM Editor/EditPK7.cs @@ -1,4 +1,4 @@ -using System; +using System; using PKHeX.Core; namespace PKHeX.WinForms.Controls; @@ -34,11 +34,9 @@ public partial class PKMEditor // Toss in Party Stats SavePartyStats(pk7); - // Unneeded Party Stats (Status, Flags, Unused) - pk7.Status_Condition = pk7.DirtType = pk7.DirtLocation = - pk7.Data[0xEF] = - pk7.Data[0xFE] = pk7.Data[0xFF] = pk7.Data[0x100] = - pk7.Data[0x101] = pk7.Data[0x102] = pk7.Data[0x103] = 0; + // Ensure party stats are essentially clean. + pk7.Data.AsSpan(0xFE).Clear(); + // Status Condition is allowed to be mutated to pre-set conditions like Burn for Guts. pk7.FixMoves(); pk7.FixRelearn(); diff --git a/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs b/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs index ca5fc8f6f..280dd975c 100644 --- a/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs +++ b/PKHeX.WinForms/Controls/PKM Editor/PKMEditor.cs @@ -1207,7 +1207,7 @@ public sealed partial class PKMEditor : UserControl, IMainEditor { bool g4 = Entity.Gen4; CB_GroundTile.Visible = Label_GroundTile.Visible = g4 && Entity.Format < 7; - if (!g4) + if (FieldsLoaded && !g4) CB_GroundTile.SelectedValue = (int)GroundTileType.None; } diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index 3b5ad7e08..b67525233 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -228,12 +228,14 @@ public partial class Main : Form showChangelog = false; // Version Check - if (Settings.Startup.Version.Length != 0 && Settings.Startup.ShowChangelogOnUpdate) // already run on system + var ver = Program.CurrentVersion; + var startup = Settings.Startup; + if (startup.ShowChangelogOnUpdate && startup.Version.Length != 0) // already run on system { - bool parsed = Version.TryParse(Settings.Startup.Version, out var lastrev); - showChangelog = parsed && lastrev < Program.CurrentVersion; + bool parsed = Version.TryParse(startup.Version, out var lastrev); + showChangelog = parsed && lastrev < ver; } - Settings.Startup.Version = Program.CurrentVersion.ToString(); // set current version so this doesn't happen until the user updates next time + startup.Version = ver.ToString(); // set current version so this doesn't happen until the user updates next time // BAK Prompt if (!Settings.Backup.BAKPrompt) @@ -261,6 +263,8 @@ public partial class Main : Form private void FormLoadPlugins() { + if (Plugins.Count != 0) + return; // already loaded #if !MERGED // merged should load dlls from within too, folder is no longer required if (!Directory.Exists(PluginPath)) return; diff --git a/PKHeX.WinForms/Subforms/PKM Editors/MemoryAmie.cs b/PKHeX.WinForms/Subforms/PKM Editors/MemoryAmie.cs index c0c7fe7c2..b06cc8e12 100644 --- a/PKHeX.WinForms/Subforms/PKM Editors/MemoryAmie.cs +++ b/PKHeX.WinForms/Subforms/PKM Editors/MemoryAmie.cs @@ -35,6 +35,8 @@ public partial class MemoryAmie : Form { tabControl1.TabPages.Remove(Tab_Residence); } + if (Entity is PK9) + tabControl1.TabPages.Remove(Tab_Other); // No Fullness/Enjoyment stored. GetLangStrings(); LoadFields(); @@ -275,20 +277,22 @@ public partial class MemoryAmie : Form private string GetMemoryString(ComboBox m, Control arg, Control q, Control f, string tr) { + var messages = GameInfo.Strings.memories; string result; bool enabled; int mem = WinFormsUtil.GetIndex(m); if (mem == 0) { string nn = Entity.Nickname; - result = string.Format(GameInfo.Strings.memories[0], nn); + result = string.Format(messages[0], nn); enabled = false; } else { + var msg = (uint)mem < messages.Length ? messages[mem] : $"{mem}"; string nn = Entity.Nickname; string a = arg.Text; - result = string.Format(GameInfo.Strings.memories[mem], nn, tr, a, f.Text, q.Text); + result = string.Format(msg, nn, tr, a, f.Text, q.Text); enabled = true; } @@ -353,8 +357,12 @@ public partial class MemoryAmie : Form private void ClickResetLocation(object sender, EventArgs e) { - Label[] senderarr = [L_Geo0, L_Geo1, L_Geo2, L_Geo3, L_Geo4]; - int index = Array.IndexOf(senderarr, sender); + if (sender is not Label l) + return; + Label[] labels = [L_Geo0, L_Geo1, L_Geo2, L_Geo3, L_Geo4]; + int index = Array.IndexOf(labels, l); + if (index < 0) + return; PrevCountries[index].SelectedValue = 0; PrevRegions[index].InitializeBinding(); diff --git a/PKHeX.WinForms/Subforms/ReportGrid.cs b/PKHeX.WinForms/Subforms/ReportGrid.cs index a7d7a05b2..49f417021 100644 --- a/PKHeX.WinForms/Subforms/ReportGrid.cs +++ b/PKHeX.WinForms/Subforms/ReportGrid.cs @@ -215,7 +215,7 @@ public partial class ReportGrid : Form private static string[] ConvertTabbedToRedditTable(ReadOnlySpan lines) { string[] newlines = new string[lines.Length + 1]; - int tabcount = lines[0].Count(c => c == '\t'); + int tabcount = lines[0].AsSpan().Count('\t'); newlines[0] = lines[0].Replace('\t', '|'); newlines[1] = string.Join(":--:", Enumerable.Repeat('|', tabcount + 2)); // 2 pipes for each end diff --git a/PKHeX.WinForms/Subforms/SAV_FolderList.cs b/PKHeX.WinForms/Subforms/SAV_FolderList.cs index 8c0a991c0..9de02bda8 100644 --- a/PKHeX.WinForms/Subforms/SAV_FolderList.cs +++ b/PKHeX.WinForms/Subforms/SAV_FolderList.cs @@ -288,7 +288,7 @@ public partial class SAV_FolderList : Form return list; } - private static void CleanBackups(string path, bool deleteNotSaves) + public static void CleanBackups(string path, bool deleteNotSaves) { var files = Directory.GetFiles(path); foreach (var file in files) @@ -371,14 +371,14 @@ public partial class SAV_FolderList : Form var cm = (CurrencyManager?)BindingContext?[dg.DataSource]; cm?.SuspendBinding(); int column = CB_FilterColumn.SelectedIndex - 1; - var text = TB_FilterTextContains.Text; + var text = TB_FilterTextContains.Text.AsSpan(); for (int i = 0; i < dg.RowCount; i++) ToggleRowVisibility(dg, column, text, i); cm?.ResumeBinding(); } - private static void ToggleRowVisibility(DataGridView dg, int column, string text, int rowIndex) + private static void ToggleRowVisibility(DataGridView dg, int column, ReadOnlySpan text, int rowIndex) { var row = dg.Rows[rowIndex]; if (text.Length == 0 || column < 0) @@ -393,6 +393,6 @@ public partial class SAV_FolderList : Form row.Visible = false; return; } - row.Visible = value.Contains(text, StringComparison.CurrentCultureIgnoreCase); // case insensitive contains + row.Visible = value.AsSpan().Contains(text, StringComparison.CurrentCultureIgnoreCase); // case insensitive contains } } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_Misc5.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_Misc5.cs index 940d3cbf2..251f30680 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_Misc5.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_Misc5.cs @@ -20,11 +20,6 @@ public partial class SAV_Misc5 : Form private ComboBox[] cbr = null!; private int ofsFly; private int[] FlyDestC = null!; - private const int ofsLibPass = 0x212BC; - private const uint keyLibPass = 2010_04_06; // 0x132B536 - private uint valLibPass; - private bool bLibPass; - private const int ofsKS = 0x25828; public SAV_Misc5(SAV5 sav) { @@ -72,18 +67,6 @@ public partial class SAV_Misc5 : Form private void SaveRecord() => SAV.Records.EndAccess(); - private static ReadOnlySpan keyKS => - [ - // 0x34525, 0x11963, // Selected City - // 0x31239, 0x15657, 0x49589, // Selected Difficulty - // 0x94525, 0x81963, 0x38569, // Selected Mystery Door - 0x35691, 0x18256, 0x59389, 0x48292, 0x09892, // Obtained Keys(EasyMode, Challenge, City, Iron, Iceberg) - 0x93389, 0x22843, 0x34771, 0xAB031, 0xB3818, // Unlocked(EasyMode, Challenge, City, Iron, Iceberg) - ]; - - private uint[] valKS = null!; - private bool[] bKS = null!; - private void ReadMain() { string[]? FlyDestA; @@ -180,30 +163,25 @@ public partial class SAV_Misc5 : Form } // LibertyPass - valLibPass = keyLibPass ^ SAV.ID32; - bLibPass = ReadUInt32LittleEndian(SAV.Data.AsSpan(ofsLibPass)) == valLibPass; - CHK_LibertyPass.Checked = bLibPass; + CHK_LibertyPass.Checked = bw.Misc.IsLibertyTicketActivated; } - else if (SAV is SAV5B2W2) + else if (SAV is SAV5B2W2 b2w2) { TC_Misc.TabPages.Remove(TAB_BWCityForest); GB_Roamer.Visible = CHK_LibertyPass.Visible = false; + + var keys = b2w2.Keys; // KeySystem string[] KeySystemA = [ "Obtain EasyKey", "Obtain ChallengeKey", "Obtain CityKey", "Obtain IronKey", "Obtain IcebergKey", - "Unlock EasyMode", "Unlock ChallengeMode", "Unlock City", "Unlock IronChamber", - "Unlock IcebergChamber", + "Unlock EasyMode", "Unlock ChallengeMode", "Unlock City", "Unlock IronChamber", "Unlock IcebergChamber", ]; - uint KSID = ReadUInt32LittleEndian(SAV.Data.AsSpan(ofsKS + 0x34)); - valKS = new uint[keyKS.Length]; - bKS = new bool[keyKS.Length]; CLB_KeySystem.Items.Clear(); - for (int i = 0; i < valKS.Length; i++) + for (int i = 0; i < 5; i++) { - valKS[i] = keyKS[i] ^ KSID; - bKS[i] = ReadUInt32LittleEndian(SAV.Data.AsSpan(ofsKS + (i << 2))) == valKS[i]; - CLB_KeySystem.Items.Add(KeySystemA[i], bKS[i]); + CLB_KeySystem.Items.Add(KeySystemA[i], keys.GetIsKeyObtained((KeyType5)i)); + CLB_KeySystem.Items.Add(KeySystemA[i + 5], keys.GetIsKeyUnlocked((KeyType5)i)); } } else @@ -274,19 +252,23 @@ public partial class SAV_Misc5 : Form } // LibertyPass - if (CHK_LibertyPass.Checked != bLibPass) - WriteUInt32LittleEndian(SAV.Data.AsSpan(ofsLibPass), bLibPass ? 0u : valLibPass); + if (CHK_LibertyPass.Checked != bw.Misc.IsLibertyTicketActivated) + bw.Misc.IsLibertyTicketActivated = CHK_LibertyPass.Checked; } - else if (SAV is SAV5B2W2) + else if (SAV is SAV5B2W2 b2w2) { // KeySystem - for (int i = 0; i < CLB_KeySystem.Items.Count; i++) + var keys = b2w2.Keys; + for (int i = 0; i < 5; i++) { - if (CLB_KeySystem.GetItemChecked(i) == bKS[i]) - continue; - var dest = SAV.Data.AsSpan(ofsKS + (i << 2)); - var value = bKS[i] ? 0u : valKS[i]; - WriteUInt32LittleEndian(dest, value); + var index = i * 2; + var obtain = CLB_KeySystem.GetItemChecked(index); + if (obtain != keys.GetIsKeyObtained((KeyType5)i)) + keys.SetIsKeyObtained((KeyType5)i, obtain); + + var unlock = CLB_KeySystem.GetItemChecked(index + 1); + if (unlock != keys.GetIsKeyUnlocked((KeyType5)i)) + keys.SetIsKeyUnlocked((KeyType5)i, unlock); } } } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen7/SAV_FestivalPlaza.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen7/SAV_FestivalPlaza.cs index e381da512..ace7f85ad 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen7/SAV_FestivalPlaza.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen7/SAV_FestivalPlaza.cs @@ -756,7 +756,7 @@ public partial class SAV_FestivalPlaza : Form { if (NUD_Grade.Value < 30 && DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, "Agent Sunglasses is reward of Grade 30.", "Continue?")) return; - SAV.Fashion.Data[0xD0] = 3; + SAV.Fashion.GiveAgentSunglasses(); B_AgentGlass.Enabled = false; System.Media.SystemSounds.Asterisk.Play(); } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_Misc8b.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_Misc8b.cs index b994b6cb1..bf3619cb2 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_Misc8b.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen8/SAV_Misc8b.cs @@ -91,6 +91,7 @@ public partial class SAV_Misc8b : Form { Unlocker.UnlockZones(); System.Media.SystemSounds.Asterisk.Play(); + B_Zones.Enabled = false; } private void B_DefeatEyecatch_Click(object sender, EventArgs e) @@ -113,5 +114,6 @@ public partial class SAV_Misc8b : Form { Unlocker.UnlockFashion(); System.Media.SystemSounds.Asterisk.Play(); + B_Fashion.Enabled = false; } } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_PokedexSVKitakami.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_PokedexSVKitakami.cs index fdcd8b751..1becf1d1c 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_PokedexSVKitakami.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_PokedexSVKitakami.cs @@ -29,8 +29,7 @@ public partial class SAV_PokedexSVKitakami : Form CB_Species.Items.Clear(); var empty = new string[32]; - foreach (ref var x in empty.AsSpan()) - x = string.Empty; + empty.AsSpan().Fill(string.Empty); CLB_FormSeen.Items.AddRange(empty); CLB_FormObtained.Items.AddRange(empty); CLB_FormHeard.Items.AddRange(empty); diff --git a/PKHeX.WinForms/Util/WinFormsUtil.cs b/PKHeX.WinForms/Util/WinFormsUtil.cs index b873b3154..fa624f9d2 100644 --- a/PKHeX.WinForms/Util/WinFormsUtil.cs +++ b/PKHeX.WinForms/Util/WinFormsUtil.cs @@ -302,7 +302,7 @@ public static class WinFormsUtil public static bool SavePKMDialog(PKM pk) { string pkx = pk.Extension; - bool allowEncrypted = pk.Format >= 3 && pkx[0] == 'p'; + bool allowEncrypted = pk.Format >= 3 && pkx.StartsWith('p'); var genericFilter = $"Decrypted PKM File|*.{pkx}" + (allowEncrypted ? $"|Encrypted PKM File|*.e{pkx[1..]}" : string.Empty) + "|Binary File|*.bin" + diff --git a/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs b/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs index 9bfee0934..d2c275601 100644 --- a/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs +++ b/Tests/PKHeX.Core.Tests/Simulator/ShowdownSetTests.cs @@ -168,7 +168,8 @@ public class ShowdownSetTests public void SimulatorParseDuplicate(string text, int moveCount) { var set = new ShowdownSet(text); - var actual = set.Moves.Count(z => z != 0); + var result = set.Moves.AsSpan(); + var actual = result.Length - result.Count(0); actual.Should().Be(moveCount); }