diff --git a/PKHeX.Core/Saves/SAV8BS.cs b/PKHeX.Core/Saves/SAV8BS.cs
index e1dfafd6c..a6e573f61 100644
--- a/PKHeX.Core/Saves/SAV8BS.cs
+++ b/PKHeX.Core/Saves/SAV8BS.cs
@@ -59,9 +59,9 @@ public SAV8BS(byte[] data, bool exportable = true) : base(data, exportable)
UgSaveData = new UgSaveData8b(this, 0x9A89C); // size: 0x27A0
// 0x9D03C - GMS_DATA // size: 0x31304, (GMS_POINT_DATA[650], ushort, ushort, byte)?; substructure GMS_POINT_HISTORY_DATA[5]
// 0xCE340 - PLAYER_NETWORK_DATA; bcatFlagArray byte[1300]
- // 0xCEA10(?) - UnionSaveData
- // 0xCEA1C(?) - CON_PHOTO_LANG_DATA -- contest photo language data; photo_data[5], photo_fx[5]
- // ZUKAN_PERSONAL_RND_DATA -- Spinda PID storage; uint[4] see, uint[4] get, uint[17] reserve
+ UnionSave = new UnionSaveData8b(this, 0xCEA10); // size: 0xC
+ ContestPhotoLanguage = new ContestPhotoLanguage8b(this, 0xCEA1C); // size: 0x18
+ ZukanExtra = new ZukanSpinda8b(this, 0xCEA34); // size: 0x64 (100)
// CON_PHOTO_EXT_DATA[5]
// GMS_POINT_HISTORY_EXT_DATA[3250]
UgCount = new UgCountRecord8b(this, 0xE8178); // size: 0x20
@@ -221,6 +221,9 @@ public override bool ChecksumsValid
public Poketch8b Poketch { get; }
public Daycare8b Daycare { get; }
public UgSaveData8b UgSaveData { get; }
+ public UnionSaveData8b UnionSave { get; }
+ public ContestPhotoLanguage8b ContestPhotoLanguage { get; }
+ public ZukanSpinda8b ZukanExtra { get; }
public UgCountRecord8b UgCount { get; }
// First Savedata Expansion!
diff --git a/PKHeX.Core/Saves/Substructures/Gen8/BS/ContestPhotoLanguage8b.cs b/PKHeX.Core/Saves/Substructures/Gen8/BS/ContestPhotoLanguage8b.cs
new file mode 100644
index 000000000..57a714a86
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen8/BS/ContestPhotoLanguage8b.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace PKHeX.Core;
+
+///
+/// Contest photo language data
+///
+/// CON_PHOTO_LANG_DATA size: 0x18
+public sealed class ContestPhotoLanguage8b : SaveBlock
+{
+ // structure:
+ // 5 (Style, Beautiful, Cute, Clever, Strong) bytes of language IDs
+ // (3+4 bytes alignment)
+ // 2 s64 reserved
+ private const int COUNT_CONTEST = 5;
+
+ public ContestPhotoLanguage8b(SAV8BS sav, int offset) : base(sav) => Offset = offset;
+
+ public byte GetLanguage(int contest) => Data[Offset + GetContestOffset(contest)];
+
+ private static int GetContestOffset(int contest)
+ {
+ if ((uint)contest >= COUNT_CONTEST)
+ throw new ArgumentOutOfRangeException(nameof(contest));
+ return contest;
+ }
+
+ public void SetLanguage(int contest, int language) => Data[Offset + GetContestOffset(contest)] = (byte)language;
+}
diff --git a/PKHeX.Core/Saves/Substructures/Gen8/BS/UnionSaveData8b.cs b/PKHeX.Core/Saves/Substructures/Gen8/BS/UnionSaveData8b.cs
new file mode 100644
index 000000000..ecd8bccb8
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen8/BS/UnionSaveData8b.cs
@@ -0,0 +1,18 @@
+using System;
+using System.ComponentModel;
+
+namespace PKHeX.Core;
+
+///
+/// Details about the Player's union room unlock/penalty.
+///
+/// UnionSaveData size: 0xC
+[TypeConverter(typeof(ExpandableObjectConverter))]
+public sealed class UnionSaveData8b : SaveBlock
+{
+ public UnionSaveData8b(SAV8BS sav, int offset) : base(sav) => Offset = offset;
+
+ public bool IsInitTalk { get => BitConverter.ToUInt32(Data, Offset) == 1; set => BitConverter.GetBytes(value ? 1u : 0u).CopyTo(Data, Offset); }
+ public int PenaltyCount { get => BitConverter.ToInt32(Data, Offset + 0x4); set => BitConverter.GetBytes(value).CopyTo(Data, Offset + 0x4); }
+ public float PenaltyTime { get => BitConverter.ToSingle(Data, Offset + 0x8); set => BitConverter.GetBytes(value).CopyTo(Data, Offset + 0x8); }
+}
diff --git a/PKHeX.Core/Saves/Substructures/Gen8/BS/Zukan8b.cs b/PKHeX.Core/Saves/Substructures/Gen8/BS/Zukan8b.cs
index e666b8c70..d69340e4a 100644
--- a/PKHeX.Core/Saves/Substructures/Gen8/BS/Zukan8b.cs
+++ b/PKHeX.Core/Saves/Substructures/Gen8/BS/Zukan8b.cs
@@ -339,11 +339,14 @@ public override void SetDex(PKM pkm)
if (pkm.IsEgg) // do not add
return;
+ var originalState = GetState(species);
bool shiny = pkm.IsShiny;
SetState(species, ZukanState8b.Caught);
SetGenderFlag(species, pkm.Gender, shiny);
SetLanguageFlag(species, pkm.Language, true);
SetHasFormFlag(species, pkm.Form, shiny, true);
+ if (species is (int)Species.Spinda)
+ ((SAV8BS)SAV).ZukanExtra.SetDex(originalState, pkm.EncryptionConstant, pkm.Gender, shiny);
}
private void SetGenderFlag(int species, int gender, bool shiny)
diff --git a/PKHeX.Core/Saves/Substructures/Gen8/BS/ZukanSpinda8b.cs b/PKHeX.Core/Saves/Substructures/Gen8/BS/ZukanSpinda8b.cs
new file mode 100644
index 000000000..f86e67fed
--- /dev/null
+++ b/PKHeX.Core/Saves/Substructures/Gen8/BS/ZukanSpinda8b.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace PKHeX.Core;
+
+///
+/// Tracks data for the game.
+///
+/// ZUKAN_PERSONAL_RND_DATA size: 0x64 (100)
+public sealed class ZukanSpinda8b : SaveBlock
+{
+ public ZukanSpinda8b(SAV8BS sav, int offset) : base(sav) => Offset = offset;
+
+ public uint GetSeen(int gender, bool shiny)
+ {
+ var ofs = GetOffset(gender, shiny);
+ return BitConverter.ToUInt32(Data, Offset + ofs);
+ }
+
+ public uint GetCaught(int gender, bool shiny)
+ {
+ var ofs = GetOffset(gender, shiny);
+ return BitConverter.ToUInt32(Data, Offset + 0x10 + ofs);
+ }
+
+ public void SetSeen(int gender, bool shiny, uint value)
+ {
+ var ofs = GetOffset(gender, shiny);
+ BitConverter.GetBytes(value).CopyTo(Data, Offset + ofs);
+ }
+
+ public void SetCaught(int gender, bool shiny, uint value)
+ {
+ var ofs = GetOffset(gender, shiny);
+ BitConverter.GetBytes(value).CopyTo(Data, Offset + 0x10 + ofs);
+ }
+
+ private static int GetOffset(int gender, bool shiny) => 4 * ((gender & 1) + (shiny ? 2 : 0));
+
+ public void SetDex(ZukanState8b state, uint ec, int gender, bool shiny)
+ {
+ if (state < ZukanState8b.Seen) // not seen yet
+ SetSeen(gender, shiny, ec);
+ if (state < ZukanState8b.Caught) // not caught yet
+ SetCaught(gender, shiny, ec);
+ }
+}