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