mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 17:43:51 +00:00
feat(extract): emit WHM+WOT+WOC for ADT terrain tiles
Final piece of the open-format emit pipeline:
--emit-terrain foo.adt → foo.whm + foo.wot + foo.woc
With this, --emit-open now produces a fully open-format zone
alongside every Blizzard MPQ extraction:
BLP → PNG (textures)
DBC → JSON (data tables)
M2 → WOM (models, with skin merge)
WMO → WOB (buildings, with group merge)
ADT → WHM/WOT (terrain heights + metadata)
→ WOC (collision mesh derived from heights)
Originals stay on disk and indexed by manifest.json so private
servers continue to load proprietary formats; wowee runtime/editor
read the open formats directly. One extraction now feeds both
audiences with no separate conversion pass.
Implementation:
- Inline WHM+WOT writer in open_format_emitter.cpp (mirrors the
editor's WoweeTerrain::exportOpen but without the PNG-preview /
normal-map deps so the extractor stays editor-independent).
- Tile coords (x,y) parsed from <map>_<x>_<y>.adt filename.
- Collision mesh derived via WoweeCollisionBuilder::fromTerrain
(terrain triangles only — WMO collision overlays would need
asset manager and aren't worth the extractor complexity).
This commit is contained in:
parent
e6ace7cce5
commit
d4c69a2b46
6 changed files with 182 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <N> 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) {
|
||||
|
|
|
|||
|
|
@ -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 <nlohmann/json.hpp>
|
||||
|
||||
|
|
@ -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<const char*>(&magic), 4);
|
||||
f.write(reinterpret_cast<const char*>(&chunks), 4);
|
||||
f.write(reinterpret_cast<const char*>(&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<const char*>(&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<const char*>(clean), 145 * 4);
|
||||
uint32_t alphaSize = std::min<uint32_t>(
|
||||
static_cast<uint32_t>(chunk.alphaMap.size()), 65536);
|
||||
f.write(reinterpret_cast<const char*>(&alphaSize), 4);
|
||||
if (alphaSize > 0)
|
||||
f.write(reinterpret_cast<const char*>(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 "<map>_<x>_<y>.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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <base>_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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue