Minor tweaks

No functional change, extracting some APIs and adding more xmldoc

Store potential learn source group as flags, might change later.
This commit is contained in:
Kurt
2026-05-30 18:51:39 -05:00
parent 801928d72c
commit 0b6a1accde
42 changed files with 1083 additions and 991 deletions

View File

@@ -18,7 +18,14 @@ public sealed class LegalMoveInfo
/// </summary>
/// <param name="move">Move to check if it can be learned</param>
/// <returns>True if it can learn the move</returns>
public bool CanLearn(ushort move) => AllowedMoves[move] != None;
public bool CanLearn(ushort move) => GetMoveSources(move) != None;
/// <summary>
/// Returns the sources that allow the provided move to be learned, or <see cref="None"/> if it cannot be learned.
/// </summary>
/// <param name="move">Move to check the sources for</param>
/// <returns>Sources that allow the move to be learned</returns>
public IndicatedSourceType GetMoveSources(ushort move) => AllowedMoves[move];
/// <summary>
/// Reloads the legality sources to permit the provided legal info.
@@ -57,58 +64,54 @@ public bool ReloadMoves(LegalityAnalysis la)
private static void ComputeEval(Span<IndicatedSourceType> type, ReadOnlySpan<bool> 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<IndicatedSourceType> type, IEncounterTemplate enc)
private static void AddEncounterMoves(Span<IndicatedSourceType> 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<IndicatedSourceType> result, IndicatedSourceType flag, ReadOnlySpan<ushort> moves)
{
foreach (var move in relearn.AsSpan())
foreach (var move in moves)
result[move] |= flag;
}
static void FlagIfNone(Span<IndicatedSourceType> result, IndicatedSourceType flag, ReadOnlySpan<ushort> 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,
}

View File

@@ -39,12 +39,6 @@ namespace PKHeX.Core;
/// <returns>True if any move is above the maximum; otherwise, false.</returns>
public bool AnyAbove(ushort max) => Move1 > max || Move2 > max || Move3 > max || Move4 > max;
/// <summary>
/// Returns the moveset as an array of four move IDs.
/// </summary>
/// <returns>An array containing the four move IDs.</returns>
public ushort[] ToArray() => [Move1, Move2, Move3, Move4];
/// <summary>
/// Gets a read-only span view of the moveset's four move IDs.
/// </summary>
@@ -176,4 +170,12 @@ public void FlagMoves(Span<bool> result)
result[Move3] = true;
result[Move4] = true;
}
public void FlagMoves(Span<IndicatedSourceType> result, IndicatedSourceType value)
{
result[Move1] |= value;
result[Move2] |= value;
result[Move3] |= value;
result[Move4] |= value;
}
}

View File

@@ -109,14 +109,8 @@ private static void GetAllMovesInternal(Span<bool> result, PKM pk, EvoCriteria e
private static void FlagEncounterMoves(IEncounterTemplate enc, Span<bool> 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);
}
}

View File

@@ -0,0 +1,49 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Provides information for Geonet/Battle Revolution player location information.
/// </summary>
/// <remarks>These values were specific to the NDS games (Generation 4)</remarks>
public static class LocaleNDS4
{
public const int CountryCount = 233;
public const int Japan = 103;
public static ReadOnlySpan<byte> 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,
};
}

View File

@@ -0,0 +1,49 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Provides information for Unity Tower player location information.
/// </summary>
/// <remarks>These values were specific to the NDS games (Generation 5)</remarks>
public static class LocaleNDS5
{
public const int CountryCount = 232;
public const int Japan = 105;
public static ReadOnlySpan<byte> 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,
};
}

View File

@@ -337,14 +337,17 @@ public static bool TryMakePKMCompatible(PKM pk, PKM target, out EntityConverterR
/// <summary>
/// Checks if a <see cref="GBPKM"/> is incompatible with the Generation 1/2 destination environment.
/// </summary>
/// <param name="pk">Target type PKM with misc properties accessible for checking.</param>
/// <param name="destJapanese">Whether the destination environment is Japanese</param>
/// <param name="srcJapanese">Whether the source PKM is Japanese</param>
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;
}
}

