diff --git a/CMakeLists.txt b/CMakeLists.txt index 06af01b9..ee871884 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -591,6 +591,7 @@ set(WOWEE_SOURCES src/pipeline/wdt_loader.cpp src/pipeline/wowee_terrain_loader.cpp src/pipeline/wowee_model.cpp + src/pipeline/wowee_model_fromm2.cpp src/pipeline/wowee_building.cpp src/pipeline/wowee_collision.cpp src/pipeline/custom_zone_discovery.cpp @@ -1177,6 +1178,10 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR) tools/asset_extract/open_format_emitter.cpp src/pipeline/dbc_loader.cpp src/pipeline/blp_loader.cpp + src/pipeline/m2_loader.cpp + src/pipeline/wmo_loader.cpp + src/pipeline/wowee_model.cpp + src/pipeline/wowee_building.cpp src/core/logger.cpp ) target_include_directories(asset_extract PRIVATE @@ -1330,6 +1335,7 @@ add_executable(wowee_editor src/pipeline/wdt_loader.cpp src/pipeline/wowee_terrain_loader.cpp src/pipeline/wowee_model.cpp + src/pipeline/wowee_model_fromm2.cpp src/pipeline/wowee_building.cpp src/pipeline/wowee_collision.cpp src/pipeline/custom_zone_discovery.cpp diff --git a/include/pipeline/wowee_model.hpp b/include/pipeline/wowee_model.hpp index 487eda21..7ea85cf5 100644 --- a/include/pipeline/wowee_model.hpp +++ b/include/pipeline/wowee_model.hpp @@ -81,6 +81,12 @@ public: // Convert an M2 model to WoweeModel (static geometry only, no animation) static WoweeModel fromM2(const std::string& m2Path, class AssetManager* am); + // Same as fromM2() but takes parsed M2 bytes + optional skin bytes + // directly. Lets the asset extractor convert during MPQ→loose-files + // without standing up an AssetManager. + static WoweeModel fromM2Bytes(const std::vector& m2Data, + const std::vector& skinData = {}); + // Check if a .wom exists static bool exists(const std::string& basePath); diff --git a/src/pipeline/wowee_model.cpp b/src/pipeline/wowee_model.cpp index 66d96c3b..7a739a06 100644 --- a/src/pipeline/wowee_model.cpp +++ b/src/pipeline/wowee_model.cpp @@ -438,30 +438,28 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath return true; } -WoweeModel WoweeModelLoader::fromM2(const std::string& m2Path, AssetManager* am) { +// Internal helper: convert a parsed M2Model (already merged with skin if +// applicable) into a WoweeModel. Shared by fromM2 (AssetManager path) +// and fromM2Bytes (extractor path). +static WoweeModel convertM2ToWom(const M2Model& m2); + +// fromM2(path, am) lives in wowee_model_fromm2.cpp so the asset extractor +// can link wowee_model.cpp without pulling in the AssetManager dependency. +// convertM2ToWom() (defined below) is the shared conversion body. +WoweeModel convertM2ToWomShared(const M2Model& m2) { return convertM2ToWom(m2); } + +WoweeModel WoweeModelLoader::fromM2Bytes(const std::vector& m2Data, + const std::vector& skinData) { WoweeModel model; - if (!am) return model; - - auto data = am->readFile(m2Path); - if (data.empty()) return model; - - auto m2 = M2Loader::load(data); - - // WotLK+ M2s store header in .m2 but geometry in .skin — always merge the - // skin file when present so we get vertices/indices/batches even for M2s - // that already report isValid() (older expansions). - { - std::string skinPath = m2Path; - auto dotPos = skinPath.rfind('.'); - if (dotPos != std::string::npos) - skinPath = skinPath.substr(0, dotPos) + "00.skin"; - auto skinData = am->readFile(skinPath); - if (!skinData.empty()) - M2Loader::loadSkin(skinData, m2); - } - + if (m2Data.empty()) return model; + auto m2 = M2Loader::load(m2Data); + if (!skinData.empty()) M2Loader::loadSkin(skinData, m2); if (!m2.isValid()) return model; + return convertM2ToWom(m2); +} +static WoweeModel convertM2ToWom(const M2Model& m2) { + WoweeModel model; model.name = m2.name; model.boundRadius = m2.boundRadius; diff --git a/src/pipeline/wowee_model_fromm2.cpp b/src/pipeline/wowee_model_fromm2.cpp new file mode 100644 index 00000000..2bbd3667 --- /dev/null +++ b/src/pipeline/wowee_model_fromm2.cpp @@ -0,0 +1,42 @@ +// AssetManager-bound implementation of WoweeModelLoader::fromM2(path, am). +// Split out from wowee_model.cpp so the asset extractor can link the core +// model loader/saver without dragging the AssetManager dependency in. + +#include "pipeline/wowee_model.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/m2_loader.hpp" + +namespace wowee { +namespace pipeline { + +// Friend bridge to the conversion helper defined in wowee_model.cpp. +WoweeModel convertM2ToWomShared(const M2Model& m2); + +WoweeModel WoweeModelLoader::fromM2(const std::string& m2Path, AssetManager* am) { + WoweeModel model; + if (!am) return model; + + auto data = am->readFile(m2Path); + if (data.empty()) return model; + + auto m2 = M2Loader::load(data); + + // WotLK+ M2s store header in .m2 but geometry in .skin — always merge + // the skin file when present so we get vertices/indices/batches even + // for M2s that already report isValid() (older expansions). + { + std::string skinPath = m2Path; + auto dotPos = skinPath.rfind('.'); + if (dotPos != std::string::npos) + skinPath = skinPath.substr(0, dotPos) + "00.skin"; + auto skinData = am->readFile(skinPath); + if (!skinData.empty()) + M2Loader::loadSkin(skinData, m2); + } + + if (!m2.isValid()) return model; + return convertM2ToWomShared(m2); +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/asset_extract/extractor.cpp b/tools/asset_extract/extractor.cpp index a9d20355..bc2154d8 100644 --- a/tools/asset_extract/extractor.cpp +++ b/tools/asset_extract/extractor.cpp @@ -979,10 +979,11 @@ 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) { + if (opts.emitPng || opts.emitJsonDbc || opts.emitWom || opts.emitWob) { std::cout << "Emitting wowee open-format side-files...\n"; OpenFormatStats ofs; - emitOpenFormats(effectiveOutputDir, opts.emitPng, opts.emitJsonDbc, ofs); + emitOpenFormats(effectiveOutputDir, opts.emitPng, opts.emitJsonDbc, + opts.emitWom, opts.emitWob, ofs); if (opts.emitPng) { std::cout << " PNG (BLP→PNG) : " << ofs.pngOk << " ok"; if (ofs.pngFail) std::cout << ", " << ofs.pngFail << " failed"; @@ -993,6 +994,16 @@ bool Extractor::run(const Options& opts) { if (ofs.jsonDbcFail) std::cout << ", " << ofs.jsonDbcFail << " failed"; std::cout << "\n"; } + if (opts.emitWom) { + std::cout << " WOM (M2→WOM) : " << ofs.womOk << " ok"; + if (ofs.womFail) std::cout << ", " << ofs.womFail << " failed"; + std::cout << "\n"; + } + if (opts.emitWob) { + std::cout << " WOB (WMO→WOB) : " << ofs.wobOk << " ok"; + if (ofs.wobFail) std::cout << ", " << ofs.wobFail << " 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 d5e7101d..2cbfea00 100644 --- a/tools/asset_extract/extractor.hpp +++ b/tools/asset_extract/extractor.hpp @@ -34,6 +34,8 @@ public: // private servers (AzerothCore/TrinityCore) read from. bool emitPng = false; // BLP → PNG side-files bool emitJsonDbc = false; // DBC → JSON side-files + bool emitWom = false; // M2 (+skin) → WOM side-files + bool emitWob = false; // WMO (+groups) → WOB side-files }; struct Stats { diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index 637f9c8b..661eebfc 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -26,7 +26,9 @@ static void printUsage(const char* prog) { << " --dbc-csv-out Write CSV DBCs into (overrides default output path)\n" << " --emit-png Emit foo.png next to every extracted foo.blp\n" << " --emit-json-dbc Emit foo.json next to every extracted foo.dbc\n" - << " --emit-open Shortcut: enable every open-format emitter (png+json)\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" << " --verify CRC32 verify all extracted files\n" << " --threads Number of extraction threads (default: auto)\n" << " --verbose Verbose output\n" @@ -59,10 +61,16 @@ int main(int argc, char** argv) { opts.emitPng = true; } else if (std::strcmp(argv[i], "--emit-json-dbc") == 0) { opts.emitJsonDbc = true; + } else if (std::strcmp(argv[i], "--emit-wom") == 0) { + opts.emitWom = true; + } else if (std::strcmp(argv[i], "--emit-wob") == 0) { + opts.emitWob = 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; } 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 f9c8c2e0..f8d95752 100644 --- a/tools/asset_extract/open_format_emitter.cpp +++ b/tools/asset_extract/open_format_emitter.cpp @@ -1,6 +1,10 @@ #include "open_format_emitter.hpp" #include "pipeline/blp_loader.hpp" #include "pipeline/dbc_loader.hpp" +#include "pipeline/wowee_model.hpp" +#include "pipeline/wowee_building.hpp" +#include "pipeline/m2_loader.hpp" +#include "pipeline/wmo_loader.hpp" #include @@ -93,11 +97,50 @@ bool emitJsonFromDbc(const std::string& dbcPath, const std::string& jsonPath) { return true; } +bool emitWomFromM2(const std::string& m2Path, const std::string& womBase) { + auto m2Bytes = readBytes(m2Path); + if (m2Bytes.empty()) return false; + // WotLK+ M2s store the actual geometry in 00.skin; merge it if + // it sits next to the .m2 (usual case after extraction). + std::vector skinBytes; + { + std::string skinPath = m2Path; + auto dot = skinPath.rfind('.'); + if (dot != std::string::npos) + skinPath = skinPath.substr(0, dot) + "00.skin"; + skinBytes = readBytes(skinPath); + } + auto wom = pipeline::WoweeModelLoader::fromM2Bytes(m2Bytes, skinBytes); + if (!wom.isValid()) return false; + return pipeline::WoweeModelLoader::save(wom, womBase); +} + +bool emitWobFromWmo(const std::string& wmoPath, const std::string& wobBase) { + auto rootBytes = readBytes(wmoPath); + if (rootBytes.empty()) return false; + auto wmo = pipeline::WMOLoader::load(rootBytes); + if (wmo.nGroups == 0) return false; + // Merge group files _NNN.wmo for groups that have them. + std::string base = wmoPath; + if (base.size() > 4) base = base.substr(0, base.size() - 4); + for (uint32_t gi = 0; gi < wmo.nGroups; ++gi) { + char suffix[16]; + std::snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi); + auto gd = readBytes(base + suffix); + if (!gd.empty()) pipeline::WMOLoader::loadGroup(gd, wmo, gi); + } + auto bld = pipeline::WoweeBuildingLoader::fromWMO( + wmo, fs::path(wmoPath).stem().string()); + if (!bld.isValid()) return false; + return pipeline::WoweeBuildingLoader::save(bld, wobBase); +} + void emitOpenFormats(const std::string& rootDir, bool emitPng, bool emitJsonDbc, + bool emitWom, bool emitWob, OpenFormatStats& stats) { if (!fs::exists(rootDir)) return; - if (!emitPng && !emitJsonDbc) return; + if (!emitPng && !emitJsonDbc && !emitWom && !emitWob) return; auto lower = [](std::string s) { std::transform(s.begin(), s.end(), s.begin(), @@ -124,6 +167,20 @@ void emitOpenFormats(const std::string& rootDir, } else { stats.jsonDbcFail++; } + } else if (emitWom && ext == ".m2") { + if (emitWomFromM2(entry.path().string(), base)) stats.womOk++; + else stats.womFail++; + } else if (emitWob && ext == ".wmo") { + // Skip group sub-files (_NNN.wmo) — those get merged + // into the root WMO during conversion. + std::string fname = entry.path().filename().string(); + auto under = fname.rfind('_'); + bool isGroup = (under != std::string::npos && + fname.size() - under == 8); // "_NNN.wmo" suffix + if (!isGroup) { + if (emitWobFromWmo(entry.path().string(), base)) stats.wobOk++; + else stats.wobFail++; + } } } } diff --git a/tools/asset_extract/open_format_emitter.hpp b/tools/asset_extract/open_format_emitter.hpp index e9e2833c..4c87391b 100644 --- a/tools/asset_extract/open_format_emitter.hpp +++ b/tools/asset_extract/open_format_emitter.hpp @@ -18,6 +18,8 @@ namespace tools { struct OpenFormatStats { uint32_t pngOk = 0, pngFail = 0; uint32_t jsonDbcOk = 0, jsonDbcFail = 0; + uint32_t womOk = 0, womFail = 0; + uint32_t wobOk = 0, wobFail = 0; }; // Convert one BLP file on disk to a PNG side-file. @@ -30,10 +32,19 @@ bool emitPngFromBlp(const std::string& blpPath, const std::string& pngPath); // the output drops into custom_zones//data/ directly. bool emitJsonFromDbc(const std::string& dbcPath, const std::string& jsonPath); +// Convert one M2 file on disk to a WOM side-file. Auto-locates and merges +// the matching 00.skin if present (WotLK+ models store geometry there). +bool emitWomFromM2(const std::string& m2Path, const std::string& womBase); + +// Convert one WMO file on disk to a WOB side-file. Auto-locates and merges +// matching _NNN.wmo group files if present. +bool emitWobFromWmo(const std::string& wmoPath, const std::string& wobBase); + // Walk an extracted-asset directory and emit open-format side-files for -// every BLP and/or DBC found. Counts are accumulated into stats. +// every requested format. Counts accumulated into stats. void emitOpenFormats(const std::string& rootDir, bool emitPng, bool emitJsonDbc, + bool emitWom, bool emitWob, OpenFormatStats& stats); } // namespace tools