feat(modloader): pdb mapping, dynamic invoke, mixins

This commit is contained in:
Jacobwasbeast
2026-03-10 17:45:25 -05:00
parent 70dbff3fac
commit be327befa4
34 changed files with 2535 additions and 7 deletions

View File

@@ -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;
}
}

View File

@@ -22,6 +22,7 @@
<ItemGroup>
<Content Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="weave.mixins.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -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<nint> 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);
}
}

View File

@@ -0,0 +1,13 @@
{
"required": false,
"minVersion": "0.1",
"package": "ExampleMod.Mixins",
"mixins": [],
"client": [
"CreeperExplosionMixin"
],
"server": [],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -0,0 +1,7 @@
namespace WeaveLoader.API.Mixins;
public enum At
{
Head = 0,
Tail = 1
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<NativeArg>();
nint ptr = ctx.Args + index * size;
return Marshal.PtrToStructure<NativeArg>(ptr);
}
public void SetArg(int index, NativeArg arg)
{
var ctx = Read();
int size = Marshal.SizeOf<NativeArg>();
nint ptr = ctx.Args + index * size;
Marshal.StructureToPtr(arg, ptr, false);
}
public NativeRet GetReturn()
{
var ctx = Read();
return Marshal.PtrToStructure<NativeRet>(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<MixinContextNative>(_ctxPtr);
}
}

View File

@@ -0,0 +1,8 @@
using System;
namespace WeaveLoader.API.Mixins;
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class OverwriteAttribute : Attribute
{
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
namespace WeaveLoader.API.Native;
public sealed class NativeClass
{
public string Name { get; }
private readonly Dictionary<string, nint> _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<T>(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<T>(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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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<string, nint> _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<T>(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<T>(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<T>(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;
}
}

View File

@@ -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<string, Dictionary<string, int>>? 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<Dictionary<string, Dictionary<string, int>>>(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<string>(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<string> GetCandidates()
{
var list = new List<string>();
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<string, Dictionary<string, int>> NormalizeOffsets(
Dictionary<string, Dictionary<string, int>> raw)
{
var result = new Dictionary<string, Dictionary<string, int>>();
foreach (var (typeName, fields) in raw)
{
string typeKey = NormalizeName(typeName);
var fieldMap = new Dictionary<string, int>();
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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<Delegate> s_callbacks = new();
private static readonly JsonSerializerOptions s_jsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
internal static void LoadMixins(IEnumerable<ModDiscovery.DiscoveredMod> 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<MixinConfig>(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<string>();
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<MixinAttribute>();
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<InjectAttribute>();
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})");
}
}
}
}

View File

@@ -9,7 +9,8 @@ internal static class ModDiscovery
internal record DiscoveredMod(
IMod Instance,
ModAttribute Metadata,
Assembly Assembly);
Assembly Assembly,
string Folder);
internal static List<DiscoveredMod> DiscoverMods(string modsPath)
{
@@ -27,7 +28,7 @@ internal static class ModDiscovery
{
var apiMod = new WeaveLoaderApiMod();
var attr = typeof(WeaveLoaderApiMod).GetCustomAttribute<ModAttribute>()!;
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<DiscoveredMod>();
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;
}
}

View File

@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RootNamespace>WeaveLoader.Core</RootNamespace>
<AssemblyName>WeaveLoader.Core</AssemblyName>
<Description>WeaveLoader core runtime - mod discovery and lifecycle management</Description>

View File

@@ -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;
}

View File

@@ -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.");

View File

@@ -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"
)

View File

@@ -0,0 +1,369 @@
#include "HookRegistry.h"
#include "SymbolRegistry.h"
#include "LogUtil.h"
#include <Windows.h>
#include <MinHook.h>
#include <unordered_map>
#include <mutex>
#include <algorithm>
#include <memory>
#include <cstring>
namespace
{
struct HookEntry
{
std::string fullName;
void* target = nullptr;
void* original = nullptr;
std::string signatureKey;
NativeType retType = NativeType_I32;
std::vector<NativeType> argTypes;
int argCount = 0;
bool hasThis = true;
std::vector<void*> pre;
std::vector<void*> post;
void* thunk = nullptr;
};
std::unordered_map<std::string, std::unique_ptr<HookEntry>> g_hooks;
std::mutex g_mutex;
HookEntry* g_activeEntry = nullptr;
bool ParseSignature(const std::string& key, NativeType& ret, std::vector<NativeType>& 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<uint64_t(__fastcall *)(void*)>(entry->original)(thisPtr);
case 1: return reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t)>(entry->original)(thisPtr, a[0]);
case 2: return reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t)>(entry->original)(thisPtr, a[0], a[1]);
case 3: return reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t)>(entry->original)(thisPtr, a[0], a[1], a[2]);
case 4: return reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t, uint64_t)>(entry->original)(thisPtr, a[0], a[1], a[2], a[3]);
case 5: return reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(entry->original)(thisPtr, a[0], a[1], a[2], a[3], a[4]);
default: return reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(entry->original)(thisPtr, a[0], a[1], a[2], a[3], a[4], a[5]);
}
}
else
{
switch (entry->argCount)
{
case 0: return reinterpret_cast<uint64_t(__fastcall *)()>(entry->original)();
case 1: return reinterpret_cast<uint64_t(__fastcall *)(uint64_t)>(entry->original)(a[0]);
case 2: return reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t)>(entry->original)(a[0], a[1]);
case 3: return reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t)>(entry->original)(a[0], a[1], a[2]);
case 4: return reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t, uint64_t)>(entry->original)(a[0], a[1], a[2], a[3]);
case 5: return reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(entry->original)(a[0], a[1], a[2], a[3], a[4]);
default: return reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(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<HookRegistry::ManagedHookFn>(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<HookRegistry::ManagedHookFn>(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<void*>(&HandleThis0);
case 1: return reinterpret_cast<void*>(&HandleThis1);
case 2: return reinterpret_cast<void*>(&HandleThis2);
case 3: return reinterpret_cast<void*>(&HandleThis3);
case 4: return reinterpret_cast<void*>(&HandleThis4);
case 5: return reinterpret_cast<void*>(&HandleThis5);
default: return reinterpret_cast<void*>(&HandleThis6);
}
}
else
{
switch (argCount)
{
case 0: return reinterpret_cast<void*>(&HandleStatic0);
case 1: return reinterpret_cast<void*>(&HandleStatic1);
case 2: return reinterpret_cast<void*>(&HandleStatic2);
case 3: return reinterpret_cast<void*>(&HandleStatic3);
case 4: return reinterpret_cast<void*>(&HandleStatic4);
case 5: return reinterpret_cast<void*>(&HandleStatic5);
default: return reinterpret_cast<void*>(&HandleStatic6);
}
}
}
void* CreateThunk(void* handler, HookEntry* entry)
{
unsigned char* code = reinterpret_cast<unsigned char*>(
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<uint64_t>(&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<uint64_t>(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<uint64_t>(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<uint64_t>(&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<std::mutex> 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<HookEntry>();
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<int>(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<std::mutex> lock(g_mutex);
auto it = g_hooks.find(fullName);
if (it == g_hooks.end())
return false;
HookEntry& entry = *it->second;
auto removeFn = [&](std::vector<void*>& list)
{
list.erase(std::remove(list.begin(), list.end(), managedCallback), list.end());
};
removeFn(entry.pre);
removeFn(entry.post);
return true;
}
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "NativeExports.h"
#include <string>
#include <vector>
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);
}

View File

@@ -11,6 +11,8 @@
#include "ManagedBlockRegistry.h"
#include "ModStrings.h"
#include "LogUtil.h"
#include "SymbolRegistry.h"
#include "HookRegistry.h"
#include <Windows.h>
#include <cstring>
#include <string>
@@ -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<size_t>(outLen - 1)) ? len : static_cast<size_t>(outLen - 1);
memcpy(outKey, key, copyLen);
outKey[copyLen] = '\0';
return static_cast<int>(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<uint64_t(__fastcall *)(void*)>(fn)(thisPtr); break;
case 1: ret = reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t)>(fn)(thisPtr, a[0]); break;
case 2: ret = reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t)>(fn)(thisPtr, a[0], a[1]); break;
case 3: ret = reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t)>(fn)(thisPtr, a[0], a[1], a[2]); break;
case 4: ret = reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t, uint64_t)>(fn)(thisPtr, a[0], a[1], a[2], a[3]); break;
case 5: ret = reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(fn)(thisPtr, a[0], a[1], a[2], a[3], a[4]); break;
default: ret = reinterpret_cast<uint64_t(__fastcall *)(void*, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(fn)(thisPtr, a[0], a[1], a[2], a[3], a[4], a[5]); break;
}
}
else
{
switch (argCount)
{
case 0: ret = reinterpret_cast<uint64_t(__fastcall *)()>(fn)(); break;
case 1: ret = reinterpret_cast<uint64_t(__fastcall *)(uint64_t)>(fn)(a[0]); break;
case 2: ret = reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t)>(fn)(a[0], a[1]); break;
case 3: ret = reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t)>(fn)(a[0], a[1], a[2]); break;
case 4: ret = reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t, uint64_t)>(fn)(a[0], a[1], a[2], a[3]); break;
case 5: ret = reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(fn)(a[0], a[1], a[2], a[3], a[4]); break;
default: ret = reinterpret_cast<uint64_t(__fastcall *)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t)>(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<uint32_t>(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"

View File

@@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <cstdint>
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);
}

