Add main menu overlay, creative inventory injection, mod discovery, logging, and Wine/Proton support

- Replace dbghelp with raw_pdb library for cross-platform PDB symbol resolution
- Add main menu branding overlay via C4JRender::Present hook
- Add creative inventory item injection from mods
- Add file-based logging (LogUtil) alongside console output
- Fix mod discovery with custom AssemblyLoadContext for proper type identity
- Add file dialog for game path selection in launcher
- Add CreativeTab enum and block/item creative tab assignment
- Unify build output to single ModLoader/build directory
This commit is contained in:
Jacobwasbeast
2026-03-06 18:58:24 -06:00
parent 65ef99fc11
commit 34177e1507
37 changed files with 1296 additions and 165 deletions

View File

@@ -20,10 +20,13 @@ public class ExampleMod : IMod
.Hardness(3.0f)
.Resistance(15f)
.Sound(SoundType.Stone)
.Icon("ruby_ore"));
.Icon("ruby_ore")
.InCreativeTab(CreativeTab.BuildingBlocks));
Ruby = Registry.Item.Register("examplemod:ruby",
new ItemProperties().MaxStackSize(64));
new ItemProperties()
.MaxStackSize(64)
.InCreativeTab(CreativeTab.Materials));
Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f);

View File

@@ -8,6 +8,9 @@
<AssemblyName>ExampleMod</AssemblyName>
<Description>Example mod for LegacyForge demonstrating the mod API</Description>
<Version>1.0.0</Version>
<OutputPath>..\build\mods</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("LegacyForge.Core")]

View File

@@ -46,6 +46,7 @@ public class BlockProperties
internal string IconValue = "stone";
internal float LightEmissionValue = 0.0f;
internal int LightBlockValue = 255;
internal CreativeTab CreativeTabValue = CreativeTab.None;
public BlockProperties Material(MaterialType material) { MaterialValue = material; return this; }
public BlockProperties Hardness(float hardness) { HardnessValue = hardness; return this; }
@@ -55,4 +56,5 @@ public class BlockProperties
public BlockProperties LightLevel(float level) { LightEmissionValue = level; return this; }
public BlockProperties LightBlocking(int level) { LightBlockValue = level; return this; }
public BlockProperties Indestructible() { HardnessValue = -1.0f; ResistanceValue = 6000000f; return this; }
public BlockProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
}

View File

@@ -45,6 +45,12 @@ public static class BlockRegistry
if (numericId < 0)
throw new InvalidOperationException($"Failed to register block '{id}'. No free IDs or invalid parameters.");
if (properties.CreativeTabValue != CreativeTab.None)
{
NativeInterop.native_add_to_creative(numericId, 1, 0, (int)properties.CreativeTabValue);
Logger.Debug($"Block '{id}' added to creative tab {properties.CreativeTabValue}");
}
Logger.Debug($"Registered block '{id}' -> numeric ID {numericId}");
return new RegisteredBlock(id, numericId);
}

View File

@@ -0,0 +1,21 @@
namespace LegacyForge.API;
/// <summary>
/// Creative inventory tabs matching the game's internal group indices.
/// Use with <see cref="Block.BlockProperties.CreativeTab"/> or
/// <see cref="Item.ItemProperties.CreativeTab"/> to make content
/// appear in the creative menu.
/// </summary>
public enum CreativeTab
{
None = -1,
BuildingBlocks = 0,
Decoration = 1,
Redstone = 2,
Transport = 3,
Materials = 4,
Food = 5,
ToolsAndWeapons = 6,
Brewing = 7,
Misc = 12
}

View File

@@ -7,6 +7,7 @@ public class ItemProperties
{
internal int MaxStackSizeValue = 64;
internal int MaxDamageValue = 0;
internal CreativeTab CreativeTabValue = CreativeTab.None;
public ItemProperties MaxStackSize(int size) { MaxStackSizeValue = size; return this; }
@@ -15,4 +16,5 @@ public class ItemProperties
/// makes the item damageable with a durability bar.
/// </summary>
public ItemProperties MaxDamage(int damage) { MaxDamageValue = damage; MaxStackSizeValue = 1; return this; }
public ItemProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
}

View File

@@ -40,6 +40,12 @@ public static class ItemRegistry
if (numericId < 0)
throw new InvalidOperationException($"Failed to register item '{id}'. No free IDs or invalid parameters.");
if (properties.CreativeTabValue != CreativeTab.None)
{
NativeInterop.native_add_to_creative(numericId, 1, 0, (int)properties.CreativeTabValue);
Logger.Debug($"Item '{id}' added to creative tab {properties.CreativeTabValue}");
}
Logger.Debug($"Registered item '{id}' -> numeric ID {numericId}");
return new RegisteredItem(id, numericId);
}

View File

@@ -8,6 +8,9 @@
<AssemblyName>LegacyForge.API</AssemblyName>
<Description>Mod API for LegacyForge - Minecraft Legacy Edition mod loader</Description>
<Version>1.0.0</Version>
<OutputPath>..\build</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
</Project>

View File

@@ -13,7 +13,13 @@ public enum LogLevel
/// </summary>
public static class Logger
{
internal static Action<string, LogLevel>? LogHandler;
private static Action<string, LogLevel>? LogHandler;
/// <summary>
/// Set the log handler that routes messages to the native runtime.
/// Called by LegacyForge.Core during initialization.
/// </summary>
public static void SetLogHandler(Action<string, LogLevel> handler) => LogHandler = handler;
public static void Debug(string message) => Log(message, LogLevel.Debug);
public static void Info(string message) => Log(message, LogLevel.Info);

View File

@@ -61,4 +61,7 @@ internal static class NativeInterop
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_subscribe_event(string eventName, IntPtr managedFnPtr);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern void native_add_to_creative(int numericId, int count, int auxValue, int groupIndex);
}

View File

@@ -8,6 +8,9 @@
<AssemblyName>LegacyForge.Core</AssemblyName>
<Description>LegacyForge core runtime - mod discovery and lifecycle management</Description>
<Version>1.0.0</Version>
<OutputPath>..\build</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>

View File

