@@ -48,7 +48,27 @@ namespace {
4848 constexpr int GMOD_RETURN_OK = 0 ;
4949 constexpr int GMOD_RETURN_EXISTS = -70 ;
5050
51- std::filesystem::path gmodDllLocation;
51+ // gMod version check / download (mirrors Updater's GitHub-release logic). gMod is
52+ // managed automatically: on startup we fetch the latest release and, if it is
53+ // newer than the copy kept in the GWToolbox folder (or none is present), download
54+ // it there and load it.
55+ struct GModReleaseAsset {
56+ std::string name;
57+ std::string browser_download_url;
58+ };
59+ struct GModRelease {
60+ std::string tag_name;
61+ std::vector<GModReleaseAsset> assets;
62+ };
63+ constexpr glz::opts gmod_json_opts{.error_on_unknown_keys = false };
64+
65+ enum class GmodUpdateStep { Idle, Checking, Downloading };
66+ GmodUpdateStep gmodUpdateStep = GmodUpdateStep::Idle;
67+
68+ std::string gmodLatestVersion; // latest release tag, 'v' stripped (e.g. "1.8.0.2")
69+ std::string gmodLocalVersion; // file version of the managed gMod.dll
70+ bool gmodLocalVersionChecked = false ; // recompute gmodLocalVersion when false
71+ std::string gmodUpdateStatus; // one-line status for the version row
5272
5373 struct TexturePackEntry {
5474 std::filesystem::path path;
@@ -161,6 +181,10 @@ namespace {
161181 // after ApplyLoadOrder; forward-declared here for InitGMod.
162182 void RestoreLoadedPacks ();
163183
184+ // Check GitHub and download a newer gMod if needed. Forward-declared so adding a
185+ // pack can trigger the download when gMod isn't loaded yet. Defined further down.
186+ void CheckAndUpdateGmod ();
187+
164188 // Translate an image RVA to a raw file offset via the section table (0 = not found).
165189 uint32_t RvaToFileOffset (const IMAGE_SECTION_HEADER * sec, unsigned n, uint32_t rva)
166190 {
@@ -244,14 +268,13 @@ namespace {
244268
245269 std::filesystem::path FindGmodDll ()
246270 {
247- // Explicit user override (Browse...) wins if it still exists.
248- if (!gmodDllLocation.empty () && std::filesystem::exists (gmodDllLocation)) return gmodDllLocation;
249-
250- // Search next to the running exe (Gw.exe) and next to GWToolbox.dll.
271+ // Prefer the toolbox-managed copy (auto-downloaded into the GWToolbox folder),
272+ // then fall back to one sitting next to the running exe or GWToolbox.dll.
251273 std::vector<std::filesystem::path> dirs;
252274 const auto add_dir = [&](std::filesystem::path d) {
253275 if (!d.empty () && std::ranges::find (dirs, d) == dirs.end ()) dirs.push_back (std::move (d));
254276 };
277+ add_dir (Resources::GetPath (" gmod" )); // toolbox-managed copy
255278 add_dir (DirOfModule (nullptr )); // running exe
256279 add_dir (DirOfModule (GWToolbox::GetDLLModule ())); // toolbox dll
257280
@@ -313,7 +336,7 @@ namespace {
313336 // 2. Find and load gMod.dll.
314337 const auto found = FindGmodDll ();
315338 if (found.empty ()) {
316- statusMessage = " Error: Could not find gMod.dll. Place it next to GWToolbox.dll or use Browse ." ;
339+ statusMessage = " gMod.dll not found yet - it is downloaded automatically. Check your internet connection ." ;
317340 return false ;
318341 }
319342
@@ -443,6 +466,7 @@ namespace {
443466 // the combined set so the packs the user had enabled are loaded again.
444467 void RestoreLoadedPacks ()
445468 {
469+ gmodLocalVersionChecked = false ; // gMod is now loaded; refresh the displayed version
446470 SyncExternalPacks (/* clear_missing=*/ false );
447471 ApplyLoadOrder ();
448472 }
@@ -488,17 +512,20 @@ namespace {
488512
489513 bool LoadTexturePack (const std::filesystem::path& path)
490514 {
491- if (!gmodReady) {
492- statusMessage = " gMod is not initialised." ;
493- return false ;
494- }
495515 if (!std::filesystem::exists (path)) {
496516 statusMessage = " File not found: " + path.string ();
497517 return false ;
498518 }
499519 auto pack = FindPack (path, true );
500520 const auto filename = pack->path .filename ().string ();
501521 pack->loaded = true ;
522+ if (!gmodReady) {
523+ // First pack configured but gMod isn't loaded yet: fetch/download it now.
524+ // The pack stays flagged loaded and is applied once gMod becomes ready.
525+ statusMessage = " Preparing gMod for: " + filename;
526+ CheckAndUpdateGmod ();
527+ return true ;
528+ }
502529 // The actual load runs on a worker thread; the result (and any failure)
503530 // is reported when it reconciles. Show an optimistic status meanwhile.
504531 statusMessage = " Loading: " + filename;
@@ -509,26 +536,171 @@ namespace {
509536
510537
511538 // =========================================================================
512- // UI helpers
539+ // gMod version check / download
513540 // =========================================================================
514541
515- void OnTexmodFileChosen (const char * path)
542+ // Read a DLL's file-version resource as "major.minor.patch.build". Empty if the
543+ // file has no version info; gMod.dll's matches its GitHub release tag.
544+ std::string GetDllFileVersion (const std::filesystem::path& path)
516545 {
517- if (!path) return ;
518- std::filesystem::path p = path;
519- if (!std::filesystem::exists (p)) return ;
520- LoadTexturePack (p);
546+ if (path.empty ()) return {};
547+ const auto wp = path.wstring ();
548+ DWORD handle = 0 ;
549+ const DWORD size = GetFileVersionInfoSizeW (wp.c_str (), &handle);
550+ if (!size) return {};
551+ std::vector<BYTE > data (size);
552+ if (!GetFileVersionInfoW (wp.c_str (), handle, size, data.data ())) return {};
553+ VS_FIXEDFILEINFO * fi = nullptr ;
554+ UINT len = 0 ;
555+ if (!VerQueryValueW (data.data (), L" \\ " , reinterpret_cast <LPVOID *>(&fi), &len) || !fi) return {};
556+ return std::format (" {}.{}.{}.{}" , HIWORD (fi->dwFileVersionMS ), LOWORD (fi->dwFileVersionMS ),
557+ HIWORD (fi->dwFileVersionLS ), LOWORD (fi->dwFileVersionLS ));
558+ }
559+
560+ // Full path of the gMod.dll currently in use: the loaded module if gMod is
561+ // active, otherwise wherever FindGmodDll would load it from.
562+ std::filesystem::path CurrentGmodDllPath ()
563+ {
564+ if (gmodReady && gmodDll) {
565+ wchar_t buf[MAX_PATH ] = {};
566+ if (GetModuleFileNameW (gmodDll, buf, MAX_PATH )) return buf;
567+ }
568+ return FindGmodDll ();
569+ }
570+
571+ void RefreshLocalGmodVersion ()
572+ {
573+ gmodLocalVersion = GetDllFileVersion (CurrentGmodDllPath ());
574+ gmodLocalVersionChecked = true ;
575+ }
576+
577+ // Where the toolbox keeps its managed gMod.dll.
578+ std::filesystem::path ManagedGmodDllPath ()
579+ {
580+ return Resources::GetPath (" gmod" ) / " gMod.dll" ;
581+ }
582+
583+ // Compare dotted version strings ("1.8.0.2"): >0 if a is newer, <0 if older, 0 if
584+ // equal. Missing trailing components count as 0, so "1.8" == "1.8.0.0".
585+ int CompareVersions (const std::string& a, const std::string& b)
586+ {
587+ const auto parse = [](const std::string& v) {
588+ std::vector<int > out;
589+ for (size_t i = 0 ; i < v.size ();) {
590+ if (v[i] < ' 0' || v[i] > ' 9' ) { ++i; continue ; }
591+ int n = 0 ;
592+ while (i < v.size () && v[i] >= ' 0' && v[i] <= ' 9' ) n = n * 10 + (v[i++] - ' 0' );
593+ out.push_back (n);
594+ }
595+ return out;
596+ };
597+ const auto va = parse (a), vb = parse (b);
598+ for (size_t i = 0 ; i < std::max (va.size (), vb.size ()); ++i) {
599+ const int x = i < va.size () ? va[i] : 0 ;
600+ const int y = i < vb.size () ? vb[i] : 0 ;
601+ if (x != y) return x < y ? -1 : 1 ;
602+ }
603+ return 0 ;
604+ }
605+
606+ // Re-initialise after the managed gMod.dll changes. gMod loads as a process-wide
607+ // d3d9 proxy and can't be hot-swapped once active, so if it is already loaded we
608+ // just ask the user to restart.
609+ void ReloadGmod ()
610+ {
611+ gmodLocalVersionChecked = false ;
612+ if (gmodReady) {
613+ statusMessage = " gMod updated. Restart Guild Wars to load the new version." ;
614+ return ;
615+ }
616+ gmodLoadAttempted = false ;
617+ InitGMod ();
521618 }
522619
523- void OnGmodDllFileChosen (const char * path)
620+ // Fetch the latest gMod release and, if it is newer than the managed copy (or none
621+ // exists), download it into the GWToolbox folder and load it. All network/disk work
622+ // runs on a worker thread; state changes are posted back to the main thread.
623+ void CheckAndUpdateGmod ()
624+ {
625+ if (gmodUpdateStep != GmodUpdateStep::Idle) return ;
626+ gmodUpdateStep = GmodUpdateStep::Checking;
627+ gmodUpdateStatus.clear ();
628+ Resources::EnqueueWorkerTask ([] {
629+ std::string response;
630+ unsigned int tries = 0 ;
631+ const auto url = " https://api.github.com/repos/gwdevhub/gMod/releases/latest" ;
632+ bool success = false ;
633+ do {
634+ success = Resources::Download (url, response);
635+ tries++;
636+ } while (!success && tries < 5 );
637+
638+ GModRelease release;
639+ const bool parsed = success && !glz::read<gmod_json_opts>(release, response);
640+
641+ std::string version, dll_url;
642+ if (parsed) {
643+ version = release.tag_name ;
644+ if (!version.empty () && (version[0 ] == ' v' || version[0 ] == ' V' )) version.erase (0 , 1 );
645+ for (const auto & asset : release.assets ) {
646+ if (asset.name == " gMod.dll" ) dll_url = asset.browser_download_url ;
647+ }
648+ }
649+
650+ if (!parsed || dll_url.empty ()) {
651+ Resources::EnqueueMainTask ([] {
652+ gmodUpdateStep = GmodUpdateStep::Idle;
653+ gmodUpdateStatus = " Could not check for gMod updates." ;
654+ });
655+ return ;
656+ }
657+
658+ const auto managed = ManagedGmodDllPath ();
659+ const std::string local = GetDllFileVersion (managed);
660+ if (!local.empty () && CompareVersions (version, local) <= 0 ) {
661+ // Already have the latest (or newer).
662+ Resources::EnqueueMainTask ([version, local] {
663+ gmodUpdateStep = GmodUpdateStep::Idle;
664+ gmodLatestVersion = version;
665+ gmodLocalVersion = local;
666+ gmodUpdateStatus = " gMod is up to date (" + version + " )." ;
667+ });
668+ return ;
669+ }
670+
671+ Resources::EnqueueMainTask ([version] {
672+ gmodUpdateStep = GmodUpdateStep::Downloading;
673+ gmodLatestVersion = version;
674+ gmodUpdateStatus = " Downloading gMod " + version + " ..." ;
675+ });
676+
677+ Resources::EnsureFolderExists (managed.parent_path ());
678+ std::wstring error;
679+ const bool ok = Resources::Download (managed, dll_url, error);
680+
681+ Resources::EnqueueMainTask ([ok, version, error] {
682+ gmodUpdateStep = GmodUpdateStep::Idle;
683+ if (!ok) {
684+ gmodUpdateStatus = " gMod download failed: " + std::string (error.begin (), error.end ());
685+ return ;
686+ }
687+ gmodLocalVersion = version;
688+ gmodUpdateStatus = " Updated gMod to " + version + " ." ;
689+ ReloadGmod ();
690+ });
691+ });
692+ }
693+
694+ // =========================================================================
695+ // UI helpers
696+ // =========================================================================
697+
698+ void OnTexmodFileChosen (const char * path)
524699 {
525700 if (!path) return ;
526701 std::filesystem::path p = path;
527702 if (!std::filesystem::exists (p)) return ;
528- gmodDllLocation = p;
529- statusMessage = " gMod DLL set to: " + gmodDllLocation.string ();
530- gmodLoadAttempted = false ;
531- InitGMod ();
703+ LoadTexturePack (p);
532704 }
533705
534706 void DrawStatusBar ()
@@ -543,15 +715,46 @@ namespace {
543715 gmodLoadAttempted = false ;
544716 InitGMod ();
545717 }
546- ImGui::SameLine ();
547- if (ImGui::SmallButton (" Browse" )) Resources::OpenFileDialog (OnGmodDllFileChosen, " dll" , Resources::GetSettingsFolderPath ().string ().c_str ());
548718 }
549719 if (!statusMessage.empty ()) {
550720 ImGui::SameLine ();
551721 ImGui::TextDisabled (" %s" , statusMessage.c_str ());
552722 }
553723 }
554724
725+ // gMod.dll status: gMod is downloaded and kept up to date automatically; show the
726+ // installed version against the latest release and offer a manual re-check.
727+ void DrawGmodSource ()
728+ {
729+ if (!gmodLocalVersionChecked) RefreshLocalGmodVersion ();
730+
731+ if (!gmodLocalVersion.empty ())
732+ ImGui::Text (" Installed gMod version: %s" , gmodLocalVersion.c_str ());
733+ else
734+ ImGui::TextDisabled (" No gMod.dll installed yet." );
735+
736+ if (!gmodLatestVersion.empty ()) {
737+ ImGui::SameLine ();
738+ if (!gmodLocalVersion.empty () && CompareVersions (gmodLatestVersion, gmodLocalVersion) <= 0 )
739+ ImGui::TextColored ({0 .4f , 1 .0f , 0 .4f , 1 .0f }, ICON_FA_CHECK " up to date" );
740+ else
741+ ImGui::TextColored ({1 .0f , 0 .85f , 0 .4f , 1 .0f }, " (latest: %s)" , gmodLatestVersion.c_str ());
742+ }
743+
744+ const bool busy = gmodUpdateStep != GmodUpdateStep::Idle;
745+ ImGui::BeginDisabled (busy);
746+ const char * label = gmodUpdateStep == GmodUpdateStep::Downloading ? " Downloading..."
747+ : gmodUpdateStep == GmodUpdateStep::Checking ? " Checking..."
748+ : " Check for gMod updates" ;
749+ if (ImGui::Button (label)) CheckAndUpdateGmod ();
750+ ImGui::EndDisabled ();
751+ if (ImGui::IsItemHovered ())
752+ ImGui::SetTooltip (" gMod is downloaded automatically and kept in your GWToolbox folder.\n Click to check for a newer version now." );
753+
754+ if (!gmodUpdateStatus.empty ())
755+ ImGui::TextDisabled (" %s" , gmodUpdateStatus.c_str ());
756+ }
757+
555758 void DrawPackList ()
556759 {
557760 ImGui::Text (" Texture packs:" );
@@ -953,6 +1156,8 @@ void TexmodModule::DrawSettingsInternal()
9531156{
9541157 DrawStatusBar ();
9551158 ImGui::Separator ();
1159+ DrawGmodSource ();
1160+ ImGui::Separator ();
9561161 DrawPackList ();
9571162 ImGui::Separator ();
9581163 if (ImGui::Button (" Add texture pack" )) Resources::OpenFileDialog (OnTexmodFileChosen, " tpf,zip,dds" , Resources::GetSettingsFolderPath ().string ().c_str ());
@@ -964,7 +1169,6 @@ void TexmodModule::DrawSettingsInternal()
9641169void TexmodModule::LoadSettings (ToolboxIni* ini)
9651170{
9661171 ToolboxModule::LoadSettings (ini);
967- gmodDllLocation = ini->GetValue (INI_SECTION , " gmod_dll_location" , " " );
9681172 const int count = ini->GetLongValue (INI_SECTION , INI_PACK_COUNT , 0 );
9691173 packs.clear ();
9701174 for (int i = 0 ; i < count; i++) {
@@ -979,12 +1183,17 @@ void TexmodModule::LoadSettings(ToolboxIni* ini)
9791183 // gMod is not ready yet (no device); the restored enabled flags are pushed to
9801184 // it later by RestoreLoadedPacks(). This call is a no-op until then.
9811185 SyncExternalPacks ();
1186+
1187+ // Only fetch gMod for users who actually use texture packs: if any are configured,
1188+ // check GitHub and download a newer gMod automatically. Loading (Update ->
1189+ // InitGMod) then picks up whatever copy is in the GWToolbox folder. Users with no
1190+ // packs get the download lazily the moment they add their first one.
1191+ if (!packs.empty ()) CheckAndUpdateGmod ();
9821192}
9831193
9841194void TexmodModule::SaveSettings (ToolboxIni* ini)
9851195{
9861196 ToolboxModule::SaveSettings (ini);
987- ini->SetValue (INI_SECTION , " gmod_dll_location" , gmodDllLocation.string ().c_str ());
9881197 ini->SetLongValue (INI_SECTION , INI_PACK_COUNT , static_cast <long >(packs.size ()));
9891198 for (size_t i = 0 ; i < packs.size (); i++) {
9901199 const std::string key = std::string (INI_PACK_PATH ) + std::to_string (i);
0 commit comments