Skip to content

Commit b4d0706

Browse files
henderkesclaude
andcommitted
Texmod: always load our own managed gMod.dll
Drop pre-existing gMod/d3d9-proxy detection and the on-disk PE export sniffing used to pick a DLL; always LoadLibrary the toolbox-managed copy. We always own the reference now, so remove the gmodLoadedByUs flag. On update, download to gMod.tmp.dll first (the live copy may be locked), then unload, rename over the original, and reload - preserving the user's enabled packs across the swap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a125591 commit b4d0706

1 file changed

Lines changed: 47 additions & 196 deletions

File tree

GWToolboxdll/Modules/TexmodModule.cpp

Lines changed: 47 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#include <d3d9.h>
1010
#include <filesystem>
1111
#include <format>
12-
#include <fstream>
1312
#include <imgui.h>
1413
#include <map>
1514
#include <memory>
@@ -21,7 +20,6 @@
2120
#include <GWCA/Managers/RenderMgr.h>
2221
#include <GWCA/Utilities/Hooker.h>
2322

24-
#include <GWToolbox.h>
2523
#include <ImGuiAddons.h>
2624
#include <Modules/Resources.h>
2725
#include <Utils/FontLoader.h>
@@ -89,10 +87,6 @@ namespace {
8987
// =========================================================================
9088

9189
HMODULE gmodDll = nullptr;
92-
// True only when this module called LoadLibrary on gmodDll, i.e. we own the
93-
// reference and must FreeLibrary it on teardown. False when gMod was already
94-
// present (e.g. injected as the d3d9.dll proxy) - that copy belongs to the game.
95-
bool gmodLoadedByUs = false;
9690

9791
std::vector<TexturePackEntry> packs;
9892

@@ -208,183 +202,47 @@ namespace {
208202
// pack can trigger the download when gMod isn't loaded yet. Defined further down.
209203
void CheckAndUpdateGmod();
210204

211-
// Translate an image RVA to a raw file offset via the section table (0 = not found).
212-
uint32_t RvaToFileOffset(const IMAGE_SECTION_HEADER* sec, unsigned n, uint32_t rva)
213-
{
214-
for (unsigned i = 0; i < n; ++i) {
215-
if (rva >= sec[i].VirtualAddress && rva < sec[i].VirtualAddress + sec[i].SizeOfRawData) return sec[i].PointerToRawData + (rva - sec[i].VirtualAddress);
216-
}
217-
return 0;
218-
}
219-
220-
// Read the exported function names from a PE file on disk. Empty on any error
221-
// or if the file is not a 32-bit PE (gMod and Guild Wars are always x86).
222-
std::vector<std::string> ReadDllExportNames(const std::filesystem::path& path)
223-
{
224-
std::vector<std::string> names;
225-
std::error_code ec;
226-
const auto size = std::filesystem::file_size(path, ec);
227-
if (ec || size < sizeof(IMAGE_DOS_HEADER)) return names;
228-
229-
std::vector<uint8_t> img(static_cast<size_t>(size));
230-
{
231-
std::ifstream f(path, std::ios::binary);
232-
if (!f.read(reinterpret_cast<char*>(img.data()), static_cast<std::streamsize>(img.size()))) return names;
233-
}
234-
235-
// Every read below is bounds-checked: the file is untrusted.
236-
const auto ok = [&](size_t off, size_t len) {
237-
return off <= img.size() && len <= img.size() - off;
238-
};
239-
240-
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(img.data());
241-
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return names;
242-
const size_t nt = static_cast<size_t>(dos->e_lfanew);
243-
if (!ok(nt, sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER))) return names;
244-
if (*reinterpret_cast<const DWORD*>(img.data() + nt) != IMAGE_NT_SIGNATURE) return names;
245-
246-
const auto* fh = reinterpret_cast<const IMAGE_FILE_HEADER*>(img.data() + nt + sizeof(DWORD));
247-
const size_t opt = nt + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER);
248-
if (!ok(opt, sizeof(WORD))) return names;
249-
if (*reinterpret_cast<const WORD*>(img.data() + opt) != IMAGE_NT_OPTIONAL_HDR32_MAGIC) return names;
250-
if (!ok(opt, sizeof(IMAGE_OPTIONAL_HEADER32))) return names;
251-
252-
const auto* oh = reinterpret_cast<const IMAGE_OPTIONAL_HEADER32*>(img.data() + opt);
253-
if (oh->NumberOfRvaAndSizes <= static_cast<DWORD>(IMAGE_DIRECTORY_ENTRY_EXPORT)) return names;
254-
const DWORD expRva = oh->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
255-
if (!expRva) return names; // no export directory
256-
257-
const size_t secOff = opt + fh->SizeOfOptionalHeader;
258-
const unsigned nSec = fh->NumberOfSections;
259-
if (!ok(secOff, static_cast<size_t>(nSec) * sizeof(IMAGE_SECTION_HEADER))) return names;
260-
const auto* sec = reinterpret_cast<const IMAGE_SECTION_HEADER*>(img.data() + secOff);
261-
262-
const uint32_t expOff = RvaToFileOffset(sec, nSec, expRva);
263-
if (!ok(expOff, sizeof(IMAGE_EXPORT_DIRECTORY))) return names;
264-
const auto* exp = reinterpret_cast<const IMAGE_EXPORT_DIRECTORY*>(img.data() + expOff);
265-
266-
const uint32_t namesOff = RvaToFileOffset(sec, nSec, exp->AddressOfNames);
267-
if (!ok(namesOff, static_cast<size_t>(exp->NumberOfNames) * sizeof(DWORD))) return names;
268-
const auto* nameRvas = reinterpret_cast<const DWORD*>(img.data() + namesOff);
269-
270-
names.reserve(exp->NumberOfNames);
271-
for (DWORD i = 0; i < exp->NumberOfNames; ++i) {
272-
const uint32_t so = RvaToFileOffset(sec, nSec, nameRvas[i]);
273-
if (!so || so >= img.size()) continue;
274-
const char* s = reinterpret_cast<const char*>(img.data() + so);
275-
const size_t len = strnlen(s, img.size() - so);
276-
if (len < img.size() - so) names.emplace_back(s, len); // skip unterminated
277-
}
278-
return names;
279-
}
280-
281-
// Mandatory gMod exports. GetFiles is required because reconciliation reads the
282-
// current load order from it.
283-
constexpr const char* kRequiredExports[] = {"AddFile", "RemoveFile", "GetFiles", "SetDevice"};
284-
285-
std::filesystem::path DirOfModule(HMODULE h)
286-
{
287-
wchar_t buf[MAX_PATH] = {};
288-
if (!GetModuleFileNameW(h, buf, MAX_PATH)) return {};
289-
return std::filesystem::path(buf).parent_path();
290-
}
291-
292-
std::filesystem::path FindGmodDll()
205+
// Where the toolbox keeps its managed gMod.dll (auto-downloaded).
206+
std::filesystem::path ManagedGmodDllPath()
293207
{
294-
// Prefer the toolbox-managed copy (auto-downloaded into the GWToolbox folder),
295-
// then fall back to one sitting next to the running exe or GWToolbox.dll.
296-
std::vector<std::filesystem::path> dirs;
297-
const auto add_dir = [&](std::filesystem::path d) {
298-
if (!d.empty() && std::ranges::find(dirs, d) == dirs.end()) dirs.push_back(std::move(d));
299-
};
300-
add_dir(Resources::GetPath("gmod")); // toolbox-managed copy
301-
add_dir(DirOfModule(nullptr)); // running exe
302-
add_dir(DirOfModule(GWToolbox::GetDLLModule())); // toolbox dll
303-
304-
std::filesystem::path found;
305-
306-
// Prefer a dedicated gMod.dll over a d3d9.dll proxy; sniff exports off
307-
// disk so an incompatible file (e.g. the system d3d9.dll) is never loaded.
308-
for (const auto& dir : dirs) {
309-
for (const wchar_t* name : {L"gMod.dll", L"d3d9.dll"}) {
310-
std::error_code ec;
311-
auto candidate = dir / name;
312-
if (!std::filesystem::exists(candidate, ec)) continue;
313-
const auto names = ReadDllExportNames(candidate);
314-
if (std::ranges::all_of(kRequiredExports, [&](const char* req) {
315-
return std::ranges::find(names, req) != names.end();
316-
})) {
317-
found = candidate;
318-
break;
319-
}
320-
}
321-
}
322-
return found;
208+
return Resources::GetPath("gmod") / "gMod.dll";
323209
}
324210

