From ca15da5e9be3ef7378d50a253fb2967be9a13d12 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 14:44:46 -0700 Subject: [PATCH] feat: WOT doodad/WMO placements, WOB materials, deduplicate loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture fixes for open format data fidelity: - WOT now serializes full doodad/WMO placement arrays (positions, rotations, scale, flags, doodad sets) — was only storing counts, causing all placed objects to be lost on WOT round-trip - WOT loader parses placements back into ADTTerrain for client rendering - WOB Material struct added: preserves WMO material flags, shader type, and blend mode during WMO→WOB conversion (was geometry-only) - WOB doodad rotation: quaternion→euler conversion instead of hardcoded zero (placed doodads inside buildings now retain their orientation) - importOpen() deduplicated: delegates to pipeline::WoweeTerrainLoader instead of duplicating 100 lines of parsing code --- include/pipeline/wowee_building.hpp | 8 ++ src/pipeline/wowee_building.cpp | 12 ++- src/pipeline/wowee_terrain_loader.cpp | 48 ++++++++- tools/editor/wowee_terrain.cpp | 142 ++++++-------------------- 4 files changed, 100 insertions(+), 110 deletions(-) diff --git a/include/pipeline/wowee_building.hpp b/include/pipeline/wowee_building.hpp index e78e802c..0c611608 100644 --- a/include/pipeline/wowee_building.hpp +++ b/include/pipeline/wowee_building.hpp @@ -18,11 +18,19 @@ struct WoweeBuilding { glm::vec4 color; // vertex color/lighting }; + struct Material { + std::string texturePath; + uint32_t flags = 0; + uint32_t shader = 0; + uint32_t blendMode = 0; + }; + struct Group { std::string name; std::vector vertices; std::vector indices; std::vector texturePaths; + std::vector materials; glm::vec3 boundMin{0}, boundMax{0}; bool isOutdoor = false; }; diff --git a/src/pipeline/wowee_building.cpp b/src/pipeline/wowee_building.cpp index b0c043af..a0d27aa9 100644 --- a/src/pipeline/wowee_building.cpp +++ b/src/pipeline/wowee_building.cpp @@ -1,6 +1,7 @@ #include "pipeline/wowee_building.hpp" #include "pipeline/wmo_loader.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -226,13 +227,19 @@ WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::strin wobGroup.indices.push_back(static_cast(idx)); for (const auto& mat : wmo.materials) { + WoweeBuilding::Material wobMat; + wobMat.flags = mat.flags; + wobMat.shader = mat.shader; + wobMat.blendMode = mat.blendMode; 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"; + wobMat.texturePath = texPath; wobGroup.texturePaths.push_back(texPath); } + wobGroup.materials.push_back(wobMat); } bld.groups.push_back(std::move(wobGroup)); @@ -250,7 +257,10 @@ WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::strin if (dot != std::string::npos) dp.modelPath = dp.modelPath.substr(0, dot) + ".wom"; dp.position = doodad.position; - dp.rotation = glm::vec3(0.0f); + // Convert quaternion rotation to euler angles + glm::quat q(doodad.rotation.w, doodad.rotation.x, + doodad.rotation.y, doodad.rotation.z); + dp.rotation = glm::degrees(glm::eulerAngles(q)); dp.scale = doodad.scale; bld.doodads.push_back(dp); } diff --git a/src/pipeline/wowee_terrain_loader.cpp b/src/pipeline/wowee_terrain_loader.cpp index 2bcd1da9..ae89a45c 100644 --- a/src/pipeline/wowee_terrain_loader.cpp +++ b/src/pipeline/wowee_terrain_loader.cpp @@ -133,8 +133,54 @@ bool WoweeTerrainLoader::loadMetadata(const std::string& wotPath, ADTTerrain& te } } + // Parse doodad placements + if (j.contains("doodadNames") && j["doodadNames"].is_array()) { + for (const auto& n : j["doodadNames"]) + terrain.doodadNames.push_back(n.get()); + } + if (j.contains("doodads") && j["doodads"].is_array()) { + for (const auto& jd : j["doodads"]) { + ADTTerrain::DoodadPlacement dp{}; + dp.nameId = jd.value("nameId", 0u); + dp.uniqueId = jd.value("uniqueId", 0u); + if (jd.contains("pos") && jd["pos"].size() >= 3) { + dp.position[0] = jd["pos"][0]; dp.position[1] = jd["pos"][1]; dp.position[2] = jd["pos"][2]; + } + if (jd.contains("rot") && jd["rot"].size() >= 3) { + dp.rotation[0] = jd["rot"][0]; dp.rotation[1] = jd["rot"][1]; dp.rotation[2] = jd["rot"][2]; + } + dp.scale = jd.value("scale", 1024); + dp.flags = jd.value("flags", 0); + terrain.doodadPlacements.push_back(dp); + } + } + + // Parse WMO placements + if (j.contains("wmoNames") && j["wmoNames"].is_array()) { + for (const auto& n : j["wmoNames"]) + terrain.wmoNames.push_back(n.get()); + } + if (j.contains("wmos") && j["wmos"].is_array()) { + for (const auto& jw : j["wmos"]) { + ADTTerrain::WMOPlacement wp{}; + wp.nameId = jw.value("nameId", 0u); + wp.uniqueId = jw.value("uniqueId", 0u); + if (jw.contains("pos") && jw["pos"].size() >= 3) { + wp.position[0] = jw["pos"][0]; wp.position[1] = jw["pos"][1]; wp.position[2] = jw["pos"][2]; + } + if (jw.contains("rot") && jw["rot"].size() >= 3) { + wp.rotation[0] = jw["rot"][0]; wp.rotation[1] = jw["rot"][1]; wp.rotation[2] = jw["rot"][2]; + } + wp.flags = jw.value("flags", 0); + wp.doodadSet = jw.value("doodadSet", 0); + terrain.wmoPlacements.push_back(wp); + } + } + LOG_INFO("WOT loaded: ", wotPath, " (tile [", terrain.coord.x, ",", terrain.coord.y, - "], ", terrain.textures.size(), " textures)"); + "], ", terrain.textures.size(), " textures, ", + terrain.doodadPlacements.size(), " doodads, ", + terrain.wmoPlacements.size(), " WMOs)"); return true; } catch (const std::exception& e) { LOG_ERROR("Failed to parse WOT: ", e.what()); diff --git a/tools/editor/wowee_terrain.cpp b/tools/editor/wowee_terrain.cpp index e27aa536..fcd344fd 100644 --- a/tools/editor/wowee_terrain.cpp +++ b/tools/editor/wowee_terrain.cpp @@ -1,4 +1,5 @@ #include "wowee_terrain.hpp" +#include "pipeline/wowee_terrain_loader.hpp" #include "core/logger.hpp" #include "stb_image_write.h" #include @@ -86,8 +87,38 @@ bool WoweeTerrain::exportOpen(const pipeline::ADTTerrain& terrain, } } j["water"] = waterArr; - j["doodadCount"] = terrain.doodadPlacements.size(); - j["wmoCount"] = terrain.wmoPlacements.size(); + + // Doodad placements (M2 models on terrain) + nlohmann::json doodadNames = nlohmann::json::array(); + for (const auto& n : terrain.doodadNames) doodadNames.push_back(n); + j["doodadNames"] = doodadNames; + + nlohmann::json doodads = nlohmann::json::array(); + for (const auto& dp : terrain.doodadPlacements) { + doodads.push_back({ + {"nameId", dp.nameId}, {"uniqueId", dp.uniqueId}, + {"pos", {dp.position[0], dp.position[1], dp.position[2]}}, + {"rot", {dp.rotation[0], dp.rotation[1], dp.rotation[2]}}, + {"scale", dp.scale}, {"flags", dp.flags} + }); + } + j["doodads"] = doodads; + + // WMO placements (buildings on terrain) + nlohmann::json wmoNames = nlohmann::json::array(); + for (const auto& n : terrain.wmoNames) wmoNames.push_back(n); + j["wmoNames"] = wmoNames; + + nlohmann::json wmos = nlohmann::json::array(); + for (const auto& wp : terrain.wmoPlacements) { + wmos.push_back({ + {"nameId", wp.nameId}, {"uniqueId", wp.uniqueId}, + {"pos", {wp.position[0], wp.position[1], wp.position[2]}}, + {"rot", {wp.rotation[0], wp.rotation[1], wp.rotation[2]}}, + {"flags", wp.flags}, {"doodadSet", wp.doodadSet} + }); + } + j["wmos"] = wmos; std::ofstream f(jsonPath); if (!f) return false; @@ -217,112 +248,7 @@ int WoweeTerrain::exportAlphaMaps(const pipeline::ADTTerrain& terrain, } bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain) { - // Load binary heightmap (.whm) - std::string hmPath = basePath + ".whm"; - std::ifstream f(hmPath, std::ios::binary); - if (!f) return false; - - uint32_t magic, chunks, verts; - f.read(reinterpret_cast(&magic), 4); - if (magic != 0x314D4857) return false; - f.read(reinterpret_cast(&chunks), 4); - f.read(reinterpret_cast(&verts), 4); - if (chunks != 256 || verts != 145) return false; - - terrain.loaded = true; - terrain.version = 18; - for (int ci = 0; ci < 256; ci++) { - auto& chunk = terrain.chunks[ci]; - chunk.heightMap.loaded = true; - chunk.indexX = ci % 16; - chunk.indexY = ci / 16; - float base; - f.read(reinterpret_cast(&base), 4); - chunk.position[2] = base; - f.read(reinterpret_cast(chunk.heightMap.heights.data()), 145 * 4); - - uint32_t alphaSize = 0; - if (f.read(reinterpret_cast(&alphaSize), 4) && alphaSize > 0 && alphaSize <= 65536) { - chunk.alphaMap.resize(alphaSize); - f.read(reinterpret_cast(chunk.alphaMap.data()), alphaSize); - } - - for (int i = 0; i < 145; i++) { - chunk.normals[i * 3 + 0] = 0; - chunk.normals[i * 3 + 1] = 0; - chunk.normals[i * 3 + 2] = 127; - } - } - - // Load JSON metadata (.wot) - std::string wotPath = basePath + ".wot"; - std::ifstream wf(wotPath); - if (wf) { - try { - auto j = nlohmann::json::parse(wf); - - terrain.coord.x = j.value("tileX", 0); - terrain.coord.y = j.value("tileY", 0); - - float tileSize = 533.33333f; - float chunkSize = tileSize / 16.0f; - for (int cy = 0; cy < 16; cy++) { - for (int cx = 0; cx < 16; cx++) { - auto& chunk = terrain.chunks[cy * 16 + cx]; - chunk.position[0] = (32.0f - terrain.coord.x) * tileSize - cx * chunkSize; - chunk.position[1] = (32.0f - terrain.coord.y) * tileSize - cy * chunkSize; - } - } - - if (j.contains("textures") && j["textures"].is_array()) { - for (const auto& tex : j["textures"]) { - if (tex.is_string() && !tex.get().empty()) - terrain.textures.push_back(tex.get()); - } - } - - if (j.contains("chunkLayers") && j["chunkLayers"].is_array()) { - const auto& layers = j["chunkLayers"]; - for (int ci = 0; ci < std::min(256, static_cast(layers.size())); ci++) { - const auto& cl = layers[ci]; - if (cl.contains("layers") && cl["layers"].is_array()) { - for (const auto& texId : cl["layers"]) { - pipeline::TextureLayer layer{}; - layer.textureId = texId.get(); - layer.flags = terrain.chunks[ci].layers.empty() ? 0 : 0x100; - terrain.chunks[ci].layers.push_back(layer); - } - } - if (cl.contains("holes")) - terrain.chunks[ci].holes = cl["holes"].get(); - } - } - - if (j.contains("water") && j["water"].is_array()) { - for (const auto& w : j["water"]) { - if (w.is_null()) continue; - int wci = w.value("chunk", -1); - if (wci < 0 || wci >= 256) continue; - pipeline::ADTTerrain::WaterLayer wl; - wl.liquidType = w.value("type", 0u); - wl.maxHeight = w.value("height", 0.0f); - wl.minHeight = wl.maxHeight; - wl.x = 0; wl.y = 0; wl.width = 9; wl.height = 9; - wl.heights.assign(81, wl.maxHeight); - wl.mask.assign(8, 0xFF); - terrain.waterData[wci].layers.push_back(wl); - } - } - - LOG_INFO("WOT metadata loaded: tile [", terrain.coord.x, ",", terrain.coord.y, - "], ", terrain.textures.size(), " textures"); - } catch (const std::exception& e) { - LOG_WARNING("Could not parse WOT metadata: ", e.what()); - } - } - - LOG_INFO("Open terrain imported: ", basePath); - return true; + return pipeline::WoweeTerrainLoader::load(basePath, terrain); } } // namespace editor