Skip to content

Instantly share code, notes, and snippets.

@NotNite
Last active March 31, 2023 17:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NotNite/3213bda8ffe060f6149b67733416b76d to your computer and use it in GitHub Desktop.
Save NotNite/3213bda8ffe060f6149b67733416b76d to your computer and use it in GitHub Desktop.
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