From 0b6a1accde0cc45bfc15a78c7a4eebb66826ce82 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sat, 30 May 2026 18:51:39 -0500 Subject: [PATCH] Minor tweaks No functional change, extracting some APIs and adding more xmldoc Store potential learn source group as flags, might change later. --- PKHeX.Core/Editing/PKM/LegalMoveInfo.cs | 73 ++- .../{Inventory => Items/Bags}/PlayerBag.cs | 0 .../Encounters/Templates/Shared/Moveset.cs | 14 +- .../LearnSource/Group/LearnGroup9a.cs | 10 +- .../Restrictions/{ => Locale}/Locale3DS.cs | 0 .../Restrictions/Locale/LocaleNDS4.cs | 49 ++ .../Restrictions/Locale/LocaleNDS5.cs | 49 ++ .../Restrictions/{ => Locale}/Vivillon3DS.cs | 0 .../PKM/Util/Conversion/EntityConverter.cs | 9 +- PKHeX.Core/PKM/Util/EffortValues.cs | 2 +- PKHeX.Core/PKM/Util/EntityBlank.cs | 7 +- PKHeX.Core/PKM/Util/EntityDetection.cs | 11 +- PKHeX.Core/Saves/Pokeathlon4.cs | 615 ------------------ PKHeX.Core/Saves/SAV3.cs | 2 +- PKHeX.Core/Saves/SAV4BR.cs | 3 + PKHeX.Core/Saves/{Storage => }/SAV4Ranch.cs | 0 .../Saves/Substructures/Gen4/Accessory4.cs | 200 +++--- .../Saves/Substructures/Gen4/Backdrop4.cs | 38 +- .../Saves/Substructures/Gen4/Geonet4.cs | 43 +- .../Substructures/Gen4/PBR/BattlePassType.cs | 14 +- .../Substructures/Gen4/PBR/GearCategory.cs | 20 +- .../Gen4/Pokeathlon/Aprijuice4.cs | 48 ++ .../Gen4/Pokeathlon/DataCard4.cs | 45 ++ .../Gen4/Pokeathlon/Pokeathlon4.cs | 118 ++++ .../Gen4/Pokeathlon/PokeathlonConnection4.cs | 24 + .../Pokeathlon/PokeathlonCourseRecord4.cs | 24 + .../Gen4/Pokeathlon/PokeathlonEvent4.cs | 20 + .../Gen4/Pokeathlon/PokeathlonEventData4.cs | 21 + .../Gen4/Pokeathlon/PokeathlonEventRecord4.cs | 31 + .../Pokeathlon/PokeathlonEventTrainer4.cs | 25 + .../Pokeathlon/PokeathlonGlobalCounters4.cs | 178 +++++ .../Pokeathlon/PokeathlonMedalManager4.cs | 66 ++ .../Gen4/Pokeathlon/PokeathlonParticipant4.cs | 27 + .../Gen4/Pokeathlon/PokeathlonStat4.cs | 15 + .../Gen4/Pokeathlon/SpeciesForm10.cs | 16 + PKHeX.Core/Saves/Substructures/Gen4/Seal4.cs | 162 ++--- .../Saves/Substructures/Gen5/UnityTower5.cs | 40 +- PKHeX.Core/Util/BinaryCodedDecimal.cs | 10 +- PKHeX.Core/Util/ReflectUtil.cs | 6 + .../PokeathlonEventTrainer4Editor.cs | 29 +- .../Subforms/Save Editors/Gen4/SAV_Geonet4.cs | 4 +- .../Save Editors/Gen5/SAV_UnityTower.cs | 6 +- 42 files changed, 1083 insertions(+), 991 deletions(-) rename PKHeX.Core/{Inventory => Items/Bags}/PlayerBag.cs (100%) rename PKHeX.Core/Legality/Restrictions/{ => Locale}/Locale3DS.cs (100%) create mode 100644 PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS4.cs create mode 100644 PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS5.cs rename PKHeX.Core/Legality/Restrictions/{ => Locale}/Vivillon3DS.cs (100%) delete mode 100644 PKHeX.Core/Saves/Pokeathlon4.cs rename PKHeX.Core/Saves/{Storage => }/SAV4Ranch.cs (100%) create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Aprijuice4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/DataCard4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Pokeathlon4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonConnection4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonCourseRecord4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEvent4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventData4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventRecord4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventTrainer4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonGlobalCounters4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonMedalManager4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonParticipant4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonStat4.cs create mode 100644 PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/SpeciesForm10.cs diff --git a/PKHeX.Core/Editing/PKM/LegalMoveInfo.cs b/PKHeX.Core/Editing/PKM/LegalMoveInfo.cs index 1c8fe88c5..3994c4dc3 100644 --- a/PKHeX.Core/Editing/PKM/LegalMoveInfo.cs +++ b/PKHeX.Core/Editing/PKM/LegalMoveInfo.cs @@ -18,7 +18,14 @@ public sealed class LegalMoveInfo /// /// Move to check if it can be learned /// True if it can learn the move - public bool CanLearn(ushort move) => AllowedMoves[move] != None; + public bool CanLearn(ushort move) => GetMoveSources(move) != None; + + /// + /// Returns the sources that allow the provided move to be learned, or if it cannot be learned. + /// + /// Move to check the sources for + /// Sources that allow the move to be learned + public IndicatedSourceType GetMoveSources(ushort move) => AllowedMoves[move]; /// /// Reloads the legality sources to permit the provided legal info. @@ -57,58 +64,54 @@ public bool ReloadMoves(LegalityAnalysis la) private static void ComputeEval(Span type, ReadOnlySpan learn, LegalityAnalysis la) { - for (int i = 0; i < type.Length; i++) + // Wipe or set as learnable based on learnability; encounter moves will be added later. + for (int i = 1; i < type.Length; i++) type[i] = learn[i] ? Learn : None; + // If the original moveset is deleted, then encounter moves are not relevant to legality and should not be added. if (!la.Entity.IsOriginalMovesetDeleted()) AddEncounterMoves(type, la.EncounterOriginal); type[0] = None; // Move ID 0 is always None } - private static void AddEncounterMoves(Span type, IEncounterTemplate enc) + private static void AddEncounterMoves(Span result, IEncounterTemplate enc) { - if (enc is IEncounterEgg egg) - { - var moves = egg.Learn.GetEggMoves(enc.Species, enc.Form); - foreach (var move in moves) - type[move] = Egg; - } - else if (enc is IMoveset {Moves: {HasMoves: true} set}) - { - foreach (var move in set.AsSpan()) - { - if (type[move] == None) - type[move] = Encounter; - } - } - else if (enc is ISingleMoveBonus single) - { - var moves = single.GetMoveBonusPossible(); - foreach (var move in moves) - { - if (type[move] == None) - type[move] = EncounterSingle; - } - } + if (enc is IRelearn { Relearn: { HasMoves: true } relearn }) + relearn.FlagMoves(result, Relearn); - if (enc is IRelearn { Relearn: {HasMoves: true} relearn}) + if (enc is IEncounterEgg egg) + FlagMoves(result, Egg, egg.Learn.GetEggMoves(enc.Species, enc.Form)); + else if (enc is IMoveset { Moves: { HasMoves: true } set}) + set.FlagMoves(result, Encounter); + else if (enc is ISingleMoveBonus { IsMoveBonusPossible: true } single) + FlagIfNone(result, EncounterSingle, single.GetMoveBonusPossible()); + + return; + + static void FlagMoves(Span result, IndicatedSourceType flag, ReadOnlySpan moves) { - foreach (var move in relearn.AsSpan()) + foreach (var move in moves) + result[move] |= flag; + } + static void FlagIfNone(Span result, IndicatedSourceType flag, ReadOnlySpan moves) + { + foreach (var move in moves) { - if (type[move] == None) - type[move] = Relearn; + if (result[move] == None) + result[move] |= flag; } } } } +[Flags] public enum IndicatedSourceType : byte { None = 0, - Learn, - Egg, - Encounter, - EncounterSingle, - Relearn, + Learn = 1 << 0, + Egg = 1 << 1, + Encounter = 1 << 2, + EncounterSingle = 1 << 3, + Relearn = 1 << 4, } diff --git a/PKHeX.Core/Inventory/PlayerBag.cs b/PKHeX.Core/Items/Bags/PlayerBag.cs similarity index 100% rename from PKHeX.Core/Inventory/PlayerBag.cs rename to PKHeX.Core/Items/Bags/PlayerBag.cs diff --git a/PKHeX.Core/Legality/Encounters/Templates/Shared/Moveset.cs b/PKHeX.Core/Legality/Encounters/Templates/Shared/Moveset.cs index 5ec390202..fe3a57acb 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Shared/Moveset.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Shared/Moveset.cs @@ -39,12 +39,6 @@ namespace PKHeX.Core; /// True if any move is above the maximum; otherwise, false. public bool AnyAbove(ushort max) => Move1 > max || Move2 > max || Move3 > max || Move4 > max; - /// - /// Returns the moveset as an array of four move IDs. - /// - /// An array containing the four move IDs. - public ushort[] ToArray() => [Move1, Move2, Move3, Move4]; - /// /// Gets a read-only span view of the moveset's four move IDs. /// @@ -176,4 +170,12 @@ public void FlagMoves(Span result) result[Move3] = true; result[Move4] = true; } + + public void FlagMoves(Span result, IndicatedSourceType value) + { + result[Move1] |= value; + result[Move2] |= value; + result[Move3] |= value; + result[Move4] |= value; + } } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs index 576ed1002..141551a3d 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs @@ -109,14 +109,8 @@ private static void GetAllMovesInternal(Span result, PKM pk, EvoCriteria e private static void FlagEncounterMoves(IEncounterTemplate enc, Span result) { if (enc is IMoveset { Moves: { HasMoves: true } x }) - { - foreach (var move in x.AsSpan()) - result[move] = true; - } + x.FlagMoves(result); if (enc is IRelearn { Relearn: { HasMoves: true } r }) - { - foreach (var move in r.AsSpan()) - result[move] = true; - } + r.FlagMoves(result); } } diff --git a/PKHeX.Core/Legality/Restrictions/Locale3DS.cs b/PKHeX.Core/Legality/Restrictions/Locale/Locale3DS.cs similarity index 100% rename from PKHeX.Core/Legality/Restrictions/Locale3DS.cs rename to PKHeX.Core/Legality/Restrictions/Locale/Locale3DS.cs diff --git a/PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS4.cs b/PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS4.cs new file mode 100644 index 000000000..a2a65b462 --- /dev/null +++ b/PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS4.cs @@ -0,0 +1,49 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Provides information for Geonet/Battle Revolution player location information. +/// +/// These values were specific to the NDS games (Generation 4) +public static class LocaleNDS4 +{ + public const int CountryCount = 233; + public const int Japan = 103; + + public static ReadOnlySpan LegalCountries => + [ + 001, 002, 003, 006, 008, 009, 012, 013, 015, 016, 017, 018, 020, 021, 022, 023, + 025, 027, 028, 029, 031, 033, 034, 035, 036, 040, 042, 043, 045, 048, 049, 050, + 052, 054, 055, 056, 058, 059, 060, 061, 062, 069, 070, 071, 072, 074, 077, 078, + 079, 080, 081, 082, 083, 085, 086, 088, 089, 090, 091, 092, 093, 094, 095, 097, + 098, 100, 101, 102, 103, 104, 107, 111, 115, 117, 118, 121, 122, 126, 129, 131, + 133, 135, 140, 142, 146, 148, 149, 150, 151, 152, 156, 157, 158, 160, 161, 163, + 164, 166, 167, 110, 171, 172, 179, 183, 186, 187, 188, 189, 192, 193, 194, 196, + 198, 199, 200, 202, 205, 207, 211, 212, 216, 218, 219, 204, 221, 220, 222, 224, + 226, 227, + ]; + + public static byte GetSubregionCount(byte country) => country switch + { + 009 => 24, // Argentina + 012 => 7, // Australia + 028 => 27, // Brazil + 036 => 13, // Canada + 043 => 31, // China + 070 => 6, // Finland + 071 => 22, // France + 077 => 16, // Germany + 094 => 35, // India + 101 => 20, // Italy + 103 => 50, // Japan + 156 => 20, // Norway + 166 => 16, // Poland + 172 => 7, // Russian Federation + 193 => 17, // Spain + 199 => 24, // Sweden + 219 => 12, // United Kingdom + 220 => 51, // United States of America + _ => 0, + }; +} diff --git a/PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS5.cs b/PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS5.cs new file mode 100644 index 000000000..266ca2ea2 --- /dev/null +++ b/PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS5.cs @@ -0,0 +1,49 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Provides information for Unity Tower player location information. +/// +/// These values were specific to the NDS games (Generation 5) +public static class LocaleNDS5 +{ + public const int CountryCount = 232; + public const int Japan = 105; + + public static ReadOnlySpan LegalCountries => + [ + 001, 002, 003, 006, 008, 009, 012, 013, 015, 016, 017, 018, 020, 021, 022, 023, + 025, 027, 028, 029, 031, 033, 034, 035, 036, 040, 042, 043, 045, 047, 048, 049, + 051, 053, 054, 058, 060, 061, 062, 063, 064, 071, 072, 073, 074, 076, 079, 080, + 081, 082, 083, 084, 085, 087, 088, 090, 091, 092, 093, 094, 095, 096, 098, 099, + 101, 102, 103, 105, 106, 109, 111, 115, 117, 118, 121, 125, 128, 130, 132, 134, + 138, 139, 141, 145, 147, 148, 149, 150, 151, 155, 156, 157, 160, 161, 163, 164, + 166, 167, 170, 173, 174, 181, 185, 186, 188, 189, 190, 191, 194, 195, 196, 198, + 199, 200, 201, 203, 205, 206, 210, 211, 215, 217, 218, 219, 220, 221, 222, 224, + 226, 227, + ]; + + public static byte GetSubregionCount(byte country) => country switch + { + 009 => 24, // Argentina + 012 => 8, // Australia + 028 => 27, // Brazil + 036 => 13, // Canada + 043 => 33, // China + 072 => 6, // Finland + 073 => 22, // France + 079 => 16, // Germany + 095 => 35, // India + 102 => 20, // Italy + 105 => 50, // Japan + 155 => 22, // Norway + 166 => 16, // Poland + 174 => 8, // Russian Federation + 195 => 17, // Spain + 200 => 22, // Sweden + 218 => 12, // United Kingdom + 220 => 51, // United States of America + _ => 0, + }; +} diff --git a/PKHeX.Core/Legality/Restrictions/Vivillon3DS.cs b/PKHeX.Core/Legality/Restrictions/Locale/Vivillon3DS.cs similarity index 100% rename from PKHeX.Core/Legality/Restrictions/Vivillon3DS.cs rename to PKHeX.Core/Legality/Restrictions/Locale/Vivillon3DS.cs diff --git a/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs b/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs index f04f47552..5132f4d1f 100644 --- a/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs +++ b/PKHeX.Core/PKM/Util/Conversion/EntityConverter.cs @@ -337,14 +337,17 @@ public static bool TryMakePKMCompatible(PKM pk, PKM target, out EntityConverterR /// /// Checks if a is incompatible with the Generation 1/2 destination environment. /// + /// Target type PKM with misc properties accessible for checking. + /// Whether the destination environment is Japanese + /// Whether the source PKM is Japanese public static bool IsCompatibleGB(PKM pk, bool destJapanese, bool srcJapanese) { if (pk.Format > 2) - return true; + return true; // Upwards transfers are unaffected by language, and Gen3+ can represent all languages. if (destJapanese == srcJapanese) - return true; + return true; // Can trade between same language sets. if (pk is SK2 sk2 && sk2.IsPossible(srcJapanese)) - return true; + return true; // Language differentiation return false; } } diff --git a/PKHeX.Core/PKM/Util/EffortValues.cs b/PKHeX.Core/PKM/Util/EffortValues.cs index c06399d2a..5904496c4 100644 --- a/PKHeX.Core/PKM/Util/EffortValues.cs +++ b/PKHeX.Core/PKM/Util/EffortValues.cs @@ -198,7 +198,7 @@ public static bool IsChampions(ReadOnlySpan evs) } /// -/// Assessment of the total EVs, compared to the maximum allowed. +/// Assessment of the total EVs (Gen3+), compared to the maximum allowed. /// public enum EffortValueGrade { diff --git a/PKHeX.Core/PKM/Util/EntityBlank.cs b/PKHeX.Core/PKM/Util/EntityBlank.cs index 4a1e742ee..bc0cc10fd 100644 --- a/PKHeX.Core/PKM/Util/EntityBlank.cs +++ b/PKHeX.Core/PKM/Util/EntityBlank.cs @@ -12,7 +12,10 @@ public static class EntityBlank /// /// Type of instance desired. /// New instance of a blank object. - public static PKM GetBlank(Type type) => type.Name switch + public static PKM GetBlank(Type type) => GetBlank(type.Name); + + /// + public static PKM GetBlank(ReadOnlySpan type) => type switch { nameof(PK1) => new PK1(), nameof(PK2) => new PK2(), @@ -33,7 +36,7 @@ public static class EntityBlank nameof(PK9) => new PK9(), nameof(PA9) => new PA9(), nameof(PKH) => new PKH(), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + _ => throw new ArgumentOutOfRangeException(nameof(type), type.ToString(), null), }; /// diff --git a/PKHeX.Core/PKM/Util/EntityDetection.cs b/PKHeX.Core/PKM/Util/EntityDetection.cs index dfd4ca686..a08dfb195 100644 --- a/PKHeX.Core/PKM/Util/EntityDetection.cs +++ b/PKHeX.Core/PKM/Util/EntityDetection.cs @@ -49,12 +49,17 @@ SIZE_5PARTY or /// public static bool IsPresentSAV4Ranch(ReadOnlySpan data) => IsPresent(data) && ReadUInt32BigEndian(data) != 0x28; // Species non-zero, ignore file end marker + /// + /// Checks the PID and species of the Gen4+ entity to determine if it is present. + /// public static bool IsPresent(ReadOnlySpan data) { if (ReadUInt32LittleEndian(data) != 0) // PID - return true; - ushort species = ReadUInt16LittleEndian(data[8..]); - return species != 0; + return true; // Empty slots are 0x00000000 PID. + + // A PID of 0x00000000 is possible to naturally occur; encryption/shuffle does not impact the first 2 bytes (species). + // The data occupying the species field can be immediately read; non-zero confirms species is present. + return ReadUInt16LittleEndian(data[8..]) != 0; } /// diff --git a/PKHeX.Core/Saves/Pokeathlon4.cs b/PKHeX.Core/Saves/Pokeathlon4.cs deleted file mode 100644 index bd3e347b8..000000000 --- a/PKHeX.Core/Saves/Pokeathlon4.cs +++ /dev/null @@ -1,615 +0,0 @@ -using System; -using static System.Buffers.Binary.BinaryPrimitives; - - -namespace PKHeX.Core; - -/// -/// Manages the Pokeathlon Data for -/// -public sealed class Pokeathlon4(Memory Raw) // 0xD9D4 within SAV4HGSS -{ - public const int SIZE = 0xB80; - - public Span Data => Raw.Span; - - // 5 courses to store record data - public PokeathlonCourseRecord4 GetCourseRecord(PokeathlonStat4 index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonStat4.Count); - return new(Raw.Slice((int)index * PokeathlonCourseRecord4.SIZE, PokeathlonCourseRecord4.SIZE)); - } - - // 0xDC, 0xDAB0 within SAV - public PokeathlonMedalManager4 Medals => new(Raw.Slice(0xDC, PokeathlonMedalManager4.SIZE)); - // 3 bytes alignment - - // 0x2CC, 0xDCA0 within SAV - public PokeathlonEventData4 GetEventSelf(PokeathlonEvent4 index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); - return new(Raw.Slice(0x2CC + ((int)index * PokeathlonEventData4.SIZE), PokeathlonEventData4.SIZE)); - } - - // 0x484, 0xDE58 within SAV - public PokeathlonConnection4 GetEventConnection(PokeathlonEvent4 index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); - return new(Raw.Slice(0x484 + ((int)index * PokeathlonConnection4.SIZE), PokeathlonConnection4.SIZE)); - } - - // 0xAEC, 0xE4C0 within SAV - - /// - /// Player's highest score for each of the ten individual events (after conversion to Athlete Points) - /// - public ushort GetBestScore(PokeathlonEvent4 index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); - return ReadUInt16LittleEndian(Data[(0xAEC + ((int)index * 2))..]); - } - - public void SetBestScore(PokeathlonEvent4 index, ushort score) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); - WriteUInt16LittleEndian(Data[(0xAEC + ((int)index * 2))..], score); - } - - // 0xB00, 0xE4D4 within SAV global counters - public PokeathlonGlobalCounters4 GlobalCounters => new(Raw.Slice(0xB00, PokeathlonGlobalCounters4.SIZE)); - - // remainder @ 0xE548 - public const uint MaxPoints = 99_999; - - /// - /// Current points count accumulated, used in buying items from shops. - /// - public uint Points { get => ReadUInt32LittleEndian(Data[0xB74..]); set => WriteUInt32LittleEndian(Data[0xB74..], Math.Min(MaxPoints, value)); } - - /// - /// Obtained Data Card indexes [0,26], where each bit represents whether a Data Card has been obtained or not. - /// They can be purchased in exchange for points in the Pokéathlon Dome (sold at the central reception desk). - /// - public uint FlagsDataCard { get => ReadUInt32LittleEndian(Data[0xB78..]); set => WriteUInt32LittleEndian(Data[0xB78..], Math.Min(DataCardAllObtained, value)); } - - // items exist as Key Items, not really advisable to have a one-shot method unlock, end-user implementation beware. - public const uint DataCardAllObtained = 0x07FFFFFFu; // 27 bits, all obtained - - /// - /// Once-daily shop purchase flags for the Athlete Shop. - /// Since shops only have 12 items daily, only 12 bits are used. - /// - public ushort FlagsDailyShop { get => ReadUInt16LittleEndian(Data[0xB7C..]); set => WriteUInt16LittleEndian(Data[0xB7C..], Math.Min(FlagsShopAllObtained, value)); } - public const ushort FlagsShopAllObtained = 0x0FFF; // 12 bits, all obtained - - // last 2 bytes unused, total size 0xB80 - - /// - /// The global Pokéathlon score is calculated as the sum of: - /// - the player's best final score in each of the five courses, - /// - the player's highest score for each of the ten individual events (after conversion to Athlete Points), - /// - the total number of medals displayed in the box in the Trust room (so each Medalist species will add five to this total). - /// - public uint CalculateGlobalScore() - { - uint result = 0; - for (PokeathlonStat4 i = 0; i < PokeathlonStat4.Count; i++) - result += GetCourseRecord(i).ScoreMax; - result += Medals.GetTotalCount(); - for (PokeathlonEvent4 i = 0; i < PokeathlonEvent4.Count; i++) - result += GetBestScore(i); - return result; - } - - public static ReadOnlySpan FriendshipTrophyThresholds => [3000, 3100, 3200, 3300, 3400, 3600, 3800, 4000, 4200, 4500]; - - public static int CalculateFriendshipTrophyCount(uint globalScore) - { - int result = 0; - foreach (var threshold in FriendshipTrophyThresholds) - { - if (globalScore >= threshold) - result++; - else - break; - } - return result; // 10 max - } -} - -public struct PokeathlonCourseRecord4(Memory Raw) -{ - public const int SIZE = 0x2C; - - private Span Data => Raw.Span; - - public ushort Score0 { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } - public ushort Score1 { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } - public ushort Score2 { get => ReadUInt16LittleEndian(Data[4..]); set => WriteUInt16LittleEndian(Data[4..], value); } - public ushort ScoreMax { get => ReadUInt16LittleEndian(Data[6..]); set => WriteUInt16LittleEndian(Data[6..], value); } - - public const int CountParticipant = 3; - - public PokeathlonParticipant4 GetParticipant(int index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, CountParticipant); - return new(Raw.Slice(8 + (index * PokeathlonParticipant4.SIZE), PokeathlonParticipant4.SIZE)); - } -} - -public struct PokeathlonParticipant4(Memory Raw) : ISpeciesForm, ITrainerID32, IFixedGender, IShiny -{ - public const int SIZE = 0xC; - - private Span Data => Raw.Span; - - private uint Packed { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, value); } - public ushort Species { get => (ushort)(Packed & 0x1FF); set => Packed = (Packed & ~0x1FFu) | ((uint)value & 0x1FF); } - public byte Form { get => (byte)((Packed >> 9) & 0x1F); set => Packed = (Packed & ~(0x1Fu << 9)) | (((uint)value & 0x1F) << 9); } - public byte Gender { get => (byte)((Packed >> 14) & 0x3); set => Packed = (Packed & ~(0x3u << 14)) | (((uint)value & 0x3) << 14); } - public bool IsShiny { get => ((Packed >> 16) & 0x1) != 0; set => Packed = (Packed & ~(0x1u << 16)) | ((value ? 1u : 0u) << 16); } - // remainder of bits unused - - /// - public uint EncryptionConstant { get => ReadUInt32LittleEndian(Data[4..]); set => WriteUInt32LittleEndian(Data[4..], value); } - - /// - public uint ID32 { get => ReadUInt32LittleEndian(Data[8..]); set => WriteUInt32LittleEndian(Data[8..], value); } - public ushort TID16 { get => ReadUInt16LittleEndian(Data[8..]); set => WriteUInt16LittleEndian(Data[8..], value); } - public ushort SID16 { get => ReadUInt16LittleEndian(Data[10..]); set => WriteUInt16LittleEndian(Data[10..], value); } - public TrainerIDFormat TrainerIDDisplayFormat => TrainerIDFormat.SixteenBit; -} - -/// -/// Stores a bitflag medal completion state for all species. -/// -/// -/// for bitflag indexes. -/// -public struct PokeathlonMedalManager4(Memory Raw) -{ - public const int SIZE = 493; // 1-indexed species [Bulbasaur..Arceus] - public const byte MaxMedalBits = 0b11111; // 5 courses, 5 bits per species - - public Span Data => Raw.Span; - - /// - /// Retrieves the medal bits for the given species, where each bit represents whether a medal for a particular course has been obtained or not. - /// - public byte GetMedal(ushort species) - { - species--; - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4); - return Data[species]; - } - - /// - /// Updates the medal bits for the given species. - /// - public void SetMedal(ushort species, byte medalBits) - { - species--; - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4); - Data[species] = medalBits; - } - - /// - /// Awards the provided bit(s) to the species. - /// - public void AwardMedal(ushort species, byte medalBit) - { - species--; - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4); - Data[species] |= medalBit; - } - - /// - /// Awards all medals to all species (complete). - /// - /// Medal value to set to every species entry. - public void SetAllMedals(byte medalBits = MaxMedalBits) => Data[..SIZE].Fill(medalBits); - - /// - /// Removes all medals from all species (resets progress). - /// - public void Clear() => SetAllMedals(0); - - public uint GetTotalCount() - { - uint result = 0; - foreach (var bits in Data[..SIZE]) - result += (uint)System.Numerics.BitOperations.PopCount((uint)bits & MaxMedalBits); - return result; - } -} - -public struct PokeathlonEventData4(Memory Raw) -{ - public const int SIZE = 0x2C; - public const uint MaxAttempts = 9_999_999; - public const uint MaxRecord = 5; - - public Span Data => Raw.Span; - - public PokeathlonEventRecord4 GetRecord(int index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, MaxRecord); - return new(Raw.Slice(index * PokeathlonEventRecord4.SIZE, PokeathlonEventRecord4.SIZE)); - } - - public uint Attempts { get => ReadUInt32LittleEndian(Data[0x28..]); set => WriteUInt32LittleEndian(Data[0x28..], Math.Min(MaxAttempts, value)); } -} - -public struct PokeathlonConnection4(Memory Raw) -{ - public const int SIZE = 0xA4; - - public const uint MaxTrainer = PokeathlonEventData4.MaxRecord; - - public Span Data => Raw.Span; - - public PokeathlonEventData4 Inner => new(Raw.Slice(0 * PokeathlonEventData4.SIZE, PokeathlonEventData4.SIZE)); - - /// - /// Correlated to the indexed records. - /// - public PokeathlonEventTrainer4 GetTrainer(int index) - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, MaxTrainer); - return new(Raw.Slice(0x2C + (index * PokeathlonEventTrainer4.SIZE), PokeathlonEventTrainer4.SIZE)); - } -} - -public struct PokeathlonEventTrainer4(Memory Raw) : ITrainerID32 -{ - public const int SIZE = 0x18; - public Span Data => Raw.Span; - public uint ID32 { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, value); } - public ushort TID16 { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } - public ushort SID16 { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } - public TrainerIDFormat TrainerIDDisplayFormat => TrainerIDFormat.SixteenBit; - - public Span OriginalTrainerTrash => Data.Slice(8, 8 * sizeof(ushort)); - - public byte Language { get => Data[0x14]; set => Data[0x14] = value; } - // remaining 3 bytes unused - - public string OT - { - get => StringConverter4.GetString(OriginalTrainerTrash); - set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, Language); - } -} - -/// -/// Event record storage for a given event type. -/// -/// -/// -/// -/// -public struct PokeathlonEventRecord4(Memory Raw) -{ - public const int SIZE = 8; - - public Span Data => Raw.Span; - - /// - /// Stores the record for the particular event. The meaning of this value depends on the event type. - /// - /// - /// Hurdle Dash: Time in frames (lower is better). - /// - public ushort Record { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } - - // team of three Pokémon responsible for this record, simply (species,form)*3. - public SpeciesForm10 Entry0 { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } - public SpeciesForm10 Entry1 { get => ReadUInt16LittleEndian(Data[4..]); set => WriteUInt16LittleEndian(Data[4..], value); } - public SpeciesForm10 Entry2 { get => ReadUInt16LittleEndian(Data[6..]); set => WriteUInt16LittleEndian(Data[6..], value); } -} - -public record struct SpeciesForm10(ushort Value) : ISpeciesForm -{ - // 10 bits species - public ushort Species { get => (ushort)(Value & 0x3FF); set => Value = (ushort)((Value & ~0x3FF) | (value & 0x3FF)); } - public byte Form { get => (byte)((Value >> 10) & 0x3F); set => Value = (ushort)((Value & ~(0x3Fu << 10)) | ((((uint)value & 0x3F) << 10))); } - - /// - /// Useful sanity check. - /// - public bool IsValid => Value is 0 || (Species != 0 && PersonalTable.HGSS.IsPresentInGame(Species, Form)); - - public static implicit operator SpeciesForm10(ushort value) => new(value); - public static implicit operator ushort(SpeciesForm10 value) => value.Value; -} - -public struct PokeathlonGlobalCounters4(Memory Raw) -{ - public const int SIZE = 0x74; - public Span Data => Raw.Span; - - public const uint MaxPlay = 59_999; - public const uint MaxStat = 9_999_999; - public const uint MaxFame = ushort.MaxValue; - - /// Time Spent in Pokéathlon, in minutes - public uint TimeSpent { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, Math.Min(MaxPlay, value)); } - - public uint SessionsJoined { get => ReadUInt32LittleEndian(Data[0x04..]); set => WriteUInt32LittleEndian(Data[0x04..], Math.Min(MaxStat, value)); } - public uint PlacedFirst { get => ReadUInt32LittleEndian(Data[0x08..]); set => WriteUInt32LittleEndian(Data[0x08..], Math.Min(MaxStat, value)); } - public uint PlacedLast { get => ReadUInt32LittleEndian(Data[0x0C..]); set => WriteUInt32LittleEndian(Data[0x0C..], Math.Min(MaxStat, value)); } - - /// - /// Bonuses Earned - /// - public uint BonusesEarned { get => ReadUInt32LittleEndian(Data[0x10..]); set => WriteUInt32LittleEndian(Data[0x10..], Math.Min(MaxStat, value)); } - - /// - /// Pokémon Instructions - /// - public uint Instructions { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], Math.Min(MaxStat, value)); } - public uint Failed { get => ReadUInt32LittleEndian(Data[0x18..]); set => WriteUInt32LittleEndian(Data[0x18..], Math.Min(MaxStat, value)); } - public uint Jumped { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], Math.Min(MaxStat, value)); } - public uint Acquired { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], Math.Min(MaxStat, value)); } - public uint Tackled { get => ReadUInt32LittleEndian(Data[0x24..]); set => WriteUInt32LittleEndian(Data[0x24..], Math.Min(MaxStat, value)); } - public uint FellDown { get => ReadUInt32LittleEndian(Data[0x28..]); set => WriteUInt32LittleEndian(Data[0x28..], Math.Min(MaxStat, value)); } - public uint Dashed { get => ReadUInt32LittleEndian(Data[0x2C..]); set => WriteUInt32LittleEndian(Data[0x2C..], Math.Min(MaxStat, value)); } - public uint Switched { get => ReadUInt32LittleEndian(Data[0x30..]); set => WriteUInt32LittleEndian(Data[0x30..], Math.Min(MaxStat, value)); } - public uint SelfImpeded { get => ReadUInt32LittleEndian(Data[0x34..]); set => WriteUInt32LittleEndian(Data[0x34..], Math.Min(MaxStat, value)); } - - public uint ConnectionJoined { get => ReadUInt32LittleEndian(Data[0x38..]); set => WriteUInt32LittleEndian(Data[0x38..], Math.Min(MaxStat, value)); } - public uint ConnectionFirst { get => ReadUInt32LittleEndian(Data[0x3C..]); set => WriteUInt32LittleEndian(Data[0x3C..], Math.Min(MaxStat, value)); } - public uint ConnectionLast { get => ReadUInt32LittleEndian(Data[0x40..]); set => WriteUInt32LittleEndian(Data[0x40..], Math.Min(MaxStat, value)); } - - // Per-event 1st-place counters - public uint this[PokeathlonEvent4 index] - { - get - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); - return ReadUInt32LittleEndian(Data[(0x44 + ((int)index * 4))..]); - } - set - { - ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); - WriteUInt32LittleEndian(Data[(0x44 + ((int)index * 4))..], Math.Min(MaxStat, value)); - } - } - - // helper to calculate - public uint TotalEventFirst - { - get - { - uint total = 0; - for (PokeathlonEvent4 i = 0; i < PokeathlonEvent4.Count; i++) - total += this[i]; - return total; - } - } - - // Aggregate event last-place total - public uint TotalEventLast { get => ReadUInt32LittleEndian(Data[0x6C..]); set => WriteUInt32LittleEndian(Data[0x6C..], Math.Min(MaxStat, value)); } - - /// - /// When the player plays the Pokéathlon over wireless play the default drinks will be replaced with the drinks of the opposing player. - /// The price of the drinks are determined on how famous the Trainers selling them are. - /// - /// - /// - /// - public uint Fame { get => ReadUInt32LittleEndian(Data[0x70..]); set => WriteUInt32LittleEndian(Data[0x70..], Math.Min(MaxFame, value)); } - - /// - /// Retrieves the value of a particular statistic for Data Card purposes, based on the provided stat identifier. - /// - public uint GetDataCardStat(DataCard4 stat) => stat switch - { - DataCard4.PlacedFirst => this.PlacedFirst, - DataCard4.PlacedLast => this.PlacedLast, - DataCard4.Dashed => this.Dashed, - DataCard4.Jumped => this.Jumped, - DataCard4.FirstHurdle => this[PokeathlonEvent4.HurdleDash], - DataCard4.FirstRelay => this[PokeathlonEvent4.RelayRun], - DataCard4.FirstPennant => this[PokeathlonEvent4.PennantCapture], - DataCard4.FirstBlockSmash => this[PokeathlonEvent4.BlockSmash], - DataCard4.FirstDiscCatch => this[PokeathlonEvent4.DiscCatch], - DataCard4.FirstSnowThrow => this[PokeathlonEvent4.SnowThrow], - DataCard4.PointsAcquired => this.Acquired, - DataCard4.Failed => this.Failed, - DataCard4.SelfImpeded => this.SelfImpeded, - DataCard4.Tackled => this.Tackled, - DataCard4.FellDown => this.FellDown, - DataCard4.FirstRingDrop => this[PokeathlonEvent4.RingDrop], - DataCard4.FirstLampJump => this[PokeathlonEvent4.LampJump], - DataCard4.FirstCirclePush => this[PokeathlonEvent4.CirclePush], - DataCard4.ConnectionFirst => this.ConnectionFirst, - DataCard4.ConnectionLast => this.ConnectionLast, - DataCard4.EventFirst => this.TotalEventFirst, // aggregate event first-place total is not stored, but can be calculated by summing per-event counters - DataCard4.EventLast => this.TotalEventLast, - DataCard4.Switched => this.Switched, - DataCard4.FirstGoalRoll => this[PokeathlonEvent4.GoalRoll], - DataCard4.BonusesEarned => this.BonusesEarned, - DataCard4.Instructions => this.Instructions, - DataCard4.TimeSpent => this.TimeSpent, - _ => throw new ArgumentOutOfRangeException(nameof(stat), stat, null), - }; - - /// - /// Updates the value of a particular statistic for Data Card purposes, based on the provided stat identifier. - /// - public void SetDataCardStat(DataCard4 stat, uint value) - { - value = Math.Min(MaxStat, value); - switch (stat) - { - case DataCard4.PlacedFirst: this.PlacedFirst = value; break; - case DataCard4.PlacedLast: this.PlacedLast = value; break; - case DataCard4.Dashed: this.Dashed = value; break; - case DataCard4.Jumped: this.Jumped = value; break; - case DataCard4.FirstHurdle: this[PokeathlonEvent4.HurdleDash] = value; break; - case DataCard4.FirstRelay: this[PokeathlonEvent4.RelayRun] = value; break; - case DataCard4.FirstPennant: this[PokeathlonEvent4.PennantCapture] = value; break; - case DataCard4.FirstBlockSmash: this[PokeathlonEvent4.BlockSmash] = value; break; - case DataCard4.FirstDiscCatch: this[PokeathlonEvent4.DiscCatch] = value; break; - case DataCard4.FirstSnowThrow: this[PokeathlonEvent4.SnowThrow] = value; break; - case DataCard4.PointsAcquired: this.Acquired = value; break; - case DataCard4.Failed: this.Failed = value; break; - case DataCard4.SelfImpeded: this.SelfImpeded = value; break; - case DataCard4.Tackled: this.Tackled = value; break; - case DataCard4.FellDown: this.FellDown = value; break; - case DataCard4.FirstRingDrop: this[PokeathlonEvent4.RingDrop] = value; break; - case DataCard4.FirstLampJump: this[PokeathlonEvent4.LampJump] = value; break; - case DataCard4.FirstCirclePush: this[PokeathlonEvent4.CirclePush] = value; break; - case DataCard4.ConnectionFirst: this.ConnectionFirst = value; break; - case DataCard4.ConnectionLast: this.ConnectionLast = value; break; - case DataCard4.EventFirst: - return; // skip, not going to spread the count across all 10 events - case DataCard4.EventLast: this.TotalEventLast = value; break; - case DataCard4.Switched: this.Switched = value; break; - case DataCard4.FirstGoalRoll: this[PokeathlonEvent4.GoalRoll] = value; break; - case DataCard4.BonusesEarned: this.BonusesEarned = value; break; - case DataCard4.Instructions: this.Instructions = value; break; - case DataCard4.TimeSpent: this.TimeSpent = value; break; - default: throw new ArgumentOutOfRangeException(nameof(stat), stat, null); - } - } - - public static ReadOnlySpan FameInclusiveThreshold => [10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000]; - - /// - /// Used to evaluate the fame level of a given trainer, ratcheting up/down based on how much fame has been aggregated. - /// - /// Player fame value - /// Value [0,12] - public static int GetFameLevel(uint value) - { - int level = 0; - foreach (var threshold in FameInclusiveThreshold) - { - if (value <= threshold) - break; - level++; - } - return level; // 12 max - } - - // disassembly also calculates fame for NPCs by summing all performance stats of their team, then (sum/3)-8, clamped [0,12]. -} -public struct Aprijuice5(Memory Raw) -{ - public Span Data => Raw.Span; - - /// - /// The price of drinks are determined by how famous the Trainers selling them are. - /// - /// - /// - /// - public ushort Fame { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } - - // Every 100 steps taken increases the mildness of an Aprijuice by 1, up to a maximum of 255. - // Any mildness increases are made before the mixing of new Apricorns into the Aprijuice is performed. - public byte Mildness { get => Data[2]; set => Data[2] = value; } - - // Each flavor is capped at a maximum of 63 points and a minimum of 0. - public byte Spicy { get => Data[3]; set => Data[3] = value; } - public byte Sour { get => Data[4]; set => Data[4] = value; } - public byte Dry { get => Data[5]; set => Data[5] = value; } - public byte Bitter { get => Data[6]; set => Data[6] = value; } - public byte Sweet { get => Data[7]; set => Data[7] = value; } - - public const ushort LevelMax = 100; - - public byte CalculateLevel() - { - var level = Spicy + Sour + Dry + Bitter + Sweet; - return (byte)Math.Clamp(level, 0, LevelMax); // never will see 0 unless it's hacked :) - } - - public const ushort PriceMin = 100; - public const ushort PriceMax = 5000; - - public ushort CalculatePrice() - { - var level = CalculateLevel(); - var price = (Fame / 10) + (level / 2); - return (ushort)Math.Clamp(price, PriceMin, PriceMax); - } - - // When a Pokémon is put into a PC box, all effects of an Aprijuice disappear. -} - -/// -/// Performance stats -/// -public enum PokeathlonStat4 : byte -{ - Speed = 0, - Power = 1, - Skill = 2, - Stamina = 3, - Jump = 4, - - Count = 5, -} - -public enum PokeathlonEvent4 : byte -{ - HurdleDash = 0, - PennantCapture = 1, - CirclePush = 2, - BlockSmash = 3, - DiscCatch = 4, - LampJump = 5, - RelayRun = 6, - RingDrop = 7, - SnowThrow = 8, - GoalRoll = 9, - - Count = 10, -} - -/// -/// Data Card indexes and the stats they retrieve in . -/// -public enum DataCard4 : byte -{ - PlacedFirst = 0, // Pokéathlon 1st Place - PlacedLast = 1, // Pokéathlon Last Place - Dashed = 2, // Times Pokémon Dashed - Jumped = 3, // Times Pokémon Jumped - - FirstHurdle = 4, // Hurdle Dash 1st Places - FirstRelay = 5, // Relay Run 1st Places - FirstPennant = 6, // Pennant Capture 1st Places - FirstBlockSmash = 7, // Block Smash 1st Places - FirstDiscCatch = 8, // Disc Catch 1st Places - FirstSnowThrow = 9, // Snow Throw 1st Places - - PointsAcquired = 10, // Pokémon Acquired Points - Failed = 11, // Pokémon Failed - SelfImpeded = 12, // Times Pokémon Self-Impeded - Tackled = 13, // Times Pokémon Tackled - FellDown = 14, // Pokémon Fell Down - - FirstRingDrop = 15, // Ring Drop 1st Places - FirstLampJump = 16, // Lamp Jump 1st Places - FirstCirclePush = 17, // Circle Push 1st Places - - ConnectionFirst = 18, // Connection 1st Places - ConnectionLast = 19, // Connection Last Places - - EventFirst = 20, // Event 1st Places - EventLast = 21, // Event Last Places - - Switched = 22, // Times Pokémon Switched - - FirstGoalRoll = 23, // Goal Roll 1st Places - - BonusesEarned = 24, // Bonuses Earned - Instructions = 25, // Pokémon Instructions - TimeSpent = 26, // Time Spent in Pokéathlon - - Count = 27, -}; diff --git a/PKHeX.Core/Saves/SAV3.cs b/PKHeX.Core/Saves/SAV3.cs index 5e920480c..e04515336 100644 --- a/PKHeX.Core/Saves/SAV3.cs +++ b/PKHeX.Core/Saves/SAV3.cs @@ -638,7 +638,7 @@ public void SetHallOfFameData(ReadOnlySpan value) /// 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 bool IsCorruptPokedexFF() => BitConverter.ToUInt64(Small[0xAC..]) == ulong.MaxValue; public sealed override void CopyChangesFrom(SaveFile sav) { diff --git a/PKHeX.Core/Saves/SAV4BR.cs b/PKHeX.Core/Saves/SAV4BR.cs index 8533f9d62..475b9c47c 100644 --- a/PKHeX.Core/Saves/SAV4BR.cs +++ b/PKHeX.Core/Saves/SAV4BR.cs @@ -314,6 +314,9 @@ public string BirthDay set => StringConverter4GC.SetStringUnicodeBR(value, BirthDayTrash); } + /// + /// + /// public int Country { get => ReadUInt16BigEndian(Data[0x3C0..]); set => WriteUInt16BigEndian(Data[0x578..], (ushort)value); } public int Region { get => ReadUInt16BigEndian(Data[0x3C2..]); set => WriteUInt16BigEndian(Data[0x57A..], (ushort)value); } diff --git a/PKHeX.Core/Saves/Storage/SAV4Ranch.cs b/PKHeX.Core/Saves/SAV4Ranch.cs similarity index 100% rename from PKHeX.Core/Saves/Storage/SAV4Ranch.cs rename to PKHeX.Core/Saves/SAV4Ranch.cs diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Accessory4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Accessory4.cs index b9ff4c40e..a00748271 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/Accessory4.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/Accessory4.cs @@ -4,110 +4,110 @@ namespace PKHeX.Core; public enum Accessory4 : byte { - WhiteFluff, - YellowFluff, - PinkFluff, - BrownFluff, - BlackFluff, - OrangeFluff, - RoundPebble, - GlitterBoulder, - SnaggyPebble, - JaggedBoulder, - BlackPebble, - MiniPebble, - PinkScale, - BlueScale, - GreenScale, - PurpleScale, - BigScale, - NarrowScale, - BlueFeather, - RedFeather, - YellowFeather, - WhiteFeather, - BlackMoustache, - WhiteMoustache, - BlackBeard, - WhiteBeard, - SmallLeaf, - BigLeaf, - NarrowLeaf, - ShedClaw, - ShedHorn, - ThinMushroom, - ThickMushroom, - Stump, - PrettyDewdrop, - SnowCrystal, - Sparks, - ShimmeringFire, - MysticFire, - Determination, - PeculiarSpoon, - PuffySmoke, - PoisonExtract, - WealthyCoin, - EerieThing, - Spring, - Seashell, - HummingNote, - ShinyPowder, - GlitterPowder, - RedFlower, - PinkFlower, - WhiteFlower, - BlueFlower, - OrangeFlower, - YellowFlower, - GooglySpecs, - BlackSpecs, - GorgeousSpecs, - SweetCandy, - Confetti, + WhiteFluff = 0, + YellowFluff = 1, + PinkFluff = 2, + BrownFluff = 3, + BlackFluff = 4, + OrangeFluff = 5, + RoundPebble = 6, + GlitterBoulder = 7, + SnaggyPebble = 8, + JaggedBoulder = 9, + BlackPebble = 10, + MiniPebble = 11, + PinkScale = 12, + BlueScale = 13, + GreenScale = 14, + PurpleScale = 15, + BigScale = 16, + NarrowScale = 17, + BlueFeather = 18, + RedFeather = 19, + YellowFeather = 20, + WhiteFeather = 21, + BlackMoustache = 22, + WhiteMoustache = 23, + BlackBeard = 24, + WhiteBeard = 25, + SmallLeaf = 26, + BigLeaf = 27, + NarrowLeaf = 28, + ShedClaw = 29, + ShedHorn = 30, + ThinMushroom = 31, + ThickMushroom = 32, + Stump = 33, + PrettyDewdrop = 34, + SnowCrystal = 35, + Sparks = 36, + ShimmeringFire = 37, + MysticFire = 38, + Determination = 39, + PeculiarSpoon = 40, + PuffySmoke = 41, + PoisonExtract = 42, + WealthyCoin = 43, + EerieThing = 44, + Spring = 45, + Seashell = 46, + HummingNote = 47, + ShinyPowder = 48, + GlitterPowder = 49, + RedFlower = 50, + PinkFlower = 51, + WhiteFlower = 52, + BlueFlower = 53, + OrangeFlower = 54, + YellowFlower = 55, + GooglySpecs = 56, + BlackSpecs = 57, + GorgeousSpecs = 58, + SweetCandy = 59, + Confetti = 60, // For accessories below this point, only 1 copy can be owned at once - ColoredParasol, - OldUmbrella, - Spotlight, - Cape, - StandingMike, - Surfboard, - Carpet, - RetroPipe, - FluffyBed, - MirrorBall, - PhotoBoard, - PinkBarrette, - RedBarrette, - BlueBarrette, - YellowBarrette, - GreenBarrette, - PinkBalloon, - RedBalloons, - BlueBalloons, - YellowBalloon, - GreenBalloons, - LaceHeadress, - TopHat, - SilkVeil, - HeroicHeadband, - ProfessorHat, - FlowerStage, - GoldPedestal, - GlassStage, - AwardPodium, - CubeStage, - TURTWIGMask, - CHIMCHARMask, - PIPLUPMask, - BigTree, - Flag, - Crown, - Tiara, + ColoredParasol = 61, + OldUmbrella = 62, + Spotlight = 63, + Cape = 64, + StandingMike = 65, + Surfboard = 66, + Carpet = 67, + RetroPipe = 68, + FluffyBed = 69, + MirrorBall = 70, + PhotoBoard = 71, + PinkBarrette = 72, + RedBarrette = 73, + BlueBarrette = 74, + YellowBarrette = 75, + GreenBarrette = 76, + PinkBalloon = 77, + RedBalloons = 78, + BlueBalloons = 79, + YellowBalloon = 80, + GreenBalloons = 81, + LaceHeadress = 82, + TopHat = 83, + SilkVeil = 84, + HeroicHeadband = 85, + ProfessorHat = 86, + FlowerStage = 87, + GoldPedestal = 88, + GlassStage = 89, + AwardPodium = 90, + CubeStage = 91, + TURTWIGMask = 92, + CHIMCHARMask = 93, + PIPLUPMask = 94, + BigTree = 95, + Flag = 96, + Crown = 97, + Tiara = 98, // Unreleased - Comet, + Comet = 99, } public static class AccessoryInfo diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Backdrop4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Backdrop4.cs index e72d10f54..40730cf51 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/Backdrop4.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/Backdrop4.cs @@ -2,28 +2,28 @@ namespace PKHeX.Core; public enum Backdrop4 : byte { - DressUp, - Ranch, - CityatNight, - SnowyTown, - Fiery, - OuterSpace, - Desert, - CumulusCloud, - FlowerPatch, - FutureRoom, - OpenSea, - TotalDarkness, - TatamiRoom, - GingerbreadRoom, - Seafloor, - Underground, - Sky, + DressUp = 0, + Ranch = 1, + CityatNight = 2, + SnowyTown = 3, + Fiery = 4, + OuterSpace = 5, + Desert = 6, + CumulusCloud = 7, + FlowerPatch = 8, + FutureRoom = 9, + OpenSea = 10, + TotalDarkness = 11, + TatamiRoom = 12, + GingerbreadRoom = 13, + Seafloor = 14, + Underground = 15, + Sky = 16, // Unreleased - Theater, + Theater = 17, - Unset, + Unset = 18, } public static class BackdropInfo diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Geonet4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Geonet4.cs index 4245fb927..6c60008a6 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/Geonet4.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/Geonet4.cs @@ -1,4 +1,4 @@ -using System; +using static PKHeX.Core.LocaleNDS4; namespace PKHeX.Core; @@ -22,44 +22,7 @@ public void Save() SAV.SetData(SAV.General.Slice(Offset, CountryCount * 16), Data); } - public const int CountryCount = 233; - private const int Japan = 103; - - private static ReadOnlySpan LegalCountries => - [ - 001, 002, 003, 006, 008, 009, 012, 013, 015, 016, 017, 018, 020, 021, 022, 023, - 025, 027, 028, 029, 031, 033, 034, 035, 036, 040, 042, 043, 045, 048, 049, 050, - 052, 054, 055, 056, 058, 059, 060, 061, 062, 069, 070, 071, 072, 074, 077, 078, - 079, 080, 081, 082, 083, 085, 086, 088, 089, 090, 091, 092, 093, 094, 095, 097, - 098, 100, 101, 102, 103, 104, 107, 111, 115, 117, 118, 121, 122, 126, 129, 131, - 133, 135, 140, 142, 146, 148, 149, 150, 151, 152, 156, 157, 158, 160, 161, 163, - 164, 166, 167, 110, 171, 172, 179, 183, 186, 187, 188, 189, 192, 193, 194, 196, - 198, 199, 200, 202, 205, 207, 211, 212, 216, 218, 219, 204, 221, 220, 222, 224, - 226, 227, - ]; - - public static byte GetSubregionCount(byte country) => country switch - { - 009 => 24, // Argentina - 012 => 7, // Australia - 028 => 27, // Brazil - 036 => 13, // Canada - 043 => 31, // China - 070 => 6, // Finland - 071 => 22, // France - 077 => 16, // Germany - 094 => 35, // India - 101 => 20, // Italy - 103 => 50, // Japan - 156 => 20, // Norway - 166 => 16, // Poland - 172 => 7, // Russian Federation - 193 => 17, // Spain - 199 => 24, // Sweden - 219 => 12, // United Kingdom - 220 => 51, // United States of America - _ => 0, - }; + public static byte GetSubregionCount(byte country) => LocaleNDS4.GetSubregionCount(country); public bool GlobalFlag { get => SAV.GeonetGlobalFlag; set => SAV.GeonetGlobalFlag = value; } @@ -79,7 +42,7 @@ public void SetCountrySubregion(byte country, byte subregion, GeonetPoint point) private void SetAllSubregions(byte country, GeonetPoint type) { - var subregionCount = GetSubregionCount(country); + var subregionCount = LocaleNDS4.GetSubregionCount(country); if (subregionCount == 0) { SetCountrySubregion(country, 0, type); diff --git a/PKHeX.Core/Saves/Substructures/Gen4/PBR/BattlePassType.cs b/PKHeX.Core/Saves/Substructures/Gen4/PBR/BattlePassType.cs index 0bcb86a5e..a3f014f51 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/PBR/BattlePassType.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/PBR/BattlePassType.cs @@ -2,11 +2,11 @@ namespace PKHeX.Core; public enum BattlePassType { - Custom, - Rental, - Friend, - Download, - Other1, - Other2, - Other3, + Custom = 0, + Rental = 1, + Friend = 2, + Download = 3, + Other1 = 4, + Other2 = 5, + Other3 = 6, } diff --git a/PKHeX.Core/Saves/Substructures/Gen4/PBR/GearCategory.cs b/PKHeX.Core/Saves/Substructures/Gen4/PBR/GearCategory.cs index e84726539..d29944138 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/PBR/GearCategory.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/PBR/GearCategory.cs @@ -2,14 +2,14 @@ namespace PKHeX.Core; public enum GearCategory : byte { - Head, - Hair, - Face, - Top, - Bottom, - Shoes, - Hands, - Bags, - Glasses, - Badges, + Head = 0, + Hair = 1, + Face = 2, + Top = 3, + Bottom = 4, + Shoes = 5, + Hands = 6, + Bags = 7, + Glasses = 8, + Badges = 9, } diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Aprijuice4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Aprijuice4.cs new file mode 100644 index 000000000..c9e67010e --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Aprijuice4.cs @@ -0,0 +1,48 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct Aprijuice4(Memory Raw) +{ + public Span Data => Raw.Span; + + /// + /// The price of drinks are determined by how famous the Trainers selling them are. + /// + /// + /// + /// + public ushort Fame { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + + // Every 100 steps taken increases the mildness of an Aprijuice by 1, up to a maximum of 255. + // Any mildness increases are made before the mixing of new Apricorns into the Aprijuice is performed. + public byte Mildness { get => Data[2]; set => Data[2] = value; } + + // Each flavor is capped at a maximum of 63 points and a minimum of 0. + public byte Spicy { get => Data[3]; set => Data[3] = value; } + public byte Sour { get => Data[4]; set => Data[4] = value; } + public byte Dry { get => Data[5]; set => Data[5] = value; } + public byte Bitter { get => Data[6]; set => Data[6] = value; } + public byte Sweet { get => Data[7]; set => Data[7] = value; } + + public const ushort LevelMax = 100; + + public byte CalculateLevel() + { + var level = Spicy + Sour + Dry + Bitter + Sweet; + return (byte)Math.Clamp(level, 0, LevelMax); // never will see 0 unless it's hacked :) + } + + public const ushort PriceMin = 100; + public const ushort PriceMax = 5000; + + public ushort CalculatePrice() + { + var level = CalculateLevel(); + var price = (Fame / 10) + (level / 2); + return (ushort)Math.Clamp(price, PriceMin, PriceMax); + } + + // When a Pokémon is put into a PC box, all effects of an Aprijuice disappear. +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/DataCard4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/DataCard4.cs new file mode 100644 index 000000000..a5bf8cf5f --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/DataCard4.cs @@ -0,0 +1,45 @@ +namespace PKHeX.Core; + +/// +/// Data Card indexes and the stats they retrieve in . +/// +public enum DataCard4 : byte +{ + PlacedFirst = 0, // Pokéathlon 1st Place + PlacedLast = 1, // Pokéathlon Last Place + Dashed = 2, // Times Pokémon Dashed + Jumped = 3, // Times Pokémon Jumped + + FirstHurdle = 4, // Hurdle Dash 1st Places + FirstRelay = 5, // Relay Run 1st Places + FirstPennant = 6, // Pennant Capture 1st Places + FirstBlockSmash = 7, // Block Smash 1st Places + FirstDiscCatch = 8, // Disc Catch 1st Places + FirstSnowThrow = 9, // Snow Throw 1st Places + + PointsAcquired = 10, // Pokémon Acquired Points + Failed = 11, // Pokémon Failed + SelfImpeded = 12, // Times Pokémon Self-Impeded + Tackled = 13, // Times Pokémon Tackled + FellDown = 14, // Pokémon Fell Down + + FirstRingDrop = 15, // Ring Drop 1st Places + FirstLampJump = 16, // Lamp Jump 1st Places + FirstCirclePush = 17, // Circle Push 1st Places + + ConnectionFirst = 18, // Connection 1st Places + ConnectionLast = 19, // Connection Last Places + + EventFirst = 20, // Event 1st Places + EventLast = 21, // Event Last Places + + Switched = 22, // Times Pokémon Switched + + FirstGoalRoll = 23, // Goal Roll 1st Places + + BonusesEarned = 24, // Bonuses Earned + Instructions = 25, // Pokémon Instructions + TimeSpent = 26, // Time Spent in Pokéathlon + + Count = 27, +}; diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Pokeathlon4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Pokeathlon4.cs new file mode 100644 index 000000000..ea8844cfa --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Pokeathlon4.cs @@ -0,0 +1,118 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + + +namespace PKHeX.Core; + +/// +/// Manages the Pokeathlon Data for +/// +public sealed class Pokeathlon4(Memory Raw) // 0xD9D4 within SAV4HGSS +{ + public const int SIZE = 0xB80; + + public Span Data => Raw.Span; + + // 5 courses to store record data + public PokeathlonCourseRecord4 GetCourseRecord(PokeathlonStat4 index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonStat4.Count); + return new(Raw.Slice((int)index * PokeathlonCourseRecord4.SIZE, PokeathlonCourseRecord4.SIZE)); + } + + // 0xDC, 0xDAB0 within SAV + public PokeathlonMedalManager4 Medals => new(Raw.Slice(0xDC, PokeathlonMedalManager4.SIZE)); + // 3 bytes alignment + + // 0x2CC, 0xDCA0 within SAV + public PokeathlonEventData4 GetEventSelf(PokeathlonEvent4 index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); + return new(Raw.Slice(0x2CC + ((int)index * PokeathlonEventData4.SIZE), PokeathlonEventData4.SIZE)); + } + + // 0x484, 0xDE58 within SAV + public PokeathlonConnection4 GetEventConnection(PokeathlonEvent4 index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); + return new(Raw.Slice(0x484 + ((int)index * PokeathlonConnection4.SIZE), PokeathlonConnection4.SIZE)); + } + + // 0xAEC, 0xE4C0 within SAV + + /// + /// Player's highest score for each of the ten individual events (after conversion to Athlete Points) + /// + public ushort GetBestScore(PokeathlonEvent4 index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); + return ReadUInt16LittleEndian(Data[(0xAEC + ((int)index * 2))..]); + } + + public void SetBestScore(PokeathlonEvent4 index, ushort score) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); + WriteUInt16LittleEndian(Data[(0xAEC + ((int)index * 2))..], score); + } + + // 0xB00, 0xE4D4 within SAV global counters + public PokeathlonGlobalCounters4 GlobalCounters => new(Raw.Slice(0xB00, PokeathlonGlobalCounters4.SIZE)); + + // remainder @ 0xE548 + public const uint MaxPoints = 99_999; + + /// + /// Current points count accumulated, used in buying items from shops. + /// + public uint Points { get => ReadUInt32LittleEndian(Data[0xB74..]); set => WriteUInt32LittleEndian(Data[0xB74..], Math.Min(MaxPoints, value)); } + + /// + /// Obtained Data Card indexes [0,26], where each bit represents whether a Data Card has been obtained or not. + /// They can be purchased in exchange for points in the Pokéathlon Dome (sold at the central reception desk). + /// + public uint FlagsDataCard { get => ReadUInt32LittleEndian(Data[0xB78..]); set => WriteUInt32LittleEndian(Data[0xB78..], Math.Min(DataCardAllObtained, value)); } + + // items exist as Key Items, not really advisable to have a one-shot method unlock, end-user implementation beware. + public const uint DataCardAllObtained = 0x07FFFFFFu; // 27 bits, all obtained + + /// + /// Once-daily shop purchase flags for the Athlete Shop. + /// Since shops only have 12 items daily, only 12 bits are used. + /// + public ushort FlagsDailyShop { get => ReadUInt16LittleEndian(Data[0xB7C..]); set => WriteUInt16LittleEndian(Data[0xB7C..], Math.Min(FlagsShopAllObtained, value)); } + public const ushort FlagsShopAllObtained = 0x0FFF; // 12 bits, all obtained + + // last 2 bytes unused, total size 0xB80 + + /// + /// The global Pokéathlon score is calculated as the sum of: + /// - the player's best final score in each of the five courses, + /// - the player's highest score for each of the ten individual events (after conversion to Athlete Points), + /// - the total number of medals displayed in the box in the Trust room (so each Medalist species will add five to this total). + /// + public uint CalculateGlobalScore() + { + uint result = 0; + for (PokeathlonStat4 i = 0; i < PokeathlonStat4.Count; i++) + result += GetCourseRecord(i).ScoreMax; + result += Medals.GetTotalCount(); + for (PokeathlonEvent4 i = 0; i < PokeathlonEvent4.Count; i++) + result += GetBestScore(i); + return result; + } + + public static ReadOnlySpan FriendshipTrophyThresholds => [3000, 3100, 3200, 3300, 3400, 3600, 3800, 4000, 4200, 4500]; + + public static int CalculateFriendshipTrophyCount(uint globalScore) + { + int result = 0; + foreach (var threshold in FriendshipTrophyThresholds) + { + if (globalScore >= threshold) + result++; + else + break; + } + return result; // 10 max + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonConnection4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonConnection4.cs new file mode 100644 index 000000000..368a72855 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonConnection4.cs @@ -0,0 +1,24 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct PokeathlonConnection4(Memory Raw) +{ + public const int SIZE = 0xA4; + + public const uint MaxTrainer = PokeathlonEventData4.MaxRecord; + + public Span Data => Raw.Span; + + public PokeathlonEventData4 Inner => new(Raw.Slice(0 * PokeathlonEventData4.SIZE, PokeathlonEventData4.SIZE)); + + /// + /// Correlated to the indexed records. + /// + public PokeathlonEventTrainer4 GetTrainer(int index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, MaxTrainer); + return new(Raw.Slice(0x2C + (index * PokeathlonEventTrainer4.SIZE), PokeathlonEventTrainer4.SIZE)); + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonCourseRecord4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonCourseRecord4.cs new file mode 100644 index 000000000..2e4cbb482 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonCourseRecord4.cs @@ -0,0 +1,24 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct PokeathlonCourseRecord4(Memory Raw) +{ + public const int SIZE = 0x2C; + + private Span Data => Raw.Span; + + public ushort Score0 { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + public ushort Score1 { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } + public ushort Score2 { get => ReadUInt16LittleEndian(Data[4..]); set => WriteUInt16LittleEndian(Data[4..], value); } + public ushort ScoreMax { get => ReadUInt16LittleEndian(Data[6..]); set => WriteUInt16LittleEndian(Data[6..], value); } + + public const int CountParticipant = 3; + + public PokeathlonParticipant4 GetParticipant(int index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, CountParticipant); + return new(Raw.Slice(8 + (index * PokeathlonParticipant4.SIZE), PokeathlonParticipant4.SIZE)); + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEvent4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEvent4.cs new file mode 100644 index 000000000..9b4d1fec1 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEvent4.cs @@ -0,0 +1,20 @@ +namespace PKHeX.Core; + +/// +/// Enumeration of the 10 different Pokeathlon events in Gen 4, used for indexing into the event data structures. +/// +public enum PokeathlonEvent4 : byte +{ + HurdleDash = 0, + PennantCapture = 1, + CirclePush = 2, + BlockSmash = 3, + DiscCatch = 4, + LampJump = 5, + RelayRun = 6, + RingDrop = 7, + SnowThrow = 8, + GoalRoll = 9, + + Count = 10, +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventData4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventData4.cs new file mode 100644 index 000000000..5542feaf7 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventData4.cs @@ -0,0 +1,21 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct PokeathlonEventData4(Memory Raw) +{ + public const int SIZE = 0x2C; + public const uint MaxAttempts = 9_999_999; + public const uint MaxRecord = 5; + + public Span Data => Raw.Span; + + public PokeathlonEventRecord4 GetRecord(int index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, MaxRecord); + return new(Raw.Slice(index * PokeathlonEventRecord4.SIZE, PokeathlonEventRecord4.SIZE)); + } + + public uint Attempts { get => ReadUInt32LittleEndian(Data[0x28..]); set => WriteUInt32LittleEndian(Data[0x28..], Math.Min(MaxAttempts, value)); } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventRecord4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventRecord4.cs new file mode 100644 index 000000000..0c95b6144 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventRecord4.cs @@ -0,0 +1,31 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// Event record storage for a given event type. +/// +/// +/// +/// +/// +public struct PokeathlonEventRecord4(Memory Raw) +{ + public const int SIZE = 8; + + public Span Data => Raw.Span; + + /// + /// Stores the record for the particular event. The meaning of this value depends on the event type. + /// + /// + /// Hurdle Dash: Time in frames (lower is better). + /// + public ushort Record { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + + // team of three Pokémon responsible for this record, simply (species,form)*3. + public SpeciesForm10 Entry0 { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } + public SpeciesForm10 Entry1 { get => ReadUInt16LittleEndian(Data[4..]); set => WriteUInt16LittleEndian(Data[4..], value); } + public SpeciesForm10 Entry2 { get => ReadUInt16LittleEndian(Data[6..]); set => WriteUInt16LittleEndian(Data[6..], value); } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventTrainer4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventTrainer4.cs new file mode 100644 index 000000000..dbd29f31d --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonEventTrainer4.cs @@ -0,0 +1,25 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct PokeathlonEventTrainer4(Memory Raw) : ITrainerID32 +{ + public const int SIZE = 0x18; + public Span Data => Raw.Span; + public uint ID32 { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, value); } + public ushort TID16 { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); } + public ushort SID16 { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); } + public TrainerIDFormat TrainerIDDisplayFormat => TrainerIDFormat.SixteenBit; + + public Span OriginalTrainerTrash => Data.Slice(8, 8 * sizeof(ushort)); + + public byte Language { get => Data[0x14]; set => Data[0x14] = value; } + // remaining 3 bytes unused + + public string OriginalTrainerName + { + get => StringConverter4.GetString(OriginalTrainerTrash); + set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, Language); + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonGlobalCounters4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonGlobalCounters4.cs new file mode 100644 index 000000000..f4f476f0f --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonGlobalCounters4.cs @@ -0,0 +1,178 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct PokeathlonGlobalCounters4(Memory Raw) +{ + public const int SIZE = 0x74; + public Span Data => Raw.Span; + + public const uint MaxPlay = 59_999; + public const uint MaxStat = 9_999_999; + public const uint MaxFame = ushort.MaxValue; + + /// Time Spent in Pokéathlon, in minutes + public uint TimeSpent { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, Math.Min(MaxPlay, value)); } + + public uint SessionsJoined { get => ReadUInt32LittleEndian(Data[0x04..]); set => WriteUInt32LittleEndian(Data[0x04..], Math.Min(MaxStat, value)); } + public uint PlacedFirst { get => ReadUInt32LittleEndian(Data[0x08..]); set => WriteUInt32LittleEndian(Data[0x08..], Math.Min(MaxStat, value)); } + public uint PlacedLast { get => ReadUInt32LittleEndian(Data[0x0C..]); set => WriteUInt32LittleEndian(Data[0x0C..], Math.Min(MaxStat, value)); } + + /// + /// Bonuses Earned + /// + public uint BonusesEarned { get => ReadUInt32LittleEndian(Data[0x10..]); set => WriteUInt32LittleEndian(Data[0x10..], Math.Min(MaxStat, value)); } + + /// + /// Pokémon Instructions + /// + public uint Instructions { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], Math.Min(MaxStat, value)); } + public uint Failed { get => ReadUInt32LittleEndian(Data[0x18..]); set => WriteUInt32LittleEndian(Data[0x18..], Math.Min(MaxStat, value)); } + public uint Jumped { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], Math.Min(MaxStat, value)); } + public uint Acquired { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], Math.Min(MaxStat, value)); } + public uint Tackled { get => ReadUInt32LittleEndian(Data[0x24..]); set => WriteUInt32LittleEndian(Data[0x24..], Math.Min(MaxStat, value)); } + public uint FellDown { get => ReadUInt32LittleEndian(Data[0x28..]); set => WriteUInt32LittleEndian(Data[0x28..], Math.Min(MaxStat, value)); } + public uint Dashed { get => ReadUInt32LittleEndian(Data[0x2C..]); set => WriteUInt32LittleEndian(Data[0x2C..], Math.Min(MaxStat, value)); } + public uint Switched { get => ReadUInt32LittleEndian(Data[0x30..]); set => WriteUInt32LittleEndian(Data[0x30..], Math.Min(MaxStat, value)); } + public uint SelfImpeded { get => ReadUInt32LittleEndian(Data[0x34..]); set => WriteUInt32LittleEndian(Data[0x34..], Math.Min(MaxStat, value)); } + + public uint ConnectionJoined { get => ReadUInt32LittleEndian(Data[0x38..]); set => WriteUInt32LittleEndian(Data[0x38..], Math.Min(MaxStat, value)); } + public uint ConnectionFirst { get => ReadUInt32LittleEndian(Data[0x3C..]); set => WriteUInt32LittleEndian(Data[0x3C..], Math.Min(MaxStat, value)); } + public uint ConnectionLast { get => ReadUInt32LittleEndian(Data[0x40..]); set => WriteUInt32LittleEndian(Data[0x40..], Math.Min(MaxStat, value)); } + + // Per-event 1st-place counters + public uint this[PokeathlonEvent4 index] + { + get + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); + return ReadUInt32LittleEndian(Data[(0x44 + ((int)index * 4))..]); + } + set + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)PokeathlonEvent4.Count); + WriteUInt32LittleEndian(Data[(0x44 + ((int)index * 4))..], Math.Min(MaxStat, value)); + } + } + + // helper to calculate + public uint TotalEventFirst + { + get + { + uint total = 0; + for (PokeathlonEvent4 i = 0; i < PokeathlonEvent4.Count; i++) + total += this[i]; + return total; + } + } + + // Aggregate event last-place total + public uint TotalEventLast { get => ReadUInt32LittleEndian(Data[0x6C..]); set => WriteUInt32LittleEndian(Data[0x6C..], Math.Min(MaxStat, value)); } + + /// + /// When the player plays the Pokéathlon over wireless play the default drinks will be replaced with the drinks of the opposing player. + /// The price of the drinks are determined on how famous the Trainers selling them are. + /// + /// + /// + /// + public uint Fame { get => ReadUInt32LittleEndian(Data[0x70..]); set => WriteUInt32LittleEndian(Data[0x70..], Math.Min(MaxFame, value)); } + + /// + /// Retrieves the value of a particular statistic for Data Card purposes, based on the provided stat identifier. + /// + public uint GetDataCardStat(DataCard4 stat) => stat switch + { + DataCard4.PlacedFirst => this.PlacedFirst, + DataCard4.PlacedLast => this.PlacedLast, + DataCard4.Dashed => this.Dashed, + DataCard4.Jumped => this.Jumped, + DataCard4.FirstHurdle => this[PokeathlonEvent4.HurdleDash], + DataCard4.FirstRelay => this[PokeathlonEvent4.RelayRun], + DataCard4.FirstPennant => this[PokeathlonEvent4.PennantCapture], + DataCard4.FirstBlockSmash => this[PokeathlonEvent4.BlockSmash], + DataCard4.FirstDiscCatch => this[PokeathlonEvent4.DiscCatch], + DataCard4.FirstSnowThrow => this[PokeathlonEvent4.SnowThrow], + DataCard4.PointsAcquired => this.Acquired, + DataCard4.Failed => this.Failed, + DataCard4.SelfImpeded => this.SelfImpeded, + DataCard4.Tackled => this.Tackled, + DataCard4.FellDown => this.FellDown, + DataCard4.FirstRingDrop => this[PokeathlonEvent4.RingDrop], + DataCard4.FirstLampJump => this[PokeathlonEvent4.LampJump], + DataCard4.FirstCirclePush => this[PokeathlonEvent4.CirclePush], + DataCard4.ConnectionFirst => this.ConnectionFirst, + DataCard4.ConnectionLast => this.ConnectionLast, + DataCard4.EventFirst => this.TotalEventFirst, // aggregate event first-place total is not stored, but can be calculated by summing per-event counters + DataCard4.EventLast => this.TotalEventLast, + DataCard4.Switched => this.Switched, + DataCard4.FirstGoalRoll => this[PokeathlonEvent4.GoalRoll], + DataCard4.BonusesEarned => this.BonusesEarned, + DataCard4.Instructions => this.Instructions, + DataCard4.TimeSpent => this.TimeSpent, + _ => throw new ArgumentOutOfRangeException(nameof(stat), stat, null), + }; + + /// + /// Updates the value of a particular statistic for Data Card purposes, based on the provided stat identifier. + /// + public void SetDataCardStat(DataCard4 stat, uint value) + { + value = Math.Min(MaxStat, value); + switch (stat) + { + case DataCard4.PlacedFirst: this.PlacedFirst = value; break; + case DataCard4.PlacedLast: this.PlacedLast = value; break; + case DataCard4.Dashed: this.Dashed = value; break; + case DataCard4.Jumped: this.Jumped = value; break; + case DataCard4.FirstHurdle: this[PokeathlonEvent4.HurdleDash] = value; break; + case DataCard4.FirstRelay: this[PokeathlonEvent4.RelayRun] = value; break; + case DataCard4.FirstPennant: this[PokeathlonEvent4.PennantCapture] = value; break; + case DataCard4.FirstBlockSmash: this[PokeathlonEvent4.BlockSmash] = value; break; + case DataCard4.FirstDiscCatch: this[PokeathlonEvent4.DiscCatch] = value; break; + case DataCard4.FirstSnowThrow: this[PokeathlonEvent4.SnowThrow] = value; break; + case DataCard4.PointsAcquired: this.Acquired = value; break; + case DataCard4.Failed: this.Failed = value; break; + case DataCard4.SelfImpeded: this.SelfImpeded = value; break; + case DataCard4.Tackled: this.Tackled = value; break; + case DataCard4.FellDown: this.FellDown = value; break; + case DataCard4.FirstRingDrop: this[PokeathlonEvent4.RingDrop] = value; break; + case DataCard4.FirstLampJump: this[PokeathlonEvent4.LampJump] = value; break; + case DataCard4.FirstCirclePush: this[PokeathlonEvent4.CirclePush] = value; break; + case DataCard4.ConnectionFirst: this.ConnectionFirst = value; break; + case DataCard4.ConnectionLast: this.ConnectionLast = value; break; + case DataCard4.EventFirst: + return; // skip, not going to spread the count across all 10 events + case DataCard4.EventLast: this.TotalEventLast = value; break; + case DataCard4.Switched: this.Switched = value; break; + case DataCard4.FirstGoalRoll: this[PokeathlonEvent4.GoalRoll] = value; break; + case DataCard4.BonusesEarned: this.BonusesEarned = value; break; + case DataCard4.Instructions: this.Instructions = value; break; + case DataCard4.TimeSpent: this.TimeSpent = value; break; + default: throw new ArgumentOutOfRangeException(nameof(stat), stat, null); + } + } + + public static ReadOnlySpan FameInclusiveThreshold => [10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000]; + + /// + /// Used to evaluate the fame level of a given trainer, ratcheting up/down based on how much fame has been aggregated. + /// + /// Player fame value + /// Value [0,12] + public static int GetFameLevel(uint value) + { + int level = 0; + foreach (var threshold in FameInclusiveThreshold) + { + if (value <= threshold) + break; + level++; + } + return level; // 12 max + } + + // disassembly also calculates fame for NPCs by summing all performance stats of their team, then (sum/3)-8, clamped [0,12]. +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonMedalManager4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonMedalManager4.cs new file mode 100644 index 000000000..2da8d2ce3 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonMedalManager4.cs @@ -0,0 +1,66 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Stores a bitflag medal completion state for all species. +/// +/// +/// for bitflag indexes. +/// +public struct PokeathlonMedalManager4(Memory Raw) +{ + public const int SIZE = 493; // 1-indexed species [Bulbasaur..Arceus] + public const byte MaxMedalBits = 0b11111; // 5 courses, 5 bits per species + + public Span Data => Raw.Span; + + /// + /// Retrieves the medal bits for the given species, where each bit represents whether a medal for a particular course has been obtained or not. + /// + public byte GetMedal(ushort species) + { + species--; + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4); + return Data[species]; + } + + /// + /// Updates the medal bits for the given species. + /// + public void SetMedal(ushort species, byte medalBits) + { + species--; + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4); + Data[species] = medalBits; + } + + /// + /// Awards the provided bit(s) to the species. + /// + public void AwardMedal(ushort species, byte medalBit) + { + species--; + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4); + Data[species] |= medalBit; + } + + /// + /// Awards all medals to all species (complete). + /// + /// Medal value to set to every species entry. + public void SetAllMedals(byte medalBits = MaxMedalBits) => Data[..SIZE].Fill(medalBits); + + /// + /// Removes all medals from all species (resets progress). + /// + public void Clear() => SetAllMedals(0); + + public uint GetTotalCount() + { + uint result = 0; + foreach (var bits in Data[..SIZE]) + result += (uint)System.Numerics.BitOperations.PopCount((uint)bits & MaxMedalBits); + return result; + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonParticipant4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonParticipant4.cs new file mode 100644 index 000000000..0475a159f --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonParticipant4.cs @@ -0,0 +1,27 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +public struct PokeathlonParticipant4(Memory Raw) : ISpeciesForm, ITrainerID32, IFixedGender, IShiny +{ + public const int SIZE = 0xC; + + private Span Data => Raw.Span; + + private uint Packed { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, value); } + public ushort Species { get => (ushort)(Packed & 0x1FF); set => Packed = (Packed & ~0x1FFu) | ((uint)value & 0x1FF); } + public byte Form { get => (byte)((Packed >> 9) & 0x1F); set => Packed = (Packed & ~(0x1Fu << 9)) | (((uint)value & 0x1F) << 9); } + public byte Gender { get => (byte)((Packed >> 14) & 0x3); set => Packed = (Packed & ~(0x3u << 14)) | (((uint)value & 0x3) << 14); } + public bool IsShiny { get => ((Packed >> 16) & 0x1) != 0; set => Packed = (Packed & ~(0x1u << 16)) | ((value ? 1u : 0u) << 16); } + // remainder of bits unused + + /// + public uint EncryptionConstant { get => ReadUInt32LittleEndian(Data[4..]); set => WriteUInt32LittleEndian(Data[4..], value); } + + /// + public uint ID32 { get => ReadUInt32LittleEndian(Data[8..]); set => WriteUInt32LittleEndian(Data[8..], value); } + public ushort TID16 { get => ReadUInt16LittleEndian(Data[8..]); set => WriteUInt16LittleEndian(Data[8..], value); } + public ushort SID16 { get => ReadUInt16LittleEndian(Data[10..]); set => WriteUInt16LittleEndian(Data[10..], value); } + public TrainerIDFormat TrainerIDDisplayFormat => TrainerIDFormat.SixteenBit; +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonStat4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonStat4.cs new file mode 100644 index 000000000..a519d5353 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/PokeathlonStat4.cs @@ -0,0 +1,15 @@ +namespace PKHeX.Core; + +/// +/// Performance stats +/// +public enum PokeathlonStat4 : byte +{ + Speed = 0, + Power = 1, + Skill = 2, + Stamina = 3, + Jump = 4, + + Count = 5, +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/SpeciesForm10.cs b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/SpeciesForm10.cs new file mode 100644 index 000000000..65b19fe37 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/SpeciesForm10.cs @@ -0,0 +1,16 @@ +namespace PKHeX.Core; + +public record struct SpeciesForm10(ushort Value) : ISpeciesForm +{ + // 10 bits species + public ushort Species { get => (ushort)(Value & 0x3FF); set => Value = (ushort)((Value & ~0x3FF) | (value & 0x3FF)); } + public byte Form { get => (byte)((Value >> 10) & 0x3F); set => Value = (ushort)((Value & ~(0x3Fu << 10)) | ((((uint)value & 0x3F) << 10))); } + + /// + /// Useful sanity check. + /// + public bool IsValid => Value is 0 || (Species != 0 && PersonalTable.HGSS.IsPresentInGame(Species, Form)); + + public static implicit operator SpeciesForm10(ushort value) => new(value); + public static implicit operator ushort(SpeciesForm10 value) => value.Value; +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Seal4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Seal4.cs index dfc1518d8..853110340 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/Seal4.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/Seal4.cs @@ -6,100 +6,100 @@ namespace PKHeX.Core; /// 80 bytes, one for each seal. public enum Seal4 { - HeartA, - HeartB, - HeartC, - HeartD, - HeartE, - HeartF, + HeartA = 0, + HeartB = 1, + HeartC = 2, + HeartD = 3, + HeartE = 4, + HeartF = 5, - StarA, - StarB, - StarC, - StarD, - StarE, - StarF, + StarA = 6, + StarB = 7, + StarC = 8, + StarD = 9, + StarE = 10, + StarF = 11, - LineA, - LineB, - LineC, - LineD, + LineA = 12, + LineB = 13, + LineC = 14, + LineD = 15, - SmokeA, - SmokeB, - SmokeC, - SmokeD, + SmokeA = 16, + SmokeB = 17, + SmokeC = 18, + SmokeD = 19, - ElectricA, - ElectricB, - ElectricC, - ElectricD, + ElectricA = 20, + ElectricB = 21, + ElectricC = 22, + ElectricD = 23, - FoamyA, - FoamyB, - FoamyC, - FoamyD, + FoamyA = 24, + FoamyB = 25, + FoamyC = 26, + FoamyD = 27, - FireA, - FireB, - FireC, - FireD, + FireA = 28, + FireB = 29, + FireC = 30, + FireD = 31, - PartyA, - PartyB, - PartyC, - PartyD, + PartyA = 32, + PartyB = 33, + PartyC = 34, + PartyD = 35, - FloraA, - FloraB, - FloraC, - FloraD, - FloraE, - FloraF, + FloraA = 36, + FloraB = 37, + FloraC = 38, + FloraD = 39, + FloraE = 40, + FloraF = 41, - SongA, - SongB, - SongC, - SongD, - SongE, - SongF, - SongG, + SongA = 42, + SongB = 43, + SongC = 44, + SongD = 45, + SongE = 46, + SongF = 47, + SongG = 48, - LetterA, - LetterB, - LetterC, - LetterD, - LetterE, - LetterF, - LetterG, - LetterH, - LetterI, - LetterJ, - LetterK, - LetterL, - LetterM, - LetterN, - LetterO, - LetterP, - LetterQ, - LetterR, - LetterS, - LetterT, - LetterU, - LetterV, - LetterW, - LetterX, - LetterY, - LetterZ, + LetterA = 49, + LetterB = 50, + LetterC = 51, + LetterD = 52, + LetterE = 53, + LetterF = 54, + LetterG = 55, + LetterH = 56, + LetterI = 57, + LetterJ = 58, + LetterK = 59, + LetterL = 60, + LetterM = 61, + LetterN = 62, + LetterO = 63, + LetterP = 64, + LetterQ = 65, + LetterR = 66, + LetterS = 67, + LetterT = 68, + LetterU = 69, + LetterV = 70, + LetterW = 71, + LetterX = 72, + LetterY = 73, + LetterZ = 74, - Shock, - Mystery, + Shock = 75, + Mystery = 76, // Unreleased - Liquid, - Burst, - Twinkle, + Liquid = 77, + Burst = 78, + Twinkle = 79, - MAX, + MAX = 80, MAXLEGAL = Liquid, } diff --git a/PKHeX.Core/Saves/Substructures/Gen5/UnityTower5.cs b/PKHeX.Core/Saves/Substructures/Gen5/UnityTower5.cs index c9a42c978..63472272f 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/UnityTower5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/UnityTower5.cs @@ -1,4 +1,5 @@ using System; +using static PKHeX.Core.LocaleNDS5; namespace PKHeX.Core; @@ -9,44 +10,7 @@ public sealed class UnityTower5(SAV5 SAV, Memory raw) : SaveBlock(SA private const int UnityTowerFlagOffset = 0x345; private const int GeonetOffset = 0x348; - public const int CountryCount = 232; - private const int Japan = 105; - - private static ReadOnlySpan LegalCountries => - [ - 001, 002, 003, 006, 008, 009, 012, 013, 015, 016, 017, 018, 020, 021, 022, 023, - 025, 027, 028, 029, 031, 033, 034, 035, 036, 040, 042, 043, 045, 047, 048, 049, - 051, 053, 054, 058, 060, 061, 062, 063, 064, 071, 072, 073, 074, 076, 079, 080, - 081, 082, 083, 084, 085, 087, 088, 090, 091, 092, 093, 094, 095, 096, 098, 099, - 101, 102, 103, 105, 106, 109, 111, 115, 117, 118, 121, 125, 128, 130, 132, 134, - 138, 139, 141, 145, 147, 148, 149, 150, 151, 155, 156, 157, 160, 161, 163, 164, - 166, 167, 170, 173, 174, 181, 185, 186, 188, 189, 190, 191, 194, 195, 196, 198, - 199, 200, 201, 203, 205, 206, 210, 211, 215, 217, 218, 219, 220, 221, 222, 224, - 226, 227, - ]; - - public static byte GetSubregionCount(byte country) => country switch - { - 009 => 24, // Argentina - 012 => 8, // Australia - 028 => 27, // Brazil - 036 => 13, // Canada - 043 => 33, // China - 072 => 6, // Finland - 073 => 22, // France - 079 => 16, // Germany - 095 => 35, // India - 102 => 20, // Italy - 105 => 50, // Japan - 155 => 22, // Norway - 166 => 16, // Poland - 174 => 8, // Russian Federation - 195 => 17, // Spain - 200 => 22, // Sweden - 218 => 12, // United Kingdom - 220 => 51, // United States of America - _ => 0, - }; + public static byte GetSubregionCount(byte country) => LocaleNDS5.GetSubregionCount(country); public bool GlobalFlag { get => Data[GeonetGlobalFlagOffset] != 0; set => Data[GeonetGlobalFlagOffset] = (byte)(value ? 1 : 0); } public bool UnityTowerFlag { get => Data[UnityTowerFlagOffset] != 0; set => Data[UnityTowerFlagOffset] = (byte)(value ? 1 : 0); } diff --git a/PKHeX.Core/Util/BinaryCodedDecimal.cs b/PKHeX.Core/Util/BinaryCodedDecimal.cs index 08e679965..7eb169346 100644 --- a/PKHeX.Core/Util/BinaryCodedDecimal.cs +++ b/PKHeX.Core/Util/BinaryCodedDecimal.cs @@ -20,6 +20,9 @@ private static byte GetLowestTuple(uint value) /// Returns a 32-bit signed integer converted from bytes in a Binary Coded Decimal format byte array. /// /// Input byte array to read from. + /// + /// Correctly defined Span length is required to read the appropriate amount of digits. + /// public static uint ReadUInt32BigEndian(ReadOnlySpan input) { uint result = 0; @@ -31,6 +34,9 @@ public static uint ReadUInt32BigEndian(ReadOnlySpan input) /// /// Writes the to the buffer. /// + /// + /// Correctly defined Span length is required to start writing at the highest digit. + /// public static void WriteUInt32BigEndian(Span data, uint value) { for (int i = data.Length - 1; i >= 0; i--, value /= 100) @@ -46,9 +52,7 @@ public static uint ReadUInt32LittleEndian(ReadOnlySpan input) return result; } - /// - /// Writes the to the buffer. - /// + /// public static void WriteUInt32LittleEndian(Span data, uint value) { for (int i = 0; i < data.Length; i++, value /= 100) diff --git a/PKHeX.Core/Util/ReflectUtil.cs b/PKHeX.Core/Util/ReflectUtil.cs index 617944f0f..42e015a8a 100644 --- a/PKHeX.Core/Util/ReflectUtil.cs +++ b/PKHeX.Core/Util/ReflectUtil.cs @@ -31,6 +31,12 @@ public static int CompareTo(this PropertyInfo pi, T obj, object value) return 0; } + /// + /// Attempts to set the requested property on to after converting it to the correct type. + /// + /// Property to set + /// Object to set property on + /// Value to set property to public static void SetValue(PropertyInfo pi, T obj, object value) { var c = ConvertValue(value, pi.PropertyType); diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen4/Pokeathlon/PokeathlonEventTrainer4Editor.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen4/Pokeathlon/PokeathlonEventTrainer4Editor.cs index 66eba08d6..0262be514 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen4/Pokeathlon/PokeathlonEventTrainer4Editor.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen4/Pokeathlon/PokeathlonEventTrainer4Editor.cs @@ -1,6 +1,6 @@ +using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Windows.Forms; using PKHeX.Core; @@ -11,29 +11,30 @@ public sealed partial class PokeathlonEventTrainer4Editor : UserControl public PokeathlonEventTrainer4Editor() { InitializeComponent(); - var languages = new List { new(GameInfo.Strings.specieslist[0], 0) }; - languages.AddRange(GameInfo.LanguageDataSource(4, EntityContext.Gen4).Where(z => z.Value is not 0)); + var available = GameInfo.LanguageDataSource(4, EntityContext.Gen4); + var languages = new List(available.Count + 1) { new(GameInfo.Strings.specieslist[0], 0) }; + languages.AddRange(available); CB_Language.InitializeBinding(); CB_Language.DataSource = new BindingSource(languages, string.Empty); } - public void LoadObject(PokeathlonEventTrainer4 entity) + public void LoadObject(PokeathlonEventTrainer4 trainer) { - TB_OT.Text = entity.OT; - TB_TID16.Text = entity.TID16.ToString(CultureInfo.InvariantCulture); - TB_SID16.Text = entity.SID16.ToString(CultureInfo.InvariantCulture); - CB_Language.SelectedValue = (int)entity.Language; + TB_OT.Text = trainer.OriginalTrainerName; + TB_TID16.Text = trainer.TID16.ToString("00000"); + TB_SID16.Text = trainer.SID16.ToString("00000"); + CB_Language.SelectedValue = (int)trainer.Language; } - public void SaveObject(PokeathlonEventTrainer4 entity) + public void SaveObject(PokeathlonEventTrainer4 trainer) { - entity.OT = TB_OT.Text; - entity.TID16 = ParseU16(TB_TID16.Text); - entity.SID16 = ParseU16(TB_SID16.Text); - entity.Language = (byte)WinFormsUtil.GetIndex(CB_Language); + trainer.OriginalTrainerName = TB_OT.Text; + trainer.TID16 = ParseU16(TB_TID16.Text); + trainer.SID16 = ParseU16(TB_SID16.Text); + trainer.Language = (byte)WinFormsUtil.GetIndex(CB_Language); } - private static ushort ParseU16(string text) + private static ushort ParseU16(ReadOnlySpan text) { if (!ushort.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) return 0; diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Geonet4.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Geonet4.cs index 4623df8c6..0eebe1ff8 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Geonet4.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Geonet4.cs @@ -38,11 +38,11 @@ private void InitializeDGVGeonet() Item_Point.InitializeBinding(); Item_Point.DataSource = pointList; - for (int i = 1; i <= Geonet4.CountryCount; i++) + for (int i = 1; i <= LocaleNDS4.CountryCount; i++) { var country = countryList[i].Value; var countryName = countryList[i].Text; - var subregionCount = Geonet4.GetSubregionCount((byte)country); + var subregionCount = LocaleNDS4.GetSubregionCount((byte)country); var subregionList = (subregionCount == 0) ? subregionListDefault : Util.GetCountryRegionList($"gen4_sr_{country:000}", Main.CurrentLanguage); if (subregionCount == 0) { diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_UnityTower.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_UnityTower.cs index 0882e5e80..049227cfa 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_UnityTower.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_UnityTower.cs @@ -40,7 +40,7 @@ private void InitializeDGVGeonet() Item_Point.InitializeBinding(); Item_Point.DataSource = pointList; - for (int i = 1; i <= UnityTower5.CountryCount; i++) + for (int i = 1; i <= LocaleNDS5.CountryCount; i++) { var country = countryList[i].Value; var countryName = countryList[i].Text; @@ -76,8 +76,8 @@ private void InitializeDGVUnityTower() { DGV_UnityTower.Rows.Clear(); - DGV_UnityTower.Rows.Add(UnityTower5.CountryCount); - for (int i = 0; i < UnityTower5.CountryCount; i++) + DGV_UnityTower.Rows.Add(LocaleNDS5.CountryCount); + for (int i = 0; i < LocaleNDS5.CountryCount; i++) { var row = DGV_UnityTower.Rows[i]; var country = countryList[i + 1].Value;