diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs
index d232241..35b8790 100644
--- a/ExampleMod/ExampleMod.cs
+++ b/ExampleMod/ExampleMod.cs
@@ -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);
diff --git a/ExampleMod/ExampleMod.csproj b/ExampleMod/ExampleMod.csproj
index 3505083..a39df79 100644
--- a/ExampleMod/ExampleMod.csproj
+++ b/ExampleMod/ExampleMod.csproj
@@ -8,6 +8,9 @@
ExampleMod
Example mod for LegacyForge demonstrating the mod API
1.0.0
+ ..\build\mods
+ false
+ false
diff --git a/LegacyForge.API/AssemblyInfo.cs b/LegacyForge.API/AssemblyInfo.cs
new file mode 100644
index 0000000..ba4d159
--- /dev/null
+++ b/LegacyForge.API/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("LegacyForge.Core")]
diff --git a/LegacyForge.API/Block/BlockProperties.cs b/LegacyForge.API/Block/BlockProperties.cs
index 6fb7e43..e1e46d9 100644
--- a/LegacyForge.API/Block/BlockProperties.cs
+++ b/LegacyForge.API/Block/BlockProperties.cs
@@ -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; }
}
diff --git a/LegacyForge.API/Block/BlockRegistry.cs b/LegacyForge.API/Block/BlockRegistry.cs
index 1a1b794..cb4541b 100644
--- a/LegacyForge.API/Block/BlockRegistry.cs
+++ b/LegacyForge.API/Block/BlockRegistry.cs
@@ -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);
}
diff --git a/LegacyForge.API/CreativeTab.cs b/LegacyForge.API/CreativeTab.cs
new file mode 100644
index 0000000..d28f0c3
--- /dev/null
+++ b/LegacyForge.API/CreativeTab.cs
@@ -0,0 +1,21 @@
+namespace LegacyForge.API;
+
+///
+/// Creative inventory tabs matching the game's internal group indices.
+/// Use with or
+/// to make content
+/// appear in the creative menu.
+///
+public enum CreativeTab
+{
+ None = -1,
+ BuildingBlocks = 0,
+ Decoration = 1,
+ Redstone = 2,
+ Transport = 3,
+ Materials = 4,
+ Food = 5,
+ ToolsAndWeapons = 6,
+ Brewing = 7,
+ Misc = 12
+}
diff --git a/LegacyForge.API/Item/ItemProperties.cs b/LegacyForge.API/Item/ItemProperties.cs
index 2db1eac..ed57e9a 100644
--- a/LegacyForge.API/Item/ItemProperties.cs
+++ b/LegacyForge.API/Item/ItemProperties.cs
@@ -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.
///
public ItemProperties MaxDamage(int damage) { MaxDamageValue = damage; MaxStackSizeValue = 1; return this; }
+ public ItemProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
}
diff --git a/LegacyForge.API/Item/ItemRegistry.cs b/LegacyForge.API/Item/ItemRegistry.cs
index 90721ac..c6c765a 100644
--- a/LegacyForge.API/Item/ItemRegistry.cs
+++ b/LegacyForge.API/Item/ItemRegistry.cs
@@ -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);
}
diff --git a/LegacyForge.API/LegacyForge.API.csproj b/LegacyForge.API/LegacyForge.API.csproj
index f58a748..e5777cb 100644
--- a/LegacyForge.API/LegacyForge.API.csproj
+++ b/LegacyForge.API/LegacyForge.API.csproj
@@ -8,6 +8,9 @@
LegacyForge.API
Mod API for LegacyForge - Minecraft Legacy Edition mod loader
1.0.0
+ ..\build
+ false
+ false
diff --git a/LegacyForge.API/Logger.cs b/LegacyForge.API/Logger.cs
index 5ff540c..b7ca514 100644
--- a/LegacyForge.API/Logger.cs
+++ b/LegacyForge.API/Logger.cs
@@ -13,7 +13,13 @@ public enum LogLevel
///
public static class Logger
{
- internal static Action? LogHandler;
+ private static Action? LogHandler;
+
+ ///
+ /// Set the log handler that routes messages to the native runtime.
+ /// Called by LegacyForge.Core during initialization.
+ ///
+ public static void SetLogHandler(Action handler) => LogHandler = handler;
public static void Debug(string message) => Log(message, LogLevel.Debug);
public static void Info(string message) => Log(message, LogLevel.Info);
diff --git a/LegacyForge.API/NativeInterop.cs b/LegacyForge.API/NativeInterop.cs
index abe69f3..93c4b70 100644
--- a/LegacyForge.API/NativeInterop.cs
+++ b/LegacyForge.API/NativeInterop.cs
@@ -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);
}
diff --git a/LegacyForge.Core/LegacyForge.Core.csproj b/LegacyForge.Core/LegacyForge.Core.csproj
index 66d72a5..f49193d 100644
--- a/LegacyForge.Core/LegacyForge.Core.csproj
+++ b/LegacyForge.Core/LegacyForge.Core.csproj
@@ -8,6 +8,9 @@
LegacyForge.Core
LegacyForge core runtime - mod discovery and lifecycle management
1.0.0
+ ..\build
+ false
+ false
diff --git a/LegacyForge.Core/LegacyForgeCore.cs b/LegacyForge.Core/LegacyForgeCore.cs
index c17d50e..a68c8a6 100644
--- a/LegacyForge.Core/LegacyForgeCore.cs
+++ b/LegacyForge.Core/LegacyForgeCore.cs
@@ -3,27 +3,17 @@ using LegacyForge.API;
namespace LegacyForge.Core;
-///
-/// 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);
-///
public static class LegacyForgeCore
{
private static ModManager? _modManager;
private static bool _initialized;
- ///
- /// Called once by C++ to initialize the managed runtime.
- /// Sets up the native log handler and prepares the mod manager.
- ///
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;
}
- ///
- /// Called by C++ to discover and load mod assemblies from the mods/ directory.
- /// The mods path is passed as a UTF-8 string pointer.
- ///
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;
+ }
}
- /// Called before MinecraftWorld_RunStaticCtors.
public static int PreInit(IntPtr args, int sizeBytes)
{
_modManager?.PreInit();
return 0;
}
- /// Called after MinecraftWorld_RunStaticCtors.
public static int Init(IntPtr args, int sizeBytes)
{
_modManager?.Init();
return 0;
}
- /// Called after Minecraft::init completes.
public static int PostInit(IntPtr args, int sizeBytes)
{
_modManager?.PostInit();
return 0;
}
- /// Called each game tick from the Minecraft::tick hook.
public static int Tick(IntPtr args, int sizeBytes)
{
_modManager?.Tick();
return 0;
}
- /// Called from the Minecraft::destroy hook during shutdown.
public static int Shutdown(IntPtr args, int sizeBytes)
{
_modManager?.Shutdown();
diff --git a/LegacyForge.Core/ModDiscovery.cs b/LegacyForge.Core/ModDiscovery.cs
index d6b1075..2b36b01 100644
--- a/LegacyForge.Core/ModDiscovery.cs
+++ b/LegacyForge.Core/ModDiscovery.cs
@@ -4,10 +4,6 @@ using LegacyForge.API;
namespace LegacyForge.Core;
-///
-/// Discovers and loads mod assemblies from the mods/ directory.
-/// Each mod is loaded into its own AssemblyLoadContext for isolation.
-///
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();
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})");
diff --git a/LegacyForge.Launcher/FileDialog.cs b/LegacyForge.Launcher/FileDialog.cs
new file mode 100644
index 0000000..f4c0d32
--- /dev/null
+++ b/LegacyForge.Launcher/FileDialog.cs
@@ -0,0 +1,61 @@
+using System.Runtime.InteropServices;
+
+namespace LegacyForge.Launcher;
+
+///
+/// Thin wrapper around the Win32 GetOpenFileName API.
+/// No WinForms/WPF dependency required.
+///
+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;
+ }
+}
diff --git a/LegacyForge.Launcher/Injector.cs b/LegacyForge.Launcher/Injector.cs
index c9c153a..15ca492 100644
--- a/LegacyForge.Launcher/Injector.cs
+++ b/LegacyForge.Launcher/Injector.cs
@@ -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
}
diff --git a/LegacyForge.Launcher/LegacyForge.Launcher.csproj b/LegacyForge.Launcher/LegacyForge.Launcher.csproj
index b0ddf4f..7bd0744 100644
--- a/LegacyForge.Launcher/LegacyForge.Launcher.csproj
+++ b/LegacyForge.Launcher/LegacyForge.Launcher.csproj
@@ -9,6 +9,26 @@
LegacyForge
LegacyForge launcher - injects the mod loader runtime into Minecraft Legacy Edition
1.0.0
+ ..\build
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegacyForge.Launcher/Program.cs b/LegacyForge.Launcher/Program.cs
index ca6f770..1c3539d 100644
--- a/LegacyForge.Launcher/Program.cs
+++ b/LegacyForge.Launcher/Program.cs
@@ -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);
diff --git a/LegacyForgeRuntime/CMakeLists.txt b/LegacyForgeRuntime/CMakeLists.txt
index 06b99c7..9e4dcdd 100644
--- a/LegacyForgeRuntime/CMakeLists.txt
+++ b/LegacyForgeRuntime/CMakeLists.txt
@@ -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"
)
diff --git a/LegacyForgeRuntime/src/CreativeInventory.cpp b/LegacyForgeRuntime/src/CreativeInventory.cpp
new file mode 100644
index 0000000..8d2ec62
--- /dev/null
+++ b/LegacyForgeRuntime/src/CreativeInventory.cpp
@@ -0,0 +1,109 @@
+#include "CreativeInventory.h"
+#include "SymbolResolver.h"
+#include
+#include
+
+namespace CreativeInventory
+{
+
+void* pCategoryGroups = nullptr;
+void* pItemInstanceCtor = nullptr;
+void* pSharedPtrCtor = nullptr;
+void* pVectorPushBack = nullptr;
+
+std::vector 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 ctor @ %p\n", pSharedPtrCtor);
+ else printf("[LegacyForge] MISSING: shared_ptr(ItemInstance*)\n");
+
+ if (pVectorPushBack) printf("[LegacyForge] vector::push_back @ %p\n", pVectorPushBack);
+ else printf("[LegacyForge] MISSING: vector>::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(pItemInstanceCtor);
+ auto spCtorFn = reinterpret_cast(pSharedPtrCtor);
+ auto pushFn = reinterpret_cast(pVectorPushBack);
+
+ char* groups = reinterpret_cast(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
diff --git a/LegacyForgeRuntime/src/CreativeInventory.h b/LegacyForgeRuntime/src/CreativeInventory.h
new file mode 100644
index 0000000..f0829d0
--- /dev/null
+++ b/LegacyForgeRuntime/src/CreativeInventory.h
@@ -0,0 +1,26 @@
+#pragma once
+#include
+
+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 s_pendingItems;
+}
diff --git a/LegacyForgeRuntime/src/DotNetHost.cpp b/LegacyForgeRuntime/src/DotNetHost.cpp
index ffbfb52..8a2f141 100644
--- a/LegacyForgeRuntime/src/DotNetHost.cpp
+++ b/LegacyForgeRuntime/src/DotNetHost.cpp
@@ -1,6 +1,5 @@
#include "DotNetHost.h"
-#define WIN32_LEAN_AND_MEAN
#include
#include
#include
@@ -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(modsPath), static_cast(strlen(modsPath)));
+ return fn_DiscoverMods(const_cast(modsPath), static_cast(strlen(modsPath)));
+ return 0;
}
void DotNetHost::CallPreInit()
diff --git a/LegacyForgeRuntime/src/DotNetHost.h b/LegacyForgeRuntime/src/DotNetHost.h
index 52b64b6..33ee951 100644
--- a/LegacyForgeRuntime/src/DotNetHost.h
+++ b/LegacyForgeRuntime/src/DotNetHost.h
@@ -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();
diff --git a/LegacyForgeRuntime/src/GameHooks.cpp b/LegacyForgeRuntime/src/GameHooks.cpp
index 0baad5a..1a47424 100644
--- a/LegacyForgeRuntime/src/GameHooks.cpp
+++ b/LegacyForgeRuntime/src/GameHooks.cpp
@@ -1,13 +1,18 @@
#include "GameHooks.h"
#include "DotNetHost.h"
+#include "CreativeInventory.h"
+#include "MainMenuOverlay.h"
#include
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);
+ }
}
diff --git a/LegacyForgeRuntime/src/GameHooks.h b/LegacyForgeRuntime/src/GameHooks.h
index 53d59f3..e38808f 100644
--- a/LegacyForgeRuntime/src/GameHooks.h
+++ b/LegacyForgeRuntime/src/GameHooks.h
@@ -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);
}
diff --git a/LegacyForgeRuntime/src/HookManager.cpp b/LegacyForgeRuntime/src/HookManager.cpp
index 709e3f8..8d0a795 100644
--- a/LegacyForgeRuntime/src/HookManager.cpp
+++ b/LegacyForgeRuntime/src/HookManager.cpp
@@ -1,6 +1,8 @@
#include "HookManager.h"
#include "GameHooks.h"
#include "SymbolResolver.h"
+#include "CreativeInventory.h"
+#include "MainMenuOverlay.h"
#include
#include
@@ -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(symbols));
+
+ if (MH_CreateHook(symbols.pCreativeStaticCtor,
+ reinterpret_cast(&GameHooks::Hooked_CreativeStaticCtor),
+ reinterpret_cast(&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(&GameHooks::Hooked_MainMenuCustomDraw),
+ reinterpret_cast(&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(symbols));
+
+ if (MH_CreateHook(symbols.pPresent,
+ reinterpret_cast(&GameHooks::Hooked_Present),
+ reinterpret_cast(&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");
diff --git a/LegacyForgeRuntime/src/LogUtil.cpp b/LegacyForgeRuntime/src/LogUtil.cpp
new file mode 100644
index 0000000..1ac9de1
--- /dev/null
+++ b/LegacyForgeRuntime/src/LogUtil.cpp
@@ -0,0 +1,40 @@
+#include "LogUtil.h"
+#include
+#include
+#include
+
+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
diff --git a/LegacyForgeRuntime/src/LogUtil.h b/LegacyForgeRuntime/src/LogUtil.h
new file mode 100644
index 0000000..5d915c3
--- /dev/null
+++ b/LegacyForgeRuntime/src/LogUtil.h
@@ -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, ...);
+}
diff --git a/LegacyForgeRuntime/src/MainMenuOverlay.cpp b/LegacyForgeRuntime/src/MainMenuOverlay.cpp
new file mode 100644
index 0000000..abe667c
--- /dev/null
+++ b/LegacyForgeRuntime/src/MainMenuOverlay.cpp
@@ -0,0 +1,170 @@
+#include "MainMenuOverlay.h"
+#include "SymbolResolver.h"
+#include "LogUtil.h"
+#include
+#include
+#include
+#include
+
+#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
diff --git a/LegacyForgeRuntime/src/MainMenuOverlay.h b/LegacyForgeRuntime/src/MainMenuOverlay.h
new file mode 100644
index 0000000..873a0e8
--- /dev/null
+++ b/LegacyForgeRuntime/src/MainMenuOverlay.h
@@ -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();
+}
diff --git a/LegacyForgeRuntime/src/NativeExports.cpp b/LegacyForgeRuntime/src/NativeExports.cpp
index 04e3004..b4a65e6 100644
--- a/LegacyForgeRuntime/src/NativeExports.cpp
+++ b/LegacyForgeRuntime/src/NativeExports.cpp
@@ -1,5 +1,7 @@
#include "NativeExports.h"
#include "IdRegistry.h"
+#include "CreativeInventory.h"
+#include "LogUtil.h"
#include
#include
@@ -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"
diff --git a/LegacyForgeRuntime/src/NativeExports.h b/LegacyForgeRuntime/src/NativeExports.h
index 500e415..8b501b1 100644
--- a/LegacyForgeRuntime/src/NativeExports.h
+++ b/LegacyForgeRuntime/src/NativeExports.h
@@ -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);
}
diff --git a/LegacyForgeRuntime/src/PdbParser.cpp b/LegacyForgeRuntime/src/PdbParser.cpp
new file mode 100644
index 0000000..b1e7b03
--- /dev/null
+++ b/LegacyForgeRuntime/src/PdbParser.cpp
@@ -0,0 +1,377 @@
+#include "PdbParser.h"
+#include "LogUtil.h"
+#include
+#include
+
+#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(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 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 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 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 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 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 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
diff --git a/LegacyForgeRuntime/src/PdbParser.h b/LegacyForgeRuntime/src/PdbParser.h
new file mode 100644
index 0000000..074a0b1
--- /dev/null
+++ b/LegacyForgeRuntime/src/PdbParser.h
@@ -0,0 +1,15 @@
+#pragma once
+#include
+
+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();
+}
diff --git a/LegacyForgeRuntime/src/SymbolResolver.cpp b/LegacyForgeRuntime/src/SymbolResolver.cpp
index 8ffd76f..0953ec9 100644
--- a/LegacyForgeRuntime/src/SymbolResolver.cpp
+++ b/LegacyForgeRuntime/src/SymbolResolver.cpp
@@ -1,25 +1,43 @@
#include "SymbolResolver.h"
+#include "PdbParser.h"
+#include "LogUtil.h"
#include
#include
+#include
-#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(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(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(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(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(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;
}
diff --git a/LegacyForgeRuntime/src/SymbolResolver.h b/LegacyForgeRuntime/src/SymbolResolver.h
index fa84de9..eeb98d6 100644
--- a/LegacyForgeRuntime/src/SymbolResolver.h
+++ b/LegacyForgeRuntime/src/SymbolResolver.h
@@ -1,7 +1,5 @@
#pragma once
-#define WIN32_LEAN_AND_MEAN
#include
-#include
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;
};
diff --git a/LegacyForgeRuntime/src/dllmain.cpp b/LegacyForgeRuntime/src/dllmain.cpp
index d57f896..3f6e8a5 100644
--- a/LegacyForgeRuntime/src/dllmain.cpp
+++ b/LegacyForgeRuntime/src/dllmain.cpp
@@ -1,58 +1,81 @@
-#define WIN32_LEAN_AND_MEAN
#include
+#include
+#include
+#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;
}