@@ -3,27 +3,17 @@ using LegacyForge.API;
namespace LegacyForge.Core;
/// <summary>
/// Entry point class loaded by the C++ DotNetHost via hostfxr.
/// All public static methods here are resolved as function pointers from native code.
/// Method signatures must match the component_entry_point_fn delegate:
/// public delegate int ComponentEntryPoint(IntPtr args, int sizeBytes);
/// </summary>
public static class LegacyForgeCore
{
private static ModManager? _modManager;
private static bool _initialized;
/// <summary>
/// Called once by C++ to initialize the managed runtime.
/// Sets up the native log handler and prepares the mod manager.
/// </summary>
public static int Initialize(IntPtr args, int sizeBytes)
{
if (_initialized) return 0;
_initialized = true;
Logger.LogHandler = (message, level) =>
Logger.SetLogHandler((message, level) =>
{
string formatted = $"[LegacyForge/{level}] {message}";
try
@@ -34,61 +24,68 @@ public static class LegacyForgeCore
{
Console.WriteLine(formatted);
}
};
});
Logger.Info("LegacyForge Core initialized");
_modManager = new ModManager();
return 0;
}
/// <summary>
/// Called by C++ to discover and load mod assemblies from the mods/ directory.
/// The mods path is passed as a UTF-8 string pointer.
/// </summary>
public static int DiscoverMods(IntPtr args, int sizeBytes)
{
string modsPath;
if (args != IntPtr.Zero && sizeBytes > 0)
modsPath = Marshal.PtrToStringUTF8(args, sizeBytes) ?? "mods";
else
modsPath = "mods";
try
{
string modsPath;
if (args != IntPtr.Zero && sizeBytes > 0)
modsPath = Marshal.PtrToStringUTF8(args, sizeBytes) ?? "mods";
else
modsPath = "mods";
Logger.Info($"Discovering mods in: {modsPath}");
var discovered = ModDiscovery.DiscoverMods(modsPath);
_modManager?.AddMods(discovered);
Logger.Info($"Loaded {discovered.Count} mod(s)");
return discovered.Count;
Logger.Info($"Discovering mods in: {modsPath}");
Logger.Info($"Directory exists: {Directory.Exists(modsPath)}");
if (Directory.Exists(modsPath))
{
var files = Directory.GetFiles(modsPath, "*.dll");
Logger.Info($"DLL files found: {string.Join(", ", files.Select(Path.GetFileName))}");
}
var discovered = ModDiscovery.DiscoverMods(modsPath);
_modManager?.AddMods(discovered);
Logger.Info($"Loaded {discovered.Count} mod(s)");
return discovered.Count;
}
catch (Exception ex)
{
Logger.Error($"DiscoverMods EXCEPTION: {ex}");
return 0;
}
}
/// <summary>Called before MinecraftWorld_RunStaticCtors.</summary>
public static int PreInit(IntPtr args, int sizeBytes)
{
_modManager?.PreInit();
return 0;
}
/// <summary>Called after MinecraftWorld_RunStaticCtors.</summary>
public static int Init(IntPtr args, int sizeBytes)
{
_modManager?.Init();
return 0;
}
/// <summary>Called after Minecraft::init completes.</summary>
public static int PostInit(IntPtr args, int sizeBytes)
{
_modManager?.PostInit();
return 0;
}
/// <summary>Called each game tick from the Minecraft::tick hook.</summary>
public static int Tick(IntPtr args, int sizeBytes)
{
_modManager?.Tick();
return 0;
}
/// <summary>Called from the Minecraft::destroy hook during shutdown.</summary>
public static int Shutdown(IntPtr args, int sizeBytes)
{
_modManager?.Shutdown();

View File

@@ -4,10 +4,6 @@ using LegacyForge.API;
namespace LegacyForge.Core;
/// <summary>
/// Discovers and loads mod assemblies from the mods/ directory.
/// Each mod is loaded into its own AssemblyLoadContext for isolation.
/// </summary>
internal static class ModDiscovery
{
internal record DiscoveredMod(
@@ -30,6 +26,12 @@ internal static class ModDiscovery
foreach (var dllPath in dllFiles)
{
string fileName = Path.GetFileName(dllPath);
// Skip the API assembly -- it's a shared dependency, not a mod
if (fileName.Equals("LegacyForge.API.dll", StringComparison.OrdinalIgnoreCase))
continue;
try
{
var discovered = LoadModAssembly(dllPath);
@@ -37,7 +39,7 @@ internal static class ModDiscovery
}
catch (Exception ex)
{
Logger.Error($"Failed to load mod from {Path.GetFileName(dllPath)}: {ex.Message}");
Logger.Error($"Failed to load mod from {fileName}: {ex.Message}");
}
}
@@ -48,11 +50,18 @@ internal static class ModDiscovery
{
var results = new List<DiscoveredMod>();
var fileName = Path.GetFileName(dllPath);
var fullPath = Path.GetFullPath(dllPath);
var loadContext = new AssemblyLoadContext(fileName, isCollectible: false);
var assembly = loadContext.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
// Load into the SAME ALC that LegacyForge.Core lives in (the hostfxr component context).
// This ensures LegacyForge.API types (IMod, ModAttribute, etc.) have the same identity.
var coreContext = AssemblyLoadContext.GetLoadContext(typeof(ModDiscovery).Assembly)
?? AssemblyLoadContext.Default;
var assembly = coreContext.LoadFromAssemblyPath(fullPath);
var modTypes = assembly.GetTypes()
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));
foreach (var type in modTypes)
@@ -67,7 +76,7 @@ internal static class ModDiscovery
try
{
var instance = (IMod)Activator.CreateInstance(type)!;
results.Add(new ModDiscovery.DiscoveredMod(instance, attr, assembly));
results.Add(new DiscoveredMod(instance, attr, assembly));
string name = string.IsNullOrEmpty(attr.Name) ? attr.Id : attr.Name;
Logger.Info($"Discovered mod: {name} v{attr.Version} by {attr.Author} ({fileName})");

View File

@@ -0,0 +1,61 @@
using System.Runtime.InteropServices;
namespace LegacyForge.Launcher;
/// <summary>
/// Thin wrapper around the Win32 GetOpenFileName API.
/// No WinForms/WPF dependency required.
/// </summary>
internal static class FileDialog
{
public static string? OpenFileDialog(string title, string filter)
{
var ofn = new OpenFileName();
ofn.lStructSize = Marshal.SizeOf(ofn);
ofn.lpstrFilter = filter;
ofn.lpstrFile = new string('\0', 260);
ofn.nMaxFile = 260;
ofn.lpstrTitle = title;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
if (GetOpenFileName(ofn))
return ofn.lpstrFile.TrimEnd('\0');
return null;
}
private const int OFN_PATHMUSTEXIST = 0x00000800;
private const int OFN_FILEMUSTEXIST = 0x00001000;
private const int OFN_NOCHANGEDIR = 0x00000008;
[DllImport("comdlg32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool GetOpenFileName([In, Out] OpenFileName ofn);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private class OpenFileName
{
public int lStructSize;
public IntPtr hwndOwner;
public IntPtr hInstance;
public string? lpstrFilter;
public string? lpstrCustomFilter;
public int nMaxCustFilter;
public int nFilterIndex;
public string? lpstrFile;
public int nMaxFile;
public string? lpstrFileTitle;
public int nMaxFileTitle;
public string? lpstrInitialDir;
public string? lpstrTitle;
public int Flags;
public short nFileOffset;
public short nFileExtension;
public string? lpstrDefExt;
public IntPtr lCustData;
public IntPtr lpfnHook;
public string? lpTemplateName;
public IntPtr pvReserved;
public int dwReserved;
public int FlagsEx;
}
}

View File

@@ -96,9 +96,30 @@ public static class Injector
"Failed to create remote thread for DLL injection");
}
WaitForSingleObject(remoteThread, 10000);
uint waitResult = WaitForSingleObject(remoteThread, 10000);
if (waitResult != 0)
{
CloseHandle(remoteThread);
VirtualFreeEx(process.ProcessHandle, remoteMem, 0, MEM_RELEASE);
TerminateProcess(process.ProcessHandle, 1);
throw new Exception(
$"Timed out waiting for DLL injection (WaitForSingleObject returned {waitResult})");
}
GetExitCodeThread(remoteThread, out uint exitCode);
CloseHandle(remoteThread);
VirtualFreeEx(process.ProcessHandle, remoteMem, 0, MEM_RELEASE);
if (exitCode == 0)
{
TerminateProcess(process.ProcessHandle, 1);
throw new Exception(
"DLL injection failed: LoadLibraryW returned NULL.\n" +
"This usually means the DLL or one of its dependencies could not be found.\n" +
"Make sure the MSVC redistributable is installed and the DLL was built in Release mode.");
}
Console.WriteLine($" LoadLibraryW returned module handle 0x{exitCode:X} -- DLL loaded in target process.");
}
public static void ResumeProcess(InjectedProcess process)
@@ -179,5 +200,8 @@ public static class Injector
[DllImport("kernel32.dll")]
private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
[DllImport("kernel32.dll")]
private static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
#endregion
}

View File

@@ -9,6 +9,26 @@
<AssemblyName>LegacyForge</AssemblyName>
<Description>LegacyForge launcher - injects the mod loader runtime into Minecraft Legacy Edition</Description>
<Version>1.0.0</Version>
<OutputPath>..\build</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<!-- Build the C++ runtime DLL via CMake before the launcher -->
<Target Name="BuildNativeRuntime" BeforeTargets="Build">
<Message Importance="high" Text="[LegacyForge] Configuring C++ runtime with CMake..." />
<Exec Command="cmake -B &quot;$(MSBuildThisFileDirectory)..\LegacyForgeRuntime\build&quot; -S &quot;$(MSBuildThisFileDirectory)..\LegacyForgeRuntime&quot; -A x64"
WorkingDirectory="$(MSBuildThisFileDirectory)..\LegacyForgeRuntime" />
<Message Importance="high" Text="[LegacyForge] Building C++ runtime..." />
<Exec Command="cmake --build &quot;$(MSBuildThisFileDirectory)..\LegacyForgeRuntime\build&quot; --config $(Configuration)"
WorkingDirectory="$(MSBuildThisFileDirectory)..\LegacyForgeRuntime" />
</Target>
<!-- Copy the runtimeconfig.json to the build output -->
<Target Name="CopyRuntimeConfig" AfterTargets="Build">
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\LegacyForge.Core\LegacyForge.Core.runtimeconfig.json"
DestinationFolder="$(OutputPath)"
SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -1,10 +1,12 @@
using System.Runtime.InteropServices;
namespace LegacyForge.Launcher;
class Program
{
private const string ConfigFile = "legacyforge.json";
private const string RuntimeDll = "LegacyForgeRuntime.dll";
private const string RuntimeDllName = "LegacyForgeRuntime.dll";
[STAThread]
static int Main(string[] args)
{
Console.WriteLine("╔══════════════════════════════════╗");
@@ -13,44 +15,58 @@ class Program
Console.WriteLine("╚══════════════════════════════════╝");
Console.WriteLine();
// All paths relative to where the exe lives, not the working directory
string baseDir = AppContext.BaseDirectory;
string configFile = Path.Combine(baseDir, "legacyforge.json");
string runtimeDll = Path.Combine(baseDir, RuntimeDllName);
string modsDir = Path.Combine(baseDir, "mods");
try
{
var config = Config.Load(ConfigFile);
var config = Config.Load(configFile);
if (args.Length > 0 && File.Exists(args[0]))
{
config.GameExePath = args[0];
config.Save(ConfigFile);
config.Save(configFile);
}
if (string.IsNullOrEmpty(config.GameExePath) || !File.Exists(config.GameExePath))
{
Console.Write("Enter path to Minecraft.Client.exe: ");
string? input = Console.ReadLine()?.Trim().Trim('"');
Console.WriteLine("Please select Minecraft.Client.exe...");
string? selected = FileDialog.OpenFileDialog(
"Select Minecraft.Client.exe",
"Executable Files (*.exe)\0*.exe\0All Files (*.*)\0*.*\0");
if (string.IsNullOrEmpty(input) || !File.Exists(input))
if (string.IsNullOrEmpty(selected) || !File.Exists(selected))
{
Console.Error.WriteLine("Error: Invalid path or file not found.");
Console.Error.WriteLine("Error: No file selected or file not found.");
return 1;
}
config.GameExePath = Path.GetFullPath(input);
config.Save(ConfigFile);
Console.WriteLine($"Saved game path to {ConfigFile}");
config.GameExePath = Path.GetFullPath(selected);
config.Save(configFile);
Console.WriteLine($"Saved game path to {configFile}");
}
if (!File.Exists(RuntimeDll))
if (!File.Exists(runtimeDll))
{
Console.Error.WriteLine($"Error: {RuntimeDll} not found in current directory.");
Console.Error.WriteLine("Make sure the runtime DLL is next to LegacyForge.exe.");
Console.Error.WriteLine($"Error: {RuntimeDllName} not found.");
Console.Error.WriteLine($"Expected at: {runtimeDll}");
Console.Error.WriteLine();
Console.Error.WriteLine("The C++ runtime DLL must be built separately with CMake:");
Console.Error.WriteLine(" cd LegacyForgeRuntime");
Console.Error.WriteLine(" cmake -B build -A x64");
Console.Error.WriteLine(" cmake --build build --config Release");
Console.Error.WriteLine();
Console.Error.WriteLine("Then copy LegacyForgeRuntime.dll to the same folder as LegacyForge.exe.");
return 1;
}
string modsDir = Path.Combine(AppContext.BaseDirectory, "mods");
if (!Directory.Exists(modsDir))
{
Directory.CreateDirectory(modsDir);
Console.WriteLine($"Created mods/ directory at {modsDir}");
Console.WriteLine($"Created mods/ directory");
}
int modCount = Directory.GetFiles(modsDir, "*.dll").Length;
@@ -58,13 +74,14 @@ class Program
Console.WriteLine($"Launching {Path.GetFileName(config.GameExePath)}...");
var process = Injector.LaunchSuspended(config.GameExePath);
Console.WriteLine($"Game process created (PID: {process.ProcessId}), injecting runtime...");
Console.WriteLine($"[OK] Game process created (PID: {process.ProcessId})");
Console.WriteLine($"[..] Injecting {RuntimeDllName}...");
Injector.InjectDll(process, RuntimeDll);
Console.WriteLine("LegacyForgeRuntime.dll injected successfully.");
Injector.InjectDll(process, runtimeDll);
Console.WriteLine($"[OK] {RuntimeDllName} injected and loaded in target process.");
Injector.ResumeProcess(process);
Console.WriteLine("Game resumed. LegacyForge is active.");
Console.WriteLine("[OK] Game resumed. LegacyForge is active.");
Console.WriteLine();
Console.WriteLine("Press any key to exit the launcher (game will keep running).");
Console.ReadKey(true);

View File

@@ -1,18 +1,37 @@
cmake_minimum_required(VERSION 3.24)
cmake_policy(SET CMP0091 NEW)
project(LegacyForgeRuntime LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Use static release CRT (/MT) for all configs to:
# 1. Avoid requiring MSVCP140.dll / VCRUNTIME140.dll / ucrtbase.dll at runtime
# 2. Match libnethost.lib which is also built with /MT
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
# ── MinHook (fetched from GitHub) ──────────────────────────────────────
include(FetchContent)
FetchContent_Declare(
minhook
GIT_REPOSITORY https://github.com/TsudaKageworthy/minhook.git
GIT_TAG master
GIT_REPOSITORY https://github.com/TsudaKageyu/minhook.git
GIT_TAG v1.3.4
)
FetchContent_MakeAvailable(minhook)
# Ensure MinHook also uses static release CRT
set_target_properties(minhook PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded")
# ── raw_pdb (fallback PDB parser for Wine/Proton) ────────────────────
set(RAWPDB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
raw_pdb
GIT_REPOSITORY https://github.com/MolecularMatters/raw_pdb.git
GIT_TAG main
)
FetchContent_MakeAvailable(raw_pdb)
set_target_properties(raw_pdb PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded")
# ── Locate .NET hosting headers and libs ───────────────────────────────
# Users can set DOTNET_ROOT or NETHOST_DIR; otherwise we try the default SDK path.
if(NOT DEFINED NETHOST_INCLUDE_DIR)
@@ -30,23 +49,36 @@ endif()
if(NETHOST_DIR)
message(STATUS "Using nethost from: ${NETHOST_DIR}")
set(NETHOST_INCLUDE_DIR "${NETHOST_DIR}")
set(NETHOST_LIB "${NETHOST_DIR}/nethost.lib")
# Prefer static lib (libnethost.lib) to avoid requiring nethost.dll at runtime
if(EXISTS "${NETHOST_DIR}/libnethost.lib")
set(NETHOST_LIB "${NETHOST_DIR}/libnethost.lib")
set(NETHOST_STATIC TRUE)
message(STATUS "Using static nethost (libnethost.lib)")
elseif(EXISTS "${NETHOST_DIR}/nethost.lib")
set(NETHOST_LIB "${NETHOST_DIR}/nethost.lib")
set(NETHOST_STATIC FALSE)
message(STATUS "Using dynamic nethost (nethost.lib) -- nethost.dll must be deployed alongside")
endif()
else()
message(STATUS "nethost not found automatically. Set NETHOST_DIR to the directory containing nethost.h and nethost.lib")
# Fall back to bundled headers if present
message(STATUS "nethost not found automatically. Set NETHOST_DIR to the directory containing nethost.h and libnethost.lib")
set(NETHOST_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include")
set(NETHOST_LIB "")
set(NETHOST_STATIC FALSE)
endif()
# ── Runtime DLL target ─────────────────────────────────────────────────
add_library(LegacyForgeRuntime SHARED
src/dllmain.cpp
src/LogUtil.cpp
src/PdbParser.cpp
src/SymbolResolver.cpp
src/HookManager.cpp
src/GameHooks.cpp
src/DotNetHost.cpp
src/IdRegistry.cpp
src/NativeExports.cpp
src/CreativeInventory.cpp
src/MainMenuOverlay.cpp
)
target_include_directories(LegacyForgeRuntime PRIVATE
@@ -55,13 +87,16 @@ target_include_directories(LegacyForgeRuntime PRIVATE
target_link_libraries(LegacyForgeRuntime PRIVATE
minhook
dbghelp
raw_pdb
)
if(EXISTS "${NETHOST_LIB}")
target_link_libraries(LegacyForgeRuntime PRIVATE "${NETHOST_LIB}")
if(NETHOST_STATIC)
target_compile_definitions(LegacyForgeRuntime PRIVATE NETHOST_USE_AS_STATIC)
endif()
else()
message(WARNING "nethost.lib not found. You may need to set NETHOST_DIR.")
message(WARNING "nethost lib not found. You may need to set NETHOST_DIR.")
endif()
target_compile_definitions(LegacyForgeRuntime PRIVATE
@@ -74,6 +109,10 @@ if(MSVC)
endif()
set_target_properties(LegacyForgeRuntime PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../build"
LIBRARY_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"
LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}/../build"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}/../build"
)

View File

@@ -0,0 +1,109 @@
#include "CreativeInventory.h"
#include "SymbolResolver.h"
#include <cstdio>
#include <cstring>
namespace CreativeInventory
{
void* pCategoryGroups = nullptr;
void* pItemInstanceCtor = nullptr;
void* pSharedPtrCtor = nullptr;
void* pVectorPushBack = nullptr;
std::vector<PendingCreativeItem> s_pendingItems;
static const int CREATIVE_GROUP_COUNT = 15;
static const int SIZEOF_MSVC_VECTOR = 24;
static const int SIZEOF_MSVC_SHARED_PTR = 16;
static const int ITEMINSTANCE_ALLOC_SIZE = 256;
typedef void (__fastcall *ItemInstanceCtor_fn)(void* thisPtr, int id, int count, int auxValue);
typedef void (__fastcall *SharedPtrCtor_fn)(void* sharedPtrThis, void* rawItemPtr);
typedef void (__fastcall *VectorPushBackMove_fn)(void* vectorThis, void* sharedPtrRvalueRef);
void AddPending(int itemId, int count, int auxValue, int groupIndex)
{
s_pendingItems.push_back({ itemId, count, auxValue, groupIndex });
printf("[LegacyForge] Queued creative item: id=%d group=%d\n", itemId, groupIndex);
}
bool ResolveSymbols(SymbolResolver& resolver)
{
pCategoryGroups = resolver.Resolve(
"?categoryGroups@IUIScene_CreativeMenu@@1PAV?$vector@V?$shared_ptr@VItemInstance@@@std@@"
"V?$allocator@V?$shared_ptr@VItemInstance@@@std@@@2@@std@@A");
pItemInstanceCtor = resolver.Resolve("??0ItemInstance@@QEAA@HHH@Z");
pSharedPtrCtor = resolver.Resolve(
"??$?0VItemInstance@@$0A@@?$shared_ptr@VItemInstance@@@std@@QEAA@PEAVItemInstance@@@Z");
pVectorPushBack = resolver.Resolve(
"?push_back@?$vector@V?$shared_ptr@VItemInstance@@@std@@V?$allocator@V?$shared_ptr@VItemInstance@@@std@@@2@@std@@"
"QEAAX$$QEAV?$shared_ptr@VItemInstance@@@2@@Z");
if (pCategoryGroups) printf("[LegacyForge] categoryGroups @ %p\n", pCategoryGroups);
else printf("[LegacyForge] MISSING: categoryGroups\n");
if (pItemInstanceCtor) printf("[LegacyForge] ItemInstance ctor @ %p\n", pItemInstanceCtor);
else printf("[LegacyForge] MISSING: ItemInstance(int,int,int)\n");
if (pSharedPtrCtor) printf("[LegacyForge] shared_ptr<II> ctor @ %p\n", pSharedPtrCtor);
else printf("[LegacyForge] MISSING: shared_ptr<ItemInstance>(ItemInstance*)\n");
if (pVectorPushBack) printf("[LegacyForge] vector::push_back @ %p\n", pVectorPushBack);
else printf("[LegacyForge] MISSING: vector<shared_ptr<II>>::push_back\n");
return pCategoryGroups && pItemInstanceCtor && pSharedPtrCtor && pVectorPushBack;
}
void InjectItems()
{
if (!pCategoryGroups || !pItemInstanceCtor || !pSharedPtrCtor || !pVectorPushBack)
{
printf("[LegacyForge] Cannot inject creative items: missing symbols\n");
return;
}
if (s_pendingItems.empty())
{
printf("[LegacyForge] No creative items to inject\n");
return;
}
auto ctorFn = reinterpret_cast<ItemInstanceCtor_fn>(pItemInstanceCtor);
auto spCtorFn = reinterpret_cast<SharedPtrCtor_fn>(pSharedPtrCtor);
auto pushFn = reinterpret_cast<VectorPushBackMove_fn>(pVectorPushBack);
char* groups = reinterpret_cast<char*>(pCategoryGroups);
for (auto& item : s_pendingItems)
{
if (item.groupIndex < 0 || item.groupIndex >= CREATIVE_GROUP_COUNT)
{
printf("[LegacyForge] Skipping creative item id=%d: invalid group %d\n",
item.itemId, item.groupIndex);
continue;
}
void* rawItem = ::operator new(ITEMINSTANCE_ALLOC_SIZE);
memset(rawItem, 0, ITEMINSTANCE_ALLOC_SIZE);
ctorFn(rawItem, item.itemId, item.count, item.auxValue);
char spBuf[16];
memset(spBuf, 0, sizeof(spBuf));
spCtorFn(spBuf, rawItem);
char* vec = groups + item.groupIndex * SIZEOF_MSVC_VECTOR;
pushFn(vec, spBuf);
printf("[LegacyForge] Injected item id=%d into creative group %d\n",
item.itemId, item.groupIndex);
}
printf("[LegacyForge] Injected %zu items into creative inventory\n", s_pendingItems.size());
s_pendingItems.clear();
}
} // namespace CreativeInventory

View File

@@ -0,0 +1,26 @@
#pragma once
#include <vector>
class SymbolResolver;
struct PendingCreativeItem
{
int itemId;
int count;
int auxValue;
int groupIndex;
};
namespace CreativeInventory
{
void AddPending(int itemId, int count, int auxValue, int groupIndex);
bool ResolveSymbols(SymbolResolver& resolver);
void InjectItems();
extern void* pCategoryGroups;
extern void* pItemInstanceCtor;
extern void* pSharedPtrCtor;
extern void* pVectorPushBack;
extern std::vector<PendingCreativeItem> s_pendingItems;
}

View File

@@ -1,6 +1,5 @@
#include "DotNetHost.h"
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <nethost.h>
#include <hostfxr.h>
@@ -162,10 +161,11 @@ void DotNetHost::CallManagedInit()
fn_Initialize(nullptr, 0);
}
void DotNetHost::CallDiscoverMods(const char* modsPath)
int DotNetHost::CallDiscoverMods(const char* modsPath)
{
if (fn_DiscoverMods)
fn_DiscoverMods(const_cast<char*>(modsPath), static_cast<int>(strlen(modsPath)));
return fn_DiscoverMods(const_cast<char*>(modsPath), static_cast<int>(strlen(modsPath)));
return 0;
}
void DotNetHost::CallPreInit()

View File

@@ -9,7 +9,7 @@ namespace DotNetHost
void Cleanup();
void CallManagedInit();
void CallDiscoverMods(const char* modsPath);
int CallDiscoverMods(const char* modsPath);
void CallPreInit();
void CallInit();
void CallPostInit();

View File

@@ -1,13 +1,18 @@
#include "GameHooks.h"
#include "DotNetHost.h"
#include "CreativeInventory.h"
#include "MainMenuOverlay.h"
#include <cstdio>
namespace GameHooks
{
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
ExitGame_fn Original_ExitGame = nullptr;
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
ExitGame_fn Original_ExitGame = nullptr;
CreativeStaticCtor_fn Original_CreativeStaticCtor = nullptr;
MainMenuCustomDraw_fn Original_MainMenuCustomDraw = nullptr;
Present_fn Original_Present = nullptr;
void Hooked_RunStaticCtors()
{
@@ -45,4 +50,25 @@ namespace GameHooks
Original_ExitGame(thisPtr);
}
void Hooked_CreativeStaticCtor()
{
printf("[LegacyForge] Hook: CreativeStaticCtor -- building vanilla creative lists\n");
Original_CreativeStaticCtor();
printf("[LegacyForge] Hook: CreativeStaticCtor -- injecting modded items\n");
CreativeInventory::InjectItems();
}
void __fastcall Hooked_MainMenuCustomDraw(void* thisPtr, void* region)
{
MainMenuOverlay::NotifyOnMainMenu();
Original_MainMenuCustomDraw(thisPtr, region);
}
void __fastcall Hooked_Present(void* thisPtr)
{
MainMenuOverlay::RenderBranding();
Original_Present(thisPtr);
}
}

View File

@@ -3,27 +3,30 @@
/// Function pointer typedefs matching the game's actual function signatures.
/// x64 MSVC uses __fastcall-like convention (this in rcx, args in rdx/r8/r9).
///
/// Verified against Minecraft.Client.pdb:
/// ?MinecraftWorld_RunStaticCtors@@YAXXZ -- void __cdecl ()
/// ?tick@Minecraft@@QEAAX_N0@Z -- void __thiscall (bool, bool)
/// ?init@Minecraft@@QEAAXXZ -- void __thiscall ()
/// ?ExitGame@CConsoleMinecraftApp@@UEAAXXZ -- void __thiscall ()
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 *ExitGame_fn)(void* thisPtr);
typedef void (*CreativeStaticCtor_fn)();
typedef void (__fastcall *MainMenuCustomDraw_fn)(void* thisPtr, void* region);
typedef void (__fastcall *Present_fn)(void* thisPtr);
namespace GameHooks
{
extern RunStaticCtors_fn Original_RunStaticCtors;
extern MinecraftTick_fn Original_MinecraftTick;
extern MinecraftInit_fn Original_MinecraftInit;
extern ExitGame_fn Original_ExitGame;
extern RunStaticCtors_fn Original_RunStaticCtors;
extern MinecraftTick_fn Original_MinecraftTick;
extern MinecraftInit_fn Original_MinecraftInit;
extern ExitGame_fn Original_ExitGame;
extern CreativeStaticCtor_fn Original_CreativeStaticCtor;
extern MainMenuCustomDraw_fn Original_MainMenuCustomDraw;
extern Present_fn Original_Present;
void Hooked_RunStaticCtors();
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures);
void __fastcall Hooked_MinecraftInit(void* thisPtr);
void __fastcall Hooked_ExitGame(void* thisPtr);
void Hooked_CreativeStaticCtor();
void __fastcall Hooked_MainMenuCustomDraw(void* thisPtr, void* region);
void __fastcall Hooked_Present(void* thisPtr);
}

View File

@@ -1,6 +1,8 @@
#include "HookManager.h"
#include "GameHooks.h"
#include "SymbolResolver.h"
#include "CreativeInventory.h"
#include "MainMenuOverlay.h"
#include <MinHook.h>
#include <cstdio>
@@ -51,7 +53,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
printf("[LegacyForge] Hooked Minecraft::init\n");
}
// Hook CConsoleMinecraftApp::ExitGame (optional -- for graceful shutdown)
// Hook CConsoleMinecraftApp::ExitGame
if (symbols.pExitGame)
{
if (MH_CreateHook(symbols.pExitGame,
@@ -66,6 +68,55 @@ bool HookManager::Install(const SymbolResolver& symbols)
}
}
// Hook IUIScene_CreativeMenu::staticCtor
if (symbols.pCreativeStaticCtor)
{
CreativeInventory::ResolveSymbols(const_cast<SymbolResolver&>(symbols));
if (MH_CreateHook(symbols.pCreativeStaticCtor,
reinterpret_cast<void*>(&GameHooks::Hooked_CreativeStaticCtor),
reinterpret_cast<void**>(&GameHooks::Original_CreativeStaticCtor)) != MH_OK)
{
printf("[LegacyForge] Warning: Failed to hook CreativeStaticCtor\n");
}
else
{
printf("[LegacyForge] Hooked CreativeStaticCtor\n");
}
}
// Hook UIScene_MainMenu::customDraw (sets main-menu detection flag)
if (symbols.pMainMenuCustomDraw)
{
if (MH_CreateHook(symbols.pMainMenuCustomDraw,
reinterpret_cast<void*>(&GameHooks::Hooked_MainMenuCustomDraw),
reinterpret_cast<void**>(&GameHooks::Original_MainMenuCustomDraw)) != MH_OK)
{
printf("[LegacyForge] Warning: Failed to hook MainMenuCustomDraw\n");
}
else
{
printf("[LegacyForge] Hooked MainMenuCustomDraw\n");
}
}
// Hook C4JRender::Present (draws branding overlay just before frame present)
if (symbols.pPresent)
{
MainMenuOverlay::ResolveSymbols(const_cast<SymbolResolver&>(symbols));
if (MH_CreateHook(symbols.pPresent,
reinterpret_cast<void*>(&GameHooks::Hooked_Present),
reinterpret_cast<void**>(&GameHooks::Original_Present)) != MH_OK)
{
printf("[LegacyForge] Warning: Failed to hook C4JRender::Present\n");
}
else
{
printf("[LegacyForge] Hooked C4JRender::Present\n");
}
}
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK)
{
printf("[LegacyForge] MH_EnableHook(MH_ALL_HOOKS) failed\n");

View File

@@ -0,0 +1,40 @@
#include "LogUtil.h"
#include <cstdio>
#include <cstdarg>
#include <string>
static std::string s_logPath;
namespace LogUtil
{
void SetBaseDir(const char* baseDir)
{
s_logPath = std::string(baseDir) + "legacyforge.log";
}
void Log(const char* fmt, ...)
{
// Also print to stdout for console visibility
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
printf("\n");
if (s_logPath.empty()) return;
FILE* f = nullptr;
fopen_s(&f, s_logPath.c_str(), "a");
if (f)
{
va_list args2;
va_start(args2, fmt);
vfprintf(f, fmt, args2);
va_end(args2);
fprintf(f, "\n");
fclose(f);
}
}
} // namespace LogUtil

View File

@@ -0,0 +1,10 @@
#pragma once
namespace LogUtil
{
// Must be called once at startup with the runtime DLL's directory (with trailing backslash)
void SetBaseDir(const char* baseDir);
// Appends a line to legacyforge.log in the base directory
void Log(const char* fmt, ...);
}

View File

@@ -0,0 +1,170 @@
#include "MainMenuOverlay.h"
#include "SymbolResolver.h"
#include "LogUtil.h"
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#define LEGACYFORGE_VERSION L"1.0.0"
namespace MainMenuOverlay
{
static int s_modCount = 0;
static bool s_onMainMenu = false;
static bool s_loggedOnce = false;
static bool s_symbolsOk = false;
// ── Resolved PDB addresses ──────────────────────────────────────────────
static void** pMinecraftInstance = nullptr;
static void* pRenderManager = nullptr;
// C4JRender member functions
typedef void (__fastcall *VoidMethod_fn)(void*);
typedef void (__fastcall *MatrixMode_fn)(void*, int);
typedef void (__fastcall *MatrixOrthogonal_fn)(void*, float, float, float, float, float, float);
typedef void (__fastcall *MatrixTranslate_fn)(void*, float, float, float);
typedef void (__fastcall *BoolMethod_fn)(void*, bool);
static VoidMethod_fn fnStartFrame = nullptr;
static VoidMethod_fn fnMatrixPush = nullptr;
static VoidMethod_fn fnMatrixPop = nullptr;
static MatrixMode_fn fnMatrixMode = nullptr;
static VoidMethod_fn fnMatrixSetIdentity = nullptr;
static MatrixOrthogonal_fn fnMatrixOrthogonal = nullptr;
static MatrixTranslate_fn fnMatrixTranslate = nullptr;
static BoolMethod_fn fnDepthTest = nullptr;
static BoolMethod_fn fnBlendEnable = nullptr;
static VoidMethod_fn fnSetMatrixDirty = nullptr;
// Font::drawShadow(const wstring&, int x, int y, int color)
typedef void (__fastcall *FontDrawShadow_fn)(void*, const std::wstring&, int, int, int);
static FontDrawShadow_fn fnDrawShadow = nullptr;
static const int C4J_GL_MODELVIEW = 0;
static const int C4J_GL_PROJECTION = 1;
// Minecraft instance member offsets (no vtable, no base class)
static const int OFF_WIDTH_PHYS = 32;
static const int OFF_HEIGHT_PHYS = 36;
static const int OFF_FONT = 432; // Font* at 0x1B0
static int ComputeGuiScale(int pixelW, int pixelH)
{
int scale = 1;
while (pixelW / (scale + 1) >= 320 && pixelH / (scale + 1) >= 240)
scale++;
return scale;
}
bool ResolveSymbols(SymbolResolver& resolver)
{
pMinecraftInstance = (void**)resolver.Resolve("?m_instance@Minecraft@@0PEAV1@EA");
pRenderManager = resolver.Resolve("?RenderManager@@3VC4JRender@@A");
fnStartFrame = (VoidMethod_fn) resolver.Resolve("?StartFrame@C4JRender@@QEAAXXZ");
fnMatrixPush = (VoidMethod_fn) resolver.Resolve("?MatrixPush@C4JRender@@QEAAXXZ");
fnMatrixPop = (VoidMethod_fn) resolver.Resolve("?MatrixPop@C4JRender@@QEAAXXZ");
fnMatrixMode = (MatrixMode_fn) resolver.Resolve("?MatrixMode@C4JRender@@QEAAXH@Z");
fnMatrixSetIdentity= (VoidMethod_fn) resolver.Resolve("?MatrixSetIdentity@C4JRender@@QEAAXXZ");
fnMatrixOrthogonal = (MatrixOrthogonal_fn)resolver.Resolve("?MatrixOrthogonal@C4JRender@@QEAAXMMMMMM@Z");
fnMatrixTranslate = (MatrixTranslate_fn) resolver.Resolve("?MatrixTranslate@C4JRender@@QEAAXMMM@Z");
fnDepthTest = (BoolMethod_fn) resolver.Resolve("?StateSetDepthTestEnable@C4JRender@@QEAAX_N@Z");
fnBlendEnable = (BoolMethod_fn) resolver.Resolve("?StateSetBlendEnable@C4JRender@@QEAAX_N@Z");
fnSetMatrixDirty = (VoidMethod_fn) resolver.Resolve("?Set_matrixDirty@C4JRender@@QEAAXXZ");
fnDrawShadow = (FontDrawShadow_fn)resolver.Resolve(
"?drawShadow@Font@@QEAAXAEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@HHH@Z");
s_symbolsOk = pMinecraftInstance && pRenderManager && fnStartFrame &&
fnMatrixPush && fnMatrixPop && fnMatrixMode &&
fnMatrixSetIdentity && fnMatrixOrthogonal &&
fnMatrixTranslate && fnDepthTest && fnDrawShadow;
if (s_symbolsOk)
LogUtil::Log("[LegacyForge] MainMenuOverlay symbols resolved OK");
else
LogUtil::Log("[LegacyForge] Warning: some MainMenuOverlay symbols missing -- branding text disabled");
return s_symbolsOk;
}
void SetModCount(int count) { s_modCount = count; }
int GetModCount() { return s_modCount; }
void NotifyOnMainMenu()
{
s_onMainMenu = true;
}
void RenderBranding()
{
if (!s_onMainMenu || !s_symbolsOk) return;
s_onMainMenu = false;
void* mc = *pMinecraftInstance;
if (!mc) return;
int pixelW = *(int*)((char*)mc + OFF_WIDTH_PHYS);
int pixelH = *(int*)((char*)mc + OFF_HEIGHT_PHYS);
if (pixelW <= 0 || pixelH <= 0 || pixelW > 8192 || pixelH > 8192) return;
void* font = *(void**)((char*)mc + OFF_FONT);
if (!font) return;
if (!s_loggedOnce)
{
LogUtil::Log("[LegacyForge] MainMenuOverlay: first render (screen %dx%d, font=%p, rm=%p)",
pixelW, pixelH, font, pRenderManager);
s_loggedOnce = true;
}
int guiScale = ComputeGuiScale(pixelW, pixelH);
int guiW = (int)ceil((double)pixelW / guiScale);
int guiH = (int)ceil((double)pixelH / guiScale);
void* rm = pRenderManager;
// Re-initialize C4JRender's D3D11 state (shaders, render targets, etc.)
// After GDraw's NoMoreGDrawThisFrame, C4JRender's shaders are not bound.
fnStartFrame(rm);
fnSetMatrixDirty(rm);
// Set up 2D orthographic projection
fnMatrixMode(rm, C4J_GL_PROJECTION);
fnMatrixPush(rm);
fnMatrixSetIdentity(rm);
fnMatrixOrthogonal(rm, 0.0f, (float)guiW, (float)guiH, 0.0f, 1000.0f, 3000.0f);
fnMatrixMode(rm, C4J_GL_MODELVIEW);
fnMatrixPush(rm);
fnMatrixSetIdentity(rm);
fnMatrixTranslate(rm, 0.0f, 0.0f, -2000.0f);
fnDepthTest(rm, false);
if (fnBlendEnable) fnBlendEnable(rm, true);
std::wstring line1 = L"LegacyForge v" LEGACYFORGE_VERSION;
wchar_t line2Buf[64];
swprintf(line2Buf, 64, L"%d mod(s) loaded successfully", s_modCount);
std::wstring line2(line2Buf);
int textX = 2;
int textY1 = guiH - 20;
int textY2 = guiH - 10;
int color = (int)0xFFFFFFFF;
fnDrawShadow(font, line1, textX, textY1, color);
fnDrawShadow(font, line2, textX, textY2, color);
fnDepthTest(rm, true);
fnMatrixMode(rm, C4J_GL_MODELVIEW);
fnMatrixPop(rm);
fnMatrixMode(rm, C4J_GL_PROJECTION);
fnMatrixPop(rm);
}
} // namespace MainMenuOverlay

View File

@@ -0,0 +1,17 @@
#pragma once
class SymbolResolver;
namespace MainMenuOverlay
{
bool ResolveSymbols(SymbolResolver& resolver);
// Called from the customDraw hook to signal we're on the main menu this frame
void NotifyOnMainMenu();
// Called from the Present hook, draws branding if on the main menu
void RenderBranding();
void SetModCount(int count);
int GetModCount();
}

View File

@@ -1,5 +1,7 @@
#include "NativeExports.h"
#include "IdRegistry.h"
#include "CreativeInventory.h"
#include "LogUtil.h"
#include <cstdio>
#include <cstring>
@@ -98,7 +100,7 @@ void native_add_furnace_recipe(
void native_log(const char* message, int level)
{
if (message)
printf("%s\n", message);
LogUtil::Log("%s", message);
}
int native_get_block_id(const char* namespacedId)
@@ -125,4 +127,9 @@ void native_subscribe_event(const char* eventName, void* managedFnPtr)
printf("[LegacyForge] Event subscription: %s\n", eventName ? eventName : "(null)");
}
void native_add_to_creative(int numericId, int count, int auxValue, int groupIndex)
{
CreativeInventory::AddPending(numericId, count, auxValue, groupIndex);
}
} // extern "C"

View File

@@ -48,4 +48,10 @@ extern "C"
__declspec(dllexport) void native_subscribe_event(
const char* eventName,
void* managedFnPtr);
__declspec(dllexport) void native_add_to_creative(
int numericId,
int count,
int auxValue,
int groupIndex);
}

View File

@@ -0,0 +1,377 @@
#include "PdbParser.h"
#include "LogUtil.h"
#include <Windows.h>
#include <cstring>
#include "PDB.h"
#include "PDB_RawFile.h"
#include "PDB_InfoStream.h"
#include "PDB_DBIStream.h"
#include "PDB_PublicSymbolStream.h"
#include "PDB_GlobalSymbolStream.h"
#include "PDB_ImageSectionStream.h"
#include "PDB_CoalescedMSFStream.h"
#include "PDB_ModuleInfoStream.h"
#include "PDB_ModuleSymbolStream.h"
struct MappedFile
{
HANDLE hFile = INVALID_HANDLE_VALUE;
HANDLE hMapping = nullptr;
void* baseAddress = nullptr;
size_t fileSize = 0;
};
static MappedFile s_mapped;
static bool s_open = false;
static PDB::RawFile* s_rawFile = nullptr;
static PDB::DBIStream* s_dbiStream = nullptr;
static PDB::ImageSectionStream* s_sectionStream = nullptr;
static PDB::PublicSymbolStream* s_publicStream = nullptr;
static PDB::GlobalSymbolStream* s_globalStream = nullptr;
static PDB::ModuleInfoStream* s_moduleStream = nullptr;
static PDB::CoalescedMSFStream* s_symbolRecords = nullptr;
static void CloseMappedFile(MappedFile& mf)
{
if (mf.baseAddress) { UnmapViewOfFile(mf.baseAddress); mf.baseAddress = nullptr; }
if (mf.hMapping) { CloseHandle(mf.hMapping); mf.hMapping = nullptr; }
if (mf.hFile != INVALID_HANDLE_VALUE) { CloseHandle(mf.hFile); mf.hFile = INVALID_HANDLE_VALUE; }
mf.fileSize = 0;
}
static bool OpenMappedFile(const char* path, MappedFile& mf)
{
mf.hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
if (mf.hFile == INVALID_HANDLE_VALUE) return false;
mf.hMapping = CreateFileMappingW(mf.hFile, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!mf.hMapping) { CloseMappedFile(mf); return false; }
mf.baseAddress = MapViewOfFile(mf.hMapping, FILE_MAP_READ, 0, 0, 0);
if (!mf.baseAddress) { CloseMappedFile(mf); return false; }
BY_HANDLE_FILE_INFORMATION fi;
if (!GetFileInformationByHandle(mf.hFile, &fi)) { CloseMappedFile(mf); return false; }
mf.fileSize = (static_cast<size_t>(fi.nFileSizeHigh) << 32) | fi.nFileSizeLow;
return true;
}
// Helpers to extract name + section/offset from various global symbol record kinds
static const char* GetGlobalSymName(const PDB::CodeView::DBI::Record* record,
uint16_t& outSection, uint32_t& outOffset)
{
switch (record->header.kind)
{
case PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32:
outSection = record->data.S_GDATA32.section;
outOffset = record->data.S_GDATA32.offset;
return record->data.S_GDATA32.name;
case PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32:
outSection = record->data.S_LDATA32.section;
outOffset = record->data.S_LDATA32.offset;
return record->data.S_LDATA32.name;
case PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32:
outSection = record->data.S_GTHREAD32.section;
outOffset = record->data.S_GTHREAD32.offset;
return record->data.S_GTHREAD32.name;
case PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32:
outSection = record->data.S_LTHREAD32.section;
outOffset = record->data.S_LTHREAD32.offset;
return record->data.S_LTHREAD32.name;
default:
return nullptr;
}
}
namespace PdbParser
{
bool Open(const char* pdbPath)
{
if (s_open) Close();
LogUtil::Log("[LegacyForge] PdbParser: opening %s", pdbPath);
if (!OpenMappedFile(pdbPath, s_mapped))
{
LogUtil::Log("[LegacyForge] PdbParser: failed to memory-map PDB file");
return false;
}
if (PDB::ValidateFile(s_mapped.baseAddress, s_mapped.fileSize) != PDB::ErrorCode::Success)
{
LogUtil::Log("[LegacyForge] PdbParser: PDB validation failed");
CloseMappedFile(s_mapped);
return false;
}
s_rawFile = new PDB::RawFile(PDB::CreateRawFile(s_mapped.baseAddress));
if (PDB::HasValidDBIStream(*s_rawFile) != PDB::ErrorCode::Success)
{
LogUtil::Log("[LegacyForge] PdbParser: invalid DBI stream");
Close();
return false;
}
const PDB::InfoStream infoStream(*s_rawFile);
if (infoStream.UsesDebugFastLink())
{
LogUtil::Log("[LegacyForge] PdbParser: PDB uses unsupported /DEBUG:FASTLINK");
Close();
return false;
}
s_dbiStream = new PDB::DBIStream(PDB::CreateDBIStream(*s_rawFile));
if (s_dbiStream->HasValidImageSectionStream(*s_rawFile) != PDB::ErrorCode::Success)
{
LogUtil::Log("[LegacyForge] PdbParser: invalid image section stream");
Close();
return false;
}
if (s_dbiStream->HasValidPublicSymbolStream(*s_rawFile) != PDB::ErrorCode::Success)
{
LogUtil::Log("[LegacyForge] PdbParser: invalid public symbol stream");
Close();
return false;
}
if (s_dbiStream->HasValidGlobalSymbolStream(*s_rawFile) != PDB::ErrorCode::Success)
{
LogUtil::Log("[LegacyForge] PdbParser: invalid global symbol stream");
Close();
return false;
}
if (s_dbiStream->HasValidSymbolRecordStream(*s_rawFile) != PDB::ErrorCode::Success)
{
LogUtil::Log("[LegacyForge] PdbParser: invalid symbol record stream");
Close();
return false;
}
s_sectionStream = new PDB::ImageSectionStream(s_dbiStream->CreateImageSectionStream(*s_rawFile));
s_symbolRecords = new PDB::CoalescedMSFStream(s_dbiStream->CreateSymbolRecordStream(*s_rawFile));
s_publicStream = new PDB::PublicSymbolStream(s_dbiStream->CreatePublicSymbolStream(*s_rawFile));
s_globalStream = new PDB::GlobalSymbolStream(s_dbiStream->CreateGlobalSymbolStream(*s_rawFile));
s_moduleStream = new PDB::ModuleInfoStream(s_dbiStream->CreateModuleInfoStream(*s_rawFile));
s_open = true;
LogUtil::Log("[LegacyForge] PdbParser: PDB opened successfully");
return true;
}
uint32_t FindSymbolRVA(const char* decoratedName)
{
if (!s_open) return 0;
// 1) Search public symbol stream (S_PUB32)
{
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)
continue;
if (strcmp(record->data.S_PUB32.name, decoratedName) == 0)
{
return s_sectionStream->ConvertSectionOffsetToRVA(
record->data.S_PUB32.section,
record->data.S_PUB32.offset);
}
}
}
// 2) Search global symbol stream (S_GDATA32, S_LDATA32, S_GTHREAD32, S_LTHREAD32)
{
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 && strcmp(name, decoratedName) == 0)
{
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
if (rva != 0) return rva;
}
}
}
// 3) Search per-module symbol streams (S_LPROC32, S_GPROC32, S_LPROC32_ID, S_GPROC32_ID, S_LDATA32, S_GDATA32)
{
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);
uint32_t foundRVA = 0;
modSymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* record)
{
if (foundRVA != 0) return;
const char* name = nullptr;
uint16_t section = 0;
uint32_t offset = 0;
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;
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;
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;
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;
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 && strcmp(name, decoratedName) == 0)
{
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
if (rva != 0) foundRVA = rva;
}
});
if (foundRVA != 0) return foundRVA;
}
}
return 0;
}
void DumpMatching(const char* substring)
{
if (!s_open) return;
LogUtil::Log("[LegacyForge] PdbParser: dumping symbols containing '%s'...", substring);
int count = 0;
// 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)
{
if (strstr(record->data.S_PUB32.name, substring))
{
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(
record->data.S_PUB32.section, record->data.S_PUB32.offset);
LogUtil::Log(" [PUB] rva=0x%08X %s", rva, record->data.S_PUB32.name);
count++;
}
}
}
}
// 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 && strstr(name, substring))
{
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
LogUtil::Log(" [GLB kind=0x%04X] rva=0x%08X %s",
(unsigned)record->header.kind, rva, name);
count++;
}
}
}
// 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;
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; 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; 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; 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; 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 && strstr(name, substring))
{
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
LogUtil::Log(" [MOD kind=0x%04X] rva=0x%08X %s",
(unsigned)record->header.kind, rva, name);
count++;
}
});
}
}
LogUtil::Log("[LegacyForge] PdbParser: found %d matching symbols", count);
}
void Close()
{
delete s_moduleStream; s_moduleStream = nullptr;
delete s_globalStream; s_globalStream = nullptr;
delete s_publicStream; s_publicStream = nullptr;
delete s_symbolRecords; s_symbolRecords = nullptr;
delete s_sectionStream; s_sectionStream = nullptr;
delete s_dbiStream; s_dbiStream = nullptr;
delete s_rawFile; s_rawFile = nullptr;
CloseMappedFile(s_mapped);
s_open = false;
}
} // namespace PdbParser

