Skip to content

Commit e92c141

Browse files
3vcloudclaude
andcommitted
fix(dat): always map the client's handle; retry until it's ready
Drop the "open our own handle" fast path - toolbox only runs injected in the client, which holds Gw.dat exclusively, so our own open never wins. Always locate and map the client's open handle. Because a texture request could momentarily race ahead of the client opening the dat, make loading retryable: EnsureLoaded now retries the scan on each call (mutex-guarded, atomic fast-path once loaded) instead of latching the first failure via call_once, and ParseIndex resets its state so a retry starts clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6ddf8ab commit e92c141

3 files changed

Lines changed: 40 additions & 53 deletions

File tree

GWToolboxdll/Modules/GwDatTextureModule.cpp

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,14 @@ bool GwDatTextureModule::ReadDatFile(const wchar_t* file_name, std::vector<uint8
197197
return false;
198198
auto& dat = GwDatArchive::Instance();
199199
const bool ok = dat.ReadFile(file_id, *bytes_out, stream_id);
200-
// Report the archive outcome exactly once.
201-
static std::once_flag reported;
202-
std::call_once(reported, [&dat] {
203-
if (!dat.Loaded()) {
204-
Log::LogW(L"[GwDat] Could not open dat '%s' (Win32 error %lu)",
205-
dat.DatPath().c_str(), dat.LastError());
206-
}
207-
else {
208-
Log::LogW(L"[GwDat] Opened dat '%s' via %s handle", dat.DatPath().c_str(),
209-
dat.UsingGameHandle() ? L"the client's" : L"our own");
210-
}
211-
});
200+
// Log once, the first time the archive is available (early failures are
201+
// transient - the client may not have opened the dat yet).
202+
if (dat.Loaded()) {
203+
static std::once_flag reported;
204+
std::call_once(reported, [&dat] {
205+
Log::LogW(L"[GwDat] Mapped client's Gw.dat ('%s')", dat.DatPath().c_str());
206+
});
207+
}
212208
return ok;
213209
}
214210

GWToolboxdll/Utils/GwDat/GwDatArchive.cpp

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,37 +81,21 @@ GwDatArchive& GwDatArchive::Instance()
8181

8282
bool GwDatArchive::EnsureLoaded()
8383
{
84-
std::call_once(m_load_once, [this] { m_loaded = ParseIndex(); });
85-
return m_loaded;
84+
if (m_loaded.load(std::memory_order_acquire))
85+
return true;
86+
std::lock_guard<std::mutex> lock(m_load_mutex);
87+
if (!m_loaded.load(std::memory_order_relaxed) && ParseIndex())
88+
m_loaded.store(true, std::memory_order_release);
89+
return m_loaded.load(std::memory_order_relaxed);
8690
}
8791

8892
bool GwDatArchive::AcquireMapping()
8993
{
90-
// Fast path: open and map our own handle. Works when the client isn't holding
91-
// the dat exclusively (e.g. it isn't running).
92-
const HANDLE own = CreateFileW(m_dat_path.c_str(), GENERIC_READ,
93-
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
94-
nullptr, OPEN_EXISTING, 0, nullptr);
95-
if (own != INVALID_HANDLE_VALUE) {
96-
const HANDLE mapping = MapDat(own);
97-
LARGE_INTEGER sz;
98-
if (GetFileSizeEx(own, &sz))
99-
m_file_size = sz.QuadPart;
100-
CloseHandle(own);
101-
if (mapping) {
102-
m_mapping = mapping;
103-
m_using_game_handle = false;
104-
return true;
105-
}
106-
}
107-
else {
108-
m_last_error = GetLastError();
109-
}
110-
11194
// The running client opens Gw.dat read/write with no sharing (NtFile.cpp), so
112-
// our own open fails. It is bound to the client's I/O completion port, so we
113-
// must not ReadFile it - instead map the client's already-open handle. Scan
114-
// the process handle table for the disk handle that maps to the archive magic.
95+
// we can't open our own handle, and it binds the handle to its I/O completion
96+
// port, so we must not ReadFile it either. Instead map the client's already-open
97+
// handle: scan the process handle table for the disk handle that maps to the
98+
// archive magic. If none is found the client hasn't opened the dat yet.
11599
for (ULONG_PTR v = 4; v <= 0x40000; v += 4) {
116100
const HANDLE h = reinterpret_cast<HANDLE>(v);
117101
if (GetFileType(h) != FILE_TYPE_DISK)
@@ -123,15 +107,24 @@ bool GwDatArchive::AcquireMapping()
123107
if (GetFileSizeEx(h, &sz))
124108
m_file_size = sz.QuadPart;
125109
m_mapping = mapping;
126-
m_using_game_handle = true;
127110
return true;
128111
}
129112
return false;
130113
}
131114

