99#include < d3d9.h>
1010#include < filesystem>
1111#include < format>
12- #include < fstream>
1312#include < imgui.h>
1413#include < map>
1514#include < memory>
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*)
12061057void 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