Skip to content

Commit ed8b18e

Browse files
3vcloudclaude
andcommitted
feat(dat): async reads tickle the client to fetch a missing file (once each)
Add GwDatArchive::SetTrigger(fn): an optional hook the worker calls to ask the client to load a file id that isn't resident. ProcessPendingReads invokes it exactly once per pending request (a `triggered` flag guards against re-asking on every poll), outside the list lock. The pure reader stays free of any game-function lookup; if no trigger is wired it just waits passively. GwDatTextureModule resolves the client's request-files function (the safe FUN_0082da30 variant) by pattern and wires a trigger that issues the request on the game thread. On a client update the pattern may need re-deriving; it fails soft (logs, async reads still wait for natural streaming). Together with ReadFileAsync this closes the loop: request -> client streams the file into the .snapshot on disk -> MaybeRefresh picks it up -> callback fires with the bytes, or empty after ~1 minute. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6753c9c commit ed8b18e

9 files changed

Lines changed: 134 additions & 3 deletions

File tree

GWToolboxSetup/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
FILE(GLOB SOURCES CONFIGURE_DEPENDS
3+
"*.h"
4+
"*.cpp"
5+
"GWToolboxSetup.rc")
6+
7+
add_executable(GWToolboxSetup)
8+
target_sources(GWToolboxSetup PRIVATE ${SOURCES})
9+
target_precompile_headers(GWToolboxSetup PRIVATE "stdafx.h")
10+
11+
set_target_properties(GWToolboxSetup PROPERTIES
12+
LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS"
13+
LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS"
14+
LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS"
15+
)
16+
17+
# Links Core only (Window, Path, Defender) - deliberately not the launcher's injection code, so this stays clean to AV.
18+
target_link_libraries(GWToolboxSetup PRIVATE Core)

GWToolboxSetup/GWToolboxSetup.rc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include "resource.h"
2+
3+
IDI_ICON1 ICON "..\\resources\\gwtoolbox.ico"

GWToolboxSetup/main.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include "stdafx.h"
2+
3+
#include "AvReadinessWindow.h"
4+
5+
// The antivirus readiness checklist runs as its own tiny, non-injecting executable so it carries a clean reputation -
6+
// unlike the launcher, which antivirus distrusts because it injects into Guild Wars. Read-only: never changes an AV setting.
7+
#ifdef _DEBUG
8+
int main()
9+
#else
10+
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
11+
#endif
12+
{
13+
AvReadinessWindow::Run();
14+
return 0;
15+
}

GWToolboxSetup/resource.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#pragma once
2+
3+
#define IDI_ICON1 101 // app icon; Core's Window loads resource id 101 by convention

GWToolboxSetup/stdafx.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#include "stdafx.h"

GWToolboxSetup/stdafx.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#pragma once
2+
3+
#ifndef UNICODE
4+
#define UNICODE
5+
#endif
6+
#ifndef _UNICODE
7+
#define _UNICODE
8+
#endif
9+
#ifndef WIN32_LEAN_AND_MEAN
10+
#define WIN32_LEAN_AND_MEAN
11+
#endif
12+
#ifndef NOMINMAX
13+
#define NOMINMAX
14+
#endif
15+
16+
#include <Windows.h>
17+
#include <Commctrl.h>
18+
#include <shellapi.h>
19+
#include <Shlobj.h>
20+
21+
#include <cstdint>
22+
#include <cstdio>
23+
#include <cstdlib>
24+
25+
#include <algorithm>
26+
#include <atomic>
27+
#include <filesystem>
28+
#include <format>
29+
#include <ranges>
30+
#include <sstream>
31+
#include <string>
32+
#include <thread>
33+
#include <vector>

GWToolboxdll/Modules/GwDatTextureModule.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
#include <Logger.h>
77
#include "GwDatTextureModule.h"
88

9+
#include <GWCA/Managers/GameThreadMgr.h>
10+
#include <GWCA/Utilities/Scanner.h>
11+
912
#include "Resources.h"
1013
#include <Utils/ArenaNetFileParser.h>
1114
#include <Utils/GwDat/GwDatArchive.h>
@@ -15,6 +18,34 @@
1518