View File

@@ -198,7 +198,7 @@ public static bool IsChampions(ReadOnlySpan<int> evs)
}
/// <summary>
/// Assessment of the total EVs, compared to the maximum allowed.
/// Assessment of the total EVs (Gen3+), compared to the maximum allowed.
/// </summary>
public enum EffortValueGrade
{

View File

@@ -12,7 +12,10 @@ public static class EntityBlank
/// </summary>
/// <param name="type">Type of <see cref="PKM"/> instance desired.</param>
/// <returns>New instance of a blank <see cref="PKM"/> object.</returns>
public static PKM GetBlank(Type type) => type.Name switch
public static PKM GetBlank(Type type) => GetBlank(type.Name);
/// <inheritdoc cref="GetBlank(Type)"/>
public static PKM GetBlank(ReadOnlySpan<char> 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),
};
/// <summary>

View File

@@ -49,12 +49,17 @@ SIZE_5PARTY or
/// </remarks>
public static bool IsPresentSAV4Ranch(ReadOnlySpan<byte> data) => IsPresent(data) && ReadUInt32BigEndian(data) != 0x28; // Species non-zero, ignore file end marker
/// <summary>
/// Checks the PID and species of the Gen4+ entity to determine if it is present.
/// </summary>
public static bool IsPresent(ReadOnlySpan<byte> 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;
}
/// <summary>

View File

