feat: WOT doodad/WMO placements, WOB materials, deduplicate loader

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
This commit is contained in:
Kelsi 2026-05-05 14:44:46 -07:00
parent d00ddd1c73
commit ca15da5e9b
4 changed files with 100 additions and 110 deletions

View file

@ -1,6 +1,7 @@
#include "pipeline/wowee_building.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/quaternion.hpp>
#include <fstream>
#include <filesystem>
#include <cstring>
@ -226,13 +227,19 @@ WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::strin
wobGroup.indices.push_back(static_cast<uint32_t>(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);
}

View file

@ -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<std::string>());
}
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<std::string>());
}
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());