View File

@@ -1037,6 +1037,120 @@ bool FindNameByRVA(uint32_t rva, char* outName, size_t nameSize, uint32_t* outOf
return true;
}
bool EnumerateSymbols(std::vector<SymbolInfo>& 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<PDB::HashRecord> 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<PDB::HashRecord> 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<PDB::HashRecord> 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<PDB::ModuleInfoStream::Module> 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;

View File

@@ -1,8 +1,18 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
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<SymbolInfo>& outSymbols);
void Close();
}

View File

@@ -0,0 +1,214 @@
#include "SymbolRegistry.h"
#include "LogUtil.h"
#include <Windows.h>
#include <fstream>
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_t>(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<uint32_t>(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<uintptr_t>(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<void*>(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;
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <unordered_map>
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<std::string, Entry> m_entries;
uintptr_t m_moduleBase = 0;
};

View File

@@ -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);

View File

@@ -0,0 +1,618 @@
#include "PdbParser.h"
#include <Windows.h>
#include <DbgHelp.h>
#include <OleAuto.h>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
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<unsigned char>(s[start])))
++start;
size_t end = s.size();
while (end > start && std::isspace(static_cast<unsigned char>(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<char>(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<std::string> SplitParams(const std::string& params)
{
std::vector<std::string> 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<TypeInfo> 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<std::string> 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<std::string>* outTypes = nullptr;
};
static BOOL CALLBACK EnumTypesCallback(PSYMBOL_INFO symInfo, ULONG, void* userContext)
{
if (!symInfo || !userContext)
return TRUE;
auto* ctx = reinterpret_cast<EnumTypesContext*>(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<std::string>& 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<std::pair<std::string, ULONG>>& outFields)
{
outFields.clear();
SYMBOL_INFO* sym = reinterpret_cast<SYMBOL_INFO*>(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<TI_FINDCHILDREN_PARAMS*>(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<size_t>(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<std::string> 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<std::pair<std::string, ULONG>> 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 <pdb_path> <out_json>\n";
return 1;
}
const char* pdbPath = argv[1];
const char* outPath = argv[2];
const char* offsetsExe = nullptr;
const char* offsetsOut = nullptr;
std::vector<std::string> 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<PdbParser::SymbolInfo> symbols;
if (!PdbParser::EnumerateSymbols(symbols))
{
std::cerr << "Failed to enumerate symbols\n";
return 1;
}
std::unordered_map<std::string, std::vector<MethodInfo>> byClass;
std::unordered_map<std::string, uint32_t> 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;
}