From 894ea1d6280073c2d8f710fdcd97ed0485f01853 Mon Sep 17 00:00:00 2001
From: abcboy101 <16735361+abcboy101@users.noreply.github.com>
Date: Thu, 9 Nov 2023 02:33:40 -0500
Subject: [PATCH] Add conversion for Gen 4 saves between KO and JP/INTL formats
(#4057)
* Refactor Gen 4 extra blocks
* Replace FetchHallBlock with extra block getter
* Add UI to convert save to/from Korean
* Do not modify uninitialized General/Storage blocks
* Detect invalid extra blocks
---
.../text/program/MessageStrings_de.txt | 2 +
.../text/program/MessageStrings_en.txt | 2 +
.../text/program/MessageStrings_es.txt | 2 +
.../text/program/MessageStrings_fr.txt | 2 +
.../text/program/MessageStrings_it.txt | 2 +
.../text/program/MessageStrings_ja.txt | 2 +
.../text/program/MessageStrings_ko.txt | 2 +
.../text/program/MessageStrings_zh.txt | 2 +
.../text/program/MessageStrings_zh2.txt | 2 +
PKHeX.Core/Saves/Blocks/BlockInfo4.cs | 81 +++++++++++++++++++
PKHeX.Core/Saves/SAV4.cs | 70 ++++++++++++++++
PKHeX.Core/Saves/SAV4DP.cs | 3 +
PKHeX.Core/Saves/SAV4HGSS.cs | 9 +++
PKHeX.Core/Saves/SAV4Pt.cs | 9 +++
.../Substructures/Gen4/SAV4BlockDetection.cs | 54 +++++++++++++
PKHeX.Core/Util/MessageStrings.cs | 2 +
.../Controls/SAV Editor/SAVEditor.Designer.cs | 22 ++++-
.../Controls/SAV Editor/SAVEditor.cs | 15 ++++
PKHeX.WinForms/Resources/text/lang_de.txt | 1 +
PKHeX.WinForms/Resources/text/lang_en.txt | 1 +
PKHeX.WinForms/Resources/text/lang_es.txt | 1 +
PKHeX.WinForms/Resources/text/lang_fr.txt | 1 +
PKHeX.WinForms/Resources/text/lang_it.txt | 1 +
PKHeX.WinForms/Resources/text/lang_ja.txt | 1 +
PKHeX.WinForms/Resources/text/lang_ko.txt | 1 +
PKHeX.WinForms/Resources/text/lang_zh.txt | 1 +
PKHeX.WinForms/Resources/text/lang_zh2.txt | 1 +
.../Subforms/Save Editors/Gen4/SAV_Misc4.cs | 29 +------
28 files changed, 290 insertions(+), 31 deletions(-)
create mode 100644 PKHeX.Core/Saves/Blocks/BlockInfo4.cs
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)