using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core
{
///
/// Common logic for data providing and manipulation.
///
public static class PKX
{
internal static readonly PersonalTable Personal = PersonalTable.LA;
public const int Generation = 8;
private static readonly HashSet Sizes = new()
{
PokeCrypto.SIZE_1JLIST, PokeCrypto.SIZE_1ULIST,
PokeCrypto.SIZE_2ULIST, PokeCrypto.SIZE_2JLIST, PokeCrypto.SIZE_2STADIUM,
PokeCrypto.SIZE_3STORED, PokeCrypto.SIZE_3PARTY,
PokeCrypto.SIZE_3CSTORED, PokeCrypto.SIZE_3XSTORED,
PokeCrypto.SIZE_4STORED, PokeCrypto.SIZE_4PARTY,
PokeCrypto.SIZE_5PARTY,
PokeCrypto.SIZE_6STORED, PokeCrypto.SIZE_6PARTY,
PokeCrypto.SIZE_8STORED, PokeCrypto.SIZE_8PARTY,
PokeCrypto.SIZE_8ASTORED, PokeCrypto.SIZE_8APARTY,
};
///
/// Determines if the given length is valid for a .
///
/// Data length of the file/array.
/// A indicating whether or not the length is valid for a .
public static bool IsPKM(long len) => Sizes.Contains((int)len);
///
/// Gets randomized EVs for a given generation format
///
/// Generation specific formatting option
/// Array containing randomized EVs (H/A/B/S/C/D)
public static int[] GetRandomEVs(int generation = Generation)
{
var rnd = Util.Rand;
if (generation > 2)
{
var evs = new int[6];
do
{
int max = 510;
for (int i = 0; i < evs.Length - 1; i++)
max -= evs[i] = (byte)Math.Min(rnd.Next(Math.Min(300, max)), 252);
evs[5] = max;
} while (evs[5] > 252);
Util.Shuffle(evs.AsSpan());
return evs;
}
else
{
var evs = new int[6];
for (int i = 0; i < evs.Length; i++)
evs[i] = rnd.Next(ushort.MaxValue + 1);
return evs;
}
}
///
/// Translates a Gender string to Gender integer.
///
/// Gender string
/// Gender integer
public static int GetGenderFromString(string s)
{
if (s.Length != 1)
return 2;
return GetGenderFromChar(s[0]);
}
private static int GetGenderFromChar(char c) => c switch
{
'♂' or 'M' => 0,
'♀' or 'F' => 1,
_ => 2,
};
///
/// Gets the nature modification values and checks if they are equal.
///
/// Nature
/// Increased stat
/// Decreased stat
/// True if nature modification values are equal or the Nature is out of range.
public static bool GetNatureModification(int nature, out int incr, out int decr)
{
incr = (nature / 5) + 1;
decr = (nature % 5) + 1;
return incr == decr || nature >= 25; // invalid
}
///
/// Updates stats according to the specified nature.
///
/// Current stats to amplify if appropriate
/// Nature
public static void ModifyStatsForNature(ushort[] stats, int nature)
{
if (GetNatureModification(nature, out int incr, out int decr))
return;
stats[incr] *= 11; stats[incr] /= 10;
stats[decr] *= 9; stats[decr] /= 10;
}
///
/// Gets a random PID according to specifications.
///
/// RNG to use
/// National Dex ID
/// Current Gender
/// Origin Generation
/// Nature
/// Form
/// Current PID
/// Used to retain ability bits.
/// Rerolled PID.
public static uint GetRandomPID(Random rnd, int species, int gender, int origin, int nature, int form, uint oldPID)
{
// Gen6+ (and VC) PIDs do not tie PID to Nature/Gender/Ability
if (origin >= 24)
return Util.Rand32(rnd);
// Below logic handles Gen3-5.
int gt = Personal[species].Gender;
bool g34 = origin <= 15;
uint abilBitVal = g34 ? oldPID & 0x0000_0001 : oldPID & 0x0001_0000;
bool g3unown = origin <= 5 && species == (int)Species.Unown;
bool singleGender = PersonalInfo.IsSingleGender(gt); // single gender, skip gender check
while (true) // Loop until we find a suitable PID
{
uint pid = Util.Rand32(rnd);
// Gen 3/4: Nature derived from PID
if (g34 && pid%25 != nature)
continue;
// Gen 3 Unown: Letter/form derived from PID
if (g3unown)
{
var pidLetter = GetUnownForm(pid);
if (pidLetter != form)
continue;
}
else if (g34)
{
if (abilBitVal != (pid & 0x0000_0001)) // keep ability bits
continue;
}
else
{
if (abilBitVal != (pid & 0x0001_0000)) // keep ability bits
continue;
}
if (singleGender) // Set Gender(less)
return pid; // PID can be anything
// Gen 3/4/5: Gender derived from PID
if (gender == GetGenderFromPIDAndRatio(pid, gt))
return pid;
}
}
///
/// Gets the Unown Forme ID from PID.
///
/// Personality ID
/// Should only be used for 3rd Generation origin specimens.
///
public static int GetUnownForm(uint pid)
{
var value = (pid & 0x3000000) >> 18 | (pid & 0x30000) >> 12 | (pid & 0x300) >> 6 | (pid & 0x3);
return (int)(value % 28);
}
///
/// Gets the gender ID of the species based on the Personality ID.
///
/// National Dex ID.
/// Personality ID.
/// Gender ID (0/1/2)
/// This method should only be used for Generations 3-5 origin.
public static int GetGenderFromPID(int species, uint pid)
{
int gt = Personal[species].Gender;
return GetGenderFromPIDAndRatio(pid, gt);
}
public static int GetGenderFromPIDAndRatio(uint pid, int gr) => gr switch
{
PersonalInfo.RatioMagicGenderless => 2,
PersonalInfo.RatioMagicFemale => 1,
PersonalInfo.RatioMagicMale => 0,
_ => (pid & 0xFF) < gr ? 1 : 0,
};
internal const string ExtensionPB7 = "pb7";
internal const string ExtensionPB8 = "pb8";
internal const string ExtensionPA8 = "pa8";
///
/// Gets an array of valid file extensions.
///
/// Maximum Generation to permit
/// Valid file extensions.
public static string[] GetPKMExtensions(int maxGeneration = Generation)
{
var result = new List();
int min = maxGeneration is <= 2 or >= 7 ? 1 : 3;
for (int i = min; i <= maxGeneration; i++)
result.Add($"pk{i}");
if (maxGeneration >= 3)
{
result.Add("ck3"); // colosseum
result.Add("xk3"); // xd
}
if (maxGeneration >= 4)
result.Add("bk4"); // battle revolution
if (maxGeneration >= 7)
result.Add(ExtensionPB7); // let's go
if (maxGeneration >= 8)
result.Add(ExtensionPB8); // Brilliant Diamond & Shining Pearl
if (maxGeneration >= 8)
result.Add(ExtensionPA8); // Legends: Arceus
return result.ToArray();
}
///
/// Roughly detects the PKM format from the file's extension.
///
/// File extension.
/// Preference if not a valid extension, usually the highest acceptable format.
/// Format hint that the file is.
public static int GetPKMFormatFromExtension(string ext, int prefer)
{
if (string.IsNullOrEmpty(ext))
return prefer;
return GetPKMFormatFromExtension(ext[^1], prefer);
}
///
/// Roughly detects the PKM format from the file's extension.
///
/// Last character of the file's extension.
/// Preference if not a valid extension, usually the highest acceptable format.
/// Format hint that the file is.
public static int GetPKMFormatFromExtension(char last, int prefer)
{
if (last is >= '1' and <= '9')
return last - '0';
return last == 'x' ? 6 : prefer;
}
internal static bool IsPKMPresentGB(ReadOnlySpan data) => data[0] != 0; // Species non-zero
internal static bool IsPKMPresentGC(ReadOnlySpan data) => ReadUInt16BigEndian(data) != 0; // Species non-zero
internal static bool IsPKMPresentGBA(ReadOnlySpan data) => (data[0x13] & 0xFB) == 2; // ignore egg flag, must be FlagHasSpecies.
internal static bool IsPKMPresent(ReadOnlySpan data)
{
if (ReadUInt32LittleEndian(data) != 0) // PID
return true;
ushort species = ReadUInt16LittleEndian(data[8..]);
return species != 0;
}
///
/// Gets a function that can check a byte array (at an offset) to see if a is possibly present.
///
///
/// Function that checks if a byte array (at an offset) has a present
public static Func GetFuncIsPKMPresent(PKM blank)
{
if (blank.Format >= 4)
return x => IsPKMPresent(x);
if (blank.Format <= 2)
return x => IsPKMPresentGB(x);
if (blank.Data.Length <= PokeCrypto.SIZE_3PARTY)
return x => IsPKMPresentGBA(x);
return x => IsPKMPresentGC(x);
}
///
/// Reorders (in place) the input array of stats to have the Speed value last rather than before the SpA/SpD stats.
///
/// Input array to reorder
/// Same array, reordered.
public static void ReorderSpeedLast(Span value)
{
var spe = value[3];
value[3] = value[4];
value[4] = value[5];
value[5] = spe;
}
}
}