@@ -1,615 +0,0 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Manages the Pokeathlon Data for <see cref="SAV4HGSS"/>
/// </summary>
public sealed class Pokeathlon4(Memory<byte> Raw) // 0xD9D4 within SAV4HGSS
{
public const int SIZE = 0xB80;
public Span<byte> 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
/// <summary>
/// Player's highest score for each of the ten individual events (after conversion to Athlete Points)
/// </summary>
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;
/// <summary>
/// Current points count accumulated, used in buying items from shops.
/// </summary>
public uint Points { get => ReadUInt32LittleEndian(Data[0xB74..]); set => WriteUInt32LittleEndian(Data[0xB74..], Math.Min(MaxPoints, value)); }
/// <summary>
/// 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).
/// </summary>
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
/// <summary>
/// Once-daily shop purchase flags for the Athlete Shop.
/// Since shops only have 12 items daily, only 12 bits are used.
/// </summary>
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
/// <summary>
/// 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).
/// </summary>
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<ushort> 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<byte> Raw)
{
public const int SIZE = 0x2C;
private Span<byte> 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>((uint)index, CountParticipant);
return new(Raw.Slice(8 + (index * PokeathlonParticipant4.SIZE), PokeathlonParticipant4.SIZE));
}
}
public struct PokeathlonParticipant4(Memory<byte> Raw) : ISpeciesForm, ITrainerID32, IFixedGender, IShiny
{
public const int SIZE = 0xC;
private Span<byte> 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
/// <summary> <see cref="PKM.EncryptionConstant"/> </summary>
public uint EncryptionConstant { get => ReadUInt32LittleEndian(Data[4..]); set => WriteUInt32LittleEndian(Data[4..], value); }
/// <summary> <see cref="PKM.ID32"/> </summary>
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;
}
/// <summary>
/// Stores a bitflag medal completion state for all species.
/// </summary>
/// <remarks>
/// <see cref="PokeathlonStat4"/> for bitflag indexes.
/// </remarks>
public struct PokeathlonMedalManager4(Memory<byte> Raw)
{
public const int SIZE = 493; // 1-indexed species [Bulbasaur..Arceus]
public const byte MaxMedalBits = 0b11111; // 5 courses, 5 bits per species
public Span<byte> Data => Raw.Span;
/// <summary>
/// Retrieves the medal bits for the given species, where each bit represents whether a medal for a particular course has been obtained or not.
/// </summary>
public byte GetMedal(ushort species)
{
species--;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4);
return Data[species];
}
/// <summary>
/// Updates the medal bits for the given species.
/// </summary>
public void SetMedal(ushort species, byte medalBits)
{
species--;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4);
Data[species] = medalBits;
}
/// <summary>
/// Awards the provided bit(s) to the species.
/// </summary>
public void AwardMedal(ushort species, byte medalBit)
{
species--;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4);
Data[species] |= medalBit;
}
/// <summary>
/// Awards all medals to all species (complete).
/// </summary>
/// <param name="medalBits">Medal value to set to every species entry.</param>
public void SetAllMedals(byte medalBits = MaxMedalBits) => Data[..SIZE].Fill(medalBits);
/// <summary>
/// Removes all medals from all species (resets progress).
/// </summary>
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<byte> Raw)
{
public const int SIZE = 0x2C;
public const uint MaxAttempts = 9_999_999;
public const uint MaxRecord = 5;
public Span<byte> 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<byte> Raw)
{
public const int SIZE = 0xA4;
public const uint MaxTrainer = PokeathlonEventData4.MaxRecord;
public Span<byte> Data => Raw.Span;
public PokeathlonEventData4 Inner => new(Raw.Slice(0 * PokeathlonEventData4.SIZE, PokeathlonEventData4.SIZE));
/// <summary>
/// Correlated to the <see cref="Inner"/> indexed records.
/// </summary>
public PokeathlonEventTrainer4 GetTrainer(int index)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, MaxTrainer);
return new(Raw.Slice(0x2C + (index * PokeathlonEventTrainer4.SIZE), PokeathlonEventTrainer4.SIZE));
}
}
public struct PokeathlonEventTrainer4(Memory<byte> Raw) : ITrainerID32
{
public const int SIZE = 0x18;
public Span<byte> 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<byte> 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);
}
}
/// <summary>
/// Event record storage for a given event type.
/// </summary>
/// <remarks>
/// <see cref="PokeathlonEvent4"/>
/// </remarks>
/// <param name="Raw"></param>
public struct PokeathlonEventRecord4(Memory<byte> Raw)
{
public const int SIZE = 8;
public Span<byte> Data => Raw.Span;
/// <summary>
/// Stores the record for the particular event. The meaning of this value depends on the event type.
/// </summary>
/// <remarks>
/// Hurdle Dash: Time in frames (lower is better).
/// </remarks>
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))); }
/// <summary>
/// Useful sanity check.
/// </summary>
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<byte> Raw)
{
public const int SIZE = 0x74;
public Span<byte> Data => Raw.Span;
public const uint MaxPlay = 59_999;
public const uint MaxStat = 9_999_999;
public const uint MaxFame = ushort.MaxValue;
/// <summary> Time Spent in Pokéathlon, in minutes </summary>
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)); }
/// <summary>
/// Bonuses Earned
/// </summary>
public uint BonusesEarned { get => ReadUInt32LittleEndian(Data[0x10..]); set => WriteUInt32LittleEndian(Data[0x10..], Math.Min(MaxStat, value)); }
/// <summary>
/// Pokémon Instructions
/// </summary>
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)); }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <see cref="Aprijuice5.Fame"/>
/// </remarks>
public uint Fame { get => ReadUInt32LittleEndian(Data[0x70..]); set => WriteUInt32LittleEndian(Data[0x70..], Math.Min(MaxFame, value)); }
/// <summary>
/// Retrieves the value of a particular statistic for Data Card purposes, based on the provided stat identifier.
/// </summary>
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),
};
/// <summary>
/// Updates the value of a particular statistic for Data Card purposes, based on the provided stat identifier.
/// </summary>
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<ushort> FameInclusiveThreshold => [10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000];
/// <summary>
/// Used to evaluate the fame level of a given trainer, ratcheting up/down based on how much fame has been aggregated.
/// </summary>
/// <param name="value">Player fame value</param>
/// <returns>Value [0,12]</returns>
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<byte> Raw)
{
public Span<byte> Data => Raw.Span;
/// <summary>
/// The price of drinks are determined by how famous the Trainers selling them are.
/// </summary>
/// <remarks>
/// <see cref="PokeathlonGlobalCounters4.Fame"/>
/// </remarks>
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.
}
/// <summary>
/// Performance stats
/// </summary>
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,
}
/// <summary>
/// Data Card indexes and the stats they retrieve in <see cref="PokeathlonGlobalCounters4"/>.
/// </summary>
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,
};

