diff --git a/Daybreak.GWCA/header/Server.h b/Daybreak.GWCA/header/Server.h index f0025443..d5c08466 100644 --- a/Daybreak.GWCA/header/Server.h +++ b/Daybreak.GWCA/header/Server.h @@ -5,6 +5,7 @@ namespace http { namespace server { bool StartServer(); + void StopServer(); void SetLogger(httplib::Logger logger); void Get(const std::string& pattern, httplib::Server::Handler handler); } diff --git a/Daybreak.GWCA/source/Server.cpp b/Daybreak.GWCA/source/Server.cpp index 6a4931ff..68ffdc59 100644 --- a/Daybreak.GWCA/source/Server.cpp +++ b/Daybreak.GWCA/source/Server.cpp @@ -68,5 +68,9 @@ namespace http { return false; } + + void StopServer() { + server.stop(); + } } } \ No newline at end of file diff --git a/Daybreak.GWCA/source/dllmain.cpp b/Daybreak.GWCA/source/dllmain.cpp index 4725bc69..69315e6a 100644 --- a/Daybreak.GWCA/source/dllmain.cpp +++ b/Daybreak.GWCA/source/dllmain.cpp @@ -2,6 +2,11 @@ #include "pch.h" #include "httplib.h" #include +#include +#include +#include +#include +#include #include "AliveModule.h" #include "ProcessIdModule.h" #include "HttpLogger.h" @@ -19,25 +24,38 @@ #include "EntityNameModule.h" #include "GameStateModule.h" #include "ItemNameModule.h" +#include +volatile bool initialized; +volatile WNDPROC oldWndProc; +std::mutex startupMutex; +HMODULE dllmodule; +HANDLE serverThread; static FILE* stdout_proxy; static FILE* stderr_proxy; +void Terminate() +{ + http::server::StopServer(); + if (serverThread) { + CloseHandle(serverThread); + } + +#ifdef BUILD_TYPE_DEBUG + if (stdout_proxy) + fclose(stdout_proxy); + if (stderr_proxy) + fclose(stderr_proxy); + FreeConsole(); +#endif +} + -static DWORD WINAPI ThreadProc(LPVOID lpModule) +static DWORD WINAPI StartHttpServer(LPVOID) { // This is a new thread so you should only initialize GWCA and setup the hook on the game thread. // When the game thread hook is setup (i.e. SetRenderCallback), you should do the next operations // on the game from within the game thread. - - HMODULE hModule = static_cast(lpModule); -#ifdef BUILD_TYPE_DEBUG - AllocConsole(); - SetConsoleTitleA("Daybreak.GWCA Console"); - freopen_s(&stdout_proxy, "CONOUT$", "w", stdout); - freopen_s(&stderr_proxy, "CONOUT$", "w", stderr); -#endif - GW::Initialize(); http::server::SetLogger(http::ConsoleLogger); http::server::Get("/alive", http::modules::HandleAlive); http::server::Get("/id", http::modules::HandleProcessId); @@ -55,28 +73,87 @@ static DWORD WINAPI ThreadProc(LPVOID lpModule) http::server::Get("/entities/name", Daybreak::Modules::EntityNameModule::GetName); http::server::Get("/items/name", Daybreak::Modules::ItemNameModule::GetName); http::server::StartServer(); + return 0; +} + +LRESULT CALLBACK WndProc(const HWND hWnd, const UINT Message, const WPARAM wParam, const LPARAM lParam) { + if (Message == WM_CLOSE || (Message == WM_SYSCOMMAND && wParam == SC_CLOSE)) { + Terminate(); + } + + return CallWindowProc(oldWndProc, hWnd, Message, wParam, lParam); +} + +LRESULT CALLBACK SafeWndProc(const HWND hWnd, const UINT Message, const WPARAM wParam, const LPARAM lParam) noexcept +{ + __try { + return WndProc(hWnd, Message, wParam, lParam); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + return CallWindowProc(oldWndProc, hWnd, Message, wParam, lParam); + } +} + +void OnWindowCreated(IDirect3DDevice9*) { + startupMutex.lock(); + if (initialized) { + startupMutex.unlock(); + return; + } #ifdef BUILD_TYPE_DEBUG - if (stdout_proxy) - fclose(stdout_proxy); - if (stderr_proxy) - fclose(stderr_proxy); - FreeConsole(); + AllocConsole(); + SetConsoleTitleA("Daybreak.GWCA Console"); + freopen_s(&stdout_proxy, "CONOUT$", "w", stdout); + freopen_s(&stderr_proxy, "CONOUT$", "w", stderr); #endif + auto handle = GW::MemoryMgr::GetGWWindowHandle(); + oldWndProc = reinterpret_cast(SetWindowLongPtrW(handle, GWL_WNDPROC, reinterpret_cast(SafeWndProc))); + serverThread = CreateThread( + NULL, + 0, + StartHttpServer, + NULL, + 0, + NULL); + startupMutex.unlock(); + initialized = true; + return; +} + + +static DWORD WINAPI Init(LPVOID) +{ + printf("Init: Setting up scanner\n"); + GW::Scanner::Initialize(); + printf("Init: Setting up hook base\n"); + GW::HookBase::Initialize(); + printf("Init: Setting up GWCA\n"); + if (!GW::Initialize()) { + printf("Init: Failed to set up GWCA\n"); + return 0; + } - FreeLibraryAndExitThread(hModule, EXIT_SUCCESS); + printf("Init: Enabling hooks\n"); + GW::HookBase::EnableHooks(); + printf("Init: Set up render callback\n"); + GW::Render::SetRenderCallback(OnWindowCreated); + printf("Init: Returning success\n"); + FreeLibraryAndExitThread(dllmodule, EXIT_SUCCESS); + return 0; } -BOOL APIENTRY DllMain( HMODULE hModule, - DWORD ul_reason_for_call, - LPVOID lpReserved - ) +// DLL entry point, dont do things in this thread unless you know what you are doing. +BOOL APIENTRY DllMain(HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved +) { DisableThreadLibraryCalls(hModule); if (ul_reason_for_call == DLL_PROCESS_ATTACH) { - HANDLE handle = CreateThread(0, 0, ThreadProc, hModule, 0, 0); + HANDLE handle = CreateThread(0, 0, Init, hModule, 0, 0); CloseHandle(handle); } diff --git a/Daybreak/Configuration/Options/LauncherOptions.cs b/Daybreak/Configuration/Options/LauncherOptions.cs index f7706573..f6d196c8 100644 --- a/Daybreak/Configuration/Options/LauncherOptions.cs +++ b/Daybreak/Configuration/Options/LauncherOptions.cs @@ -43,5 +43,6 @@ public sealed class LauncherOptions [JsonProperty(nameof(ModStartupTimeout))] [OptionName(Name = "Mod Startup Timeout", Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks")] - public int ModStartupTimeout { get; set; } = 30; + [OptionRange(MinValue = 30, MaxValue = 300)] + public double ModStartupTimeout { get; set; } = 30; } diff --git a/Daybreak/Configuration/Options/ToolboxOptions.cs b/Daybreak/Configuration/Options/ToolboxOptions.cs index 232c955b..7d1f4a28 100644 --- a/Daybreak/Configuration/Options/ToolboxOptions.cs +++ b/Daybreak/Configuration/Options/ToolboxOptions.cs @@ -13,4 +13,9 @@ public sealed class ToolboxOptions [JsonProperty(nameof(Enabled))] [OptionName(Name = "Enabled", Description = "If true, Daybreak will also launch GWToolboxdll when launching GuildWars")] public bool Enabled { get; set; } + + [JsonProperty(nameof(StartupDelay))] + [OptionName(Name = "Startup Delay", Description = "Amount of seconds that Daybreak will wait for GWToolbox to start before continuing with the other mods")] + [OptionRange(MinValue = 1, MaxValue = 10)] + public double StartupDelay { get; set; } = 1; } diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index 5b078469..2f18b422 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -418,8 +418,8 @@ public override void RegisterNotificationHandlers(INotificationHandlerProducer n public override void RegisterMods(IModsManager modsManager) { - modsManager.RegisterMod(); modsManager.RegisterMod(); + modsManager.RegisterMod(); modsManager.RegisterMod(); modsManager.RegisterMod(); modsManager.RegisterMod(); diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 12eaa3c4..7d50971b 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -13,7 +13,7 @@ preview Daybreak.ico true - 0.9.8.135 + 0.9.8.136 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Services/GWCA/GWCAInjector.cs b/Daybreak/Services/GWCA/GWCAInjector.cs index 9283983f..d55aab67 100644 --- a/Daybreak/Services/GWCA/GWCAInjector.cs +++ b/Daybreak/Services/GWCA/GWCAInjector.cs @@ -40,10 +40,14 @@ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationT return Task.CompletedTask; } - public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) + public async Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) { - this.injector.Inject(process, ModulePath); - return Task.CompletedTask; + if (!await this.injector.Inject(process, ModulePath, cancellationToken)) + { + this.notificationService.NotifyError( + title: "Unable to inject GWCA into Guild Wars process", + description: "Daybreak integration with the Guild Wars process will be affected. Some Daybreak functionality might not work"); + } } public async Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken) diff --git a/Daybreak/Services/Injection/IProcessInjector.cs b/Daybreak/Services/Injection/IProcessInjector.cs index 9cbb565e..16682eaa 100644 --- a/Daybreak/Services/Injection/IProcessInjector.cs +++ b/Daybreak/Services/Injection/IProcessInjector.cs @@ -1,7 +1,9 @@ using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; namespace Daybreak.Services.Injection; public interface IProcessInjector { - bool Inject(Process process, string pathToDll); + Task Inject(Process process, string pathToDll, CancellationToken cancellationToken); } diff --git a/Daybreak/Services/Injection/ProcessInjector.cs b/Daybreak/Services/Injection/ProcessInjector.cs index 9bac7fc2..609e6394 100644 --- a/Daybreak/Services/Injection/ProcessInjector.cs +++ b/Daybreak/Services/Injection/ProcessInjector.cs @@ -7,6 +7,8 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Daybreak.Services.Injection; internal sealed class ProcessInjector : IProcessInjector @@ -19,29 +21,36 @@ public ProcessInjector( this.logger = logger.ThrowIfNull(); } - public bool Inject(Process process, string pathToDll) + public Task Inject(Process process, string pathToDll, CancellationToken cancellationToken) { - return this.InjectWithWinApi(process, pathToDll); + return Task.Factory.StartNew(() => + { + return this.InjectWithWinApi(process, pathToDll); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } private bool InjectWithWinApi(Process process, string pathToDll) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(InjectWithWinApi), pathToDll); var modulefullpath = Path.GetFullPath(pathToDll); if (!File.Exists(modulefullpath)) { + scopedLogger.LogError("Dll to inject not found"); return false; } var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll"); if (hKernel32 == IntPtr.Zero) { + scopedLogger.LogError("Unable to get a handle of kernel32.dll"); return false; } var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW"); if (hLoadLib == IntPtr.Zero) { + scopedLogger.LogError("Unable to get the address of LoadLibraryW"); return false; } @@ -49,33 +58,44 @@ private bool InjectWithWinApi(Process process, string pathToDll) 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */); if (hStringBuffer == IntPtr.Zero) { + scopedLogger.LogError("Unable to allocate memory for module path"); return false; } WriteWString(process, hStringBuffer, modulefullpath); if (ReadWString(process, hStringBuffer, 260) != modulefullpath) { + scopedLogger.LogError("Module path string is not correct"); return false; } var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _); if (hThread == IntPtr.Zero) { + scopedLogger.LogError("Unable to create remote thread"); return false; } var threadResult = NativeMethods.WaitForSingleObject(hThread, 5000u); if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */) { + scopedLogger.LogError($"Exception occurred while waiting for the remote thread. Result is {threadResult}"); return false; } - if (NativeMethods.GetExitCodeThread(hThread, out _) == 0) + var dllResult = NativeMethods.GetExitCodeThread(hThread, out _); + if (dllResult == 0) { + scopedLogger.LogError($"Injected dll returned non-success status code {dllResult}"); return false; } var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */); + if (!memoryFreeResult) + { + scopedLogger.LogError($"Failed to free dll memory"); + } + return memoryFreeResult; } diff --git a/Daybreak/Services/ReShade/ReShadeService.cs b/Daybreak/Services/ReShade/ReShadeService.cs index 7458be29..c540757e 100644 --- a/Daybreak/Services/ReShade/ReShadeService.cs +++ b/Daybreak/Services/ReShade/ReShadeService.cs @@ -119,26 +119,23 @@ public void OnClosing() public IEnumerable GetCustomArguments() => Enumerable.Empty(); - public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) + public async Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) { - return Task.Run(() => + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.OnGuildWarsCreated), process?.MainModule?.FileName ?? string.Empty); + if (await this.processInjector.Inject(process!, ReShadeDllPath, cancellationToken)) { - var scopedLogger = this.logger.CreateScopedLogger(nameof(this.OnGuildWarsCreated), process?.MainModule?.FileName ?? string.Empty); - if (this.processInjector.Inject(process!, ReShadeDllPath)) - { - scopedLogger.LogInformation("Injected ReShade dll"); - this.notificationService.NotifyInformation( - title: "ReShade started", - description: "ReShade has been injected"); - } - else - { - scopedLogger.LogError("Failed to inject ReShade dll"); - this.notificationService.NotifyError( - title: "ReShade failed to start", - description: "Failed to inject ReShade"); - } - }, cancellationToken); + scopedLogger.LogInformation("Injected ReShade dll"); + this.notificationService.NotifyInformation( + title: "ReShade started", + description: "ReShade has been injected"); + } + else + { + scopedLogger.LogError("Failed to inject ReShade dll"); + this.notificationService.NotifyError( + title: "ReShade failed to start", + description: "Failed to inject ReShade"); + } } public Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/Daybreak/Services/Toolbox/ToolboxService.cs b/Daybreak/Services/Toolbox/ToolboxService.cs index 4993c6b5..f0bd9459 100644 --- a/Daybreak/Services/Toolbox/ToolboxService.cs +++ b/Daybreak/Services/Toolbox/ToolboxService.cs @@ -65,9 +65,15 @@ public ToolboxService( public Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken) => Task.CompletedTask; - public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) + public async Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) { - return Task.Run(() => this.LaunchToolbox(process), cancellationToken); + await this.LaunchToolbox(process, cancellationToken); + + /* + * Toolbox startup conflicts with Daybreak GWCA integration. Wait some time + * so that toolbox has the chance to set up its hooks. + */ + await Task.Delay(TimeSpan.FromSeconds(this.toolboxOptions.Value.StartupDelay), cancellationToken); } public Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken) => Task.CompletedTask; @@ -150,7 +156,7 @@ private async Task SetupToolboxDll(ToolboxInstallationStatus toolboxInstal return true; } - private void LaunchToolbox(Process process) + private async Task LaunchToolbox(Process process, CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.LaunchToolbox), string.Empty); if (this.toolboxOptions.Value.Enabled is false) @@ -167,12 +173,12 @@ private void LaunchToolbox(Process process) } scopedLogger.LogInformation("Injecting toolbox dll"); - if (this.processInjector.Inject(process, dll)) + if (await this.processInjector.Inject(process, dll, cancellationToken)) { scopedLogger.LogInformation("Injected toolbox dll"); this.notificationService.NotifyInformation( title: "GWToolbox started", - description: "GWToolbox has been injected"); + description: "GWToolbox has been injected. Delaying startup so that GWToolbox has time to initialize"); } else { diff --git a/Daybreak/Services/UMod/UModService.cs b/Daybreak/Services/UMod/UModService.cs index cc2a7133..7080a779 100644 --- a/Daybreak/Services/UMod/UModService.cs +++ b/Daybreak/Services/UMod/UModService.cs @@ -84,10 +84,7 @@ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationT public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) { - return Task.Run(() => - { - this.processInjector.Inject(process, Path.Combine(Path.GetFullPath(UModDirectory), D3D9Dll)); - }, cancellationToken); + return this.processInjector.Inject(process, Path.Combine(Path.GetFullPath(UModDirectory), D3D9Dll), cancellationToken); } public async Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken)