Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix toolbox and gwca integration #460

Merged
merged 1 commit into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading