mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat: WHM alpha maps + nlohmann/json for WOT format
- WHM binary now includes per-chunk alpha map data (alphaSize + data) so custom zones render with proper texture blending in the client - WOT exporter rewritten with nlohmann/json (was manual string concat) - WOT loader rewritten with nlohmann/json (was naive substring parsing) - Backward compatible: old WHM files without alpha data still load fine
This commit is contained in:
parent
6b3cdd325a
commit
815787933b
2 changed files with 106 additions and 165 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
|
@ -49,6 +50,13 @@ bool WoweeTerrainLoader::loadHeightmap(const std::string& whmPath, ADTTerrain& t
|
|||
|
||||
f.read(reinterpret_cast<char*>(chunk.heightMap.heights.data()), 145 * 4);
|
||||
|
||||
// Read alpha map data (may not be present in older WHM files)
|
||||
uint32_t alphaSize = 0;
|
||||
if (f.read(reinterpret_cast<char*>(&alphaSize), 4) && alphaSize > 0 && alphaSize <= 65536) {
|
||||
chunk.alphaMap.resize(alphaSize);
|
||||
f.read(reinterpret_cast<char*>(chunk.alphaMap.data()), alphaSize);
|
||||
}
|
||||
|
||||
// Default normals (up)
|
||||
for (int i = 0; i < 145; i++) {
|
||||
chunk.normals[i * 3 + 0] = 0;
|
||||
|
|
@ -65,139 +73,73 @@ bool WoweeTerrainLoader::loadMetadata(const std::string& wotPath, ADTTerrain& te
|
|||
std::ifstream f(wotPath);
|
||||
if (!f) return false;
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
try {
|
||||
auto j = nlohmann::json::parse(f);
|
||||
|
||||
// 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 = j.value("tileX", 0);
|
||||
terrain.coord.y = j.value("tileY", 0);
|
||||
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
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 textures
|
||||
if (j.contains("textures") && j["textures"].is_array()) {
|
||||
for (const auto& tex : j["textures"]) {
|
||||
if (tex.is_string() && !tex.get<std::string>().empty())
|
||||
terrain.textures.push_back(tex.get<std::string>());
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Parse chunk layers
|
||||
if (j.contains("chunkLayers") && j["chunkLayers"].is_array()) {
|
||||
const auto& layers = j["chunkLayers"];
|
||||
for (int ci = 0; ci < std::min(256, static_cast<int>(layers.size())); ci++) {
|
||||
const auto& cl = layers[ci];
|
||||
if (cl.contains("layers") && cl["layers"].is_array()) {
|
||||
for (const auto& texId : cl["layers"]) {
|
||||
TextureLayer layer{};
|
||||
layer.textureId = texId.get<uint32_t>();
|
||||
layer.flags = terrain.chunks[ci].layers.empty() ? 0 : 0x100;
|
||||
terrain.chunks[ci].layers.push_back(layer);
|
||||
}
|
||||
}
|
||||
pos = endObj;
|
||||
} else {
|
||||
pos = (nextNull != std::string::npos && nextNull < waterClose) ? nextNull + 4 : waterClose;
|
||||
if (cl.contains("holes"))
|
||||
terrain.chunks[ci].holes = cl["holes"].get<uint16_t>();
|
||||
}
|
||||
ci++;
|
||||
if (pos >= waterClose) break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("WOT loaded: ", wotPath, " (tile [", terrain.coord.x, ",", terrain.coord.y,
|
||||
"], ", terrain.textures.size(), " textures)");
|
||||
return true;
|
||||
// Parse water data
|
||||
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;
|
||||
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 loaded: ", wotPath, " (tile [", terrain.coord.x, ",", terrain.coord.y,
|
||||
"], ", terrain.textures.size(), " textures)");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Failed to parse WOT: ", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WoweeTerrainLoader::load(const std::string& basePath, ADTTerrain& terrain) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "wowee_terrain.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "stb_image_write.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
|
@ -25,74 +26,72 @@ bool WoweeTerrain::exportOpen(const pipeline::ADTTerrain& terrain,
|
|||
f.write(reinterpret_cast<const char*>(&magic), 4);
|
||||
f.write(reinterpret_cast<const char*>(&chunks), 4);
|
||||
f.write(reinterpret_cast<const char*>(&verts), 4);
|
||||
// Per-chunk: baseHeight(4) + heights[145](580)
|
||||
// Per-chunk: baseHeight(4) + heights[145](580) + alphaSize(4) + alphaData(N)
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
const auto& chunk = terrain.chunks[ci];
|
||||
float base = chunk.position[2];
|
||||
f.write(reinterpret_cast<const char*>(&base), 4);
|
||||
f.write(reinterpret_cast<const char*>(chunk.heightMap.heights.data()), 145 * 4);
|
||||
uint32_t alphaSize = static_cast<uint32_t>(chunk.alphaMap.size());
|
||||
f.write(reinterpret_cast<const char*>(&alphaSize), 4);
|
||||
if (alphaSize > 0) {
|
||||
f.write(reinterpret_cast<const char*>(chunk.alphaMap.data()), alphaSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export JSON metadata (.wot = Wowee Open Terrain)
|
||||
std::string jsonPath = basePath + ".wot";
|
||||
{
|
||||
std::ofstream f(jsonPath);
|
||||
if (!f) return false;
|
||||
f << "{\n";
|
||||
f << " \"format\": \"wot-1.0\",\n";
|
||||
f << " \"editor\": \"wowee-editor-0.8.0\",\n";
|
||||
f << " \"tileX\": " << tileX << ",\n";
|
||||
f << " \"tileY\": " << tileY << ",\n";
|
||||
f << " \"chunkGrid\": [16, 16],\n";
|
||||
f << " \"vertsPerChunk\": 145,\n";
|
||||
f << " \"heightmapFile\": \"" << fs::path(hmPath).filename().string() << "\",\n";
|
||||
f << " \"textures\": [\n";
|
||||
for (size_t i = 0; i < terrain.textures.size(); i++) {
|
||||
f << " \"" << terrain.textures[i] << "\"";
|
||||
if (i + 1 < terrain.textures.size()) f << ",";
|
||||
f << "\n";
|
||||
}
|
||||
f << " ],\n";
|
||||
f << " \"tileSize\": 533.33333,\n";
|
||||
f << " \"chunkSize\": 33.33333,\n";
|
||||
f << " \"chunkLayers\": [\n";
|
||||
nlohmann::json j;
|
||||
j["format"] = "wot-1.0";
|
||||
j["editor"] = "wowee-editor-1.0.0";
|
||||
j["tileX"] = tileX;
|
||||
j["tileY"] = tileY;
|
||||
j["chunkGrid"] = {16, 16};
|
||||
j["vertsPerChunk"] = 145;
|
||||
j["heightmapFile"] = fs::path(hmPath).filename().string();
|
||||
j["tileSize"] = 533.33333f;
|
||||
j["chunkSize"] = 33.33333f;
|
||||
|
||||
nlohmann::json texArr = nlohmann::json::array();
|
||||
for (const auto& tex : terrain.textures) texArr.push_back(tex);
|
||||
j["textures"] = texArr;
|
||||
|
||||
nlohmann::json chunkArr = nlohmann::json::array();
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
const auto& chunk = terrain.chunks[ci];
|
||||
f << " {\"layers\": [";
|
||||
for (size_t li = 0; li < chunk.layers.size(); li++) {
|
||||
f << chunk.layers[li].textureId;
|
||||
if (li + 1 < chunk.layers.size()) f << ",";
|
||||
}
|
||||
f << "], \"holes\": " << chunk.holes;
|
||||
// Include alpha map presence flag
|
||||
nlohmann::json cl;
|
||||
nlohmann::json layerIds = nlohmann::json::array();
|
||||
for (const auto& layer : chunk.layers) layerIds.push_back(layer.textureId);
|
||||
cl["layers"] = layerIds;
|
||||
cl["holes"] = chunk.holes;
|
||||
bool hasAlpha = false;
|
||||
for (size_t li = 1; li < chunk.layers.size(); li++)
|
||||
if (chunk.layers[li].useAlpha()) { hasAlpha = true; break; }
|
||||
f << ", \"hasAlpha\": " << (hasAlpha ? "true" : "false");
|
||||
f << "}";
|
||||
if (ci < 255) f << ",";
|
||||
f << "\n";
|
||||
cl["hasAlpha"] = hasAlpha;
|
||||
chunkArr.push_back(cl);
|
||||
}
|
||||
f << " ],\n";
|
||||
// Water data
|
||||
f << " \"water\": [\n";
|
||||
j["chunkLayers"] = chunkArr;
|
||||
|
||||
nlohmann::json waterArr = nlohmann::json::array();
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
const auto& water = terrain.waterData[ci];
|
||||
if (water.hasWater()) {
|
||||
f << " {\"chunk\": " << ci
|
||||
<< ", \"type\": " << water.layers[0].liquidType
|
||||
<< ", \"height\": " << water.layers[0].maxHeight << "}";
|
||||
waterArr.push_back({{"chunk", ci},
|
||||
{"type", water.layers[0].liquidType},
|
||||
{"height", water.layers[0].maxHeight}});
|
||||
} else {
|
||||
f << " null";
|
||||
waterArr.push_back(nullptr);
|
||||
}
|
||||
if (ci < 255) f << ",";
|
||||
f << "\n";
|
||||
}
|
||||
f << " ],\n";
|
||||
f << " \"doodadCount\": " << terrain.doodadPlacements.size() << ",\n";
|
||||
f << " \"wmoCount\": " << terrain.wmoPlacements.size() << "\n";
|
||||
f << "}\n";
|
||||
j["water"] = waterArr;
|
||||
j["doodadCount"] = terrain.doodadPlacements.size();
|
||||
j["wmoCount"] = terrain.wmoPlacements.size();
|
||||
|
||||
std::ofstream f(jsonPath);
|
||||
if (!f) return false;
|
||||
f << j.dump(2) << "\n";
|
||||
}
|
||||
|
||||
LOG_INFO("Open terrain exported: ", basePath, " (.wot + .whm)");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue