mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat: integrate Wowee Open Terrain loader into client terrain pipeline
The wowee client can now load custom zones exported from the editor
using the novel WOT/WHM format — no Blizzard files needed.
Loading priority in TerrainManager::prepareTile():
1. Check custom_zones/{mapName}/{mapName}_{x}_{y}.wot/.whm
2. Check output/{mapName}/{mapName}_{x}_{y}.wot/.whm (editor output)
3. Fall back to World\Maps\...\*.adt (standard extracted data)
Pipeline:
- WoweeTerrainLoader in src/pipeline/ (shared between client + editor)
- Loads .whm binary heightmap (WHM1 magic, 256 chunks × 145 floats)
- Loads .wot JSON metadata (textures, layers, holes, water)
- Populates the same ADTTerrain struct the mesh generator uses
- obj0 merge only runs for ADT-loaded tiles (custom zones have no obj0)
To use: export zone from editor → files appear in output/ → client
loads them automatically on next terrain request for that map name.
This commit is contained in:
parent
94e6d5276e
commit
954894460e
4 changed files with 282 additions and 15 deletions
|
|
@ -589,6 +589,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wmo_loader.cpp
|
||||
src/pipeline/adt_loader.cpp
|
||||
src/pipeline/wdt_loader.cpp
|
||||
src/pipeline/wowee_terrain_loader.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
|
@ -1313,6 +1314,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wmo_loader.cpp
|
||||
src/pipeline/adt_loader.cpp
|
||||
src/pipeline/wdt_loader.cpp
|
||||
src/pipeline/wowee_terrain_loader.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
# Rendering core
|
||||
|
|
|
|||
28
include/pipeline/wowee_terrain_loader.hpp
Normal file
28
include/pipeline/wowee_terrain_loader.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Loader for the Wowee Open Terrain format (.wot/.whm)
|
||||
// Novel format — no Blizzard structures, fully portable
|
||||
class WoweeTerrainLoader {
|
||||
public:
|
||||
// Load terrain from .whm binary heightmap file
|
||||
static bool loadHeightmap(const std::string& whmPath, ADTTerrain& terrain);
|
||||
|
||||
// Load terrain metadata from .wot JSON file
|
||||
static bool loadMetadata(const std::string& wotPath, ADTTerrain& terrain);
|
||||
|
||||
// Full load: .wot metadata + .whm heightmap
|
||||
static bool load(const std::string& basePath, ADTTerrain& terrain);
|
||||
|
||||
// Check if a wowee open terrain exists at the given base path
|
||||
static bool exists(const std::string& basePath);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
210
src/pipeline/wowee_terrain_loader.cpp
Normal file
210
src/pipeline/wowee_terrain_loader.cpp
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
static constexpr uint32_t WHM_MAGIC = 0x314D4857; // "WHM1"
|
||||
|
||||
bool WoweeTerrainLoader::exists(const std::string& basePath) {
|
||||
return std::filesystem::exists(basePath + ".whm") &&
|
||||
std::filesystem::exists(basePath + ".wot");
|
||||
}
|
||||
|
||||
bool WoweeTerrainLoader::loadHeightmap(const std::string& whmPath, ADTTerrain& terrain) {
|
||||
std::ifstream f(whmPath, std::ios::binary);
|
||||
if (!f) return false;
|
||||
|
||||
uint32_t magic, chunks, verts;
|
||||
f.read(reinterpret_cast<char*>(&magic), 4);
|
||||
if (magic != WHM_MAGIC) {
|
||||
LOG_ERROR("Not a WHM file: ", whmPath);
|
||||
return false;
|
||||
}
|
||||
f.read(reinterpret_cast<char*>(&chunks), 4);
|
||||
f.read(reinterpret_cast<char*>(&verts), 4);
|
||||
|
||||
if (chunks != 256 || verts != 145) {
|
||||
LOG_ERROR("WHM unexpected dimensions: ", chunks, " chunks, ", verts, " verts");
|
||||
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;
|
||||
chunk.flags = 0;
|
||||
chunk.holes = 0;
|
||||
|
||||
float base;
|
||||
f.read(reinterpret_cast<char*>(&base), 4);
|
||||
chunk.position[2] = base;
|
||||
|
||||
f.read(reinterpret_cast<char*>(chunk.heightMap.heights.data()), 145 * 4);
|
||||
|
||||
// Default normals (up)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WHM loaded: ", whmPath, " (256 chunks, 145 verts each)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WoweeTerrainLoader::loadMetadata(const std::string& wotPath, ADTTerrain& terrain) {
|
||||
std::ifstream f(wotPath);
|
||||
if (!f) return false;
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
// Parse tile coordinates
|
||||
auto findInt = [&](const std::string& key) -> int {
|
||||
auto pos = content.find("\"" + key + "\"");
|
||||
if (pos == std::string::npos) return 0;
|
||||
pos = content.find(':', pos);
|
||||
return std::stoi(content.substr(pos + 1));
|
||||
};
|
||||
|
||||
terrain.coord.x = findInt("tileX");
|
||||
terrain.coord.y = findInt("tileY");
|
||||
|
||||
// Compute chunk world positions from tile coordinates
|
||||
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;
|
||||
// position[2] already set by heightmap loader
|
||||
}
|
||||
}
|
||||
|
||||
// Parse textures array
|
||||
auto texStart = content.find("\"textures\"");
|
||||
if (texStart != std::string::npos) {
|
||||
size_t pos = texStart;
|
||||
while ((pos = content.find('"', pos + 1)) != std::string::npos) {
|
||||
if (content[pos - 1] == '[' || content[pos - 1] == ',') {
|
||||
auto end = content.find('"', pos + 1);
|
||||
if (end == std::string::npos) break;
|
||||
std::string tex = content.substr(pos + 1, end - pos - 1);
|
||||
if (tex != "textures" && !tex.empty())
|
||||
terrain.textures.push_back(tex);
|
||||
pos = end;
|
||||
}
|
||||
auto closeBracket = content.find(']', texStart);
|
||||
if (pos > closeBracket) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse chunk layers
|
||||
auto layersStart = content.find("\"chunkLayers\"");
|
||||
if (layersStart != std::string::npos) {
|
||||
size_t pos = layersStart;
|
||||
int ci = 0;
|
||||
while (ci < 256 && (pos = content.find('{', pos + 1)) != std::string::npos) {
|
||||
auto endObj = content.find('}', pos);
|
||||
if (endObj == std::string::npos) break;
|
||||
auto layersClose = content.find(']', content.find("\"chunkLayers\""));
|
||||
if (pos > layersClose) break;
|
||||
|
||||
std::string block = content.substr(pos, endObj - pos + 1);
|
||||
|
||||
// Parse layers array
|
||||
auto lStart = block.find("\"layers\":[");
|
||||
if (lStart != std::string::npos) {
|
||||
lStart += 10;
|
||||
auto lEnd = block.find(']', lStart);
|
||||
std::string layerStr = block.substr(lStart, lEnd - lStart);
|
||||
// Parse comma-separated integers
|
||||
size_t lp = 0;
|
||||
while (lp < layerStr.size()) {
|
||||
while (lp < layerStr.size() && !std::isdigit(layerStr[lp])) lp++;
|
||||
if (lp >= layerStr.size()) break;
|
||||
uint32_t texId = std::stoi(layerStr.substr(lp));
|
||||
TextureLayer layer{};
|
||||
layer.textureId = texId;
|
||||
layer.flags = (terrain.chunks[ci].layers.empty()) ? 0 : 0x100;
|
||||
terrain.chunks[ci].layers.push_back(layer);
|
||||
while (lp < layerStr.size() && std::isdigit(layerStr[lp])) lp++;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse holes
|
||||
auto holesPos = block.find("\"holes\":");
|
||||
if (holesPos != std::string::npos)
|
||||
terrain.chunks[ci].holes = static_cast<uint16_t>(std::stoi(block.substr(holesPos + 8)));
|
||||
|
||||
ci++;
|
||||
pos = endObj;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse water data
|
||||
auto waterStart = content.find("\"water\"");
|
||||
if (waterStart != std::string::npos) {
|
||||
size_t pos = waterStart;
|
||||
int ci = 0;
|
||||
while (ci < 256) {
|
||||
auto nextObj = content.find('{', pos + 1);
|
||||
auto nextNull = content.find("null", pos + 1);
|
||||
auto waterClose = content.find(']', waterStart);
|
||||
|
||||
if (nextObj != std::string::npos && nextObj < waterClose &&
|
||||
(nextNull == std::string::npos || nextObj < nextNull)) {
|
||||
auto endObj = content.find('}', nextObj);
|
||||
std::string block = content.substr(nextObj, endObj - nextObj + 1);
|
||||
|
||||
auto chunkPos = block.find("\"chunk\":");
|
||||
auto typePos = block.find("\"type\":");
|
||||
auto heightPos = block.find("\"height\":");
|
||||
|
||||
if (chunkPos != std::string::npos) {
|
||||
int wci = std::stoi(block.substr(chunkPos + 8));
|
||||
if (wci >= 0 && wci < 256) {
|
||||
ADTTerrain::WaterLayer wl;
|
||||
wl.liquidType = (typePos != std::string::npos) ?
|
||||
static_cast<uint16_t>(std::stoi(block.substr(typePos + 7))) : 0;
|
||||
wl.maxHeight = (heightPos != std::string::npos) ?
|
||||
std::stof(block.substr(heightPos + 9)) : 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
pos = endObj;
|
||||
} else {
|
||||
pos = (nextNull != std::string::npos && nextNull < waterClose) ? nextNull + 4 : waterClose;
|
||||
}
|
||||
ci++;
|
||||
if (pos >= waterClose) break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOT loaded: ", wotPath, " (tile [", terrain.coord.x, ",", terrain.coord.y,
|
||||
"], ", terrain.textures.size(), " textures)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WoweeTerrainLoader::load(const std::string& basePath, ADTTerrain& terrain) {
|
||||
if (!loadHeightmap(basePath + ".whm", terrain)) return false;
|
||||
if (!loadMetadata(basePath + ".wot", terrain)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
#include "rendering/camera.hpp"
|
||||
#include "audio/ambient_sound_manager.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "core/memory_monitor.hpp"
|
||||
#include "core/profiler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
|
|
@ -310,28 +311,53 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
// Early-exit check — worker should bail fast during shutdown
|
||||
if (!workerRunning.load()) return nullptr;
|
||||
|
||||
// Load ADT file
|
||||
std::string adtPath = getADTPath(coord);
|
||||
auto adtData = assetManager->readFile(adtPath);
|
||||
// Try Wowee Open Terrain format first (custom zones)
|
||||
std::string wotBase = "custom_zones/" + mapName + "/" + mapName + "_" +
|
||||
std::to_string(coord.x) + "_" + std::to_string(coord.y);
|
||||
auto terrainPtr = std::make_unique<pipeline::ADTTerrain>();
|
||||
bool loadedFromWot = false;
|
||||
|
||||
if (adtData.empty()) {
|
||||
logMissingAdtOnce(adtPath);
|
||||
return nullptr;
|
||||
if (pipeline::WoweeTerrainLoader::exists(wotBase)) {
|
||||
if (pipeline::WoweeTerrainLoader::load(wotBase, *terrainPtr)) {
|
||||
loadedFromWot = true;
|
||||
LOG_INFO("Loaded custom zone terrain: ", wotBase);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ADT — allocate on heap to avoid stack overflow on macOS
|
||||
// (ADTTerrain contains std::array<MapChunk,256> ≈ 280 KB; macOS worker
|
||||
// threads default to 512 KB stack, so two on-stack copies would overflow)
|
||||
auto terrainPtr = std::make_unique<pipeline::ADTTerrain>(pipeline::ADTLoader::load(adtData));
|
||||
if (!terrainPtr->isLoaded()) {
|
||||
LOG_ERROR("Failed to parse ADT terrain: ", adtPath);
|
||||
return nullptr;
|
||||
// Also check output directory (editor exports here)
|
||||
if (!loadedFromWot) {
|
||||
std::string outputBase = "output/" + mapName + "/" + mapName + "_" +
|
||||
std::to_string(coord.x) + "_" + std::to_string(coord.y);
|
||||
if (pipeline::WoweeTerrainLoader::exists(outputBase)) {
|
||||
if (pipeline::WoweeTerrainLoader::load(outputBase, *terrainPtr)) {
|
||||
loadedFromWot = true;
|
||||
LOG_INFO("Loaded editor output terrain: ", outputBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to ADT format
|
||||
if (!loadedFromWot) {
|
||||
std::string adtPath = getADTPath(coord);
|
||||
auto adtData = assetManager->readFile(adtPath);
|
||||
|
||||
if (adtData.empty()) {
|
||||
logMissingAdtOnce(adtPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
*terrainPtr = pipeline::ADTLoader::load(adtData);
|
||||
if (!terrainPtr->isLoaded()) {
|
||||
LOG_ERROR("Failed to parse ADT terrain: ", adtPath);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!workerRunning.load()) return nullptr;
|
||||
|
||||
// WotLK split ADTs can store placements in *_obj0.adt.
|
||||
// Merge object chunks so doodads/WMOs (including ground clutter) are available.
|
||||
// Only needed for ADT-loaded tiles, not for WOT custom zones.
|
||||
if (!loadedFromWot) {
|
||||
std::string objPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||
std::to_string(coord.x) + "_" + std::to_string(coord.y) + "_obj0.adt";
|
||||
auto objData = assetManager->readFile(objPath);
|
||||
|
|
@ -386,6 +412,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} // end if (!loadedFromWot) obj0 merge
|
||||
|
||||
// Set tile coordinates so mesh knows where to position this tile in world
|
||||
terrainPtr->coord.x = x;
|
||||
|
|
@ -394,7 +421,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
// Generate mesh
|
||||
pipeline::TerrainMesh mesh = pipeline::TerrainMeshGenerator::generate(*terrainPtr);
|
||||
if (mesh.validChunkCount == 0) {
|
||||
LOG_ERROR("Failed to generate terrain mesh: ", adtPath);
|
||||
LOG_ERROR("Failed to generate terrain mesh for tile [", x, ",", y, "]");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue