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;