From be327befa427c3631c93b5e16eac97be617e1b02 Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Tue, 10 Mar 2026 17:45:25 -0500 Subject: [PATCH] feat(modloader): pdb mapping, dynamic invoke, mixins --- ExampleMod/ExampleMod.cs | 19 + ExampleMod/ExampleMod.csproj | 1 + ExampleMod/Mixins/CreeperExplosionMixin.cs | 127 ++++ ExampleMod/weave.mixins.json | 13 + WeaveLoader.API/Mixins/At.cs | 7 + WeaveLoader.API/Mixins/InjectAttribute.cs | 18 + WeaveLoader.API/Mixins/MixinAttribute.cs | 14 + WeaveLoader.API/Mixins/MixinContext.cs | 68 ++ WeaveLoader.API/Mixins/OverwriteAttribute.cs | 8 + WeaveLoader.API/Native/NativeClass.cs | 54 ++ WeaveLoader.API/Native/NativeInvoker.cs | 23 + WeaveLoader.API/Native/NativeLevel.cs | 10 + WeaveLoader.API/Native/NativeMemory.cs | 25 + WeaveLoader.API/Native/NativeObject.cs | 109 ++++ WeaveLoader.API/Native/NativeOffsets.cs | 189 ++++++ WeaveLoader.API/Native/NativeSymbol.cs | 19 + WeaveLoader.API/Native/NativeTypes.cs | 37 ++ WeaveLoader.API/NativeInterop.cs | 18 + WeaveLoader.Core/MixinLoader.cs | 170 +++++ WeaveLoader.Core/ModDiscovery.cs | 12 +- WeaveLoader.Core/WeaveLoader.Core.csproj | 1 + WeaveLoader.Core/WeaveLoaderCore.cs | 1 + WeaveLoader.Launcher/Program.cs | 41 ++ WeaveLoaderRuntime/CMakeLists.txt | 30 + WeaveLoaderRuntime/src/HookRegistry.cpp | 369 +++++++++++ WeaveLoaderRuntime/src/HookRegistry.h | 28 + WeaveLoaderRuntime/src/NativeExports.cpp | 107 ++++ WeaveLoaderRuntime/src/NativeExports.h | 31 + WeaveLoaderRuntime/src/PdbParser.cpp | 114 ++++ WeaveLoaderRuntime/src/PdbParser.h | 11 + WeaveLoaderRuntime/src/SymbolRegistry.cpp | 214 +++++++ WeaveLoaderRuntime/src/SymbolRegistry.h | 28 + WeaveLoaderRuntime/src/dllmain.cpp | 8 + WeaveLoaderRuntime/tools/pdbdump.cpp | 618 +++++++++++++++++++ 34 files changed, 2535 insertions(+), 7 deletions(-) create mode 100644 ExampleMod/Mixins/CreeperExplosionMixin.cs create mode 100644 ExampleMod/weave.mixins.json create mode 100644 WeaveLoader.API/Mixins/At.cs create mode 100644 WeaveLoader.API/Mixins/InjectAttribute.cs create mode 100644 WeaveLoader.API/Mixins/MixinAttribute.cs create mode 100644 WeaveLoader.API/Mixins/MixinContext.cs create mode 100644 WeaveLoader.API/Mixins/OverwriteAttribute.cs create mode 100644 WeaveLoader.API/Native/NativeClass.cs create mode 100644 WeaveLoader.API/Native/NativeInvoker.cs create mode 100644 WeaveLoader.API/Native/NativeLevel.cs create mode 100644 WeaveLoader.API/Native/NativeMemory.cs create mode 100644 WeaveLoader.API/Native/NativeObject.cs create mode 100644 WeaveLoader.API/Native/NativeOffsets.cs create mode 100644 WeaveLoader.API/Native/NativeSymbol.cs create mode 100644 WeaveLoader.API/Native/NativeTypes.cs create mode 100644 WeaveLoader.Core/MixinLoader.cs create mode 100644 WeaveLoaderRuntime/src/HookRegistry.cpp create mode 100644 WeaveLoaderRuntime/src/HookRegistry.h create mode 100644 WeaveLoaderRuntime/src/SymbolRegistry.cpp create mode 100644 WeaveLoaderRuntime/src/SymbolRegistry.h create mode 100644 WeaveLoaderRuntime/tools/pdbdump.cpp 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; +}