View File

@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
namespace PdbParser
{
bool Open(const char* pdbPath);
// Returns the RVA for a decorated symbol name, or 0 on failure.
uint32_t FindSymbolRVA(const char* decoratedName);
// Logs all symbols whose name contains the given substring (for debugging).
void DumpMatching(const char* substring);
void Close();
}

View File

@@ -1,25 +1,43 @@
#include "SymbolResolver.h"
#include "PdbParser.h"
#include "LogUtil.h"
#include <cstdio>
#include <cstring>
#include <string>
#pragma comment(lib, "dbghelp.lib")
// Exact MSVC-decorated symbol names from the game's PDB.
// These are verified against the actual Minecraft.Client.pdb.
static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ";
static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z";
static const char* SYM_MINECRAFT_INIT = "?init@Minecraft@@QEAAXXZ";
static const char* SYM_EXIT_GAME = "?ExitGame@CConsoleMinecraftApp@@UEAAXXZ";
static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ";
static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z";
static const char* SYM_MINECRAFT_INIT = "?init@Minecraft@@QEAAXXZ";
static const char* SYM_EXIT_GAME = "?ExitGame@CConsoleMinecraftApp@@UEAAXXZ";
static const char* SYM_CREATIVE_STATIC_CTOR = "?staticCtor@IUIScene_CreativeMenu@@SAXXZ";
static const char* SYM_MAINMENU_CUSTOMDRAW = "?customDraw@UIScene_MainMenu@@UEAAXPEAUIggyCustomDrawCallbackRegion@@@Z";
static const char* SYM_PRESENT = "?Present@C4JRender@@QEAAXXZ";
bool SymbolResolver::Initialize()
{
m_process = GetCurrentProcess();
SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_DEBUG);
if (!SymInitialize(m_process, nullptr, TRUE))
m_moduleBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
if (!m_moduleBase)
{
printf("[LegacyForge] SymInitialize failed (error %lu)\n", GetLastError());
LogUtil::Log("[LegacyForge] Failed to get module base address");
return false;
}
// Derive PDB path from executable path: replace .exe with .pdb
char exePath[MAX_PATH] = {0};
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
std::string pdbPath(exePath);
size_t dotPos = pdbPath.rfind('.');
if (dotPos != std::string::npos)
pdbPath = pdbPath.substr(0, dotPos) + ".pdb";
else
pdbPath += ".pdb";
LogUtil::Log("[LegacyForge] PDB path: %s", pdbPath.c_str());
LogUtil::Log("[LegacyForge] Module base: %p", reinterpret_cast<void*>(m_moduleBase));
if (!PdbParser::Open(pdbPath.c_str()))
{
LogUtil::Log("[LegacyForge] ERROR: Failed to open PDB file");
return false;
}
@@ -31,50 +49,54 @@ void* SymbolResolver::Resolve(const char* decoratedName)
{
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, decoratedName, symbol))
uint32_t rva = PdbParser::FindSymbolRVA(decoratedName);
if (rva == 0)
{
return reinterpret_cast<void*>(symbol->Address);
LogUtil::Log("[LegacyForge] Symbol not found in PDB: '%s'", decoratedName);
return nullptr;
}
printf("[LegacyForge] SymFromName failed for '%s' (error %lu)\n", decoratedName, GetLastError());
return nullptr;
return reinterpret_cast<void*>(m_moduleBase + rva);
}
bool SymbolResolver::ResolveGameFunctions()
{
pRunStaticCtors = Resolve(SYM_RUN_STATIC_CTORS);
pMinecraftTick = Resolve(SYM_MINECRAFT_TICK);
pMinecraftInit = Resolve(SYM_MINECRAFT_INIT);
pExitGame = Resolve(SYM_EXIT_GAME);
LogUtil::Log("[LegacyForge] Resolving game functions via raw PDB parser...");
if (pRunStaticCtors) printf("[LegacyForge] RunStaticCtors @ %p\n", pRunStaticCtors);
else printf("[LegacyForge] MISSING: MinecraftWorld_RunStaticCtors\n");
pRunStaticCtors = Resolve(SYM_RUN_STATIC_CTORS);
pMinecraftTick = Resolve(SYM_MINECRAFT_TICK);
pMinecraftInit = Resolve(SYM_MINECRAFT_INIT);
pExitGame = Resolve(SYM_EXIT_GAME);
pCreativeStaticCtor = Resolve(SYM_CREATIVE_STATIC_CTOR);
pMainMenuCustomDraw = Resolve(SYM_MAINMENU_CUSTOMDRAW);
pPresent = Resolve(SYM_PRESENT);
if (pMinecraftTick) printf("[LegacyForge] Minecraft::tick @ %p\n", pMinecraftTick);
else printf("[LegacyForge] MISSING: Minecraft::tick\n");
auto logSym = [](const char* name, void* ptr) {
if (ptr)
LogUtil::Log("[LegacyForge] %-25s @ %p", name, ptr);
else
LogUtil::Log("[LegacyForge] MISSING: %s", name);
};
if (pMinecraftInit) printf("[LegacyForge] Minecraft::init @ %p\n", pMinecraftInit);
else printf("[LegacyForge] MISSING: Minecraft::init\n");
logSym("RunStaticCtors", pRunStaticCtors);
logSym("Minecraft::tick", pMinecraftTick);
logSym("Minecraft::init", pMinecraftInit);
logSym("ExitGame", pExitGame);
logSym("CreativeStaticCtor", pCreativeStaticCtor);
logSym("MainMenuCustomDraw", pMainMenuCustomDraw);
logSym("C4JRender::Present", pPresent);
if (pExitGame) printf("[LegacyForge] ExitGame @ %p\n", pExitGame);
else printf("[LegacyForge] MISSING: CConsoleMinecraftApp::ExitGame\n");
bool ok = pRunStaticCtors && pMinecraftTick && pMinecraftInit;
if (ok)
LogUtil::Log("[LegacyForge] All critical symbols resolved (via raw PDB parser)");
else
LogUtil::Log("[LegacyForge] CRITICAL symbols missing - hooks will not be installed");
// RunStaticCtors, tick, and init are critical. ExitGame is optional (graceful shutdown).
return pRunStaticCtors && pMinecraftTick && pMinecraftInit;
return ok;
}
void SymbolResolver::Cleanup()
{
if (m_initialized)
{
SymCleanup(m_process);
m_initialized = false;
}
PdbParser::Close();
m_initialized = false;
}

