mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-09-10 15:35:59 -05:00
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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
49
PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS4.cs
Normal file
49
PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS4.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
49
PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS5.cs
Normal file
49
PKHeX.Core/Legality/Restrictions/Locale/LocaleNDS5.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
48
PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Aprijuice4.cs
Normal file
48
PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Aprijuice4.cs
Normal 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.
|
||||
}
|
||||
45
PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/DataCard4.cs
Normal file
45
PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/DataCard4.cs
Normal 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,
|
||||
};
|
||||
118
PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Pokeathlon4.cs
Normal file
118
PKHeX.Core/Saves/Substructures/Gen4/Pokeathlon/Pokeathlon4.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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].
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user