using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading; using Dalamud.Game; using Dalamud.Interface.Internal; using Newtonsoft.Json; using Reloaded.Memory.Buffers; using Serilog; using Serilog.Core; using Serilog.Events; using static Dalamud.Injector.NativeFunctions; namespace Dalamud.Injector { /// /// Entrypoint to the program. /// public sealed class EntryPoint { /// /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot. /// /// Count of arguments. /// char** string arguments. public delegate void MainDelegate(int argc, IntPtr argvPtr); /// /// Start the Dalamud injector. /// /// Count of arguments. /// byte** string arguments. public static void Main(int argc, IntPtr argvPtr) { InitUnhandledException(); InitLogging(); var args = new string[argc]; unsafe { var argv = (IntPtr*)argvPtr; for (var i = 0; i < argc; i++) { args[i] = Marshal.PtrToStringUni(argv[i]); } } var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; if (cwd.FullName != Directory.GetCurrentDirectory()) { Log.Debug($"Changing cwd to {cwd}"); Directory.SetCurrentDirectory(cwd.FullName); } var process = GetProcess(args.ElementAtOrDefault(1)); var startInfo = GetStartInfo(args.ElementAtOrDefault(2), process); startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); // This seems to help with the STATUS_INTERNAL_ERROR condition Thread.Sleep(1000); Inject(process, startInfo); Thread.Sleep(1000); } private static void InitUnhandledException() { AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => { if (Log.Logger == null) { Console.WriteLine($"A fatal error has occurred: {eventArgs.ExceptionObject}"); } else { var exObj = eventArgs.ExceptionObject; if (exObj is Exception ex) { Log.Error(ex, "A fatal error has occurred."); } else { Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}"); } } #if DEBUG var caption = "Debug Error"; var message = $"Couldn't inject.\nMake sure that Dalamud was not injected into your target process " + $"as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" + $"{eventArgs.ExceptionObject}"; #else var caption = "XIVLauncher Error"; var message = "Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\n" + "If this keeps happening, please report this error."; #endif _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok); Environment.Exit(0); }; } private static void InitLogging() { var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); #if DEBUG var logPath = Path.Combine(baseDirectory, "injector.log"); #else var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.injector.log"); #endif var levelSwitch = new LoggingLevelSwitch(); #if DEBUG levelSwitch.MinimumLevel = LogEventLevel.Verbose; #else levelSwitch.MinimumLevel = LogEventLevel.Information; #endif Log.Logger = new LoggerConfiguration() .WriteTo.Async(a => a.File(logPath)) .WriteTo.Sink(SerilogEventSink.Instance) .MinimumLevel.ControlledBy(levelSwitch) .CreateLogger(); } private static Process GetProcess(string arg) { Process process; var pid = -1; if (arg != default) { pid = int.Parse(arg); } switch (pid) { case -1: process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault(); if (process == default) { throw new Exception("Could not find process"); } break; case -2: var exePath = "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe"; var exeArgs = new StringBuilder() .Append("DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 ") .Append("DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 ") .Append("DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 ") .Append("DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 ") .Append("DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 ") .Append("DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 ") .Append("DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 ") .Append("DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 ") .Append("DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 ") .Append("SYS.Region=0 language=1 version=1.0.0.0 ") .Append("DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0").ToString(); process = Process.Start(exePath, exeArgs); Thread.Sleep(1000); break; default: process = Process.GetProcessById(pid); break; } return process; } private static DalamudStartInfo GetStartInfo(string arg, Process process) { DalamudStartInfo startInfo; if (arg != default) { startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(arg))); } else { var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName); var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); var gameVer = GameVersion.Parse(gameVerStr); startInfo = new DalamudStartInfo { WorkingDirectory = null, ConfigurationPath = Path.Combine(xivlauncherDir, "dalamudConfig.json"), PluginDirectory = Path.Combine(xivlauncherDir, "installedPlugins"), DefaultPluginDirectory = Path.Combine(xivlauncherDir, "devPlugins"), AssetDirectory = Path.Combine(xivlauncherDir, "dalamudAssets"), GameVersion = gameVer, Language = ClientLanguage.English, OptOutMbCollection = false, }; Log.Debug( "Creating a new StartInfo with:\n" + $" WorkingDirectory: {startInfo.WorkingDirectory}\n" + $" ConfigurationPath: {startInfo.ConfigurationPath}\n" + $" PluginDirectory: {startInfo.PluginDirectory}\n" + $" DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" + $" AssetDirectory: {startInfo.AssetDirectory}\n" + $" GameVersion: {startInfo.GameVersion}\n" + $" Language: {startInfo.Language}\n" + $" OptOutMbCollection: {startInfo.OptOutMbCollection}"); Log.Information("A Dalamud start info was not found in the program arguments. One has been generated for you."); Log.Information("Copy the following contents into the program arguments:"); var startInfoJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo))); Log.Information(startInfoJson); } return startInfo; } private static void Inject(Process process, DalamudStartInfo startInfo) { var nethostName = "nethost.dll"; var bootName = "Dalamud.Boot.dll"; var nethostPath = Path.GetFullPath(nethostName); var bootPath = Path.GetFullPath(bootName); // ====================================================== using var injector = new Injector(process); injector.LoadLibrary(nethostPath, out _); injector.LoadLibrary(bootPath, out var bootModule); // ====================================================== var startInfoJson = JsonConvert.SerializeObject(startInfo); var startInfoBytes = Encoding.UTF8.GetBytes(startInfoJson); using var startInfoBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(startInfoBytes.Length + 0x8); var startInfoAddress = startInfoBuffer.Add(startInfoBytes); if (startInfoAddress == IntPtr.Zero) throw new Exception("Unable to allocate start info JSON"); injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress); injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode); // ====================================================== if (exitCode > 0) { Log.Error($"Dalamud.Boot::Initialize returned {exitCode}"); return; } Log.Information("Done"); } } }