diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs
index 30dd0ee..a8d1097 100644
--- a/ExampleMod/ExampleMod.cs
+++ b/ExampleMod/ExampleMod.cs
@@ -9,6 +9,8 @@ namespace ExampleMod;
Description = "A sample mod demonstrating the WeaveLoader API")]
public class ExampleMod : IMod
{
+ private static nint s_currentLevel;
+ private static bool s_hasLevel;
public static RegisteredBlock? RubyOre;
public static RegisteredBlock? RubyStone;
public static RegisteredBlock? RubyWoodPlanks;
@@ -189,6 +191,17 @@ public class ExampleMod : IMod
public void OnInitialize()
{
+ GameEvents.OnWorldLoaded += (_, e) =>
+ {
+ s_currentLevel = e.NativeLevelPointer;
+ s_hasLevel = s_currentLevel != 0;
+ };
+ GameEvents.OnWorldUnloaded += (_, __) =>
+ {
+ s_currentLevel = 0;
+ s_hasLevel = false;
+ };
+
RubyOre = Registry.Block.Register("examplemod:ruby_ore",
new BlockProperties()
.Material(MaterialType.Stone)
@@ -391,4 +404,10 @@ public class ExampleMod : IMod
{
Logger.Info("Example Mod shutting down.");
}
+
+ internal static bool TryGetCurrentLevel(out nint levelPtr)
+ {
+ levelPtr = s_currentLevel;
+ return s_hasLevel && levelPtr != 0;
+ }
}
diff --git a/ExampleMod/ExampleMod.csproj b/ExampleMod/ExampleMod.csproj
index 859c996..7421baa 100644
--- a/ExampleMod/ExampleMod.csproj
+++ b/ExampleMod/ExampleMod.csproj
@@ -22,6 +22,7 @@
+
diff --git a/ExampleMod/Mixins/CreeperExplosionMixin.cs b/ExampleMod/Mixins/CreeperExplosionMixin.cs
new file mode 100644
index 0000000..a6af6ea
--- /dev/null
+++ b/ExampleMod/Mixins/CreeperExplosionMixin.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using WeaveLoader.API;
+using WeaveLoader.API.Mixins;
+using WeaveLoader.API.Native;
+
+namespace ExampleMod.Mixins;
+
+[Mixin("Creeper")]
+public static class CreeperExplosionMixin
+{
+ private const int Radius = 6;
+
+ private static readonly HashSet s_processed = new();
+ private static int s_triggerLogCount;
+ private static bool s_offsetsReady;
+ private static bool s_offsetsWarned;
+ private static int s_swellOffset;
+ private static int s_maxSwellOffset;
+ private static int s_isClientSideOffset;
+
+ [Inject("tick", At.Tail)]
+ private static void OnTick(MixinContext ctx)
+ {
+ nint thisPtr = ctx.ThisPtr;
+ if (thisPtr == 0 || ExampleMod.RubySand == null)
+ return;
+
+ nint levelPtr = 0;
+ if (NativeOffsets.TryGet("Entity", "level", out var levelOffset))
+ levelPtr = NativeMemory.ReadPtr(thisPtr, levelOffset);
+
+ if (levelPtr == 0 && !ExampleMod.TryGetCurrentLevel(out levelPtr))
+ return;
+
+ if (!EnsureOffsets())
+ return;
+
+ if (s_isClientSideOffset != 0 && NativeMemory.ReadBool(levelPtr, s_isClientSideOffset))
+ return;
+
+ int swell = NativeMemory.ReadInt32(thisPtr, s_swellOffset);
+ int maxSwell = NativeMemory.ReadInt32(thisPtr, s_maxSwellOffset);
+ if (swell < maxSwell)
+ return;
+
+ if (!NativeMemory.ReadBool(thisPtr, NativeOffsets.Entity.Removed))
+ return;
+
+ if (!s_processed.Add(thisPtr))
+ return;
+ if (s_processed.Count > 256)
+ s_processed.Clear();
+
+ if (s_triggerLogCount < 1)
+ {
+ Logger.Info($"CreeperExplosionMixin triggered at ({NativeMemory.ReadDouble(thisPtr, NativeOffsets.Entity.X):F2}, {NativeMemory.ReadDouble(thisPtr, NativeOffsets.Entity.Y):F2}, {NativeMemory.ReadDouble(thisPtr, NativeOffsets.Entity.Z):F2})");
+ s_triggerLogCount++;
+ }
+
+ int x = (int)Math.Floor(NativeMemory.ReadDouble(thisPtr, NativeOffsets.Entity.X));
+ int y = (int)Math.Floor(NativeMemory.ReadDouble(thisPtr, NativeOffsets.Entity.Y));
+ int z = (int)Math.Floor(NativeMemory.ReadDouble(thisPtr, NativeOffsets.Entity.Z));
+ PaintSphereTop(levelPtr, x, y, z);
+ }
+
+ private static bool EnsureOffsets()
+ {
+ if (s_offsetsReady)
+ return true;
+
+ bool ok = true;
+ ok &= NativeOffsets.TryGet("Creeper", "swell", out s_swellOffset);
+ ok &= NativeOffsets.TryGet("Creeper", "maxSwell", out s_maxSwellOffset);
+ NativeOffsets.TryGet("Level", "isClientSide", out s_isClientSideOffset);
+
+ s_offsetsReady = ok;
+ if (!s_offsetsReady && !s_offsetsWarned)
+ {
+ Logger.Warning("CreeperExplosionMixin: missing offsets for Creeper swell/maxSwell.");
+ s_offsetsWarned = true;
+ }
+ return s_offsetsReady;
+ }
+
+ private static void PaintSphereTop(nint levelPtr, int centerX, int centerY, int centerZ)
+ {
+ int minY = Math.Max(0, centerY - Radius);
+ int maxY = Math.Min(255, centerY + Radius);
+ int radiusSq = Radius * Radius;
+ var targets = new List<(int X, int Y, int Z)>();
+
+ for (int dx = -Radius; dx <= Radius; dx++)
+ {
+ int dxSq = dx * dx;
+ for (int dz = -Radius; dz <= Radius; dz++)
+ {
+ int distSq = dxSq + dz * dz;
+ if (distSq > radiusSq)
+ continue;
+
+ int worldX = centerX + dx;
+ int worldZ = centerZ + dz;
+ int verticalRadius = (int)Math.Floor(Math.Sqrt(radiusSq - distSq));
+ int startY = Math.Min(maxY, centerY + verticalRadius);
+ int endY = Math.Max(minY, centerY - verticalRadius);
+
+ int topY = -1;
+ for (int y = startY; y >= endY; --y)
+ {
+ int id = NativeLevel.GetTile(levelPtr, worldX, y, worldZ);
+ if (id != 0)
+ {
+ topY = y;
+ break;
+ }
+ }
+
+ if (topY >= 0)
+ targets.Add((worldX, topY, worldZ));
+ }
+ }
+
+ foreach (var (tx, ty, tz) in targets)
+ NativeLevel.SetTile(levelPtr, tx, ty, tz, ExampleMod.RubySand!.NumericId, 0, 2);
+ }
+}
diff --git a/ExampleMod/weave.mixins.json b/ExampleMod/weave.mixins.json
new file mode 100644
index 0000000..8145263
--- /dev/null
+++ b/ExampleMod/weave.mixins.json
@@ -0,0 +1,13 @@
+{
+ "required": false,
+ "minVersion": "0.1",
+ "package": "ExampleMod.Mixins",
+ "mixins": [],
+ "client": [
+ "CreeperExplosionMixin"
+ ],
+ "server": [],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/WeaveLoader.API/Mixins/At.cs b/WeaveLoader.API/Mixins/At.cs
new file mode 100644
index 0000000..3c88f61
--- /dev/null
+++ b/WeaveLoader.API/Mixins/At.cs
@@ -0,0 +1,7 @@
+namespace WeaveLoader.API.Mixins;
+
+public enum At
+{
+ Head = 0,
+ Tail = 1
+}
diff --git a/WeaveLoader.API/Mixins/InjectAttribute.cs b/WeaveLoader.API/Mixins/InjectAttribute.cs
new file mode 100644
index 0000000..5289c89
--- /dev/null
+++ b/WeaveLoader.API/Mixins/InjectAttribute.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace WeaveLoader.API.Mixins;
+
+[AttributeUsage(AttributeTargets.Method, Inherited = false)]
+public sealed class InjectAttribute : Attribute
+{
+ public string Method { get; }
+ public At At { get; }
+ public int Require { get; set; } = 0;
+ public bool Cancellable { get; set; } = false;
+
+ public InjectAttribute(string method, At at = At.Head)
+ {
+ Method = method;
+ At = at;
+ }
+}
diff --git a/WeaveLoader.API/Mixins/MixinAttribute.cs b/WeaveLoader.API/Mixins/MixinAttribute.cs
new file mode 100644
index 0000000..3081ef4
--- /dev/null
+++ b/WeaveLoader.API/Mixins/MixinAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace WeaveLoader.API.Mixins;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public sealed class MixinAttribute : Attribute
+{
+ public string Target { get; }
+
+ public MixinAttribute(string target)
+ {
+ Target = target;
+ }
+}
diff --git a/WeaveLoader.API/Mixins/MixinContext.cs b/WeaveLoader.API/Mixins/MixinContext.cs
new file mode 100644
index 0000000..48d3142
--- /dev/null
+++ b/WeaveLoader.API/Mixins/MixinContext.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Runtime.InteropServices;
+using WeaveLoader.API.Native;
+
+namespace WeaveLoader.API.Mixins;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct MixinContextNative
+{
+ public nint ThisPtr;
+ public nint Args;
+ public int ArgCount;
+ public nint Ret;
+ public int Cancel;
+}
+
+public sealed class MixinContext
+{
+ private readonly nint _ctxPtr;
+
+ internal MixinContext(nint ctxPtr)
+ {
+ _ctxPtr = ctxPtr;
+ }
+
+ public nint ThisPtr => Read().ThisPtr;
+ public int ArgCount => Read().ArgCount;
+
+ public NativeArg GetArg(int index)
+ {
+ var ctx = Read();
+ int size = Marshal.SizeOf();
+ nint ptr = ctx.Args + index * size;
+ return Marshal.PtrToStructure(ptr);
+ }
+
+ public void SetArg(int index, NativeArg arg)
+ {
+ var ctx = Read();
+ int size = Marshal.SizeOf();
+ nint ptr = ctx.Args + index * size;
+ Marshal.StructureToPtr(arg, ptr, false);
+ }
+
+ public NativeRet GetReturn()
+ {
+ var ctx = Read();
+ return Marshal.PtrToStructure(ctx.Ret);
+ }
+
+ public void SetReturn(NativeRet ret)
+ {
+ var ctx = Read();
+ Marshal.StructureToPtr(ret, ctx.Ret, false);
+ }
+
+ public void Cancel()
+ {
+ var ctx = Read();
+ ctx.Cancel = 1;
+ Marshal.StructureToPtr(ctx, _ctxPtr, false);
+ }
+
+ private MixinContextNative Read()
+ {
+ return Marshal.PtrToStructure(_ctxPtr);
+ }
+}
diff --git a/WeaveLoader.API/Mixins/OverwriteAttribute.cs b/WeaveLoader.API/Mixins/OverwriteAttribute.cs
new file mode 100644
index 0000000..b42e013
--- /dev/null
+++ b/WeaveLoader.API/Mixins/OverwriteAttribute.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace WeaveLoader.API.Mixins;
+
+[AttributeUsage(AttributeTargets.Method, Inherited = false)]
+public sealed class OverwriteAttribute : Attribute
+{
+}
diff --git a/WeaveLoader.API/Native/NativeClass.cs b/WeaveLoader.API/Native/NativeClass.cs
new file mode 100644
index 0000000..006374d
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeClass.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+
+namespace WeaveLoader.API.Native;
+
+public sealed class NativeClass
+{
+ public string Name { get; }
+ private readonly Dictionary _methodCache = new();
+
+ public NativeClass(string name)
+ {
+ Name = name;
+ }
+
+ public bool HasMethod(string method)
+ => Resolve(BuildFullName(method)) != 0;
+
+ public bool CallVoid(string method, params object[] args)
+ {
+ string fullName = BuildFullName(method);
+ nint fn = Resolve(fullName);
+ if (fn == 0)
+ return false;
+ if (!NativeObject.TryBuildArgs(args, out var nativeArgs))
+ return false;
+ return NativeInvoker.TryInvoke(fn, 0, false, NativeType.I32, nativeArgs, out _);
+ }
+
+ public T Call(string method, params object[] args)
+ {
+ string fullName = BuildFullName(method);
+ nint fn = Resolve(fullName);
+ if (fn == 0)
+ return default!;
+ if (!NativeObject.TryBuildArgs(args, out var nativeArgs))
+ return default!;
+ NativeType retType = NativeObject.GetReturnType(typeof(T));
+ if (!NativeInvoker.TryInvoke(fn, 0, false, retType, nativeArgs, out var ret))
+ return default!;
+ return NativeObject.ConvertReturn(ret);
+ }
+
+ private string BuildFullName(string method)
+ => method.Contains("::") ? method : $"{Name}::{method}";
+
+ private nint Resolve(string fullName)
+ {
+ if (_methodCache.TryGetValue(fullName, out var cached))
+ return cached;
+ nint fn = NativeSymbol.Find(fullName);
+ _methodCache[fullName] = fn;
+ return fn;
+ }
+}
diff --git a/WeaveLoader.API/Native/NativeInvoker.cs b/WeaveLoader.API/Native/NativeInvoker.cs
new file mode 100644
index 0000000..108ca32
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeInvoker.cs
@@ -0,0 +1,23 @@
+namespace WeaveLoader.API.Native;
+
+internal static class NativeInvoker
+{
+ internal static bool TryInvoke(string fullName, nint thisPtr, bool hasThis, NativeType retType, NativeArg[] args, out NativeRet ret)
+ {
+ ret = new NativeRet { Type = retType };
+ nint fn = NativeSymbol.Find(fullName);
+ if (fn == 0)
+ return false;
+ int ok = NativeInterop.native_invoke(fn, thisPtr, hasThis ? 1 : 0, args, args.Length, ref ret);
+ return ok != 0;
+ }
+
+ internal static bool TryInvoke(nint fn, nint thisPtr, bool hasThis, NativeType retType, NativeArg[] args, out NativeRet ret)
+ {
+ ret = new NativeRet { Type = retType };
+ if (fn == 0)
+ return false;
+ int ok = NativeInterop.native_invoke(fn, thisPtr, hasThis ? 1 : 0, args, args.Length, ref ret);
+ return ok != 0;
+ }
+}
diff --git a/WeaveLoader.API/Native/NativeLevel.cs b/WeaveLoader.API/Native/NativeLevel.cs
new file mode 100644
index 0000000..18e4595
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeLevel.cs
@@ -0,0 +1,10 @@
+namespace WeaveLoader.API.Native;
+
+public static class NativeLevel
+{
+ public static int GetTile(nint levelPtr, int x, int y, int z)
+ => NativeInterop.native_level_get_tile(levelPtr, x, y, z);
+
+ public static bool SetTile(nint levelPtr, int x, int y, int z, int blockId, int data = 0, int flags = 2)
+ => NativeInterop.native_level_set_tile(levelPtr, x, y, z, blockId, data, flags) != 0;
+}
diff --git a/WeaveLoader.API/Native/NativeMemory.cs b/WeaveLoader.API/Native/NativeMemory.cs
new file mode 100644
index 0000000..2ee9984
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeMemory.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace WeaveLoader.API.Native;
+
+public static class NativeMemory
+{
+ public static byte ReadByte(nint basePtr, int offset)
+ => Marshal.ReadByte(basePtr, offset);
+
+ public static int ReadInt32(nint basePtr, int offset)
+ => Marshal.ReadInt32(basePtr, offset);
+
+ public static long ReadInt64(nint basePtr, int offset)
+ => Marshal.ReadInt64(basePtr, offset);
+
+ public static double ReadDouble(nint basePtr, int offset)
+ => BitConverter.Int64BitsToDouble(ReadInt64(basePtr, offset));
+
+ public static bool ReadBool(nint basePtr, int offset)
+ => ReadByte(basePtr, offset) != 0;
+
+ public static nint ReadPtr(nint basePtr, int offset)
+ => Marshal.ReadIntPtr(basePtr, offset);
+}
diff --git a/WeaveLoader.API/Native/NativeObject.cs b/WeaveLoader.API/Native/NativeObject.cs
new file mode 100644
index 0000000..3bf26b3
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeObject.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+
+namespace WeaveLoader.API.Native;
+
+public sealed class NativeObject
+{
+ public string ClassName { get; }
+ public nint Pointer { get; }
+ private readonly Dictionary _methodCache = new();
+
+ public NativeObject(string className, nint pointer)
+ {
+ ClassName = className;
+ Pointer = pointer;
+ }
+
+ public bool HasMethod(string method)
+ => Resolve(BuildFullName(method)) != 0;
+
+ public bool CallVoid(string method, params object[] args)
+ {
+ string fullName = BuildFullName(method);
+ nint fn = Resolve(fullName);
+ if (fn == 0)
+ return false;
+ if (!TryBuildArgs(args, out var nativeArgs))
+ return false;
+ return NativeInvoker.TryInvoke(fn, Pointer, true, NativeType.I32, nativeArgs, out _);
+ }
+
+ public T Call(string method, params object[] args)
+ {
+ string fullName = BuildFullName(method);
+ nint fn = Resolve(fullName);
+ if (fn == 0)
+ return default!;
+ if (!TryBuildArgs(args, out var nativeArgs))
+ return default!;
+ NativeType retType = GetReturnType(typeof(T));
+ if (!NativeInvoker.TryInvoke(fn, Pointer, true, retType, nativeArgs, out var ret))
+ return default!;
+ return ConvertReturn(ret);
+ }
+
+ private string BuildFullName(string method)
+ => method.Contains("::") ? method : $"{ClassName}::{method}";
+
+ private nint Resolve(string fullName)
+ {
+ if (_methodCache.TryGetValue(fullName, out var cached))
+ return cached;
+ nint fn = NativeSymbol.Find(fullName);
+ _methodCache[fullName] = fn;
+ return fn;
+ }
+
+ internal static bool TryBuildArgs(object[] args, out NativeArg[] nativeArgs)
+ {
+ nativeArgs = new NativeArg[args.Length];
+ for (int i = 0; i < args.Length; ++i)
+ {
+ object arg = args[i];
+ switch (arg)
+ {
+ case int v:
+ nativeArgs[i] = NativeArg.FromInt(v);
+ break;
+ case long v:
+ nativeArgs[i] = NativeArg.FromLong(v);
+ break;
+ case bool v:
+ nativeArgs[i] = NativeArg.FromBool(v);
+ break;
+ case nint v:
+ nativeArgs[i] = NativeArg.FromPtr(v);
+ break;
+ default:
+ return false;
+ }
+ }
+ return true;
+ }
+
+ internal static NativeType GetReturnType(Type t)
+ {
+ if (t == typeof(int))
+ return NativeType.I32;
+ if (t == typeof(long))
+ return NativeType.I64;
+ if (t == typeof(bool))
+ return NativeType.Bool;
+ if (t == typeof(nint) || t == typeof(IntPtr))
+ return NativeType.Ptr;
+ return NativeType.I32;
+ }
+
+ internal static T ConvertReturn(NativeRet ret)
+ {
+ object value = ret.Type switch
+ {
+ NativeType.Bool => ret.AsBool(),
+ NativeType.I64 => ret.AsLong(),
+ NativeType.Ptr => typeof(T) == typeof(IntPtr) ? (object)(IntPtr)ret.AsPtr() : ret.AsPtr(),
+ _ => ret.AsInt()
+ };
+ return (T)value;
+ }
+}
diff --git a/WeaveLoader.API/Native/NativeOffsets.cs b/WeaveLoader.API/Native/NativeOffsets.cs
new file mode 100644
index 0000000..9ccf87d
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeOffsets.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace WeaveLoader.API.Native;
+
+public static class NativeOffsets
+{
+ private static Dictionary>? s_offsets;
+ private static bool s_loaded;
+ private static bool s_logged;
+
+ static NativeOffsets()
+ {
+ TryLoadFromMetadata();
+ }
+
+ public static class Entity
+ {
+ public static int X = 0x78;
+ public static int Y = 0x80;
+ public static int Z = 0x88;
+ public static int Removed = 0xC7;
+ }
+
+ public static bool HasData => s_loaded;
+
+ public static bool TryLoadFromFile(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ return false;
+
+ try
+ {
+ var json = File.ReadAllText(path);
+ var root = JsonSerializer.Deserialize>>(json);
+ if (root == null)
+ return false;
+
+ s_offsets = NormalizeOffsets(root);
+ s_loaded = true;
+
+ if (s_offsets.TryGetValue("entity", out var entity))
+ {
+ if (entity.TryGetValue("x", out var x)) Entity.X = x;
+ if (entity.TryGetValue("y", out var y)) Entity.Y = y;
+ if (entity.TryGetValue("z", out var z)) Entity.Z = z;
+ if (entity.TryGetValue("removed", out var removed)) Entity.Removed = removed;
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ try
+ {
+ Logger.Warning($"Offsets load failed for {path}: {ex.Message}");
+ }
+ catch
+ {
+ }
+ return false;
+ }
+ }
+
+ public static bool TryGet(string typeName, string fieldName, out int offset)
+ {
+ offset = 0;
+ if (s_offsets == null)
+ return false;
+ string typeKey = NormalizeName(typeName);
+ string fieldKey = NormalizeFieldName(fieldName);
+ if (!s_offsets.TryGetValue(typeKey, out var fields))
+ return false;
+ return fields.TryGetValue(fieldKey, out offset);
+ }
+
+ private static void TryLoadFromMetadata()
+ {
+ if (s_logged)
+ return;
+ s_logged = true;
+
+ var candidates = new List(GetCandidates());
+ foreach (var candidate in candidates)
+ {
+ if (TryLoadFromFile(candidate))
+ return;
+ }
+
+ try
+ {
+ Logger.Warning("Offsets not loaded. offsets.json not found in metadata.");
+ }
+ catch
+ {
+ }
+ }
+
+ private static IEnumerable GetCandidates()
+ {
+ var list = new List();
+ var baseDir = AppContext.BaseDirectory;
+ list.Add(Path.Combine(baseDir, "metadata", "offsets.json"));
+ list.Add(Path.Combine(baseDir, "offsets.json"));
+
+ var modsPath = GetNativeModsPath();
+ if (!string.IsNullOrWhiteSpace(modsPath))
+ {
+ var root = Path.GetDirectoryName(modsPath) ?? "";
+ if (!string.IsNullOrWhiteSpace(root))
+ {
+ list.Add(Path.Combine(root, "metadata", "offsets.json"));
+ list.Add(Path.Combine(root, "offsets.json"));
+ }
+ }
+
+ var cwd = Directory.GetCurrentDirectory();
+ list.Add(Path.Combine(cwd, "metadata", "offsets.json"));
+ list.Add(Path.Combine(cwd, "offsets.json"));
+
+ try
+ {
+ string loaderBuild = Path.Combine(@"Z:\home\jacobwasbeast\MinecraftLegacyEdition\ModLoader\build", "metadata", "offsets.json");
+ list.Add(loaderBuild);
+ }
+ catch
+ {
+ }
+
+ return list;
+ }
+
+ private static string? GetNativeModsPath()
+ {
+ try
+ {
+ var ptr = NativeInterop.native_get_mods_path();
+ if (ptr == nint.Zero)
+ return null;
+ return Marshal.PtrToStringAnsi(ptr);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static Dictionary> NormalizeOffsets(
+ Dictionary> raw)
+ {
+ var result = new Dictionary>();
+ foreach (var (typeName, fields) in raw)
+ {
+ string typeKey = NormalizeName(typeName);
+ var fieldMap = new Dictionary();
+ foreach (var (fieldName, offset) in fields)
+ {
+ string fieldKey = NormalizeFieldName(fieldName);
+ if (!fieldMap.ContainsKey(fieldKey))
+ fieldMap[fieldKey] = offset;
+ }
+ result[typeKey] = fieldMap;
+ }
+ return result;
+ }
+
+ private static string NormalizeName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return "";
+ return name.Trim().ToLowerInvariant();
+ }
+
+ private static string NormalizeFieldName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return "";
+ string trimmed = name.Trim();
+ if (trimmed.StartsWith("m_", StringComparison.OrdinalIgnoreCase))
+ trimmed = trimmed.Substring(2);
+ if (trimmed.StartsWith("_", StringComparison.OrdinalIgnoreCase))
+ trimmed = trimmed.Substring(1);
+ if (trimmed.StartsWith("m", StringComparison.OrdinalIgnoreCase) && trimmed.Length > 1 && char.IsUpper(trimmed[1]))
+ trimmed = trimmed.Substring(1);
+ return trimmed.ToLowerInvariant();
+ }
+}
diff --git a/WeaveLoader.API/Native/NativeSymbol.cs b/WeaveLoader.API/Native/NativeSymbol.cs
new file mode 100644
index 0000000..aebc6b9
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeSymbol.cs
@@ -0,0 +1,19 @@
+using System.Text;
+
+namespace WeaveLoader.API.Native;
+
+internal static class NativeSymbol
+{
+ internal static nint Find(string fullName)
+ => NativeInterop.native_find_symbol(fullName);
+
+ internal static bool Has(string fullName)
+ => NativeInterop.native_has_symbol(fullName) != 0;
+
+ internal static string? GetSignatureKey(string fullName)
+ {
+ var sb = new StringBuilder(64);
+ int len = NativeInterop.native_get_signature_key(fullName, sb, sb.Capacity);
+ return len > 0 ? sb.ToString() : null;
+ }
+}
diff --git a/WeaveLoader.API/Native/NativeTypes.cs b/WeaveLoader.API/Native/NativeTypes.cs
new file mode 100644
index 0000000..b49fe71
--- /dev/null
+++ b/WeaveLoader.API/Native/NativeTypes.cs
@@ -0,0 +1,37 @@
+using System.Runtime.InteropServices;
+
+namespace WeaveLoader.API.Native;
+
+public enum NativeType : byte
+{
+ I32 = 1,
+ I64 = 2,
+ F32 = 3,
+ F64 = 4,
+ Ptr = 5,
+ Bool = 6
+}
+
+[StructLayout(LayoutKind.Sequential)]
+public struct NativeArg
+{
+ public NativeType Type;
+ public ulong Value;
+
+ public static NativeArg FromInt(int value) => new() { Type = NativeType.I32, Value = unchecked((ulong)value) };
+ public static NativeArg FromLong(long value) => new() { Type = NativeType.I64, Value = unchecked((ulong)value) };
+ public static NativeArg FromBool(bool value) => new() { Type = NativeType.Bool, Value = value ? 1UL : 0UL };
+ public static NativeArg FromPtr(nint value) => new() { Type = NativeType.Ptr, Value = unchecked((ulong)value) };
+}
+
+[StructLayout(LayoutKind.Sequential)]
+public struct NativeRet
+{
+ public NativeType Type;
+ public ulong Value;
+
+ public int AsInt() => unchecked((int)Value);
+ public long AsLong() => unchecked((long)Value);
+ public bool AsBool() => Value != 0;
+ public nint AsPtr() => unchecked((nint)Value);
+}
diff --git a/WeaveLoader.API/NativeInterop.cs b/WeaveLoader.API/NativeInterop.cs
index c9092ed..3f0c190 100644
--- a/WeaveLoader.API/NativeInterop.cs
+++ b/WeaveLoader.API/NativeInterop.cs
@@ -224,4 +224,22 @@ internal static class NativeInterop
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern void native_add_to_creative(int numericId, int count, int auxValue, int groupIndex);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
+ internal static extern nint native_find_symbol(string fullName);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
+ internal static extern int native_has_symbol(string fullName);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
+ internal static extern int native_get_signature_key(string fullName, System.Text.StringBuilder outKey, int outLen);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int native_invoke(nint fn, nint thisPtr, int hasThis, Native.NativeArg[] args, int argCount, ref Native.NativeRet ret);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
+ internal static extern int native_mixin_add(string fullName, int at, nint managedCallback, int require);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
+ internal static extern int native_mixin_remove(string fullName, nint managedCallback);
}
diff --git a/WeaveLoader.Core/MixinLoader.cs b/WeaveLoader.Core/MixinLoader.cs
new file mode 100644
index 0000000..60d9f16
--- /dev/null
+++ b/WeaveLoader.Core/MixinLoader.cs
@@ -0,0 +1,170 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using WeaveLoader.API;
+using WeaveLoader.API.Mixins;
+
+namespace WeaveLoader.Core;
+
+internal static class MixinLoader
+{
+ private sealed class MixinConfig
+ {
+ public bool Required { get; set; } = false;
+ public string? Package { get; set; }
+ public string[]? Mixins { get; set; }
+ public string[]? Client { get; set; }
+ public string[]? Server { get; set; }
+ public InjectorConfig? Injectors { get; set; }
+ }
+
+ private sealed class InjectorConfig
+ {
+ public int DefaultRequire { get; set; } = 0;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MixinContextNative
+ {
+ public nint ThisPtr;
+ public nint Args;
+ public int ArgCount;
+ public nint Ret;
+ public int Cancel;
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate void MixinCallback(ref MixinContextNative ctx);
+
+ private sealed class CallbackWrapper
+ {
+ private readonly MethodInfo _method;
+
+ public CallbackWrapper(MethodInfo method)
+ {
+ _method = method;
+ }
+
+ public unsafe void Invoke(ref MixinContextNative ctx)
+ {
+ object?[]? args = null;
+ if (_method.GetParameters().Length == 1)
+ {
+ fixed (MixinContextNative* pCtx = &ctx)
+ {
+ args = new object?[] { new MixinContext((nint)pCtx) };
+ }
+ }
+ _method.Invoke(null, args);
+ }
+ }
+
+ private static readonly List s_callbacks = new();
+ private static readonly JsonSerializerOptions s_jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ internal static void LoadMixins(IEnumerable mods)
+ {
+ foreach (var mod in mods)
+ LoadMixinsForMod(mod);
+ }
+
+ private static void LoadMixinsForMod(ModDiscovery.DiscoveredMod mod)
+ {
+ if (string.IsNullOrWhiteSpace(mod.Folder))
+ return;
+
+ string configPath = Path.Combine(mod.Folder, "weave.mixins.json");
+ if (!File.Exists(configPath))
+ return;
+
+ MixinConfig? config;
+ try
+ {
+ config = JsonSerializer.Deserialize(File.ReadAllText(configPath), s_jsonOptions);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Failed to parse mixin config for {mod.Metadata.Id}: {ex.Message}");
+ return;
+ }
+
+ if (config == null || string.IsNullOrWhiteSpace(config.Package))
+ return;
+
+ Logger.Info($"Loading mixins for {mod.Metadata.Id} from {configPath}");
+
+ var mixinTypes = new List();
+ if (config.Mixins != null) mixinTypes.AddRange(config.Mixins);
+ if (config.Client != null) mixinTypes.AddRange(config.Client);
+ if (config.Server != null) mixinTypes.AddRange(config.Server);
+
+ foreach (string mixinName in mixinTypes)
+ {
+ string fullTypeName = $"{config.Package}.{mixinName}";
+ Type? type = mod.Assembly.GetType(fullTypeName, false);
+ if (type == null)
+ {
+ Logger.Warning($"Mixin type not found: {fullTypeName}");
+ continue;
+ }
+
+ var mixinAttr = type.GetCustomAttribute();
+ if (mixinAttr == null)
+ {
+ Logger.Warning($"Mixin missing [Mixin] attribute: {fullTypeName}");
+ continue;
+ }
+
+ Logger.Info($"Processing mixin: {fullTypeName} -> {mixinAttr.Target}");
+
+ foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
+ {
+ var inject = method.GetCustomAttribute();
+ if (inject == null)
+ continue;
+
+ if (!method.IsStatic)
+ {
+ Logger.Warning($"Mixin method must be static: {method.DeclaringType?.FullName}.{method.Name}");
+ continue;
+ }
+
+ if (method.GetParameters().Length > 1)
+ {
+ Logger.Warning($"Mixin method has too many parameters: {method.DeclaringType?.FullName}.{method.Name}");
+ continue;
+ }
+
+ if (method.GetParameters().Length == 1 &&
+ method.GetParameters()[0].ParameterType != typeof(MixinContext))
+ {
+ Logger.Warning($"Mixin method parameter must be MixinContext: {method.DeclaringType?.FullName}.{method.Name}");
+ continue;
+ }
+
+ string targetName = mixinAttr.Target;
+ string fullName = inject.Method.Contains("::") ? inject.Method : $"{targetName}::{inject.Method}";
+ int require = inject.Require > 0 ? inject.Require : (config.Injectors?.DefaultRequire ?? 0);
+
+ if (NativeInterop.native_has_symbol(fullName) == 0)
+ {
+ Logger.Warning($"Mixin target not found in mapping: {fullName}");
+ }
+
+ var wrapper = new CallbackWrapper(method);
+ MixinCallback callback = wrapper.Invoke;
+ nint fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
+ s_callbacks.Add(callback);
+
+ int ok = NativeInterop.native_mixin_add(fullName, (int)inject.At, fnPtr, require);
+ if (ok == 0)
+ Logger.Warning($"Mixin hook failed: {fullName}");
+ else
+ Logger.Info($"Mixin hook installed: {fullName} ({inject.At})");
+ }
+ }
+ }
+}
diff --git a/WeaveLoader.Core/ModDiscovery.cs b/WeaveLoader.Core/ModDiscovery.cs
index ea31ffb..7a69bbe 100644
--- a/WeaveLoader.Core/ModDiscovery.cs
+++ b/WeaveLoader.Core/ModDiscovery.cs
@@ -9,7 +9,8 @@ internal static class ModDiscovery
internal record DiscoveredMod(
IMod Instance,
ModAttribute Metadata,
- Assembly Assembly);
+ Assembly Assembly,
+ string Folder);
internal static List DiscoverMods(string modsPath)
{
@@ -27,7 +28,7 @@ internal static class ModDiscovery
{
var apiMod = new WeaveLoaderApiMod();
var attr = typeof(WeaveLoaderApiMod).GetCustomAttribute()!;
- mods.Add(new DiscoveredMod(apiMod, attr, typeof(ModDiscovery).Assembly));
+ mods.Add(new DiscoveredMod(apiMod, attr, typeof(ModDiscovery).Assembly, apiFolder));
Logger.Info($"Discovered mod: {attr.Name} v{attr.Version} by {attr.Author} (mods/WeaveLoader.API/)");
}
@@ -68,6 +69,7 @@ internal static class ModDiscovery
var results = new List();
var fileName = Path.GetFileName(dllPath);
var fullPath = Path.GetFullPath(dllPath);
+ var folder = Path.GetDirectoryName(fullPath) ?? "";
// Load into the SAME ALC that WeaveLoader.Core lives in (the hostfxr component context).
// This ensures WeaveLoader.API types (IMod, ModAttribute, etc.) have the same identity.
@@ -76,7 +78,6 @@ internal static class ModDiscovery
var assembly = coreContext.LoadFromAssemblyPath(fullPath);
var allTypes = assembly.GetTypes();
- Logger.Debug($"{fileName}: {allTypes.Length} type(s), checking for IMod implementations...");
var modTypes = allTypes
.Where(t => t.IsClass && !t.IsAbstract && typeof(IMod).IsAssignableFrom(t));
@@ -93,7 +94,7 @@ internal static class ModDiscovery
try
{
var instance = (IMod)Activator.CreateInstance(type)!;
- results.Add(new DiscoveredMod(instance, attr, assembly));
+ results.Add(new DiscoveredMod(instance, attr, assembly, folder));
string name = string.IsNullOrEmpty(attr.Name) ? attr.Id : attr.Name;
Logger.Info($"Discovered mod: {name} v{attr.Version} by {attr.Author} ({fileName})");
@@ -104,9 +105,6 @@ internal static class ModDiscovery
}
}
- if (results.Count == 0)
- Logger.Debug($"No IMod implementations found in {fileName}");
-
return results;
}
}
diff --git a/WeaveLoader.Core/WeaveLoader.Core.csproj b/WeaveLoader.Core/WeaveLoader.Core.csproj
index 866eb1c..6234786 100644
--- a/WeaveLoader.Core/WeaveLoader.Core.csproj
+++ b/WeaveLoader.Core/WeaveLoader.Core.csproj
@@ -4,6 +4,7 @@
net8.0
enable
enable
+ true
WeaveLoader.Core
WeaveLoader.Core
WeaveLoader core runtime - mod discovery and lifecycle management
diff --git a/WeaveLoader.Core/WeaveLoaderCore.cs b/WeaveLoader.Core/WeaveLoaderCore.cs
index db23e6d..3834b95 100644
--- a/WeaveLoader.Core/WeaveLoaderCore.cs
+++ b/WeaveLoader.Core/WeaveLoaderCore.cs
@@ -55,6 +55,7 @@ public static class WeaveLoaderCore
var discovered = ModDiscovery.DiscoverMods(modsPath);
_modManager?.AddMods(discovered);
+ MixinLoader.LoadMixins(discovered);
Logger.Info($"Loaded {discovered.Count} mod(s)");
return discovered.Count;
}
diff --git a/WeaveLoader.Launcher/Program.cs b/WeaveLoader.Launcher/Program.cs
index 5445852..77d70cf 100644
--- a/WeaveLoader.Launcher/Program.cs
+++ b/WeaveLoader.Launcher/Program.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Runtime.InteropServices;
namespace WeaveLoader.Launcher;
@@ -20,6 +21,9 @@ class Program
string configFile = Path.Combine(baseDir, "weaveloader.json");
string runtimeDll = Path.Combine(baseDir, RuntimeDllName);
string modsDir = Path.Combine(baseDir, "mods");
+ string metadataDir = Path.Combine(baseDir, "metadata");
+ string mappingPath = Path.Combine(metadataDir, "mapping.json");
+ string pdbDumpExe = Path.Combine(baseDir, "pdbdump.exe");
try
{
@@ -59,6 +63,43 @@ class Program
Console.WriteLine($"Saved game path to {configFile}");
}
+ bool mappingMissing = !File.Exists(mappingPath);
+ bool offsetsMissing = !File.Exists(Path.Combine(metadataDir, "offsets.json"));
+
+ if (mappingMissing || offsetsMissing)
+ {
+ if (!Directory.Exists(metadataDir))
+ Directory.CreateDirectory(metadataDir);
+
+ string pdbPath = Path.ChangeExtension(config.GameExePath, ".pdb") ?? "";
+ string offsetsPath = Path.Combine(metadataDir, "offsets.json");
+ if (!File.Exists(pdbDumpExe))
+ {
+ Console.WriteLine($"[WARN] pdbdump.exe not found at {pdbDumpExe} (mapping.json will not be generated)");
+ }
+ else if (!File.Exists(pdbPath))
+ {
+ Console.WriteLine($"[WARN] PDB not found at {pdbPath} (mapping.json will not be generated)");
+ }
+ else
+ {
+ Console.WriteLine("[..] Generating metadata from PDB...");
+ var psi = new ProcessStartInfo
+ {
+ FileName = pdbDumpExe,
+ Arguments = $"\"{pdbPath}\" \"{mappingPath}\" --offsets \"{config.GameExePath}\" \"{offsetsPath}\" --all-types",
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ using var proc = Process.Start(psi);
+ proc?.WaitForExit();
+ if (proc == null || proc.ExitCode != 0 || !File.Exists(mappingPath))
+ Console.WriteLine("[WARN] mapping.json generation failed");
+ else
+ Console.WriteLine("[OK] mapping.json generated");
+ }
+ }
+
if (!File.Exists(runtimeDll))
{
Console.Error.WriteLine($"Error: {RuntimeDllName} not found.");
diff --git a/WeaveLoaderRuntime/CMakeLists.txt b/WeaveLoaderRuntime/CMakeLists.txt
index d6e87f7..2a570db 100644
--- a/WeaveLoaderRuntime/CMakeLists.txt
+++ b/WeaveLoaderRuntime/CMakeLists.txt
@@ -81,8 +81,10 @@ add_library(WeaveLoaderRuntime SHARED
src/LogUtil.cpp
src/CrashHandler.cpp
src/PdbParser.cpp
+ src/SymbolRegistry.cpp
src/SymbolResolver.cpp
src/HookManager.cpp
+ src/HookRegistry.cpp
src/GameHooks.cpp
src/DotNetHost.cpp
src/IdRegistry.cpp
@@ -138,3 +140,31 @@ set_target_properties(WeaveLoaderRuntime PROPERTIES
LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}/../build"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}/../build"
)
+
+# ── PDB dump tool ─────────────────────────────────────────────────────
+add_executable(pdbdump
+ tools/pdbdump.cpp
+ src/PdbParser.cpp
+ src/LogUtil.cpp
+)
+
+target_link_libraries(pdbdump PRIVATE
+ raw_pdb
+ Dbghelp
+ OleAut32
+)
+
+target_include_directories(pdbdump PRIVATE
+ "${CMAKE_CURRENT_SOURCE_DIR}/src"
+)
+
+target_compile_definitions(pdbdump PRIVATE
+ WIN32_LEAN_AND_MEAN
+ _CRT_SECURE_NO_WARNINGS
+)
+
+set_target_properties(pdbdump PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../build"
+ RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}/../build"
+ RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}/../build"
+)
diff --git a/WeaveLoaderRuntime/src/HookRegistry.cpp b/WeaveLoaderRuntime/src/HookRegistry.cpp
new file mode 100644
index 0000000..eec68a1
--- /dev/null
+++ b/WeaveLoaderRuntime/src/HookRegistry.cpp
@@ -0,0 +1,369 @@
+#include "HookRegistry.h"
+#include "SymbolRegistry.h"
+#include "LogUtil.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+ struct HookEntry
+ {
+ std::string fullName;
+ void* target = nullptr;
+ void* original = nullptr;
+ std::string signatureKey;
+ NativeType retType = NativeType_I32;
+ std::vector argTypes;
+ int argCount = 0;
+ bool hasThis = true;
+ std::vector pre;
+ std::vector post;
+ void* thunk = nullptr;
+ };
+
+ std::unordered_map> g_hooks;
+ std::mutex g_mutex;
+ HookEntry* g_activeEntry = nullptr;
+
+ bool ParseSignature(const std::string& key, NativeType& ret, std::vector& args)
+ {
+ if (key.empty())
+ return false;
+ args.clear();
+ auto mapToken = [](char c, NativeType& out) -> bool
+ {
+ switch (c)
+ {
+ case 'i': out = NativeType_I32; return true;
+ case 'l': out = NativeType_I64; return true;
+ case 'p': out = NativeType_Ptr; return true;
+ case 'b': out = NativeType_Bool; return true;
+ case 'v': out = NativeType_I32; return true;
+ default: return false;
+ }
+ };
+
+ NativeType retType;
+ if (!mapToken(key[0], retType))
+ return false;
+ ret = retType;
+
+ size_t pos = 1;
+ while (pos < key.size())
+ {
+ if (key[pos] != '_')
+ break;
+ ++pos;
+ if (pos >= key.size())
+ break;
+ NativeType argType;
+ if (!mapToken(key[pos], argType))
+ return false;
+ args.push_back(argType);
+ ++pos;
+ }
+ return true;
+ }
+
+ uint64_t CallOriginal(HookEntry* entry, void* thisPtr, const uint64_t* a)
+ {
+ if (!entry || !entry->original)
+ return 0;
+ if (entry->hasThis)
+ {
+ switch (entry->argCount)
+ {
+ case 0: return reinterpret_cast(entry->original)(thisPtr);
+ case 1: return reinterpret_cast(entry->original)(thisPtr, a[0]);
+ case 2: return reinterpret_cast(entry->original)(thisPtr, a[0], a[1]);
+ case 3: return reinterpret_cast(entry->original)(thisPtr, a[0], a[1], a[2]);
+ case 4: return reinterpret_cast(entry->original)(thisPtr, a[0], a[1], a[2], a[3]);
+ case 5: return reinterpret_cast(entry->original)(thisPtr, a[0], a[1], a[2], a[3], a[4]);
+ default: return reinterpret_cast(entry->original)(thisPtr, a[0], a[1], a[2], a[3], a[4], a[5]);
+ }
+ }
+ else
+ {
+ switch (entry->argCount)
+ {
+ case 0: return reinterpret_cast(entry->original)();
+ case 1: return reinterpret_cast(entry->original)(a[0]);
+ case 2: return reinterpret_cast(entry->original)(a[0], a[1]);
+ case 3: return reinterpret_cast(entry->original)(a[0], a[1], a[2]);
+ case 4: return reinterpret_cast(entry->original)(a[0], a[1], a[2], a[3]);
+ case 5: return reinterpret_cast(entry->original)(a[0], a[1], a[2], a[3], a[4]);
+ default: return reinterpret_cast(entry->original)(a[0], a[1], a[2], a[3], a[4], a[5]);
+ }
+ }
+ }
+
+ uint64_t InvokeHooks(HookEntry* entry, void* thisPtr, uint64_t* a)
+ {
+ if (!entry)
+ return 0;
+
+ NativeArg args[6] = {};
+ for (int i = 0; i < entry->argCount && i < 6; ++i)
+ {
+ args[i].type = entry->argTypes[i];
+ args[i].value = a[i];
+ }
+
+ NativeRet ret{};
+ ret.type = entry->retType;
+ ret.value = 0;
+
+ HookRegistry::HookContext ctx;
+ ctx.thisPtr = thisPtr;
+ ctx.args = args;
+ ctx.argCount = entry->argCount;
+ ctx.ret = &ret;
+ ctx.cancel = 0;
+
+ for (void* cb : entry->pre)
+ reinterpret_cast(cb)(&ctx);
+
+ for (int i = 0; i < entry->argCount && i < 6; ++i)
+ a[i] = args[i].value;
+
+ if (ctx.cancel == 0)
+ ret.value = CallOriginal(entry, thisPtr, a);
+
+ for (void* cb : entry->post)
+ reinterpret_cast(cb)(&ctx);
+
+ return ret.value;
+ }
+
+ uint64_t __fastcall HandleThis0(void* thisPtr)
+ {
+ uint64_t args[1] = {};
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+ uint64_t __fastcall HandleThis1(void* thisPtr, uint64_t a0)
+ {
+ uint64_t args[1] = { a0 };
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+ uint64_t __fastcall HandleThis2(void* thisPtr, uint64_t a0, uint64_t a1)
+ {
+ uint64_t args[2] = { a0, a1 };
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+ uint64_t __fastcall HandleThis3(void* thisPtr, uint64_t a0, uint64_t a1, uint64_t a2)
+ {
+ uint64_t args[3] = { a0, a1, a2 };
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+ uint64_t __fastcall HandleThis4(void* thisPtr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3)
+ {
+ uint64_t args[4] = { a0, a1, a2, a3 };
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+ uint64_t __fastcall HandleThis5(void* thisPtr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4)
+ {
+ uint64_t args[5] = { a0, a1, a2, a3, a4 };
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+ uint64_t __fastcall HandleThis6(void* thisPtr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)
+ {
+ uint64_t args[6] = { a0, a1, a2, a3, a4, a5 };
+ return InvokeHooks(g_activeEntry, thisPtr, args);
+ }
+
+ uint64_t __fastcall HandleStatic0()
+ {
+ uint64_t args[1] = {};
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+ uint64_t __fastcall HandleStatic1(uint64_t a0)
+ {
+ uint64_t args[1] = { a0 };
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+ uint64_t __fastcall HandleStatic2(uint64_t a0, uint64_t a1)
+ {
+ uint64_t args[2] = { a0, a1 };
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+ uint64_t __fastcall HandleStatic3(uint64_t a0, uint64_t a1, uint64_t a2)
+ {
+ uint64_t args[3] = { a0, a1, a2 };
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+ uint64_t __fastcall HandleStatic4(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3)
+ {
+ uint64_t args[4] = { a0, a1, a2, a3 };
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+ uint64_t __fastcall HandleStatic5(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4)
+ {
+ uint64_t args[5] = { a0, a1, a2, a3, a4 };
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+ uint64_t __fastcall HandleStatic6(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)
+ {
+ uint64_t args[6] = { a0, a1, a2, a3, a4, a5 };
+ return InvokeHooks(g_activeEntry, nullptr, args);
+ }
+
+ void* SelectHandler(bool hasThis, int argCount)
+ {
+ if (hasThis)
+ {
+ switch (argCount)
+ {
+ case 0: return reinterpret_cast(&HandleThis0);
+ case 1: return reinterpret_cast(&HandleThis1);
+ case 2: return reinterpret_cast(&HandleThis2);
+ case 3: return reinterpret_cast(&HandleThis3);
+ case 4: return reinterpret_cast(&HandleThis4);
+ case 5: return reinterpret_cast(&HandleThis5);
+ default: return reinterpret_cast(&HandleThis6);
+ }
+ }
+ else
+ {
+ switch (argCount)
+ {
+ case 0: return reinterpret_cast(&HandleStatic0);
+ case 1: return reinterpret_cast(&HandleStatic1);
+ case 2: return reinterpret_cast(&HandleStatic2);
+ case 3: return reinterpret_cast(&HandleStatic3);
+ case 4: return reinterpret_cast(&HandleStatic4);
+ case 5: return reinterpret_cast(&HandleStatic5);
+ default: return reinterpret_cast(&HandleStatic6);
+ }
+ }
+ }
+
+ void* CreateThunk(void* handler, HookEntry* entry)
+ {
+ unsigned char* code = reinterpret_cast(
+ VirtualAlloc(nullptr, 128, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
+ if (!code)
+ return nullptr;
+
+ unsigned char* p = code;
+ auto write64 = [&](uint64_t value)
+ {
+ memcpy(p, &value, sizeof(value));
+ p += sizeof(value);
+ };
+
+ // mov rax, &g_activeEntry
+ *p++ = 0x48; *p++ = 0xB8; write64(reinterpret_cast(&g_activeEntry));
+ // mov r11, [rax]
+ *p++ = 0x4C; *p++ = 0x8B; *p++ = 0x18;
+ // push r11
+ *p++ = 0x41; *p++ = 0x53;
+ // mov r11, entry
+ *p++ = 0x49; *p++ = 0xBB; write64(reinterpret_cast(entry));
+ // mov [rax], r11
+ *p++ = 0x4C; *p++ = 0x89; *p++ = 0x18;
+ // sub rsp, 32
+ *p++ = 0x48; *p++ = 0x83; *p++ = 0xEC; *p++ = 0x20;
+ // mov r11, handler
+ *p++ = 0x49; *p++ = 0xBB; write64(reinterpret_cast(handler));
+ // call r11
+ *p++ = 0x41; *p++ = 0xFF; *p++ = 0xD3;
+ // add rsp, 32
+ *p++ = 0x48; *p++ = 0x83; *p++ = 0xC4; *p++ = 0x20;
+ // mov rax, &g_activeEntry
+ *p++ = 0x48; *p++ = 0xB8; write64(reinterpret_cast(&g_activeEntry));
+ // pop r10
+ *p++ = 0x41; *p++ = 0x5A;
+ // mov [rax], r10
+ *p++ = 0x4C; *p++ = 0x89; *p++ = 0x10;
+ // ret
+ *p++ = 0xC3;
+
+ return code;
+ }
+}
+
+namespace HookRegistry
+{
+ bool AddHook(const char* fullName, int at, void* managedCallback, int require)
+ {
+ (void)require;
+ if (!fullName || !managedCallback)
+ return false;
+
+ std::lock_guard lock(g_mutex);
+ HookEntry* entry = nullptr;
+ auto it = g_hooks.find(fullName);
+ if (it == g_hooks.end())
+ {
+ const SymbolRegistry::Entry* sym = SymbolRegistry::Instance().FindEntry(fullName);
+ if (!sym || !sym->address)
+ return false;
+
+ auto created = std::make_unique();
+ created->fullName = fullName;
+ created->target = sym->address;
+ created->signatureKey = sym->signatureKey;
+ created->hasThis = !sym->isStatic;
+
+ if (!ParseSignature(created->signatureKey, created->retType, created->argTypes))
+ return false;
+ created->argCount = static_cast(created->argTypes.size());
+ if (created->argCount > 6)
+ return false;
+
+ void* handler = SelectHandler(created->hasThis, created->argCount);
+ created->thunk = CreateThunk(handler, created.get());
+ if (!created->thunk)
+ return false;
+
+ if (MH_Initialize() != MH_OK && MH_Initialize() != MH_ERROR_ALREADY_INITIALIZED)
+ return false;
+
+ if (MH_CreateHook(created->target, created->thunk, &created->original) != MH_OK)
+ return false;
+ if (MH_EnableHook(created->target) != MH_OK)
+ return false;
+
+ auto inserted = g_hooks.emplace(fullName, std::move(created));
+ entry = inserted.first->second.get();
+ }
+ else
+ {
+ entry = it->second.get();
+ }
+
+ if (!entry)
+ return false;
+
+ if (at == HookAt_Tail)
+ entry->post.push_back(managedCallback);
+ else
+ entry->pre.push_back(managedCallback);
+
+ LogUtil::Log("[WeaveLoader] HookRegistry: hooked %s", fullName);
+ return true;
+ }
+
+ bool RemoveHook(const char* fullName, void* managedCallback)
+ {
+ if (!fullName || !managedCallback)
+ return false;
+ std::lock_guard lock(g_mutex);
+ auto it = g_hooks.find(fullName);
+ if (it == g_hooks.end())
+ return false;
+ HookEntry& entry = *it->second;
+ auto removeFn = [&](std::vector& list)
+ {
+ list.erase(std::remove(list.begin(), list.end(), managedCallback), list.end());
+ };
+ removeFn(entry.pre);
+ removeFn(entry.post);
+ return true;
+ }
+}
diff --git a/WeaveLoaderRuntime/src/HookRegistry.h b/WeaveLoaderRuntime/src/HookRegistry.h
new file mode 100644
index 0000000..e17b00b
--- /dev/null
+++ b/WeaveLoaderRuntime/src/HookRegistry.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "NativeExports.h"
+#include
+#include
+
+namespace HookRegistry
+{
+ enum HookAt
+ {
+ HookAt_Head = 0,
+ HookAt_Tail = 1
+ };
+
+ struct HookContext
+ {
+ void* thisPtr = nullptr;
+ NativeArg* args = nullptr;
+ int argCount = 0;
+ NativeRet* ret = nullptr;
+ int cancel = 0;
+ };
+
+ using ManagedHookFn = void (__cdecl *)(HookContext* ctx);
+
+ bool AddHook(const char* fullName, int at, void* managedCallback, int require);
+ bool RemoveHook(const char* fullName, void* managedCallback);
+}
diff --git a/WeaveLoaderRuntime/src/NativeExports.cpp b/WeaveLoaderRuntime/src/NativeExports.cpp
index 47c6a3f..cd2e3f3 100644
--- a/WeaveLoaderRuntime/src/NativeExports.cpp
+++ b/WeaveLoaderRuntime/src/NativeExports.cpp
@@ -11,6 +11,8 @@
#include "ManagedBlockRegistry.h"
#include "ModStrings.h"
#include "LogUtil.h"
+#include "SymbolRegistry.h"
+#include "HookRegistry.h"
#include
#include
#include
@@ -770,4 +772,109 @@ void native_add_to_creative(int numericId, int count, int auxValue, int groupInd
CreativeInventory::AddPending(numericId, count, auxValue, groupIndex);
}
+void* native_find_symbol(const char* fullName)
+{
+ return SymbolRegistry::Instance().FindAddress(fullName);
+}
+
+int native_has_symbol(const char* fullName)
+{
+ return SymbolRegistry::Instance().Has(fullName) ? 1 : 0;
+}
+
+int native_get_signature_key(const char* fullName, char* outKey, int outLen)
+{
+ if (!outKey || outLen <= 0)
+ return 0;
+ const char* key = SymbolRegistry::Instance().FindSignatureKey(fullName);
+ if (!key || !key[0])
+ return 0;
+ const size_t len = strlen(key);
+ const size_t copyLen = (len < static_cast(outLen - 1)) ? len : static_cast(outLen - 1);
+ memcpy(outKey, key, copyLen);
+ outKey[copyLen] = '\0';
+ return static_cast(copyLen);
+}
+
+int native_invoke(void* fn, void* thisPtr, int hasThis, const NativeArg* args, int argCount, NativeRet* outRet)
+{
+ if (!fn || argCount < 0)
+ return 0;
+
+ for (int i = 0; i < argCount; ++i)
+ {
+ switch (args[i].type)
+ {
+ case NativeType_I32:
+ case NativeType_I64:
+ case NativeType_Ptr:
+ case NativeType_Bool:
+ break;
+ default:
+ return 0;
+ }
+ }
+
+ uint64_t a[6] = {};
+ const int maxArgs = (argCount > 6) ? 6 : argCount;
+ for (int i = 0; i < maxArgs; ++i)
+ a[i] = args[i].value;
+
+ uint64_t ret = 0;
+ if (hasThis)
+ {
+ switch (argCount)
+ {
+ case 0: ret = reinterpret_cast(fn)(thisPtr); break;
+ case 1: ret = reinterpret_cast(fn)(thisPtr, a[0]); break;
+ case 2: ret = reinterpret_cast(fn)(thisPtr, a[0], a[1]); break;
+ case 3: ret = reinterpret_cast(fn)(thisPtr, a[0], a[1], a[2]); break;
+ case 4: ret = reinterpret_cast(fn)(thisPtr, a[0], a[1], a[2], a[3]); break;
+ case 5: ret = reinterpret_cast(fn)(thisPtr, a[0], a[1], a[2], a[3], a[4]); break;
+ default: ret = reinterpret_cast(fn)(thisPtr, a[0], a[1], a[2], a[3], a[4], a[5]); break;
+ }
+ }
+ else
+ {
+ switch (argCount)
+ {
+ case 0: ret = reinterpret_cast(fn)(); break;
+ case 1: ret = reinterpret_cast(fn)(a[0]); break;
+ case 2: ret = reinterpret_cast(fn)(a[0], a[1]); break;
+ case 3: ret = reinterpret_cast(fn)(a[0], a[1], a[2]); break;
+ case 4: ret = reinterpret_cast(fn)(a[0], a[1], a[2], a[3]); break;
+ case 5: ret = reinterpret_cast(fn)(a[0], a[1], a[2], a[3], a[4]); break;
+ default: ret = reinterpret_cast(fn)(a[0], a[1], a[2], a[3], a[4], a[5]); break;
+ }
+ }
+
+ if (outRet)
+ {
+ switch (outRet->type)
+ {
+ case NativeType_I32:
+ outRet->value = static_cast(ret);
+ break;
+ case NativeType_Bool:
+ outRet->value = ret ? 1 : 0;
+ break;
+ default:
+ outRet->value = ret;
+ break;
+ }
+ }
+
+ return 1;
+}
+
+int native_mixin_add(const char* fullName, int at, void* managedCallback, int require)
+{
+ return HookRegistry::AddHook(fullName, at, managedCallback, require) ? 1 : 0;
+}
+
+int native_mixin_remove(const char* fullName, void* managedCallback)
+{
+ return HookRegistry::RemoveHook(fullName, managedCallback) ? 1 : 0;
+}
+
} // extern "C"
diff --git a/WeaveLoaderRuntime/src/NativeExports.h b/WeaveLoaderRuntime/src/NativeExports.h
index d12dce1..f194628 100644
--- a/WeaveLoaderRuntime/src/NativeExports.h
+++ b/WeaveLoaderRuntime/src/NativeExports.h
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
namespace NativeExports
{
@@ -14,6 +15,28 @@ namespace NativeExports
/// to IdRegistry for numeric ID allocation.
extern "C"
{
+ enum NativeType : uint8_t
+ {
+ NativeType_I32 = 1,
+ NativeType_I64 = 2,
+ NativeType_F32 = 3,
+ NativeType_F64 = 4,
+ NativeType_Ptr = 5,
+ NativeType_Bool = 6
+ };
+
+ struct NativeArg
+ {
+ NativeType type;
+ uint64_t value;
+ };
+
+ struct NativeRet
+ {
+ NativeType type;
+ uint64_t value;
+ };
+
__declspec(dllexport) int native_register_block(
const char* namespacedId,
int materialId,
@@ -170,4 +193,12 @@ extern "C"
int count,
int auxValue,
int groupIndex);
+
+ __declspec(dllexport) void* native_find_symbol(const char* fullName);
+ __declspec(dllexport) int native_has_symbol(const char* fullName);
+ __declspec(dllexport) int native_get_signature_key(const char* fullName, char* outKey, int outLen);
+ __declspec(dllexport) int native_invoke(void* fn, void* thisPtr, int hasThis, const NativeArg* args, int argCount, NativeRet* outRet);
+
+ __declspec(dllexport) int native_mixin_add(const char* fullName, int at, void* managedCallback, int require);
+ __declspec(dllexport) int native_mixin_remove(const char* fullName, void* managedCallback);
}
diff --git a/WeaveLoaderRuntime/src/PdbParser.cpp b/WeaveLoaderRuntime/src/PdbParser.cpp
index 2bebdc4..38d1cf9 100644
--- a/WeaveLoaderRuntime/src/PdbParser.cpp
+++ b/WeaveLoaderRuntime/src/PdbParser.cpp
@@ -1037,6 +1037,120 @@ bool FindNameByRVA(uint32_t rva, char* outName, size_t nameSize, uint32_t* outOf
return true;
}
+bool EnumerateSymbols(std::vector& outSymbols)
+{
+ if (!s_open)
+ return false;
+
+ outSymbols.clear();
+
+ auto pushSymbol = [&](const char* name, uint32_t rva, bool isProc, uint8_t source)
+ {
+ if (!name || !name[0] || rva == 0)
+ return;
+ SymbolInfo info;
+ info.name = name;
+ info.rva = rva;
+ info.isProc = isProc;
+ info.source = source;
+ outSymbols.push_back(std::move(info));
+ };
+
+ // Public symbols
+ {
+ const PDB::ArrayView records = s_publicStream->GetRecords();
+ for (const PDB::HashRecord& hashRecord : records)
+ {
+ const PDB::CodeView::DBI::Record* record = s_publicStream->GetRecord(*s_symbolRecords, hashRecord);
+ if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
+ {
+ const uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(
+ record->data.S_PUB32.section, record->data.S_PUB32.offset);
+ pushSymbol(record->data.S_PUB32.name, rva, true, 1);
+ }
+ }
+ }
+
+ // Global symbols
+ {
+ const PDB::ArrayView records = s_globalStream->GetRecords();
+ for (const PDB::HashRecord& hashRecord : records)
+ {
+ const PDB::CodeView::DBI::Record* record = s_globalStream->GetRecord(*s_symbolRecords, hashRecord);
+ uint16_t section = 0;
+ uint32_t offset = 0;
+ const char* name = GetGlobalSymName(record, section, offset);
+ if (name)
+ {
+ const uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
+ pushSymbol(name, rva, false, 2);
+ }
+ }
+ }
+
+ // Global PROCREF/LPROCREF
+ {
+ const PDB::ArrayView records = s_globalStream->GetRecords();
+ for (const PDB::HashRecord& hashRecord : records)
+ {
+ const PDB::CodeView::DBI::Record* record = s_globalStream->GetRecord(*s_symbolRecords, hashRecord);
+ uint16_t moduleIndex = 0;
+ uint32_t symbolOffset = 0;
+ const char* name = GetProcRefName(record, moduleIndex, symbolOffset);
+ if (!name)
+ continue;
+ const uint32_t rva = ResolveProcRefRVA(moduleIndex, symbolOffset);
+ if (rva != 0)
+ pushSymbol(name, rva, true, 3);
+ }
+ }
+
+ // Module symbols
+ {
+ const PDB::ArrayView modules = s_moduleStream->GetModules();
+ for (const PDB::ModuleInfoStream::Module& mod : modules)
+ {
+ if (!mod.HasSymbolStream())
+ continue;
+
+ const PDB::ModuleSymbolStream modSymStream = mod.CreateSymbolStream(*s_rawFile);
+ modSymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* record)
+ {
+ const char* name = nullptr;
+ uint16_t section = 0;
+ uint32_t offset = 0;
+ bool isProc = false;
+
+ switch (record->header.kind)
+ {
+ case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32:
+ name = record->data.S_LPROC32.name; section = record->data.S_LPROC32.section; offset = record->data.S_LPROC32.offset; isProc = true; break;
+ case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32:
+ name = record->data.S_GPROC32.name; section = record->data.S_GPROC32.section; offset = record->data.S_GPROC32.offset; isProc = true; break;
+ case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID:
+ name = record->data.S_LPROC32_ID.name; section = record->data.S_LPROC32_ID.section; offset = record->data.S_LPROC32_ID.offset; isProc = true; break;
+ case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID:
+ name = record->data.S_GPROC32_ID.name; section = record->data.S_GPROC32_ID.section; offset = record->data.S_GPROC32_ID.offset; isProc = true; break;
+ case PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32:
+ name = record->data.S_LDATA32.name; section = record->data.S_LDATA32.section; offset = record->data.S_LDATA32.offset; break;
+ case PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32:
+ name = record->data.S_GDATA32.name; section = record->data.S_GDATA32.section; offset = record->data.S_GDATA32.offset; break;
+ default:
+ return;
+ }
+
+ if (name)
+ {
+ const uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
+ pushSymbol(name, rva, isProc, 4);
+ }
+ });
+ }
+ }
+
+ return true;
+}
+
void Close()
{
delete s_moduleStream; s_moduleStream = nullptr;
diff --git a/WeaveLoaderRuntime/src/PdbParser.h b/WeaveLoaderRuntime/src/PdbParser.h
index 460e4d5..204ec10 100644
--- a/WeaveLoaderRuntime/src/PdbParser.h
+++ b/WeaveLoaderRuntime/src/PdbParser.h
@@ -1,8 +1,18 @@
#pragma once
#include
+#include
+#include
namespace PdbParser
{
+ struct SymbolInfo
+ {
+ uint32_t rva = 0;
+ std::string name;
+ bool isProc = false;
+ uint8_t source = 0;
+ };
+
bool Open(const char* pdbPath);
// Returns the RVA for a decorated symbol name, or 0 on failure.
@@ -29,5 +39,6 @@ namespace PdbParser
// Returns true if found. outName receives the symbol name, outOffset
// the byte distance from the symbol's start address.
bool FindNameByRVA(uint32_t rva, char* outName, size_t nameSize, uint32_t* outOffset);
+ bool EnumerateSymbols(std::vector& outSymbols);
void Close();
}
diff --git a/WeaveLoaderRuntime/src/SymbolRegistry.cpp b/WeaveLoaderRuntime/src/SymbolRegistry.cpp
new file mode 100644
index 0000000..3f283c3
--- /dev/null
+++ b/WeaveLoaderRuntime/src/SymbolRegistry.cpp
@@ -0,0 +1,214 @@
+#include "SymbolRegistry.h"
+#include "LogUtil.h"
+#include
+#include
+
+namespace
+{
+ bool ReadFile(const char* path, std::string& out)
+ {
+ std::ifstream in(path, std::ios::in | std::ios::binary);
+ if (!in.is_open())
+ return false;
+ in.seekg(0, std::ios::end);
+ const std::streamsize size = in.tellg();
+ in.seekg(0, std::ios::beg);
+ if (size <= 0)
+ return false;
+ out.resize(static_cast(size));
+ in.read(&out[0], size);
+ return true;
+ }
+
+ size_t SkipWhitespace(const std::string& s, size_t pos)
+ {
+ while (pos < s.size() && (s[pos] == ' ' || s[pos] == '\n' || s[pos] == '\r' || s[pos] == '\t'))
+ ++pos;
+ return pos;
+ }
+
+ bool ExtractStringField(const std::string& s, size_t start, size_t end, const char* key, std::string& out)
+ {
+ const std::string needle = std::string("\"") + key + "\"";
+ size_t pos = s.find(needle, start);
+ if (pos == std::string::npos || pos >= end)
+ return false;
+ pos = s.find(':', pos);
+ if (pos == std::string::npos || pos >= end)
+ return false;
+ pos = SkipWhitespace(s, pos + 1);
+ if (pos >= end || s[pos] != '"')
+ return false;
+ ++pos;
+ std::string value;
+ while (pos < end)
+ {
+ char c = s[pos++];
+ if (c == '\\' && pos < end)
+ {
+ char esc = s[pos++];
+ switch (esc)
+ {
+ case '"': value.push_back('"'); break;
+ case '\\': value.push_back('\\'); break;
+ case 'n': value.push_back('\n'); break;
+ case 'r': value.push_back('\r'); break;
+ case 't': value.push_back('\t'); break;
+ default: value.push_back(esc); break;
+ }
+ continue;
+ }
+ if (c == '"')
+ break;
+ value.push_back(c);
+ }
+ out = std::move(value);
+ return true;
+ }
+
+ bool ExtractNumberField(const std::string& s, size_t start, size_t end, const char* key, uint32_t& out)
+ {
+ const std::string needle = std::string("\"") + key + "\"";
+ size_t pos = s.find(needle, start);
+ if (pos == std::string::npos || pos >= end)
+ return false;
+ pos = s.find(':', pos);
+ if (pos == std::string::npos || pos >= end)
+ return false;
+ pos = SkipWhitespace(s, pos + 1);
+ if (pos >= end)
+ return false;
+ uint32_t value = 0;
+ while (pos < end && s[pos] >= '0' && s[pos] <= '9')
+ {
+ value = value * 10 + static_cast(s[pos] - '0');
+ ++pos;
+ }
+ out = value;
+ return true;
+ }
+
+ bool ExtractBoolField(const std::string& s, size_t start, size_t end, const char* key, bool& out)
+ {
+ const std::string needle = std::string("\"") + key + "\"";
+ size_t pos = s.find(needle, start);
+ if (pos == std::string::npos || pos >= end)
+ return false;
+ pos = s.find(':', pos);
+ if (pos == std::string::npos || pos >= end)
+ return false;
+ pos = SkipWhitespace(s, pos + 1);
+ if (pos + 4 <= end && s.compare(pos, 4, "true") == 0)
+ {
+ out = true;
+ return true;
+ }
+ if (pos + 5 <= end && s.compare(pos, 5, "false") == 0)
+ {
+ out = false;
+ return true;
+ }
+ return false;
+ }
+}
+
+SymbolRegistry& SymbolRegistry::Instance()
+{
+ static SymbolRegistry instance;
+ return instance;
+}
+
+bool SymbolRegistry::LoadFromFile(const char* path)
+{
+ if (!path || !path[0])
+ return false;
+
+ std::string data;
+ if (!ReadFile(path, data))
+ {
+ LogUtil::Log("[WeaveLoader] SymbolRegistry: failed to read mapping '%s'", path);
+ return false;
+ }
+
+ m_entries.clear();
+ m_moduleBase = reinterpret_cast(GetModuleHandleA(nullptr));
+ if (!m_moduleBase)
+ {
+ LogUtil::Log("[WeaveLoader] SymbolRegistry: failed to get module base");
+ return false;
+ }
+
+ size_t pos = 0;
+ size_t count = 0;
+ while (true)
+ {
+ const size_t fullPos = data.find("\"fullName\"", pos);
+ if (fullPos == std::string::npos)
+ break;
+
+ const size_t objEnd = data.find('}', fullPos);
+ if (objEnd == std::string::npos)
+ break;
+
+ std::string fullName;
+ uint32_t rva = 0;
+ std::string sig;
+
+ if (ExtractStringField(data, fullPos, objEnd, "fullName", fullName) &&
+ ExtractNumberField(data, fullPos, objEnd, "rva", rva))
+ {
+ ExtractStringField(data, fullPos, objEnd, "signatureKey", sig);
+ bool isStatic = false;
+ ExtractBoolField(data, fullPos, objEnd, "isStatic", isStatic);
+ Entry entry;
+ entry.rva = rva;
+ entry.address = reinterpret_cast(m_moduleBase + rva);
+ entry.signatureKey = sig;
+ entry.isStatic = isStatic;
+ m_entries[fullName] = std::move(entry);
+ ++count;
+ }
+
+ pos = objEnd + 1;
+ }
+
+ LogUtil::Log("[WeaveLoader] SymbolRegistry: loaded %zu symbol(s) from %s", count, path);
+ return count > 0;
+}
+
+bool SymbolRegistry::Has(const char* fullName) const
+{
+ if (!fullName)
+ return false;
+ return m_entries.find(fullName) != m_entries.end();
+}
+
+void* SymbolRegistry::FindAddress(const char* fullName) const
+{
+ if (!fullName)
+ return nullptr;
+ auto it = m_entries.find(fullName);
+ if (it == m_entries.end())
+ return nullptr;
+ return it->second.address;
+}
+
+const char* SymbolRegistry::FindSignatureKey(const char* fullName) const
+{
+ if (!fullName)
+ return nullptr;
+ auto it = m_entries.find(fullName);
+ if (it == m_entries.end())
+ return nullptr;
+ return it->second.signatureKey.c_str();
+}
+
+const SymbolRegistry::Entry* SymbolRegistry::FindEntry(const char* fullName) const
+{
+ if (!fullName)
+ return nullptr;
+ auto it = m_entries.find(fullName);
+ if (it == m_entries.end())
+ return nullptr;
+ return &it->second;
+}
diff --git a/WeaveLoaderRuntime/src/SymbolRegistry.h b/WeaveLoaderRuntime/src/SymbolRegistry.h
new file mode 100644
index 0000000..63f0c39
--- /dev/null
+++ b/WeaveLoaderRuntime/src/SymbolRegistry.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include
+#include
+
+class SymbolRegistry
+{
+public:
+ struct Entry
+ {
+ uint32_t rva = 0;
+ void* address = nullptr;
+ std::string signatureKey;
+ bool isStatic = false;
+ };
+
+ static SymbolRegistry& Instance();
+
+ bool LoadFromFile(const char* path);
+ bool Has(const char* fullName) const;
+ void* FindAddress(const char* fullName) const;
+ const char* FindSignatureKey(const char* fullName) const;
+ const Entry* FindEntry(const char* fullName) const;
+
+private:
+ std::unordered_map m_entries;
+ uintptr_t m_moduleBase = 0;
+};
diff --git a/WeaveLoaderRuntime/src/dllmain.cpp b/WeaveLoaderRuntime/src/dllmain.cpp
index 9d75989..ab05783 100644
--- a/WeaveLoaderRuntime/src/dllmain.cpp
+++ b/WeaveLoaderRuntime/src/dllmain.cpp
@@ -5,6 +5,7 @@
#include "CrashHandler.h"
#include "PdbParser.h"
#include "SymbolResolver.h"
+#include "SymbolRegistry.h"
#include "HookManager.h"
#include "DotNetHost.h"
#include "MainMenuOverlay.h"
@@ -32,6 +33,13 @@ DWORD WINAPI InitThread(LPVOID lpParam)
std::string baseDir = GetDllDirectory(g_hModule);
LogUtil::Log("[WeaveLoader] Runtime DLL directory: %s", baseDir.c_str());
+ std::string mappingPath = baseDir + "metadata\\mapping.json";
+ if (!SymbolRegistry::Instance().LoadFromFile(mappingPath.c_str()))
+ {
+ std::string fallbackPath = baseDir + "mapping.json";
+ SymbolRegistry::Instance().LoadFromFile(fallbackPath.c_str());
+ }
+
char cwd[MAX_PATH] = {0};
GetCurrentDirectoryA(MAX_PATH, cwd);
LogUtil::Log("[WeaveLoader] Game working directory: %s", cwd);
diff --git a/WeaveLoaderRuntime/tools/pdbdump.cpp b/WeaveLoaderRuntime/tools/pdbdump.cpp
new file mode 100644
index 0000000..ae9a1ae
--- /dev/null
+++ b/WeaveLoaderRuntime/tools/pdbdump.cpp
@@ -0,0 +1,618 @@
+#include "PdbParser.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+#ifndef SymTagData
+ constexpr DWORD SymTagData = 7;
+#endif
+ std::string Trim(const std::string& s)
+ {
+ size_t start = 0;
+ while (start < s.size() && std::isspace(static_cast(s[start])))
+ ++start;
+ size_t end = s.size();
+ while (end > start && std::isspace(static_cast(s[end - 1])))
+ --end;
+ return s.substr(start, end - start);
+ }
+
+ std::string ToLower(std::string s)
+ {
+ std::transform(s.begin(), s.end(), s.begin(),
+ [](unsigned char c) { return static_cast(std::tolower(c)); });
+ return s;
+ }
+
+ void CleanName(std::string& s)
+ {
+ s.erase(std::remove_if(s.begin(), s.end(),
+ [](unsigned char c)
+ {
+ return c == '\0' || c < 0x20;
+ }), s.end());
+ s = Trim(s);
+ }
+
+ bool Contains(const std::string& s, const char* needle)
+ {
+ return s.find(needle) != std::string::npos;
+ }
+
+ std::string Demangle(const std::string& decorated)
+ {
+ char buffer[2048] = {};
+ if (UnDecorateSymbolName(decorated.c_str(), buffer, sizeof(buffer), UNDNAME_COMPLETE) == 0)
+ return decorated;
+ return std::string(buffer);
+ }
+
+ struct TypeInfo
+ {
+ std::string raw;
+ std::string canonical;
+ char token = 'u';
+ bool callable = false;
+ bool hasRef = false;
+ };
+
+ TypeInfo NormalizeType(const std::string& input)
+ {
+ TypeInfo info;
+ info.raw = Trim(input);
+ std::string t = info.raw;
+ if (t.empty())
+ return info;
+
+ if (t.find('&') != std::string::npos)
+ info.hasRef = true;
+
+ bool isPtr = t.find('*') != std::string::npos;
+ std::string lower = ToLower(t);
+
+ auto stripToken = [&](const char* token)
+ {
+ size_t pos = lower.find(token);
+ if (pos != std::string::npos)
+ {
+ t.erase(pos, strlen(token));
+ lower.erase(pos, strlen(token));
+ }
+ };
+
+ const char* qualifiers[] = {
+ "const", "volatile", "class", "struct", "enum", "__ptr64", "__cdecl",
+ "__thiscall", "__stdcall", "__vectorcall", "__fastcall"
+ };
+ for (const char* q : qualifiers)
+ stripToken(q);
+
+ t = Trim(t);
+ lower = ToLower(t);
+
+ if (isPtr || Contains(lower, "*"))
+ {
+ info.canonical = "ptr";
+ info.token = 'p';
+ info.callable = !info.hasRef;
+ return info;
+ }
+
+ if (Contains(lower, "bool"))
+ {
+ info.canonical = "bool";
+ info.token = 'b';
+ info.callable = !info.hasRef;
+ return info;
+ }
+ if (Contains(lower, "float"))
+ {
+ info.canonical = "float";
+ info.token = 'f';
+ info.callable = !info.hasRef;
+ return info;
+ }
+ if (Contains(lower, "double"))
+ {
+ info.canonical = "double";
+ info.token = 'd';
+ info.callable = !info.hasRef;
+ return info;
+ }
+ if (Contains(lower, "long long") || Contains(lower, "__int64") || Contains(lower, "int64"))
+ {
+ info.canonical = "int64";
+ info.token = 'l';
+ info.callable = !info.hasRef;
+ return info;
+ }
+ if (Contains(lower, "int") || Contains(lower, "short") || Contains(lower, "char") || Contains(lower, "long"))
+ {
+ info.canonical = "int";
+ info.token = 'i';
+ info.callable = !info.hasRef;
+ return info;
+ }
+ if (Contains(lower, "void"))
+ {
+ info.canonical = "void";
+ info.token = 'v';
+ info.callable = true;
+ return info;
+ }
+
+ info.canonical = Trim(t);
+ info.token = 'u';
+ info.callable = false;
+ return info;
+ }
+
+ std::vector SplitParams(const std::string& params)
+ {
+ std::vector out;
+ std::string current;
+ int depth = 0;
+ for (size_t i = 0; i < params.size(); ++i)
+ {
+ char c = params[i];
+ if (c == '<')
+ ++depth;
+ else if (c == '>' && depth > 0)
+ --depth;
+
+ if (c == ',' && depth == 0)
+ {
+ out.push_back(Trim(current));
+ current.clear();
+ continue;
+ }
+ current.push_back(c);
+ }
+ if (!current.empty())
+ out.push_back(Trim(current));
+ return out;
+ }
+
+ struct MethodInfo
+ {
+ std::string className;
+ std::string methodName;
+ std::string fullName;
+ std::string decorated;
+ std::string undecorated;
+ uint32_t rva = 0;
+ bool isStatic = false;
+ bool isVirtual = false;
+ TypeInfo returnType;
+ std::vector params;
+ std::string signatureKey;
+ char tier = 'B';
+ };
+
+ std::string ExtractReturnType(const std::string& undecorated, size_t methodPos)
+ {
+ std::string prefix = Trim(undecorated.substr(0, methodPos));
+ if (prefix.empty())
+ return "void";
+
+ std::vector tokens;
+ {
+ std::istringstream iss(prefix);
+ std::string tok;
+ while (iss >> tok)
+ tokens.push_back(tok);
+ }
+
+ auto isQualifier = [](const std::string& tok)
+ {
+ static const char* kQualifiers[] = {
+ "public:", "private:", "protected:", "static", "virtual", "inline",
+ "__cdecl", "__thiscall", "__stdcall", "__vectorcall", "__fastcall"
+ };
+ for (const char* q : kQualifiers)
+ if (tok == q)
+ return true;
+ return false;
+ };
+
+ std::string result;
+ for (const std::string& tok : tokens)
+ {
+ if (isQualifier(tok))
+ continue;
+ if (!result.empty())
+ result.push_back(' ');
+ result += tok;
+ }
+
+ if (result.empty())
+ return "void";
+ return result;
+ }
+
+ MethodInfo BuildMethod(const PdbParser::SymbolInfo& sym)
+ {
+ MethodInfo info;
+ info.decorated = sym.name;
+ info.rva = sym.rva;
+ info.undecorated = Demangle(info.decorated);
+
+ const size_t parenPos = info.undecorated.find('(');
+ const size_t closePos = info.undecorated.rfind(')');
+ if (parenPos == std::string::npos || closePos == std::string::npos || closePos <= parenPos)
+ return info;
+
+ const std::string beforeParen = info.undecorated.substr(0, parenPos);
+ const size_t classPos = beforeParen.rfind("::");
+ if (classPos != std::string::npos)
+ {
+ size_t nameStart = classPos + 2;
+ info.methodName = beforeParen.substr(nameStart);
+ size_t classStart = beforeParen.rfind(' ', classPos);
+ if (classStart == std::string::npos)
+ classStart = 0;
+ else
+ classStart += 1;
+ info.className = beforeParen.substr(classStart, classPos - classStart);
+ }
+ else
+ {
+ info.className = "Global";
+ info.methodName = Trim(beforeParen);
+ }
+
+ info.fullName = info.className + "::" + info.methodName;
+ info.isStatic = Contains(info.undecorated, " static ");
+ info.isVirtual = Contains(info.undecorated, " virtual ");
+
+ std::string returnStr = ExtractReturnType(info.undecorated, classPos == std::string::npos ? 0 : classPos);
+ info.returnType = NormalizeType(returnStr);
+
+ std::string paramsStr = info.undecorated.substr(parenPos + 1, closePos - parenPos - 1);
+ paramsStr = Trim(paramsStr);
+ if (!paramsStr.empty() && paramsStr != "void")
+ {
+ for (const std::string& p : SplitParams(paramsStr))
+ {
+ if (p.empty())
+ continue;
+ info.params.push_back(NormalizeType(p));
+ }
+ }
+
+ std::ostringstream sig;
+ sig << info.returnType.token;
+ for (const TypeInfo& p : info.params)
+ sig << "_" << p.token;
+ info.signatureKey = sig.str();
+
+ bool ok = info.returnType.callable;
+ for (const TypeInfo& p : info.params)
+ ok = ok && p.callable;
+ info.tier = ok ? 'A' : 'B';
+ return info;
+ }
+
+ void WriteJsonString(std::ostream& out, const std::string& s)
+ {
+ out << '"';
+ for (char c : s)
+ {
+ switch (c)
+ {
+ case '"': out << "\\\""; break;
+ case '\\': out << "\\\\"; break;
+ case '\n': out << "\\n"; break;
+ case '\r': out << "\\r"; break;
+ case '\t': out << "\\t"; break;
+ default: out << c; break;
+ }
+ }
+ out << '"';
+ }
+}
+
+struct EnumTypesContext
+{
+ std::vector* outTypes = nullptr;
+};
+
+static BOOL CALLBACK EnumTypesCallback(PSYMBOL_INFO symInfo, ULONG, void* userContext)
+{
+ if (!symInfo || !userContext)
+ return TRUE;
+ auto* ctx = reinterpret_cast(userContext);
+ if (!ctx->outTypes)
+ return TRUE;
+ if (symInfo->NameLen > 0)
+ {
+ std::string name(symInfo->Name, symInfo->NameLen);
+ CleanName(name);
+ if (!name.empty())
+ ctx->outTypes->push_back(std::move(name));
+ }
+ return TRUE;
+}
+
+static bool WriteOffsetsJson(const char* exePath, const char* outPath, const std::vector& types, bool allTypes)
+{
+ if (!exePath || !outPath)
+ return false;
+
+ HANDLE proc = GetCurrentProcess();
+ SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
+ if (!SymInitialize(proc, nullptr, FALSE))
+ return false;
+
+ DWORD64 base = SymLoadModuleEx(proc, nullptr, exePath, nullptr, 0, 0, nullptr, 0);
+ if (base == 0)
+ {
+ SymCleanup(proc);
+ return false;
+ }
+
+ auto getTypeFields = [&](const std::string& typeName, std::vector>& outFields)
+ {
+ outFields.clear();
+ SYMBOL_INFO* sym = reinterpret_cast(calloc(1, sizeof(SYMBOL_INFO) + MAX_SYM_NAME));
+ if (!sym)
+ return;
+ sym->SizeOfStruct = sizeof(SYMBOL_INFO);
+ sym->MaxNameLen = MAX_SYM_NAME;
+ if (!SymGetTypeFromName(proc, base, typeName.c_str(), sym))
+ {
+ free(sym);
+ return;
+ }
+
+ DWORD typeId = sym->TypeIndex;
+ ULONG childrenCount = 0;
+ if (!SymGetTypeInfo(proc, base, typeId, TI_GET_CHILDRENCOUNT, &childrenCount) || childrenCount == 0)
+ {
+ free(sym);
+ return;
+ }
+
+ const size_t paramsSize = sizeof(TI_FINDCHILDREN_PARAMS) + sizeof(ULONG) * childrenCount;
+ auto* params = reinterpret_cast(calloc(1, paramsSize));
+ if (!params)
+ {
+ free(sym);
+ return;
+ }
+
+ params->Count = childrenCount;
+ params->Start = 0;
+ if (!SymGetTypeInfo(proc, base, typeId, TI_FINDCHILDREN, params))
+ {
+ free(params);
+ free(sym);
+ return;
+ }
+
+ for (ULONG i = 0; i < childrenCount; ++i)
+ {
+ DWORD childId = params->ChildId[i];
+ DWORD tag = 0;
+ if (!SymGetTypeInfo(proc, base, childId, TI_GET_SYMTAG, &tag))
+ continue;
+ if (tag != SymTagData)
+ continue;
+
+ BSTR bstrName = nullptr;
+ if (!SymGetTypeInfo(proc, base, childId, TI_GET_SYMNAME, &bstrName) || !bstrName)
+ continue;
+
+ ULONG offset = 0;
+ if (!SymGetTypeInfo(proc, base, childId, TI_GET_OFFSET, &offset))
+ {
+ SysFreeString(bstrName);
+ continue;
+ }
+
+ int len = WideCharToMultiByte(CP_UTF8, 0, bstrName, -1, nullptr, 0, nullptr, nullptr);
+ std::string name;
+ if (len > 1)
+ {
+ name.resize(static_cast(len - 1));
+ WideCharToMultiByte(CP_UTF8, 0, bstrName, -1, name.data(), len, nullptr, nullptr);
+ }
+ SysFreeString(bstrName);
+
+ CleanName(name);
+ if (!name.empty())
+ outFields.emplace_back(std::move(name), offset);
+ }
+
+ free(params);
+ free(sym);
+ };
+
+ std::vector resolvedTypes = types;
+ if (allTypes)
+ {
+ resolvedTypes.clear();
+ EnumTypesContext ctx{ &resolvedTypes };
+ SymEnumTypes(proc, base, EnumTypesCallback, &ctx);
+ std::sort(resolvedTypes.begin(), resolvedTypes.end());
+ resolvedTypes.erase(std::unique(resolvedTypes.begin(), resolvedTypes.end()), resolvedTypes.end());
+ }
+
+ std::ofstream out(outPath, std::ios::out | std::ios::trunc);
+ if (!out.is_open())
+ {
+ SymCleanup(proc);
+ return false;
+ }
+
+ out << "{\n";
+ bool firstType = true;
+ for (const std::string& typeName : resolvedTypes)
+ {
+ std::vector> fields;
+ getTypeFields(typeName, fields);
+ if (fields.empty())
+ continue;
+
+ if (!firstType) out << ",\n";
+ firstType = false;
+ out << " ";
+ WriteJsonString(out, typeName);
+ out << ": {\n";
+ bool firstField = true;
+ for (const auto& [fieldName, offset] : fields)
+ {
+ if (!firstField) out << ",\n";
+ firstField = false;
+ out << " ";
+ WriteJsonString(out, fieldName);
+ out << ": " << offset;
+ }
+ out << "\n }";
+ }
+ out << "\n}\n";
+
+ SymCleanup(proc);
+ return true;
+}
+
+int main(int argc, char** argv)
+{
+ if (argc < 3)
+ {
+ std::cerr << "Usage: pdbdump \n";
+ return 1;
+ }
+
+ const char* pdbPath = argv[1];
+ const char* outPath = argv[2];
+
+ const char* offsetsExe = nullptr;
+ const char* offsetsOut = nullptr;
+ std::vector offsetTypes;
+ bool allTypes = false;
+ for (int i = 3; i < argc; ++i)
+ {
+ if (strcmp(argv[i], "--offsets") == 0 && i + 2 < argc)
+ {
+ offsetsExe = argv[++i];
+ offsetsOut = argv[++i];
+ }
+ else if (strcmp(argv[i], "--type") == 0 && i + 1 < argc)
+ {
+ offsetTypes.emplace_back(argv[++i]);
+ }
+ else if (strcmp(argv[i], "--all-types") == 0)
+ {
+ allTypes = true;
+ }
+ }
+ if (!allTypes && offsetTypes.empty())
+ allTypes = true;
+
+ if (!PdbParser::Open(pdbPath))
+ {
+ std::cerr << "Failed to open PDB: " << pdbPath << "\n";
+ return 1;
+ }
+
+ std::vector symbols;
+ if (!PdbParser::EnumerateSymbols(symbols))
+ {
+ std::cerr << "Failed to enumerate symbols\n";
+ return 1;
+ }
+
+ std::unordered_map> byClass;
+ std::unordered_map seen;
+ for (const auto& sym : symbols)
+ {
+ if (!sym.isProc)
+ continue;
+
+ MethodInfo mi = BuildMethod(sym);
+ if (mi.methodName.empty())
+ continue;
+
+ const std::string key = mi.decorated + ":" + std::to_string(mi.rva);
+ if (seen.emplace(key, mi.rva).second == false)
+ continue;
+
+ byClass[mi.className].push_back(std::move(mi));
+ }
+
+ std::ofstream out(outPath, std::ios::out | std::ios::trunc);
+ if (!out.is_open())
+ {
+ std::cerr << "Failed to open output: " << outPath << "\n";
+ return 1;
+ }
+
+ out << "{\n \"types\": [\n";
+ bool firstType = true;
+ for (auto& [className, methods] : byClass)
+ {
+ if (!firstType) out << ",\n";
+ firstType = false;
+ out << " {\"name\": ";
+ WriteJsonString(out, className);
+ out << ", \"methods\": [\n";
+
+ bool firstMethod = true;
+ for (const MethodInfo& mi : methods)
+ {
+ if (!firstMethod) out << ",\n";
+ firstMethod = false;
+
+ out << " {\"name\": ";
+ WriteJsonString(out, mi.methodName);
+ out << ", \"fullName\": ";
+ WriteJsonString(out, mi.fullName);
+ out << ", \"decorated\": ";
+ WriteJsonString(out, mi.decorated);
+ out << ", \"undecorated\": ";
+ WriteJsonString(out, mi.undecorated);
+ out << ", \"rva\": " << mi.rva;
+ out << ", \"isStatic\": " << (mi.isStatic ? "true" : "false");
+ out << ", \"isVirtual\": " << (mi.isVirtual ? "true" : "false");
+ out << ", \"returnType\": ";
+ WriteJsonString(out, mi.returnType.canonical);
+ out << ", \"paramTypes\": [";
+ for (size_t i = 0; i < mi.params.size(); ++i)
+ {
+ if (i) out << ", ";
+ WriteJsonString(out, mi.params[i].canonical);
+ }
+ out << "]";
+ out << ", \"signatureKey\": ";
+ WriteJsonString(out, mi.signatureKey);
+ out << ", \"callableTier\": ";
+ WriteJsonString(out, std::string(1, mi.tier));
+ out << "}";
+ }
+
+ out << "\n ]}";
+ }
+ out << "\n ]\n}\n";
+
+ std::cout << "Wrote " << outPath << "\n";
+
+ if (offsetsExe && offsetsOut)
+ {
+ if (WriteOffsetsJson(offsetsExe, offsetsOut, offsetTypes, allTypes))
+ std::cout << "Wrote " << offsetsOut << "\n";
+ else
+ std::cout << "Failed to write " << offsetsOut << "\n";
+ }
+ return 0;
+}