132115
bool GwDatArchive::ParseIndex()
133116
{
134-
m_dat_path = ResolveDatPath();
117+
// Reset any state from a previous failed attempt so retries start clean.
118+
if (m_mapping) {
119+
CloseHandle(reinterpret_cast<HANDLE>(m_mapping));
120+
m_mapping = nullptr;
121+
}
122+
m_file_size = 0;
123+
m_slots.clear();
124+
m_fileid_to_slot.clear();
125+
126+
if (m_dat_path.empty())
127+
m_dat_path = ResolveDatPath();
135128
if (m_dat_path.empty())
136129
return false;
137130

GWToolboxdll/Utils/GwDat/GwDatArchive.h

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <atomic>
34
#include <cstdint>
45
#include <mutex>
56
#include <string>
@@ -18,16 +19,15 @@ class GwDatArchive {
1819
public:
1920
static GwDatArchive& Instance();
2021

21-
// Opens and indexes Gw.dat on first use. Thread-safe and idempotent.
22+
// Indexes Gw.dat by mapping the client's open handle. Retries on each call
23+
// until it succeeds, so an early request before the client has opened the dat
24+
// doesn't latch failure. Thread-safe.
2225
bool EnsureLoaded();
2326

24-
// Diagnostics (valid after EnsureLoaded()): the resolved dat path, whether the
25-
// index loaded, the Win32 error from a failing CreateFile (0 if none), and
26-
// whether we fell back to the client's own (exclusively-held) dat handle.
27+
// Diagnostics (valid after EnsureLoaded()): the resolved dat path and whether
28+
// the index has loaded.
2729
const std::wstring& DatPath() const { return m_dat_path; }
28-
bool Loaded() const { return m_loaded; }
29-
unsigned long LastError() const { return m_last_error; }
30-
bool UsingGameHandle() const { return m_using_game_handle; }
30+
bool Loaded() const { return m_loaded.load(std::memory_order_acquire); }
3131

3232
// Reads the decompressed bytes for a GW file id (the dat "file number", the
3333
// same value ArenaNetFileParser::FileHashToFileId produces). stream_id is an
@@ -71,13 +71,11 @@ class GwDatArchive {
7171
#pragma pack(pop)
7272
static_assert(sizeof(MftEntry) == 24, "on-disk MFT entry must be 24 bytes");
7373

74-
bool ParseIndex(); // reads header + MFT once, driven by EnsureLoaded's call_once
75-
bool AcquireMapping(); // maps the dat via our own handle, or the client's open one
74+
bool ParseIndex(); // resets state, then reads header + MFT; safe to retry
75+
bool AcquireMapping(); // maps the client's open dat handle; false if not found yet
7676

77-
std::once_flag m_load_once;
78-
bool m_loaded = false;
79-
bool m_using_game_handle = false;
80-
unsigned long m_last_error = 0; // Win32 error from a failed CreateFile
77+
std::mutex m_load_mutex;
78+
std::atomic<bool> m_loaded{false};
8179
void* m_mapping = nullptr; // file-mapping HANDLE for the dat, kept for the archive lifetime
8280
long long m_file_size = 0; // dat size captured when the mapping was created
8381
std::wstring m_dat_path;

0 commit comments

Comments
 (0)