feat: complete client integration for all 6 open formats

- Wire WOB buildings into WMO render pipeline (loads→converts→renders)
- Implement JSON DBC loading in DBCFile::loadJSON() with nlohmann/json
- Wire JSON DBC override into AssetManager (custom_zones/output scan)
- Add WMO→WOB conversion with full geometry (fromWMO)
- Replace placeholder WOB export with real WMO→WOB conversion in editor
- Add --convert-wmo CLI flag for batch WMO→WOB conversion
- Store discovered custom zones on Renderer with getCustomZones() accessor
- Add isCustomZone_ member to TerrainManager

All 6 Blizzard format replacements now fully load in the client:
  ADT→WOT/WHM, WDT→zone.json, BLP→PNG, DBC→JSON, M2→WOM, WMO→WOB
This commit is contained in:
Kelsi 2026-05-05 12:41:19 -07:00
parent d8f2388635
commit 4fc0361f7a
11 changed files with 271 additions and 47 deletions

View file

@ -149,6 +149,8 @@ private:
* Rebuilds the same in-memory layout as binary load. * Rebuilds the same in-memory layout as binary load.
*/ */
bool loadCSV(const std::vector<uint8_t>& csvData); bool loadCSV(const std::vector<uint8_t>& csvData);
bool loadJSON(const std::vector<uint8_t>& jsonData);
}; };
/** /**

View file

@ -56,6 +56,9 @@ public:
// Convert WOB to WMOModel for the client's WMO renderer // Convert WOB to WMOModel for the client's WMO renderer
static bool toWMOModel(const WoweeBuilding& building, class WMOModel& outModel); static bool toWMOModel(const WoweeBuilding& building, class WMOModel& outModel);
// Convert WMOModel to WOB (for editor export)
static WoweeBuilding fromWMO(const class WMOModel& wmo, const std::string& name = "");
}; };
} // namespace pipeline } // namespace pipeline

View file

@ -14,6 +14,7 @@
#include "rendering/vk_frame_data.hpp" #include "rendering/vk_frame_data.hpp"
#include "rendering/vk_utils.hpp" #include "rendering/vk_utils.hpp"
#include "rendering/sky_system.hpp" #include "rendering/sky_system.hpp"
#include "pipeline/custom_zone_discovery.hpp"
namespace wowee { namespace wowee {
namespace core { class Window; } namespace core { class Window; }
@ -188,6 +189,8 @@ public:
game::ZoneManager* getZoneManager() { return zoneManager.get(); } game::ZoneManager* getZoneManager() { return zoneManager.get(); }
LightingManager* getLightingManager() { return lightingManager.get(); } LightingManager* getLightingManager() { return lightingManager.get(); }
const std::vector<pipeline::CustomZoneInfo>& getCustomZones() const { return customZones_; }
private: private:
void runDeferredWorldInitStep(float deltaTime); void runDeferredWorldInitStep(float deltaTime);
@ -267,6 +270,7 @@ private:
void renderShadowPass(); void renderShadowPass();
glm::mat4 computeLightSpaceMatrix(); glm::mat4 computeLightSpaceMatrix();
std::vector<pipeline::CustomZoneInfo> customZones_;
pipeline::AssetManager* cachedAssetManager = nullptr; pipeline::AssetManager* cachedAssetManager = nullptr;
// Spell visual effects — owned SpellVisualSystem (extracted from Renderer §4.4) // Spell visual effects — owned SpellVisualSystem (extracted from Renderer §4.4)

View file

@ -201,6 +201,8 @@ public:
* @param mapName Map name (e.g., "Azeroth", "Kalimdor") * @param mapName Map name (e.g., "Azeroth", "Kalimdor")
*/ */
void setMapName(const std::string& mapName) { this->mapName = mapName; } void setMapName(const std::string& mapName) { this->mapName = mapName; }
bool isCustomZone() const { return isCustomZone_; }
void setCustomZone(bool custom) { isCustomZone_ = custom; }
/** /**
* Load a single tile * Load a single tile
@ -352,6 +354,7 @@ private:
float timeSinceLastUpdate = 0.0f; float timeSinceLastUpdate = 0.0f;
float proactiveStreamTimer_ = 0.0f; float proactiveStreamTimer_ = 0.0f;
bool taxiStreamingMode_ = false; bool taxiStreamingMode_ = false;
bool isCustomZone_ = false;
// Tile size constants (WoW ADT specifications) // Tile size constants (WoW ADT specifications)
// A tile (ADT) = 16x16 chunks = 533.33 units across // A tile (ADT) = 16x16 chunks = 533.33 units across

View file

@ -312,23 +312,30 @@ std::shared_ptr<DBCFile> AssetManager::loadDBC(const std::string& name) {
} }
// Check for JSON DBC from custom zones (wowee open format) // Check for JSON DBC from custom zones (wowee open format)
// JSON DBCs exported by the editor contain the same record data
// but the DBCFile::load() only handles binary — so JSON overrides
// are logged for now and will need a JSON→DBC converter in future.
if (dbcData.empty()) { if (dbcData.empty()) {
std::string baseName = name; std::string baseName = name;
auto dot = baseName.rfind('.'); auto dot = baseName.rfind('.');
if (dot != std::string::npos) baseName = baseName.substr(0, dot); if (dot != std::string::npos) baseName = baseName.substr(0, dot);
for (const std::string& dir : {"custom_zones", "output"}) { for (const char* dir : {"custom_zones", "output"}) {
if (!std::filesystem::exists(dir)) continue; if (!std::filesystem::exists(dir)) continue;
for (auto& entry : std::filesystem::directory_iterator(dir)) { for (auto& entry : std::filesystem::directory_iterator(dir)) {
if (!entry.is_directory()) continue; if (!entry.is_directory()) continue;
std::string jsonPath = entry.path().string() + "/data/" + baseName + ".json"; std::string jsonPath = entry.path().string() + "/data/" + baseName + ".json";
if (std::filesystem::exists(jsonPath)) { if (std::filesystem::exists(jsonPath)) {
LOG_DEBUG("JSON DBC available (not yet loaded): ", 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; break;
} }
} }
if (!dbcData.empty()) break;
} }
} }

View file

@ -1,5 +1,6 @@
#include "pipeline/dbc_loader.hpp" #include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
#include <nlohmann/json.hpp>
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <set> #include <set>
@ -37,6 +38,15 @@ bool DBCFile::load(const std::vector<uint8_t>& dbcData) {
return loadCSV(dbcData); return loadCSV(dbcData);
} }
// Detect JSON format: starts with '{'
if (dbcData[0] == '{' || (dbcData[0] <= ' ' && dbcData.size() > 1)) {
size_t start = 0;
while (start < dbcData.size() && dbcData[start] <= ' ') start++;
if (start < dbcData.size() && dbcData[start] == '{') {
return loadJSON(dbcData);
}
}
if (dbcData.size() < sizeof(DBCHeader)) { if (dbcData.size() < sizeof(DBCHeader)) {
LOG_ERROR("DBC data too small for header"); LOG_ERROR("DBC data too small for header");
return false; return false;
@ -368,5 +378,74 @@ bool DBCFile::loadCSV(const std::vector<uint8_t>& csvData) {
return true; return true;
} }
bool DBCFile::loadJSON(const std::vector<uint8_t>& jsonData) {
try {
auto j = nlohmann::json::parse(jsonData.begin(), jsonData.end());
if (!j.contains("records") || !j["records"].is_array()) {
LOG_ERROR("JSON DBC: missing 'records' array");
return false;
}
const auto& records = j["records"];
if (records.empty()) {
LOG_WARNING("JSON DBC: empty records array");
return false;
}
fieldCount = j.value("fieldCount", 0u);
if (fieldCount == 0 && !records[0].empty()) {
fieldCount = static_cast<uint32_t>(records[0].size());
}
if (fieldCount == 0) return false;
recordSize = fieldCount * 4;
recordCount = static_cast<uint32_t>(records.size());
stringBlock.clear();
stringBlock.push_back(0);
recordData.resize(static_cast<size_t>(recordCount) * recordSize, 0);
for (uint32_t i = 0; i < recordCount; i++) {
const auto& row = records[i];
uint32_t* fields = reinterpret_cast<uint32_t*>(
recordData.data() + static_cast<size_t>(i) * recordSize);
uint32_t cols = std::min(fieldCount, static_cast<uint32_t>(row.size()));
for (uint32_t col = 0; col < cols; col++) {
const auto& val = row[col];
if (val.is_string()) {
const std::string& str = val.get_ref<const std::string&>();
if (str.empty()) {
fields[col] = 0;
} else {
fields[col] = static_cast<uint32_t>(stringBlock.size());
stringBlock.insert(stringBlock.end(), str.begin(), str.end());
stringBlock.push_back(0);
}
} else if (val.is_number_float()) {
float f = val.get<float>();
std::memcpy(&fields[col], &f, 4);
} else if (val.is_number_integer()) {
fields[col] = val.get<uint32_t>();
}
}
}
stringBlockSize = static_cast<uint32_t>(stringBlock.size());
loaded = true;
idCacheBuilt = false;
idToIndexCache.clear();
LOG_INFO("Loaded JSON DBC: ", recordCount, " records, ",
fieldCount, " fields, ", stringBlockSize, " string bytes");
return true;
} catch (const std::exception& e) {
LOG_ERROR("JSON DBC parse error: ", e.what());
return false;
}
}
} // namespace pipeline } // namespace pipeline
} // namespace wowee } // namespace wowee

View file

@ -196,5 +196,69 @@ bool WoweeBuildingLoader::toWMOModel(const WoweeBuilding& building, WMOModel& ou
return true; return true;
} }
WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::string& name) {
WoweeBuilding bld;
bld.name = name.empty() ? "Converted WMO" : name;
float maxDist = 0.0f;
for (const auto& grp : wmo.groups) {
WoweeBuilding::Group wobGroup;
wobGroup.name = grp.name;
wobGroup.isOutdoor = (grp.flags & 0x08) != 0;
wobGroup.boundMin = grp.boundingBoxMin;
wobGroup.boundMax = grp.boundingBoxMax;
wobGroup.vertices.reserve(grp.vertices.size());
for (const auto& v : grp.vertices) {
WoweeBuilding::Vertex wv;
wv.position = v.position;
wv.normal = v.normal;
wv.texCoord = v.texCoord;
wv.color = v.color;
wobGroup.vertices.push_back(wv);
float d = glm::length(v.position);
if (d > maxDist) maxDist = d;
}
wobGroup.indices.reserve(grp.indices.size());
for (uint16_t idx : grp.indices)
wobGroup.indices.push_back(static_cast<uint32_t>(idx));
for (const auto& mat : wmo.materials) {
if (mat.texture1 < wmo.textures.size()) {
std::string texPath = wmo.textures[mat.texture1];
auto dot = texPath.rfind('.');
if (dot != std::string::npos)
texPath = texPath.substr(0, dot) + ".png";
wobGroup.texturePaths.push_back(texPath);
}
}
bld.groups.push_back(std::move(wobGroup));
}
bld.boundRadius = maxDist;
for (const auto& doodad : wmo.doodads) {
auto nameIt = wmo.doodadNames.find(doodad.nameIndex);
if (nameIt == wmo.doodadNames.end()) continue;
WoweeBuilding::DoodadPlacement dp;
dp.modelPath = nameIt->second;
auto dot = dp.modelPath.rfind('.');
if (dot != std::string::npos)
dp.modelPath = dp.modelPath.substr(0, dot) + ".wom";
dp.position = doodad.position;
dp.rotation = glm::vec3(0.0f);
dp.scale = doodad.scale;
bld.doodads.push_back(dp);
}
LOG_INFO("WOB from WMO: ", bld.name, " (", bld.groups.size(), " groups, ",
bld.doodads.size(), " doodads)");
return bld;
}
} // namespace pipeline } // namespace pipeline
} // namespace wowee } // namespace wowee

View file

@ -1887,13 +1887,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
LOG_INFO("Initializing renderers for map: ", mapName); LOG_INFO("Initializing renderers for map: ", mapName);
// Scan for custom zones on first initialization // Scan for custom zones on first initialization
static bool customZonesScanned = false; if (customZones_.empty()) {
if (!customZonesScanned) { customZones_ = pipeline::CustomZoneDiscovery::scan({"custom_zones", "output"});
customZonesScanned = true; if (!customZones_.empty()) {
auto customZones = pipeline::CustomZoneDiscovery::scan({"custom_zones", "output"});
if (!customZones.empty()) {
LOG_INFO("=== Custom Zones Available ==="); LOG_INFO("=== Custom Zones Available ===");
for (const auto& z : customZones) { for (const auto& z : customZones_) {
LOG_INFO(" ", z.name, " (", z.directory, ")", LOG_INFO(" ", z.name, " (", z.directory, ")",
z.hasCreatures ? " [NPCs]" : "", z.hasCreatures ? " [NPCs]" : "",
z.hasQuests ? " [Quests]" : ""); z.hasQuests ? " [Quests]" : "");

View file

@ -597,6 +597,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId]; const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
// Check for WOB open format first (custom zone buildings) // Check for WOB open format first (custom zone buildings)
bool wobLoaded = false;
pipeline::WMOModel wmoModel;
{ {
std::string wobBase = wmoPath; std::string wobBase = wmoPath;
auto wobDot = wobBase.rfind('.'); auto wobDot = wobBase.rfind('.');
@ -607,10 +609,9 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
if (pipeline::WoweeBuildingLoader::exists(prefix + wobBase)) { if (pipeline::WoweeBuildingLoader::exists(prefix + wobBase)) {
auto wob = pipeline::WoweeBuildingLoader::load(prefix + wobBase); auto wob = pipeline::WoweeBuildingLoader::load(prefix + wobBase);
if (wob.isValid()) { if (wob.isValid()) {
pipeline::WMOModel wobAsWmo; if (pipeline::WoweeBuildingLoader::toWMOModel(wob, wmoModel)) {
if (pipeline::WoweeBuildingLoader::toWMOModel(wob, wobAsWmo)) {
LOG_INFO("Loaded WOB building: ", prefix + wobBase); LOG_INFO("Loaded WOB building: ", prefix + wobBase);
// TODO: feed wobAsWmo into the WMO render pipeline wobLoaded = true;
} }
} }
break; break;
@ -618,37 +619,39 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
} }
} }
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath); if (!wobLoaded) {
if (wmoData.empty()) continue; std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
if (wmoData.empty()) continue;
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); wmoModel = pipeline::WMOLoader::load(wmoData);
if (wmoModel.nGroups > 0) { if (wmoModel.nGroups > 0) {
std::string basePath = wmoPath; std::string basePath = wmoPath;
std::string extension; std::string extension;
if (basePath.size() > 4) { if (basePath.size() > 4) {
extension = basePath.substr(basePath.size() - 4); extension = basePath.substr(basePath.size() - 4);
std::string extLower = extension; std::string extLower = extension;
for (char& c : extLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); for (char& c : extLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (extLower == ".wmo") { if (extLower == ".wmo") {
basePath = basePath.substr(0, basePath.size() - 4); basePath = basePath.substr(0, basePath.size() - 4);
}
} }
}
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char groupSuffix[16]; char groupSuffix[16];
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str()); snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
std::string groupPath = basePath + groupSuffix; std::string groupPath = basePath + groupSuffix;
std::vector<uint8_t> groupData = assetManager->readFile(groupPath); std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
if (groupData.empty()) { if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
groupData = assetManager->readFile(basePath + groupSuffix); groupData = assetManager->readFile(basePath + groupSuffix);
} }
if (groupData.empty()) { if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi); snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
groupData = assetManager->readFile(basePath + groupSuffix); groupData = assetManager->readFile(basePath + groupSuffix);
} }
if (!groupData.empty()) { if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi); pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
}
} }
} }
} }

View file

@ -7,6 +7,7 @@
#include "dbc_exporter.hpp" #include "dbc_exporter.hpp"
#include "pipeline/wowee_model.hpp" #include "pipeline/wowee_model.hpp"
#include "pipeline/wowee_building.hpp" #include "pipeline/wowee_building.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/coordinates.hpp" #include "core/coordinates.hpp"
#include "rendering/vk_context.hpp" #include "rendering/vk_context.hpp"
#include "pipeline/adt_loader.hpp" #include "pipeline/adt_loader.hpp"
@ -761,19 +762,36 @@ void EditorApp::exportZone(const std::string& outputDir) {
std::unordered_set<std::string> convertedWMOs; std::unordered_set<std::string> convertedWMOs;
for (const auto& obj : objectPlacer_.getObjects()) { for (const auto& obj : objectPlacer_.getObjects()) {
if (obj.type == PlaceableType::WMO && !convertedWMOs.count(obj.path)) { if (obj.type == PlaceableType::WMO && !convertedWMOs.count(obj.path)) {
// Create a placeholder WOB (full WMO→WOB conversion needs group loading)
pipeline::WoweeBuilding bld;
bld.name = obj.path;
std::string wobPath = obj.path; std::string wobPath = obj.path;
std::replace(wobPath.begin(), wobPath.end(), '\\', '/'); std::replace(wobPath.begin(), wobPath.end(), '\\', '/');
auto dot = wobPath.rfind('.'); auto dot = wobPath.rfind('.');
if (dot != std::string::npos) wobPath = wobPath.substr(0, dot); if (dot != std::string::npos) wobPath = wobPath.substr(0, dot);
pipeline::WoweeBuildingLoader::save(bld, base + "/buildings/" + wobPath);
auto wmoData = assetManager_->readFile(obj.path);
if (!wmoData.empty()) {
auto wmoModel = pipeline::WMOLoader::load(wmoData);
if (wmoModel.nGroups > 0) {
std::string wmoBase = obj.path;
if (wmoBase.size() > 4) wmoBase = wmoBase.substr(0, wmoBase.size() - 4);
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char suffix[16];
snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi);
auto gd = assetManager_->readFile(wmoBase + suffix);
if (!gd.empty()) pipeline::WMOLoader::loadGroup(gd, wmoModel, gi);
}
}
auto bld = pipeline::WoweeBuildingLoader::fromWMO(wmoModel, obj.path);
pipeline::WoweeBuildingLoader::save(bld, base + "/buildings/" + wobPath);
} else {
pipeline::WoweeBuilding bld;
bld.name = obj.path;
pipeline::WoweeBuildingLoader::save(bld, base + "/buildings/" + wobPath);
}
convertedWMOs.insert(obj.path); convertedWMOs.insert(obj.path);
} }
} }
if (!convertedWMOs.empty()) if (!convertedWMOs.empty())
LOG_INFO("Created ", convertedWMOs.size(), " WOB building placeholders"); LOG_INFO("Converted ", convertedWMOs.size(), " WMO buildings to WOB");
} }
// Export used textures as PNG (open format replacement for BLP) // Export used textures as PNG (open format replacement for BLP)

View file

@ -1,5 +1,7 @@
#include "editor_app.hpp" #include "editor_app.hpp"
#include "pipeline/wowee_model.hpp" #include "pipeline/wowee_model.hpp"
#include "pipeline/wowee_building.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/asset_manager.hpp" #include "pipeline/asset_manager.hpp"
#include "pipeline/custom_zone_discovery.hpp" #include "pipeline/custom_zone_discovery.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
@ -13,6 +15,7 @@ static void printUsage(const char* argv0) {
LOG_INFO(" --data <path> Path to extracted WoW data (manifest.json)"); LOG_INFO(" --data <path> Path to extracted WoW data (manifest.json)");
LOG_INFO(" --adt <map> <x> <y> Load an ADT tile on startup"); LOG_INFO(" --adt <map> <x> <y> Load an ADT tile on startup");
LOG_INFO(" --convert-m2 <path> Convert M2 model to WOM open format (no GUI)"); LOG_INFO(" --convert-m2 <path> Convert M2 model to WOM open format (no GUI)");
LOG_INFO(" --convert-wmo <path> Convert WMO building to WOB open format (no GUI)");
LOG_INFO(" --list-zones List discovered custom zones and exit"); LOG_INFO(" --list-zones List discovered custom zones and exit");
LOG_INFO(" --version Show version and format info"); LOG_INFO(" --version Show version and format info");
LOG_INFO(""); LOG_INFO("");
@ -81,6 +84,46 @@ int main(int argc, char* argv[]) {
} }
} }
// Batch convert mode: --convert-wmo converts WMO to WOB
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--convert-wmo") == 0 && i + 1 < argc) {
std::string wmoPath = argv[++i];
LOG_INFO("Batch convert mode: WMO→WOB for ", wmoPath);
if (dataPath.empty()) dataPath = "Data";
wowee::pipeline::AssetManager am;
if (am.initialize(dataPath)) {
auto wmoData = am.readFile(wmoPath);
if (!wmoData.empty()) {
auto wmoModel = wowee::pipeline::WMOLoader::load(wmoData);
if (wmoModel.nGroups > 0) {
std::string wmoBase = wmoPath;
if (wmoBase.size() > 4) wmoBase = wmoBase.substr(0, wmoBase.size() - 4);
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char suffix[16];
snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi);
auto gd = am.readFile(wmoBase + suffix);
if (!gd.empty()) wowee::pipeline::WMOLoader::loadGroup(gd, wmoModel, gi);
}
}
auto wob = wowee::pipeline::WoweeBuildingLoader::fromWMO(wmoModel, wmoPath);
if (wob.isValid()) {
std::string outPath = wmoPath;
auto dot = outPath.rfind('.');
if (dot != std::string::npos) outPath = outPath.substr(0, dot);
wowee::pipeline::WoweeBuildingLoader::save(wob, "output/buildings/" + outPath);
LOG_INFO("Converted: ", wmoPath, " → output/buildings/", outPath, ".wob");
} else {
LOG_ERROR("Failed to convert: ", wmoPath);
}
} else {
LOG_ERROR("WMO file not found: ", wmoPath);
}
am.shutdown();
}
return 0;
}
}
if (dataPath.empty()) { if (dataPath.empty()) {
dataPath = "Data"; dataPath = "Data";
LOG_INFO("No --data path specified, using default: ", dataPath); LOG_INFO("No --data path specified, using default: ", dataPath);