diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cd2ed8d..18178f57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -591,6 +591,7 @@ set(WOWEE_SOURCES src/pipeline/wdt_loader.cpp src/pipeline/wowee_terrain_loader.cpp src/pipeline/wowee_model.cpp + src/pipeline/wowee_building.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1320,6 +1321,7 @@ add_executable(wowee_editor src/pipeline/wdt_loader.cpp src/pipeline/wowee_terrain_loader.cpp src/pipeline/wowee_model.cpp + src/pipeline/wowee_building.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_building.hpp b/include/pipeline/wowee_building.hpp new file mode 100644 index 00000000..d124316d --- /dev/null +++ b/include/pipeline/wowee_building.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Building format (.wob) — novel WMO replacement +// Buildings with multiple groups, portals, and doodad sets +struct WoweeBuilding { + struct Vertex { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 texCoord; + glm::vec4 color; // vertex color/lighting + }; + + struct Group { + std::string name; + std::vector vertices; + std::vector indices; + std::vector texturePaths; + glm::vec3 boundMin{0}, boundMax{0}; + bool isOutdoor = false; + }; + + struct Portal { + int groupA = -1, groupB = -1; + std::vector vertices; // portal polygon + }; + + struct DoodadPlacement { + std::string modelPath; // .wom path + glm::vec3 position; + glm::vec3 rotation; + float scale = 1.0f; + }; + + std::string name; + std::vector groups; + std::vector portals; + std::vector doodads; + float boundRadius = 1.0f; + + bool isValid() const { return !groups.empty(); } +}; + +class WoweeBuildingLoader { +public: + static WoweeBuilding load(const std::string& basePath); + static bool save(const WoweeBuilding& building, const std::string& basePath); + static bool exists(const std::string& basePath); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_building.cpp b/src/pipeline/wowee_building.cpp new file mode 100644 index 00000000..23538b46 --- /dev/null +++ b/src/pipeline/wowee_building.cpp @@ -0,0 +1,165 @@ +#include "pipeline/wowee_building.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace pipeline { + +static constexpr uint32_t WOB_MAGIC = 0x31424F57; // "WOB1" + +bool WoweeBuildingLoader::exists(const std::string& basePath) { + return std::filesystem::exists(basePath + ".wob"); +} + +WoweeBuilding WoweeBuildingLoader::load(const std::string& basePath) { + WoweeBuilding bld; + std::ifstream f(basePath + ".wob", std::ios::binary); + if (!f) return bld; + + uint32_t magic; + f.read(reinterpret_cast(&magic), 4); + if (magic != WOB_MAGIC) return bld; + + uint32_t groupCount, portalCount, doodadCount; + f.read(reinterpret_cast(&groupCount), 4); + f.read(reinterpret_cast(&portalCount), 4); + f.read(reinterpret_cast(&doodadCount), 4); + f.read(reinterpret_cast(&bld.boundRadius), 4); + + uint16_t nameLen; + f.read(reinterpret_cast(&nameLen), 2); + bld.name.resize(nameLen); + f.read(bld.name.data(), nameLen); + + for (uint32_t gi = 0; gi < groupCount; gi++) { + WoweeBuilding::Group grp; + uint16_t gnLen; + f.read(reinterpret_cast(&gnLen), 2); + grp.name.resize(gnLen); + f.read(grp.name.data(), gnLen); + + uint32_t vc, ic, tc; + f.read(reinterpret_cast(&vc), 4); + f.read(reinterpret_cast(&ic), 4); + f.read(reinterpret_cast(&tc), 4); + uint8_t outdoor; + f.read(reinterpret_cast(&outdoor), 1); + grp.isOutdoor = (outdoor != 0); + f.read(reinterpret_cast(&grp.boundMin), 12); + f.read(reinterpret_cast(&grp.boundMax), 12); + + grp.vertices.resize(vc); + f.read(reinterpret_cast(grp.vertices.data()), vc * sizeof(WoweeBuilding::Vertex)); + grp.indices.resize(ic); + f.read(reinterpret_cast(grp.indices.data()), ic * 4); + + for (uint32_t ti = 0; ti < tc; ti++) { + uint16_t tl; + f.read(reinterpret_cast(&tl), 2); + std::string tp(tl, '\0'); + f.read(tp.data(), tl); + grp.texturePaths.push_back(tp); + } + bld.groups.push_back(std::move(grp)); + } + + for (uint32_t pi = 0; pi < portalCount; pi++) { + WoweeBuilding::Portal portal; + f.read(reinterpret_cast(&portal.groupA), 4); + f.read(reinterpret_cast(&portal.groupB), 4); + uint32_t pvCount; + f.read(reinterpret_cast(&pvCount), 4); + portal.vertices.resize(pvCount); + f.read(reinterpret_cast(portal.vertices.data()), pvCount * 12); + bld.portals.push_back(portal); + } + + for (uint32_t di = 0; di < doodadCount; di++) { + WoweeBuilding::DoodadPlacement dp; + uint16_t pl; + f.read(reinterpret_cast(&pl), 2); + dp.modelPath.resize(pl); + f.read(dp.modelPath.data(), pl); + f.read(reinterpret_cast(&dp.position), 12); + f.read(reinterpret_cast(&dp.rotation), 12); + f.read(reinterpret_cast(&dp.scale), 4); + bld.doodads.push_back(dp); + } + + LOG_INFO("WOB loaded: ", basePath, " (", groupCount, " groups, ", + portalCount, " portals, ", doodadCount, " doodads)"); + return bld; +} + +bool WoweeBuildingLoader::save(const WoweeBuilding& bld, const std::string& basePath) { + namespace fs = std::filesystem; + fs::create_directories(fs::path(basePath).parent_path()); + + std::ofstream f(basePath + ".wob", std::ios::binary); + if (!f) return false; + + f.write(reinterpret_cast(&WOB_MAGIC), 4); + uint32_t gc = static_cast(bld.groups.size()); + uint32_t pc = static_cast(bld.portals.size()); + uint32_t dc = static_cast(bld.doodads.size()); + f.write(reinterpret_cast(&gc), 4); + f.write(reinterpret_cast(&pc), 4); + f.write(reinterpret_cast(&dc), 4); + f.write(reinterpret_cast(&bld.boundRadius), 4); + + uint16_t nl = static_cast(bld.name.size()); + f.write(reinterpret_cast(&nl), 2); + f.write(bld.name.data(), nl); + + for (const auto& grp : bld.groups) { + uint16_t gnl = static_cast(grp.name.size()); + f.write(reinterpret_cast(&gnl), 2); + f.write(grp.name.data(), gnl); + + uint32_t vc = static_cast(grp.vertices.size()); + uint32_t ic = static_cast(grp.indices.size()); + uint32_t tc = static_cast(grp.texturePaths.size()); + f.write(reinterpret_cast(&vc), 4); + f.write(reinterpret_cast(&ic), 4); + f.write(reinterpret_cast(&tc), 4); + uint8_t outdoor = grp.isOutdoor ? 1 : 0; + f.write(reinterpret_cast(&outdoor), 1); + f.write(reinterpret_cast(&grp.boundMin), 12); + f.write(reinterpret_cast(&grp.boundMax), 12); + + f.write(reinterpret_cast(grp.vertices.data()), + vc * sizeof(WoweeBuilding::Vertex)); + f.write(reinterpret_cast(grp.indices.data()), ic * 4); + + for (const auto& tp : grp.texturePaths) { + uint16_t tl = static_cast(tp.size()); + f.write(reinterpret_cast(&tl), 2); + f.write(tp.data(), tl); + } + } + + for (const auto& portal : bld.portals) { + f.write(reinterpret_cast(&portal.groupA), 4); + f.write(reinterpret_cast(&portal.groupB), 4); + uint32_t pvCount = static_cast(portal.vertices.size()); + f.write(reinterpret_cast(&pvCount), 4); + f.write(reinterpret_cast(portal.vertices.data()), pvCount * 12); + } + + for (const auto& dp : bld.doodads) { + uint16_t pl = static_cast(dp.modelPath.size()); + f.write(reinterpret_cast(&pl), 2); + f.write(dp.modelPath.data(), pl); + f.write(reinterpret_cast(&dp.position), 12); + f.write(reinterpret_cast(&dp.rotation), 12); + f.write(reinterpret_cast(&dp.scale), 4); + } + + LOG_INFO("WOB saved: ", basePath, ".wob (", gc, " groups)"); + return true; +} + +} // namespace pipeline +} // namespace wowee