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