Skip to content

Commit 2571964

Browse files
committed
Texmod: auto-download gMod from GitHub, gated on having packs
Check gMod's latest GitHub release and download gMod.dll into the GWToolbox folder when it's newer than the managed copy (or missing), then load it. Version is read from the dll's PE file-version resource and compared component-wise so only newer builds are fetched. The download is gated on actually using texture packs: it runs on startup only when packs are configured, and lazily the first time a pack is added while gMod isn't loaded. A manual "Check for gMod updates" button is always available. Drops the Browse/choose-your-own dll path and the gmod_dll_location setting; FindGmodDll now prefers the toolbox-managed copy.
1 parent 4060ae3 commit 2571964

1 file changed

Lines changed: 234 additions & 25 deletions

File tree

GWToolboxdll/Modules/TexmodModule.cpp

Lines changed: 234 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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.\nClick 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()
9641169
void 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

9841194
void 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

Comments
 (0)