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:
Jacobwasbeast
2026-03-06 15:11:53 -06:00
commit de22a24100
45 changed files with 2489 additions and 0 deletions

View 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()
{
}

View 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();
}

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

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

View 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();
}

View File

@@ -0,0 +1,9 @@
#pragma once
class SymbolResolver;
namespace HookManager
{
bool Install(const SymbolResolver& symbols);
void Cleanup();
}

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

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

View 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"

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

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

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

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