View File

@@ -1,7 +1,5 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <DbgHelp.h>
class SymbolResolver
{
@@ -10,14 +8,17 @@ public:
bool ResolveGameFunctions();
void Cleanup();
void* Resolve(const char* functionName);
void* Resolve(const char* decoratedName);
void* pRunStaticCtors = nullptr; // MinecraftWorld_RunStaticCtors
void* pMinecraftTick = nullptr; // Minecraft::tick(bool, bool)
void* pMinecraftInit = nullptr; // Minecraft::init()
void* pExitGame = nullptr; // CConsoleMinecraftApp::ExitGame()
void* pRunStaticCtors = nullptr; // MinecraftWorld_RunStaticCtors
void* pMinecraftTick = nullptr; // Minecraft::tick(bool, bool)
void* pMinecraftInit = nullptr; // Minecraft::init()
void* pExitGame = nullptr; // CConsoleMinecraftApp::ExitGame()
void* pCreativeStaticCtor = nullptr; // IUIScene_CreativeMenu::staticCtor()
void* pMainMenuCustomDraw = nullptr; // UIScene_MainMenu::customDraw()
void* pPresent = nullptr; // C4JRender::Present()
private:
HANDLE m_process = nullptr;
uintptr_t m_moduleBase = 0;
bool m_initialized = false;
};