View File

@@ -638,7 +638,7 @@ public void SetHallOfFameData(ReadOnlySpan<byte> value)
/// <summary> Only used in Emerald for storing the Battle Video. </summary>
public Memory<byte> GetFinalExternalData() => Buffer.Slice(0x1F000, SIZE_SECTOR_USED);
public bool IsCorruptPokedexFF() => MemoryMarshal.Read<ulong>(Small[0xAC..]) == ulong.MaxValue;
public bool IsCorruptPokedexFF() => BitConverter.ToUInt64(Small[0xAC..]) == ulong.MaxValue;
public sealed override void CopyChangesFrom(SaveFile sav)
{

View File

@@ -314,6 +314,9 @@ public string BirthDay
set => StringConverter4GC.SetStringUnicodeBR(value, BirthDayTrash);
}
/// <summary>
/// <see cref="LocaleNDS4.LegalCountries"/>
/// </summary>
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); }

View File

@@ -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

View File

@@ -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

View File

@@ -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<byte> 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);

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -0,0 +1,48 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct Aprijuice4(Memory<byte> Raw)
{
public Span<byte> Data => Raw.Span;
/// <summary>
/// The price of drinks are determined by how famous the Trainers selling them are.
/// </summary>
/// <remarks>
/// <see cref="PokeathlonGlobalCounters4.Fame"/>
/// </remarks>
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.
}

View File

@@ -0,0 +1,45 @@
namespace PKHeX.Core;
/// <summary>
/// Data Card indexes and the stats they retrieve in <see cref="PokeathlonGlobalCounters4"/>.
/// </summary>
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,
};

View File

@@ -0,0 +1,118 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Manages the Pokeathlon Data for <see cref="SAV4HGSS"/>
/// </summary>
public sealed class Pokeathlon4(Memory<byte> Raw) // 0xD9D4 within SAV4HGSS
{
public const int SIZE = 0xB80;
public Span<byte> 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
/// <summary>
/// Player's highest score for each of the ten individual events (after conversion to Athlete Points)
/// </summary>
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;
/// <summary>
/// Current points count accumulated, used in buying items from shops.
/// </summary>
public uint Points { get => ReadUInt32LittleEndian(Data[0xB74..]); set => WriteUInt32LittleEndian(Data[0xB74..], Math.Min(MaxPoints, value)); }
/// <summary>
/// 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).
/// </summary>
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
/// <summary>
/// Once-daily shop purchase flags for the Athlete Shop.
/// Since shops only have 12 items daily, only 12 bits are used.
/// </summary>
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
/// <summary>
/// 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).
/// </summary>
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<ushort> 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
}
}

View File

@@ -0,0 +1,24 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct PokeathlonConnection4(Memory<byte> Raw)
{
public const int SIZE = 0xA4;
public const uint MaxTrainer = PokeathlonEventData4.MaxRecord;
public Span<byte> Data => Raw.Span;
public PokeathlonEventData4 Inner => new(Raw.Slice(0 * PokeathlonEventData4.SIZE, PokeathlonEventData4.SIZE));
/// <summary>
/// Correlated to the <see cref="Inner"/> indexed records.
/// </summary>
public PokeathlonEventTrainer4 GetTrainer(int index)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, MaxTrainer);
return new(Raw.Slice(0x2C + (index * PokeathlonEventTrainer4.SIZE), PokeathlonEventTrainer4.SIZE));
}
}

View File

@@ -0,0 +1,24 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct PokeathlonCourseRecord4(Memory<byte> Raw)
{
public const int SIZE = 0x2C;
private Span<byte> 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>((uint)index, CountParticipant);
return new(Raw.Slice(8 + (index * PokeathlonParticipant4.SIZE), PokeathlonParticipant4.SIZE));
}
}

