Skip to content

Commit

Permalink
Fix toolbox and gwca integration
Browse files Browse the repository at this point in the history
Closes #458
  • Loading branch information
AlexMacocian committed Nov 3, 2023
1 parent ed90d56 commit b1b50ee
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 58 deletions.
1 change: 1 addition & 0 deletions Daybreak.GWCA/header/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions Daybreak.GWCA/source/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,9 @@ namespace http {

return false;
}

void StopServer() {
server.stop();
}
}
}
119 changes: 98 additions & 21 deletions Daybreak.GWCA/source/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
#include "pch.h"
#include "httplib.h"
#include <GWCA/GWCA.h>
#include <GWCA/Utilities/Scanner.h>
#include <GWCA/Utilities/Hook.h>
#include <GWCA/Utilities/Hooker.h>
#include <GWCA/Managers/RenderMgr.h>
#include <GWCA/Managers/MemoryMgr.h>
#include "AliveModule.h"
#include "ProcessIdModule.h"
#include "HttpLogger.h"
Expand All @@ -19,25 +24,38 @@
#include "EntityNameModule.h"
#include "GameStateModule.h"
#include "ItemNameModule.h"
#include <mutex>

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<HMODULE>(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);
Expand All @@ -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<WNDPROC>(SetWindowLongPtrW(handle, GWL_WNDPROC, reinterpret_cast<LONG>(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);
}

Expand Down
3 changes: 2 additions & 1 deletion Daybreak/Configuration/Options/LauncherOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(MinValue = 30, MaxValue = 300)]
public double ModStartupTimeout { get; set; } = 30;
}
5 changes: 5 additions & 0 deletions Daybreak/Configuration/Options/ToolboxOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(MinValue = 1, MaxValue = 10)]
public double StartupDelay { get; set; } = 1;
}
2 changes: 1 addition & 1 deletion Daybreak/Configuration/ProjectConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,8 @@ public override void RegisterNotificationHandlers(INotificationHandlerProducer n

public override void RegisterMods(IModsManager modsManager)
{
modsManager.RegisterMod<IToolboxService, ToolboxService>();
modsManager.RegisterMod<IUModService, UModService>();
modsManager.RegisterMod<IToolboxService, ToolboxService>();
modsManager.RegisterMod<IDSOALService, DSOALService>();
modsManager.RegisterMod<IGuildwarsScreenPlacer, GuildwarsScreenPlacer>();
modsManager.RegisterMod<IReShadeService, ReShadeService>();
Expand Down
2 changes: 1 addition & 1 deletion Daybreak/Daybreak.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.8.135</Version>
<Version>0.9.8.136</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
Expand Down
10 changes: 7 additions & 3 deletions Daybreak/Services/GWCA/GWCAInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion Daybreak/Services/Injection/IProcessInjector.cs
Original file line number Diff line number Diff line change
@@ -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<bool> Inject(Process process, string pathToDll, CancellationToken cancellationToken);
}
26 changes: 23 additions & 3 deletions Daybreak/Services/Injection/ProcessInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,63 +21,81 @@ public ProcessInjector(
this.logger = logger.ThrowIfNull();
}

public bool Inject(Process process, string pathToDll)
public Task<bool> 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;
}

var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)),
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;
}

Expand Down
33 changes: 15 additions & 18 deletions Daybreak/Services/ReShade/ReShadeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,23 @@ public void OnClosing()

public IEnumerable<string> GetCustomArguments() => Enumerable.Empty<string>();

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;
Expand Down
Loading

0 comments on commit b1b50ee

Please sign in to comment.