From 4fc0361f7ada49eab3abde92a8bf710a504df74c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 12:41:19 -0700 Subject: [PATCH] feat: complete client integration for all 6 open formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- include/pipeline/dbc_loader.hpp | 2 + include/pipeline/wowee_building.hpp | 3 + include/rendering/renderer.hpp | 4 ++ include/rendering/terrain_manager.hpp | 3 + src/pipeline/asset_manager.cpp | 17 ++++-- src/pipeline/dbc_loader.cpp | 79 +++++++++++++++++++++++++++ src/pipeline/wowee_building.cpp | 64 ++++++++++++++++++++++ src/rendering/renderer.cpp | 10 ++-- src/rendering/terrain_manager.cpp | 65 +++++++++++----------- tools/editor/editor_app.cpp | 28 ++++++++-- tools/editor/main.cpp | 43 +++++++++++++++ 11 files changed, 271 insertions(+), 47 deletions(-) diff --git a/include/pipeline/dbc_loader.hpp b/include/pipeline/dbc_loader.hpp index 49f9df63..6cf63899 100644 --- a/include/pipeline/dbc_loader.hpp +++ b/include/pipeline/dbc_loader.hpp @@ -149,6 +149,8 @@ private: * Rebuilds the same in-memory layout as binary load. */ bool loadCSV(const std::vector& csvData); + + bool loadJSON(const std::vector& jsonData); }; /** diff --git a/include/pipeline/wowee_building.hpp b/include/pipeline/wowee_building.hpp index 98e9d625..e78e802c 100644 --- a/include/pipeline/wowee_building.hpp +++ b/include/pipeline/wowee_building.hpp @@ -56,6 +56,9 @@ public: // Convert WOB to WMOModel for the client's WMO renderer 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 diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index d92fb76a..9e92f2a0 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -14,6 +14,7 @@ #include "rendering/vk_frame_data.hpp" #include "rendering/vk_utils.hpp" #include "rendering/sky_system.hpp" +#include "pipeline/custom_zone_discovery.hpp" namespace wowee { namespace core { class Window; } @@ -188,6 +189,8 @@ public: game::ZoneManager* getZoneManager() { return zoneManager.get(); } LightingManager* getLightingManager() { return lightingManager.get(); } + const std::vector& getCustomZones() const { return customZones_; } + private: void runDeferredWorldInitStep(float deltaTime); @@ -267,6 +270,7 @@ private: void renderShadowPass(); glm::mat4 computeLightSpaceMatrix(); + std::vector customZones_; pipeline::AssetManager* cachedAssetManager = nullptr; // Spell visual effects — owned SpellVisualSystem (extracted from Renderer §4.4) diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 85090dab..dc9c0626 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -201,6 +201,8 @@ public: * @param mapName Map name (e.g., "Azeroth", "Kalimdor") */ void setMapName(const std::string& mapName) { this->mapName = mapName; } + bool isCustomZone() const { return isCustomZone_; } + void setCustomZone(bool custom) { isCustomZone_ = custom; } /** * Load a single tile @@ -352,6 +354,7 @@ private: float timeSinceLastUpdate = 0.0f; float proactiveStreamTimer_ = 0.0f; bool taxiStreamingMode_ = false; + bool isCustomZone_ = false; // Tile size constants (WoW ADT specifications) // A tile (ADT) = 16x16 chunks = 533.33 units across diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 2bff1972..a38583de 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -312,23 +312,30 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { } // 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()) { std::string baseName = name; auto dot = baseName.rfind('.'); 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; for (auto& entry : std::filesystem::directory_iterator(dir)) { if (!entry.is_directory()) continue; std::string jsonPath = entry.path().string() + "/data/" + baseName + ".json"; 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(sz)); + jf.seekg(0); + jf.read(reinterpret_cast(dbcData.data()), sz); + LOG_INFO("Loading JSON DBC override: ", jsonPath); + } + } break; } } + if (!dbcData.empty()) break; } } diff --git a/src/pipeline/dbc_loader.cpp b/src/pipeline/dbc_loader.cpp index bb35ba44..b6f397b3 100644 --- a/src/pipeline/dbc_loader.cpp +++ b/src/pipeline/dbc_loader.cpp @@ -1,5 +1,6 @@ #include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -37,6 +38,15 @@ bool DBCFile::load(const std::vector& 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)) { LOG_ERROR("DBC data too small for header"); return false; @@ -368,5 +378,74 @@ bool DBCFile::loadCSV(const std::vector& csvData) { return true; } +bool DBCFile::loadJSON(const std::vector& 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(records[0].size()); + } + if (fieldCount == 0) return false; + + recordSize = fieldCount * 4; + recordCount = static_cast(records.size()); + + stringBlock.clear(); + stringBlock.push_back(0); + + recordData.resize(static_cast(recordCount) * recordSize, 0); + + for (uint32_t i = 0; i < recordCount; i++) { + const auto& row = records[i]; + uint32_t* fields = reinterpret_cast( + recordData.data() + static_cast(i) * recordSize); + + uint32_t cols = std::min(fieldCount, static_cast(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(); + if (str.empty()) { + fields[col] = 0; + } else { + fields[col] = static_cast(stringBlock.size()); + stringBlock.insert(stringBlock.end(), str.begin(), str.end()); + stringBlock.push_back(0); + } + } else if (val.is_number_float()) { + float f = val.get(); + std::memcpy(&fields[col], &f, 4); + } else if (val.is_number_integer()) { + fields[col] = val.get(); + } + } + } + + stringBlockSize = static_cast(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 wowee diff --git a/src/pipeline/wowee_building.cpp b/src/pipeline/wowee_building.cpp index 74ca4ac6..b0c043af 100644 --- a/src/pipeline/wowee_building.cpp +++ b/src/pipeline/wowee_building.cpp @@ -196,5 +196,69 @@ bool WoweeBuildingLoader::toWMOModel(const WoweeBuilding& building, WMOModel& ou 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(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 wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 019fe65d..67a14ff4 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1887,13 +1887,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s LOG_INFO("Initializing renderers for map: ", mapName); // Scan for custom zones on first initialization - static bool customZonesScanned = false; - if (!customZonesScanned) { - customZonesScanned = true; - auto customZones = pipeline::CustomZoneDiscovery::scan({"custom_zones", "output"}); - if (!customZones.empty()) { + if (customZones_.empty()) { + customZones_ = pipeline::CustomZoneDiscovery::scan({"custom_zones", "output"}); + if (!customZones_.empty()) { LOG_INFO("=== Custom Zones Available ==="); - for (const auto& z : customZones) { + for (const auto& z : customZones_) { LOG_INFO(" ", z.name, " (", z.directory, ")", z.hasCreatures ? " [NPCs]" : "", z.hasQuests ? " [Quests]" : ""); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index ad4b4d6e..b13ed706 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -597,6 +597,8 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId]; // Check for WOB open format first (custom zone buildings) + bool wobLoaded = false; + pipeline::WMOModel wmoModel; { std::string wobBase = wmoPath; auto wobDot = wobBase.rfind('.'); @@ -607,10 +609,9 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { if (pipeline::WoweeBuildingLoader::exists(prefix + wobBase)) { auto wob = pipeline::WoweeBuildingLoader::load(prefix + wobBase); if (wob.isValid()) { - pipeline::WMOModel wobAsWmo; - if (pipeline::WoweeBuildingLoader::toWMOModel(wob, wobAsWmo)) { + if (pipeline::WoweeBuildingLoader::toWMOModel(wob, wmoModel)) { LOG_INFO("Loaded WOB building: ", prefix + wobBase); - // TODO: feed wobAsWmo into the WMO render pipeline + wobLoaded = true; } } break; @@ -618,37 +619,39 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { } } - std::vector wmoData = assetManager->readFile(wmoPath); - if (wmoData.empty()) continue; + if (!wobLoaded) { + std::vector wmoData = assetManager->readFile(wmoPath); + if (wmoData.empty()) continue; - pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); - if (wmoModel.nGroups > 0) { - std::string basePath = wmoPath; - std::string extension; - if (basePath.size() > 4) { - extension = basePath.substr(basePath.size() - 4); - std::string extLower = extension; - for (char& c : extLower) c = static_cast(std::tolower(static_cast(c))); - if (extLower == ".wmo") { - basePath = basePath.substr(0, basePath.size() - 4); + wmoModel = pipeline::WMOLoader::load(wmoData); + if (wmoModel.nGroups > 0) { + std::string basePath = wmoPath; + std::string extension; + if (basePath.size() > 4) { + extension = basePath.substr(basePath.size() - 4); + std::string extLower = extension; + for (char& c : extLower) c = static_cast(std::tolower(static_cast(c))); + if (extLower == ".wmo") { + basePath = basePath.substr(0, basePath.size() - 4); + } } - } - for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { - char groupSuffix[16]; - snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str()); - std::string groupPath = basePath + groupSuffix; - std::vector groupData = assetManager->readFile(groupPath); - if (groupData.empty()) { - snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); - groupData = assetManager->readFile(basePath + groupSuffix); - } - if (groupData.empty()) { - snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi); - groupData = assetManager->readFile(basePath + groupSuffix); - } - if (!groupData.empty()) { - pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi); + for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { + char groupSuffix[16]; + snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str()); + std::string groupPath = basePath + groupSuffix; + std::vector groupData = assetManager->readFile(groupPath); + if (groupData.empty()) { + snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); + groupData = assetManager->readFile(basePath + groupSuffix); + } + if (groupData.empty()) { + snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi); + groupData = assetManager->readFile(basePath + groupSuffix); + } + if (!groupData.empty()) { + pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi); + } } } } diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 95842f24..9b34d098 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -7,6 +7,7 @@ #include "dbc_exporter.hpp" #include "pipeline/wowee_model.hpp" #include "pipeline/wowee_building.hpp" +#include "pipeline/wmo_loader.hpp" #include "core/coordinates.hpp" #include "rendering/vk_context.hpp" #include "pipeline/adt_loader.hpp" @@ -761,19 +762,36 @@ void EditorApp::exportZone(const std::string& outputDir) { std::unordered_set convertedWMOs; for (const auto& obj : objectPlacer_.getObjects()) { 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::replace(wobPath.begin(), wobPath.end(), '\\', '/'); auto dot = wobPath.rfind('.'); 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); } } 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) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 43b058ec..276a31fe 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -1,5 +1,7 @@ #include "editor_app.hpp" #include "pipeline/wowee_model.hpp" +#include "pipeline/wowee_building.hpp" +#include "pipeline/wmo_loader.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/custom_zone_discovery.hpp" #include "core/logger.hpp" @@ -13,6 +15,7 @@ static void printUsage(const char* argv0) { LOG_INFO(" --data Path to extracted WoW data (manifest.json)"); LOG_INFO(" --adt Load an ADT tile on startup"); LOG_INFO(" --convert-m2 Convert M2 model to WOM open format (no GUI)"); + LOG_INFO(" --convert-wmo Convert WMO building to WOB open format (no GUI)"); LOG_INFO(" --list-zones List discovered custom zones and exit"); LOG_INFO(" --version Show version and format 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()) { dataPath = "Data"; LOG_INFO("No --data path specified, using default: ", dataPath);