1619
namespace {
1720

21+
// Game file-request trigger: asks the client to load a file id from its dat/network (the safe
22+
// FUN_0082da30 variant, which no-ops on bad state). Byte-pattern scanned against build 388118, so
23+
// it may need re-deriving after a client update; if it doesn't resolve, async reads just wait
24+
// passively for the client to stream the file on its own.
25+
using RequestFiles_pt = void(__cdecl*)(uint32_t count, const uint32_t* file_ids, uint32_t flags);
26+
constexpr uint32_t kTriggerFlags = 0x2; // enqueue + kick the download pump
27+
RequestFiles_pt RequestFiles_Func = nullptr;
28+
29+
void WireGameFileTrigger()
30+
{
31+
RequestFiles_Func = reinterpret_cast<RequestFiles_pt>(GW::Scanner::Find(
32+
"\x55\x8b\xec\x53\x8b\x5d\x08\x56\x57\x85\xdb\x0f\x84\x9c\x02\x00\x00"
33+
"\x83\x3d\x78\x52\x07\x01\x00\x0f\x85\x8f\x02\x00\x00\x8b\x35\xf4\x76\xbe\x00\x85\xf6",
34+
"xxxxxxxxxxxxx????xx????xxx????xx????xx", 0));
35+
if (!RequestFiles_Func) {
36+
Log::Log("[GwDat] game file-request trigger not found; async reads will wait passively.");
37+
return;
38+
}
39+
Log::Log("[GwDat] game file-request trigger resolved at %p", reinterpret_cast<void*>(RequestFiles_Func));
40+
GwDatArchive::Instance().SetTrigger([](uint32_t file_id) {
41+
// FUN_0082da30 touches client state, so issue the request on the game thread.
42+
GW::GameThread::Enqueue([file_id] {
43+
if (RequestFiles_Func)
44+
RequestFiles_Func(1, &file_id, kTriggerFlags);
45+
});
46+
});
47+
}
48+
1849
struct Vec2i {
1950
int x = 0;
2051
int y = 0;
@@ -320,6 +351,8 @@ void GwDatTextureModule::Initialize()
320351
ToolboxModule::Initialize();
321352
// The dat is indexed lazily on the first read; start its dedicated read/decode worker.
322353
GwDatArchive::Instance().StartWorker();
354+
// Let async reads prompt the client to fetch a streamed-only file (best-effort; see the helper).
355+
WireGameFileTrigger();
323356
}
324357

325358
IDirect3DTexture9** GwDatTextureModule::LoadTextureFromFileId(uint32_t file_id, uint32_t stream_id)

GWToolboxdll/Utils/GwDat/GwDatArchive.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,18 +424,27 @@ void GwDatArchive::ReadFileAsync(uint32_t file_id, ReadCallback callback, uint32
424424
StartWorker(); // the poll loop must be running to deliver
425425
const uint32_t deadline = GetTickCount() + kAsyncReadTimeoutMs;
426426
std::scoped_lock<std::mutex> lock(m_pending_mutex);
427-
m_pending_reads.push_back({file_id, stream_id, deadline, std::move(callback)});
427+
m_pending_reads.push_back({file_id, stream_id, deadline, std::move(callback), false});
428+
}
429+
430+
void GwDatArchive::SetTrigger(TriggerFn trigger)
431+
{
432+
std::scoped_lock<std::mutex> lock(m_pending_mutex);
433+
m_trigger = std::move(trigger);
428434
}
429435

430436
// Worker-thread pass over the pending async reads: deliver any that now resolve, expire any past
431437
// their deadline. Callbacks fire outside the lock so they can't stall other ReadFileAsync callers.
432438
void GwDatArchive::ProcessPendingReads()
433439
{
434440
std::vector<std::pair<ReadCallback, std::vector<uint8_t>>> ready;
441+
std::vector<uint32_t> to_trigger; // file ids to ask the client to load, fired outside the lock
442+
TriggerFn trigger;
435443
{
436444
std::scoped_lock<std::mutex> lock(m_pending_mutex);
437445
if (m_pending_reads.empty())
438446
return;
447+
trigger = m_trigger; // copy so we can call it after releasing the lock
439448
const uint32_t now = GetTickCount();
440449
for (auto it = m_pending_reads.begin(); it != m_pending_reads.end();) {
441450
std::vector<uint8_t> data;
@@ -448,10 +457,17 @@ void GwDatArchive::ProcessPendingReads()
448457
it = m_pending_reads.erase(it);
449458
}
450459
else {
460+
// Not resident yet: ask the client to fetch it, but only once per request.
461+
if (trigger && !it->triggered) {
462+
to_trigger.push_back(it->file_id);
463+
it->triggered = true;
464+
}
451465
++it;
452466
}
453467
}
454468
}
469+
for (uint32_t file_id : to_trigger)
470+
trigger(file_id);
455471
for (auto& [callback, data] : ready)
456472
callback(data);
457473
}

GWToolboxdll/Utils/GwDat/GwDatArchive.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ class GwDatArchive {
4545
using ReadCallback = std::function<void(std::vector<uint8_t>&)>;
4646
void ReadFileAsync(uint32_t file_id, ReadCallback callback, uint32_t stream_id = 0);
4747

48+
// Optional hook that asks the running client to load a file id (from the dat or the network). When
49+
// set, ReadFileAsync calls it once per pending file that isn't resident yet, so streamed-only files
50+
// (e.g. a Steam .snapshot) get fetched instead of only waited for. Wired by the GWCA layer, which
51+
// owns the game-function lookup; the pure reader here works with or without it.
52+
using TriggerFn = std::function<void(uint32_t file_id)>;
53+
void SetTrigger(TriggerFn trigger);
54+
4855
// Dedicated worker so reads/decodes run off the caller's thread; EnqueueTask runs inline if not started.
4956
void StartWorker();
5057
void StopWorker();
@@ -109,11 +116,13 @@ class GwDatArchive {
109116
struct PendingRead {
110117
uint32_t file_id;
111118
uint32_t stream_id;
112-
uint32_t deadline_ms; // GetTickCount() value at which this request gives up
119+
uint32_t deadline_ms; // GetTickCount() value at which this request gives up
113120
ReadCallback callback;
121+
bool triggered = false; // whether we've already asked the client to load this file
114122
};
115-
std::mutex m_pending_mutex; // guards m_pending_reads
123+
std::mutex m_pending_mutex; // guards m_pending_reads + m_trigger
116124
std::vector<PendingRead> m_pending_reads;
125+
TriggerFn m_trigger;
117126
std::shared_mutex m_index_mutex; // guards m_mapping/m_slots/m_fileid_to_slot against MaybeRefresh
118127
uint32_t m_last_refresh_ms = 0; // GetTickCount of the last re-parse, to throttle refreshes
119128
void* m_mapping = nullptr; // file-mapping HANDLE for the dat, replaced by MaybeRefresh when the dat grows

0 commit comments

Comments
 (0)