mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-07-05 02:24:19 +00:00
Initial commit: LegacyForge mod loader for Minecraft Legacy Edition
SKSE-style external mod loader with zero game source modifications. - LegacyForge.Launcher: C# console app that injects runtime DLL into game process - LegacyForgeRuntime: C++ DLL with PDB symbol resolution, MinHook function hooking, and .NET CoreCLR hosting - LegacyForge.Core: C# mod discovery and lifecycle management - LegacyForge.API: Fabric-style mod API with namespaced string IDs, fluent property builders, and event system - ExampleMod: Sample mod demonstrating block/item registration
This commit is contained in:
198
LegacyForgeRuntime/src/DotNetHost.cpp
Normal file
198
LegacyForgeRuntime/src/DotNetHost.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "DotNetHost.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <nethost.h>
|
||||
#include <hostfxr.h>
|
||||
#include <coreclr_delegates.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
// hostfxr function pointers
|
||||
static hostfxr_initialize_for_runtime_config_fn init_fptr = nullptr;
|
||||
static hostfxr_get_runtime_delegate_fn get_delegate_fptr = nullptr;
|
||||
static hostfxr_close_fn close_fptr = nullptr;
|
||||
|
||||
// Managed entry points (component_entry_point_fn signature)
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE *managed_entry_fn)(void* args, int sizeBytes);
|
||||
|
||||
static managed_entry_fn fn_Initialize = nullptr;
|
||||
static managed_entry_fn fn_DiscoverMods = nullptr;
|
||||
static managed_entry_fn fn_PreInit = nullptr;
|
||||
static managed_entry_fn fn_Init = nullptr;
|
||||
static managed_entry_fn fn_PostInit = nullptr;
|
||||
static managed_entry_fn fn_Tick = nullptr;
|
||||
static managed_entry_fn fn_Shutdown = nullptr;
|
||||
|
||||
static bool LoadHostfxr()
|
||||
{
|
||||
char_t buffer[MAX_PATH];
|
||||
size_t buffer_size = sizeof(buffer) / sizeof(char_t);
|
||||
|
||||
int rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
|
||||
if (rc != 0)
|
||||
{
|
||||
printf("[LegacyForge] get_hostfxr_path failed: 0x%x\n", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
HMODULE lib = LoadLibraryW(buffer);
|
||||
if (!lib)
|
||||
{
|
||||
printf("[LegacyForge] Failed to load hostfxr\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
init_fptr = reinterpret_cast<hostfxr_initialize_for_runtime_config_fn>(
|
||||
GetProcAddress(lib, "hostfxr_initialize_for_runtime_config"));
|
||||
get_delegate_fptr = reinterpret_cast<hostfxr_get_runtime_delegate_fn>(
|
||||
GetProcAddress(lib, "hostfxr_get_runtime_delegate"));
|
||||
close_fptr = reinterpret_cast<hostfxr_close_fn>(
|
||||
GetProcAddress(lib, "hostfxr_close"));
|
||||
|
||||
return init_fptr && get_delegate_fptr && close_fptr;
|
||||
}
|
||||
|
||||
static load_assembly_and_get_function_pointer_fn GetDotNetLoadAssembly(const wchar_t* configPath)
|
||||
{
|
||||
hostfxr_handle cxt = nullptr;
|
||||
int rc = init_fptr(configPath, nullptr, &cxt);
|
||||
if (rc != 0 || cxt == nullptr)
|
||||
{
|
||||
printf("[LegacyForge] hostfxr_initialize failed: 0x%x\n", rc);
|
||||
if (cxt) close_fptr(cxt);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void* load_fn = nullptr;
|
||||
rc = get_delegate_fptr(cxt, hdt_load_assembly_and_get_function_pointer, &load_fn);
|
||||
if (rc != 0 || load_fn == nullptr)
|
||||
{
|
||||
printf("[LegacyForge] hostfxr_get_runtime_delegate failed: 0x%x\n", rc);
|
||||
}
|
||||
|
||||
close_fptr(cxt);
|
||||
return reinterpret_cast<load_assembly_and_get_function_pointer_fn>(load_fn);
|
||||
}
|
||||
|
||||
static bool ResolveManagedMethod(
|
||||
load_assembly_and_get_function_pointer_fn load_fn,
|
||||
const wchar_t* assemblyPath,
|
||||
const wchar_t* methodName,
|
||||
managed_entry_fn* outFn)
|
||||
{
|
||||
int rc = load_fn(
|
||||
assemblyPath,
|
||||
L"LegacyForge.Core.LegacyForgeCore, LegacyForge.Core",
|
||||
methodName,
|
||||
nullptr, // delegate_type_name (null = default component_entry_point_fn)
|
||||
nullptr,
|
||||
reinterpret_cast<void**>(outFn));
|
||||
|
||||
return rc == 0 && *outFn != nullptr;
|
||||
}
|
||||
|
||||
bool DotNetHost::Initialize()
|
||||
{
|
||||
if (!LoadHostfxr())
|
||||
{
|
||||
printf("[LegacyForge] Failed to load hostfxr library\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Paths relative to the runtime DLL (which is in the same dir as LegacyForge.Core.dll)
|
||||
wchar_t modulePath[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, modulePath, MAX_PATH);
|
||||
|
||||
std::wstring exeDir(modulePath);
|
||||
size_t lastSlash = exeDir.find_last_of(L"\\/");
|
||||
if (lastSlash != std::wstring::npos)
|
||||
exeDir = exeDir.substr(0, lastSlash + 1);
|
||||
else
|
||||
exeDir = L".\\";
|
||||
|
||||
// Look for LegacyForge files next to the runtime DLL, not the game exe
|
||||
HMODULE hSelf = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&DotNetHost::Initialize), &hSelf);
|
||||
|
||||
wchar_t selfPath[MAX_PATH];
|
||||
GetModuleFileNameW(hSelf, selfPath, MAX_PATH);
|
||||
std::wstring selfDir(selfPath);
|
||||
lastSlash = selfDir.find_last_of(L"\\/");
|
||||
if (lastSlash != std::wstring::npos)
|
||||
selfDir = selfDir.substr(0, lastSlash + 1);
|
||||
else
|
||||
selfDir = L".\\";
|
||||
|
||||
std::wstring configPath = selfDir + L"LegacyForge.Core.runtimeconfig.json";
|
||||
std::wstring assemblyPath = selfDir + L"LegacyForge.Core.dll";
|
||||
|
||||
auto load_fn = GetDotNetLoadAssembly(configPath.c_str());
|
||||
if (!load_fn)
|
||||
{
|
||||
printf("[LegacyForge] Failed to get load_assembly_and_get_function_pointer\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"Initialize", &fn_Initialize);
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"DiscoverMods", &fn_DiscoverMods);
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"PreInit", &fn_PreInit);
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"Init", &fn_Init);
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"PostInit", &fn_PostInit);
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"Tick", &fn_Tick);
|
||||
ok &= ResolveManagedMethod(load_fn, assemblyPath.c_str(), L"Shutdown", &fn_Shutdown);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
printf("[LegacyForge] Failed to resolve one or more managed entry points\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("[LegacyForge] All managed entry points resolved\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void DotNetHost::CallManagedInit()
|
||||
{
|
||||
if (fn_Initialize)
|
||||
fn_Initialize(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::CallDiscoverMods(const char* modsPath)
|
||||
{
|
||||
if (fn_DiscoverMods)
|
||||
fn_DiscoverMods(const_cast<char*>(modsPath), static_cast<int>(strlen(modsPath)));
|
||||
}
|
||||
|
||||
void DotNetHost::CallPreInit()
|
||||
{
|
||||
if (fn_PreInit) fn_PreInit(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::CallInit()
|
||||
{
|
||||
if (fn_Init) fn_Init(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::CallPostInit()
|
||||
{
|
||||
if (fn_PostInit) fn_PostInit(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::CallTick()
|
||||
{
|
||||
if (fn_Tick) fn_Tick(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::CallShutdown()
|
||||
{
|
||||
if (fn_Shutdown) fn_Shutdown(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::Cleanup()
|
||||
{
|
||||
}
|
||||
18
LegacyForgeRuntime/src/DotNetHost.h
Normal file
18
LegacyForgeRuntime/src/DotNetHost.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
/// Hosts the .NET CoreCLR runtime inside the game process using the hostfxr API.
|
||||
/// Loads LegacyForge.Core.dll and resolves managed entry point methods.
|
||||
namespace DotNetHost
|
||||
{
|
||||
bool Initialize();
|
||||
void Cleanup();
|
||||
|
||||
void CallManagedInit();
|
||||
void CallDiscoverMods(const char* modsPath);
|
||||
void CallPreInit();
|
||||
void CallInit();
|
||||
void CallPostInit();
|
||||
void CallTick();
|
||||
void CallShutdown();
|
||||
}
|
||||
48
LegacyForgeRuntime/src/GameHooks.cpp
Normal file
48
LegacyForgeRuntime/src/GameHooks.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "GameHooks.h"
|
||||
#include "DotNetHost.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace GameHooks
|
||||
{
|
||||
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
|
||||
MinecraftTick_fn Original_MinecraftTick = nullptr;
|
||||
MinecraftInit_fn Original_MinecraftInit = nullptr;
|
||||
MinecraftDestroy_fn Original_MinecraftDestroy = nullptr;
|
||||
|
||||
void Hooked_RunStaticCtors()
|
||||
{
|
||||
printf("[LegacyForge] RunStaticCtors hook fired -- calling PreInit\n");
|
||||
DotNetHost::CallPreInit();
|
||||
|
||||
Original_RunStaticCtors();
|
||||
|
||||
printf("[LegacyForge] RunStaticCtors complete -- calling Init\n");
|
||||
DotNetHost::CallInit();
|
||||
}
|
||||
|
||||
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures)
|
||||
{
|
||||
Original_MinecraftTick(thisPtr, bFirst, bUpdateTextures);
|
||||
|
||||
if (bFirst)
|
||||
{
|
||||
DotNetHost::CallTick();
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall Hooked_MinecraftInit(void* thisPtr)
|
||||
{
|
||||
Original_MinecraftInit(thisPtr);
|
||||
|
||||
printf("[LegacyForge] Minecraft::init complete -- calling PostInit\n");
|
||||
DotNetHost::CallPostInit();
|
||||
}
|
||||
|
||||
void __fastcall Hooked_MinecraftDestroy(void* thisPtr)
|
||||
{
|
||||
printf("[LegacyForge] Minecraft::destroy -- calling Shutdown\n");
|
||||
DotNetHost::CallShutdown();
|
||||
|
||||
Original_MinecraftDestroy(thisPtr);
|
||||
}
|
||||
}
|
||||
22
LegacyForgeRuntime/src/GameHooks.h
Normal file
22
LegacyForgeRuntime/src/GameHooks.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
/// Function pointer typedefs matching the game's function signatures.
|
||||
/// On x64 MSVC, member functions use __fastcall-like convention (this in rcx).
|
||||
typedef void (*RunStaticCtors_fn)();
|
||||
typedef void (__fastcall *MinecraftTick_fn)(void* thisPtr, bool bFirst, bool bUpdateTextures);
|
||||
typedef void (__fastcall *MinecraftInit_fn)(void* thisPtr);
|
||||
typedef void (__fastcall *MinecraftDestroy_fn)(void* thisPtr);
|
||||
|
||||
namespace GameHooks
|
||||
{
|
||||
extern RunStaticCtors_fn Original_RunStaticCtors;
|
||||
extern MinecraftTick_fn Original_MinecraftTick;
|
||||
extern MinecraftInit_fn Original_MinecraftInit;
|
||||
extern MinecraftDestroy_fn Original_MinecraftDestroy;
|
||||
|
||||
void Hooked_RunStaticCtors();
|
||||
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures);
|
||||
void __fastcall Hooked_MinecraftInit(void* thisPtr);
|
||||
void __fastcall Hooked_MinecraftDestroy(void* thisPtr);
|
||||
}
|
||||
77
LegacyForgeRuntime/src/HookManager.cpp
Normal file
77
LegacyForgeRuntime/src/HookManager.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "HookManager.h"
|
||||
#include "GameHooks.h"
|
||||
#include "SymbolResolver.h"
|
||||
#include <MinHook.h>
|
||||
#include <cstdio>
|
||||
|
||||
bool HookManager::Install(const SymbolResolver& symbols)
|
||||
{
|
||||
if (MH_Initialize() != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] MH_Initialize failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hook MinecraftWorld_RunStaticCtors
|
||||
if (symbols.pRunStaticCtors)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pRunStaticCtors,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_RunStaticCtors),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_RunStaticCtors)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Failed to hook RunStaticCtors\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hook Minecraft::tick
|
||||
if (symbols.pMinecraftTick)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pMinecraftTick,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_MinecraftTick),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_MinecraftTick)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Failed to hook Minecraft::tick\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hook Minecraft::init
|
||||
if (symbols.pMinecraftInit)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pMinecraftInit,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_MinecraftInit),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_MinecraftInit)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Failed to hook Minecraft::init\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hook Minecraft::destroy
|
||||
if (symbols.pMinecraftDestroy)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pMinecraftDestroy,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_MinecraftDestroy),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_MinecraftDestroy)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Failed to hook Minecraft::destroy\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] MH_EnableHook(MH_ALL_HOOKS) failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("[LegacyForge] All hooks installed and enabled\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void HookManager::Cleanup()
|
||||
{
|
||||
MH_DisableHook(MH_ALL_HOOKS);
|
||||
MH_Uninitialize();
|
||||
}
|
||||
9
LegacyForgeRuntime/src/HookManager.h
Normal file
9
LegacyForgeRuntime/src/HookManager.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class SymbolResolver;
|
||||
|
||||
namespace HookManager
|
||||
{
|
||||
bool Install(const SymbolResolver& symbols);
|
||||
void Cleanup();
|
||||
}
|
||||
78
LegacyForgeRuntime/src/IdRegistry.cpp
Normal file
78
LegacyForgeRuntime/src/IdRegistry.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "IdRegistry.h"
|
||||
#include <cstdio>
|
||||
|
||||
IdRegistry& IdRegistry::Instance()
|
||||
{
|
||||
static IdRegistry instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
IdRegistry::IdRegistry()
|
||||
{
|
||||
m_registries[static_cast<int>(Type::Block)].nextFreeId = BLOCK_MOD_START;
|
||||
m_registries[static_cast<int>(Type::Item)].nextFreeId = ITEM_MOD_START;
|
||||
m_registries[static_cast<int>(Type::Entity)].nextFreeId = ENTITY_MOD_START;
|
||||
}
|
||||
|
||||
int IdRegistry::Register(Type type, const std::string& namespacedId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto& reg = m_registries[static_cast<int>(type)];
|
||||
|
||||
auto it = reg.stringToNum.find(namespacedId);
|
||||
if (it != reg.stringToNum.end())
|
||||
return it->second;
|
||||
|
||||
int maxId;
|
||||
switch (type)
|
||||
{
|
||||
case Type::Block: maxId = BLOCK_MAX; break;
|
||||
case Type::Item: maxId = ITEM_MAX; break;
|
||||
case Type::Entity: maxId = ENTITY_MAX; break;
|
||||
default: return -1;
|
||||
}
|
||||
|
||||
if (reg.nextFreeId > maxId)
|
||||
{
|
||||
printf("[LegacyForge] IdRegistry: No free IDs for type %d (max %d)\n",
|
||||
static_cast<int>(type), maxId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int id = reg.nextFreeId++;
|
||||
|
||||
// Skip IDs that are already taken by vanilla entries
|
||||
while (reg.numToString.count(id) && id <= maxId)
|
||||
id = reg.nextFreeId++;
|
||||
|
||||
if (id > maxId) return -1;
|
||||
|
||||
reg.stringToNum[namespacedId] = id;
|
||||
reg.numToString[id] = namespacedId;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
int IdRegistry::GetNumericId(Type type, const std::string& namespacedId) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto& reg = m_registries[static_cast<int>(type)];
|
||||
auto it = reg.stringToNum.find(namespacedId);
|
||||
return (it != reg.stringToNum.end()) ? it->second : -1;
|
||||
}
|
||||
|
||||
std::string IdRegistry::GetStringId(Type type, int numericId) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto& reg = m_registries[static_cast<int>(type)];
|
||||
auto it = reg.numToString.find(numericId);
|
||||
return (it != reg.numToString.end()) ? it->second : "";
|
||||
}
|
||||
|
||||
void IdRegistry::RegisterVanilla(Type type, int numericId, const std::string& namespacedId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto& reg = m_registries[static_cast<int>(type)];
|
||||
reg.stringToNum[namespacedId] = numericId;
|
||||
reg.numToString[numericId] = namespacedId;
|
||||
}
|
||||
48
LegacyForgeRuntime/src/IdRegistry.h
Normal file
48
LegacyForgeRuntime/src/IdRegistry.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
|
||||
/// Maps namespaced string IDs ("namespace:path") to auto-allocated numeric IDs.
|
||||
/// Separate pools for blocks, items, and entities.
|
||||
/// Thread-safe for registration calls from the .NET runtime.
|
||||
class IdRegistry
|
||||
{
|
||||
public:
|
||||
enum class Type { Block, Item, Entity };
|
||||
|
||||
static IdRegistry& Instance();
|
||||
|
||||
/// Register a new entry and auto-allocate a numeric ID.
|
||||
/// Returns the allocated numeric ID, or -1 on failure.
|
||||
int Register(Type type, const std::string& namespacedId);
|
||||
|
||||
/// Look up the numeric ID for a string ID. Returns -1 if not found.
|
||||
int GetNumericId(Type type, const std::string& namespacedId) const;
|
||||
|
||||
/// Look up the string ID for a numeric ID. Returns empty string if not found.
|
||||
std::string GetStringId(Type type, int numericId) const;
|
||||
|
||||
/// Pre-register a vanilla entry with a known numeric ID.
|
||||
void RegisterVanilla(Type type, int numericId, const std::string& namespacedId);
|
||||
|
||||
private:
|
||||
IdRegistry();
|
||||
|
||||
struct RegistryData
|
||||
{
|
||||
std::unordered_map<std::string, int> stringToNum;
|
||||
std::unordered_map<int, std::string> numToString;
|
||||
int nextFreeId;
|
||||
};
|
||||
|
||||
static constexpr int BLOCK_MOD_START = 256;
|
||||
static constexpr int BLOCK_MAX = 4095;
|
||||
static constexpr int ITEM_MOD_START = 1000;
|
||||
static constexpr int ITEM_MAX = 31999;
|
||||
static constexpr int ENTITY_MOD_START = 1000;
|
||||
static constexpr int ENTITY_MAX = 9999;
|
||||
|
||||
RegistryData m_registries[3];
|
||||
mutable std::mutex m_mutex;
|
||||
};
|
||||
128
LegacyForgeRuntime/src/NativeExports.cpp
Normal file
128
LegacyForgeRuntime/src/NativeExports.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "NativeExports.h"
|
||||
#include "IdRegistry.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
int native_register_block(
|
||||
const char* namespacedId,
|
||||
int materialId,
|
||||
float hardness,
|
||||
float resistance,
|
||||
int soundType,
|
||||
const char* iconName,
|
||||
float lightEmission,
|
||||
int lightBlock)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
|
||||
int id = IdRegistry::Instance().Register(IdRegistry::Type::Block, namespacedId);
|
||||
if (id < 0)
|
||||
{
|
||||
printf("[LegacyForge] Failed to allocate block ID for '%s'\n", namespacedId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO: Once we have access to game headers, create a GenericTile instance
|
||||
// and insert it into Tile::tiles[id]. For now we just allocate the ID.
|
||||
printf("[LegacyForge] Registered block '%s' -> ID %d (hardness=%.1f, resistance=%.1f)\n",
|
||||
namespacedId, id, hardness, resistance);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
int native_register_item(
|
||||
const char* namespacedId,
|
||||
int maxStackSize,
|
||||
int maxDamage)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
|
||||
int id = IdRegistry::Instance().Register(IdRegistry::Type::Item, namespacedId);
|
||||
if (id < 0)
|
||||
{
|
||||
printf("[LegacyForge] Failed to allocate item ID for '%s'\n", namespacedId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO: Create GenericItem and insert into Item::items[256 + id]
|
||||
printf("[LegacyForge] Registered item '%s' -> ID %d (stack=%d, durability=%d)\n",
|
||||
namespacedId, id, maxStackSize, maxDamage);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
int native_register_entity(
|
||||
const char* namespacedId,
|
||||
float width,
|
||||
float height,
|
||||
int trackingRange)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
|
||||
int id = IdRegistry::Instance().Register(IdRegistry::Type::Entity, namespacedId);
|
||||
if (id < 0)
|
||||
{
|
||||
printf("[LegacyForge] Failed to allocate entity ID for '%s'\n", namespacedId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO: Register with EntityIO via resolved PDB symbols
|
||||
printf("[LegacyForge] Registered entity '%s' -> ID %d (%.1fx%.1f)\n",
|
||||
namespacedId, id, width, height);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
void native_add_shaped_recipe(
|
||||
const char* resultId,
|
||||
int resultCount,
|
||||
const char* pattern,
|
||||
const char* ingredientIds)
|
||||
{
|
||||
// TODO: Parse pattern and ingredients, call game's recipe registration
|
||||
printf("[LegacyForge] Added shaped recipe: %dx %s\n", resultCount, resultId);
|
||||
}
|
||||
|
||||
void native_add_furnace_recipe(
|
||||
const char* inputId,
|
||||
const char* outputId,
|
||||
float xp)
|
||||
{
|
||||
// TODO: Call game's FurnaceRecipes::addRecipe via resolved symbols
|
||||
printf("[LegacyForge] Added furnace recipe: %s -> %s (%.1f xp)\n", inputId, outputId, xp);
|
||||
}
|
||||
|
||||
void native_log(const char* message, int level)
|
||||
{
|
||||
if (message)
|
||||
printf("%s\n", message);
|
||||
}
|
||||
|
||||
int native_get_block_id(const char* namespacedId)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
return IdRegistry::Instance().GetNumericId(IdRegistry::Type::Block, namespacedId);
|
||||
}
|
||||
|
||||
int native_get_item_id(const char* namespacedId)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
return IdRegistry::Instance().GetNumericId(IdRegistry::Type::Item, namespacedId);
|
||||
}
|
||||
|
||||
int native_get_entity_id(const char* namespacedId)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
return IdRegistry::Instance().GetNumericId(IdRegistry::Type::Entity, namespacedId);
|
||||
}
|
||||
|
||||
void native_subscribe_event(const char* eventName, void* managedFnPtr)
|
||||
{
|
||||
// TODO: Store managed callback pointers and invoke from game hooks
|
||||
printf("[LegacyForge] Event subscription: %s\n", eventName ? eventName : "(null)");
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
51
LegacyForgeRuntime/src/NativeExports.h
Normal file
51
LegacyForgeRuntime/src/NativeExports.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
/// Exported C functions callable from C# via P/Invoke.
|
||||
/// All registration functions accept namespaced string IDs and delegate
|
||||
/// to IdRegistry for numeric ID allocation.
|
||||
extern "C"
|
||||
{
|
||||
__declspec(dllexport) int native_register_block(
|
||||
const char* namespacedId,
|
||||
int materialId,
|
||||
float hardness,
|
||||
float resistance,
|
||||
int soundType,
|
||||
const char* iconName,
|
||||
float lightEmission,
|
||||
int lightBlock);
|
||||
|
||||
__declspec(dllexport) int native_register_item(
|
||||
const char* namespacedId,
|
||||
int maxStackSize,
|
||||
int maxDamage);
|
||||
|
||||
__declspec(dllexport) int native_register_entity(
|
||||
const char* namespacedId,
|
||||
float width,
|
||||
float height,
|
||||
int trackingRange);
|
||||
|
||||
__declspec(dllexport) void native_add_shaped_recipe(
|
||||
const char* resultId,
|
||||
int resultCount,
|
||||
const char* pattern,
|
||||
const char* ingredientIds);
|
||||
|
||||
__declspec(dllexport) void native_add_furnace_recipe(
|
||||
const char* inputId,
|
||||
const char* outputId,
|
||||
float xp);
|
||||
|
||||
__declspec(dllexport) void native_log(
|
||||
const char* message,
|
||||
int level);
|
||||
|
||||
__declspec(dllexport) int native_get_block_id(const char* namespacedId);
|
||||
__declspec(dllexport) int native_get_item_id(const char* namespacedId);
|
||||
__declspec(dllexport) int native_get_entity_id(const char* namespacedId);
|
||||
|
||||
__declspec(dllexport) void native_subscribe_event(
|
||||
const char* eventName,
|
||||
void* managedFnPtr);
|
||||
}
|
||||
121
LegacyForgeRuntime/src/SymbolResolver.cpp
Normal file
121
LegacyForgeRuntime/src/SymbolResolver.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "SymbolResolver.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#pragma comment(lib, "dbghelp.lib")
|
||||
|
||||
bool SymbolResolver::Initialize()
|
||||
{
|
||||
m_process = GetCurrentProcess();
|
||||
|
||||
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_DEBUG);
|
||||
|
||||
if (!SymInitialize(m_process, nullptr, TRUE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void* SymbolResolver::Resolve(const char* functionName)
|
||||
{
|
||||
if (!m_initialized) return nullptr;
|
||||
|
||||
char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(char)];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
SYMBOL_INFO* symbol = reinterpret_cast<SYMBOL_INFO*>(buffer);
|
||||
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
symbol->MaxNameLen = MAX_SYM_NAME;
|
||||
|
||||
if (SymFromName(m_process, functionName, symbol))
|
||||
{
|
||||
return reinterpret_cast<void*>(symbol->Address);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct EnumContext
|
||||
{
|
||||
const char* targetName;
|
||||
void* result;
|
||||
};
|
||||
|
||||
static BOOL CALLBACK EnumSymbolsCallback(PSYMBOL_INFO pSymInfo, ULONG SymbolSize, PVOID UserContext)
|
||||
{
|
||||
auto* ctx = static_cast<EnumContext*>(UserContext);
|
||||
if (strstr(pSymInfo->Name, ctx->targetName) != nullptr)
|
||||
{
|
||||
ctx->result = reinterpret_cast<void*>(pSymInfo->Address);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool SymbolResolver::ResolveGameFunctions()
|
||||
{
|
||||
// MinecraftWorld_RunStaticCtors is a free function (not mangled in complex ways)
|
||||
pRunStaticCtors = Resolve("MinecraftWorld_RunStaticCtors");
|
||||
if (!pRunStaticCtors)
|
||||
{
|
||||
// Try with wildcard enumeration
|
||||
EnumContext ctx = { "MinecraftWorld_RunStaticCtors", nullptr };
|
||||
SymEnumSymbols(m_process, 0, "*MinecraftWorld_RunStaticCtors*", EnumSymbolsCallback, &ctx);
|
||||
pRunStaticCtors = ctx.result;
|
||||
}
|
||||
|
||||
// Minecraft::tick -- MSVC mangles this. Try undecorated name first.
|
||||
pMinecraftTick = Resolve("Minecraft::tick");
|
||||
if (!pMinecraftTick)
|
||||
{
|
||||
EnumContext ctx = { "Minecraft::tick", nullptr };
|
||||
SymEnumSymbols(m_process, 0, "*Minecraft*tick*", EnumSymbolsCallback, &ctx);
|
||||
pMinecraftTick = ctx.result;
|
||||
}
|
||||
|
||||
// Minecraft::init
|
||||
pMinecraftInit = Resolve("Minecraft::init");
|
||||
if (!pMinecraftInit)
|
||||
{
|
||||
EnumContext ctx = { "Minecraft::init", nullptr };
|
||||
SymEnumSymbols(m_process, 0, "*Minecraft*init*", EnumSymbolsCallback, &ctx);
|
||||
pMinecraftInit = ctx.result;
|
||||
}
|
||||
|
||||
// Minecraft::destroy
|
||||
pMinecraftDestroy = Resolve("Minecraft::destroy");
|
||||
if (!pMinecraftDestroy)
|
||||
{
|
||||
EnumContext ctx = { "Minecraft::destroy", nullptr };
|
||||
SymEnumSymbols(m_process, 0, "*Minecraft*destroy*", EnumSymbolsCallback, &ctx);
|
||||
pMinecraftDestroy = ctx.result;
|
||||
}
|
||||
|
||||
bool allResolved = pRunStaticCtors && pMinecraftTick && pMinecraftInit && pMinecraftDestroy;
|
||||
|
||||
if (pRunStaticCtors) printf("[LegacyForge] Resolved RunStaticCtors @ %p\n", pRunStaticCtors);
|
||||
else printf("[LegacyForge] MISSING: MinecraftWorld_RunStaticCtors\n");
|
||||
|
||||
if (pMinecraftTick) printf("[LegacyForge] Resolved Minecraft::tick @ %p\n", pMinecraftTick);
|
||||
else printf("[LegacyForge] MISSING: Minecraft::tick\n");
|
||||
|
||||
if (pMinecraftInit) printf("[LegacyForge] Resolved Minecraft::init @ %p\n", pMinecraftInit);
|
||||
else printf("[LegacyForge] MISSING: Minecraft::init\n");
|
||||
|
||||
if (pMinecraftDestroy) printf("[LegacyForge] Resolved Minecraft::destroy @ %p\n", pMinecraftDestroy);
|
||||
else printf("[LegacyForge] MISSING: Minecraft::destroy\n");
|
||||
|
||||
return allResolved;
|
||||
}
|
||||
|
||||
void SymbolResolver::Cleanup()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
SymCleanup(m_process);
|
||||
m_initialized = false;
|
||||
}
|
||||
}
|
||||
23
LegacyForgeRuntime/src/SymbolResolver.h
Normal file
23
LegacyForgeRuntime/src/SymbolResolver.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <DbgHelp.h>
|
||||
|
||||
class SymbolResolver
|
||||
{
|
||||
public:
|
||||
bool Initialize();
|
||||
bool ResolveGameFunctions();
|
||||
void Cleanup();
|
||||
|
||||
void* Resolve(const char* functionName);
|
||||
|
||||
void* pRunStaticCtors = nullptr;
|
||||
void* pMinecraftTick = nullptr;
|
||||
void* pMinecraftInit = nullptr;
|
||||
void* pMinecraftDestroy = nullptr;
|
||||
|
||||
private:
|
||||
HANDLE m_process = nullptr;
|
||||
bool m_initialized = false;
|
||||
};
|
||||
75
LegacyForgeRuntime/src/dllmain.cpp
Normal file
75
LegacyForgeRuntime/src/dllmain.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include "SymbolResolver.h"
|
||||
#include "HookManager.h"
|
||||
#include "DotNetHost.h"
|
||||
|
||||
static HMODULE g_hModule = nullptr;
|
||||
|
||||
static void LogToFile(const char* msg)
|
||||
{
|
||||
FILE* f = nullptr;
|
||||
fopen_s(&f, "legacyforge.log", "a");
|
||||
if (f)
|
||||
{
|
||||
fprintf(f, "%s\n", msg);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
DWORD WINAPI InitThread(LPVOID lpParam)
|
||||
{
|
||||
LogToFile("[LegacyForge] InitThread started");
|
||||
|
||||
SymbolResolver symbols;
|
||||
if (!symbols.Initialize())
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to initialize symbol resolver. Is the PDB present?");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] Symbol resolver initialized");
|
||||
|
||||
if (!symbols.ResolveGameFunctions())
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to resolve one or more game functions");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] Game functions resolved from PDB");
|
||||
|
||||
if (!HookManager::Install(symbols))
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to install hooks");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] Hooks installed");
|
||||
|
||||
if (!DotNetHost::Initialize())
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to initialize .NET host");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] .NET runtime initialized");
|
||||
|
||||
DotNetHost::CallManagedInit();
|
||||
DotNetHost::CallDiscoverMods("mods");
|
||||
LogToFile("[LegacyForge] Mod discovery complete. Ready.");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
g_hModule = hModule;
|
||||
DisableThreadLibraryCalls(hModule);
|
||||
CreateThread(nullptr, 0, InitThread, nullptr, 0, nullptr);
|
||||
break;
|
||||
|
||||
case DLL_PROCESS_DETACH:
|
||||
HookManager::Cleanup();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
Reference in New Issue
Block a user