mirror of
https://github.com/kwsch/NHSE.git
synced 2026-08-23 02:55:24 -05:00
Add EffectiveGuacamole port
simple to understand; no longer need to run a separate toolchain if there (n)ever is an update in the future.
This commit is contained in:
102
NHSE.Core/Save/Meta/EffectiveGuacamole.cs
Normal file
102
NHSE.Core/Save/Meta/EffectiveGuacamole.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Utility to scan save file parts for their hashed regions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses the same bruteforce approach as https://github.com/3096/effective-guacamole/blob/3bc30c4584cde0ebc526c1bfec3394ea1df1d475/generate_hash_sections/main.cpp
|
||||
/// Data files are {gap, hash+(hashed-data)}[] where gap is up to 0x110 bytes of non-hashed data.
|
||||
/// The gap is 0, 0x10, 0x100, or 0x110, but we'll check for % 0x10 increments.
|
||||
/// Run this static method by feeding in your savedata folder path, and it will output to console.
|
||||
///
|
||||
/// ```csharp
|
||||
/// const string folder = @"E:\acnh\matt30";
|
||||
/// EffectiveGuacamole.DumpHashes(folder);
|
||||
/// ```
|
||||
/// </remarks>
|
||||
public static class EffectiveGuacamole
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes out detected hash offsets and lengths for all files in the given folder.
|
||||
/// </summary>
|
||||
/// <param name="folder">Save folder to scan.</param>
|
||||
public static void DumpHashes(string folder)
|
||||
{
|
||||
// Scan for all .dat files
|
||||
var files = Directory.EnumerateFiles(folder, "*.dat", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(file);
|
||||
if (name.EndsWith("Header"))
|
||||
continue;
|
||||
|
||||
var parent = Path.GetDirectoryName(file)!;
|
||||
DumpHashes(parent, name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DumpHashes(string folder, string file)
|
||||
{
|
||||
// Ensure header exists for decryption
|
||||
var hdr = Path.Combine(folder, $"{file}Header.dat");
|
||||
if (!File.Exists(hdr))
|
||||
return;
|
||||
|
||||
// Decrypt file
|
||||
var dat = Path.Combine(folder, $"{file}.dat");
|
||||
var hd = File.ReadAllBytes(hdr);
|
||||
var md = File.ReadAllBytes(dat);
|
||||
Encryption.Decrypt(hd, md);
|
||||
|
||||
// Brute-force scan for hashes
|
||||
var hashes = ScanHashes(md);
|
||||
|
||||
// Output results
|
||||
Console.WriteLine(dat);
|
||||
foreach (var hash in hashes)
|
||||
Console.WriteLine($"(0x{hash.HashOffset:x6}, 0x{hash.HashLength:x6}),");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
public static List<(int HashOffset, int HashLength)> ScanHashes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var results = new List<(int HashOffset, int HashLength)>();
|
||||
if (TryPopulate(data, results, 0x100))
|
||||
return results;
|
||||
return [];
|
||||
}
|
||||
|
||||
private static bool TryPopulate(ReadOnlySpan<byte> data, List<(int HashOffset, int HashLength)> results, int offset)
|
||||
{
|
||||
results.Clear();
|
||||
var gap = 0;
|
||||
while (true)
|
||||
{
|
||||
if (offset + 4 >= data.Length)
|
||||
break; // end
|
||||
|
||||
var hash = ReadUInt32LittleEndian(data[offset..]);
|
||||
var possible = data[(offset + 4)..];
|
||||
|
||||
var length = Murmur3.GetLength(possible, hash);
|
||||
if (length == -1)
|
||||
{
|
||||
if (gap >= 0x110) // max gap
|
||||
throw new Exception("Max gap exceeded.");
|
||||
offset += 0x10;
|
||||
gap += 0x10;
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add((offset, length));
|
||||
offset += 4 + length;
|
||||
gap = 0;
|
||||
}
|
||||
return results.Count != 0 && offset == data.Length;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user