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
This commit is contained in:
abcboy101 2023-11-09 02:33:40 -05:00 committed by GitHub
parent 1f6d2de891
commit 894ea1d628
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 290 additions and 31 deletions

View file

@ -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?

View file

@ -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?

View file

@ -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?

View file

@ -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 ?

View file

@ -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?

View file

@ -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?

View file

@ -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 = 체크섬 정보를 클립보드로 복사하시겠습니까?

View file

@ -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 = 导出校验值信息到剪贴板?

View file

@ -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 = 是否匯出校驗值信息至剪貼簿?

View file

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Gen4 Extra Block Info
/// </summary>
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<byte> data) => ReadUInt32LittleEndian(data[Offset..]);
public uint GetMagic(ReadOnlySpan<byte> data) => ReadUInt32LittleEndian(data[FooterOffset..]);
public uint GetRevision(ReadOnlySpan<byte> data) => ReadUInt32LittleEndian(data[(FooterOffset + 0x4)..]);
public int GetSize(ReadOnlySpan<byte> data) => ReadInt32LittleEndian(data[(FooterOffset + 0x8)..]);
public ushort GetID(ReadOnlySpan<byte> data) => ReadUInt16LittleEndian(data[(FooterOffset + 0xC)..]);
private ushort GetChecksum(ReadOnlySpan<byte> data) => Checksums.CRC16_CCITT(data.Slice(Offset, Length - 2));
private bool IsInitialized(ReadOnlySpan<byte> data)
{
return (ID == 0 && GetRevision(data) != 0xFFFFFFFF) || (ID != 0 && GetKey(data) != 0xFFFFFFFF);
}
public bool SizeValid(ReadOnlySpan<byte> data)
{
return GetSize(data) == Length;
}
protected override bool ChecksumValid(ReadOnlySpan<byte> data)
{
if (!IsInitialized(data))
return true;
ushort chk = GetChecksum(data);
if (chk != ReadUInt16LittleEndian(data[(FooterOffset + 14)..]))
return false;
return true;
}
public bool IsValid(ReadOnlySpan<byte> data)
{
return IsInitialized(data) && SizeValid(data) && ChecksumValid(data);
}
protected override void SetChecksum(Span<byte> data)
{
if (!IsInitialized(data))
return;
ushort chk = GetChecksum(data);
WriteUInt16LittleEndian(data[(FooterOffset + 14)..], chk);
}
protected void SetMagic(Span<byte> data, uint magic)
{
if (!IsInitialized(data))
return;
WriteUInt32LittleEndian(data[FooterOffset..], magic);
}
public static void SetMagics(IEnumerable<BlockInfo4> blocks, Span<byte> data, uint magic)
{
foreach (var b in blocks)
b.SetMagic(data, magic);
}
}
public static partial class Extensions
{
public static void SetMagics(this IEnumerable<BlockInfo4> blocks, Span<byte> data, uint magic) => BlockInfo4.SetMagics(blocks, data, magic);
}

View file

@ -27,6 +27,12 @@ public abstract class SAV4 : SaveFile, IEventFlag37
protected sealed override Span<byte> BoxBuffer => Storage;
protected sealed override Span<byte> PartyBuffer => General;
private readonly Memory<byte> BackupStorageBuffer;
private readonly Memory<byte> BackupGeneralBuffer;
private Span<byte> BackupStorage => BackupStorageBuffer.Span;
private Span<byte> BackupGeneral => BackupGeneralBuffer.Span;
protected abstract IReadOnlyList<BlockInfo4> ExtraBlocks { get; }
public abstract Zukan4 Dex { get; }
protected abstract int EventFlag { get; }
@ -38,6 +44,8 @@ public abstract class SAV4 : SaveFile, IEventFlag37
{
GeneralBuffer = new byte[gSize];
StorageBuffer = new byte[sSize];
BackupGeneralBuffer = new byte[gSize];
BackupStorageBuffer = new byte[sSize];
ClearBoxes();
}
@ -50,6 +58,11 @@ public abstract class SAV4 : SaveFile, IEventFlag37
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 abstract class SAV4 : SaveFile, IEventFlag37
private static ushort GetBlockChecksumSaved(ReadOnlySpan<byte> data) => ReadUInt16LittleEndian(data[^2..]);
private bool GetBlockChecksumValid(ReadOnlySpan<byte> 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 abstract class SAV4 : SaveFile, IEventFlag37
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 abstract class SAV4 : SaveFile, IEventFlag37
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 @@ public abstract class SAV4 : SaveFile, IEventFlag37
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 abstract class SAV4 : SaveFile, IEventFlag37
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);

View file

@ -30,6 +30,9 @@ public sealed class SAV4DP : SAV4Sinnoh
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()
{

View file

@ -32,6 +32,14 @@ public sealed class SAV4HGSS : SAV4
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 @@ public sealed class SAV4HGSS : SAV4
Trainer1 = 0x64;
Party = 0x98;
PokeDex = 0x12B8;
Extra = 0x230C;
Geonet = 0x8D44;
WondercardFlags = 0x9D3C;
WondercardData = 0x9E3C;

View file

@ -29,6 +29,14 @@ public sealed class SAV4Pt : SAV4Sinnoh
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 @@ public sealed class SAV4Pt : SAV4Sinnoh
Trainer1 = 0x68;
Party = 0xA0;
PokeDex = 0x1328;
Extra = 0x2820;
Geonet = 0xA4C4;
WondercardFlags = 0xB4C0;
WondercardData = 0xB5C0;

View file

@ -48,4 +48,58 @@ public static class SAV4BlockDetection
return Same;
}
/// <summary>
/// Compares two extra blocks to determine which is newest.
/// </summary>
/// <returns>0=Primary, 1=Secondary, -1=Uninitialized.</returns>
public static int CompareExtra(ReadOnlySpan<byte> data1, ReadOnlySpan<byte> 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<byte> data1, ReadOnlySpan<byte> 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
}
}

View file

@ -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.";

View file

@ -106,6 +106,7 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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 @@ namespace PKHeX.WinForms.Controls
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;
}
}

View file

@ -739,6 +739,19 @@ public partial class SAVEditor : UserControl, ISlotViewer<PictureBox>, ISaveFile
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 @@ public partial class SAVEditor : UserControl, ISlotViewer<PictureBox>, ISaveFile
{
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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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=ぼんぐりのみ

View file

@ -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=규토리

View file

@ -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=球果

View file

@ -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=果球

View file

@ -46,7 +46,7 @@ public partial class SAV_Misc4 : Form
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 partial class SAV_Misc4 : Form
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 @@ public partial class SAV_Misc4 : Form
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)