Last active
March 31, 2023 17:18
-
-
Save NotNite/3213bda8ffe060f6149b67733416b76d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Runtime.InteropServices; | |
using Dalamud.Data; | |
using Dalamud.Game; | |
using FFXIVClientStructs.Havok; | |
namespace Toolkitty.Utils; | |
// shoutouts aers, winter, perch, couldn't have done it without you besties | |
// supported: | |
// sklb->hkx (SklbToHkx) | |
// sklb->xml (SklbToXml) | |
// hkx->xml (HkxToXml) | |
// xml->hkx (XmlToHkx) | |
// hkx->sklb (SpliceSklb) | |
public unsafe class Havok { | |
[Flags] | |
internal enum hkSerializeUtil_SaveOptionBits : int { | |
SAVE_DEFAULT = 0x0, | |
SAVE_TEXT_FORMAT = 0x1, | |
SAVE_SERIALIZE_IGNORED_MEMBERS = 0x2, | |
SAVE_WRITE_ATTRIBUTES = 0x4, | |
SAVE_CONCISE = 0x8 | |
} | |
internal enum hkSerializeUtil_LoadOptionBits : int { | |
LOAD_DEFAULT = 0, | |
LOAD_FAIL_IF_VERSIONING = 1, | |
LOAD_FORCED = 2 | |
} | |
internal struct hkTypeInfoRegistry { } | |
internal struct hkClassNameRegistry { } | |
[StructLayout(LayoutKind.Explicit, Size = 0x18)] | |
internal struct hkSerializeUtil_LoadOptions { | |
[FieldOffset(0x0)] | |
internal hkEnum<hkSerializeUtil_LoadOptionBits, int> options; | |
[FieldOffset(0x8)] | |
internal hkClassNameRegistry* m_classNameReg; | |
[FieldOffset(0x10)] | |
internal hkTypeInfoRegistry* m_typeInfoReg; | |
} | |
[StructLayout(LayoutKind.Explicit, Size = 0x18)] | |
internal struct hkOStream { | |
[FieldOffset(0x10)] | |
internal hkRefPtr<hkStreamWriter> m_writer; | |
} | |
[StructLayout(LayoutKind.Explicit, Size = 0x10)] | |
internal struct hkStreamWriter { } | |
[StructLayout(LayoutKind.Explicit, Size = 0x10)] | |
internal struct hkStreamReader { } | |
[StructLayout(LayoutKind.Explicit, Size = 0x50)] | |
internal struct hkClass { } | |
[StructLayout(LayoutKind.Explicit, Size = 0x48)] | |
internal struct hkResourceVtbl { | |
[FieldOffset(0x38)] | |
internal delegate* unmanaged[Stdcall] <void*, byte*, void*, void*> getContentsPointer; | |
} | |
[StructLayout(LayoutKind.Explicit, Size = 0x8)] // larger | |
internal struct hkBuiltinTypeRegistry { | |
[FieldOffset(0x0)] | |
internal hkBuiltinTypeRegistryVtbl* vtbl; | |
} | |
[StructLayout(LayoutKind.Explicit, Size = 0x48)] | |
internal struct hkBuiltinTypeRegistryVtbl { | |
[FieldOffset(0x20)] | |
internal delegate* unmanaged[Stdcall] <hkBuiltinTypeRegistry*, hkTypeInfoRegistry*> GetTypeInfoRegistry; | |
[FieldOffset(0x28)] | |
internal delegate* unmanaged[Stdcall] <hkBuiltinTypeRegistry*, hkClassNameRegistry*> GetClassNameRegistry; | |
} | |
private delegate hkResource* hkSerializeUtil_LoadDelegate( | |
char* path, | |
void* idk, | |
hkSerializeUtil_LoadOptions* options | |
); | |
private delegate hkResult* hkSerializeUtil_SaveDelegate( | |
hkResult* result, | |
void* obj, | |
hkClass* klass, | |
hkStreamWriter* writer, | |
hkFlags<hkSerializeUtil_SaveOptionBits, uint> flags | |
); | |
private delegate void hkOstream_CtorDelegate(hkOStream* self, byte* streamWriter); | |
private delegate void hkOstream_DtorDelegate(hkOStream* self); | |
private hkSerializeUtil_LoadDelegate hkSerializeUtil_Load; | |
private hkSerializeUtil_SaveDelegate hkSerializeUtil_Save; | |
private hkOstream_CtorDelegate hkOstream_Ctor; | |
private hkOstream_DtorDelegate hkOstream_Dtor; | |
private hkClass* hkRootLevelContainerClass; | |
private hkBuiltinTypeRegistry* hkBuiltinTypeRegistrySingleton; | |
private SigScanner sigScanner; | |
private DataManager dataManager; | |
private T GetDelegate<T>(string sig) { | |
var addr = this.sigScanner.ScanText(sig); | |
return Marshal.GetDelegateForFunctionPointer<T>(addr); | |
} | |
public Havok(SigScanner scanner, DataManager dataManager) { | |
this.sigScanner = scanner; | |
this.dataManager = dataManager; | |
// FIXME: sigs look unstable | |
// last updated 2023.02.28.0000.0000 | |
this.hkSerializeUtil_Load = this.GetDelegate<hkSerializeUtil_LoadDelegate>( | |
"40 53 48 83 EC 60 41 0F 10 00 48 8B DA 48 8B D1 F2 41 0F 10 48 ?? 48 8D 4C 24 ?? 0F 29 44 24 ?? F2 0F 11 4C 24 ?? E8 ?? ?? ?? ?? 4C 8D 44 24 ?? 48 8B D3 48 8B 48 10 E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 8B D8 E8 ?? ?? ?? ?? 48 8B C3 48 83 C4 60 5B C3 CC CC CC CC CC CC CC CC CC CC CC CC CC CC 48 89 5C 24 ??" | |
); | |
this.hkSerializeUtil_Save = this.GetDelegate<hkSerializeUtil_SaveDelegate>( | |
"40 53 48 83 EC 30 8B 44 24 60 48 8B D9 89 44 24 28" | |
); | |
this.hkOstream_Ctor = this.GetDelegate<hkOstream_CtorDelegate>( | |
"48 89 5C 24 ?? 57 48 83 EC 20 C7 41 ?? ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 01 48 8B F9 48 C7 41 ?? ?? ?? ?? ?? 4C 8B C2 48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 41 B9 ?? ?? ?? ?? 48 8B 01 FF 50 28" | |
); | |
this.hkOstream_Dtor = this.GetDelegate<hkOstream_DtorDelegate>( | |
"E8 ?? ?? ?? ?? 44 8B 44 24 ?? 4C 8B 7C 24 ??" | |
); | |
this.hkRootLevelContainerClass = (hkClass*) this.sigScanner.GetStaticAddressFromSig( | |
"48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 83 C4 78 C3 CC CC CC 48 83 EC 78 33 C9 C7 44 24 ?? ?? ?? ?? ?? 89 4C 24 60 48 8D 05 ?? ?? ?? ?? 48 89 4C 24 ?? 48 8D 15 ?? ?? ?? ?? 48 89 4C 24 ?? 45 33 C0 C7 44 24 ?? ?? ?? ?? ?? 44 8D 49 18" | |
); | |
// My formatter hates this line so i'm splitting it into two | |
var typeRegistry = this.sigScanner.GetStaticAddressFromSig( | |
"48 8B 0D ?? ?? ?? ?? 48 8B 01 FF 50 20 4C 8B 0F 48 8B D3 4C 8B C0 48 8B CF 48 8B 5C 24 ?? 48 83 C4 20 5F 49 FF 61 48" | |
); | |
this.hkBuiltinTypeRegistrySingleton = *(hkBuiltinTypeRegistry**) typeRegistry; | |
} | |
private string CreateTempFile() { | |
var s = File.Create(Path.GetTempFileName()); | |
s.Close(); | |
return s.Name; | |
} | |
private int? GetHavokOffset(byte[] bytes) { | |
var reader = new BinaryReader(new MemoryStream(bytes)); | |
var magic = reader.ReadInt32(); | |
if (magic != 0x736B6C62) { | |
return null; | |
} | |
var version = reader.ReadInt32(); | |
if (version != 0x31333030) { | |
// version one | |
reader.ReadInt16(); // skip unkOffset | |
return reader.ReadInt16(); | |
} else { | |
// version two | |
reader.ReadInt32(); // skip unkOffset | |
return reader.ReadInt32(); | |
} | |
} | |
public byte[]? SklbToHkx(byte[] sklb) { | |
var offset = this.GetHavokOffset(sklb); | |
if (offset == null) return null; | |
var offsetValue = offset.Value; | |
return sklb[offsetValue..]; | |
} | |
public string? SklbToXml(byte[] sklb) { | |
var hkx = this.SklbToHkx(sklb); | |
if (hkx == null) return null; | |
return HkxToXml(hkx); | |
} | |
public string? HkxToXml(byte[] xml) { | |
var tempHkx = this.CreateTempFile(); | |
File.WriteAllBytes(tempHkx, xml); | |
var resource = this.Read(tempHkx); | |
File.Delete(tempHkx); | |
if (resource == null) return null; | |
var options = (uint) (hkSerializeUtil_SaveOptionBits.SAVE_SERIALIZE_IGNORED_MEMBERS | |
| hkSerializeUtil_SaveOptionBits.SAVE_TEXT_FORMAT | |
| hkSerializeUtil_SaveOptionBits.SAVE_WRITE_ATTRIBUTES); | |
var file = this.Write(resource, options); | |
if (file == null) return null; | |
file.Close(); | |
var bytes = File.ReadAllText(file.Name); | |
File.Delete(file.Name); | |
return bytes; | |
} | |
public byte[]? XmlToHkx(string xml) { | |
var tempXml = this.CreateTempFile(); | |
File.WriteAllText(tempXml, xml); | |
var resource = this.Read(tempXml); | |
File.Delete(tempXml); | |
if (resource == null) return null; | |
var options = (uint) (hkSerializeUtil_SaveOptionBits.SAVE_SERIALIZE_IGNORED_MEMBERS | |
| hkSerializeUtil_SaveOptionBits.SAVE_WRITE_ATTRIBUTES); | |
var file = this.Write(resource, options); | |
if (file == null) return null; | |
file.Close(); | |
var bytes = File.ReadAllBytes(file.Name); | |
File.Delete(file.Name); | |
return bytes; | |
} | |
public byte[]? SpliceSklb(string origPath, byte[] hkx) { | |
var file = this.dataManager.GetFile(origPath); | |
if (file == null) return null; | |
var offset = this.GetHavokOffset(file.Data); | |
if (offset == null) return null; | |
var header = file.Data[..offset.Value]; | |
var newFile = new byte[header.Length + hkx.Length]; | |
header.CopyTo(newFile, 0); | |
hkx.CopyTo(newFile, header.Length); | |
return newFile; | |
} | |
private hkResource* Read(string filePath) { | |
var path = Marshal.StringToHGlobalAnsi(filePath); | |
hkSerializeUtil_LoadOptions* loadOptions = stackalloc hkSerializeUtil_LoadOptions[1]; | |
loadOptions->m_typeInfoReg = | |
hkBuiltinTypeRegistrySingleton->vtbl->GetTypeInfoRegistry(hkBuiltinTypeRegistrySingleton); | |
loadOptions->m_classNameReg = | |
hkBuiltinTypeRegistrySingleton->vtbl->GetClassNameRegistry(hkBuiltinTypeRegistrySingleton); | |
loadOptions->options = new hkEnum<hkSerializeUtil_LoadOptionBits, int>(); | |
loadOptions->options.Storage = (int) hkSerializeUtil_LoadOptionBits.LOAD_DEFAULT; | |
var resource = this.hkSerializeUtil_Load((char*) path, null, loadOptions); | |
return resource; | |
} | |
private FileStream? Write( | |
hkResource* resource, | |
uint optionBits | |
) { | |
hkOStream* oStream = stackalloc hkOStream[1]; | |
var tempFile = this.CreateTempFile(); | |
var path = Marshal.StringToHGlobalAnsi(tempFile); | |
hkOstream_Ctor(oStream, (byte*) path); | |
hkResult* result = stackalloc hkResult[1]; | |
var options = new hkFlags<hkSerializeUtil_SaveOptionBits, uint>(); | |
options.Storage = optionBits; | |
var name = @"hkRootLevelContainer"u8; | |
var resourceVtbl = *(hkResourceVtbl**) resource; | |
hkRootLevelContainer* resourcePtr; | |
fixed (byte* n = name) { | |
resourcePtr = (hkRootLevelContainer*) resourceVtbl->getContentsPointer( | |
resource, n, hkBuiltinTypeRegistrySingleton->vtbl->GetTypeInfoRegistry(hkBuiltinTypeRegistrySingleton)); | |
} | |
if (resourcePtr == null) { | |
this.hkOstream_Dtor(oStream); | |
return null; | |
} | |
hkSerializeUtil_Save(result, resourcePtr, hkRootLevelContainerClass, oStream->m_writer.ptr, options); | |
this.hkOstream_Dtor(oStream); | |
if (result->Result == hkResult.hkResultEnum.Failure) { | |
return null; | |
} | |
return new FileStream(tempFile, FileMode.Open); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment