From 886f4daf2e3c10ac293601334bca53c344ad8f49 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Feb 2026 00:00:26 -0800 Subject: [PATCH] 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. --- Data/expansions/tbc/dbc_layouts.json | 4 +- Data/expansions/wotlk/dbc_layouts.json | 4 +- include/core/application.hpp | 2 + include/game/expansion_profile.hpp | 1 + src/core/application.cpp | 56 ++++++++++++++------- src/game/expansion_profile.cpp | 6 +++ src/game/game_handler.cpp | 2 + src/pipeline/dbc_layout.cpp | 4 +- src/rendering/character_preview.cpp | 33 ++++++------- src/ui/character_create_screen.cpp | 8 +-- tools/asset_extract/extractor.cpp | 68 ++++++++++++++++++++++++++ tools/asset_extract/extractor.hpp | 1 + tools/asset_extract/main.cpp | 8 +++ 13 files changed, 151 insertions(+), 46 deletions(-) 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;