diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json index d31b5d18..e7712f13 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -9,8 +9,8 @@ }, "CharSections": { "RaceID": 1, "SexID": 2, "BaseSection": 3, - "Texture1": 4, "Texture2": 5, "Texture3": 6, - "VariationIndex": 8, "ColorIndex": 9 + "VariationIndex": 4, "ColorIndex": 5, + "Texture1": 6, "Texture2": 7, "Texture3": 8 }, "SpellIcon": { "ID": 0, "Path": 1 }, "FactionTemplate": { diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json index 8b9c1ff1..b8bedd19 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -9,8 +9,8 @@ }, "CharSections": { "RaceID": 1, "SexID": 2, "BaseSection": 3, - "Texture1": 4, "Texture2": 5, "Texture3": 6, - "VariationIndex": 8, "ColorIndex": 9 + "VariationIndex": 4, "ColorIndex": 5, + "Texture1": 6, "Texture2": 7, "Texture3": 8 }, "SpellIcon": { "ID": 0, "Path": 1 }, "FactionTemplate": { diff --git a/include/core/application.hpp b/include/core/application.hpp index b9ced15a..1ad9ac48 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -216,6 +216,8 @@ private: bool wasAutoAttacking_ = false; void processPendingMount(); bool creatureLookupsBuilt_ = false; + bool mapNameCacheLoaded_ = false; + std::unordered_map mapNameById_; // Deferred creature spawn queue (throttles spawning to avoid hangs) struct PendingCreatureSpawn { diff --git a/include/game/expansion_profile.hpp b/include/game/expansion_profile.hpp index 7676b026..31e66974 100644 --- a/include/game/expansion_profile.hpp +++ b/include/game/expansion_profile.hpp @@ -30,6 +30,7 @@ struct ExpansionProfile { std::string locale = "enUS"; uint32_t timezone = 0; std::string dataPath; // Absolute path to expansion data dir + std::string assetManifest; // Path to expansion-specific asset manifest (absolute, resolved from dataPath) uint32_t maxLevel = 60; std::vector races; std::vector classes; diff --git a/src/core/application.cpp b/src/core/application.cpp index 3c8a2280..0d3e3e23 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -231,6 +231,16 @@ bool Application::initialize() { } hdPackManager_->applyToAssetManager(assetManager.get(), expansionId); } + + // Load expansion-specific asset overlay (priority 50, below HD packs at 100+) + if (expansionRegistry_) { + auto* activeProfile = expansionRegistry_->getActive(); + if (activeProfile && !activeProfile->assetManifest.empty()) { + if (assetManager->addOverlayManifest(activeProfile->assetManifest, 50, "expansion_overlay")) { + LOG_INFO("Added expansion asset overlay: ", activeProfile->assetManifest); + } + } + } } else { LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable"); LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory"); @@ -493,7 +503,19 @@ void Application::reloadExpansionData() { if (assetManager && !profile->dataPath.empty()) { assetManager->setExpansionDataPath(profile->dataPath); assetManager->clearDBCCache(); + + // Swap expansion asset overlay + assetManager->removeOverlay("expansion_overlay"); + if (!profile->assetManifest.empty()) { + if (assetManager->addOverlayManifest(profile->assetManifest, 50, "expansion_overlay")) { + LOG_INFO("Swapped expansion asset overlay: ", profile->assetManifest); + } + } } + + // Reset map name cache so it reloads from new expansion's Map.dbc + mapNameCacheLoaded_ = false; + mapNameById_.clear(); } void Application::logoutToLogin() { @@ -1828,13 +1850,13 @@ void Application::spawnPlayerCharacter() { uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (raceId != targetRaceId || sexId != targetSexId) continue; // Section 0 = skin: match by colorIndex = skin byte - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4; + const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { std::string tex1 = charSectionsDbc->getString(r, csTex1); if (!tex1.empty()) { @@ -2409,28 +2431,26 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Resolve map folder name from Map.dbc (authoritative for world/instance maps). // This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor. - static bool mapNameCacheLoaded = false; - static std::unordered_map mapNameById; - if (!mapNameCacheLoaded && assetManager) { - mapNameCacheLoaded = true; + if (!mapNameCacheLoaded_ && assetManager) { + mapNameCacheLoaded_ = true; if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) { - mapNameById.reserve(mapDbc->getRecordCount()); + mapNameById_.reserve(mapDbc->getRecordCount()); const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr; for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) { uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0); std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1); - if (!internalName.empty() && mapNameById.find(id) == mapNameById.end()) { - mapNameById[id] = std::move(internalName); + if (!internalName.empty() && mapNameById_.find(id) == mapNameById_.end()) { + mapNameById_[id] = std::move(internalName); } } - LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById.size(), " entries"); + LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById_.size(), " entries"); } else { LOG_WARNING("Map.dbc not available; using fallback map-id mapping"); } } std::string mapName; - if (auto it = mapNameById.find(mapId); it != mapNameById.end()) { + if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) { mapName = it->second; } else { mapName = mapIdToName(mapId); @@ -3122,15 +3142,15 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x uint32_t raceId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["RaceID"] : 1); uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2); uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3); - uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 8); - uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 9); + uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 4); + uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 5); if (raceId != targetRace || sexId != targetSex) continue; if (section != 3) continue; // Section 3 = hair if (variation != static_cast(extra.hairStyleId)) continue; if (colorIdx != static_cast(extra.hairColorId)) continue; - hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 4); + hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 6); break; } @@ -3667,7 +3687,7 @@ void Application::spawnOnlinePlayer(uint64_t guid, const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; uint32_t targetRaceId = raceId; uint32_t targetSexId = genderId; - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4; + const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; bool foundSkin = false; bool foundUnderwear = false; @@ -3679,8 +3699,8 @@ void Application::spawnOnlinePlayer(uint64_t guid, uint32_t rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t rSex = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (rRace != targetRaceId || rSex != targetSexId) continue; diff --git a/src/game/expansion_profile.cpp b/src/game/expansion_profile.cpp index 9b14e0b7..00aa068d 100644 --- a/src/game/expansion_profile.cpp +++ b/src/game/expansion_profile.cpp @@ -176,6 +176,12 @@ bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::stri v = jsonValue(json, "locale"); if (!v.empty()) p.locale = v; p.timezone = static_cast(jsonInt(json, "timezone", static_cast(p.timezone))); } + // Expansion-specific asset manifest (overlay for base data) + p.assetManifest = jsonValue(json, "assetManifest"); + if (!p.assetManifest.empty() && p.assetManifest[0] != '/') { + p.assetManifest = dirPath + "/" + p.assetManifest; + } + p.maxLevel = static_cast(jsonInt(json, "maxLevel", 60)); p.races = jsonUintArray(json, "races"); p.classes = jsonUintArray(json, "classes"); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 61d5777d..56c0a24a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -274,6 +274,7 @@ void GameHandler::update(float deltaTime) { auto pkt = InspectPacket::build(guid); socket->send(pkt); inspectRateLimit_ = 0.75f; // keep it gentle + LOG_INFO("Sent CMSG_INSPECT for player 0x", std::hex, guid, std::dec); } } @@ -5543,6 +5544,7 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::mapgetLayout("CharSections") : nullptr; + uint32_t fRace = csL ? (*csL)["RaceID"] : 1; + uint32_t fSex = csL ? (*csL)["SexID"] : 2; + uint32_t fBase = csL ? (*csL)["BaseSection"] : 3; + uint32_t fVar = csL ? (*csL)["VariationIndex"] : 4; + uint32_t fColor = csL ? (*csL)["ColorIndex"] : 5; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); - uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t raceId = charSectionsDbc->getUInt32(r, fRace); + uint32_t sexId = charSectionsDbc->getUInt32(r, fSex); + uint32_t baseSection = charSectionsDbc->getUInt32(r, fBase); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, fVar); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, fColor); if (raceId != targetRaceId || sexId != targetSexId) continue; // Section 0: Body skin (variation=0, colorIndex = skin color) if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); if (!tex1.empty()) { bodySkinPath_ = tex1; foundSkin = true; @@ -189,8 +194,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 1 && !foundFace && variationIndex == static_cast(face) && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); - std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7); if (!tex1.empty()) faceLowerPath = tex1; if (!tex2.empty()) faceUpperPath = tex2; foundFace = true; @@ -199,7 +204,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 3 && !foundHair && variationIndex == static_cast(hairStyle) && colorIndex == static_cast(hairColor)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); if (!tex1.empty()) { hairScalpPath = tex1; foundHair = true; @@ -208,7 +213,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // Section 4: Underwear (variation=0, colorIndex = skin color) else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == static_cast(skin)) { - uint32_t texBase = csL ? (*csL)["Texture1"] : 4; + uint32_t texBase = csL ? (*csL)["Texture1"] : 6; for (uint32_t f = texBase; f <= texBase + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) { @@ -220,14 +225,9 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, } } - LOG_INFO("CharPreview tex lookup: bodySkin=", bodySkinPath_, " faceLower=", faceLowerPath, - " faceUpper=", faceUpperPath, " hairScalp=", hairScalpPath, - " underwear=", underwearPaths.size()); - // Assign texture filenames on model before GPU upload for (size_t ti = 0; ti < model.textures.size(); ti++) { auto& tex = model.textures[ti]; - LOG_INFO(" model.textures[", ti, "]: type=", tex.type, " filename=", tex.filename); if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) { tex.filename = bodySkinPath_; } else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) { @@ -256,9 +256,6 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, LOG_WARNING("CharacterPreview: failed to load model to GPU"); return false; } - LOG_INFO("CharPreview: model loaded to GPU, textureLookup size=", model.textureLookup.size(), - " materials=", model.materials.size(), " batches=", model.batches.size()); - // Composite body skin + face + underwear overlays if (!bodySkinPath_.empty()) { std::vector layers; diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index b3583350..e33650de 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -179,8 +179,8 @@ void CharacterCreateScreen::updateAppearanceRanges() { if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (baseSection == 0 && variationIndex == 0) { skinMax = std::max(skinMax, static_cast(colorIndex)); @@ -206,8 +206,8 @@ void CharacterCreateScreen::updateAppearanceRanges() { if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (baseSection == 1 && colorIndex == static_cast(skin)) { faceMax = std::max(faceMax, static_cast(variationIndex)); diff --git a/tools/asset_extract/extractor.cpp b/tools/asset_extract/extractor.cpp index c835f6c5..0fd12b55 100644 --- a/tools/asset_extract/extractor.cpp +++ b/tools/asset_extract/extractor.cpp @@ -246,6 +246,58 @@ static std::unordered_set buildWantedDbcSet(const Extractor::Option return wanted; } +// Load all entry keys from a manifest.json into a set of normalized WoW paths. +// This is a minimal parser — just extracts the keys from the "entries" object +// without pulling in a full JSON library. +static std::unordered_set loadManifestKeys(const std::string& manifestPath) { + std::unordered_set keys; + std::ifstream f(manifestPath); + if (!f.is_open()) { + std::cerr << "Failed to open reference manifest: " << manifestPath << "\n"; + return keys; + } + + // Find the "entries" section, then extract keys from each line + bool inEntries = false; + std::string line; + while (std::getline(f, line)) { + if (!inEntries) { + if (line.find("\"entries\"") != std::string::npos) { + inEntries = true; + } + continue; + } + + // End of entries block + size_t closeBrace = line.find_first_not_of(" \t"); + if (closeBrace != std::string::npos && line[closeBrace] == '}') { + break; + } + + // Extract key: find first quoted string on the line + size_t q1 = line.find('"'); + if (q1 == std::string::npos) continue; + size_t q2 = q1 + 1; + // Find closing quote (handle escaped backslashes) + std::string key; + while (q2 < line.size() && line[q2] != '"') { + if (line[q2] == '\\' && q2 + 1 < line.size()) { + key += line[q2 + 1]; // unescape \\, \", etc. + q2 += 2; + } else { + key += line[q2]; + q2++; + } + } + + if (!key.empty()) { + keys.insert(key); // Already normalized (lowercase, backslashes) + } + } + + return keys; +} + // Known WoW client locales static const std::vector kKnownLocales = { "enUS", "enGB", "deDE", "frFR", "esES", "esMX", @@ -485,6 +537,22 @@ bool Extractor::run(const Options& opts) { return false; } + // Delta extraction: filter out files that already exist in the reference manifest + if (!opts.referenceManifest.empty()) { + auto refKeys = loadManifestKeys(opts.referenceManifest); + if (refKeys.empty()) { + std::cerr << "Warning: reference manifest is empty or failed to load\n"; + } else { + size_t before = files.size(); + files.erase(std::remove_if(files.begin(), files.end(), + [&refKeys](const std::string& wowPath) { + return refKeys.count(normalizeWowPath(wowPath)) > 0; + }), files.end()); + std::cout << "Delta filter: " << before << " -> " << files.size() + << " files (" << (before - files.size()) << " already in reference)\n"; + } + } + if (files.empty()) { std::cerr << "No files to extract\n"; return false; diff --git a/tools/asset_extract/extractor.hpp b/tools/asset_extract/extractor.hpp index 6245920e..e9aa646d 100644 --- a/tools/asset_extract/extractor.hpp +++ b/tools/asset_extract/extractor.hpp @@ -25,6 +25,7 @@ public: bool skipDbcExtraction = false; // Extract visual assets only (recommended when CSV DBCs are in repo) bool onlyUsedDbcs = false; // Extract only the DBC files wowee uses (implies DBFilesClient/*.dbc filter) std::string dbcCsvOutputDir; // When set, write CSVs into this directory instead of outputDir/expansions//db + std::string referenceManifest; // If set, only extract files NOT in this manifest (delta extraction) }; struct Stats { diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index e7d9ee6d..09748644 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -19,6 +19,8 @@ static void printUsage(const char* prog) { << " --skip-dbc Do not extract DBFilesClient/*.dbc (visual assets only)\n" << " --dbc-csv Convert selected DBFilesClient/*.dbc to CSV under\n" << " /expansions//db/*.csv (for committing)\n" + << " --reference-manifest \n" + << " Only extract files NOT in this manifest (delta extraction)\n" << " --dbc-csv-out Write CSV DBCs into (overrides default output path)\n" << " --verify CRC32 verify all extracted files\n" << " --threads Number of extraction threads (default: auto)\n" @@ -50,6 +52,8 @@ int main(int argc, char** argv) { opts.generateDbcCsv = true; } else if (std::strcmp(argv[i], "--dbc-csv-out") == 0 && i + 1 < argc) { opts.dbcCsvOutputDir = argv[++i]; + } else if (std::strcmp(argv[i], "--reference-manifest") == 0 && i + 1 < argc) { + opts.referenceManifest = argv[++i]; } else if (std::strcmp(argv[i], "--verify") == 0) { opts.verify = true; } else if (std::strcmp(argv[i], "--verbose") == 0) { @@ -114,6 +118,10 @@ int main(int argc, char** argv) { } } + if (!opts.referenceManifest.empty()) { + std::cout << "Reference: " << opts.referenceManifest << " (delta mode)\n"; + } + if (!wowee::tools::Extractor::run(opts)) { std::cerr << "Extraction failed!\n"; return 1;