View File

@@ -1,58 +1,81 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <cstdio>
#include <string>
#include "LogUtil.h"
#include "SymbolResolver.h"
#include "HookManager.h"
#include "DotNetHost.h"
#include "MainMenuOverlay.h"
static HMODULE g_hModule = nullptr;
static void LogToFile(const char* msg)
static std::string GetDllDirectory(HMODULE hModule)
{
FILE* f = nullptr;
fopen_s(&f, "legacyforge.log", "a");
if (f)
{
fprintf(f, "%s\n", msg);
fclose(f);
}
char path[MAX_PATH] = {0};
GetModuleFileNameA(hModule, path, MAX_PATH);
std::string s(path);
size_t pos = s.find_last_of("\\/");
if (pos != std::string::npos)
return s.substr(0, pos + 1);
return ".\\";
}
DWORD WINAPI InitThread(LPVOID lpParam)
{
LogToFile("[LegacyForge] InitThread started");
std::string baseDir = GetDllDirectory(g_hModule);
LogUtil::SetBaseDir(baseDir.c_str());
LogUtil::Log("[LegacyForge] InitThread started (module=%p)", g_hModule);
LogUtil::Log("[LegacyForge] Runtime DLL directory: %s", baseDir.c_str());
char cwd[MAX_PATH] = {0};
GetCurrentDirectoryA(MAX_PATH, cwd);
LogUtil::Log("[LegacyForge] Game working directory: %s", cwd);
char exePath[MAX_PATH] = {0};
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
LogUtil::Log("[LegacyForge] Host executable: %s", exePath);
SymbolResolver symbols;
if (!symbols.Initialize())
{
LogToFile("[LegacyForge] ERROR: Failed to initialize symbol resolver. Is the PDB present?");
LogUtil::Log("[LegacyForge] ERROR: Failed to initialize symbol resolver. Is the PDB present?");
return 1;
}
LogToFile("[LegacyForge] Symbol resolver initialized");
LogUtil::Log("[LegacyForge] Symbol resolver initialized");
if (!symbols.ResolveGameFunctions())
{
LogToFile("[LegacyForge] ERROR: Failed to resolve one or more game functions");
LogUtil::Log("[LegacyForge] ERROR: Failed to resolve critical game functions.");
return 1;
}
LogToFile("[LegacyForge] Game functions resolved from PDB");
LogUtil::Log("[LegacyForge] Game functions resolved from PDB");
if (!HookManager::Install(symbols))
{
LogToFile("[LegacyForge] ERROR: Failed to install hooks");
LogUtil::Log("[LegacyForge] ERROR: Failed to install hooks");
symbols.Cleanup();
return 1;
}
LogToFile("[LegacyForge] Hooks installed");
LogUtil::Log("[LegacyForge] Hooks installed");
// All symbol resolution is complete; release the PDB memory map
symbols.Cleanup();
if (!DotNetHost::Initialize())
{
LogToFile("[LegacyForge] ERROR: Failed to initialize .NET host");
LogUtil::Log("[LegacyForge] ERROR: Failed to initialize .NET host");
return 1;
}
LogToFile("[LegacyForge] .NET runtime initialized");
LogUtil::Log("[LegacyForge] .NET runtime initialized");
std::string modsPath = baseDir + "mods";
LogUtil::Log("[LegacyForge] Mods directory: %s", modsPath.c_str());
DotNetHost::CallManagedInit();
DotNetHost::CallDiscoverMods("mods");
LogToFile("[LegacyForge] Mod discovery complete. Ready.");
int modCount = DotNetHost::CallDiscoverMods(modsPath.c_str());
MainMenuOverlay::SetModCount(modCount);
LogUtil::Log("[LegacyForge] Mod discovery complete (%d mods). Ready.", modCount);
return 0;
}