mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-10 06:34:19 +00:00
Add NSO save handler for Gen1/2 saves
Closes #3921 Not sure if the RTC handling is correct (always 0x20 length?) but at least we have a non-fixed-sized header handled with some leniency for different builds.
This commit is contained in:
parent
88569d1107
commit
8b5e4e798b
9 changed files with 192 additions and 11 deletions
|
@ -28,6 +28,7 @@ public sealed record SaveFileMetadata(SaveFile SAV)
|
|||
|
||||
private byte[] Footer = []; // .dsv
|
||||
private byte[] Header = []; // .gci
|
||||
private ISaveHandler? Handler;
|
||||
|
||||
private string BAKSuffix => $" [{SAV.ShortSummary}].bak";
|
||||
|
||||
|
@ -53,19 +54,22 @@ public sealed record SaveFileMetadata(SaveFile SAV)
|
|||
public byte[] Finalize(byte[] data, BinaryExportSetting setting)
|
||||
{
|
||||
if (Footer.Length > 0 && setting.HasFlag(BinaryExportSetting.IncludeFooter))
|
||||
return [..data, ..Footer];
|
||||
data = [..data, ..Footer];
|
||||
if (Header.Length > 0 && setting.HasFlag(BinaryExportSetting.IncludeHeader))
|
||||
return [..Header, ..data];
|
||||
data = [..Header, ..data];
|
||||
if (setting != BinaryExportSetting.None)
|
||||
Handler?.Finalize(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the details of any trimmed header and footer arrays to a <see cref="SaveFile"/> object.
|
||||
/// </summary>
|
||||
public void SetExtraInfo(byte[] header, byte[] footer)
|
||||
public void SetExtraInfo(byte[] header, byte[] footer, ISaveHandler handler)
|
||||
{
|
||||
Header = header;
|
||||
Footer = footer;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -128,7 +132,7 @@ public sealed record SaveFileMetadata(SaveFile SAV)
|
|||
var flags = BinaryExportSetting.None;
|
||||
if (ext == ".dsv")
|
||||
flags |= BinaryExportSetting.IncludeFooter;
|
||||
if (ext == ".gci" || SAV is IGCSaveFile {MemoryCard: null})
|
||||
if (ext == ".gci" || SAV is IGCSaveFile {MemoryCard: null} || ext == ".sram")
|
||||
flags |= BinaryExportSetting.IncludeHeader;
|
||||
return flags;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,12 @@ public interface ISaveHandler
|
|||
/// <param name="input">Combined data</param>
|
||||
/// <returns>Null if not a valid save file for this handler's format. Returns an object containing header, footer, and inner data references.</returns>
|
||||
SaveHandlerSplitResult? TrySplit(ReadOnlySpan<byte> input);
|
||||
|
||||
/// <summary>
|
||||
/// When exporting a save file, the handler might want to update the header/footer.
|
||||
/// </summary>
|
||||
/// <param name="input">Combined data</param>
|
||||
void Finalize(Span<byte> input);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ public sealed class SaveHandlerARDS : ISaveHandler
|
|||
// No authentication to see if it actually is a header; no size collisions expected.
|
||||
var header = input[..sizeHeader].ToArray();
|
||||
var data = input[sizeHeader..].ToArray();
|
||||
return new SaveHandlerSplitResult(data, header, []);
|
||||
return new SaveHandlerSplitResult(data, header, [], this);
|
||||
}
|
||||
|
||||
public void Finalize(Span<byte> data) { }
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ public sealed class SaveHandlerBizHawk : ISaveHandler
|
|||
var footer = input[realSize..].ToArray();
|
||||
var data = input[..realSize].ToArray();
|
||||
|
||||
return new SaveHandlerSplitResult(data, [], footer);
|
||||
return new SaveHandlerSplitResult(data, [], footer, this);
|
||||
}
|
||||
|
||||
public void Finalize(Span<byte> data) { }
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ public sealed class SaveHandlerDeSmuME : ISaveHandler
|
|||
var footer = input[RealSize..].ToArray();
|
||||
var data = input[..RealSize].ToArray();
|
||||
|
||||
return new SaveHandlerSplitResult(data, [], footer);
|
||||
return new SaveHandlerSplitResult(data, [], footer, this);
|
||||
}
|
||||
|
||||
public void Finalize(Span<byte> data) { }
|
||||
}
|
||||
|
|
|
@ -55,9 +55,11 @@ public sealed class SaveHandlerGCI : ISaveHandler
|
|||
var header = input[..headerSize].ToArray();
|
||||
var data = input[headerSize..].ToArray();
|
||||
|
||||
return new SaveHandlerSplitResult(data, header, []);
|
||||
return new SaveHandlerSplitResult(data, header, [], this);
|
||||
}
|
||||
|
||||
public void Finalize(Span<byte> data) { }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the game code is one of the recognizable versions.
|
||||
/// </summary>
|
||||
|
|
161
PKHeX.Core/Saves/Util/Recognition/SaveHandlerNSO.cs
Normal file
161
PKHeX.Core/Saves/Util/Recognition/SaveHandlerNSO.cs
Normal file
|
@ -0,0 +1,161 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static PKHeX.Core.HeaderInfoNSO;
|
||||
|
||||
namespace PKHeX.Core;
|
||||
|
||||
public sealed class SaveHandlerNSO : ISaveHandler
|
||||
{
|
||||
private static ReadOnlySpan<byte> Magic => "SRAM"u8;
|
||||
|
||||
public bool IsRecognized(long size) => IsPlausible(size);
|
||||
|
||||
public static ReadOnlySpan<byte> GetHashROM(ReadOnlySpan<byte> header) => new UnpackedHeaderNSOReadOnly(header).HashROM;
|
||||
|
||||
public SaveHandlerSplitResult? TrySplit(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length < SmallestSize)
|
||||
return null;
|
||||
if (!input.StartsWith(Magic))
|
||||
return null;
|
||||
|
||||
var obj = new UnpackedHeaderNSOReadOnly(input);
|
||||
var header = input[..obj.Length];
|
||||
var data = input[obj.Length..];
|
||||
return new SaveHandlerSplitResult([..data], [..header], [], this);
|
||||
}
|
||||
|
||||
public void Finalize(Span<byte> input)
|
||||
{
|
||||
// Need to re-sign the save file.
|
||||
var obj = new UnpackedHeaderNSO(input);
|
||||
var hashSave = obj.SaveHash;
|
||||
var data = input[obj.Length..];
|
||||
WriteHash(data, hashSave);
|
||||
}
|
||||
|
||||
public static void WriteHash(ReadOnlySpan<byte> input, Span<byte> result)
|
||||
{
|
||||
// Compute hash into the result span, then inflate in place.
|
||||
System.Security.Cryptography.SHA1.HashData(input, result);
|
||||
InflateAscii(result);
|
||||
}
|
||||
|
||||
public static void InflateAscii(Span<byte> data)
|
||||
{
|
||||
// Since we're expanding in-place, our current hash is 20 bytes.
|
||||
// We can expand backwards; no byte will be overwritten until it is no longer needed.
|
||||
for (int i = 19, j = data.Length - 2; i >= 0; i--, j -= 2)
|
||||
{
|
||||
byte b = data[i];
|
||||
data[j + 1] = GetHexCharLower(b & 0x0F);
|
||||
data[j] = GetHexCharLower(b >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte GetHexCharLower(int value) => (byte)(value < 10 ? value + '0' : value - 10 + 'a');
|
||||
}
|
||||
|
||||
public static class HeaderInfoNSO
|
||||
{
|
||||
public const int SIZE_HASH = 40; // Inflated from bytes to ASCII chars
|
||||
public const int OffsetROM = 0x08;
|
||||
public const int SIZE_RTC = 0x20;
|
||||
|
||||
// Anatomy:
|
||||
// 0x00 - 0x03: Magic
|
||||
// 0x04 - 0x07: (u32 ???) 3?
|
||||
// 0x08 - 0x2F: SHA1 hash of the ROM
|
||||
// 0x30 - 0x33: Length of following Ascii string
|
||||
// 0x34: u8[length] Ascii Build
|
||||
// .... bool8 HasRTC
|
||||
// .... {if true, 0x20 bytes of RTC}
|
||||
// SHA1 hash of the save data
|
||||
// remainder: save data
|
||||
|
||||
public const int SmallestSize = SaveUtil.SIZE_G1RAW;
|
||||
|
||||
public static ReadOnlySpan<int> AllowedBaseSizes =>
|
||||
[
|
||||
SaveUtil.SIZE_G1RAW, // smallest
|
||||
SaveUtil.SIZE_G2RAW_J,
|
||||
];
|
||||
|
||||
public static bool IsPlausible(long size)
|
||||
{
|
||||
foreach (var inner in AllowedBaseSizes)
|
||||
{
|
||||
if (IsPlausible(size, inner))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPlausible(long size, int expected)
|
||||
{
|
||||
// allow different builds and RTC presence
|
||||
var delta = (uint)(size - expected);
|
||||
return delta is >= 0x68 and <= 0xA0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Readonly version of <see cref="UnpackedHeaderNSO"/>
|
||||
/// </summary>
|
||||
public readonly ref struct UnpackedHeaderNSOReadOnly
|
||||
{
|
||||
public readonly ReadOnlySpan<byte> HashROM;
|
||||
public readonly ReadOnlySpan<byte> Build;
|
||||
public readonly bool HasRTC;
|
||||
public readonly ReadOnlySpan<byte> RTC;
|
||||
public readonly ReadOnlySpan<byte> HashSaveFile;
|
||||
public readonly int Length; // of this header
|
||||
|
||||
public UnpackedHeaderNSOReadOnly(ReadOnlySpan<byte> input)
|
||||
{
|
||||
HashROM = input.Slice(OffsetROM, SIZE_HASH);
|
||||
var length = input[0x30]; // really is a 32-bit number, but is never bigger than 255.
|
||||
Build = input.Slice(0x34, length);
|
||||
|
||||
var ofs = 0x34 + length;
|
||||
HasRTC = input[ofs++] != 0;
|
||||
if (HasRTC)
|
||||
{
|
||||
RTC = input.Slice(ofs, SIZE_RTC);
|
||||
ofs += SIZE_RTC;
|
||||
}
|
||||
HashSaveFile = input.Slice(ofs, SIZE_HASH); ofs += SIZE_HASH;
|
||||
Length = ofs;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacked Header of an NSO save file's variable-sized header.
|
||||
/// </summary>
|
||||
public readonly ref struct UnpackedHeaderNSO
|
||||
{
|
||||
public readonly Span<byte> ROMHash;
|
||||
public readonly Span<byte> Build;
|
||||
public readonly bool HasRTC;
|
||||
public readonly Span<byte> RTC;
|
||||
public readonly Span<byte> SaveHash;
|
||||
public readonly int Length;
|
||||
|
||||
public UnpackedHeaderNSO(Span<byte> input)
|
||||
{
|
||||
ROMHash = input.Slice(OffsetROM, SIZE_HASH);
|
||||
var length = input[0x30]; // really is a 32-bit number, but is never bigger than 255.
|
||||
Build = input.Slice(0x34, length);
|
||||
|
||||
var ofs = 0x34 + length;
|
||||
HasRTC = input[ofs++] != 0;
|
||||
if (HasRTC)
|
||||
{
|
||||
RTC = input.Slice(ofs, SIZE_RTC);
|
||||
ofs += SIZE_RTC;
|
||||
}
|
||||
SaveHash = input.Slice(ofs, SIZE_HASH); ofs += SIZE_HASH;
|
||||
Length = ofs;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
namespace PKHeX.Core;
|
||||
|
||||
public sealed class SaveHandlerSplitResult(byte[] Data, byte[] Header, byte[] Footer)
|
||||
public sealed class SaveHandlerSplitResult(byte[] Data, byte[] Header, byte[] Footer, ISaveHandler Handler)
|
||||
{
|
||||
public readonly byte[] Header = Header;
|
||||
public readonly byte[] Footer = Footer;
|
||||
public readonly byte[] Data = Data;
|
||||
public readonly ISaveHandler Handler = Handler;
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ public static class SaveUtil
|
|||
new SaveHandlerDeSmuME(),
|
||||
new SaveHandlerBizHawk(),
|
||||
new SaveHandlerARDS(),
|
||||
new SaveHandlerNSO(),
|
||||
];
|
||||
#endif
|
||||
|
||||
|
@ -699,7 +700,7 @@ public static class SaveUtil
|
|||
continue;
|
||||
|
||||
var meta = sav.Metadata;
|
||||
meta.SetExtraInfo(split.Header, split.Footer);
|
||||
meta.SetExtraInfo(split.Header, split.Footer, split.Handler);
|
||||
if (path is not null)
|
||||
meta.SetExtraInfo(path);
|
||||
return sav;
|
||||
|
@ -787,7 +788,7 @@ public static class SaveUtil
|
|||
}
|
||||
|
||||
if (split != null)
|
||||
sav.Metadata.SetExtraInfo(split.Header, split.Footer);
|
||||
sav.Metadata.SetExtraInfo(split.Header, split.Footer, split.Handler);
|
||||
return sav;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue