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

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