diff --git a/CMakeLists.txt b/CMakeLists.txt index 8749a9c4..06af01b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1174,7 +1174,9 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR) tools/asset_extract/extractor.cpp tools/asset_extract/path_mapper.cpp tools/asset_extract/manifest_writer.cpp + tools/asset_extract/open_format_emitter.cpp src/pipeline/dbc_loader.cpp + src/pipeline/blp_loader.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 0612c6fa..a9d20355 100644 --- a/tools/asset_extract/extractor.cpp +++ b/tools/asset_extract/extractor.cpp @@ -1,6 +1,7 @@ #include "extractor.hpp" #include "path_mapper.hpp" #include "manifest_writer.hpp" +#include "open_format_emitter.hpp" #include @@ -975,6 +976,25 @@ 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) { + std::cout << "Emitting wowee open-format side-files...\n"; + OpenFormatStats ofs; + emitOpenFormats(effectiveOutputDir, opts.emitPng, opts.emitJsonDbc, ofs); + if (opts.emitPng) { + std::cout << " PNG (BLP→PNG) : " << ofs.pngOk << " ok"; + if (ofs.pngFail) std::cout << ", " << ofs.pngFail << " failed"; + std::cout << "\n"; + } + if (opts.emitJsonDbc) { + std::cout << " JSON (DBC→JSON) : " << ofs.jsonDbcOk << " ok"; + if (ofs.jsonDbcFail) std::cout << ", " << ofs.jsonDbcFail << " failed"; + std::cout << "\n"; + } + } + // Cache WoW.exe for Warden MEM_CHECK responses { const char* exeNames[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" }; diff --git a/tools/asset_extract/extractor.hpp b/tools/asset_extract/extractor.hpp index 48588273..d5e7101d 100644 --- a/tools/asset_extract/extractor.hpp +++ b/tools/asset_extract/extractor.hpp @@ -27,6 +27,13 @@ public: std::string dbcCsvOutputDir; // When set, write CSVs into this directory instead of outputDir/expansions//db std::string referenceManifest; // If set, only extract files NOT in this manifest (delta extraction) std::string listFile; // External listfile for MPQ enumeration (resolves unnamed hash entries) + // Open-format emission: post-extract pass that writes wowee + // open-format side-files (e.g. foo.blp → foo.png) without + // touching the original. Lets wowee's runtime/editor consume + // the open formats while keeping the proprietary copies that + // private servers (AzerothCore/TrinityCore) read from. + bool emitPng = false; // BLP → PNG side-files + bool emitJsonDbc = false; // DBC → JSON side-files }; struct Stats { diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index 0add3e99..637f9c8b 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -24,6 +24,9 @@ static void printUsage(const char* prog) { << " --reference-manifest \n" << " Only extract files NOT in this manifest (delta extraction)\n" << " --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" << " --verify CRC32 verify all extracted files\n" << " --threads Number of extraction threads (default: auto)\n" << " --verbose Verbose output\n" @@ -52,6 +55,14 @@ int main(int argc, char** argv) { opts.skipDbcExtraction = true; } else if (std::strcmp(argv[i], "--dbc-csv") == 0) { opts.generateDbcCsv = true; + } else if (std::strcmp(argv[i], "--emit-png") == 0) { + opts.emitPng = true; + } else if (std::strcmp(argv[i], "--emit-json-dbc") == 0) { + opts.emitJsonDbc = 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; } 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 new file mode 100644 index 00000000..f9c8c2e0 --- /dev/null +++ b/tools/asset_extract/open_format_emitter.cpp @@ -0,0 +1,132 @@ +#include "open_format_emitter.hpp" +#include "pipeline/blp_loader.hpp" +#include "pipeline/dbc_loader.hpp" + +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" + +#include +#include +#include +#include + +namespace wowee { +namespace tools { + +namespace fs = std::filesystem; + +static std::vector readBytes(const std::string& path) { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) return {}; + auto sz = f.tellg(); + if (sz <= 0) return {}; + std::vector buf(static_cast(sz)); + f.seekg(0); + f.read(reinterpret_cast(buf.data()), sz); + return buf; +} + +bool emitPngFromBlp(const std::string& blpPath, const std::string& pngPath) { + auto bytes = readBytes(blpPath); + if (bytes.empty()) return false; + auto img = pipeline::BLPLoader::load(bytes); + if (!img.isValid()) return false; + // Same dimension/buffer-size sanity guards as the editor's texture + // exporter so we never feed stbi_write_png an invalid buffer. + const size_t expected = static_cast(img.width) * img.height * 4; + if (img.width <= 0 || img.height <= 0 || + img.width > 8192 || img.height > 8192 || + img.data.size() < expected) { + return false; + } + fs::create_directories(fs::path(pngPath).parent_path()); + return stbi_write_png(pngPath.c_str(), img.width, img.height, 4, + img.data.data(), img.width * 4) != 0; +} + +bool emitJsonFromDbc(const std::string& dbcPath, const std::string& jsonPath) { + auto bytes = readBytes(dbcPath); + if (bytes.empty()) return false; + pipeline::DBCFile dbc; + if (!dbc.load(bytes)) return false; + + nlohmann::json j; + j["format"] = "wowee-dbc-json-1.0"; + // Source field carries the original DBC name (without dirs) so the + // editor's runtime DBC overlay system can match it to the right slot. + j["source"] = fs::path(dbcPath).filename().string(); + j["recordCount"] = dbc.getRecordCount(); + j["fieldCount"] = dbc.getFieldCount(); + + nlohmann::json records = nlohmann::json::array(); + for (uint32_t i = 0; i < dbc.getRecordCount(); ++i) { + nlohmann::json row = nlohmann::json::array(); + for (uint32_t f = 0; f < dbc.getFieldCount(); ++f) { + // Same heuristic the editor's DBCExporter::exportAsJson uses: + // prefer string if printable + non-empty, else float if it + // looks like one, else uint32. The runtime loadJSON accepts + // any of the three branches. + uint32_t val = dbc.getUInt32(i, f); + std::string s = dbc.getString(i, f); + if (!s.empty() && s[0] != '\0' && s.size() < 200) { + row.push_back(s); + } else { + float fv = dbc.getFloat(i, f); + if (val != 0 && fv != 0.0f && fv > -1e10f && fv < 1e10f && + static_cast(fv) != val) { + row.push_back(fv); + } else { + row.push_back(val); + } + } + } + records.push_back(std::move(row)); + } + j["records"] = std::move(records); + + fs::create_directories(fs::path(jsonPath).parent_path()); + std::ofstream out(jsonPath); + if (!out) return false; + out << j.dump(2) << "\n"; + return true; +} + +void emitOpenFormats(const std::string& rootDir, + bool emitPng, bool emitJsonDbc, + OpenFormatStats& stats) { + if (!fs::exists(rootDir)) return; + if (!emitPng && !emitJsonDbc) return; + + auto lower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return s; + }; + + for (auto& entry : fs::recursive_directory_iterator(rootDir)) { + if (!entry.is_regular_file()) continue; + std::string ext = lower(entry.path().extension().string()); + std::string base = entry.path().string(); + if (base.size() > ext.size()) + base = base.substr(0, base.size() - ext.size()); + + if (emitPng && ext == ".blp") { + if (emitPngFromBlp(entry.path().string(), base + ".png")) { + stats.pngOk++; + } else { + stats.pngFail++; + } + } else if (emitJsonDbc && ext == ".dbc") { + if (emitJsonFromDbc(entry.path().string(), base + ".json")) { + stats.jsonDbcOk++; + } else { + stats.jsonDbcFail++; + } + } + } +} + +} // namespace tools +} // namespace wowee diff --git a/tools/asset_extract/open_format_emitter.hpp b/tools/asset_extract/open_format_emitter.hpp new file mode 100644 index 00000000..e9e2833c --- /dev/null +++ b/tools/asset_extract/open_format_emitter.hpp @@ -0,0 +1,40 @@ +#pragma once + +// Convert proprietary Blizzard formats to wowee open formats as a +// post-extraction pass. Each emit*() reads a single file from disk and +// writes the open-format equivalent SIDE-BY-SIDE — the original file is +// left untouched so private servers (AzerothCore/TrinityCore) that +// expect .blp/.dbc/.m2/.wmo continue to work unchanged. +// +// Naming: foo.blp → foo.png, foo.dbc → foo.json, foo.m2 → foo.wom, +// foo.wmo → foo.wob. + +#include +#include + +namespace wowee { +namespace tools { + +struct OpenFormatStats { + uint32_t pngOk = 0, pngFail = 0; + uint32_t jsonDbcOk = 0, jsonDbcFail = 0; +}; + +// Convert one BLP file on disk to a PNG side-file. +// Returns true on success; false on missing file, invalid BLP, or PNG write error. +bool emitPngFromBlp(const std::string& blpPath, const std::string& pngPath); + +// Convert one DBC file on disk to a JSON side-file. +// JSON layout: {format, source, recordCount, fieldCount, records:[[...], ...]} +// — same schema the editor's runtime DBC loader (loadJSON) accepts so +// the output drops into custom_zones//data/ directly. +bool emitJsonFromDbc(const std::string& dbcPath, const std::string& jsonPath); + +// Walk an extracted-asset directory and emit open-format side-files for +// every BLP and/or DBC found. Counts are accumulated into stats. +void emitOpenFormats(const std::string& rootDir, + bool emitPng, bool emitJsonDbc, + OpenFormatStats& stats); + +} // namespace tools +} // namespace wowee