Add per-expansion asset overlay system and fix CharSections DBC layout

Expansion overlays allow each expansion to supplement the base asset data
via an assetManifest field in expansion.json, loaded at priority 50 (below
HD packs). The asset extractor gains --reference-manifest for delta-only
extraction. Also fixes CharSections field indices (VariationIndex=4,
ColorIndex=5, Texture1=6) across all DBC layout references.
This commit is contained in:
Kelsi 2026-02-14 00:00:26 -08:00
parent 85864ab05b
commit 886f4daf2e
13 changed files with 151 additions and 46 deletions

View file

@ -9,8 +9,8 @@
}, },
"CharSections": { "CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3, "RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6, "VariationIndex": 4, "ColorIndex": 5,
"VariationIndex": 8, "ColorIndex": 9 "Texture1": 6, "Texture2": 7, "Texture3": 8
}, },
"SpellIcon": { "ID": 0, "Path": 1 }, "SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": { "FactionTemplate": {

View file

@ -9,8 +9,8 @@
}, },
"CharSections": { "CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3, "RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6, "VariationIndex": 4, "ColorIndex": 5,
"VariationIndex": 8, "ColorIndex": 9 "Texture1": 6, "Texture2": 7, "Texture3": 8
}, },
"SpellIcon": { "ID": 0, "Path": 1 }, "SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": { "FactionTemplate": {

View file

@ -216,6 +216,8 @@ private:
bool wasAutoAttacking_ = false; bool wasAutoAttacking_ = false;
void processPendingMount(); void processPendingMount();
bool creatureLookupsBuilt_ = false; bool creatureLookupsBuilt_ = false;
bool mapNameCacheLoaded_ = false;
std::unordered_map<uint32_t, std::string> mapNameById_;
// Deferred creature spawn queue (throttles spawning to avoid hangs) // Deferred creature spawn queue (throttles spawning to avoid hangs)
struct PendingCreatureSpawn { struct PendingCreatureSpawn {

View file

@ -30,6 +30,7 @@ struct ExpansionProfile {
std::string locale = "enUS"; std::string locale = "enUS";
uint32_t timezone = 0; uint32_t timezone = 0;
std::string dataPath; // Absolute path to expansion data dir 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; uint32_t maxLevel = 60;
std::vector<uint32_t> races; std::vector<uint32_t> races;
std::vector<uint32_t> classes; std::vector<uint32_t> classes;

View file

@ -231,6 +231,16 @@ bool Application::initialize() {
} }
hdPackManager_->applyToAssetManager(assetManager.get(), expansionId); 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 { } else {
LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable"); 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"); 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()) { if (assetManager && !profile->dataPath.empty()) {
assetManager->setExpansionDataPath(profile->dataPath); assetManager->setExpansionDataPath(profile->dataPath);
assetManager->clearDBCCache(); 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() { void Application::logoutToLogin() {
@ -1828,13 +1850,13 @@ void Application::spawnPlayerCharacter() {
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (raceId != targetRaceId || sexId != targetSexId) continue; if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0 = skin: match by colorIndex = skin byte // 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) { if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
std::string tex1 = charSectionsDbc->getString(r, csTex1); std::string tex1 = charSectionsDbc->getString(r, csTex1);
if (!tex1.empty()) { 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). // 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. // This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor.
static bool mapNameCacheLoaded = false; if (!mapNameCacheLoaded_ && assetManager) {
static std::unordered_map<uint32_t, std::string> mapNameById; mapNameCacheLoaded_ = true;
if (!mapNameCacheLoaded && assetManager) {
mapNameCacheLoaded = true;
if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) { 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; const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr;
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) { for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0); uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0);
std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1); std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
if (!internalName.empty() && mapNameById.find(id) == mapNameById.end()) { if (!internalName.empty() && mapNameById_.find(id) == mapNameById_.end()) {
mapNameById[id] = std::move(internalName); 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 { } else {
LOG_WARNING("Map.dbc not available; using fallback map-id mapping"); LOG_WARNING("Map.dbc not available; using fallback map-id mapping");
} }
} }
std::string mapName; std::string mapName;
if (auto it = mapNameById.find(mapId); it != mapNameById.end()) { if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) {
mapName = it->second; mapName = it->second;
} else { } else {
mapName = mapIdToName(mapId); 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 raceId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2); uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2);
uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3); uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3);
uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 8); uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 4);
uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 9); uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 5);
if (raceId != targetRace || sexId != targetSex) continue; if (raceId != targetRace || sexId != targetSex) continue;
if (section != 3) continue; // Section 3 = hair if (section != 3) continue; // Section 3 = hair
if (variation != static_cast<uint32_t>(extra.hairStyleId)) continue; if (variation != static_cast<uint32_t>(extra.hairStyleId)) continue;
if (colorIdx != static_cast<uint32_t>(extra.hairColorId)) continue; if (colorIdx != static_cast<uint32_t>(extra.hairColorId)) continue;
hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 4); hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 6);
break; break;
} }
@ -3667,7 +3687,7 @@ void Application::spawnOnlinePlayer(uint64_t guid,
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
uint32_t targetRaceId = raceId; uint32_t targetRaceId = raceId;
uint32_t targetSexId = genderId; uint32_t targetSexId = genderId;
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4; const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6;
bool foundSkin = false; bool foundSkin = false;
bool foundUnderwear = 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 rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t rSex = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t rSex = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (rRace != targetRaceId || rSex != targetSexId) continue; if (rRace != targetRaceId || rSex != targetSexId) continue;

View file

@ -176,6 +176,12 @@ bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::stri
v = jsonValue(json, "locale"); if (!v.empty()) p.locale = v; v = jsonValue(json, "locale"); if (!v.empty()) p.locale = v;
p.timezone = static_cast<uint32_t>(jsonInt(json, "timezone", static_cast<int>(p.timezone))); p.timezone = static_cast<uint32_t>(jsonInt(json, "timezone", static_cast<int>(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<uint32_t>(jsonInt(json, "maxLevel", 60)); p.maxLevel = static_cast<uint32_t>(jsonInt(json, "maxLevel", 60));
p.races = jsonUintArray(json, "races"); p.races = jsonUintArray(json, "races");
p.classes = jsonUintArray(json, "classes"); p.classes = jsonUintArray(json, "classes");

View file

@ -274,6 +274,7 @@ void GameHandler::update(float deltaTime) {
auto pkt = InspectPacket::build(guid); auto pkt = InspectPacket::build(guid);
socket->send(pkt); socket->send(pkt);
inspectRateLimit_ = 0.75f; // keep it gentle 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::map<ui
// Layout not detected yet — queue this player for inspect as fallback. // Layout not detected yet — queue this player for inspect as fallback.
if (socket && state == WorldState::IN_WORLD) { if (socket && state == WorldState::IN_WORLD) {
pendingAutoInspect_.insert(guid); pendingAutoInspect_.insert(guid);
LOG_INFO("Queued player 0x", std::hex, guid, std::dec, " for auto-inspect (layout not detected)");
} }
return; return;
} }

View file

@ -24,8 +24,8 @@ void DBCLayout::loadWotlkDefaults() {
// CharSections.dbc // CharSections.dbc
layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 }, layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 },
{ "Texture1", 4 }, { "Texture2", 5 }, { "Texture3", 6 }, { "VariationIndex", 4 }, { "ColorIndex", 5 },
{ "VariationIndex", 8 }, { "ColorIndex", 9 }}}; { "Texture1", 6 }, { "Texture2", 7 }, { "Texture3", 8 }}};
// SpellIcon.dbc (Icon.dbc in code but actually SpellIcon) // SpellIcon.dbc (Icon.dbc in code but actually SpellIcon)
layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}}; layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}};