211+
// Load our own managed copy of gMod.dll and hand it the game's device. We never
212+
// adopt a pre-existing gMod in the process; the toolbox always owns the copy it loads.
325213
bool InitGMod()
326214
{
327215
if (gmodReady) return true;
328216

329-
auto GuildWars_IDirect3DDevice9_Instance = GW::Render::GetDevice();
330-
if (!GuildWars_IDirect3DDevice9_Instance) {
217+
auto device = GW::Render::GetDevice();
218+
if (!device) {
331219
statusMessage = "Error: Could not get IDirect3DDevice9 from GW::Render::GetDevice().";
332220
return false;
333221
}
334222

335-
// 1. Check whether a compatible module is already loaded in this process
336-
// (a dedicated gMod.dll, or a d3d9.dll proxy). GetProcAddress here loads
337-
// nothing new. A loaded module that lacks the exports (e.g. the system
338-
// d3d9.dll) is simply skipped rather than treated as an error.
339-
for (const wchar_t* name : {L"gMod.dll", L"d3d9.dll"}) {
340-
HMODULE h = GetModuleHandleW(name);
341-
if (!h) continue;
342-
gmodDll = h;
343-
gmodLoadedByUs = false; // already in the process; we don't own this reference
344-
if (!ResolveTextureClientFunctions()) {
345-
gmodDll = nullptr;
346-
continue;
347-
}
348-
pfnSetDevice(GuildWars_IDirect3DDevice9_Instance);
349-
gmodReady = true;
350-
RestoreLoadedPacks();
351-
statusMessage = "gMod was already active (pre-loaded). Texture pack loading enabled.";
352-
return true;
353-
}
354-
355-
// Only load our own copy of gMod when there is a pack to serve. With no packs
356-
// there is nothing to mod, so we leave gMod unloaded rather than have it hook
357-
// the device for nothing. (A gMod already injected as the d3d9 proxy was
358-
// adopted above regardless of this.)
223+
// Nothing to mod with no packs: leave gMod unloaded rather than hook the device for nothing.
359224
if (packs.empty()) return false;
360-
361-
if (gmodLoadAttempted) {
362-
// statusMessage = "Error: gMod initialization already attempted and failed. Please fix the issue and click Retry.";
363-
return false;
364-
}
225+
if (gmodLoadAttempted) return false;
365226
gmodLoadAttempted = true;
366-
// 2. Find and load gMod.dll.
367-
const auto found = FindGmodDll();
368-
if (found.empty()) {
227+
228+
const auto path = ManagedGmodDllPath();
229+
std::error_code ec;
230+
if (!std::filesystem::exists(path, ec)) {
369231
statusMessage = "gMod.dll not found yet - it is downloaded automatically. Check your internet connection.";
370232
return false;
371233
}
372234

373-
gmodDll = LoadLibraryW(found.wstring().c_str());
235+
gmodDll = LoadLibraryW(path.wstring().c_str());
374236
if (!gmodDll) {
375-
statusMessage = "Error: Could not load gMod.dll. Make sure it is next to GWToolbox.dll.";
237+
statusMessage = "Error: Could not load gMod.dll.";
376238
return false;
377239
}
378-
gmodLoadedByUs = true; // we own this reference and must FreeLibrary it on teardown
379-
380-
// 3. Resolve TextureClient shim exports.
381240
if (!ResolveTextureClientFunctions()) {
382241
FreeLibrary(gmodDll);
383242
gmodDll = nullptr;
384-
gmodLoadedByUs = false;
385243
return false;
386244
}
387-
pfnSetDevice(GuildWars_IDirect3DDevice9_Instance);
245+
pfnSetDevice(device);
388246
gmodReady = true;
389247
RestoreLoadedPacks();
390248
return true;
@@ -525,14 +383,10 @@ namespace {
525383
pfnGetFiles = nullptr;
526384
pfnSetDevice = nullptr;
527385

528-
// Free the dll only if we loaded it. gMod's DllMain(DLL_PROCESS_DETACH) reverts
529-
// all its D3D9 vtable hooks (RemoveAllD3D9Hooks), so this is its clean shutdown
530-
// path. A gMod injected as the d3d9 proxy is owned by the game - never free it.
531-
if (gmodLoadedByUs && gmodDll) {
532-
FreeLibrary(gmodDll);
533-
}
386+
// Free our dll. gMod's DllMain(DLL_PROCESS_DETACH) reverts all its D3D9 vtable
387+
// hooks (RemoveAllD3D9Hooks), so this is its clean shutdown path.
388+
if (gmodDll) FreeLibrary(gmodDll);
534389
gmodDll = nullptr;
535-
gmodLoadedByUs = false;
536390
gmodLoadAttempted = false; // allow a fresh load later (e.g. when a pack is added)
537391
gmodLocalVersionChecked = false;
538392
}
@@ -602,29 +456,12 @@ namespace {
602456
return std::format("{}.{}.{}.{}", HIWORD(fi->dwFileVersionMS), LOWORD(fi->dwFileVersionMS), HIWORD(fi->dwFileVersionLS), LOWORD(fi->dwFileVersionLS));
603457
}
604458

605-
// Full path of the gMod.dll currently in use: the loaded module if gMod is
606-
// active, otherwise wherever FindGmodDll would load it from.
607-
std::filesystem::path CurrentGmodDllPath()
608-
{
609-
if (gmodReady && gmodDll) {
610-
wchar_t buf[MAX_PATH] = {};
611-
if (GetModuleFileNameW(gmodDll, buf, MAX_PATH)) return buf;
612-
}
613-
return FindGmodDll();
614-
}
615-
616459
void RefreshLocalGmodVersion()
617460
{
618-
gmodLocalVersion = GetDllFileVersion(CurrentGmodDllPath());
461+
gmodLocalVersion = GetDllFileVersion(ManagedGmodDllPath());
619462
gmodLocalVersionChecked = true;
620463
}
621464

622-
// Where the toolbox keeps its managed gMod.dll.
623-
std::filesystem::path ManagedGmodDllPath()
624-
{
625-
return Resources::GetPath("gmod") / "gMod.dll";
626-
}
627-
628465
// Compare dotted version strings ("1.8.0.2"): >0 if a is newer, <0 if older, 0 if
629466
// equal. Missing trailing components count as 0, so "1.8" == "1.8.0.0".
630467
int CompareVersions(const std::string& a, const std::string& b)
@@ -652,16 +489,11 @@ namespace {
652489
return 0;
653490
}
654491

655-
// Re-initialise after the managed gMod.dll changes. gMod loads as a process-wide
656-
// d3d9 proxy and can't be hot-swapped once active, so if it is already loaded we
657-
// just ask the user to restart.
492+
// Load the managed gMod.dll after it has changed on disk. The caller unloads any
493+
// active copy first, so we just clear the load-attempt latch and init afresh.
658494
void ReloadGmod()
659495
{
660496
gmodLocalVersionChecked = false;
661-
if (gmodReady) {
662-
statusMessage = "gMod updated. Restart Guild Wars to load the new version.";
663-
return;
664-
}
665497
gmodLoadAttempted = false;
666498
InitGMod();
667499
}
@@ -723,16 +555,37 @@ namespace {
723555
gmodUpdateStatus = "Downloading gMod " + version + "...";
724556
});
725557

558+
// Download to a temp file: our gMod.dll may still be loaded (and thus locked),
559+
// so we can't write the real file yet. We swap it in on the main thread below.
560+
const auto tmp = managed.parent_path() / "gMod.tmp.dll";
726561
Resources::EnsureFolderExists(managed.parent_path());
727562
std::wstring error;
728-
const bool ok = Resources::Download(managed, dll_url, error);
563+
const bool ok = Resources::Download(tmp, dll_url, error);
729564

730-
Resources::EnqueueMainTask([ok, version, error] {
565+
Resources::EnqueueMainTask([ok, version, error, managed, tmp] {
731566
gmodUpdateStep = GmodUpdateStep::Idle;
732567
if (!ok) {
733568
gmodUpdateStatus = "gMod download failed: " + TextUtils::WStringToString(error);
734569
return;
735570
}
571+
// Unload our copy so the file is no longer locked, then rename the temp
572+
// download over the original and load the new version. ShutdownGMod clears
573+
// the loaded flags, so remember the enabled packs to restore them after.
574+
std::vector<std::filesystem::path> enabled;
575+
for (const auto& pack : packs) {
576+
if (pack.loaded) enabled.push_back(pack.path);
577+
}
578+
ShutdownGMod();
579+
std::error_code ec;
580+
std::filesystem::rename(tmp, managed, ec);
581+
if (ec) {
582+
std::filesystem::remove(tmp, ec);
583+
gmodUpdateStatus = "gMod update failed: could not replace gMod.dll.";
584+
return;
585+
}
586+
for (auto& pack : packs) {
587+
pack.loaded = std::ranges::find(enabled, pack.path) != enabled.end();
588+
}
736589
gmodLocalVersion = version;
737590
gmodUpdateStatus = "Updated gMod to " + version + ".";
738591
ReloadGmod();
@@ -1189,9 +1042,7 @@ void TexmodModule::Update(float)
11891042
// Unload gMod once nothing needs it (requested when the last pack was removed).
11901043
if (gmodUnloadRequested) {
11911044
gmodUnloadRequested = false;
1192-
// Only unload what we own; an injected proxy is left to the game. ShutdownGMod
1193-
// already no-ops the FreeLibrary in that case, but skip the pack teardown too.
1194-
if (gmodLoadedByUs) ShutdownGMod();
1045+
ShutdownGMod();
11951046
}
11961047

11971048
if (gmodReady) return;
@@ -1206,7 +1057,7 @@ void TexmodModule::Draw(IDirect3DDevice9*)
12061057
void TexmodModule::Terminate()
12071058
{
12081059
TeardownTextureCapture();
1209-
ShutdownGMod(); // unloads every pack and frees gMod.dll if we loaded it
1060+
ShutdownGMod(); // unloads every pack and frees gMod.dll
12101061
ToolboxModule::Terminate();
12111062
}
12121063

0 commit comments

Comments
 (0)