mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-06-10 15:01:55 +00:00
feat(modloader): pdb mapping, dynamic invoke, mixins
This commit is contained in:
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);
|
||||
}
|
||||
Reference in New Issue
Block a user