mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-06-20 11:35:39 +00:00
feat(modloader): pdb mapping, dynamic invoke, mixins
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="weave.mixins.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
127
ExampleMod/Mixins/CreeperExplosionMixin.cs
Normal file
127
ExampleMod/Mixins/CreeperExplosionMixin.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
ExampleMod/weave.mixins.json
Normal file
13
ExampleMod/weave.mixins.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"required": false,
|
||||
"minVersion": "0.1",
|
||||
"package": "ExampleMod.Mixins",
|
||||
"mixins": [],
|
||||
"client": [
|
||||
"CreeperExplosionMixin"
|
||||
],
|
||||
"server": [],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
7
WeaveLoader.API/Mixins/At.cs
Normal file
7
WeaveLoader.API/Mixins/At.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace WeaveLoader.API.Mixins;
|
||||
|
||||
public enum At
|
||||
{
|
||||
Head = 0,
|
||||
Tail = 1
|
||||
}
|
||||
18
WeaveLoader.API/Mixins/InjectAttribute.cs
Normal file
18
WeaveLoader.API/Mixins/InjectAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
14
WeaveLoader.API/Mixins/MixinAttribute.cs
Normal file
14
WeaveLoader.API/Mixins/MixinAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
68
WeaveLoader.API/Mixins/MixinContext.cs
Normal file
68
WeaveLoader.API/Mixins/MixinContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
8
WeaveLoader.API/Mixins/OverwriteAttribute.cs
Normal file
8
WeaveLoader.API/Mixins/OverwriteAttribute.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace WeaveLoader.API.Mixins;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
public sealed class OverwriteAttribute : Attribute
|
||||
{
|
||||
}
|
||||
54
WeaveLoader.API/Native/NativeClass.cs
Normal file
54
WeaveLoader.API/Native/NativeClass.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
23
WeaveLoader.API/Native/NativeInvoker.cs
Normal file
23
WeaveLoader.API/Native/NativeInvoker.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
10
WeaveLoader.API/Native/NativeLevel.cs
Normal file
10
WeaveLoader.API/Native/NativeLevel.cs
Normal 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;
|
||||
}
|
||||
25
WeaveLoader.API/Native/NativeMemory.cs
Normal file
25
WeaveLoader.API/Native/NativeMemory.cs
Normal 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);
|
||||
}
|
||||
109
WeaveLoader.API/Native/NativeObject.cs
Normal file
109
WeaveLoader.API/Native/NativeObject.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
189
WeaveLoader.API/Native/NativeOffsets.cs
Normal file
189
WeaveLoader.API/Native/NativeOffsets.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
19
WeaveLoader.API/Native/NativeSymbol.cs
Normal file
19
WeaveLoader.API/Native/NativeSymbol.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
37
WeaveLoader.API/Native/NativeTypes.cs
Normal file
37
WeaveLoader.API/Native/NativeTypes.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
170
WeaveLoader.Core/MixinLoader.cs
Normal file
170
WeaveLoader.Core/MixinLoader.cs
Normal 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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
369
WeaveLoaderRuntime/src/HookRegistry.cpp
Normal file
369
WeaveLoaderRuntime/src/HookRegistry.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
28
WeaveLoaderRuntime/src/HookRegistry.h
Normal file
28
WeaveLoaderRuntime/src/HookRegistry.h
Normal 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);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
214
WeaveLoaderRuntime/src/SymbolRegistry.cpp
Normal file
214
WeaveLoaderRuntime/src/SymbolRegistry.cpp
Normal 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;
|
||||
}
|
||||
28
WeaveLoaderRuntime/src/SymbolRegistry.h
Normal file
28
WeaveLoaderRuntime/src/SymbolRegistry.h
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
618
WeaveLoaderRuntime/tools/pdbdump.cpp
Normal file
618
WeaveLoaderRuntime/tools/pdbdump.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user