diff --git a/CMakeLists.txt b/CMakeLists.txt index ee871884..70f3d29c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1180,8 +1180,10 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR) src/pipeline/blp_loader.cpp src/pipeline/m2_loader.cpp src/pipeline/wmo_loader.cpp + src/pipeline/adt_loader.cpp src/pipeline/wowee_model.cpp src/pipeline/wowee_building.cpp + src/pipeline/wowee_collision.cpp src/core/logger.cpp ) target_include_directories(asset_extract PRIVATE diff --git a/tools/asset_extract/extractor.cpp b/tools/asset_extract/extractor.cpp index bc2154d8..3e278a42 100644 --- a/tools/asset_extract/extractor.cpp +++ b/tools/asset_extract/extractor.cpp @@ -979,11 +979,12 @@ bool Extractor::run(const Options& opts) { // Open-format emission: walk the extracted tree and write // wowee-format side-files (PNG / JSON DBC) next to each .blp/.dbc. // Originals are left untouched so private servers continue to work. - if (opts.emitPng || opts.emitJsonDbc || opts.emitWom || opts.emitWob) { + if (opts.emitPng || opts.emitJsonDbc || opts.emitWom || opts.emitWob || + opts.emitTerrain) { std::cout << "Emitting wowee open-format side-files...\n"; OpenFormatStats ofs; emitOpenFormats(effectiveOutputDir, opts.emitPng, opts.emitJsonDbc, - opts.emitWom, opts.emitWob, ofs); + opts.emitWom, opts.emitWob, opts.emitTerrain, ofs); if (opts.emitPng) { std::cout << " PNG (BLP→PNG) : " << ofs.pngOk << " ok"; if (ofs.pngFail) std::cout << ", " << ofs.pngFail << " failed"; @@ -1004,6 +1005,11 @@ bool Extractor::run(const Options& opts) { if (ofs.wobFail) std::cout << ", " << ofs.wobFail << " failed"; std::cout << "\n"; } + if (opts.emitTerrain) { + std::cout << " WHM/WOT/WOC (ADT) : " << ofs.whmOk << " ok"; + if (ofs.whmFail) std::cout << ", " << ofs.whmFail << " failed"; + std::cout << "\n"; + } } // Cache WoW.exe for Warden MEM_CHECK responses diff --git a/tools/asset_extract/extractor.hpp b/tools/asset_extract/extractor.hpp index 2cbfea00..9c2fe9c0 100644 --- a/tools/asset_extract/extractor.hpp +++ b/tools/asset_extract/extractor.hpp @@ -36,6 +36,7 @@ public: bool emitJsonDbc = false; // DBC → JSON side-files bool emitWom = false; // M2 (+skin) → WOM side-files bool emitWob = false; // WMO (+groups) → WOB side-files + bool emitTerrain = false; // ADT → WHM + WOT + WOC side-files }; struct Stats { diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index 661eebfc..8681eac9 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -28,7 +28,8 @@ static void printUsage(const char* prog) { << " --emit-json-dbc Emit foo.json next to every extracted foo.dbc\n" << " --emit-wom Emit foo.wom next to every extracted foo.m2 (+skin)\n" << " --emit-wob Emit foo.wob next to every extracted foo.wmo (+groups)\n" - << " --emit-open Shortcut: enable every open-format emitter (png+json+wom+wob)\n" + << " --emit-terrain Emit foo.whm + foo.wot + foo.woc next to every foo.adt\n" + << " --emit-open Shortcut: enable every open-format emitter (png+json+wom+wob+terrain)\n" << " --verify CRC32 verify all extracted files\n" << " --threads Number of extraction threads (default: auto)\n" << " --verbose Verbose output\n" @@ -65,12 +66,15 @@ int main(int argc, char** argv) { opts.emitWom = true; } else if (std::strcmp(argv[i], "--emit-wob") == 0) { opts.emitWob = true; + } else if (std::strcmp(argv[i], "--emit-terrain") == 0) { + opts.emitTerrain = true; } else if (std::strcmp(argv[i], "--emit-open") == 0) { // Meta-flag: turn on every available open-format emitter. opts.emitPng = true; opts.emitJsonDbc = true; opts.emitWom = true; opts.emitWob = true; + opts.emitTerrain = true; } else if (std::strcmp(argv[i], "--dbc-csv-out") == 0 && i + 1 < argc) { opts.dbcCsvOutputDir = argv[++i]; } else if (std::strcmp(argv[i], "--listfile") == 0 && i + 1 < argc) { diff --git a/tools/asset_extract/open_format_emitter.cpp b/tools/asset_extract/open_format_emitter.cpp index f8d95752..222b3432 100644 --- a/tools/asset_extract/open_format_emitter.cpp +++ b/tools/asset_extract/open_format_emitter.cpp @@ -3,8 +3,10 @@ #include "pipeline/dbc_loader.hpp" #include "pipeline/wowee_model.hpp" #include "pipeline/wowee_building.hpp" +#include "pipeline/wowee_collision.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" +#include "pipeline/adt_loader.hpp" #include @@ -115,6 +117,151 @@ bool emitWomFromM2(const std::string& m2Path, const std::string& womBase) { return pipeline::WoweeModelLoader::save(wom, womBase); } +// Inline WHM+WOT writer. Mirrors the structure of WoweeTerrain::exportOpen +// in the editor but stripped to the bytes the runtime needs (no PNG +// previews, no normal map). Keeps the asset extractor independent of +// the editor target. +static bool writeWhmWot(const pipeline::ADTTerrain& terrain, + const std::string& outBase, int tileX, int tileY) { + namespace fs = std::filesystem; + fs::create_directories(fs::path(outBase).parent_path()); + + // .whm — binary heightmap, fixed 256 chunks * 145 floats + { + std::ofstream f(outBase + ".whm", std::ios::binary); + if (!f) return false; + uint32_t magic = 0x314D4857; // "WHM1" + uint32_t chunks = 256, verts = 145; + f.write(reinterpret_cast(&magic), 4); + f.write(reinterpret_cast(&chunks), 4); + f.write(reinterpret_cast(&verts), 4); + for (int ci = 0; ci < 256; ci++) { + const auto& chunk = terrain.chunks[ci]; + float base = std::isfinite(chunk.position[2]) ? chunk.position[2] : 0.0f; + f.write(reinterpret_cast(&base), 4); + float clean[145]; + for (int v = 0; v < 145; v++) { + clean[v] = chunk.heightMap.heights[v]; + if (!std::isfinite(clean[v])) clean[v] = 0.0f; + } + f.write(reinterpret_cast(clean), 145 * 4); + uint32_t alphaSize = std::min( + static_cast(chunk.alphaMap.size()), 65536); + f.write(reinterpret_cast(&alphaSize), 4); + if (alphaSize > 0) + f.write(reinterpret_cast(chunk.alphaMap.data()), alphaSize); + } + } + + // .wot — JSON metadata (textures + chunkLayers + water + placements) + { + nlohmann::json j; + j["format"] = "wot-1.0"; + j["editor"] = "asset_extract-1.0.0"; + j["tileX"] = tileX; + j["tileY"] = tileY; + j["chunkGrid"] = {16, 16}; + j["vertsPerChunk"] = 145; + j["heightmapFile"] = fs::path(outBase + ".whm").filename().string(); + + 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]; + 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; + chunkArr.push_back(cl); + } + j["chunkLayers"] = chunkArr; + + nlohmann::json waterArr = nlohmann::json::array(); + for (int ci = 0; ci < 256; ci++) { + const auto& w = terrain.waterData[ci]; + if (w.hasWater()) { + float h = std::isfinite(w.layers[0].maxHeight) ? w.layers[0].maxHeight : 0.0f; + waterArr.push_back({{"chunk", ci}, + {"type", w.layers[0].liquidType}, + {"height", h}}); + } else { + waterArr.push_back(nullptr); + } + } + j["water"] = waterArr; + + auto san = [](float x) { return std::isfinite(x) ? x : 0.0f; }; + nlohmann::json doodadNames = nlohmann::json::array(); + for (const auto& n : terrain.doodadNames) doodadNames.push_back(n); + j["doodadNames"] = doodadNames; + nlohmann::json doodads = nlohmann::json::array(); + for (const auto& dp : terrain.doodadPlacements) { + doodads.push_back({ + {"nameId", dp.nameId}, {"uniqueId", dp.uniqueId}, + {"pos", {san(dp.position[0]), san(dp.position[1]), san(dp.position[2])}}, + {"rot", {san(dp.rotation[0]), san(dp.rotation[1]), san(dp.rotation[2])}}, + {"scale", dp.scale}, {"flags", dp.flags} + }); + } + j["doodads"] = doodads; + + nlohmann::json wmoNames = nlohmann::json::array(); + for (const auto& n : terrain.wmoNames) wmoNames.push_back(n); + j["wmoNames"] = wmoNames; + nlohmann::json wmos = nlohmann::json::array(); + for (const auto& wp : terrain.wmoPlacements) { + wmos.push_back({ + {"nameId", wp.nameId}, {"uniqueId", wp.uniqueId}, + {"pos", {san(wp.position[0]), san(wp.position[1]), san(wp.position[2])}}, + {"rot", {san(wp.rotation[0]), san(wp.rotation[1]), san(wp.rotation[2])}}, + {"flags", wp.flags}, {"doodadSet", wp.doodadSet} + }); + } + j["wmos"] = wmos; + + std::ofstream f(outBase + ".wot"); + if (!f) return false; + f << j.dump(2) << "\n"; + } + return true; +} + +bool emitTerrainFromAdt(const std::string& adtPath, const std::string& outBase) { + auto bytes = readBytes(adtPath); + if (bytes.empty()) return false; + auto terrain = pipeline::ADTLoader::load(bytes); + if (!terrain.loaded) return false; + + // Parse "__.adt" tile coords from the filename so the WOT + // can record them; fall back to (32,32) if the layout is unexpected. + int tileX = 32, tileY = 32; + { + std::string stem = fs::path(adtPath).stem().string(); + auto last = stem.rfind('_'); + auto prev = (last != std::string::npos) ? stem.rfind('_', last - 1) : std::string::npos; + if (last != std::string::npos && prev != std::string::npos) { + try { + tileX = std::stoi(stem.substr(prev + 1, last - prev - 1)); + tileY = std::stoi(stem.substr(last + 1)); + } catch (...) {} + } + } + terrain.coord.x = tileX; + terrain.coord.y = tileY; + + if (!writeWhmWot(terrain, outBase, tileX, tileY)) return false; + + // Also build a terrain-only WOC (collision mesh) so the runtime can + // do walkability queries without re-deriving from the heightmap. + auto col = pipeline::WoweeCollisionBuilder::fromTerrain(terrain); + pipeline::WoweeCollisionBuilder::save(col, outBase + ".woc"); + return true; +} + bool emitWobFromWmo(const std::string& wmoPath, const std::string& wobBase) { auto rootBytes = readBytes(wmoPath); if (rootBytes.empty()) return false; @@ -138,9 +285,10 @@ bool emitWobFromWmo(const std::string& wmoPath, const std::string& wobBase) { void emitOpenFormats(const std::string& rootDir, bool emitPng, bool emitJsonDbc, bool emitWom, bool emitWob, + bool emitTerrain, OpenFormatStats& stats) { if (!fs::exists(rootDir)) return; - if (!emitPng && !emitJsonDbc && !emitWom && !emitWob) return; + if (!emitPng && !emitJsonDbc && !emitWom && !emitWob && !emitTerrain) return; auto lower = [](std::string s) { std::transform(s.begin(), s.end(), s.begin(), @@ -181,6 +329,14 @@ void emitOpenFormats(const std::string& rootDir, if (emitWobFromWmo(entry.path().string(), base)) stats.wobOk++; else stats.wobFail++; } + } else if (emitTerrain && ext == ".adt") { + if (emitTerrainFromAdt(entry.path().string(), base)) { + stats.whmOk++; + stats.wocOk++; + } else { + stats.whmFail++; + stats.wocFail++; + } } } } diff --git a/tools/asset_extract/open_format_emitter.hpp b/tools/asset_extract/open_format_emitter.hpp index 4c87391b..6c6c96b3 100644 --- a/tools/asset_extract/open_format_emitter.hpp +++ b/tools/asset_extract/open_format_emitter.hpp @@ -20,6 +20,8 @@ struct OpenFormatStats { uint32_t jsonDbcOk = 0, jsonDbcFail = 0; uint32_t womOk = 0, womFail = 0; uint32_t wobOk = 0, wobFail = 0; + uint32_t whmOk = 0, whmFail = 0; + uint32_t wocOk = 0, wocFail = 0; }; // Convert one BLP file on disk to a PNG side-file. @@ -40,11 +42,18 @@ bool emitWomFromM2(const std::string& m2Path, const std::string& womBase); // matching _NNN.wmo group files if present. bool emitWobFromWmo(const std::string& wmoPath, const std::string& wobBase); +// Convert one ADT file on disk to a WHM (binary heightmap) + WOT (JSON +// metadata) pair, plus a WOC (collision mesh) for movement queries. +// Returns true if all three were written. tileX/tileY are parsed from +// the filename (e.g. "MapName_32_48.adt"). +bool emitTerrainFromAdt(const std::string& adtPath, const std::string& outBase); + // Walk an extracted-asset directory and emit open-format side-files for // every requested format. Counts accumulated into stats. void emitOpenFormats(const std::string& rootDir, bool emitPng, bool emitJsonDbc, bool emitWom, bool emitWob, + bool emitTerrain, OpenFormatStats& stats); } // namespace tools