#include "pipeline/asset_manager.hpp" #include "core/logger.hpp" #include "core/memory_monitor.hpp" #include #include #include #include #include #include #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::max() / (1024ull * 1024ull))) { return 0; } return static_cast(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(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 = 32768ull * 1024ull * 1024ull; 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 = (float)fileCacheHits / (fileCacheHits + fileCacheMisses) * 100.0f; LOG_INFO("File cache stats: ", fileCacheHits, " hits, ", fileCacheMisses, " misses (", (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; } } } // Fall back to base manifest return manifest_.resolveFilesystemPath(normalizedPath); } BLPImage AssetManager::loadTexture(const std::string& path) { 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 blpData = readFile(normalizedPath); if (blpData.empty()) { static std::unordered_set loggedMissingTextures; static bool missingTextureLogSuppressed = false; static const size_t kMaxMissingTextureLogKeys = parseEnvCount("WOWEE_TEXTURE_MISS_LOG_KEYS", 400); 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::unordered_set loggedDecodeFails; static bool decodeFailLogSuppressed = false; static const size_t kMaxDecodeFailLogKeys = parseEnvCount("WOWEE_TEXTURE_DECODE_LOG_KEYS", 200); 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(); std::string fsPath = resolveFile(normalizedPath); if (fsPath.empty()) return BLPImage(); // Replace .blp/.BLP extension with .png std::string pngPath = fsPath.substr(0, fsPath.size() - 4) + ".png"; if (!LooseFileReader::fileExists(pngPath)) { 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(); } BLPImage image; image.width = w; image.height = h; image.channels = 4; image.format = BLPFormat::BLP2; image.compression = BLPCompression::ARGB8888; image.data.assign(pixels, pixels + (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 AssetManager::loadDBC(const std::string& name) { 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 dbcData; // Try binary DBC from extracted MPQs first (preferred source) { std::string dbcPath = "DBFilesClient\\" + name; dbcData = readFile(dbcPath); } // 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)); f.read(reinterpret_cast(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(); 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 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 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)); f.read(reinterpret_cast(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(); 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 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 AssetManager::readFile(const std::string& path) const { if (!initialized) { return {}; } std::string normalized = normalizePath(path); // Check cache first { std::lock_guard cacheLock(cacheMutex); auto it = fileCache.find(normalized); if (it != fileCache.end()) { it->second.lastAccessTime = ++fileCacheAccessCounter; fileCacheHits++; return it->second.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 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 AssetManager::readFileOptional(const std::string& path) const { if (!initialized) { return {}; } if (!fileExists(path)) { return {}; } return readFile(path); } void AssetManager::clearDBCCache() { std::lock_guard lock(cacheMutex); dbcCache.clear(); LOG_INFO("Cleared DBC cache"); } void AssetManager::clearCache() { std::lock_guard 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(std::tolower(c)); }); return normalized; } } // namespace pipeline } // namespace wowee