using System; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; /// /// Pokédex structure used by games. /// public sealed class Zukan4 : ZukanBase { private readonly byte[] Data; private readonly int Offset; // General structure: u32 magic, 4*bitflags, u32 spinda, form flags, language flags, more form flags, upgrade flags /* 4 BitRegions with 0x40*8 bits * Region 0: Caught (Captured/Owned) flags * Region 1: Seen flags * Region 2: First Seen Gender * Region 3: Second Seen Gender * When setting a newly seen species (first time), we set the gender bit to both First and Second regions. * When setting an already-seen species, we set the Second region bit if the now-seen gender-bit is not equal to the first-seen bit. * 4 possible states: 00, 01, 10, 11 * 00 - 1Seen: Male Only * 01 - 2Seen: Male First, Female Second * 10 - 2Seen: Female First, Male Second * 11 - 1Seen: Female Only * assuming the species is seen, (bit1 ^ bit2) + 1 = genders in dex */ public const string GENDERLESS = "Genderless"; public const string MALE = "Male"; public const string FEMALE = "Female"; private const int SIZE_REGION = 0x40; private const int COUNT_REGION = 4; private const int OFS_SPINDA = sizeof(uint) + (COUNT_REGION * SIZE_REGION); private const int OFS_FORM1 = OFS_SPINDA + sizeof(uint); private bool HGSS => SAV is SAV4HGSS; private bool DP => SAV is SAV4DP; public Zukan4(SAV4 sav, int offset) : base(sav, offset) { Data = sav.General; Offset = offset; } public uint Magic { get => ReadUInt32LittleEndian(Data.AsSpan(Offset)); set => WriteUInt32LittleEndian(Data.AsSpan(Offset), value); } public override bool GetCaught(ushort species) => GetRegionFlag(0, species - 1); public override bool GetSeen(ushort species) => GetRegionFlag(1, species - 1); public int GetSeenGenderFirst(ushort species) => GetRegionFlag(2, species - 1) ? 1 : 0; public int GetSeenGenderSecond(ushort species) => GetRegionFlag(3, species - 1) ? 1 : 0; public bool GetSeenSingleGender(ushort species) => GetSeenGenderFirst(species) == GetSeenGenderSecond(species); private bool GetRegionFlag(int region, int index) { var ofs = Offset + 4 + (region * SIZE_REGION) + (index >> 3); return FlagUtil.GetFlag(Data, ofs, index); } public void SetCaught(ushort species, bool value = true) => SetRegionFlag(0, species - 1, value); public void SetSeen(ushort species, bool value = true) => SetRegionFlag(1, species - 1, value); public void SetSeenGenderFirst(ushort species, int value = 0) => SetRegionFlag(2, species - 1, value == 1); public void SetSeenGenderSecond(ushort species, int value = 0) => SetRegionFlag(3, species - 1, value == 1); private void SetRegionFlag(int region, int index, bool value) { var ofs = Offset + 4 + (region * SIZE_REGION) + (index >> 3); FlagUtil.SetFlag(Data, ofs, index, value); } public uint SpindaPID { get => ReadUInt32LittleEndian(Data.AsSpan(Offset + OFS_SPINDA)); set => WriteUInt32LittleEndian(Data.AsSpan(Offset), value); } public static string[] GetFormNames4Dex(ushort species) { string[] formNames = FormConverter.GetFormList(species, GameInfo.Strings.types, GameInfo.Strings.forms, Array.Empty(), EntityContext.Gen4); if (species == (int)Species.Pichu) formNames = new[] { MALE, FEMALE, formNames[1] }; // Spiky return formNames; } public const byte FORM_NONE = byte.MaxValue; public byte[] GetForms(ushort species) { const int brSize = 0x40; if (species == (int)Species.Deoxys) { uint val = (uint)(Data[Offset + 0x4 + (1 * brSize) - 1] | (Data[Offset + 0x4 + (2 * brSize) - 1] << 8)); return GetDexFormValues(val, 4, 4); } int FormOffset1 = Offset + 4 + (4 * brSize) + 4; switch (species) { case (int)Species.Shellos: // Shellos return GetDexFormValues(Data[FormOffset1 + 0], 1, 2); case (int)Species.Gastrodon: // Gastrodon return GetDexFormValues(Data[FormOffset1 + 1], 1, 2); case (int)Species.Burmy: // Burmy return GetDexFormValues(Data[FormOffset1 + 2], 2, 3); case (int)Species.Wormadam: // Wormadam return GetDexFormValues(Data[FormOffset1 + 3], 2, 3); case (int)Species.Unown: // Unown return Data.AsSpan(FormOffset1 + 4, 0x1C).ToArray(); } if (DP) return Array.Empty(); int PokeDexLanguageFlags = FormOffset1 + (HGSS ? 0x3C : 0x20); int FormOffset2 = PokeDexLanguageFlags + 0x1F4; return species switch { (int)Species.Rotom => GetDexFormValues(ReadUInt32LittleEndian(Data.AsSpan(FormOffset2)), 3, 6), (int)Species.Shaymin => GetDexFormValues(Data[FormOffset2 + 4], 1, 2), (int)Species.Giratina => GetDexFormValues(Data[FormOffset2 + 5], 1, 2), (int)Species.Pichu when HGSS => GetDexFormValues(Data[FormOffset2 + 6], 2, 3), _ => Array.Empty(), }; } public void SetForms(ushort species, ReadOnlySpan forms) { const int brSize = 0x40; switch (species) { case (int)Species.Deoxys: // Deoxys uint newval = SetDexFormValues(forms, 4, 4); Data[Offset + 0x4 + (1 * brSize) - 1] = (byte)(newval & 0xFF); Data[Offset + 0x4 + (2 * brSize) - 1] = (byte)((newval >> 8) & 0xFF); break; } int FormOffset1 = Offset + OFS_FORM1; switch (species) { case (int)Species.Shellos: // Shellos Data[FormOffset1 + 0] = (byte)SetDexFormValues(forms, 1, 2); return; case (int)Species.Gastrodon: // Gastrodon Data[FormOffset1 + 1] = (byte)SetDexFormValues(forms, 1, 2); return; case (int)Species.Burmy: // Burmy Data[FormOffset1 + 2] = (byte)SetDexFormValues(forms, 2, 3); return; case (int)Species.Wormadam: // Wormadam Data[FormOffset1 + 3] = (byte)SetDexFormValues(forms, 2, 3); return; case (int)Species.Unown: // Unown var unown = Data.AsSpan(FormOffset1 + 4, 0x1C); forms.CopyTo(unown); if (forms.Length != unown.Length) unown[forms.Length..].Fill(FORM_NONE); return; } if (DP) return; int PokeDexLanguageFlags = FormOffset1 + (HGSS ? 0x3C : 0x20); int FormOffset2 = PokeDexLanguageFlags + 0x1F4; switch (species) { case (int)Species.Rotom: // Rotom var value = SetDexFormValues(forms, 3, 6); WriteUInt32LittleEndian(Data.AsSpan(FormOffset2), value); return; case (int)Species.Shaymin: // Shaymin Data[FormOffset2 + 4] = (byte)SetDexFormValues(forms, 1, 2); return; case (int)Species.Giratina: // Giratina Data[FormOffset2 + 5] = (byte)SetDexFormValues(forms, 1, 2); return; case (int)Species.Pichu when HGSS: // Pichu Data[FormOffset2 + 6] = (byte)SetDexFormValues(forms, 2, 3); return; } } private static byte[] GetDexFormValues(uint Value, int BitsPerForm, int readCt) { byte[] Forms = new byte[readCt]; int n1 = 0xFF >> (8 - BitsPerForm); for (int i = 0; i < Forms.Length; i++) { int val = (int)(Value >> (i * BitsPerForm)) & n1; if (n1 == val && BitsPerForm > 1) Forms[i] = byte.MaxValue; else Forms[i] = (byte)val; } // (BitsPerForm > 1) was already handled, handle (BitsPerForm == 1) if (BitsPerForm == 1 && Forms[0] == Forms[1] && Forms[0] == 1) Forms[0] = Forms[1] = byte.MaxValue; return Forms; } private static uint SetDexFormValues(ReadOnlySpan Forms, int BitsPerForm, int readCt) { int n1 = 0xFF >> (8 - BitsPerForm); uint Value = 0xFFFFFFFF << (readCt * BitsPerForm); for (int i = 0; i < Forms.Length; i++) { int val = Forms[i]; if (val == -1) val = n1; Value |= (uint)(val << (BitsPerForm * i)); if (i >= readCt) throw new ArgumentOutOfRangeException(nameof(readCt), "Array count should be less than bitfield count"); } return Value; } private static bool TryInsertForm(Span forms, byte form) { if (forms.IndexOf(form) >= 0) return false; // already in list // insert at first empty var index = forms.IndexOf(FORM_NONE); if (index < 0) return false; // no free slots? forms[index] = form; return true; } private const byte UnownEmpty = byte.MaxValue; public int GetUnownFormIndex(byte form) { var ofs = Offset + OFS_FORM1 + 4; for (byte i = 0; i < 0x1C; i++) { byte val = Data[ofs + i]; if (val == form) return i; if (val == FORM_NONE) // end of populated indexes return UnownEmpty; } return UnownEmpty; } public int GetUnownFormIndexNext(byte form) { var ofs = Offset + OFS_FORM1 + 4; for (int i = 0; i < 0x1C; i++) { byte val = Data[ofs + i]; if (val == form) return i; if (val == FORM_NONE) return i; } return UnownEmpty; } public void ClearUnownForms() { var ofs = Offset + OFS_FORM1 + 4; for (int i = 0; i < 0x1C; i++) Data[ofs + i] = FORM_NONE; } public bool GetUnownForm(byte form) => GetUnownFormIndex(form) != UnownEmpty; public void AddUnownForm(byte form) { var index = GetUnownFormIndexNext(form); if (index == UnownEmpty) return; var ofs = Offset + OFS_FORM1 + 4; Data[ofs + index] = form; } public override void SetDex(PKM pk) { var species = pk.Species; if (species is 0 or > Legal.MaxSpeciesID_4) return; if (pk.IsEgg) // do not add return; var gender = pk.Gender; var form = pk.Form; var language = pk.Language; SetDex(species, gender, form, language); } private void SetDex(ushort species, int gender, byte form, int language) { SetCaught(species); SetSeenGender(species, gender); SetSeen(species); SetForms(species, form, gender); SetLanguage(species, language); } public void SetSeenGender(ushort species, int gender) { if (!GetSeen(species)) SetSeenGenderNewFlag(species, gender); else if (GetSeenSingleGender(species)) SetSeenGenderSecond(species, gender); } public void SetSeenGenderNewFlag(ushort species, int gender) { SetSeenGenderFirst(species, gender); SetSeenGenderSecond(species, gender); } public void SetSeenGenderNeither(ushort species) { SetSeenGenderFirst(species, 0); SetSeenGenderSecond(species, 0); } private void SetForms(ushort species, byte form, int gender) { if (species == (int)Species.Unown) // Unown { AddUnownForm(form); return; } var forms = GetForms(species); if (forms.Length == 0) return; if (species == (int)Species.Pichu && HGSS) // Pichu (HGSS Only) { var formID = form == 1 ? (byte)2 : (byte)gender; if (TryInsertForm(forms, formID)) SetForms(species, forms); } else { if (TryInsertForm(forms, form)) SetForms(species, forms); } } public void SetLanguage(ushort species, int language, bool value = true) { int lang = GetGen4LanguageBitIndex(language); SetLanguageBitIndex(species, lang, value); } public bool GetLanguageBitIndex(ushort species, int lang) { int dpl = 1 + Array.IndexOf(DPLangSpecies, species); if (DP && dpl < 0) return false; int FormOffset1 = Offset + OFS_FORM1; int PokeDexLanguageFlags = FormOffset1 + (HGSS ? 0x3C : 0x20); var ofs = PokeDexLanguageFlags + (DP ? dpl : species); return FlagUtil.GetFlag(Data, ofs, lang & 7); } public void SetLanguageBitIndex(ushort species, int lang, bool value) { int dpl = 1 + Array.IndexOf(DPLangSpecies, species); if (DP && dpl <= 0) return; int FormOffset1 = Offset + OFS_FORM1; int PokeDexLanguageFlags = FormOffset1 + (HGSS ? 0x3C : 0x20); var ofs = PokeDexLanguageFlags + (DP ? dpl : species); FlagUtil.SetFlag(Data, ofs, lang & 7, value); } public bool HasLanguage(ushort species) => GetSpeciesLanguageByteIndex(species) >= 0; private int GetSpeciesLanguageByteIndex(ushort species) { if (DP) return Array.IndexOf(DPLangSpecies, species); return species; } private static readonly int[] DPLangSpecies = { 23, 25, 54, 77, 120, 129, 202, 214, 215, 216, 228, 278, 287, 315 }; public static int GetGen4LanguageBitIndex(int lang) => --lang switch { 3 => 4, // invert ITA/GER 4 => 3, // invert ITA/GER > 5 => 0, // Japanese < 0 => 1, // English _ => lang, }; [Flags] public enum SetDexArgs { None, SeenAll = 1 << 0, CaughtNone = 1 << 1, CaughtAll = 1 << 2, SetNoLanguages = 1 << 3, SetAllLanguages = 1 << 4, SetSingleLanguage = 1 << 5, SetAllForms = 1 << 6, Complete = SeenAll | CaughtAll | SetAllLanguages | SetAllForms, } public void ModifyAll(ushort species, SetDexArgs args, int lang = 0) { if (args == SetDexArgs.None) { ClearSeen(species); return; } if ((args & SetDexArgs.SeenAll) != 0) CompleteSeen(species); if ((args & SetDexArgs.CaughtNone) != 0) { SetCaught(species, false); ClearLanguages(species); } else if ((args & SetDexArgs.CaughtAll) != 0) { SetCaught(species); } if ((args & SetDexArgs.SetNoLanguages) != 0) { ClearLanguages(species); } if ((args & SetDexArgs.SetAllLanguages) != 0) { SetLanguages(species); } else if ((args & SetDexArgs.SetSingleLanguage) != 0) { SetLanguage(species, lang); } if ((args & SetDexArgs.SetAllForms) != 0) { CompleteForms(species); } } private void CompleteForms(ushort species) { var forms = GetFormNames4Dex(species); if (forms.Length <= 1) return; Span values = stackalloc byte[forms.Length]; for (byte i = 1; i < values.Length; i++) values[i] = i; SetForms(species, values); } private void CompleteSeen(ushort species) { SetSeen(species); var pi = PersonalTable.HGSS[species]; if (pi.IsDualGender) { SetSeenGenderFirst(species, 0); SetSeenGenderSecond(species, 1); } else { SetSeenGender(species, pi.FixedGender() & 1); } } public void ClearSeen(ushort species) { SetCaught(species, false); SetSeen(species, false); SetSeenGenderNeither(species); SetForms(species, ReadOnlySpan.Empty); ClearLanguages(species); } private const int LangCount = 6; private void ClearLanguages(ushort species) { for (int i = 0; i < 8; i++) SetLanguageBitIndex(species, i, false); } private void SetLanguages(ushort species, bool value = true) { for (int i = 0; i < LangCount; i++) SetLanguageBitIndex(species, i, value); } // Bulk Manipulation public override void CompleteDex(bool shinyToo = false) => IterateAll(z => ModifyAll(z, SetDexArgs.Complete)); public override void SeenNone() => IterateAll(ClearSeen); public override void CaughtNone() => IterateAll(z => SetCaught(z, false)); public override void SeenAll(bool shinyToo = false) => IterateAll(CompleteSeen); public override void SetDexEntryAll(ushort species, bool shinyToo = false) => ModifyAll(species, SetDexArgs.Complete); public override void ClearDexEntryAll(ushort species) => ModifyAll(species, SetDexArgs.None); private static void IterateAll(Action a) { for (ushort i = 1; i <= Legal.MaxSpeciesID_4; i++) a(i); } public override void SetAllSeen(bool value = true, bool shinyToo = false) { if (!value) { SeenNone(); return; } IterateAll(CompleteSeen); } public override void CaughtAll(bool shinyToo = false) { SeenAll(); IterateAll(z => SetCaught(z)); } }