View File

@@ -0,0 +1,20 @@
namespace PKHeX.Core;
/// <summary>
/// Enumeration of the 10 different Pokeathlon events in Gen 4, used for indexing into the event data structures.
/// </summary>
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,
}

View File

@@ -0,0 +1,21 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct PokeathlonEventData4(Memory<byte> Raw)
{
public const int SIZE = 0x2C;
public const uint MaxAttempts = 9_999_999;
public const uint MaxRecord = 5;
public Span<byte> 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)); }
}

View File

@@ -0,0 +1,31 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Event record storage for a given event type.
/// </summary>
/// <remarks>
/// <see cref="PokeathlonEvent4"/>
/// </remarks>
/// <param name="Raw"></param>
public struct PokeathlonEventRecord4(Memory<byte> Raw)
{
public const int SIZE = 8;
public Span<byte> Data => Raw.Span;
/// <summary>
/// Stores the record for the particular event. The meaning of this value depends on the event type.
/// </summary>
/// <remarks>
/// Hurdle Dash: Time in frames (lower is better).
/// </remarks>
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); }
}

View File

@@ -0,0 +1,25 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct PokeathlonEventTrainer4(Memory<byte> Raw) : ITrainerID32
{
public const int SIZE = 0x18;
public Span<byte> 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<byte> 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);
}
}

View File

@@ -0,0 +1,178 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct PokeathlonGlobalCounters4(Memory<byte> Raw)
{
public const int SIZE = 0x74;
public Span<byte> Data => Raw.Span;
public const uint MaxPlay = 59_999;
public const uint MaxStat = 9_999_999;
public const uint MaxFame = ushort.MaxValue;
/// <summary> Time Spent in Pokéathlon, in minutes </summary>
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)); }
/// <summary>
/// Bonuses Earned
/// </summary>
public uint BonusesEarned { get => ReadUInt32LittleEndian(Data[0x10..]); set => WriteUInt32LittleEndian(Data[0x10..], Math.Min(MaxStat, value)); }
/// <summary>
/// Pokémon Instructions
/// </summary>
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)); }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <see cref="Aprijuice5.Fame"/>
/// </remarks>
public uint Fame { get => ReadUInt32LittleEndian(Data[0x70..]); set => WriteUInt32LittleEndian(Data[0x70..], Math.Min(MaxFame, value)); }
/// <summary>
/// Retrieves the value of a particular statistic for Data Card purposes, based on the provided stat identifier.
/// </summary>
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),
};
/// <summary>
/// Updates the value of a particular statistic for Data Card purposes, based on the provided stat identifier.
/// </summary>
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<ushort> FameInclusiveThreshold => [10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000];
/// <summary>
/// Used to evaluate the fame level of a given trainer, ratcheting up/down based on how much fame has been aggregated.
/// </summary>
/// <param name="value">Player fame value</param>
/// <returns>Value [0,12]</returns>
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].
}

View File

@@ -0,0 +1,66 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Stores a bitflag medal completion state for all species.
/// </summary>
/// <remarks>
/// <see cref="PokeathlonStat4"/> for bitflag indexes.
/// </remarks>
public struct PokeathlonMedalManager4(Memory<byte> Raw)
{
public const int SIZE = 493; // 1-indexed species [Bulbasaur..Arceus]
public const byte MaxMedalBits = 0b11111; // 5 courses, 5 bits per species
public Span<byte> Data => Raw.Span;
/// <summary>
/// Retrieves the medal bits for the given species, where each bit represents whether a medal for a particular course has been obtained or not.
/// </summary>
public byte GetMedal(ushort species)
{
species--;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4);
return Data[species];
}
/// <summary>
/// Updates the medal bits for the given species.
/// </summary>
public void SetMedal(ushort species, byte medalBits)
{
species--;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4);
Data[species] = medalBits;
}
/// <summary>
/// Awards the provided bit(s) to the species.
/// </summary>
public void AwardMedal(ushort species, byte medalBit)
{
species--;
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(species, (ushort)Legal.MaxSpeciesID_4);
Data[species] |= medalBit;
}
/// <summary>
/// Awards all medals to all species (complete).
/// </summary>
/// <param name="medalBits">Medal value to set to every species entry.</param>
public void SetAllMedals(byte medalBits = MaxMedalBits) => Data[..SIZE].Fill(medalBits);
/// <summary>
/// Removes all medals from all species (resets progress).
/// </summary>
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;
}
}

