mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 18:13:52 +00:00
feat(extract): emit open-format side-files (BLP→PNG, DBC→JSON)
The asset_extract tool now optionally writes wowee open-format
copies next to each extracted proprietary file:
--emit-png foo.blp → foo.png
--emit-json-dbc foo.dbc → foo.json
--emit-open shortcut for both
Originals are left untouched, so private servers (AzerothCore,
TrinityCore) that load from the manifest's .blp/.dbc paths
continue to work unchanged. The wowee runtime / editor can now
consume the open formats directly without an extra conversion pass.
Implementation:
- New tools/asset_extract/open_format_emitter.{hpp,cpp} encapsulates
the post-extract walk + per-file conversion.
- BLP→PNG uses BLPLoader::load + stbi_write_png with the same
dimension/buffer-size sanity guards the editor's texture exporter
applies.
- DBC→JSON mirrors the editor's DBCExporter::exportAsJson schema
(string/float/uint heuristic) so the runtime DBC overlay loader
can consume the output drop-in.
This commit is contained in:
parent
9e801f93b6
commit
5ed2008621
6 changed files with 212 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "extractor.hpp"
|
||||
#include "path_mapper.hpp"
|
||||
#include "manifest_writer.hpp"
|
||||
#include "open_format_emitter.hpp"
|
||||
|
||||
#include <StormLib.h>
|
||||
|
||||
|
|
@ -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" };
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ public:
|
|||
std::string dbcCsvOutputDir; // When set, write CSVs into this directory instead of outputDir/expansions/<exp>/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 {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ static void printUsage(const char* prog) {
|
|||
<< " --reference-manifest <path>\n"
|
||||
<< " Only extract files NOT in this manifest (delta extraction)\n"
|
||||
<< " --dbc-csv-out <dir> Write CSV DBCs into <dir> (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 <N> 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) {
|
||||
|
|
|
|||
132
tools/asset_extract/open_format_emitter.cpp
Normal file
132
tools/asset_extract/open_format_emitter.cpp
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#include "open_format_emitter.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace wowee {
|
||||
namespace tools {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::vector<uint8_t> 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<uint8_t> buf(static_cast<size_t>(sz));
|
||||
f.seekg(0);
|
||||
f.read(reinterpret_cast<char*>(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<size_t>(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<uint32_t>(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<char>(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
|
||||
40
tools/asset_extract/open_format_emitter.hpp
Normal file
40
tools/asset_extract/open_format_emitter.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <cstdint>
|
||||
|
||||
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/<zone>/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
|
||||
Loading…
Add table
Add a link
Reference in a new issue