View file

@ -167,19 +167,24 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("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++) { for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t raceId = charSectionsDbc->getUInt32(r, fRace);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t sexId = charSectionsDbc->getUInt32(r, fSex);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t baseSection = charSectionsDbc->getUInt32(r, fBase);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); uint32_t variationIndex = charSectionsDbc->getUInt32(r, fVar);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); uint32_t colorIndex = charSectionsDbc->getUInt32(r, fColor);
if (raceId != targetRaceId || sexId != targetSexId) continue; if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0: Body skin (variation=0, colorIndex = skin color) // Section 0: Body skin (variation=0, colorIndex = skin color)
if (baseSection == 0 && !foundSkin && if (baseSection == 0 && !foundSkin &&
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) { variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!tex1.empty()) { if (!tex1.empty()) {
bodySkinPath_ = tex1; bodySkinPath_ = tex1;
foundSkin = true; foundSkin = true;
@ -189,8 +194,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
else if (baseSection == 1 && !foundFace && else if (baseSection == 1 && !foundFace &&
variationIndex == static_cast<uint32_t>(face) && variationIndex == static_cast<uint32_t>(face) &&
colorIndex == static_cast<uint32_t>(skin)) { colorIndex == static_cast<uint32_t>(skin)) {
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6);
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5); std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7);
if (!tex1.empty()) faceLowerPath = tex1; if (!tex1.empty()) faceLowerPath = tex1;
if (!tex2.empty()) faceUpperPath = tex2; if (!tex2.empty()) faceUpperPath = tex2;
foundFace = true; foundFace = true;
@ -199,7 +204,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
else if (baseSection == 3 && !foundHair && else if (baseSection == 3 && !foundHair &&
variationIndex == static_cast<uint32_t>(hairStyle) && variationIndex == static_cast<uint32_t>(hairStyle) &&
colorIndex == static_cast<uint32_t>(hairColor)) { colorIndex == static_cast<uint32_t>(hairColor)) {
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!tex1.empty()) { if (!tex1.empty()) {
hairScalpPath = tex1; hairScalpPath = tex1;
foundHair = true; foundHair = true;
@ -208,7 +213,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
// Section 4: Underwear (variation=0, colorIndex = skin color) // Section 4: Underwear (variation=0, colorIndex = skin color)
else if (baseSection == 4 && !foundUnderwear && else if (baseSection == 4 && !foundUnderwear &&
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) { variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
uint32_t texBase = csL ? (*csL)["Texture1"] : 4; uint32_t texBase = csL ? (*csL)["Texture1"] : 6;
for (uint32_t f = texBase; f <= texBase + 2; f++) { for (uint32_t f = texBase; f <= texBase + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f); std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) { 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 // Assign texture filenames on model before GPU upload
for (size_t ti = 0; ti < model.textures.size(); ti++) { for (size_t ti = 0; ti < model.textures.size(); ti++) {
auto& tex = model.textures[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()) { if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) {
tex.filename = bodySkinPath_; tex.filename = bodySkinPath_;
} else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) { } 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"); LOG_WARNING("CharacterPreview: failed to load model to GPU");
return false; 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 // Composite body skin + face + underwear overlays
if (!bodySkinPath_.empty()) { if (!bodySkinPath_.empty()) {
std::vector<std::string> layers; std::vector<std::string> layers;

View file

@ -179,8 +179,8 @@ void CharacterCreateScreen::updateAppearanceRanges() {
if (raceId != targetRaceId || sexId != targetSexId) continue; if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (baseSection == 0 && variationIndex == 0) { if (baseSection == 0 && variationIndex == 0) {
skinMax = std::max(skinMax, static_cast<int>(colorIndex)); skinMax = std::max(skinMax, static_cast<int>(colorIndex));
@ -206,8 +206,8 @@ void CharacterCreateScreen::updateAppearanceRanges() {
if (raceId != targetRaceId || sexId != targetSexId) continue; if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) { if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
faceMax = std::max(faceMax, static_cast<int>(variationIndex)); faceMax = std::max(faceMax, static_cast<int>(variationIndex));

View file

@ -246,6 +246,58 @@ static std::unordered_set<std::string> buildWantedDbcSet(const Extractor::Option
return wanted; 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<std::string> loadManifestKeys(const std::string& manifestPath) {
std::unordered_set<std::string> 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 // Known WoW client locales
static const std::vector<std::string> kKnownLocales = { static const std::vector<std::string> kKnownLocales = {
"enUS", "enGB", "deDE", "frFR", "esES", "esMX", "enUS", "enGB", "deDE", "frFR", "esES", "esMX",
@ -485,6 +537,22 @@ bool Extractor::run(const Options& opts) {
return false; 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()) { if (files.empty()) {
std::cerr << "No files to extract\n"; std::cerr << "No files to extract\n";
return false; return false;

View file

@ -25,6 +25,7 @@ public:
bool skipDbcExtraction = false; // Extract visual assets only (recommended when CSV DBCs are in repo) 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) 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/<exp>/db std::string dbcCsvOutputDir; // When set, write CSVs into this directory instead of outputDir/expansions/<exp>/db
std::string referenceManifest; // If set, only extract files NOT in this manifest (delta extraction)
}; };
struct Stats { struct Stats {

View file

@ -19,6 +19,8 @@ static void printUsage(const char* prog) {
<< " --skip-dbc Do not extract DBFilesClient/*.dbc (visual assets only)\n" << " --skip-dbc Do not extract DBFilesClient/*.dbc (visual assets only)\n"
<< " --dbc-csv Convert selected DBFilesClient/*.dbc to CSV under\n" << " --dbc-csv Convert selected DBFilesClient/*.dbc to CSV under\n"
<< " <output>/expansions/<expansion>/db/*.csv (for committing)\n" << " <output>/expansions/<expansion>/db/*.csv (for committing)\n"
<< " --reference-manifest <path>\n"
<< " Only extract files NOT in this manifest (delta extraction)\n"
<< " --dbc-csv-out <dir> Write CSV DBCs into <dir> (overrides default output path)\n" << " --dbc-csv-out <dir> Write CSV DBCs into <dir> (overrides default output path)\n"
<< " --verify CRC32 verify all extracted files\n" << " --verify CRC32 verify all extracted files\n"
<< " --threads <N> Number of extraction threads (default: auto)\n" << " --threads <N> Number of extraction threads (default: auto)\n"
@ -50,6 +52,8 @@ int main(int argc, char** argv) {
opts.generateDbcCsv = true; opts.generateDbcCsv = true;
} else if (std::strcmp(argv[i], "--dbc-csv-out") == 0 && i + 1 < argc) { } else if (std::strcmp(argv[i], "--dbc-csv-out") == 0 && i + 1 < argc) {
opts.dbcCsvOutputDir = argv[++i]; 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) { } else if (std::strcmp(argv[i], "--verify") == 0) {
opts.verify = true; opts.verify = true;
} else if (std::strcmp(argv[i], "--verbose") == 0) { } 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)) { if (!wowee::tools::Extractor::run(opts)) {
std::cerr << "Extraction failed!\n"; std::cerr << "Extraction failed!\n";
return 1; return 1;