View File

@@ -0,0 +1,27 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public struct PokeathlonParticipant4(Memory<byte> Raw) : ISpeciesForm, ITrainerID32, IFixedGender, IShiny
{
public const int SIZE = 0xC;
private Span<byte> 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
/// <summary> <see cref="PKM.EncryptionConstant"/> </summary>
public uint EncryptionConstant { get => ReadUInt32LittleEndian(Data[4..]); set => WriteUInt32LittleEndian(Data[4..], value); }
/// <summary> <see cref="PKM.ID32"/> </summary>
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;
}

View File

@@ -0,0 +1,15 @@
namespace PKHeX.Core;
/// <summary>
/// Performance stats
/// </summary>
public enum PokeathlonStat4 : byte
{
Speed = 0,
Power = 1,
Skill = 2,
Stamina = 3,
Jump = 4,
Count = 5,
}

View File

@@ -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))); }
/// <summary>
/// Useful sanity check.
/// </summary>
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;
}

View File

@@ -6,100 +6,100 @@ namespace PKHeX.Core;
/// <remarks>80 bytes, one for each seal.</remarks>
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,
}

View File

@@ -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<byte> raw) : SaveBlock<SAV5>(SA
private const int UnityTowerFlagOffset = 0x345;
private const int GeonetOffset = 0x348;
public const int CountryCount = 232;
private const int Japan = 105;
private static ReadOnlySpan<byte> 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); }

View File

@@ -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.
/// </summary>
/// <param name="input">Input byte array to read from.</param>
/// <remarks>
/// Correctly defined Span length is required to read the appropriate amount of digits.
/// </remarks>
public static uint ReadUInt32BigEndian(ReadOnlySpan<byte> input)
{
uint result = 0;
@@ -31,6 +34,9 @@ public static uint ReadUInt32BigEndian(ReadOnlySpan<byte> input)
/// <summary>
/// Writes the <see cref="value"/> to the <see cref="data"/> buffer.
/// </summary>
/// <remarks>
/// Correctly defined Span length is required to start writing at the highest digit.
/// </remarks>
public static void WriteUInt32BigEndian(Span<byte> data, uint value)
{
for (int i = data.Length - 1; i >= 0; i--, value /= 100)
@@ -46,9 +52,7 @@ public static uint ReadUInt32LittleEndian(ReadOnlySpan<byte> input)
return result;
}
/// <summary>
/// Writes the <see cref="value"/> to the <see cref="data"/> buffer.
/// </summary>
/// <inheritdoc cref="WriteUInt32BigEndian"/>
public static void WriteUInt32LittleEndian(Span<byte> data, uint value)
{
for (int i = 0; i < data.Length; i++, value /= 100)

View File

@@ -31,6 +31,12 @@ public static int CompareTo<T>(this PropertyInfo pi, T obj, object value)
return 0;
}
/// <summary>
/// Attempts to set the requested property on <see cref="obj"/> to <see cref="value"/> after converting it to the correct type.
/// </summary>
/// <param name="pi">Property to set</param>
/// <param name="obj">Object to set property on</param>
/// <param name="value">Value to set property to</param>
public static void SetValue<T>(PropertyInfo pi, T obj, object value)
{
var c = ConvertValue(value, pi.PropertyType);

View File

@@ -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<ComboItem> { 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<ComboItem>(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<char> text)
{
if (!ushort.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
return 0;

View File

@@ -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)
{

View File

@@ -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;