mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 01:53:52 +00:00
AssetManager::loadDBC now probes for a .json sidecar in the same directory as the binary DBC the manifest resolves to. asset_extract --emit-json-dbc writes that sidecar on extraction, so the runtime client transparently falls back to it when the binary DBC is absent (e.g. open-format-only extraction for end-to-end format testing). Order: binary DBC > JSON sidecar > custom_zones JSON > CSV > error. Servers (AzerothCore/TrinityCore) only read the binary DBC, so this change is invisible to them — the wowee runtime is the only consumer that tries the JSON path. The PNG sidecar pickup (tryLoadPngOverride) was already in place from prior work — this completes the symmetric runtime-side wiring for both BLP→PNG and DBC→JSON open-format outputs.
720 lines
25 KiB
C++
720 lines
25 KiB
C++
#include "pipeline/asset_manager.hpp"
|
|
#include "core/logger.hpp"
|
|
#include "core/memory_monitor.hpp"
|
|
#include "core/profiler.hpp"
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <unordered_set>
|
|
|
|
#include "stb_image.h"
|
|
|
|
namespace wowee {
|
|
namespace pipeline {
|
|
|
|
namespace {
|
|
size_t parseEnvSizeMB(const char* name) {
|
|
const char* v = std::getenv(name);
|
|
if (!v || !*v) {
|
|
return 0;
|
|
}
|
|
char* end = nullptr;
|
|
unsigned long long mb = std::strtoull(v, &end, 10);
|
|
if (end == v || mb == 0) {
|
|
return 0;
|
|
}
|
|
if (mb > (std::numeric_limits<size_t>::max() / (1024ull * 1024ull))) {
|
|
return 0;
|
|
}
|
|
return static_cast<size_t>(mb);
|
|
}
|
|
|
|
size_t parseEnvCount(const char* name, size_t defValue) {
|
|
const char* v = std::getenv(name);
|
|
if (!v || !*v) {
|
|
return defValue;
|
|
}
|
|
char* end = nullptr;
|
|
unsigned long long n = std::strtoull(v, &end, 10);
|
|
if (end == v || n == 0) {
|
|
return defValue;
|
|
}
|
|
return static_cast<size_t>(n);
|
|
}
|
|
} // namespace
|
|
|
|
AssetManager::AssetManager() = default;
|
|
AssetManager::~AssetManager() {
|
|
shutdown();
|
|
}
|
|
|
|
bool AssetManager::initialize(const std::string& dataPath_) {
|
|
if (initialized) {
|
|
LOG_WARNING("AssetManager already initialized");
|
|
return true;
|
|
}
|
|
|
|
dataPath = dataPath_;
|
|
overridePath_ = dataPath + "/override";
|
|
LOG_INFO("Initializing asset manager with data path: ", dataPath);
|
|
|
|
setupFileCacheBudget();
|
|
|
|
std::string manifestPath = dataPath + "/manifest.json";
|
|
if (!std::filesystem::exists(manifestPath)) {
|
|
LOG_ERROR("manifest.json not found in: ", dataPath);
|
|
LOG_ERROR("Run asset_extract to extract MPQ archives first");
|
|
return false;
|
|
}
|
|
|
|
if (!manifest_.load(manifestPath)) {
|
|
LOG_ERROR("Failed to load manifest");
|
|
return false;
|
|
}
|
|
|
|
if (std::filesystem::is_directory(overridePath_)) {
|
|
LOG_INFO("Override directory found: ", overridePath_);
|
|
}
|
|
|
|
initialized = true;
|
|
LOG_INFO("Asset manager initialized: ", manifest_.getEntryCount(),
|
|
" files indexed (file cache: ", fileCacheBudget / (1024 * 1024), " MB)");
|
|
return true;
|
|
}
|
|
|
|
void AssetManager::setupFileCacheBudget() {
|
|
auto& memMonitor = core::MemoryMonitor::getInstance();
|
|
size_t recommendedBudget = memMonitor.getRecommendedCacheBudget();
|
|
size_t dynamicBudget = (recommendedBudget * 3) / 4;
|
|
|
|
const size_t envFixedMB = parseEnvSizeMB("WOWEE_FILE_CACHE_MB");
|
|
const size_t envMaxMB = parseEnvSizeMB("WOWEE_FILE_CACHE_MAX_MB");
|
|
|
|
const size_t minBudgetBytes = 256ull * 1024ull * 1024ull;
|
|
const size_t defaultMaxBudgetBytes = 12288ull * 1024ull * 1024ull; // 12 GB max for file cache
|
|
const size_t maxBudgetBytes = (envMaxMB > 0)
|
|
? (envMaxMB * 1024ull * 1024ull)
|
|
: defaultMaxBudgetBytes;
|
|
|
|
if (envFixedMB > 0) {
|
|
fileCacheBudget = envFixedMB * 1024ull * 1024ull;
|
|
if (fileCacheBudget < minBudgetBytes) {
|
|
fileCacheBudget = minBudgetBytes;
|
|
}
|
|
LOG_WARNING("Asset file cache fixed via WOWEE_FILE_CACHE_MB=", envFixedMB,
|
|
" (effective ", fileCacheBudget / (1024 * 1024), " MB)");
|
|
} else {
|
|
fileCacheBudget = std::clamp(dynamicBudget, minBudgetBytes, maxBudgetBytes);
|
|
}
|
|
}
|
|
|
|
void AssetManager::shutdown() {
|
|
if (!initialized) {
|
|
return;
|
|
}
|
|
|
|
LOG_INFO("Shutting down asset manager");
|
|
|
|
if (fileCacheHits + fileCacheMisses > 0) {
|
|
float hitRate = static_cast<float>(fileCacheHits) / (fileCacheHits + fileCacheMisses) * 100.0f;
|
|
LOG_INFO("File cache stats: ", fileCacheHits, " hits, ", fileCacheMisses, " misses (",
|
|
static_cast<int>(hitRate), "% hit rate), ", fileCacheTotalBytes / 1024 / 1024, " MB cached");
|
|
}
|
|
|
|
clearCache();
|
|
initialized = false;
|
|
}
|
|
|
|
std::string AssetManager::resolveFile(const std::string& normalizedPath) const {
|
|
// Check override directory first (for HD upgrades, custom textures)
|
|
if (!overridePath_.empty()) {
|
|
const auto* entry = manifest_.lookup(normalizedPath);
|
|
if (entry && !entry->filesystemPath.empty()) {
|
|
std::string overrideFsPath = overridePath_ + "/" + entry->filesystemPath;
|
|
if (LooseFileReader::fileExists(overrideFsPath)) {
|
|
return overrideFsPath;
|
|
}
|
|
}
|
|
}
|
|
// Primary manifest
|
|
std::string primaryPath = manifest_.resolveFilesystemPath(normalizedPath);
|
|
if (!primaryPath.empty()) return primaryPath;
|
|
|
|
// If a base-path fallback is configured (expansion-specific primary that only
|
|
// holds DBC overrides), retry against the base extraction.
|
|
if (!baseFallbackDataPath_.empty()) {
|
|
return baseFallbackManifest_.resolveFilesystemPath(normalizedPath);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void AssetManager::setBaseFallbackPath(const std::string& basePath) {
|
|
if (basePath.empty() || basePath == dataPath) return; // nothing to do
|
|
std::string manifestPath = basePath + "/manifest.json";
|
|
if (!std::filesystem::exists(manifestPath)) {
|
|
LOG_DEBUG("AssetManager: base fallback manifest not found at ", manifestPath,
|
|
" — fallback disabled");
|
|
return;
|
|
}
|
|
if (baseFallbackManifest_.load(manifestPath)) {
|
|
baseFallbackDataPath_ = basePath;
|
|
LOG_INFO("AssetManager: base fallback path set to '", basePath,
|
|
"' (", baseFallbackManifest_.getEntryCount(), " files)");
|
|
}
|
|
}
|
|
|
|
BLPImage AssetManager::loadTexture(const std::string& path) {
|
|
ZoneScopedN("AssetManager::loadTexture");
|
|
if (!initialized) {
|
|
LOG_ERROR("AssetManager not initialized");
|
|
return BLPImage();
|
|
}
|
|
|
|
std::string normalizedPath = normalizePath(path);
|
|
|
|
LOG_DEBUG("Loading texture: ", normalizedPath);
|
|
|
|
// Check for PNG override
|
|
BLPImage pngImage = tryLoadPngOverride(normalizedPath);
|
|
if (pngImage.isValid()) {
|
|
return pngImage;
|
|
}
|
|
|
|
std::vector<uint8_t> blpData = readFile(normalizedPath);
|
|
if (blpData.empty()) {
|
|
static std::mutex logMtx;
|
|
static std::unordered_set<std::string> loggedMissingTextures;
|
|
static bool missingTextureLogSuppressed = false;
|
|
static const size_t kMaxMissingTextureLogKeys =
|
|
parseEnvCount("WOWEE_TEXTURE_MISS_LOG_KEYS", 400);
|
|
std::lock_guard<std::mutex> lock(logMtx);
|
|
if (loggedMissingTextures.size() < kMaxMissingTextureLogKeys &&
|
|
loggedMissingTextures.insert(normalizedPath).second) {
|
|
LOG_WARNING("Texture not found: ", normalizedPath);
|
|
} else if (!missingTextureLogSuppressed && loggedMissingTextures.size() >= kMaxMissingTextureLogKeys) {
|
|
LOG_WARNING("Texture-not-found warning key cache reached ", kMaxMissingTextureLogKeys,
|
|
" entries; suppressing new unique texture-miss logs");
|
|
missingTextureLogSuppressed = true;
|
|
}
|
|
return BLPImage();
|
|
}
|
|
|
|
BLPImage image = BLPLoader::load(blpData);
|
|
if (!image.isValid()) {
|
|
static std::mutex logMtx;
|
|
static std::unordered_set<std::string> loggedDecodeFails;
|
|
static bool decodeFailLogSuppressed = false;
|
|
static const size_t kMaxDecodeFailLogKeys =
|
|
parseEnvCount("WOWEE_TEXTURE_DECODE_LOG_KEYS", 200);
|
|
std::lock_guard<std::mutex> lock(logMtx);
|
|
if (loggedDecodeFails.size() < kMaxDecodeFailLogKeys &&
|
|
loggedDecodeFails.insert(normalizedPath).second) {
|
|
LOG_ERROR("Failed to load texture: ", normalizedPath);
|
|
} else if (!decodeFailLogSuppressed && loggedDecodeFails.size() >= kMaxDecodeFailLogKeys) {
|
|
LOG_WARNING("Texture-decode warning key cache reached ", kMaxDecodeFailLogKeys,
|
|
" entries; suppressing new unique decode-failure logs");
|
|
decodeFailLogSuppressed = true;
|
|
}
|
|
return BLPImage();
|
|
}
|
|
|
|
LOG_DEBUG("Loaded texture: ", normalizedPath, " (", image.width, "x", image.height, ")");
|
|
return image;
|
|
}
|
|
|
|
BLPImage AssetManager::tryLoadPngOverride(const std::string& normalizedPath) const {
|
|
if (normalizedPath.size() < 4) return BLPImage();
|
|
|
|
std::string ext = normalizedPath.substr(normalizedPath.size() - 4);
|
|
if (ext != ".blp") return BLPImage();
|
|
|
|
// Try the standard sidecar path first: extracted .blp's directory + .png.
|
|
std::string fsPath = resolveFile(normalizedPath);
|
|
std::string pngPath;
|
|
if (!fsPath.empty() && fsPath.size() >= 4) {
|
|
pngPath = fsPath.substr(0, fsPath.size() - 4) + ".png";
|
|
if (!LooseFileReader::fileExists(pngPath)) pngPath.clear();
|
|
}
|
|
|
|
// Fallback: probe well-known custom-zone texture roots so that PNG-only
|
|
// assets ship without needing a phantom BLP manifest entry. Path is
|
|
// forward-slash + lowercase to match the editor's PNG export convention.
|
|
if (pngPath.empty()) {
|
|
std::string norm = normalizedPath;
|
|
std::replace(norm.begin(), norm.end(), '\\', '/');
|
|
std::transform(norm.begin(), norm.end(), norm.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
std::string candidate = norm.substr(0, norm.size() - 4) + ".png";
|
|
for (const char* root : {"custom_zones/textures/", "output/textures/"}) {
|
|
std::string p = std::string(root) + candidate;
|
|
if (LooseFileReader::fileExists(p)) { pngPath = p; break; }
|
|
}
|
|
}
|
|
if (pngPath.empty()) return BLPImage();
|
|
|
|
int w, h, channels;
|
|
unsigned char* pixels = stbi_load(pngPath.c_str(), &w, &h, &channels, 4);
|
|
if (!pixels) {
|
|
LOG_WARNING("PNG override exists but failed to load: ", pngPath);
|
|
return BLPImage();
|
|
}
|
|
// Cap texture dimensions. WoW textures top out at 4K; stbi can return
|
|
// 32K x 32K which would allocate 4GB on a malicious PNG.
|
|
if (w <= 0 || h <= 0 || w > 8192 || h > 8192) {
|
|
LOG_WARNING("PNG override dimensions out of range (", w, "x", h, "): ", pngPath);
|
|
stbi_image_free(pixels);
|
|
return BLPImage();
|
|
}
|
|
|
|
BLPImage image;
|
|
image.width = w;
|
|
image.height = h;
|
|
image.channels = 4;
|
|
image.format = BLPFormat::BLP2;
|
|
image.compression = BLPCompression::ARGB8888;
|
|
image.data.assign(pixels, pixels + (static_cast<size_t>(w) * h * 4));
|
|
stbi_image_free(pixels);
|
|
|
|
LOG_INFO("PNG override loaded: ", pngPath, " (", w, "x", h, ")");
|
|
return image;
|
|
}
|
|
|
|
void AssetManager::setExpansionDataPath(const std::string& path) {
|
|
expansionDataPath_ = path;
|
|
LOG_INFO("Expansion data path for CSV DBCs: ", expansionDataPath_);
|
|
}
|
|
|
|
std::shared_ptr<DBCFile> AssetManager::loadDBC(const std::string& name) {
|
|
ZoneScopedN("AssetManager::loadDBC");
|
|
if (!initialized) {
|
|
LOG_ERROR("AssetManager not initialized");
|
|
return nullptr;
|
|
}
|
|
|
|
auto it = dbcCache.find(name);
|
|
if (it != dbcCache.end()) {
|
|
LOG_DEBUG("DBC already loaded (cached): ", name);
|
|
return it->second;
|
|
}
|
|
|
|
LOG_DEBUG("Loading DBC: ", name);
|
|
|
|
std::vector<uint8_t> dbcData;
|
|
|
|
// Try binary DBC from extracted MPQs first (preferred source).
|
|
std::string dbcPath = "DBFilesClient\\" + name;
|
|
{
|
|
dbcData = readFile(dbcPath);
|
|
}
|
|
|
|
// If asset_extract was run with --emit-json-dbc, the DBC's directory
|
|
// also contains a JSON sidecar. Use it when the binary is missing
|
|
// (lets users run with PNG/JSON-only extractions for testing the
|
|
// open-format end-to-end path). Server-mode never reads via this
|
|
// code path, so private-server compat is unaffected.
|
|
if (dbcData.empty()) {
|
|
std::string normalizedDbc = normalizePath(dbcPath);
|
|
std::string fsPath = resolveFile(normalizedDbc);
|
|
if (!fsPath.empty() && fsPath.size() >= 4) {
|
|
std::string sidecar = fsPath.substr(0, fsPath.size() - 4) + ".json";
|
|
if (std::filesystem::exists(sidecar)) {
|
|
std::ifstream jf(sidecar, std::ios::binary | std::ios::ate);
|
|
if (jf) {
|
|
auto sz = jf.tellg();
|
|
if (sz > 0) {
|
|
dbcData.resize(static_cast<size_t>(sz));
|
|
jf.seekg(0);
|
|
jf.read(reinterpret_cast<char*>(dbcData.data()), sz);
|
|
LOG_INFO("Loading JSON DBC sidecar: ", sidecar);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try Data/db/ directory (pre-extracted binary DBCs shared across expansions)
|
|
if (dbcData.empty()) {
|
|
// dataPath is expansion-specific (e.g. Data/expansions/wotlk/); go up to Data/
|
|
for (const std::string& base : {dataPath + "/db/" + name,
|
|
dataPath + "/../../db/" + name,
|
|
"Data/db/" + name}) {
|
|
if (std::filesystem::exists(base)) {
|
|
std::ifstream f(base, std::ios::binary | std::ios::ate);
|
|
if (f) {
|
|
auto size = f.tellg();
|
|
if (size > 0) {
|
|
f.seekg(0);
|
|
dbcData.resize(static_cast<size_t>(size));
|
|
f.read(reinterpret_cast<char*>(dbcData.data()), size);
|
|
LOG_INFO("Loaded binary DBC from: ", base, " (", size, " bytes)");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for JSON DBC from custom zones (wowee open format)
|
|
if (dbcData.empty()) {
|
|
std::string baseName = name;
|
|
auto dot = baseName.rfind('.');
|
|
if (dot != std::string::npos) baseName = baseName.substr(0, dot);
|
|
for (const char* dir : {"custom_zones", "output"}) {
|
|
if (!std::filesystem::exists(dir)) continue;
|
|
for (auto& entry : std::filesystem::directory_iterator(dir)) {
|
|
if (!entry.is_directory()) continue;
|
|
std::string jsonPath = entry.path().string() + "/data/" + baseName + ".json";
|
|
if (std::filesystem::exists(jsonPath)) {
|
|
std::ifstream jf(jsonPath, std::ios::binary | std::ios::ate);
|
|
if (jf) {
|
|
auto sz = jf.tellg();
|
|
if (sz > 0) {
|
|
dbcData.resize(static_cast<size_t>(sz));
|
|
jf.seekg(0);
|
|
jf.read(reinterpret_cast<char*>(dbcData.data()), sz);
|
|
LOG_INFO("Loading JSON DBC override: ", jsonPath);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!dbcData.empty()) break;
|
|
}
|
|
}
|
|
|
|
// Fall back to expansion-specific CSV (e.g. Data/expansions/wotlk/db/Spell.csv)
|
|
if (dbcData.empty() && !expansionDataPath_.empty()) {
|
|
std::string baseName = name;
|
|
auto dot = baseName.rfind('.');
|
|
if (dot != std::string::npos) {
|
|
baseName = baseName.substr(0, dot);
|
|
}
|
|
std::string csvPath = expansionDataPath_ + "/db/" + baseName + ".csv";
|
|
if (std::filesystem::exists(csvPath)) {
|
|
std::ifstream f(csvPath, std::ios::binary | std::ios::ate);
|
|
if (f) {
|
|
auto size = f.tellg();
|
|
if (size > 0) {
|
|
f.seekg(0);
|
|
dbcData.resize(static_cast<size_t>(size));
|
|
f.read(reinterpret_cast<char*>(dbcData.data()), size);
|
|
LOG_INFO("Binary DBC not found, using CSV fallback: ", csvPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dbcData.empty()) {
|
|
LOG_WARNING("DBC not found: ", name);
|
|
return nullptr;
|
|
}
|
|
|
|
auto dbc = std::make_shared<DBCFile>();
|
|
if (!dbc->load(dbcData)) {
|
|
LOG_ERROR("Failed to load DBC: ", name);
|
|
return nullptr;
|
|
}
|
|
|
|
dbcCache[name] = dbc;
|
|
|
|
LOG_INFO("Loaded DBC: ", name, " (", dbc->getRecordCount(), " records)");
|
|
return dbc;
|
|
}
|
|
|
|
std::shared_ptr<DBCFile> AssetManager::loadDBCOptional(const std::string& name) {
|
|
// Check cache first
|
|
auto it = dbcCache.find(name);
|
|
if (it != dbcCache.end()) return it->second;
|
|
|
|
// Try binary DBC
|
|
std::vector<uint8_t> dbcData;
|
|
{
|
|
std::string dbcPath = "DBFilesClient\\" + name;
|
|
dbcData = readFile(dbcPath);
|
|
}
|
|
|
|
// Fall back to expansion-specific CSV
|
|
if (dbcData.empty() && !expansionDataPath_.empty()) {
|
|
std::string baseName = name;
|
|
auto dot = baseName.rfind('.');
|
|
if (dot != std::string::npos) baseName = baseName.substr(0, dot);
|
|
std::string csvPath = expansionDataPath_ + "/db/" + baseName + ".csv";
|
|
if (std::filesystem::exists(csvPath)) {
|
|
std::ifstream f(csvPath, std::ios::binary | std::ios::ate);
|
|
if (f) {
|
|
auto size = f.tellg();
|
|
if (size > 0) {
|
|
f.seekg(0);
|
|
dbcData.resize(static_cast<size_t>(size));
|
|
f.read(reinterpret_cast<char*>(dbcData.data()), size);
|
|
LOG_INFO("Binary DBC not found, using CSV fallback: ", csvPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dbcData.empty()) {
|
|
// Expected on some expansions — log at debug level only.
|
|
LOG_DEBUG("Optional DBC not found (expected on some expansions): ", name);
|
|
return nullptr;
|
|
}
|
|
|
|
auto dbc = std::make_shared<DBCFile>();
|
|
if (!dbc->load(dbcData)) {
|
|
LOG_ERROR("Failed to load DBC: ", name);
|
|
return nullptr;
|
|
}
|
|
|
|
dbcCache[name] = dbc;
|
|
LOG_INFO("Loaded optional DBC: ", name, " (", dbc->getRecordCount(), " records)");
|
|
return dbc;
|
|
}
|
|
|
|
std::shared_ptr<DBCFile> AssetManager::getDBC(const std::string& name) const {
|
|
auto it = dbcCache.find(name);
|
|
if (it != dbcCache.end()) {
|
|
return it->second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool AssetManager::fileExists(const std::string& path) const {
|
|
if (!initialized) {
|
|
return false;
|
|
}
|
|
std::string normalized = normalizePath(path);
|
|
return manifest_.hasEntry(normalized);
|
|
}
|
|
|
|
std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
|
|
if (!initialized) {
|
|
return {};
|
|
}
|
|
|
|
std::string normalized = normalizePath(path);
|
|
|
|
// Check cache first (shared lock allows concurrent reads)
|
|
{
|
|
std::shared_lock<std::shared_mutex> cacheLock(cacheMutex);
|
|
auto it = fileCache.find(normalized);
|
|
if (it != fileCache.end()) {
|
|
auto data = it->second.data;
|
|
cacheLock.unlock();
|
|
fileCacheHits++;
|
|
return data;
|
|
}
|
|
}
|
|
|
|
// Read from filesystem (override dir first, then base manifest)
|
|
std::string fsPath = resolveFile(normalized);
|
|
if (fsPath.empty()) {
|
|
return {};
|
|
}
|
|
|
|
auto data = LooseFileReader::readFile(fsPath);
|
|
if (data.empty()) {
|
|
LOG_WARNING("Manifest entry exists but file unreadable: ", fsPath);
|
|
return data;
|
|
}
|
|
|
|
// Add to cache if within budget
|
|
size_t fileSize = data.size();
|
|
if (fileSize > 0 && fileSize < fileCacheBudget / 2) {
|
|
std::lock_guard<std::shared_mutex> cacheLock(cacheMutex);
|
|
// Evict old entries if needed (LRU)
|
|
while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) {
|
|
auto lru = fileCache.begin();
|
|
for (auto it = fileCache.begin(); it != fileCache.end(); ++it) {
|
|
if (it->second.lastAccessTime < lru->second.lastAccessTime) {
|
|
lru = it;
|
|
}
|
|
}
|
|
fileCacheTotalBytes -= lru->second.data.size();
|
|
fileCache.erase(lru);
|
|
}
|
|
|
|
CachedFile cached;
|
|
cached.data = data;
|
|
cached.lastAccessTime = ++fileCacheAccessCounter;
|
|
fileCache[normalized] = std::move(cached);
|
|
fileCacheTotalBytes += fileSize;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
std::vector<uint8_t> AssetManager::readFileOptional(const std::string& path) const {
|
|
if (!initialized) {
|
|
return {};
|
|
}
|
|
if (!fileExists(path)) {
|
|
return {};
|
|
}
|
|
return readFile(path);
|
|
}
|
|
|
|
void AssetManager::clearDBCCache() {
|
|
std::lock_guard<std::shared_mutex> lock(cacheMutex);
|
|
dbcCache.clear();
|
|
LOG_INFO("Cleared DBC cache");
|
|
}
|
|
|
|
void AssetManager::clearCache() {
|
|
std::lock_guard<std::shared_mutex> lock(cacheMutex);
|
|
dbcCache.clear();
|
|
fileCache.clear();
|
|
fileCacheTotalBytes = 0;
|
|
fileCacheAccessCounter = 0;
|
|
LOG_INFO("Cleared asset cache (DBC + file cache)");
|
|
}
|
|
|
|
size_t AssetManager::purgeExtractedAssets() {
|
|
clearCache();
|
|
|
|
if (dataPath.empty()) {
|
|
LOG_WARNING("Cannot purge: no data path set");
|
|
return 0;
|
|
}
|
|
|
|
size_t removed = 0;
|
|
namespace fs = std::filesystem;
|
|
|
|
// Extracted MPQ content directories
|
|
const char* extractedDirs[] = {
|
|
"db", "character", "creature", "terrain", "world",
|
|
"interface", "item", "sound", "spell", "environment",
|
|
"misc", "enUS",
|
|
// Case variants (Windows-extracted assets)
|
|
"Character", "Creature", "World"
|
|
};
|
|
|
|
for (const auto& dir : extractedDirs) {
|
|
fs::path p = fs::path(dataPath) / dir;
|
|
if (fs::exists(p)) {
|
|
std::error_code ec;
|
|
auto count = fs::remove_all(p, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", p.string(), " (", count, " entries)");
|
|
removed += count;
|
|
} else {
|
|
LOG_WARNING("Failed to remove ", p.string(), ": ", ec.message());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Root manifest
|
|
fs::path manifestPath = fs::path(dataPath) / "manifest.json";
|
|
if (fs::exists(manifestPath)) {
|
|
std::error_code ec;
|
|
fs::remove(manifestPath, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", manifestPath.string());
|
|
++removed;
|
|
}
|
|
}
|
|
|
|
// Override directory
|
|
if (!overridePath_.empty() && fs::exists(overridePath_)) {
|
|
std::error_code ec;
|
|
auto count = fs::remove_all(overridePath_, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", overridePath_, " (", count, " entries)");
|
|
removed += count;
|
|
}
|
|
}
|
|
|
|
// HD texture packs
|
|
fs::path hdPath = fs::path(dataPath) / "hd";
|
|
if (fs::exists(hdPath)) {
|
|
std::error_code ec;
|
|
auto count = fs::remove_all(hdPath, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", hdPath.string(), " (", count, " entries)");
|
|
removed += count;
|
|
}
|
|
}
|
|
|
|
// Per-expansion extracted assets, manifests, and overlays
|
|
fs::path expansionsDir = fs::path(dataPath) / "expansions";
|
|
if (fs::is_directory(expansionsDir)) {
|
|
for (auto& expEntry : fs::directory_iterator(expansionsDir)) {
|
|
if (!expEntry.is_directory()) continue;
|
|
fs::path expDir = expEntry.path();
|
|
|
|
// Extracted assets
|
|
fs::path assetsDir = expDir / "assets";
|
|
if (fs::exists(assetsDir)) {
|
|
std::error_code ec;
|
|
auto count = fs::remove_all(assetsDir, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", assetsDir.string(), " (", count, " entries)");
|
|
removed += count;
|
|
}
|
|
}
|
|
|
|
// Expansion manifest
|
|
fs::path expManifest = expDir / "manifest.json";
|
|
if (fs::exists(expManifest)) {
|
|
std::error_code ec;
|
|
fs::remove(expManifest, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", expManifest.string());
|
|
++removed;
|
|
}
|
|
}
|
|
|
|
// Overlay
|
|
fs::path overlayDir = expDir / "overlay";
|
|
if (fs::exists(overlayDir)) {
|
|
std::error_code ec;
|
|
auto count = fs::remove_all(overlayDir, ec);
|
|
if (!ec) {
|
|
LOG_INFO("Purged: ", overlayDir.string(), " (", count, " entries)");
|
|
removed += count;
|
|
}
|
|
}
|
|
|
|
// Generated CSVs
|
|
fs::path dbDir = expDir / "db";
|
|
if (fs::is_directory(dbDir)) {
|
|
for (auto& f : fs::directory_iterator(dbDir)) {
|
|
if (f.path().extension() == ".csv") {
|
|
std::error_code ec;
|
|
fs::remove(f.path(), ec);
|
|
if (!ec) ++removed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset manifest state so initialize() knows it needs re-extraction
|
|
manifest_ = AssetManifest();
|
|
initialized = false;
|
|
|
|
LOG_INFO("Purge complete: ", removed, " entries removed from ", dataPath);
|
|
return removed;
|
|
}
|
|
|
|
std::string AssetManager::normalizePath(const std::string& path) const {
|
|
std::string normalized = path;
|
|
std::replace(normalized.begin(), normalized.end(), '/', '\\');
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
// Reject path traversal sequences
|
|
if (normalized.find("..\\") != std::string::npos ||
|
|
normalized.find("../") != std::string::npos ||
|
|
normalized == "..") {
|
|
LOG_WARNING("Path traversal rejected: ", path);
|
|
return {};
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
} // namespace pipeline
|
|
} // namespace wowee
|