WoW Model Viewer
Your premiere tool for viewing, equipping and animating World of Warcraft models.
Loading...
Searching...
No Matches
GameLoader.cpp
Go to the documentation of this file.
1#include "GameLoader.h"
2#include "AppState.h"
3
4#ifdef _WIN32
5#include <windows.h>
6#endif
7
8#include <algorithm>
9#include <cstring>
10#include <fstream>
11#include <mutex>
12#include <string>
13#include <thread>
14
15#include "Logger.h"
16#include "Game.h"
17#include "WoWFolder.h"
18#include "WoWDatabase.h"
19#include "HttpClient.h"
20#include "CharTexture.h"
21#include "RaceInfos.h"
22#include "FileBrowserPanel.h"
23#include "string_utils.h"
24
25namespace GameLoader
26{
27
28// ---- Path helper ----------------------------------------------------------
29
30std::filesystem::path getApplicationDirPath()
31{
32#ifdef _WIN32
33 wchar_t buf[MAX_PATH]{};
34 GetModuleFileNameW(nullptr, buf, MAX_PATH);
35 return std::filesystem::path(buf).parent_path();
36#else
37 return std::filesystem::current_path();
38#endif
39}
40
41// ---- Thread-safe load status helpers --------------------------------------
42
43void setLoadStatus(const std::string& s, AppState& app)
44{
45 std::lock_guard<std::mutex> lock(app.loading.loadStatusMutex);
46 app.loading.loadStatus = s;
47}
48
49std::string getLoadStatus(AppState& app)
50{
51 std::lock_guard<std::mutex> lock(app.loading.loadStatusMutex);
52 return app.loading.loadStatus;
53}
54
55// ---- Support-file download ------------------------------------------------
56
57static bool downloadFile(const std::string& url, const std::filesystem::path& dest,
58 const std::string& label, AppState& app,
59 bool replaceSeparators = false)
60{
61 LOG_INFO << "Downloading " << label << "...";
62 setLoadStatus("Downloading " + label + "...", app);
63
64 const auto resp = HttpClient::Get(url);
65 if (!resp.success)
66 {
67 LOG_ERROR << "Failed to download " << label << ": " << resp.error;
68 setLoadStatus("Failed to download " + label + ": " + resp.error, app);
69 return false;
70 }
71
72 std::string content = resp.body;
73 if (replaceSeparators)
74 std::replace(content.begin(), content.end(), ' ', ';');
75
76 std::ofstream file(dest, std::ios::binary);
77 if (!file.is_open())
78 {
79 LOG_ERROR << "Failed to write " << label << " to " << dest.string();
80 setLoadStatus("Failed to write " + label, app);
81 return false;
82 }
83
84 file.write(content.data(), content.size());
85 file.close();
86 LOG_INFO << label << " saved to " << dest.string();
87 return true;
88}
89
91{
92 namespace fs = std::filesystem;
93
94 const fs::path appDir = getApplicationDirPath();
95 const fs::path listfilePath = appDir / "listfile.csv";
96 const fs::path keysPath = appDir / "extraEncryptionKeys.csv";
97
98 std::error_code ec;
99 const bool listfileMissing = !fs::exists(listfilePath, ec) || fs::file_size(listfilePath, ec) == 0;
100 const bool keysMissing = !fs::exists(keysPath, ec) || fs::file_size(keysPath, ec) == 0;
101
102 if (listfileMissing)
103 {
104 if (!downloadFile("https://github.com/wowdev/wow-listfile/releases/latest/download/community-listfile.csv",
105 listfilePath, "listfile.csv", app))
106 return false;
107 }
108
109 if (keysMissing)
110 {
111 if (!downloadFile("https://raw.githubusercontent.com/wowdev/TACTKeys/master/WoW.txt",
112 keysPath, "extraEncryptionKeys.csv", app, true))
113 return false;
114 }
115
116 // Ensure the dbdefs cache directory exists (DBDs are downloaded on demand)
117 const fs::path dbdDir = appDir / "games" / "wow" / "dbdefs";
118 fs::create_directories(dbdDir, ec);
119
120 return true;
121}
122
123// ---- InitDatabase ---------------------------------------------------------
124
125static void initDatabase(AppState& app)
126{
127 LOG_INFO << "Initializing Databases...";
128 setLoadStatus("Initializing database...", app);
129
130 namespace fs = std::filesystem;
131 const fs::path appDir = getApplicationDirPath();
132 const fs::path cachePath = appDir / "wowdb.sqlite";
133 const fs::path versionPath = appDir / "wowdb.version";
134
135 const std::string currentVersion = GAMEDIRECTORY.version();
136 bool cacheValid = false;
137
138 const bool enableDbCache = app.settings.enableDbCache;
139
140 std::error_code ec;
141 if (enableDbCache &&
142 fs::exists(cachePath, ec) && fs::file_size(cachePath, ec) > 0 &&
143 fs::exists(versionPath, ec))
144 {
145 std::ifstream vf(versionPath);
146 std::string cachedVersion;
147 if (std::getline(vf, cachedVersion) && cachedVersion == currentVersion)
148 {
149 cacheValid = true;
150 LOG_INFO << "Database cache is valid for version " << currentVersion;
151 }
152 }
153
154 if (!cacheValid)
155 {
156 LOG_INFO << "Database cache miss — will rebuild from DB2 files.";
157 fs::remove(cachePath, ec);
158 fs::remove(versionPath, ec);
159 }
160
161 if (enableDbCache)
162 {
163 GAMEDATABASE.setCachePath(cachePath.string());
164 GAMEDATABASE.setFastMode();
165 }
166
167 const fs::path dbdDir = appDir / "games" / "wow" / "dbdefs";
168
169 GAMEDATABASE.setDbdBaseUrl(
170 "https://raw.githubusercontent.com/wowdev/WoWDBDefs/refs/heads/master/definitions/%s.dbd");
171
172 GAMEDATABASE.setManifestUrl(
173 "https://raw.githubusercontent.com/wowdev/WoWDBDefs/refs/heads/master/manifest.json");
174 GAMEDATABASE.downloadAndParseManifest();
175
176 LOG_INFO << "Attempting on-demand DBD-based database init from" << dbdDir.string();
177 if (!GAMEDATABASE.initFromDBD(dbdDir.string(), currentVersion))
178 {
179 app.loading.initDB = false;
180 LOG_ERROR << "Database initialization failed!";
181 setLoadStatus("Database initialization failed!", app);
182 fs::remove(cachePath, ec);
183 fs::remove(versionPath, ec);
184 return;
185 }
186
187 if (enableDbCache && !cacheValid)
188 {
189 std::ofstream vf(versionPath, std::ios::trunc);
190 vf << currentVersion;
191 LOG_INFO << "Database cache written for version " << currentVersion;
192 }
193
194 LOG_INFO << "Database initialization succeeded.";
195 app.loading.loadProgress = 0.60f;
196
197 LOG_INFO << "initDatabase: CharTexture::initRegions...";
199 app.loading.loadProgress = 0.65f;
200
201 LOG_INFO << "initDatabase: RaceInfos::init...";
203 app.loading.loadProgress = 0.70f;
204
205 app.loading.initDB = true;
206
207 LOG_INFO << "initDatabase: skipping Creature table (disabled).";
208 app.loading.loadProgress = 0.80f;
209
210 LOG_INFO << "initDatabase: skipping Item/ItemSparse loading (disabled).";
211 app.loading.loadProgress = 0.90f;
212 LOG_INFO << "Finished initiating database files.";
213}
214
215// ---- loadWoW (CASC + listfile + database) ---------------------------------
216
217static void loadWoW(const core::GameConfig& config, AppState& app)
218{
219 app.loading.loadProgress = 0.0f;
220 setLoadStatus("Opening CASC storage...", app);
221
222 if (!GAMEDIRECTORY.setConfig(config))
223 {
224 LOG_ERROR << "Could not load WoW Data folder (error "
225 << GAMEDIRECTORY.lastError() << ").";
226 setLoadStatus("Failed to open CASC storage (error "
227 + std::to_string(GAMEDIRECTORY.lastError()) + ").", app);
228 app.loading.loadThreadDone = true;
229 return;
230 }
231
232 LOG_INFO << "Major version: " << GAMEDIRECTORY.majorVersion();
233 app.loading.loadProgress = 0.05f;
234
235 const std::string baseConfigFolder = "games/wow/";
236 LOG_INFO << "Using config folder: " << baseConfigFolder;
237 core::Game::instance().setConfigFolder(baseConfigFolder);
238
239 setLoadStatus("Loading file list...", app);
240 app.loading.loadProgress = 0.10f;
241 GAMEDIRECTORY.setProgressCallback([&app](int current, int total) {
242 if (total > 0)
243 app.loading.loadProgress = 0.10f + 0.40f * static_cast<float>(current) / static_cast<float>(total);
244 });
245 GAMEDIRECTORY.initFromListfile("../../listfile.csv");
246 GAMEDIRECTORY.setProgressCallback(nullptr);
247 app.loading.loadProgress = 0.50f;
248
249 setLoadStatus("Initializing database...", app);
250 app.loading.loadProgress = 0.55f;
251 initDatabase(app);
252
253 if (!app.loading.initDB)
254 {
255 app.loading.loadThreadDone = true;
256 return;
257 }
258
259 app.loading.loadProgress = 1.0f;
260 setLoadStatus("World of Warcraft loaded successfully.", app);
261 app.loading.loadThreadSuccess = true;
262 app.loading.loadThreadDone = true;
263}
264
265// ---- Public API -----------------------------------------------------------
266
268{
269 setLoadStatus("Checking support files...", app);
271 {
272 app.loading.loadThreadDone = true;
273 return;
274 }
275 loadWoW(config, app);
276}
277
279{
280 app.loading.loadProgress = 0.0f;
281 app.loading.loadThreadDone = false;
282 app.loading.loadThreadSuccess = false;
283 app.loading.loadInProgress = true;
284
285 if (app.loading.loadThread.joinable())
286 app.loading.loadThread.join();
287
288 app.loading.loadThread = std::thread(loadWoWThreadFunc, config, std::ref(app));
289}
290
292{
293 if (!app.loading.loadInProgress || !app.loading.loadThreadDone.load())
294 return;
295
296 if (app.loading.loadThread.joinable())
297 app.loading.loadThread.join();
298
299 app.loading.loadInProgress = false;
300
301 if (app.loading.loadThreadSuccess.load())
302 {
303 app.loading.isWoWLoaded = true;
305 LOG_INFO << "World of Warcraft loaded successfully. Version: "
306 << GAMEDIRECTORY.version() << " Locale: " << GAMEDIRECTORY.locale();
307 app.settings.save();
308 }
309}
310
312{
314 return;
315
317
318 app.loading.loadInProgress = true;
319 app.loading.loadProgress = 0.0f;
320 setLoadStatus("Validating game path...", app);
321
322 namespace fs = std::filesystem;
323 std::string path = app.settings.gamePath;
324 if (path.empty() || !fs::is_directory(path))
325 {
326 setLoadStatus("Please set a valid WoW Data folder path in Options > Settings.", app);
327 app.loading.loadInProgress = false;
328 return;
329 }
330
331 if (path.back() != '/' && path.back() != '\\')
332 path += '\\';
333
334 {
335 std::string lower = core::toLower(path);
336 if (lower.find("data\\") == std::string::npos && lower.find("data/") == std::string::npos)
337 path += "Data\\";
338 }
339 app.settings.gamePath = path;
340
341 if (!core::Game::instance().initDone())
342 core::Game::instance().init(std::make_unique<wow::WoWFolder>(app.settings.gamePath), std::make_unique<wow::WoWDatabase>());
343
344 app.loading.pendingConfigs = GAMEDIRECTORY.configsFound();
345
346 if (app.loading.pendingConfigs.empty())
347 {
348 LOG_ERROR << "No locale found in WoW folder.";
349 setLoadStatus("No locale found in the WoW folder.", app);
350 app.loading.loadInProgress = false;
351 return;
352 }
353
354 if (app.loading.pendingConfigs.size() == 1)
355 {
357 }
358 else
359 {
360 app.loading.selectedConfig = 0;
361 app.loading.showConfigPopup = true;
362 app.loading.loadInProgress = false;
363 }
364}
365
366} // namespace GameLoader
#define GAMEDATABASE
Definition Game.h:10
#define GAMEDIRECTORY
Definition Game.h:9
#define LOG_ERROR
Definition Logger.h:11
#define LOG_INFO
Definition Logger.h:10
static void initRegions()
static void init()
Initialise the global race info table from the database.
Definition RaceInfos.cpp:12
Describes a detected game installation (locale, version, product).
Definition GameFolder.h:16
static Game & instance()
Access the singleton instance (Meyers singleton).
Definition Game.h:22
void init(std::unique_ptr< core::GameFolder > folder, std::unique_ptr< core::GameDatabase > db)
Initialise with the given folder and database backends.
Definition Game.cpp:7
void setConfigFolder(const std::string &folder)
Definition Game.h:40
void markDirty()
Mark the file tree as dirty so it will be rebuilt on the next draw().
std::filesystem::path getApplicationDirPath()
Return the directory containing the running executable.
void setLoadStatus(const std::string &s, AppState &app)
Thread-safe load-status setters / getters (lock app.loadStatusMutex).
static void initDatabase(AppState &app)
static bool downloadFile(const std::string &url, const std::filesystem::path &dest, const std::string &label, AppState &app, bool replaceSeparators=false)
void pollAsyncLoad(AppState &app)
static void loadWoW(const core::GameConfig &config, AppState &app)
static bool checkAndDownloadSupportFiles(AppState &app)
void launchLoadThread(const core::GameConfig &config, AppState &app)
Spawn the background loading thread for the given config.
std::string getLoadStatus(AppState &app)
void loadWoWThreadFunc(core::GameConfig config, AppState &app)
void beginLoadWoW(AppState &app)
Response Get(const std::string &url, const ProgressCallback &progress=nullptr)
Perform a synchronous HTTP(S) GET request.
std::string toLower(const std::string &s)
bool enableDbCache
Definition AppSettings.h:13
std::string gamePath
Definition AppSettings.h:10
void save() const
Persist to Config.ini and save the ImGui layout to disk.
Top-level aggregate of all mutable application state.
Definition AppState.h:261
LoadingState loading
Definition AppState.h:263
AppSettings settings
Definition AppState.h:269
std::atomic< bool > loadThreadDone
Definition AppState.h:98
std::atomic< float > loadProgress
Definition AppState.h:94
std::string pathBuf
Definition AppState.h:100
bool showConfigPopup
Definition AppState.h:101
bool isWoWLoaded
Definition AppState.h:91
std::thread loadThread
Definition AppState.h:96
std::atomic< bool > initDB
Definition AppState.h:92
std::atomic< bool > loadThreadSuccess
Definition AppState.h:99
bool loadInProgress
Definition AppState.h:95
int selectedConfig
Definition AppState.h:103
std::mutex loadStatusMutex
Definition AppState.h:97
std::vector< core::GameConfig > pendingConfigs
Definition AppState.h:102
std::string loadStatus
Definition AppState.h:93