mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-07-08 03:38:08 +00:00
Add main menu overlay, creative inventory injection, mod discovery, logging, and Wine/Proton support
- Replace dbghelp with raw_pdb library for cross-platform PDB symbol resolution - Add main menu branding overlay via C4JRender::Present hook - Add creative inventory item injection from mods - Add file-based logging (LogUtil) alongside console output - Fix mod discovery with custom AssemblyLoadContext for proper type identity - Add file dialog for game path selection in launcher - Add CreativeTab enum and block/item creative tab assignment - Unify build output to single ModLoader/build directory
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<AssemblyName>ExampleMod</AssemblyName>
|
||||
<Description>Example mod for LegacyForge demonstrating the mod API</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<OutputPath>..\build\mods</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
3
LegacyForge.API/AssemblyInfo.cs
Normal file
3
LegacyForge.API/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("LegacyForge.Core")]
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
21
LegacyForge.API/CreativeTab.cs
Normal file
21
LegacyForge.API/CreativeTab.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace LegacyForge.API;
|
||||
|
||||
/// <summary>
|
||||
/// Creative inventory tabs matching the game's internal group indices.
|
||||
/// Use with <see cref="Block.BlockProperties.CreativeTab"/> or
|
||||
/// <see cref="Item.ItemProperties.CreativeTab"/> to make content
|
||||
/// appear in the creative menu.
|
||||
/// </summary>
|
||||
public enum CreativeTab
|
||||
{
|
||||
None = -1,
|
||||
BuildingBlocks = 0,
|
||||
Decoration = 1,
|
||||
Redstone = 2,
|
||||
Transport = 3,
|
||||
Materials = 4,
|
||||
Food = 5,
|
||||
ToolsAndWeapons = 6,
|
||||
Brewing = 7,
|
||||
Misc = 12
|
||||
}
|
||||
@@ -7,6 +7,7 @@ public class ItemProperties
|
||||
{
|
||||
internal int MaxStackSizeValue = 64;
|
||||
internal int MaxDamageValue = 0;
|
||||
internal CreativeTab CreativeTabValue = CreativeTab.None;
|
||||
|
||||
public ItemProperties MaxStackSize(int size) { MaxStackSizeValue = size; return this; }
|
||||
|
||||
@@ -15,4 +16,5 @@ public class ItemProperties
|
||||
/// makes the item damageable with a durability bar.
|
||||
/// </summary>
|
||||
public ItemProperties MaxDamage(int damage) { MaxDamageValue = damage; MaxStackSizeValue = 1; return this; }
|
||||
public ItemProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<AssemblyName>LegacyForge.API</AssemblyName>
|
||||
<Description>Mod API for LegacyForge - Minecraft Legacy Edition mod loader</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<OutputPath>..\build</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,7 +13,13 @@ public enum LogLevel
|
||||
/// </summary>
|
||||
public static class Logger
|
||||
{
|
||||
internal static Action<string, LogLevel>? LogHandler;
|
||||
private static Action<string, LogLevel>? LogHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Set the log handler that routes messages to the native runtime.
|
||||
/// Called by LegacyForge.Core during initialization.
|
||||
/// </summary>
|
||||
public static void SetLogHandler(Action<string, LogLevel> handler) => LogHandler = handler;
|
||||
|
||||
public static void Debug(string message) => Log(message, LogLevel.Debug);
|
||||
public static void Info(string message) => Log(message, LogLevel.Info);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<AssemblyName>LegacyForge.Core</AssemblyName>
|
||||
<Description>LegacyForge core runtime - mod discovery and lifecycle management</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<OutputPath>..\build</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,27 +3,17 @@ using LegacyForge.API;
|
||||
|
||||
namespace LegacyForge.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point class loaded by the C++ DotNetHost via hostfxr.
|
||||
/// All public static methods here are resolved as function pointers from native code.
|
||||
/// Method signatures must match the component_entry_point_fn delegate:
|
||||
/// public delegate int ComponentEntryPoint(IntPtr args, int sizeBytes);
|
||||
/// </summary>
|
||||
public static class LegacyForgeCore
|
||||
{
|
||||
private static ModManager? _modManager;
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Called once by C++ to initialize the managed runtime.
|
||||
/// Sets up the native log handler and prepares the mod manager.
|
||||
/// </summary>
|
||||
public static int Initialize(IntPtr args, int sizeBytes)
|
||||
{
|
||||
if (_initialized) return 0;
|
||||
_initialized = true;
|
||||
|
||||
Logger.LogHandler = (message, level) =>
|
||||
Logger.SetLogHandler((message, level) =>
|
||||
{
|
||||
string formatted = $"[LegacyForge/{level}] {message}";
|
||||
try
|
||||
@@ -34,61 +24,68 @@ public static class LegacyForgeCore
|
||||
{
|
||||
Console.WriteLine(formatted);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Logger.Info("LegacyForge Core initialized");
|
||||
_modManager = new ModManager();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by C++ to discover and load mod assemblies from the mods/ directory.
|
||||
/// The mods path is passed as a UTF-8 string pointer.
|
||||
/// </summary>
|
||||
public static int DiscoverMods(IntPtr args, int sizeBytes)
|
||||
{
|
||||
string modsPath;
|
||||
if (args != IntPtr.Zero && sizeBytes > 0)
|
||||
modsPath = Marshal.PtrToStringUTF8(args, sizeBytes) ?? "mods";
|
||||
else
|
||||
modsPath = "mods";
|
||||
try
|
||||
{
|
||||
string modsPath;
|
||||
if (args != IntPtr.Zero && sizeBytes > 0)
|
||||
modsPath = Marshal.PtrToStringUTF8(args, sizeBytes) ?? "mods";
|
||||
else
|
||||
modsPath = "mods";
|
||||
|
||||
Logger.Info($"Discovering mods in: {modsPath}");
|
||||
var discovered = ModDiscovery.DiscoverMods(modsPath);
|
||||
_modManager?.AddMods(discovered);
|
||||
Logger.Info($"Loaded {discovered.Count} mod(s)");
|
||||
return discovered.Count;
|
||||
Logger.Info($"Discovering mods in: {modsPath}");
|
||||
Logger.Info($"Directory exists: {Directory.Exists(modsPath)}");
|
||||
|
||||
if (Directory.Exists(modsPath))
|
||||
{
|
||||
var files = Directory.GetFiles(modsPath, "*.dll");
|
||||
Logger.Info($"DLL files found: {string.Join(", ", files.Select(Path.GetFileName))}");
|
||||
}
|
||||
|
||||
var discovered = ModDiscovery.DiscoverMods(modsPath);
|
||||
_modManager?.AddMods(discovered);
|
||||
Logger.Info($"Loaded {discovered.Count} mod(s)");
|
||||
return discovered.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"DiscoverMods EXCEPTION: {ex}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called before MinecraftWorld_RunStaticCtors.</summary>
|
||||
public static int PreInit(IntPtr args, int sizeBytes)
|
||||
{
|
||||
_modManager?.PreInit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Called after MinecraftWorld_RunStaticCtors.</summary>
|
||||
public static int Init(IntPtr args, int sizeBytes)
|
||||
{
|
||||
_modManager?.Init();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Called after Minecraft::init completes.</summary>
|
||||
public static int PostInit(IntPtr args, int sizeBytes)
|
||||
{
|
||||
_modManager?.PostInit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Called each game tick from the Minecraft::tick hook.</summary>
|
||||
public static int Tick(IntPtr args, int sizeBytes)
|
||||
{
|
||||
_modManager?.Tick();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Called from the Minecraft::destroy hook during shutdown.</summary>
|
||||
public static int Shutdown(IntPtr args, int sizeBytes)
|
||||
{
|
||||
_modManager?.Shutdown();
|
||||
|
||||
@@ -4,10 +4,6 @@ using LegacyForge.API;
|
||||
|
||||
namespace LegacyForge.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and loads mod assemblies from the mods/ directory.
|
||||
/// Each mod is loaded into its own AssemblyLoadContext for isolation.
|
||||
/// </summary>
|
||||
internal static class ModDiscovery
|
||||
{
|
||||
internal record DiscoveredMod(
|
||||
@@ -30,6 +26,12 @@ internal static class ModDiscovery
|
||||
|
||||
foreach (var dllPath in dllFiles)
|
||||
{
|
||||
string fileName = Path.GetFileName(dllPath);
|
||||
|
||||
// Skip the API assembly -- it's a shared dependency, not a mod
|
||||
if (fileName.Equals("LegacyForge.API.dll", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var discovered = LoadModAssembly(dllPath);
|
||||
@@ -37,7 +39,7 @@ internal static class ModDiscovery
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to load mod from {Path.GetFileName(dllPath)}: {ex.Message}");
|
||||
Logger.Error($"Failed to load mod from {fileName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +50,18 @@ internal static class ModDiscovery
|
||||
{
|
||||
var results = new List<DiscoveredMod>();
|
||||
var fileName = Path.GetFileName(dllPath);
|
||||
var fullPath = Path.GetFullPath(dllPath);
|
||||
|
||||
var loadContext = new AssemblyLoadContext(fileName, isCollectible: false);
|
||||
var assembly = loadContext.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
|
||||
// Load into the SAME ALC that LegacyForge.Core lives in (the hostfxr component context).
|
||||
// This ensures LegacyForge.API types (IMod, ModAttribute, etc.) have the same identity.
|
||||
var coreContext = AssemblyLoadContext.GetLoadContext(typeof(ModDiscovery).Assembly)
|
||||
?? AssemblyLoadContext.Default;
|
||||
var assembly = coreContext.LoadFromAssemblyPath(fullPath);
|
||||
|
||||
var modTypes = assembly.GetTypes()
|
||||
var allTypes = assembly.GetTypes();
|
||||
Logger.Debug($"{fileName}: {allTypes.Length} type(s), checking for IMod implementations...");
|
||||
|
||||
var modTypes = allTypes
|
||||
.Where(t => t.IsClass && !t.IsAbstract && typeof(IMod).IsAssignableFrom(t));
|
||||
|
||||
foreach (var type in modTypes)
|
||||
@@ -67,7 +76,7 @@ internal static class ModDiscovery
|
||||
try
|
||||
{
|
||||
var instance = (IMod)Activator.CreateInstance(type)!;
|
||||
results.Add(new ModDiscovery.DiscoveredMod(instance, attr, assembly));
|
||||
results.Add(new DiscoveredMod(instance, attr, assembly));
|
||||
|
||||
string name = string.IsNullOrEmpty(attr.Name) ? attr.Id : attr.Name;
|
||||
Logger.Info($"Discovered mod: {name} v{attr.Version} by {attr.Author} ({fileName})");
|
||||
|
||||
61
LegacyForge.Launcher/FileDialog.cs
Normal file
61
LegacyForge.Launcher/FileDialog.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LegacyForge.Launcher;
|
||||
|
||||
/// <summary>
|
||||
/// Thin wrapper around the Win32 GetOpenFileName API.
|
||||
/// No WinForms/WPF dependency required.
|
||||
/// </summary>
|
||||
internal static class FileDialog
|
||||
{
|
||||
public static string? OpenFileDialog(string title, string filter)
|
||||
{
|
||||
var ofn = new OpenFileName();
|
||||
ofn.lStructSize = Marshal.SizeOf(ofn);
|
||||
ofn.lpstrFilter = filter;
|
||||
ofn.lpstrFile = new string('\0', 260);
|
||||
ofn.nMaxFile = 260;
|
||||
ofn.lpstrTitle = title;
|
||||
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
|
||||
if (GetOpenFileName(ofn))
|
||||
return ofn.lpstrFile.TrimEnd('\0');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private const int OFN_PATHMUSTEXIST = 0x00000800;
|
||||
private const int OFN_FILEMUSTEXIST = 0x00001000;
|
||||
private const int OFN_NOCHANGEDIR = 0x00000008;
|
||||
|
||||
[DllImport("comdlg32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool GetOpenFileName([In, Out] OpenFileName ofn);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private class OpenFileName
|
||||
{
|
||||
public int lStructSize;
|
||||
public IntPtr hwndOwner;
|
||||
public IntPtr hInstance;
|
||||
public string? lpstrFilter;
|
||||
public string? lpstrCustomFilter;
|
||||
public int nMaxCustFilter;
|
||||
public int nFilterIndex;
|
||||
public string? lpstrFile;
|
||||
public int nMaxFile;
|
||||
public string? lpstrFileTitle;
|
||||
public int nMaxFileTitle;
|
||||
public string? lpstrInitialDir;
|
||||
public string? lpstrTitle;
|
||||
public int Flags;
|
||||
public short nFileOffset;
|
||||
public short nFileExtension;
|
||||
public string? lpstrDefExt;
|
||||
public IntPtr lCustData;
|
||||
public IntPtr lpfnHook;
|
||||
public string? lpTemplateName;
|
||||
public IntPtr pvReserved;
|
||||
public int dwReserved;
|
||||
public int FlagsEx;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,6 +9,26 @@
|
||||
<AssemblyName>LegacyForge</AssemblyName>
|
||||
<Description>LegacyForge launcher - injects the mod loader runtime into Minecraft Legacy Edition</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<OutputPath>..\build</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Build the C++ runtime DLL via CMake before the launcher -->
|
||||
<Target Name="BuildNativeRuntime" BeforeTargets="Build">
|
||||
<Message Importance="high" Text="[LegacyForge] Configuring C++ runtime with CMake..." />
|
||||
<Exec Command="cmake -B "$(MSBuildThisFileDirectory)..\LegacyForgeRuntime\build" -S "$(MSBuildThisFileDirectory)..\LegacyForgeRuntime" -A x64"
|
||||
WorkingDirectory="$(MSBuildThisFileDirectory)..\LegacyForgeRuntime" />
|
||||
<Message Importance="high" Text="[LegacyForge] Building C++ runtime..." />
|
||||
<Exec Command="cmake --build "$(MSBuildThisFileDirectory)..\LegacyForgeRuntime\build" --config $(Configuration)"
|
||||
WorkingDirectory="$(MSBuildThisFileDirectory)..\LegacyForgeRuntime" />
|
||||
</Target>
|
||||
|
||||
<!-- Copy the runtimeconfig.json to the build output -->
|
||||
<Target Name="CopyRuntimeConfig" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\LegacyForge.Core\LegacyForge.Core.runtimeconfig.json"
|
||||
DestinationFolder="$(OutputPath)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
109
LegacyForgeRuntime/src/CreativeInventory.cpp
Normal file
109
LegacyForgeRuntime/src/CreativeInventory.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "CreativeInventory.h"
|
||||
#include "SymbolResolver.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace CreativeInventory
|
||||
{
|
||||
|
||||
void* pCategoryGroups = nullptr;
|
||||
void* pItemInstanceCtor = nullptr;
|
||||
void* pSharedPtrCtor = nullptr;
|
||||
void* pVectorPushBack = nullptr;
|
||||
|
||||
std::vector<PendingCreativeItem> s_pendingItems;
|
||||
|
||||
static const int CREATIVE_GROUP_COUNT = 15;
|
||||
static const int SIZEOF_MSVC_VECTOR = 24;
|
||||
static const int SIZEOF_MSVC_SHARED_PTR = 16;
|
||||
static const int ITEMINSTANCE_ALLOC_SIZE = 256;
|
||||
|
||||
typedef void (__fastcall *ItemInstanceCtor_fn)(void* thisPtr, int id, int count, int auxValue);
|
||||
typedef void (__fastcall *SharedPtrCtor_fn)(void* sharedPtrThis, void* rawItemPtr);
|
||||
typedef void (__fastcall *VectorPushBackMove_fn)(void* vectorThis, void* sharedPtrRvalueRef);
|
||||
|
||||
void AddPending(int itemId, int count, int auxValue, int groupIndex)
|
||||
{
|
||||
s_pendingItems.push_back({ itemId, count, auxValue, groupIndex });
|
||||
printf("[LegacyForge] Queued creative item: id=%d group=%d\n", itemId, groupIndex);
|
||||
}
|
||||
|
||||
bool ResolveSymbols(SymbolResolver& resolver)
|
||||
{
|
||||
pCategoryGroups = resolver.Resolve(
|
||||
"?categoryGroups@IUIScene_CreativeMenu@@1PAV?$vector@V?$shared_ptr@VItemInstance@@@std@@"
|
||||
"V?$allocator@V?$shared_ptr@VItemInstance@@@std@@@2@@std@@A");
|
||||
|
||||
pItemInstanceCtor = resolver.Resolve("??0ItemInstance@@QEAA@HHH@Z");
|
||||
|
||||
pSharedPtrCtor = resolver.Resolve(
|
||||
"??$?0VItemInstance@@$0A@@?$shared_ptr@VItemInstance@@@std@@QEAA@PEAVItemInstance@@@Z");
|
||||
|
||||
pVectorPushBack = resolver.Resolve(
|
||||
"?push_back@?$vector@V?$shared_ptr@VItemInstance@@@std@@V?$allocator@V?$shared_ptr@VItemInstance@@@std@@@2@@std@@"
|
||||
"QEAAX$$QEAV?$shared_ptr@VItemInstance@@@2@@Z");
|
||||
|
||||
if (pCategoryGroups) printf("[LegacyForge] categoryGroups @ %p\n", pCategoryGroups);
|
||||
else printf("[LegacyForge] MISSING: categoryGroups\n");
|
||||
|
||||
if (pItemInstanceCtor) printf("[LegacyForge] ItemInstance ctor @ %p\n", pItemInstanceCtor);
|
||||
else printf("[LegacyForge] MISSING: ItemInstance(int,int,int)\n");
|
||||
|
||||
if (pSharedPtrCtor) printf("[LegacyForge] shared_ptr<II> ctor @ %p\n", pSharedPtrCtor);
|
||||
else printf("[LegacyForge] MISSING: shared_ptr<ItemInstance>(ItemInstance*)\n");
|
||||
|
||||
if (pVectorPushBack) printf("[LegacyForge] vector::push_back @ %p\n", pVectorPushBack);
|
||||
else printf("[LegacyForge] MISSING: vector<shared_ptr<II>>::push_back\n");
|
||||
|
||||
return pCategoryGroups && pItemInstanceCtor && pSharedPtrCtor && pVectorPushBack;
|
||||
}
|
||||
|
||||
void InjectItems()
|
||||
{
|
||||
if (!pCategoryGroups || !pItemInstanceCtor || !pSharedPtrCtor || !pVectorPushBack)
|
||||
{
|
||||
printf("[LegacyForge] Cannot inject creative items: missing symbols\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_pendingItems.empty())
|
||||
{
|
||||
printf("[LegacyForge] No creative items to inject\n");
|
||||
return;
|
||||
}
|
||||
|
||||
auto ctorFn = reinterpret_cast<ItemInstanceCtor_fn>(pItemInstanceCtor);
|
||||
auto spCtorFn = reinterpret_cast<SharedPtrCtor_fn>(pSharedPtrCtor);
|
||||
auto pushFn = reinterpret_cast<VectorPushBackMove_fn>(pVectorPushBack);
|
||||
|
||||
char* groups = reinterpret_cast<char*>(pCategoryGroups);
|
||||
|
||||
for (auto& item : s_pendingItems)
|
||||
{
|
||||
if (item.groupIndex < 0 || item.groupIndex >= CREATIVE_GROUP_COUNT)
|
||||
{
|
||||
printf("[LegacyForge] Skipping creative item id=%d: invalid group %d\n",
|
||||
item.itemId, item.groupIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
void* rawItem = ::operator new(ITEMINSTANCE_ALLOC_SIZE);
|
||||
memset(rawItem, 0, ITEMINSTANCE_ALLOC_SIZE);
|
||||
ctorFn(rawItem, item.itemId, item.count, item.auxValue);
|
||||
|
||||
char spBuf[16];
|
||||
memset(spBuf, 0, sizeof(spBuf));
|
||||
spCtorFn(spBuf, rawItem);
|
||||
|
||||
char* vec = groups + item.groupIndex * SIZEOF_MSVC_VECTOR;
|
||||
pushFn(vec, spBuf);
|
||||
|
||||
printf("[LegacyForge] Injected item id=%d into creative group %d\n",
|
||||
item.itemId, item.groupIndex);
|
||||
}
|
||||
|
||||
printf("[LegacyForge] Injected %zu items into creative inventory\n", s_pendingItems.size());
|
||||
s_pendingItems.clear();
|
||||
}
|
||||
|
||||
} // namespace CreativeInventory
|
||||
26
LegacyForgeRuntime/src/CreativeInventory.h
Normal file
26
LegacyForgeRuntime/src/CreativeInventory.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
|
||||
class SymbolResolver;
|
||||
|
||||
struct PendingCreativeItem
|
||||
{
|
||||
int itemId;
|
||||
int count;
|
||||
int auxValue;
|
||||
int groupIndex;
|
||||
};
|
||||
|
||||
namespace CreativeInventory
|
||||
{
|
||||
void AddPending(int itemId, int count, int auxValue, int groupIndex);
|
||||
bool ResolveSymbols(SymbolResolver& resolver);
|
||||
void InjectItems();
|
||||
|
||||
extern void* pCategoryGroups;
|
||||
extern void* pItemInstanceCtor;
|
||||
extern void* pSharedPtrCtor;
|
||||
extern void* pVectorPushBack;
|
||||
|
||||
extern std::vector<PendingCreativeItem> s_pendingItems;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "DotNetHost.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <nethost.h>
|
||||
#include <hostfxr.h>
|
||||
@@ -162,10 +161,11 @@ void DotNetHost::CallManagedInit()
|
||||
fn_Initialize(nullptr, 0);
|
||||
}
|
||||
|
||||
void DotNetHost::CallDiscoverMods(const char* modsPath)
|
||||
int DotNetHost::CallDiscoverMods(const char* modsPath)
|
||||
{
|
||||
if (fn_DiscoverMods)
|
||||
fn_DiscoverMods(const_cast<char*>(modsPath), static_cast<int>(strlen(modsPath)));
|
||||
return fn_DiscoverMods(const_cast<char*>(modsPath), static_cast<int>(strlen(modsPath)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DotNetHost::CallPreInit()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
#include "GameHooks.h"
|
||||
#include "DotNetHost.h"
|
||||
#include "CreativeInventory.h"
|
||||
#include "MainMenuOverlay.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace GameHooks
|
||||
{
|
||||
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
|
||||
MinecraftTick_fn Original_MinecraftTick = nullptr;
|
||||
MinecraftInit_fn Original_MinecraftInit = nullptr;
|
||||
ExitGame_fn Original_ExitGame = nullptr;
|
||||
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
|
||||
MinecraftTick_fn Original_MinecraftTick = nullptr;
|
||||
MinecraftInit_fn Original_MinecraftInit = nullptr;
|
||||
ExitGame_fn Original_ExitGame = nullptr;
|
||||
CreativeStaticCtor_fn Original_CreativeStaticCtor = nullptr;
|
||||
MainMenuCustomDraw_fn Original_MainMenuCustomDraw = nullptr;
|
||||
Present_fn Original_Present = nullptr;
|
||||
|
||||
void Hooked_RunStaticCtors()
|
||||
{
|
||||
@@ -45,4 +50,25 @@ namespace GameHooks
|
||||
|
||||
Original_ExitGame(thisPtr);
|
||||
}
|
||||
|
||||
void Hooked_CreativeStaticCtor()
|
||||
{
|
||||
printf("[LegacyForge] Hook: CreativeStaticCtor -- building vanilla creative lists\n");
|
||||
Original_CreativeStaticCtor();
|
||||
|
||||
printf("[LegacyForge] Hook: CreativeStaticCtor -- injecting modded items\n");
|
||||
CreativeInventory::InjectItems();
|
||||
}
|
||||
|
||||
void __fastcall Hooked_MainMenuCustomDraw(void* thisPtr, void* region)
|
||||
{
|
||||
MainMenuOverlay::NotifyOnMainMenu();
|
||||
Original_MainMenuCustomDraw(thisPtr, region);
|
||||
}
|
||||
|
||||
void __fastcall Hooked_Present(void* thisPtr)
|
||||
{
|
||||
MainMenuOverlay::RenderBranding();
|
||||
Original_Present(thisPtr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "HookManager.h"
|
||||
#include "GameHooks.h"
|
||||
#include "SymbolResolver.h"
|
||||
#include "CreativeInventory.h"
|
||||
#include "MainMenuOverlay.h"
|
||||
#include <MinHook.h>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -51,7 +53,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
|
||||
printf("[LegacyForge] Hooked Minecraft::init\n");
|
||||
}
|
||||
|
||||
// Hook CConsoleMinecraftApp::ExitGame (optional -- for graceful shutdown)
|
||||
// Hook CConsoleMinecraftApp::ExitGame
|
||||
if (symbols.pExitGame)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pExitGame,
|
||||
@@ -66,6 +68,55 @@ bool HookManager::Install(const SymbolResolver& symbols)
|
||||
}
|
||||
}
|
||||
|
||||
// Hook IUIScene_CreativeMenu::staticCtor
|
||||
if (symbols.pCreativeStaticCtor)
|
||||
{
|
||||
CreativeInventory::ResolveSymbols(const_cast<SymbolResolver&>(symbols));
|
||||
|
||||
if (MH_CreateHook(symbols.pCreativeStaticCtor,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_CreativeStaticCtor),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_CreativeStaticCtor)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Warning: Failed to hook CreativeStaticCtor\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("[LegacyForge] Hooked CreativeStaticCtor\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Hook UIScene_MainMenu::customDraw (sets main-menu detection flag)
|
||||
if (symbols.pMainMenuCustomDraw)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pMainMenuCustomDraw,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_MainMenuCustomDraw),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_MainMenuCustomDraw)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Warning: Failed to hook MainMenuCustomDraw\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("[LegacyForge] Hooked MainMenuCustomDraw\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Hook C4JRender::Present (draws branding overlay just before frame present)
|
||||
if (symbols.pPresent)
|
||||
{
|
||||
MainMenuOverlay::ResolveSymbols(const_cast<SymbolResolver&>(symbols));
|
||||
|
||||
if (MH_CreateHook(symbols.pPresent,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_Present),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_Present)) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] Warning: Failed to hook C4JRender::Present\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("[LegacyForge] Hooked C4JRender::Present\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK)
|
||||
{
|
||||
printf("[LegacyForge] MH_EnableHook(MH_ALL_HOOKS) failed\n");
|
||||
|
||||
40
LegacyForgeRuntime/src/LogUtil.cpp
Normal file
40
LegacyForgeRuntime/src/LogUtil.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "LogUtil.h"
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <string>
|
||||
|
||||
static std::string s_logPath;
|
||||
|
||||
namespace LogUtil
|
||||
{
|
||||
|
||||
void SetBaseDir(const char* baseDir)
|
||||
{
|
||||
s_logPath = std::string(baseDir) + "legacyforge.log";
|
||||
}
|
||||
|
||||
void Log(const char* fmt, ...)
|
||||
{
|
||||
// Also print to stdout for console visibility
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vprintf(fmt, args);
|
||||
va_end(args);
|
||||
printf("\n");
|
||||
|
||||
if (s_logPath.empty()) return;
|
||||
|
||||
FILE* f = nullptr;
|
||||
fopen_s(&f, s_logPath.c_str(), "a");
|
||||
if (f)
|
||||
{
|
||||
va_list args2;
|
||||
va_start(args2, fmt);
|
||||
vfprintf(f, fmt, args2);
|
||||
va_end(args2);
|
||||
fprintf(f, "\n");
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace LogUtil
|
||||
10
LegacyForgeRuntime/src/LogUtil.h
Normal file
10
LegacyForgeRuntime/src/LogUtil.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
namespace LogUtil
|
||||
{
|
||||
// Must be called once at startup with the runtime DLL's directory (with trailing backslash)
|
||||
void SetBaseDir(const char* baseDir);
|
||||
|
||||
// Appends a line to legacyforge.log in the base directory
|
||||
void Log(const char* fmt, ...);
|
||||
}
|
||||
170
LegacyForgeRuntime/src/MainMenuOverlay.cpp
Normal file
170
LegacyForgeRuntime/src/MainMenuOverlay.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
#include "MainMenuOverlay.h"
|
||||
#include "SymbolResolver.h"
|
||||
#include "LogUtil.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#define LEGACYFORGE_VERSION L"1.0.0"
|
||||
|
||||
namespace MainMenuOverlay
|
||||
{
|
||||
|
||||
static int s_modCount = 0;
|
||||
static bool s_onMainMenu = false;
|
||||
static bool s_loggedOnce = false;
|
||||
static bool s_symbolsOk = false;
|
||||
|
||||
// ── Resolved PDB addresses ──────────────────────────────────────────────
|
||||
static void** pMinecraftInstance = nullptr;
|
||||
static void* pRenderManager = nullptr;
|
||||
|
||||
// C4JRender member functions
|
||||
typedef void (__fastcall *VoidMethod_fn)(void*);
|
||||
typedef void (__fastcall *MatrixMode_fn)(void*, int);
|
||||
typedef void (__fastcall *MatrixOrthogonal_fn)(void*, float, float, float, float, float, float);
|
||||
typedef void (__fastcall *MatrixTranslate_fn)(void*, float, float, float);
|
||||
typedef void (__fastcall *BoolMethod_fn)(void*, bool);
|
||||
|
||||
static VoidMethod_fn fnStartFrame = nullptr;
|
||||
static VoidMethod_fn fnMatrixPush = nullptr;
|
||||
static VoidMethod_fn fnMatrixPop = nullptr;
|
||||
static MatrixMode_fn fnMatrixMode = nullptr;
|
||||
static VoidMethod_fn fnMatrixSetIdentity = nullptr;
|
||||
static MatrixOrthogonal_fn fnMatrixOrthogonal = nullptr;
|
||||
static MatrixTranslate_fn fnMatrixTranslate = nullptr;
|
||||
static BoolMethod_fn fnDepthTest = nullptr;
|
||||
static BoolMethod_fn fnBlendEnable = nullptr;
|
||||
static VoidMethod_fn fnSetMatrixDirty = nullptr;
|
||||
|
||||
// Font::drawShadow(const wstring&, int x, int y, int color)
|
||||
typedef void (__fastcall *FontDrawShadow_fn)(void*, const std::wstring&, int, int, int);
|
||||
static FontDrawShadow_fn fnDrawShadow = nullptr;
|
||||
|
||||
static const int C4J_GL_MODELVIEW = 0;
|
||||
static const int C4J_GL_PROJECTION = 1;
|
||||
|
||||
// Minecraft instance member offsets (no vtable, no base class)
|
||||
static const int OFF_WIDTH_PHYS = 32;
|
||||
static const int OFF_HEIGHT_PHYS = 36;
|
||||
static const int OFF_FONT = 432; // Font* at 0x1B0
|
||||
|
||||
static int ComputeGuiScale(int pixelW, int pixelH)
|
||||
{
|
||||
int scale = 1;
|
||||
while (pixelW / (scale + 1) >= 320 && pixelH / (scale + 1) >= 240)
|
||||
scale++;
|
||||
return scale;
|
||||
}
|
||||
|
||||
bool ResolveSymbols(SymbolResolver& resolver)
|
||||
{
|
||||
pMinecraftInstance = (void**)resolver.Resolve("?m_instance@Minecraft@@0PEAV1@EA");
|
||||
pRenderManager = resolver.Resolve("?RenderManager@@3VC4JRender@@A");
|
||||
|
||||
fnStartFrame = (VoidMethod_fn) resolver.Resolve("?StartFrame@C4JRender@@QEAAXXZ");
|
||||
fnMatrixPush = (VoidMethod_fn) resolver.Resolve("?MatrixPush@C4JRender@@QEAAXXZ");
|
||||
fnMatrixPop = (VoidMethod_fn) resolver.Resolve("?MatrixPop@C4JRender@@QEAAXXZ");
|
||||
fnMatrixMode = (MatrixMode_fn) resolver.Resolve("?MatrixMode@C4JRender@@QEAAXH@Z");
|
||||
fnMatrixSetIdentity= (VoidMethod_fn) resolver.Resolve("?MatrixSetIdentity@C4JRender@@QEAAXXZ");
|
||||
fnMatrixOrthogonal = (MatrixOrthogonal_fn)resolver.Resolve("?MatrixOrthogonal@C4JRender@@QEAAXMMMMMM@Z");
|
||||
fnMatrixTranslate = (MatrixTranslate_fn) resolver.Resolve("?MatrixTranslate@C4JRender@@QEAAXMMM@Z");
|
||||
fnDepthTest = (BoolMethod_fn) resolver.Resolve("?StateSetDepthTestEnable@C4JRender@@QEAAX_N@Z");
|
||||
fnBlendEnable = (BoolMethod_fn) resolver.Resolve("?StateSetBlendEnable@C4JRender@@QEAAX_N@Z");
|
||||
fnSetMatrixDirty = (VoidMethod_fn) resolver.Resolve("?Set_matrixDirty@C4JRender@@QEAAXXZ");
|
||||
|
||||
fnDrawShadow = (FontDrawShadow_fn)resolver.Resolve(
|
||||
"?drawShadow@Font@@QEAAXAEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@HHH@Z");
|
||||
|
||||
s_symbolsOk = pMinecraftInstance && pRenderManager && fnStartFrame &&
|
||||
fnMatrixPush && fnMatrixPop && fnMatrixMode &&
|
||||
fnMatrixSetIdentity && fnMatrixOrthogonal &&
|
||||
fnMatrixTranslate && fnDepthTest && fnDrawShadow;
|
||||
|
||||
if (s_symbolsOk)
|
||||
LogUtil::Log("[LegacyForge] MainMenuOverlay symbols resolved OK");
|
||||
else
|
||||
LogUtil::Log("[LegacyForge] Warning: some MainMenuOverlay symbols missing -- branding text disabled");
|
||||
|
||||
return s_symbolsOk;
|
||||
}
|
||||
|
||||
void SetModCount(int count) { s_modCount = count; }
|
||||
int GetModCount() { return s_modCount; }
|
||||
|
||||
void NotifyOnMainMenu()
|
||||
{
|
||||
s_onMainMenu = true;
|
||||
}
|
||||
|
||||
void RenderBranding()
|
||||
{
|
||||
if (!s_onMainMenu || !s_symbolsOk) return;
|
||||
s_onMainMenu = false;
|
||||
|
||||
void* mc = *pMinecraftInstance;
|
||||
if (!mc) return;
|
||||
|
||||
int pixelW = *(int*)((char*)mc + OFF_WIDTH_PHYS);
|
||||
int pixelH = *(int*)((char*)mc + OFF_HEIGHT_PHYS);
|
||||
if (pixelW <= 0 || pixelH <= 0 || pixelW > 8192 || pixelH > 8192) return;
|
||||
|
||||
void* font = *(void**)((char*)mc + OFF_FONT);
|
||||
if (!font) return;
|
||||
|
||||
if (!s_loggedOnce)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] MainMenuOverlay: first render (screen %dx%d, font=%p, rm=%p)",
|
||||
pixelW, pixelH, font, pRenderManager);
|
||||
s_loggedOnce = true;
|
||||
}
|
||||
|
||||
int guiScale = ComputeGuiScale(pixelW, pixelH);
|
||||
int guiW = (int)ceil((double)pixelW / guiScale);
|
||||
int guiH = (int)ceil((double)pixelH / guiScale);
|
||||
|
||||
void* rm = pRenderManager;
|
||||
|
||||
// Re-initialize C4JRender's D3D11 state (shaders, render targets, etc.)
|
||||
// After GDraw's NoMoreGDrawThisFrame, C4JRender's shaders are not bound.
|
||||
fnStartFrame(rm);
|
||||
fnSetMatrixDirty(rm);
|
||||
|
||||
// Set up 2D orthographic projection
|
||||
fnMatrixMode(rm, C4J_GL_PROJECTION);
|
||||
fnMatrixPush(rm);
|
||||
fnMatrixSetIdentity(rm);
|
||||
fnMatrixOrthogonal(rm, 0.0f, (float)guiW, (float)guiH, 0.0f, 1000.0f, 3000.0f);
|
||||
|
||||
fnMatrixMode(rm, C4J_GL_MODELVIEW);
|
||||
fnMatrixPush(rm);
|
||||
fnMatrixSetIdentity(rm);
|
||||
fnMatrixTranslate(rm, 0.0f, 0.0f, -2000.0f);
|
||||
|
||||
fnDepthTest(rm, false);
|
||||
if (fnBlendEnable) fnBlendEnable(rm, true);
|
||||
|
||||
std::wstring line1 = L"LegacyForge v" LEGACYFORGE_VERSION;
|
||||
|
||||
wchar_t line2Buf[64];
|
||||
swprintf(line2Buf, 64, L"%d mod(s) loaded successfully", s_modCount);
|
||||
std::wstring line2(line2Buf);
|
||||
|
||||
int textX = 2;
|
||||
int textY1 = guiH - 20;
|
||||
int textY2 = guiH - 10;
|
||||
int color = (int)0xFFFFFFFF;
|
||||
|
||||
fnDrawShadow(font, line1, textX, textY1, color);
|
||||
fnDrawShadow(font, line2, textX, textY2, color);
|
||||
|
||||
fnDepthTest(rm, true);
|
||||
|
||||
fnMatrixMode(rm, C4J_GL_MODELVIEW);
|
||||
fnMatrixPop(rm);
|
||||
fnMatrixMode(rm, C4J_GL_PROJECTION);
|
||||
fnMatrixPop(rm);
|
||||
}
|
||||
|
||||
} // namespace MainMenuOverlay
|
||||
17
LegacyForgeRuntime/src/MainMenuOverlay.h
Normal file
17
LegacyForgeRuntime/src/MainMenuOverlay.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
class SymbolResolver;
|
||||
|
||||
namespace MainMenuOverlay
|
||||
{
|
||||
bool ResolveSymbols(SymbolResolver& resolver);
|
||||
|
||||
// Called from the customDraw hook to signal we're on the main menu this frame
|
||||
void NotifyOnMainMenu();
|
||||
|
||||
// Called from the Present hook, draws branding if on the main menu
|
||||
void RenderBranding();
|
||||
|
||||
void SetModCount(int count);
|
||||
int GetModCount();
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "NativeExports.h"
|
||||
#include "IdRegistry.h"
|
||||
#include "CreativeInventory.h"
|
||||
#include "LogUtil.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -98,7 +100,7 @@ void native_add_furnace_recipe(
|
||||
void native_log(const char* message, int level)
|
||||
{
|
||||
if (message)
|
||||
printf("%s\n", message);
|
||||
LogUtil::Log("%s", message);
|
||||
}
|
||||
|
||||
int native_get_block_id(const char* namespacedId)
|
||||
@@ -125,4 +127,9 @@ void native_subscribe_event(const char* eventName, void* managedFnPtr)
|
||||
printf("[LegacyForge] Event subscription: %s\n", eventName ? eventName : "(null)");
|
||||
}
|
||||
|
||||
void native_add_to_creative(int numericId, int count, int auxValue, int groupIndex)
|
||||
{
|
||||
CreativeInventory::AddPending(numericId, count, auxValue, groupIndex);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
377
LegacyForgeRuntime/src/PdbParser.cpp
Normal file
377
LegacyForgeRuntime/src/PdbParser.cpp
Normal file
@@ -0,0 +1,377 @@
|
||||
#include "PdbParser.h"
|
||||
#include "LogUtil.h"
|
||||
#include <Windows.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "PDB.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_InfoStream.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
#include "PDB_PublicSymbolStream.h"
|
||||
#include "PDB_GlobalSymbolStream.h"
|
||||
#include "PDB_ImageSectionStream.h"
|
||||
#include "PDB_CoalescedMSFStream.h"
|
||||
#include "PDB_ModuleInfoStream.h"
|
||||
#include "PDB_ModuleSymbolStream.h"
|
||||
|
||||
struct MappedFile
|
||||
{
|
||||
HANDLE hFile = INVALID_HANDLE_VALUE;
|
||||
HANDLE hMapping = nullptr;
|
||||
void* baseAddress = nullptr;
|
||||
size_t fileSize = 0;
|
||||
};
|
||||
|
||||
static MappedFile s_mapped;
|
||||
static bool s_open = false;
|
||||
|
||||
static PDB::RawFile* s_rawFile = nullptr;
|
||||
static PDB::DBIStream* s_dbiStream = nullptr;
|
||||
static PDB::ImageSectionStream* s_sectionStream = nullptr;
|
||||
static PDB::PublicSymbolStream* s_publicStream = nullptr;
|
||||
static PDB::GlobalSymbolStream* s_globalStream = nullptr;
|
||||
static PDB::ModuleInfoStream* s_moduleStream = nullptr;
|
||||
static PDB::CoalescedMSFStream* s_symbolRecords = nullptr;
|
||||
|
||||
static void CloseMappedFile(MappedFile& mf)
|
||||
{
|
||||
if (mf.baseAddress) { UnmapViewOfFile(mf.baseAddress); mf.baseAddress = nullptr; }
|
||||
if (mf.hMapping) { CloseHandle(mf.hMapping); mf.hMapping = nullptr; }
|
||||
if (mf.hFile != INVALID_HANDLE_VALUE) { CloseHandle(mf.hFile); mf.hFile = INVALID_HANDLE_VALUE; }
|
||||
mf.fileSize = 0;
|
||||
}
|
||||
|
||||
static bool OpenMappedFile(const char* path, MappedFile& mf)
|
||||
{
|
||||
mf.hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
|
||||
if (mf.hFile == INVALID_HANDLE_VALUE) return false;
|
||||
|
||||
mf.hMapping = CreateFileMappingW(mf.hFile, nullptr, PAGE_READONLY, 0, 0, nullptr);
|
||||
if (!mf.hMapping) { CloseMappedFile(mf); return false; }
|
||||
|
||||
mf.baseAddress = MapViewOfFile(mf.hMapping, FILE_MAP_READ, 0, 0, 0);
|
||||
if (!mf.baseAddress) { CloseMappedFile(mf); return false; }
|
||||
|
||||
BY_HANDLE_FILE_INFORMATION fi;
|
||||
if (!GetFileInformationByHandle(mf.hFile, &fi)) { CloseMappedFile(mf); return false; }
|
||||
mf.fileSize = (static_cast<size_t>(fi.nFileSizeHigh) << 32) | fi.nFileSizeLow;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helpers to extract name + section/offset from various global symbol record kinds
|
||||
static const char* GetGlobalSymName(const PDB::CodeView::DBI::Record* record,
|
||||
uint16_t& outSection, uint32_t& outOffset)
|
||||
{
|
||||
switch (record->header.kind)
|
||||
{
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32:
|
||||
outSection = record->data.S_GDATA32.section;
|
||||
outOffset = record->data.S_GDATA32.offset;
|
||||
return record->data.S_GDATA32.name;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32:
|
||||
outSection = record->data.S_LDATA32.section;
|
||||
outOffset = record->data.S_LDATA32.offset;
|
||||
return record->data.S_LDATA32.name;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32:
|
||||
outSection = record->data.S_GTHREAD32.section;
|
||||
outOffset = record->data.S_GTHREAD32.offset;
|
||||
return record->data.S_GTHREAD32.name;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32:
|
||||
outSection = record->data.S_LTHREAD32.section;
|
||||
outOffset = record->data.S_LTHREAD32.offset;
|
||||
return record->data.S_LTHREAD32.name;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
namespace PdbParser
|
||||
{
|
||||
|
||||
bool Open(const char* pdbPath)
|
||||
{
|
||||
if (s_open) Close();
|
||||
|
||||
LogUtil::Log("[LegacyForge] PdbParser: opening %s", pdbPath);
|
||||
|
||||
if (!OpenMappedFile(pdbPath, s_mapped))
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: failed to memory-map PDB file");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PDB::ValidateFile(s_mapped.baseAddress, s_mapped.fileSize) != PDB::ErrorCode::Success)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: PDB validation failed");
|
||||
CloseMappedFile(s_mapped);
|
||||
return false;
|
||||
}
|
||||
|
||||
s_rawFile = new PDB::RawFile(PDB::CreateRawFile(s_mapped.baseAddress));
|
||||
|
||||
if (PDB::HasValidDBIStream(*s_rawFile) != PDB::ErrorCode::Success)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: invalid DBI stream");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const PDB::InfoStream infoStream(*s_rawFile);
|
||||
if (infoStream.UsesDebugFastLink())
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: PDB uses unsupported /DEBUG:FASTLINK");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
s_dbiStream = new PDB::DBIStream(PDB::CreateDBIStream(*s_rawFile));
|
||||
|
||||
if (s_dbiStream->HasValidImageSectionStream(*s_rawFile) != PDB::ErrorCode::Success)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: invalid image section stream");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (s_dbiStream->HasValidPublicSymbolStream(*s_rawFile) != PDB::ErrorCode::Success)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: invalid public symbol stream");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (s_dbiStream->HasValidGlobalSymbolStream(*s_rawFile) != PDB::ErrorCode::Success)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: invalid global symbol stream");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
if (s_dbiStream->HasValidSymbolRecordStream(*s_rawFile) != PDB::ErrorCode::Success)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] PdbParser: invalid symbol record stream");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
s_sectionStream = new PDB::ImageSectionStream(s_dbiStream->CreateImageSectionStream(*s_rawFile));
|
||||
s_symbolRecords = new PDB::CoalescedMSFStream(s_dbiStream->CreateSymbolRecordStream(*s_rawFile));
|
||||
s_publicStream = new PDB::PublicSymbolStream(s_dbiStream->CreatePublicSymbolStream(*s_rawFile));
|
||||
s_globalStream = new PDB::GlobalSymbolStream(s_dbiStream->CreateGlobalSymbolStream(*s_rawFile));
|
||||
s_moduleStream = new PDB::ModuleInfoStream(s_dbiStream->CreateModuleInfoStream(*s_rawFile));
|
||||
|
||||
s_open = true;
|
||||
LogUtil::Log("[LegacyForge] PdbParser: PDB opened successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t FindSymbolRVA(const char* decoratedName)
|
||||
{
|
||||
if (!s_open) return 0;
|
||||
|
||||
// 1) Search public symbol stream (S_PUB32)
|
||||
{
|
||||
const PDB::ArrayView<PDB::HashRecord> records = s_publicStream->GetRecords();
|
||||
for (const PDB::HashRecord& hashRecord : records)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = s_publicStream->GetRecord(*s_symbolRecords, hashRecord);
|
||||
if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
|
||||
continue;
|
||||
|
||||
if (strcmp(record->data.S_PUB32.name, decoratedName) == 0)
|
||||
{
|
||||
return s_sectionStream->ConvertSectionOffsetToRVA(
|
||||
record->data.S_PUB32.section,
|
||||
record->data.S_PUB32.offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Search global symbol stream (S_GDATA32, S_LDATA32, S_GTHREAD32, S_LTHREAD32)
|
||||
{
|
||||
const PDB::ArrayView<PDB::HashRecord> records = s_globalStream->GetRecords();
|
||||
for (const PDB::HashRecord& hashRecord : records)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = s_globalStream->GetRecord(*s_symbolRecords, hashRecord);
|
||||
uint16_t section = 0;
|
||||
uint32_t offset = 0;
|
||||
const char* name = GetGlobalSymName(record, section, offset);
|
||||
if (name && strcmp(name, decoratedName) == 0)
|
||||
{
|
||||
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
|
||||
if (rva != 0) return rva;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Search per-module symbol streams (S_LPROC32, S_GPROC32, S_LPROC32_ID, S_GPROC32_ID, S_LDATA32, S_GDATA32)
|
||||
{
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = s_moduleStream->GetModules();
|
||||
for (const PDB::ModuleInfoStream::Module& mod : modules)
|
||||
{
|
||||
if (!mod.HasSymbolStream())
|
||||
continue;
|
||||
|
||||
const PDB::ModuleSymbolStream modSymStream = mod.CreateSymbolStream(*s_rawFile);
|
||||
uint32_t foundRVA = 0;
|
||||
|
||||
modSymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* record)
|
||||
{
|
||||
if (foundRVA != 0) return;
|
||||
|
||||
const char* name = nullptr;
|
||||
uint16_t section = 0;
|
||||
uint32_t offset = 0;
|
||||
|
||||
switch (record->header.kind)
|
||||
{
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32:
|
||||
name = record->data.S_LPROC32.name;
|
||||
section = record->data.S_LPROC32.section;
|
||||
offset = record->data.S_LPROC32.offset;
|
||||
break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32:
|
||||
name = record->data.S_GPROC32.name;
|
||||
section = record->data.S_GPROC32.section;
|
||||
offset = record->data.S_GPROC32.offset;
|
||||
break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID:
|
||||
name = record->data.S_LPROC32_ID.name;
|
||||
section = record->data.S_LPROC32_ID.section;
|
||||
offset = record->data.S_LPROC32_ID.offset;
|
||||
break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID:
|
||||
name = record->data.S_GPROC32_ID.name;
|
||||
section = record->data.S_GPROC32_ID.section;
|
||||
offset = record->data.S_GPROC32_ID.offset;
|
||||
break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32:
|
||||
name = record->data.S_LDATA32.name;
|
||||
section = record->data.S_LDATA32.section;
|
||||
offset = record->data.S_LDATA32.offset;
|
||||
break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32:
|
||||
name = record->data.S_GDATA32.name;
|
||||
section = record->data.S_GDATA32.section;
|
||||
offset = record->data.S_GDATA32.offset;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (name && strcmp(name, decoratedName) == 0)
|
||||
{
|
||||
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
|
||||
if (rva != 0) foundRVA = rva;
|
||||
}
|
||||
});
|
||||
|
||||
if (foundRVA != 0) return foundRVA;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DumpMatching(const char* substring)
|
||||
{
|
||||
if (!s_open) return;
|
||||
|
||||
LogUtil::Log("[LegacyForge] PdbParser: dumping symbols containing '%s'...", substring);
|
||||
int count = 0;
|
||||
|
||||
// Public symbols
|
||||
{
|
||||
const PDB::ArrayView<PDB::HashRecord> records = s_publicStream->GetRecords();
|
||||
for (const PDB::HashRecord& hashRecord : records)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = s_publicStream->GetRecord(*s_symbolRecords, hashRecord);
|
||||
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
|
||||
{
|
||||
if (strstr(record->data.S_PUB32.name, substring))
|
||||
{
|
||||
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(
|
||||
record->data.S_PUB32.section, record->data.S_PUB32.offset);
|
||||
LogUtil::Log(" [PUB] rva=0x%08X %s", rva, record->data.S_PUB32.name);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global symbols
|
||||
{
|
||||
const PDB::ArrayView<PDB::HashRecord> records = s_globalStream->GetRecords();
|
||||
for (const PDB::HashRecord& hashRecord : records)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = s_globalStream->GetRecord(*s_symbolRecords, hashRecord);
|
||||
uint16_t section = 0;
|
||||
uint32_t offset = 0;
|
||||
const char* name = GetGlobalSymName(record, section, offset);
|
||||
if (name && strstr(name, substring))
|
||||
{
|
||||
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
|
||||
LogUtil::Log(" [GLB kind=0x%04X] rva=0x%08X %s",
|
||||
(unsigned)record->header.kind, rva, name);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Module symbols
|
||||
{
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = s_moduleStream->GetModules();
|
||||
for (const PDB::ModuleInfoStream::Module& mod : modules)
|
||||
{
|
||||
if (!mod.HasSymbolStream()) continue;
|
||||
const PDB::ModuleSymbolStream modSymStream = mod.CreateSymbolStream(*s_rawFile);
|
||||
modSymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* record)
|
||||
{
|
||||
const char* name = nullptr;
|
||||
uint16_t section = 0;
|
||||
uint32_t offset = 0;
|
||||
|
||||
switch (record->header.kind)
|
||||
{
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32:
|
||||
name = record->data.S_LPROC32.name; section = record->data.S_LPROC32.section; offset = record->data.S_LPROC32.offset; break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32:
|
||||
name = record->data.S_GPROC32.name; section = record->data.S_GPROC32.section; offset = record->data.S_GPROC32.offset; break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID:
|
||||
name = record->data.S_LPROC32_ID.name; section = record->data.S_LPROC32_ID.section; offset = record->data.S_LPROC32_ID.offset; break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID:
|
||||
name = record->data.S_GPROC32_ID.name; section = record->data.S_GPROC32_ID.section; offset = record->data.S_GPROC32_ID.offset; break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32:
|
||||
name = record->data.S_LDATA32.name; section = record->data.S_LDATA32.section; offset = record->data.S_LDATA32.offset; break;
|
||||
case PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32:
|
||||
name = record->data.S_GDATA32.name; section = record->data.S_GDATA32.section; offset = record->data.S_GDATA32.offset; break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (name && strstr(name, substring))
|
||||
{
|
||||
uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset);
|
||||
LogUtil::Log(" [MOD kind=0x%04X] rva=0x%08X %s",
|
||||
(unsigned)record->header.kind, rva, name);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LogUtil::Log("[LegacyForge] PdbParser: found %d matching symbols", count);
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
delete s_moduleStream; s_moduleStream = nullptr;
|
||||
delete s_globalStream; s_globalStream = nullptr;
|
||||
delete s_publicStream; s_publicStream = nullptr;
|
||||
delete s_symbolRecords; s_symbolRecords = nullptr;
|
||||
delete s_sectionStream; s_sectionStream = nullptr;
|
||||
delete s_dbiStream; s_dbiStream = nullptr;
|
||||
delete s_rawFile; s_rawFile = nullptr;
|
||||
CloseMappedFile(s_mapped);
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
} // namespace PdbParser
|
||||
15
LegacyForgeRuntime/src/PdbParser.h
Normal file
15
LegacyForgeRuntime/src/PdbParser.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace PdbParser
|
||||
{
|
||||
bool Open(const char* pdbPath);
|
||||
|
||||
// Returns the RVA for a decorated symbol name, or 0 on failure.
|
||||
uint32_t FindSymbolRVA(const char* decoratedName);
|
||||
|
||||
// Logs all symbols whose name contains the given substring (for debugging).
|
||||
void DumpMatching(const char* substring);
|
||||
|
||||
void Close();
|
||||
}
|
||||
@@ -1,25 +1,43 @@
|
||||
#include "SymbolResolver.h"
|
||||
#include "PdbParser.h"
|
||||
#include "LogUtil.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#pragma comment(lib, "dbghelp.lib")
|
||||
|
||||
// Exact MSVC-decorated symbol names from the game's PDB.
|
||||
// These are verified against the actual Minecraft.Client.pdb.
|
||||
static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ";
|
||||
static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z";
|
||||
static const char* SYM_MINECRAFT_INIT = "?init@Minecraft@@QEAAXXZ";
|
||||
static const char* SYM_EXIT_GAME = "?ExitGame@CConsoleMinecraftApp@@UEAAXXZ";
|
||||
static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ";
|
||||
static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z";
|
||||
static const char* SYM_MINECRAFT_INIT = "?init@Minecraft@@QEAAXXZ";
|
||||
static const char* SYM_EXIT_GAME = "?ExitGame@CConsoleMinecraftApp@@UEAAXXZ";
|
||||
static const char* SYM_CREATIVE_STATIC_CTOR = "?staticCtor@IUIScene_CreativeMenu@@SAXXZ";
|
||||
static const char* SYM_MAINMENU_CUSTOMDRAW = "?customDraw@UIScene_MainMenu@@UEAAXPEAUIggyCustomDrawCallbackRegion@@@Z";
|
||||
static const char* SYM_PRESENT = "?Present@C4JRender@@QEAAXXZ";
|
||||
|
||||
bool SymbolResolver::Initialize()
|
||||
{
|
||||
m_process = GetCurrentProcess();
|
||||
|
||||
SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_DEBUG);
|
||||
|
||||
if (!SymInitialize(m_process, nullptr, TRUE))
|
||||
m_moduleBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
|
||||
if (!m_moduleBase)
|
||||
{
|
||||
printf("[LegacyForge] SymInitialize failed (error %lu)\n", GetLastError());
|
||||
LogUtil::Log("[LegacyForge] Failed to get module base address");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Derive PDB path from executable path: replace .exe with .pdb
|
||||
char exePath[MAX_PATH] = {0};
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
std::string pdbPath(exePath);
|
||||
size_t dotPos = pdbPath.rfind('.');
|
||||
if (dotPos != std::string::npos)
|
||||
pdbPath = pdbPath.substr(0, dotPos) + ".pdb";
|
||||
else
|
||||
pdbPath += ".pdb";
|
||||
|
||||
LogUtil::Log("[LegacyForge] PDB path: %s", pdbPath.c_str());
|
||||
LogUtil::Log("[LegacyForge] Module base: %p", reinterpret_cast<void*>(m_moduleBase));
|
||||
|
||||
if (!PdbParser::Open(pdbPath.c_str()))
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] ERROR: Failed to open PDB file");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,50 +49,54 @@ void* SymbolResolver::Resolve(const char* decoratedName)
|
||||
{
|
||||
if (!m_initialized) return nullptr;
|
||||
|
||||
char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(char)];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
SYMBOL_INFO* symbol = reinterpret_cast<SYMBOL_INFO*>(buffer);
|
||||
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
symbol->MaxNameLen = MAX_SYM_NAME;
|
||||
|
||||
if (SymFromName(m_process, decoratedName, symbol))
|
||||
uint32_t rva = PdbParser::FindSymbolRVA(decoratedName);
|
||||
if (rva == 0)
|
||||
{
|
||||
return reinterpret_cast<void*>(symbol->Address);
|
||||
LogUtil::Log("[LegacyForge] Symbol not found in PDB: '%s'", decoratedName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
printf("[LegacyForge] SymFromName failed for '%s' (error %lu)\n", decoratedName, GetLastError());
|
||||
return nullptr;
|
||||
return reinterpret_cast<void*>(m_moduleBase + rva);
|
||||
}
|
||||
|
||||
bool SymbolResolver::ResolveGameFunctions()
|
||||
{
|
||||
pRunStaticCtors = Resolve(SYM_RUN_STATIC_CTORS);
|
||||
pMinecraftTick = Resolve(SYM_MINECRAFT_TICK);
|
||||
pMinecraftInit = Resolve(SYM_MINECRAFT_INIT);
|
||||
pExitGame = Resolve(SYM_EXIT_GAME);
|
||||
LogUtil::Log("[LegacyForge] Resolving game functions via raw PDB parser...");
|
||||
|
||||
if (pRunStaticCtors) printf("[LegacyForge] RunStaticCtors @ %p\n", pRunStaticCtors);
|
||||
else printf("[LegacyForge] MISSING: MinecraftWorld_RunStaticCtors\n");
|
||||
pRunStaticCtors = Resolve(SYM_RUN_STATIC_CTORS);
|
||||
pMinecraftTick = Resolve(SYM_MINECRAFT_TICK);
|
||||
pMinecraftInit = Resolve(SYM_MINECRAFT_INIT);
|
||||
pExitGame = Resolve(SYM_EXIT_GAME);
|
||||
pCreativeStaticCtor = Resolve(SYM_CREATIVE_STATIC_CTOR);
|
||||
pMainMenuCustomDraw = Resolve(SYM_MAINMENU_CUSTOMDRAW);
|
||||
pPresent = Resolve(SYM_PRESENT);
|
||||
|
||||
if (pMinecraftTick) printf("[LegacyForge] Minecraft::tick @ %p\n", pMinecraftTick);
|
||||
else printf("[LegacyForge] MISSING: Minecraft::tick\n");
|
||||
auto logSym = [](const char* name, void* ptr) {
|
||||
if (ptr)
|
||||
LogUtil::Log("[LegacyForge] %-25s @ %p", name, ptr);
|
||||
else
|
||||
LogUtil::Log("[LegacyForge] MISSING: %s", name);
|
||||
};
|
||||
|
||||
if (pMinecraftInit) printf("[LegacyForge] Minecraft::init @ %p\n", pMinecraftInit);
|
||||
else printf("[LegacyForge] MISSING: Minecraft::init\n");
|
||||
logSym("RunStaticCtors", pRunStaticCtors);
|
||||
logSym("Minecraft::tick", pMinecraftTick);
|
||||
logSym("Minecraft::init", pMinecraftInit);
|
||||
logSym("ExitGame", pExitGame);
|
||||
logSym("CreativeStaticCtor", pCreativeStaticCtor);
|
||||
logSym("MainMenuCustomDraw", pMainMenuCustomDraw);
|
||||
logSym("C4JRender::Present", pPresent);
|
||||
|
||||
if (pExitGame) printf("[LegacyForge] ExitGame @ %p\n", pExitGame);
|
||||
else printf("[LegacyForge] MISSING: CConsoleMinecraftApp::ExitGame\n");
|
||||
bool ok = pRunStaticCtors && pMinecraftTick && pMinecraftInit;
|
||||
if (ok)
|
||||
LogUtil::Log("[LegacyForge] All critical symbols resolved (via raw PDB parser)");
|
||||
else
|
||||
LogUtil::Log("[LegacyForge] CRITICAL symbols missing - hooks will not be installed");
|
||||
|
||||
// RunStaticCtors, tick, and init are critical. ExitGame is optional (graceful shutdown).
|
||||
return pRunStaticCtors && pMinecraftTick && pMinecraftInit;
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SymbolResolver::Cleanup()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
SymCleanup(m_process);
|
||||
m_initialized = false;
|
||||
}
|
||||
PdbParser::Close();
|
||||
m_initialized = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <DbgHelp.h>
|
||||
|
||||
class SymbolResolver
|
||||
{
|
||||
@@ -10,14 +8,17 @@ public:
|
||||
bool ResolveGameFunctions();
|
||||
void Cleanup();
|
||||
|
||||
void* Resolve(const char* functionName);
|
||||
void* Resolve(const char* decoratedName);
|
||||
|
||||
void* pRunStaticCtors = nullptr; // MinecraftWorld_RunStaticCtors
|
||||
void* pMinecraftTick = nullptr; // Minecraft::tick(bool, bool)
|
||||
void* pMinecraftInit = nullptr; // Minecraft::init()
|
||||
void* pExitGame = nullptr; // CConsoleMinecraftApp::ExitGame()
|
||||
void* pRunStaticCtors = nullptr; // MinecraftWorld_RunStaticCtors
|
||||
void* pMinecraftTick = nullptr; // Minecraft::tick(bool, bool)
|
||||
void* pMinecraftInit = nullptr; // Minecraft::init()
|
||||
void* pExitGame = nullptr; // CConsoleMinecraftApp::ExitGame()
|
||||
void* pCreativeStaticCtor = nullptr; // IUIScene_CreativeMenu::staticCtor()
|
||||
void* pMainMenuCustomDraw = nullptr; // UIScene_MainMenu::customDraw()
|
||||
void* pPresent = nullptr; // C4JRender::Present()
|
||||
|
||||
private:
|
||||
HANDLE m_process = nullptr;
|
||||
uintptr_t m_moduleBase = 0;
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
@@ -1,58 +1,81 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include "LogUtil.h"
|
||||
#include "SymbolResolver.h"
|
||||
#include "HookManager.h"
|
||||
#include "DotNetHost.h"
|
||||
#include "MainMenuOverlay.h"
|
||||
|
||||
static HMODULE g_hModule = nullptr;
|
||||
|
||||
static void LogToFile(const char* msg)
|
||||
static std::string GetDllDirectory(HMODULE hModule)
|
||||
{
|
||||
FILE* f = nullptr;
|
||||
fopen_s(&f, "legacyforge.log", "a");
|
||||
if (f)
|
||||
{
|
||||
fprintf(f, "%s\n", msg);
|
||||
fclose(f);
|
||||
}
|
||||
char path[MAX_PATH] = {0};
|
||||
GetModuleFileNameA(hModule, path, MAX_PATH);
|
||||
std::string s(path);
|
||||
size_t pos = s.find_last_of("\\/");
|
||||
if (pos != std::string::npos)
|
||||
return s.substr(0, pos + 1);
|
||||
return ".\\";
|
||||
}
|
||||
|
||||
DWORD WINAPI InitThread(LPVOID lpParam)
|
||||
{
|
||||
LogToFile("[LegacyForge] InitThread started");
|
||||
std::string baseDir = GetDllDirectory(g_hModule);
|
||||
LogUtil::SetBaseDir(baseDir.c_str());
|
||||
|
||||
LogUtil::Log("[LegacyForge] InitThread started (module=%p)", g_hModule);
|
||||
LogUtil::Log("[LegacyForge] Runtime DLL directory: %s", baseDir.c_str());
|
||||
|
||||
char cwd[MAX_PATH] = {0};
|
||||
GetCurrentDirectoryA(MAX_PATH, cwd);
|
||||
LogUtil::Log("[LegacyForge] Game working directory: %s", cwd);
|
||||
|
||||
char exePath[MAX_PATH] = {0};
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
LogUtil::Log("[LegacyForge] Host executable: %s", exePath);
|
||||
|
||||
SymbolResolver symbols;
|
||||
if (!symbols.Initialize())
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to initialize symbol resolver. Is the PDB present?");
|
||||
LogUtil::Log("[LegacyForge] ERROR: Failed to initialize symbol resolver. Is the PDB present?");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] Symbol resolver initialized");
|
||||
LogUtil::Log("[LegacyForge] Symbol resolver initialized");
|
||||
|
||||
if (!symbols.ResolveGameFunctions())
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to resolve one or more game functions");
|
||||
LogUtil::Log("[LegacyForge] ERROR: Failed to resolve critical game functions.");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] Game functions resolved from PDB");
|
||||
LogUtil::Log("[LegacyForge] Game functions resolved from PDB");
|
||||
|
||||
if (!HookManager::Install(symbols))
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to install hooks");
|
||||
LogUtil::Log("[LegacyForge] ERROR: Failed to install hooks");
|
||||
symbols.Cleanup();
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] Hooks installed");
|
||||
LogUtil::Log("[LegacyForge] Hooks installed");
|
||||
|
||||
// All symbol resolution is complete; release the PDB memory map
|
||||
symbols.Cleanup();
|
||||
|
||||
if (!DotNetHost::Initialize())
|
||||
{
|
||||
LogToFile("[LegacyForge] ERROR: Failed to initialize .NET host");
|
||||
LogUtil::Log("[LegacyForge] ERROR: Failed to initialize .NET host");
|
||||
return 1;
|
||||
}
|
||||
LogToFile("[LegacyForge] .NET runtime initialized");
|
||||
LogUtil::Log("[LegacyForge] .NET runtime initialized");
|
||||
|
||||
std::string modsPath = baseDir + "mods";
|
||||
LogUtil::Log("[LegacyForge] Mods directory: %s", modsPath.c_str());
|
||||
|
||||
DotNetHost::CallManagedInit();
|
||||
DotNetHost::CallDiscoverMods("mods");
|
||||
LogToFile("[LegacyForge] Mod discovery complete. Ready.");
|
||||
int modCount = DotNetHost::CallDiscoverMods(modsPath.c_str());
|
||||
MainMenuOverlay::SetModCount(modCount);
|
||||
LogUtil::Log("[LegacyForge] Mod discovery complete (%d mods). Ready.", modCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user