From 0d2312ec8d7ceee517ba50eaf0b85feae12fc0d0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 06:25:04 -0700 Subject: [PATCH] refactor(editor): extract single-file --convert-* into cli_convert_single.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the five single-file format converters (--convert-m2, --convert-wmo, --convert-dbc-json, --convert-json-dbc, --convert-blp-png) out of two dedicated post-loop blocks in main.cpp into a new cli_convert_single.{hpp,cpp} module. The batch wrappers in cli_convert.cpp keep working — they shell out to wowee_editor with these flags. main.cpp shrinks by ~335 lines (10,979 → 10,644). The two trailing for-loops were dead code after wiring the dispatcher into the main loop, so they're gone too. --- CMakeLists.txt | 1 + tools/editor/cli_convert_single.cpp | 387 ++++++++++++++++++++++++++++ tools/editor/cli_convert_single.hpp | 27 ++ tools/editor/main.cpp | 345 +------------------------ 4 files changed, 420 insertions(+), 340 deletions(-) create mode 100644 tools/editor/cli_convert_single.cpp create mode 100644 tools/editor/cli_convert_single.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e1f25b2..08536d8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1324,6 +1324,7 @@ add_executable(wowee_editor tools/editor/cli_export.cpp tools/editor/cli_bake.cpp tools/editor/cli_migrate.cpp + tools/editor/cli_convert_single.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_convert_single.cpp b/tools/editor/cli_convert_single.cpp new file mode 100644 index 00000000..4ab1015d --- /dev/null +++ b/tools/editor/cli_convert_single.cpp @@ -0,0 +1,387 @@ +#include "cli_convert_single.hpp" + +#include "pipeline/asset_manager.hpp" +#include "pipeline/wowee_model.hpp" +#include "pipeline/wowee_building.hpp" +#include "pipeline/wmo_loader.hpp" +#include "pipeline/dbc_loader.hpp" +#include "pipeline/blp_loader.hpp" +#include "stb_image_write.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleConvertM2(int& i, int /*argc*/, char** argv, + std::string& dataPath) { + std::string m2Path = argv[++i]; + std::printf("Converting M2-WOM: %s\n", m2Path.c_str()); + if (dataPath.empty()) dataPath = "Data"; + wowee::pipeline::AssetManager am; + if (!am.initialize(dataPath)) { + std::fprintf(stderr, "FAILED: cannot initialize asset manager\n"); + return 1; + } + auto wom = wowee::pipeline::WoweeModelLoader::fromM2(m2Path, &am); + if (!wom.isValid()) { + std::fprintf(stderr, "FAILED: %s\n", m2Path.c_str()); + am.shutdown(); + return 1; + } + std::string outPath = m2Path; + auto dot = outPath.rfind('.'); + if (dot != std::string::npos) outPath = outPath.substr(0, dot); + wowee::pipeline::WoweeModelLoader::save(wom, "output/models/" + outPath); + std::printf("OK: output/models/%s.wom (v%u, %zu verts, %zu bones, %zu batches)\n", + outPath.c_str(), wom.version, wom.vertices.size(), + wom.bones.size(), wom.batches.size()); + am.shutdown(); + return 0; +} + +int handleConvertWmo(int& i, int /*argc*/, char** argv, + std::string& dataPath) { + std::string wmoPath = argv[++i]; + std::printf("Converting WMO-WOB: %s\n", wmoPath.c_str()); + if (dataPath.empty()) dataPath = "Data"; + wowee::pipeline::AssetManager am; + if (!am.initialize(dataPath)) { + std::fprintf(stderr, "FAILED: cannot initialize asset manager\n"); + return 1; + } + auto wmoData = am.readFile(wmoPath); + if (wmoData.empty()) { + std::fprintf(stderr, "FAILED: file not found: %s\n", wmoPath.c_str()); + am.shutdown(); + return 1; + } + auto wmoModel = wowee::pipeline::WMOLoader::load(wmoData); + if (wmoModel.nGroups > 0) { + std::string wmoBase = wmoPath; + if (wmoBase.size() > 4) wmoBase = wmoBase.substr(0, wmoBase.size() - 4); + for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { + char suffix[16]; + snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi); + auto gd = am.readFile(wmoBase + suffix); + if (!gd.empty()) wowee::pipeline::WMOLoader::loadGroup(gd, wmoModel, gi); + } + } + auto wob = wowee::pipeline::WoweeBuildingLoader::fromWMO(wmoModel, wmoPath); + if (!wob.isValid()) { + std::fprintf(stderr, "FAILED: %s\n", wmoPath.c_str()); + am.shutdown(); + return 1; + } + std::string outPath = wmoPath; + auto dot = outPath.rfind('.'); + if (dot != std::string::npos) outPath = outPath.substr(0, dot); + wowee::pipeline::WoweeBuildingLoader::save(wob, "output/buildings/" + outPath); + std::printf("OK: output/buildings/%s.wob (%zu groups)\n", + outPath.c_str(), wob.groups.size()); + am.shutdown(); + return 0; +} + +int handleConvertDbcJson(int& i, int argc, char** argv) { + // Standalone DBC -> JSON sidecar conversion. Mirrors what + // asset_extract --emit-open does for one file at a time, so + // designers don't have to re-run a full extraction just to + // refresh one DBC sidecar. + std::string dbcPath = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + outPath = argv[++i]; + } + if (outPath.empty()) { + outPath = dbcPath; + if (outPath.size() >= 4 && + outPath.substr(outPath.size() - 4) == ".dbc") { + outPath = outPath.substr(0, outPath.size() - 4); + } + outPath += ".json"; + } + std::ifstream in(dbcPath, std::ios::binary); + if (!in) { + std::fprintf(stderr, "convert-dbc-json: cannot open %s\n", dbcPath.c_str()); + return 1; + } + std::vector bytes((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + wowee::pipeline::DBCFile dbc; + if (!dbc.load(bytes)) { + std::fprintf(stderr, "convert-dbc-json: failed to parse %s\n", dbcPath.c_str()); + return 1; + } + // Same JSON schema asset_extract emits, so the editor's runtime + // overlay loader picks the file up without changes. + nlohmann::json j; + j["format"] = "wowee-dbc-json-1.0"; + j["source"] = std::filesystem::path(dbcPath).filename().string(); + j["recordCount"] = dbc.getRecordCount(); + j["fieldCount"] = dbc.getFieldCount(); + nlohmann::json records = nlohmann::json::array(); + for (uint32_t r = 0; r < dbc.getRecordCount(); ++r) { + nlohmann::json row = nlohmann::json::array(); + for (uint32_t f = 0; f < dbc.getFieldCount(); ++f) { + // Same heuristic as open_format_emitter::emitJsonFromDbc: + // prefer string > float > uint32 based on what the + // bytes plausibly are. Round-trips through loadJSON. + uint32_t val = dbc.getUInt32(r, f); + std::string s = dbc.getString(r, f); + if (!s.empty() && s[0] != '\0' && s.size() < 200) { + row.push_back(s); + } else { + float fv = dbc.getFloat(r, 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); + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, "convert-dbc-json: cannot write %s\n", outPath.c_str()); + return 1; + } + out << j.dump(2) << "\n"; + std::printf("Converted %s -> %s\n", dbcPath.c_str(), outPath.c_str()); + std::printf(" %u records x %u fields\n", + dbc.getRecordCount(), dbc.getFieldCount()); + return 0; +} + +int handleConvertJsonDbc(int& i, int argc, char** argv) { + // Reverse direction — JSON sidecar back to binary DBC. Useful + // for shipping edited content to private servers (AzerothCore / + // TrinityCore) which only consume binary DBC. The output is + // byte-compatible with the original Blizzard format. + std::string jsonPath = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + outPath = argv[++i]; + } + if (outPath.empty()) { + outPath = jsonPath; + if (outPath.size() >= 5 && + outPath.substr(outPath.size() - 5) == ".json") { + outPath = outPath.substr(0, outPath.size() - 5); + } + outPath += ".dbc"; + } + std::ifstream in(jsonPath); + if (!in) { + std::fprintf(stderr, "convert-json-dbc: cannot open %s\n", jsonPath.c_str()); + return 1; + } + nlohmann::json doc; + try { in >> doc; } + catch (const std::exception& e) { + std::fprintf(stderr, "convert-json-dbc: bad JSON in %s (%s)\n", + jsonPath.c_str(), e.what()); + return 1; + } + uint32_t fieldCount = doc.value("fieldCount", 0u); + if (!doc.contains("records") || !doc["records"].is_array()) { + std::fprintf(stderr, "convert-json-dbc: missing 'records' array in %s\n", + jsonPath.c_str()); + return 1; + } + const auto& records = doc["records"]; + uint32_t recordCount = static_cast(records.size()); + if (fieldCount == 0 && recordCount > 0 && records[0].is_array()) { + // Tolerate JSON files that drop fieldCount — derive from row. + fieldCount = static_cast(records[0].size()); + } + if (fieldCount == 0) { + std::fprintf(stderr, + "convert-json-dbc: cannot determine fieldCount in %s\n", + jsonPath.c_str()); + return 1; + } + uint32_t recordSize = fieldCount * 4; + // Build records + string block. Strings are deduped: identical + // strings reuse the same offset in the block. The first byte + // of the block is always '\0' so offset=0 means empty string, + // matching Blizzard's convention. + std::vector recordBytes(recordCount * recordSize, 0); + std::vector stringBlock; + stringBlock.push_back(0); // leading NUL — empty-string offset + std::unordered_map stringOffsets; + stringOffsets[""] = 0; + auto internString = [&](const std::string& s) -> uint32_t { + if (s.empty()) return 0; + auto it = stringOffsets.find(s); + if (it != stringOffsets.end()) return it->second; + uint32_t off = static_cast(stringBlock.size()); + for (char c : s) stringBlock.push_back(static_cast(c)); + stringBlock.push_back(0); + stringOffsets[s] = off; + return off; + }; + int convertErrors = 0; + for (uint32_t r = 0; r < recordCount; ++r) { + const auto& row = records[r]; + if (!row.is_array() || row.size() != fieldCount) { + convertErrors++; + continue; + } + uint8_t* dst = recordBytes.data() + r * recordSize; + for (uint32_t f = 0; f < fieldCount; ++f) { + uint32_t val = 0; + const auto& cell = row[f]; + if (cell.is_string()) { + val = internString(cell.get()); + } else if (cell.is_number_float()) { + float fv = cell.get(); + std::memcpy(&val, &fv, 4); + } else if (cell.is_number_unsigned()) { + val = cell.get(); + } else if (cell.is_number_integer()) { + // Negative ints reinterpret as uint32 (DBC has no + // separate signed type; the consumer interprets). + int32_t sv = cell.get(); + std::memcpy(&val, &sv, 4); + } else if (cell.is_boolean()) { + val = cell.get() ? 1u : 0u; + } else if (cell.is_null()) { + val = 0; + } else { + convertErrors++; + } + // Little-endian write — DBC is always LE per Blizzard + // format spec, regardless of host architecture. + dst[f * 4 + 0] = val & 0xFF; + dst[f * 4 + 1] = (val >> 8) & 0xFF; + dst[f * 4 + 2] = (val >> 16) & 0xFF; + dst[f * 4 + 3] = (val >> 24) & 0xFF; + } + } + // Header: WDBC magic + 4 uint32s (recordCount, fieldCount, + // recordSize, stringBlockSize). + std::ofstream out(outPath, std::ios::binary); + if (!out) { + std::fprintf(stderr, "convert-json-dbc: cannot write %s\n", outPath.c_str()); + return 1; + } + uint32_t header[5] = { + 0x43424457u, // 'WDBC' little-endian + recordCount, fieldCount, recordSize, + static_cast(stringBlock.size()) + }; + out.write(reinterpret_cast(header), sizeof(header)); + out.write(reinterpret_cast(recordBytes.data()), + recordBytes.size()); + out.write(reinterpret_cast(stringBlock.data()), + stringBlock.size()); + out.close(); + std::printf("Converted %s -> %s\n", jsonPath.c_str(), outPath.c_str()); + std::printf(" %u records x %u fields, %zu-byte string block\n", + recordCount, fieldCount, stringBlock.size()); + if (convertErrors > 0) { + std::printf(" warning: %d cell(s) had unrecognized types\n", convertErrors); + } + return 0; +} + +int handleConvertBlpPng(int& i, int argc, char** argv) { + // Standalone BLP -> PNG conversion. Same code path as + // asset_extract --emit-open's per-file walker, but for one + // texture without re-running a full extraction. + std::string blpPath = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + outPath = argv[++i]; + } + if (outPath.empty()) { + outPath = blpPath; + if (outPath.size() >= 4 && + outPath.substr(outPath.size() - 4) == ".blp") { + outPath = outPath.substr(0, outPath.size() - 4); + } + outPath += ".png"; + } + std::ifstream in(blpPath, std::ios::binary); + if (!in) { + std::fprintf(stderr, "convert-blp-png: cannot open %s\n", blpPath.c_str()); + return 1; + } + std::vector bytes((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + auto img = wowee::pipeline::BLPLoader::load(bytes); + if (!img.isValid()) { + std::fprintf(stderr, "convert-blp-png: failed to decode %s\n", + blpPath.c_str()); + return 1; + } + // Same dimension/buffer-size guards as the asset_extract + // emitter 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) { + std::fprintf(stderr, "convert-blp-png: invalid dimensions or data (%dx%d, %zu bytes)\n", + img.width, img.height, img.data.size()); + return 1; + } + // Ensure output directory exists; fs::create_directories with + // an empty path is a no-op so we don't need to special-case + // 'png in cwd'. + std::filesystem::create_directories( + std::filesystem::path(outPath).parent_path()); + int rc = stbi_write_png(outPath.c_str(), + img.width, img.height, 4, + img.data.data(), img.width * 4); + if (!rc) { + std::fprintf(stderr, "convert-blp-png: stbi_write_png failed for %s\n", + outPath.c_str()); + return 1; + } + std::printf("Converted %s -> %s\n", blpPath.c_str(), outPath.c_str()); + std::printf(" %dx%d, %zu bytes (RGBA8)\n", + img.width, img.height, img.data.size()); + return 0; +} + +} // namespace + +bool handleConvertSingle(int& i, int argc, char** argv, + std::string& dataPath, int& outRc) { + if (std::strcmp(argv[i], "--convert-m2") == 0 && i + 1 < argc) { + outRc = handleConvertM2(i, argc, argv, dataPath); return true; + } + if (std::strcmp(argv[i], "--convert-wmo") == 0 && i + 1 < argc) { + outRc = handleConvertWmo(i, argc, argv, dataPath); return true; + } + if (std::strcmp(argv[i], "--convert-dbc-json") == 0 && i + 1 < argc) { + outRc = handleConvertDbcJson(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--convert-json-dbc") == 0 && i + 1 < argc) { + outRc = handleConvertJsonDbc(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--convert-blp-png") == 0 && i + 1 < argc) { + outRc = handleConvertBlpPng(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_convert_single.hpp b/tools/editor/cli_convert_single.hpp new file mode 100644 index 00000000..8873a185 --- /dev/null +++ b/tools/editor/cli_convert_single.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the single-file format-conversion handlers. These are +// the source-of-truth implementations; the batch wrappers in +// cli_convert.cpp shell out to wowee_editor with these flags. +// --convert-m2 M2 → WOM (uses asset manager + dataPath) +// --convert-wmo WMO → WOB (uses asset manager + dataPath) +// --convert-dbc-json DBC → JSON sidecar +// --convert-json-dbc JSON sidecar → binary DBC +// --convert-blp-png BLP → PNG +// +// dataPath is mutated to "Data" if empty so the M2/WMO handlers +// have a default location to look in. +// +// Returns true if matched; outRc holds the exit code. +bool handleConvertSingle(int& i, int argc, char** argv, + std::string& dataPath, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 778d7032..cfdd9d69 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -25,6 +25,7 @@ #include "cli_export.hpp" #include "cli_bake.hpp" #include "cli_migrate.hpp" +#include "cli_convert_single.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -427,6 +428,10 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleMigrate(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleConvertSingle(i, argc, argv, + dataPath, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -10616,346 +10621,6 @@ int main(int argc, char* argv[]) { } } - // Batch convert mode: --convert converts M2 to WOM - for (int i = 1; i < argc; i++) { - if (std::strcmp(argv[i], "--convert-m2") == 0 && i + 1 < argc) { - std::string m2Path = argv[++i]; - std::printf("Converting M2→WOM: %s\n", m2Path.c_str()); - if (dataPath.empty()) dataPath = "Data"; - wowee::pipeline::AssetManager am; - if (am.initialize(dataPath)) { - auto wom = wowee::pipeline::WoweeModelLoader::fromM2(m2Path, &am); - if (wom.isValid()) { - std::string outPath = m2Path; - auto dot = outPath.rfind('.'); - if (dot != std::string::npos) outPath = outPath.substr(0, dot); - wowee::pipeline::WoweeModelLoader::save(wom, "output/models/" + outPath); - std::printf("OK: output/models/%s.wom (v%u, %zu verts, %zu bones, %zu batches)\n", - outPath.c_str(), wom.version, wom.vertices.size(), - wom.bones.size(), wom.batches.size()); - } else { - std::fprintf(stderr, "FAILED: %s\n", m2Path.c_str()); - am.shutdown(); - return 1; - } - am.shutdown(); - } else { - std::fprintf(stderr, "FAILED: cannot initialize asset manager\n"); - return 1; - } - return 0; - } - } - - // Batch convert mode: --convert-wmo converts WMO to WOB - for (int i = 1; i < argc; i++) { - if (std::strcmp(argv[i], "--convert-wmo") == 0 && i + 1 < argc) { - std::string wmoPath = argv[++i]; - std::printf("Converting WMO→WOB: %s\n", wmoPath.c_str()); - if (dataPath.empty()) dataPath = "Data"; - wowee::pipeline::AssetManager am; - if (am.initialize(dataPath)) { - auto wmoData = am.readFile(wmoPath); - if (!wmoData.empty()) { - auto wmoModel = wowee::pipeline::WMOLoader::load(wmoData); - if (wmoModel.nGroups > 0) { - std::string wmoBase = wmoPath; - if (wmoBase.size() > 4) wmoBase = wmoBase.substr(0, wmoBase.size() - 4); - for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { - char suffix[16]; - snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi); - auto gd = am.readFile(wmoBase + suffix); - if (!gd.empty()) wowee::pipeline::WMOLoader::loadGroup(gd, wmoModel, gi); - } - } - auto wob = wowee::pipeline::WoweeBuildingLoader::fromWMO(wmoModel, wmoPath); - if (wob.isValid()) { - std::string outPath = wmoPath; - auto dot = outPath.rfind('.'); - if (dot != std::string::npos) outPath = outPath.substr(0, dot); - wowee::pipeline::WoweeBuildingLoader::save(wob, "output/buildings/" + outPath); - std::printf("OK: output/buildings/%s.wob (%zu groups)\n", - outPath.c_str(), wob.groups.size()); - } else { - std::fprintf(stderr, "FAILED: %s\n", wmoPath.c_str()); - am.shutdown(); - return 1; - } - } else { - std::fprintf(stderr, "FAILED: file not found: %s\n", wmoPath.c_str()); - am.shutdown(); - return 1; - } - am.shutdown(); - } else { - std::fprintf(stderr, "FAILED: cannot initialize asset manager\n"); - return 1; - } - return 0; - } - if (std::strcmp(argv[i], "--convert-dbc-json") == 0 && i + 1 < argc) { - // Standalone DBC -> JSON sidecar conversion. Mirrors what - // asset_extract --emit-open does for one file at a time, so - // designers don't have to re-run a full extraction just to - // refresh one DBC sidecar. - std::string dbcPath = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') { - outPath = argv[++i]; - } - if (outPath.empty()) { - outPath = dbcPath; - if (outPath.size() >= 4 && - outPath.substr(outPath.size() - 4) == ".dbc") { - outPath = outPath.substr(0, outPath.size() - 4); - } - outPath += ".json"; - } - std::ifstream in(dbcPath, std::ios::binary); - if (!in) { - std::fprintf(stderr, "convert-dbc-json: cannot open %s\n", dbcPath.c_str()); - return 1; - } - std::vector bytes((std::istreambuf_iterator(in)), - std::istreambuf_iterator()); - wowee::pipeline::DBCFile dbc; - if (!dbc.load(bytes)) { - std::fprintf(stderr, "convert-dbc-json: failed to parse %s\n", dbcPath.c_str()); - return 1; - } - // Same JSON schema asset_extract emits, so the editor's runtime - // overlay loader picks the file up without changes. - nlohmann::json j; - j["format"] = "wowee-dbc-json-1.0"; - j["source"] = std::filesystem::path(dbcPath).filename().string(); - j["recordCount"] = dbc.getRecordCount(); - j["fieldCount"] = dbc.getFieldCount(); - nlohmann::json records = nlohmann::json::array(); - for (uint32_t r = 0; r < dbc.getRecordCount(); ++r) { - nlohmann::json row = nlohmann::json::array(); - for (uint32_t f = 0; f < dbc.getFieldCount(); ++f) { - // Same heuristic as open_format_emitter::emitJsonFromDbc: - // prefer string > float > uint32 based on what the - // bytes plausibly are. Round-trips through loadJSON. - uint32_t val = dbc.getUInt32(r, f); - std::string s = dbc.getString(r, f); - if (!s.empty() && s[0] != '\0' && s.size() < 200) { - row.push_back(s); - } else { - float fv = dbc.getFloat(r, 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); - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, "convert-dbc-json: cannot write %s\n", outPath.c_str()); - return 1; - } - out << j.dump(2) << "\n"; - std::printf("Converted %s -> %s\n", dbcPath.c_str(), outPath.c_str()); - std::printf(" %u records x %u fields\n", - dbc.getRecordCount(), dbc.getFieldCount()); - return 0; - } - if (std::strcmp(argv[i], "--convert-json-dbc") == 0 && i + 1 < argc) { - // Reverse direction — JSON sidecar back to binary DBC. Useful - // for shipping edited content to private servers (AzerothCore / - // TrinityCore) which only consume binary DBC. The output is - // byte-compatible with the original Blizzard format. - std::string jsonPath = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') { - outPath = argv[++i]; - } - if (outPath.empty()) { - outPath = jsonPath; - if (outPath.size() >= 5 && - outPath.substr(outPath.size() - 5) == ".json") { - outPath = outPath.substr(0, outPath.size() - 5); - } - outPath += ".dbc"; - } - std::ifstream in(jsonPath); - if (!in) { - std::fprintf(stderr, "convert-json-dbc: cannot open %s\n", jsonPath.c_str()); - return 1; - } - nlohmann::json doc; - try { in >> doc; } - catch (const std::exception& e) { - std::fprintf(stderr, "convert-json-dbc: bad JSON in %s (%s)\n", - jsonPath.c_str(), e.what()); - return 1; - } - uint32_t fieldCount = doc.value("fieldCount", 0u); - if (!doc.contains("records") || !doc["records"].is_array()) { - std::fprintf(stderr, "convert-json-dbc: missing 'records' array in %s\n", - jsonPath.c_str()); - return 1; - } - const auto& records = doc["records"]; - uint32_t recordCount = static_cast(records.size()); - if (fieldCount == 0 && recordCount > 0 && records[0].is_array()) { - // Tolerate JSON files that drop fieldCount — derive from row. - fieldCount = static_cast(records[0].size()); - } - if (fieldCount == 0) { - std::fprintf(stderr, - "convert-json-dbc: cannot determine fieldCount in %s\n", - jsonPath.c_str()); - return 1; - } - uint32_t recordSize = fieldCount * 4; - // Build records + string block. Strings are deduped: identical - // strings reuse the same offset in the block. The first byte - // of the block is always '\0' so offset=0 means empty string, - // matching Blizzard's convention. - std::vector recordBytes(recordCount * recordSize, 0); - std::vector stringBlock; - stringBlock.push_back(0); // leading NUL — empty-string offset - std::unordered_map stringOffsets; - stringOffsets[""] = 0; - auto internString = [&](const std::string& s) -> uint32_t { - if (s.empty()) return 0; - auto it = stringOffsets.find(s); - if (it != stringOffsets.end()) return it->second; - uint32_t off = static_cast(stringBlock.size()); - for (char c : s) stringBlock.push_back(static_cast(c)); - stringBlock.push_back(0); - stringOffsets[s] = off; - return off; - }; - int convertErrors = 0; - for (uint32_t r = 0; r < recordCount; ++r) { - const auto& row = records[r]; - if (!row.is_array() || row.size() != fieldCount) { - convertErrors++; - continue; - } - uint8_t* dst = recordBytes.data() + r * recordSize; - for (uint32_t f = 0; f < fieldCount; ++f) { - uint32_t val = 0; - const auto& cell = row[f]; - if (cell.is_string()) { - val = internString(cell.get()); - } else if (cell.is_number_float()) { - float fv = cell.get(); - std::memcpy(&val, &fv, 4); - } else if (cell.is_number_unsigned()) { - val = cell.get(); - } else if (cell.is_number_integer()) { - // Negative ints reinterpret as uint32 (DBC has no - // separate signed type; the consumer interprets). - int32_t sv = cell.get(); - std::memcpy(&val, &sv, 4); - } else if (cell.is_boolean()) { - val = cell.get() ? 1u : 0u; - } else if (cell.is_null()) { - val = 0; - } else { - convertErrors++; - } - // Little-endian write — DBC is always LE per Blizzard - // format spec, regardless of host architecture. - dst[f * 4 + 0] = val & 0xFF; - dst[f * 4 + 1] = (val >> 8) & 0xFF; - dst[f * 4 + 2] = (val >> 16) & 0xFF; - dst[f * 4 + 3] = (val >> 24) & 0xFF; - } - } - // Header: WDBC magic + 4 uint32s (recordCount, fieldCount, - // recordSize, stringBlockSize). - std::ofstream out(outPath, std::ios::binary); - if (!out) { - std::fprintf(stderr, "convert-json-dbc: cannot write %s\n", outPath.c_str()); - return 1; - } - uint32_t header[5] = { - 0x43424457u, // 'WDBC' little-endian - recordCount, fieldCount, recordSize, - static_cast(stringBlock.size()) - }; - out.write(reinterpret_cast(header), sizeof(header)); - out.write(reinterpret_cast(recordBytes.data()), - recordBytes.size()); - out.write(reinterpret_cast(stringBlock.data()), - stringBlock.size()); - out.close(); - std::printf("Converted %s -> %s\n", jsonPath.c_str(), outPath.c_str()); - std::printf(" %u records x %u fields, %zu-byte string block\n", - recordCount, fieldCount, stringBlock.size()); - if (convertErrors > 0) { - std::printf(" warning: %d cell(s) had unrecognized types\n", convertErrors); - } - return 0; - } - if (std::strcmp(argv[i], "--convert-blp-png") == 0 && i + 1 < argc) { - // Standalone BLP -> PNG conversion. Same code path as - // asset_extract --emit-open's per-file walker, but for one - // texture without re-running a full extraction. - std::string blpPath = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') { - outPath = argv[++i]; - } - if (outPath.empty()) { - outPath = blpPath; - if (outPath.size() >= 4 && - outPath.substr(outPath.size() - 4) == ".blp") { - outPath = outPath.substr(0, outPath.size() - 4); - } - outPath += ".png"; - } - std::ifstream in(blpPath, std::ios::binary); - if (!in) { - std::fprintf(stderr, "convert-blp-png: cannot open %s\n", blpPath.c_str()); - return 1; - } - std::vector bytes((std::istreambuf_iterator(in)), - std::istreambuf_iterator()); - auto img = wowee::pipeline::BLPLoader::load(bytes); - if (!img.isValid()) { - std::fprintf(stderr, "convert-blp-png: failed to decode %s\n", - blpPath.c_str()); - return 1; - } - // Same dimension/buffer-size guards as the asset_extract - // emitter 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) { - std::fprintf(stderr, "convert-blp-png: invalid dimensions or data (%dx%d, %zu bytes)\n", - img.width, img.height, img.data.size()); - return 1; - } - // Ensure output directory exists; fs::create_directories with - // an empty path is a no-op so we don't need to special-case - // 'png in cwd'. - std::filesystem::create_directories( - std::filesystem::path(outPath).parent_path()); - int rc = stbi_write_png(outPath.c_str(), - img.width, img.height, 4, - img.data.data(), img.width * 4); - if (!rc) { - std::fprintf(stderr, "convert-blp-png: stbi_write_png failed for %s\n", - outPath.c_str()); - return 1; - } - std::printf("Converted %s -> %s\n", blpPath.c_str(), outPath.c_str()); - std::printf(" %dx%d, %zu bytes (RGBA8)\n", - img.width, img.height, img.data.size()); - return 0; - } - } if (dataPath.empty()) { dataPath = "Data";