diff --git a/src/pipeline/wowee_terrain_loader.cpp b/src/pipeline/wowee_terrain_loader.cpp index 125fcaf2..2bcd1da9 100644 --- a/src/pipeline/wowee_terrain_loader.cpp +++ b/src/pipeline/wowee_terrain_loader.cpp @@ -1,5 +1,6 @@ #include "pipeline/wowee_terrain_loader.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -49,6 +50,13 @@ bool WoweeTerrainLoader::loadHeightmap(const std::string& whmPath, ADTTerrain& t f.read(reinterpret_cast(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(&alphaSize), 4) && alphaSize > 0 && alphaSize <= 65536) { + chunk.alphaMap.resize(alphaSize); + f.read(reinterpret_cast(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(f)), - std::istreambuf_iterator()); + 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().empty()) + terrain.textures.push_back(tex.get()); } - - // Parse holes - auto holesPos = block.find("\"holes\":"); - if (holesPos != std::string::npos) - terrain.chunks[ci].holes = static_cast(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(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(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(); + 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(); } - 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) { diff --git a/tools/editor/wowee_terrain.cpp b/tools/editor/wowee_terrain.cpp index 8548d31e..2b51e3f9 100644 --- a/tools/editor/wowee_terrain.cpp +++ b/tools/editor/wowee_terrain.cpp @@ -1,6 +1,7 @@ #include "wowee_terrain.hpp" #include "core/logger.hpp" #include "stb_image_write.h" +#include #include #include #include @@ -25,74 +26,72 @@ bool WoweeTerrain::exportOpen(const pipeline::ADTTerrain& terrain, f.write(reinterpret_cast(&magic), 4); f.write(reinterpret_cast(&chunks), 4); f.write(reinterpret_cast(&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(&base), 4); f.write(reinterpret_cast(chunk.heightMap.heights.data()), 145 * 4); + uint32_t alphaSize = static_cast(chunk.alphaMap.size()); + f.write(reinterpret_cast(&alphaSize), 4); + if (alphaSize > 0) { + f.write(reinterpret_cast(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)");