diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_de.txt b/PKHeX.Core/Resources/text/program/MessageStrings_de.txt index 9b4d93b0e..fcdfc7173 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_de.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_de.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = Alle 3 Pokémon für jeden Freund entsperren? MsgSaveGen2RTCResetPassword = RTC Reset Passwort: {0:00000} MsgSaveGen2RTCResetBitflag = Möchtest du die Uhr zurücksetzen? MsgSaveJPEGExportFail = Der Spielstand enthält keine Grafik Daten! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = Spielstand wurde bearbeitet. Integrität kann nicht geprüft werden. MsgSaveChecksumValid = Alle Prüfsummen sind gültig. MsgSaveChecksumFailExport = Prüfsummen in die Zwischenablage kopieren? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_en.txt b/PKHeX.Core/Resources/text/program/MessageStrings_en.txt index 4c944f4f5..032957629 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_en.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_en.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = Unlock all 3 slots for each friend? MsgSaveGen2RTCResetPassword = RTC Reset Password: {0:00000} MsgSaveGen2RTCResetBitflag = Would you like to reset the RTC? MsgSaveJPEGExportFail = No picture data found in the save file! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = Save has been edited. Cannot integrity check. MsgSaveChecksumValid = Checksums are valid. MsgSaveChecksumFailExport = Export Checksum Info to Clipboard? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_es.txt b/PKHeX.Core/Resources/text/program/MessageStrings_es.txt index dae315eab..4a5ea2bc2 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_es.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_es.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = ¿Desbloquear los 3 espacios para cada amigo? MsgSaveGen2RTCResetPassword = Contraseña de reseteo de RTC: {0:00000} MsgSaveGen2RTCResetBitflag = ¿Le gustaría reiniciar el RTC? MsgSaveJPEGExportFail = ¡No se han encontrado datos de imagen en el archivo de guardado! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = El archivo de guardado ha sido editado. No se puede verificar la integridad. MsgSaveChecksumValid = Suma de verificación válido. MsgSaveChecksumFailExport = ¿Exportar información de la suma de verificación al portapapeles? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_fr.txt b/PKHeX.Core/Resources/text/program/MessageStrings_fr.txt index 1ed321264..a808fd056 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_fr.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_fr.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = Débloquer les 3 emplacements pour chaque ami MsgSaveGen2RTCResetPassword = Mot de passe de réinitialisation RTC: {0:00000} MsgSaveGen2RTCResetBitflag = Souhaitez-vous réinitialiser le RTC? MsgSaveJPEGExportFail = Aucune donnée d'image trouvée dans le fichier de sauvegarde! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = La sauvegarde a été éditée. Impossible de vérifier l'intégrité. MsgSaveChecksumValid = Sommes de contrôle valides. MsgSaveChecksumFailExport = Copier les informations de somme de contrôle ? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_it.txt b/PKHeX.Core/Resources/text/program/MessageStrings_it.txt index 4abb1a6ca..b0b215930 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_it.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_it.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = Sbloccare tutti i 3 slot per ogni amico? MsgSaveGen2RTCResetPassword = RTC Reset Password: {0:00000} MsgSaveGen2RTCResetBitflag = Vuoi resettare l'orologio RTC? MsgSaveJPEGExportFail = Nessuna immagine è stata trovata in questo salvataggio! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = Il salvataggio è stato modificato. Impossibile eseguire il controllo di integrità. MsgSaveChecksumValid = I Checksum sono validi. MsgSaveChecksumFailExport = Esportare le info sui Checksum negli appunti? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_ja.txt b/PKHeX.Core/Resources/text/program/MessageStrings_ja.txt index 015f7cbd6..d89fa2dbf 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_ja.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_ja.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = Unlock all 3 slots for each friend? MsgSaveGen2RTCResetPassword = RTC Reset Password: {0:00000} MsgSaveGen2RTCResetBitflag = Would you like to reset the RTC? MsgSaveJPEGExportFail = No picture data found in the save file! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = Save has been edited. Cannot integrity check. MsgSaveChecksumValid = Checksums are valid. MsgSaveChecksumFailExport = Export Checksum Info to Clipboard? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_ko.txt b/PKHeX.Core/Resources/text/program/MessageStrings_ko.txt index 8df82c811..9e51b89e7 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_ko.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_ko.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = 각 친구마다 3개 슬롯을 모두 해금 MsgSaveGen2RTCResetPassword = RTC 초기화 비밀번호: {0:00000} MsgSaveGen2RTCResetBitflag = RTC를 초기화하시겠습니까? MsgSaveJPEGExportFail = 세이브 파일에서 사진 데이터를 찾을 수 없습니다! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = 세이브가 수정되었습니다. 무결성 검사를 할 수 없습니다. MsgSaveChecksumValid = 체크섬 검증에 성공했습니다. MsgSaveChecksumFailExport = 체크섬 정보를 클립보드로 복사하시겠습니까? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_zh.txt b/PKHeX.Core/Resources/text/program/MessageStrings_zh.txt index 1fa094ece..c54321b40 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_zh.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_zh.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = 解锁所有朋友狩猎的三个槽位? MsgSaveGen2RTCResetPassword = 时钟重设密码: {0:00000} MsgSaveGen2RTCResetBitflag = 你想重置RTC吗? MsgSaveJPEGExportFail = 无法在存档文件中找到图片数据! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = 存档已被编辑。无法进行完整性检查。 MsgSaveChecksumValid = 检验值正确。 MsgSaveChecksumFailExport = 导出校验值信息到剪贴板? diff --git a/PKHeX.Core/Resources/text/program/MessageStrings_zh2.txt b/PKHeX.Core/Resources/text/program/MessageStrings_zh2.txt index bde541591..750c7327d 100644 --- a/PKHeX.Core/Resources/text/program/MessageStrings_zh2.txt +++ b/PKHeX.Core/Resources/text/program/MessageStrings_zh2.txt @@ -144,6 +144,8 @@ MsgSaveGen6FriendSafariCheatDesc = 解鎖所有朋友狩獵之三個格子? MsgSaveGen2RTCResetPassword = 時鐘重設密碼: {0:00000} MsgSaveGen2RTCResetBitflag = 你想重置RTC嗎? MsgSaveJPEGExportFail = 無法在儲存資料檔案中找到圖片資料! +MsgSaveGen4ConvertKorean = Would you like to convert this Japanese/International save file to be playable with Korean games? +MsgSaveGen4ConvertInternational = Would you like to convert this Korean save file to be playable with Japanese/International games? MsgSaveChecksumFailEdited = 儲存資料經已被編輯。無法進行完整性檢查。 MsgSaveChecksumValid = 檢驗值正確。 MsgSaveChecksumFailExport = 是否匯出校驗值信息至剪貼簿? diff --git a/PKHeX.Core/Saves/Blocks/BlockInfo4.cs b/PKHeX.Core/Saves/Blocks/BlockInfo4.cs new file mode 100644 index 000000000..dc00bdaf8 --- /dev/null +++ b/PKHeX.Core/Saves/Blocks/BlockInfo4.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// Gen4 Extra Block Info +/// +public class BlockInfo4 : BlockInfo +{ + private const int SIZE_FOOTER = 0x10; + private readonly int FooterOffset; + + public BlockInfo4(uint id, int offset, int length) + { + ID = id; + Offset = offset; + Length = length; + FooterOffset = offset + length - SIZE_FOOTER; + } + + public uint GetKey(ReadOnlySpan data) => ReadUInt32LittleEndian(data[Offset..]); + public uint GetMagic(ReadOnlySpan data) => ReadUInt32LittleEndian(data[FooterOffset..]); + public uint GetRevision(ReadOnlySpan data) => ReadUInt32LittleEndian(data[(FooterOffset + 0x4)..]); + public int GetSize(ReadOnlySpan data) => ReadInt32LittleEndian(data[(FooterOffset + 0x8)..]); + public ushort GetID(ReadOnlySpan data) => ReadUInt16LittleEndian(data[(FooterOffset + 0xC)..]); + private ushort GetChecksum(ReadOnlySpan data) => Checksums.CRC16_CCITT(data.Slice(Offset, Length - 2)); + + private bool IsInitialized(ReadOnlySpan data) + { + return (ID == 0 && GetRevision(data) != 0xFFFFFFFF) || (ID != 0 && GetKey(data) != 0xFFFFFFFF); + } + + public bool SizeValid(ReadOnlySpan data) + { + return GetSize(data) == Length; + } + + protected override bool ChecksumValid(ReadOnlySpan data) + { + if (!IsInitialized(data)) + return true; + + ushort chk = GetChecksum(data); + if (chk != ReadUInt16LittleEndian(data[(FooterOffset + 14)..])) + return false; + return true; + } + + public bool IsValid(ReadOnlySpan data) + { + return IsInitialized(data) && SizeValid(data) && ChecksumValid(data); + } + + protected override void SetChecksum(Span data) + { + if (!IsInitialized(data)) + return; + ushort chk = GetChecksum(data); + WriteUInt16LittleEndian(data[(FooterOffset + 14)..], chk); + } + + protected void SetMagic(Span data, uint magic) + { + if (!IsInitialized(data)) + return; + WriteUInt32LittleEndian(data[FooterOffset..], magic); + } + + public static void SetMagics(IEnumerable blocks, Span data, uint magic) + { + foreach (var b in blocks) + b.SetMagic(data, magic); + } +} + +public static partial class Extensions +{ + public static void SetMagics(this IEnumerable blocks, Span data, uint magic) => BlockInfo4.SetMagics(blocks, data, magic); +} diff --git a/PKHeX.Core/Saves/SAV4.cs b/PKHeX.Core/Saves/SAV4.cs index 7135bd961..dadf8e561 100644 --- a/PKHeX.Core/Saves/SAV4.cs +++ b/PKHeX.Core/Saves/SAV4.cs @@ -27,6 +27,12 @@ public abstract class SAV4 : SaveFile, IEventFlag37 protected sealed override Span BoxBuffer => Storage; protected sealed override Span PartyBuffer => General; + private readonly Memory BackupStorageBuffer; + private readonly Memory BackupGeneralBuffer; + private Span BackupStorage => BackupStorageBuffer.Span; + private Span BackupGeneral => BackupGeneralBuffer.Span; + protected abstract IReadOnlyList ExtraBlocks { get; } + public abstract Zukan4 Dex { get; } protected abstract int EventFlag { get; } @@ -38,6 +44,8 @@ protected SAV4(int gSize, int sSize) { GeneralBuffer = new byte[gSize]; StorageBuffer = new byte[sSize]; + BackupGeneralBuffer = new byte[gSize]; + BackupStorageBuffer = new byte[sSize]; ClearBoxes(); } @@ -50,6 +58,11 @@ protected SAV4(byte[] data, int gSize, int sSize, int sStart) : base(data) var sbo = (StorageBlockPosition == 0 ? 0 : PartitionSize) + sStart; GeneralBuffer = Data.AsMemory(gbo, gSize); StorageBuffer = Data.AsMemory(sbo, sSize); + + var gboBackup = (GeneralBlockPosition != 0 ? 0 : PartitionSize); + var sboBackup = (StorageBlockPosition != 0 ? 0 : PartitionSize) + sStart; + BackupGeneralBuffer = Data.AsMemory(gboBackup, gSize); + BackupStorageBuffer = Data.AsMemory(sboBackup, sSize); } // Configuration @@ -101,10 +114,29 @@ public sealed override void CopyChangesFrom(SaveFile sav) private static ushort GetBlockChecksumSaved(ReadOnlySpan data) => ReadUInt16LittleEndian(data[^2..]); private bool GetBlockChecksumValid(ReadOnlySpan data) => CalcBlockChecksum(data) == GetBlockChecksumSaved(data); + + protected void SetMagics(uint magic) + { + WriteUInt32LittleEndian(General[^8..^4], magic); + WriteUInt32LittleEndian(Storage[^8..^4], magic); + if (ReadUInt32LittleEndian(BackupGeneral[^8..^4]) != 0xFFFFFFFF) + WriteUInt32LittleEndian(BackupGeneral[^8..^4], magic); + if (ReadUInt32LittleEndian(BackupStorage[^8..^4]) != 0xFFFFFFFF) + WriteUInt32LittleEndian(BackupStorage[^8..^4], magic); + ExtraBlocks.SetMagics(Data.AsSpan(), magic); + ExtraBlocks.SetMagics(Data.AsSpan(PartitionSize..), magic); + } + protected sealed override void SetChecksums() { WriteUInt16LittleEndian(General[^2..], CalcBlockChecksum(General)); WriteUInt16LittleEndian(Storage[^2..], CalcBlockChecksum(Storage)); + if (ReadUInt32LittleEndian(BackupGeneral[^8..^4]) != 0xFFFFFFFF) + WriteUInt16LittleEndian(BackupGeneral[^2..], CalcBlockChecksum(BackupGeneral)); + if (ReadUInt32LittleEndian(BackupStorage[^8..^4]) != 0xFFFFFFFF) + WriteUInt16LittleEndian(BackupStorage[^2..], CalcBlockChecksum(BackupStorage)); + ExtraBlocks.SetChecksums(Data.AsSpan()); + ExtraBlocks.SetChecksums(Data.AsSpan(PartitionSize..)); } public sealed override bool ChecksumsValid @@ -115,6 +147,10 @@ public sealed override bool ChecksumsValid return false; if (!GetBlockChecksumValid(Storage)) return false; + if (!ExtraBlocks.GetChecksumsValid(Data.AsSpan())) + return false; + if (!ExtraBlocks.GetChecksumsValid(Data.AsSpan(PartitionSize..))) + return false; return true; } @@ -129,6 +165,10 @@ public sealed override string ChecksumInfo list.Add("Small block checksum is invalid"); if (!GetBlockChecksumValid(Storage)) list.Add("Large block checksum is invalid"); + if (!ExtraBlocks.GetChecksumsValid(Data.AsSpan())) + list.Add(ExtraBlocks.GetChecksumInfo(Data.AsSpan())); + if (!ExtraBlocks.GetChecksumsValid(Data.AsSpan(PartitionSize..))) + list.Add(ExtraBlocks.GetChecksumInfo(Data.AsSpan(PartitionSize..))); return list.Count != 0 ? string.Join(Environment.NewLine, list) : "Checksums are valid."; } @@ -140,10 +180,36 @@ private static int GetActiveBlock(ReadOnlySpan data, [ConstantExpected(Min return SAV4BlockDetection.CompareFooters(data, offset, offset + PartitionSize); } + private int GetActiveExtraBlock(BlockInfo4 block) + { + int index = (int)block.ID; + + // Hall of Fame + if (index == 0) + return SAV4BlockDetection.CompareExtra(Data, Data.AsSpan(PartitionSize), block); + + // Battle Hall/Battle Videos + var KeyOffset = Extra; + var KeyBackupOffset = Extra + 0x4 * (ExtraBlocks.Count - 1); + var PreferOffset = Extra + 2 * 0x4 * (ExtraBlocks.Count - 1); + var key = ReadUInt32LittleEndian(General[(KeyOffset + 0x4 * (index - 1))..]); + var keyBackup = ReadUInt32LittleEndian(General[(KeyBackupOffset + 0x4 * (index - 1))..]); + var prefer = General[(PreferOffset + (index - 1))]; + return SAV4BlockDetection.CompareExtra(Data, Data.AsSpan(PartitionSize), block, key, keyBackup, prefer); + } + + public Hall4? GetHall() + { + var block = ExtraBlocks[1]; + var active = GetActiveExtraBlock(block); + return active == -1 ? null : new Hall4(Data, (active == 0 ? 0 : PartitionSize) + block.Offset); + } + protected int WondercardFlags = int.MinValue; protected int AdventureInfo = int.MinValue; protected int Seal = int.MinValue; public int Geonet = int.MinValue; + protected int Extra = int.MinValue; protected int Trainer1; public int GTS { get; protected set; } = int.MinValue; @@ -268,6 +334,10 @@ public int Region set { if (value < 0) return; General[Geonet + 2] = (byte)value; } } + public const uint MAGIC_JAPAN_INTL = 0x20060623; + public const uint MAGIC_KOREAN = 0x20070903; + public uint Magic { get => ReadUInt32LittleEndian(General[^8..^4]); set => SetMagics(value); } + protected sealed override PK4 GetPKM(byte[] data) => new(data); protected sealed override byte[] DecryptPKM(byte[] data) => PokeCrypto.DecryptArray45(data); diff --git a/PKHeX.Core/Saves/SAV4DP.cs b/PKHeX.Core/Saves/SAV4DP.cs index 9fa0efa69..af9e59bf8 100644 --- a/PKHeX.Core/Saves/SAV4DP.cs +++ b/PKHeX.Core/Saves/SAV4DP.cs @@ -30,6 +30,9 @@ public SAV4DP(byte[] data) : base(data, GeneralSize, StorageSize, GeneralSize) private const int GeneralSize = 0xC100; private const int StorageSize = 0x121E0; // Start 0xC100, +4 starts box data + protected override BlockInfo4[] ExtraBlocks => new[] { + new BlockInfo4(0, 0x20000, 0x2AC0), // Hall of Fame + }; private void Initialize() { diff --git a/PKHeX.Core/Saves/SAV4HGSS.cs b/PKHeX.Core/Saves/SAV4HGSS.cs index f59923695..e5370b48c 100644 --- a/PKHeX.Core/Saves/SAV4HGSS.cs +++ b/PKHeX.Core/Saves/SAV4HGSS.cs @@ -32,6 +32,14 @@ public SAV4HGSS(byte[] data) : base(data, GeneralSize, StorageSize, GeneralSize private const int StorageSize = 0x12310; // Start 0xF700, +0 starts box data private const int GeneralGap = 0xD8; protected override int FooterSize => 0x10; + protected override BlockInfo4[] ExtraBlocks => new[] { + new BlockInfo4(0, 0x23000, 0x2AC0), // Hall of Fame + new BlockInfo4(1, 0x26000, 0x0BB0), // Battle Hall + new BlockInfo4(2, 0x27000, 0x1D60), // Battle Video (My Video) + new BlockInfo4(3, 0x29000, 0x1D60), // Battle Video (Other Videos 1) + new BlockInfo4(4, 0x2B000, 0x1D60), // Battle Video (Other Videos 2) + new BlockInfo4(5, 0x2D000, 0x1D60), // Battle Video (Other Videos 3) + }; private void Initialize() { @@ -48,6 +56,7 @@ private void GetSAVOffsets() Trainer1 = 0x64; Party = 0x98; PokeDex = 0x12B8; + Extra = 0x230C; Geonet = 0x8D44; WondercardFlags = 0x9D3C; WondercardData = 0x9E3C; diff --git a/PKHeX.Core/Saves/SAV4Pt.cs b/PKHeX.Core/Saves/SAV4Pt.cs index ad925279c..42a0ee61b 100644 --- a/PKHeX.Core/Saves/SAV4Pt.cs +++ b/PKHeX.Core/Saves/SAV4Pt.cs @@ -29,6 +29,14 @@ public SAV4Pt(byte[] data) : base(data, GeneralSize, StorageSize, GeneralSize) private const int GeneralSize = 0xCF2C; private const int StorageSize = 0x121E4; // Start 0xCF2C, +4 starts box data + protected override BlockInfo4[] ExtraBlocks => new[] { + new BlockInfo4(0, 0x20000, 0x2AC0), // Hall of Fame + new BlockInfo4(1, 0x23000, 0x0BB0), // Battle Hall + new BlockInfo4(2, 0x24000, 0x1D60), // Battle Video (My Video) + new BlockInfo4(3, 0x26000, 0x1D60), // Battle Video (Other Videos 1) + new BlockInfo4(4, 0x28000, 0x1D60), // Battle Video (Other Videos 2) + new BlockInfo4(5, 0x2A000, 0x1D60), // Battle Video (Other Videos 3) + }; private void Initialize() { @@ -45,6 +53,7 @@ private void GetSAVOffsets() Trainer1 = 0x68; Party = 0xA0; PokeDex = 0x1328; + Extra = 0x2820; Geonet = 0xA4C4; WondercardFlags = 0xB4C0; WondercardData = 0xB5C0; diff --git a/PKHeX.Core/Saves/Substructures/Gen4/SAV4BlockDetection.cs b/PKHeX.Core/Saves/Substructures/Gen4/SAV4BlockDetection.cs index 3ab0a8526..75f2506c1 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/SAV4BlockDetection.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/SAV4BlockDetection.cs @@ -48,4 +48,58 @@ private static int CompareCounters(uint counter1, uint counter2) return Same; } + + + /// + /// Compares two extra blocks to determine which is newest. + /// + /// 0=Primary, 1=Secondary, -1=Uninitialized. + public static int CompareExtra(ReadOnlySpan data1, ReadOnlySpan data2, BlockInfo4 block) + { + // The Hall of Fame block uses a counter in the footer to determine which copy is used. + // Entering the Hall of Fame overwrites both copies with the new data. + var rev1 = block.GetRevision(data1); + var rev2 = block.GetRevision(data2); + var valid1 = rev1 != 0xFFFFFFFF && block.SizeValid(data1); + var valid2 = rev2 != 0xFFFFFFFF && block.SizeValid(data2); + if (valid1 && (rev1 >= rev2 || !valid2)) + return First; + if (valid2 && (rev2 > rev1 || !valid1)) + return Second; + return -1; // Uninitialized + } + + public static int CompareExtra(ReadOnlySpan data1, ReadOnlySpan data2, BlockInfo4 block, uint key, uint keyBackup, byte prefer) + { + // The Battle Hall/Battle Videos use a key in the General block to check if the block is valid. + // If the key is 0xFFFFFFFF, the block is uninitialized or deleted. + // Which partition is checked first is determined by a byte in the General block. + var key1 = block.GetKey(data1); + var key2 = block.GetKey(data2); + var valid1 = key1 != 0xFFFFFFFF && block.SizeValid(data1); + var valid2 = key2 != 0xFFFFFFFF && block.SizeValid(data2); + if (prefer == First) + { + if (valid1 && key == key1) + return First; + if (valid2 && key == key2) + return Second; + if (valid1 && keyBackup == key1) + return First; + if (valid2 && keyBackup == key2) + return Second; + } + else + { + if (valid2 && key == key2) + return Second; + if (valid1 && key == key1) + return First; + if (valid2 && keyBackup == key2) + return Second; + if (valid1 && keyBackup == key1) + return First; + } + return -1; // Uninitialized + } } diff --git a/PKHeX.Core/Util/MessageStrings.cs b/PKHeX.Core/Util/MessageStrings.cs index 46520e87e..e0e8aad14 100644 --- a/PKHeX.Core/Util/MessageStrings.cs +++ b/PKHeX.Core/Util/MessageStrings.cs @@ -190,6 +190,8 @@ public static class MessageStrings public static string MsgSaveGen2RTCResetPassword { get; set; } = "RTC Reset Password: {0:00000}"; public static string MsgSaveGen2RTCResetBitflag { get; set; } = "Would you like to reset the RTC?"; public static string MsgSaveJPEGExportFail { get; set; } = "No picture data found in the save file!"; + public static string MsgSaveGen4ConvertKorean { get; set; } = "Would you like to convert this Japanese/International save file to be playable with Korean games?"; + public static string MsgSaveGen4ConvertInternational { get; set; } = "Would you like to convert this Korean save file to be playable with Japanese/International games?"; public static string MsgSaveChecksumFailEdited { get; set; } = "Save has been edited. Cannot integrity check."; public static string MsgSaveChecksumValid { get; set; } = "Checksums are valid."; diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.Designer.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.Designer.cs index b22aff15f..efb8e58db 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.Designer.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.Designer.cs @@ -106,6 +106,7 @@ private void InitializeComponent() TB_Secure1 = new System.Windows.Forms.TextBox(); L_GameSync = new System.Windows.Forms.Label(); TB_GameSync = new System.Windows.Forms.TextBox(); + B_ConvertKorean = new System.Windows.Forms.Button(); tabBoxMulti.SuspendLayout(); Tab_Box.SuspendLayout(); Tab_PartyBattle.SuspendLayout(); @@ -835,6 +836,7 @@ private void InitializeComponent() FLP_SAVToolsMisc.Controls.Add(B_VerifySaveEntities); FLP_SAVToolsMisc.Controls.Add(Menu_ExportBAK); FLP_SAVToolsMisc.Controls.Add(B_JPEG); + FLP_SAVToolsMisc.Controls.Add(B_ConvertKorean); FLP_SAVToolsMisc.Dock = System.Windows.Forms.DockStyle.Top; FLP_SAVToolsMisc.Location = new System.Drawing.Point(0, 0); FLP_SAVToolsMisc.Margin = new System.Windows.Forms.Padding(0); @@ -848,7 +850,7 @@ private void InitializeComponent() B_SaveBoxBin.Margin = new System.Windows.Forms.Padding(0); B_SaveBoxBin.Name = "B_SaveBoxBin"; B_SaveBoxBin.Size = new System.Drawing.Size(88, 48); - B_SaveBoxBin.TabIndex = 8; + B_SaveBoxBin.TabIndex = 1; B_SaveBoxBin.Text = "Save Box Data++"; B_SaveBoxBin.UseVisualStyleBackColor = true; B_SaveBoxBin.Click += B_SaveBoxBin_Click; @@ -870,7 +872,7 @@ private void InitializeComponent() B_VerifySaveEntities.Margin = new System.Windows.Forms.Padding(0); B_VerifySaveEntities.Name = "B_VerifySaveEntities"; B_VerifySaveEntities.Size = new System.Drawing.Size(88, 48); - B_VerifySaveEntities.TabIndex = 104; + B_VerifySaveEntities.TabIndex = 3; B_VerifySaveEntities.Text = "Verify All PKMs"; B_VerifySaveEntities.UseVisualStyleBackColor = true; B_VerifySaveEntities.Click += ClickVerifyStoredEntities; @@ -881,7 +883,7 @@ private void InitializeComponent() Menu_ExportBAK.Margin = new System.Windows.Forms.Padding(0); Menu_ExportBAK.Name = "Menu_ExportBAK"; Menu_ExportBAK.Size = new System.Drawing.Size(88, 48); - Menu_ExportBAK.TabIndex = 103; + Menu_ExportBAK.TabIndex = 4; Menu_ExportBAK.Text = "Export Backup"; Menu_ExportBAK.UseVisualStyleBackColor = true; Menu_ExportBAK.Click += Menu_ExportBAK_Click; @@ -892,7 +894,7 @@ private void InitializeComponent() B_JPEG.Margin = new System.Windows.Forms.Padding(0); B_JPEG.Name = "B_JPEG"; B_JPEG.Size = new System.Drawing.Size(88, 48); - B_JPEG.TabIndex = 12; + B_JPEG.TabIndex = 5; B_JPEG.Text = "Save PGL .JPEG"; B_JPEG.UseVisualStyleBackColor = true; B_JPEG.Click += B_JPEG_Click; @@ -982,6 +984,17 @@ private void InitializeComponent() TB_GameSync.TabIndex = 10; TB_GameSync.Validated += UpdateStringSeed; // + // B_ConvertKorean + // + B_ConvertKorean.Location = new System.Drawing.Point(0, 48); + B_ConvertKorean.Margin = new System.Windows.Forms.Padding(0); + B_ConvertKorean.Name = "B_ConvertKorean"; + B_ConvertKorean.Size = new System.Drawing.Size(88, 48); + B_ConvertKorean.TabIndex = 6; + B_ConvertKorean.Text = "Korean Save Conversion"; + B_ConvertKorean.UseVisualStyleBackColor = true; + B_ConvertKorean.Click += B_ConvertKorean_Click; + // // SAVEditor // AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; @@ -1081,5 +1094,6 @@ private void InitializeComponent() private System.Windows.Forms.Button B_Poffins; private System.Windows.Forms.Button B_VerifySaveEntities; private System.Windows.Forms.Button B_RaidsSevenStar; + private System.Windows.Forms.Button B_ConvertKorean; } } diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs index aa2f51e1d..886e1c8d7 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs @@ -739,6 +739,19 @@ private void B_JPEG_Click(object sender, EventArgs e) File.WriteAllBytes(sfd.FileName, jpeg); } + private void B_ConvertKorean_Click(object sender, EventArgs e) + { + if (SAV.Generation != 4) + return; + var s4 = (SAV4)SAV; + var isKorean = s4.Magic == SAV4.MAGIC_KOREAN; + var msg = isKorean ? MsgSaveGen4ConvertInternational : MsgSaveGen4ConvertKorean; + if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, msg)) + return; + s4.Magic = isKorean ? SAV4.MAGIC_JAPAN_INTL : SAV4.MAGIC_KOREAN; + SAV.State.Edited = true; + } + private void ClickVerifyCHK(object sender, EventArgs e) { if (SAV.State.Edited) @@ -1113,11 +1126,13 @@ private void ToggleViewSubEditors(SaveFile sav) { FLP_SAVtools.Visible = false; B_JPEG.Visible = false; + B_ConvertKorean.Visible = false; SL_Extra.HideAllSlots(); return; } GB_Daycare.Visible = sav.HasDaycare; + B_ConvertKorean.Visible = sav is SAV4; B_OpenPokeblocks.Visible = sav is SAV6AO; B_OpenSecretBase.Visible = sav is SAV6AO; B_OpenPokepuffs.Visible = sav is ISaveBlock6Main; diff --git a/PKHeX.WinForms/Resources/text/lang_de.txt b/PKHeX.WinForms/Resources/text/lang_de.txt index a8945ce88..c3bd64c37 100644 --- a/PKHeX.WinForms/Resources/text/lang_de.txt +++ b/PKHeX.WinForms/Resources/text/lang_de.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C-Gear Skin Main.B_Clear=Löschen Main.B_FestivalPlaza=Festival-Plaza Main.B_JPEG=Speichere PGL .JPEG +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=Briefbox Main.B_MoveShop=Attacken Tutor Main.B_OpenApricorn=Aprikokos diff --git a/PKHeX.WinForms/Resources/text/lang_en.txt b/PKHeX.WinForms/Resources/text/lang_en.txt index 0e218575f..e533f59de 100644 --- a/PKHeX.WinForms/Resources/text/lang_en.txt +++ b/PKHeX.WinForms/Resources/text/lang_en.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C-Gear Skin Main.B_Clear=Clear Main.B_FestivalPlaza=Festival Plaza Main.B_JPEG=Save PGL .JPEG +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=Mail Box Main.B_MoveShop=Move Shop Main.B_OpenApricorn=Apricorns diff --git a/PKHeX.WinForms/Resources/text/lang_es.txt b/PKHeX.WinForms/Resources/text/lang_es.txt index 82154eccb..e431a6352 100644 --- a/PKHeX.WinForms/Resources/text/lang_es.txt +++ b/PKHeX.WinForms/Resources/text/lang_es.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C-Gear Main.B_Clear=Limpiar Main.B_FestivalPlaza=Festi Plaza Main.B_JPEG=Guardar PGL .JPEG +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=Correo Main.B_MoveShop=Tienda Movs. Main.B_OpenApricorn=Bonguri diff --git a/PKHeX.WinForms/Resources/text/lang_fr.txt b/PKHeX.WinForms/Resources/text/lang_fr.txt index 513bd5958..691489dce 100644 --- a/PKHeX.WinForms/Resources/text/lang_fr.txt +++ b/PKHeX.WinForms/Resources/text/lang_fr.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=Fonds C-Gear Main.B_Clear=Effacer Main.B_FestivalPlaza=Place Festival Main.B_JPEG=Sauver image PGL +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=Boîte aux lettres Main.B_MoveShop=Move Shop Main.B_OpenApricorn=Noigrumes diff --git a/PKHeX.WinForms/Resources/text/lang_it.txt b/PKHeX.WinForms/Resources/text/lang_it.txt index 5c670adb9..74222fbd9 100644 --- a/PKHeX.WinForms/Resources/text/lang_it.txt +++ b/PKHeX.WinForms/Resources/text/lang_it.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C-Gear Skin Main.B_Clear=Pulisci Main.B_FestivalPlaza=Festiplaza Main.B_JPEG=Salva PGL .JPEG +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=Messaggi Main.B_MoveShop=Negozio Mosse Main.B_OpenApricorn=Ghicocche diff --git a/PKHeX.WinForms/Resources/text/lang_ja.txt b/PKHeX.WinForms/Resources/text/lang_ja.txt index 60bd21f5d..4e3e98e1f 100644 --- a/PKHeX.WinForms/Resources/text/lang_ja.txt +++ b/PKHeX.WinForms/Resources/text/lang_ja.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=Cギア スキン Main.B_Clear=Clear Main.B_FestivalPlaza=フェスサークル Main.B_JPEG=PGL 画像保存 +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=メールボックス Main.B_MoveShop=Move Shop Main.B_OpenApricorn=ぼんぐりのみ diff --git a/PKHeX.WinForms/Resources/text/lang_ko.txt b/PKHeX.WinForms/Resources/text/lang_ko.txt index 6d637f644..aea72dfd1 100644 --- a/PKHeX.WinForms/Resources/text/lang_ko.txt +++ b/PKHeX.WinForms/Resources/text/lang_ko.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C기어 스킨 Main.B_Clear=지우기 Main.B_FestivalPlaza=페스서클 Main.B_JPEG=PGL .JPEG 저장 +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=메일박스 Main.B_MoveShop=Move Shop Main.B_OpenApricorn=규토리 diff --git a/PKHeX.WinForms/Resources/text/lang_zh.txt b/PKHeX.WinForms/Resources/text/lang_zh.txt index d5d3b0c1a..842284077 100644 --- a/PKHeX.WinForms/Resources/text/lang_zh.txt +++ b/PKHeX.WinForms/Resources/text/lang_zh.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C装置皮肤 Main.B_Clear=清理 Main.B_FestivalPlaza=圆庆广场 Main.B_JPEG=保存PGL.JPEG +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=邮箱 Main.B_MoveShop=招式商店 Main.B_OpenApricorn=球果 diff --git a/PKHeX.WinForms/Resources/text/lang_zh2.txt b/PKHeX.WinForms/Resources/text/lang_zh2.txt index 4732e8236..4676d459e 100644 --- a/PKHeX.WinForms/Resources/text/lang_zh2.txt +++ b/PKHeX.WinForms/Resources/text/lang_zh2.txt @@ -177,6 +177,7 @@ Main.B_CGearSkin=C裝置皮膚 Main.B_Clear=清理 Main.B_FestivalPlaza=圓慶廣場 Main.B_JPEG=儲存PGL.JPEG +Main.B_ConvertKorean=Korean Save Conversion Main.B_MailBox=郵箱 Main.B_MoveShop=招式商店 Main.B_OpenApricorn=果球 diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Misc4.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Misc4.cs index b95ee8998..33144a239 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Misc4.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Misc4.cs @@ -46,7 +46,7 @@ public SAV_Misc4(SAV4 sav) new[] { 2, 0, 0x696C, 0x10, 0x7F00 }, new[] { 0, 0, 0x699C, 0x04, 0x7F04 }, }; - Hall = FetchHallBlock(SAV, 0x2820); + Hall = SAV.GetHall(); break; case GameVersion.HG or GameVersion.SS or GameVersion.HGSS: ofsFlag = 0x10C4; @@ -63,7 +63,7 @@ public SAV_Misc4(SAV4 sav) new[] { 2, 0, 0x52F0, 0x10, 0x6884 }, new[] { 0, 0, 0x5320, 0x04, 0x6888 }, }; - Hall = FetchHallBlock(SAV, 0x230C); + Hall = SAV.GetHall(); break; default: return; } @@ -514,31 +514,6 @@ private void ReadBattleFrontier() CB_Stats1.SelectedIndex = 0; } - private static Hall4? FetchHallBlock(SAV4 sav, int magicKeyOffset) - { - for (int i = 0; i < 2; i++, magicKeyOffset += 0x14) - { - var h = ReadInt32LittleEndian(sav.General[magicKeyOffset..]); - if (h == -1) - continue; - - for (int j = 0; j < 0x20; j++) - { - for (int k = 0, a = (j + 0x20) << 12; k < 2; k++, a += 0x40000) - { - var span = sav.Data.AsSpan(a); - if (h != ReadInt32LittleEndian(span)) - continue; - if (ReadInt16LittleEndian(span[0xBA8..]) != 0xBA0) - continue; - return new Hall4(sav.Data, a); - } - } - } - - return null; - } - private void SaveBattleFrontier() { if (ofsPrints > 0)