mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat: complete client integration for all 6 open formats
- Wire WOB buildings into WMO render pipeline (loads→converts→renders) - Implement JSON DBC loading in DBCFile::loadJSON() with nlohmann/json - Wire JSON DBC override into AssetManager (custom_zones/output scan) - Add WMO→WOB conversion with full geometry (fromWMO) - Replace placeholder WOB export with real WMO→WOB conversion in editor - Add --convert-wmo CLI flag for batch WMO→WOB conversion - Store discovered custom zones on Renderer with getCustomZones() accessor - Add isCustomZone_ member to TerrainManager All 6 Blizzard format replacements now fully load in the client: ADT→WOT/WHM, WDT→zone.json, BLP→PNG, DBC→JSON, M2→WOM, WMO→WOB
This commit is contained in:
parent
d8f2388635
commit
4fc0361f7a
11 changed files with 271 additions and 47 deletions
|
|
@ -312,23 +312,30 @@ std::shared_ptr<DBCFile> AssetManager::loadDBC(const std::string& name) {
|
|||
}
|
||||
|
||||
// Check for JSON DBC from custom zones (wowee open format)
|
||||
// JSON DBCs exported by the editor contain the same record data
|
||||
// but the DBCFile::load() only handles binary — so JSON overrides
|
||||
// are logged for now and will need a JSON→DBC converter in future.
|
||||
if (dbcData.empty()) {
|
||||
std::string baseName = name;
|
||||
auto dot = baseName.rfind('.');
|
||||
if (dot != std::string::npos) baseName = baseName.substr(0, dot);
|
||||
for (const std::string& dir : {"custom_zones", "output"}) {
|
||||
for (const char* dir : {"custom_zones", "output"}) {
|
||||
if (!std::filesystem::exists(dir)) continue;
|
||||
for (auto& entry : std::filesystem::directory_iterator(dir)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
std::string jsonPath = entry.path().string() + "/data/" + baseName + ".json";
|
||||
if (std::filesystem::exists(jsonPath)) {
|
||||
LOG_DEBUG("JSON DBC available (not yet loaded): ", jsonPath);
|
||||
std::ifstream jf(jsonPath, std::ios::binary | std::ios::ate);
|
||||
if (jf) {
|
||||
auto sz = jf.tellg();
|
||||
if (sz > 0) {
|
||||
dbcData.resize(static_cast<size_t>(sz));
|
||||
jf.seekg(0);
|
||||
jf.read(reinterpret_cast<char*>(dbcData.data()), sz);
|
||||
LOG_INFO("Loading JSON DBC override: ", jsonPath);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dbcData.empty()) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
|
|
@ -37,6 +38,15 @@ bool DBCFile::load(const std::vector<uint8_t>& dbcData) {
|
|||
return loadCSV(dbcData);
|
||||
}
|
||||
|
||||
// Detect JSON format: starts with '{'
|
||||
if (dbcData[0] == '{' || (dbcData[0] <= ' ' && dbcData.size() > 1)) {
|
||||
size_t start = 0;
|
||||
while (start < dbcData.size() && dbcData[start] <= ' ') start++;
|
||||
if (start < dbcData.size() && dbcData[start] == '{') {
|
||||
return loadJSON(dbcData);
|
||||
}
|
||||
}
|
||||
|
||||
if (dbcData.size() < sizeof(DBCHeader)) {
|
||||
LOG_ERROR("DBC data too small for header");
|
||||
return false;
|
||||
|
|
@ -368,5 +378,74 @@ bool DBCFile::loadCSV(const std::vector<uint8_t>& csvData) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool DBCFile::loadJSON(const std::vector<uint8_t>& jsonData) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(jsonData.begin(), jsonData.end());
|
||||
|
||||
if (!j.contains("records") || !j["records"].is_array()) {
|
||||
LOG_ERROR("JSON DBC: missing 'records' array");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& records = j["records"];
|
||||
if (records.empty()) {
|
||||
LOG_WARNING("JSON DBC: empty records array");
|
||||
return false;
|
||||
}
|
||||
|
||||
fieldCount = j.value("fieldCount", 0u);
|
||||
if (fieldCount == 0 && !records[0].empty()) {
|
||||
fieldCount = static_cast<uint32_t>(records[0].size());
|
||||
}
|
||||
if (fieldCount == 0) return false;
|
||||
|
||||
recordSize = fieldCount * 4;
|
||||
recordCount = static_cast<uint32_t>(records.size());
|
||||
|
||||
stringBlock.clear();
|
||||
stringBlock.push_back(0);
|
||||
|
||||
recordData.resize(static_cast<size_t>(recordCount) * recordSize, 0);
|
||||
|
||||
for (uint32_t i = 0; i < recordCount; i++) {
|
||||
const auto& row = records[i];
|
||||
uint32_t* fields = reinterpret_cast<uint32_t*>(
|
||||
recordData.data() + static_cast<size_t>(i) * recordSize);
|
||||
|
||||
uint32_t cols = std::min(fieldCount, static_cast<uint32_t>(row.size()));
|
||||
for (uint32_t col = 0; col < cols; col++) {
|
||||
const auto& val = row[col];
|
||||
if (val.is_string()) {
|
||||
const std::string& str = val.get_ref<const std::string&>();
|
||||
if (str.empty()) {
|
||||
fields[col] = 0;
|
||||
} else {
|
||||
fields[col] = static_cast<uint32_t>(stringBlock.size());
|
||||
stringBlock.insert(stringBlock.end(), str.begin(), str.end());
|
||||
stringBlock.push_back(0);
|
||||
}
|
||||
} else if (val.is_number_float()) {
|
||||
float f = val.get<float>();
|
||||
std::memcpy(&fields[col], &f, 4);
|
||||
} else if (val.is_number_integer()) {
|
||||
fields[col] = val.get<uint32_t>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stringBlockSize = static_cast<uint32_t>(stringBlock.size());
|
||||
loaded = true;
|
||||
idCacheBuilt = false;
|
||||
idToIndexCache.clear();
|
||||
|
||||
LOG_INFO("Loaded JSON DBC: ", recordCount, " records, ",
|
||||
fieldCount, " fields, ", stringBlockSize, " string bytes");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("JSON DBC parse error: ", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -196,5 +196,69 @@ bool WoweeBuildingLoader::toWMOModel(const WoweeBuilding& building, WMOModel& ou
|
|||
return true;
|
||||
}
|
||||
|
||||
WoweeBuilding WoweeBuildingLoader::fromWMO(const WMOModel& wmo, const std::string& name) {
|
||||
WoweeBuilding bld;
|
||||
bld.name = name.empty() ? "Converted WMO" : name;
|
||||
|
||||
float maxDist = 0.0f;
|
||||
for (const auto& grp : wmo.groups) {
|
||||
WoweeBuilding::Group wobGroup;
|
||||
wobGroup.name = grp.name;
|
||||
wobGroup.isOutdoor = (grp.flags & 0x08) != 0;
|
||||
wobGroup.boundMin = grp.boundingBoxMin;
|
||||
wobGroup.boundMax = grp.boundingBoxMax;
|
||||
|
||||
wobGroup.vertices.reserve(grp.vertices.size());
|
||||
for (const auto& v : grp.vertices) {
|
||||
WoweeBuilding::Vertex wv;
|
||||
wv.position = v.position;
|
||||
wv.normal = v.normal;
|
||||
wv.texCoord = v.texCoord;
|
||||
wv.color = v.color;
|
||||
wobGroup.vertices.push_back(wv);
|
||||
|
||||
float d = glm::length(v.position);
|
||||
if (d > maxDist) maxDist = d;
|
||||
}
|
||||
|
||||
wobGroup.indices.reserve(grp.indices.size());
|
||||
for (uint16_t idx : grp.indices)
|
||||
wobGroup.indices.push_back(static_cast<uint32_t>(idx));
|
||||
|
||||
for (const auto& mat : wmo.materials) {
|
||||
if (mat.texture1 < wmo.textures.size()) {
|
||||
std::string texPath = wmo.textures[mat.texture1];
|
||||
auto dot = texPath.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
texPath = texPath.substr(0, dot) + ".png";
|
||||
wobGroup.texturePaths.push_back(texPath);
|
||||
}
|
||||
}
|
||||
|
||||
bld.groups.push_back(std::move(wobGroup));
|
||||
}
|
||||
|
||||
bld.boundRadius = maxDist;
|
||||
|
||||
for (const auto& doodad : wmo.doodads) {
|
||||
auto nameIt = wmo.doodadNames.find(doodad.nameIndex);
|
||||
if (nameIt == wmo.doodadNames.end()) continue;
|
||||
|
||||
WoweeBuilding::DoodadPlacement dp;
|
||||
dp.modelPath = nameIt->second;
|
||||
auto dot = dp.modelPath.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
dp.modelPath = dp.modelPath.substr(0, dot) + ".wom";
|
||||
dp.position = doodad.position;
|
||||
dp.rotation = glm::vec3(0.0f);
|
||||
dp.scale = doodad.scale;
|
||||
bld.doodads.push_back(dp);
|
||||
}
|
||||
|
||||
LOG_INFO("WOB from WMO: ", bld.name, " (", bld.groups.size(), " groups, ",
|
||||
bld.doodads.size(), " doodads)");
|
||||
return bld;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -1887,13 +1887,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
|
|||
LOG_INFO("Initializing renderers for map: ", mapName);
|
||||
|
||||
// Scan for custom zones on first initialization
|
||||
static bool customZonesScanned = false;
|
||||
if (!customZonesScanned) {
|
||||
customZonesScanned = true;
|
||||
auto customZones = pipeline::CustomZoneDiscovery::scan({"custom_zones", "output"});
|
||||
if (!customZones.empty()) {
|
||||
if (customZones_.empty()) {
|
||||
customZones_ = pipeline::CustomZoneDiscovery::scan({"custom_zones", "output"});
|
||||
if (!customZones_.empty()) {
|
||||
LOG_INFO("=== Custom Zones Available ===");
|
||||
for (const auto& z : customZones) {
|
||||
for (const auto& z : customZones_) {
|
||||
LOG_INFO(" ", z.name, " (", z.directory, ")",
|
||||
z.hasCreatures ? " [NPCs]" : "",
|
||||
z.hasQuests ? " [Quests]" : "");
|
||||
|
|
|
|||
|
|
@ -597,6 +597,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
|
||||
|
||||
// Check for WOB open format first (custom zone buildings)
|
||||
bool wobLoaded = false;
|
||||
pipeline::WMOModel wmoModel;
|
||||
{
|
||||
std::string wobBase = wmoPath;
|
||||
auto wobDot = wobBase.rfind('.');
|
||||
|
|
@ -607,10 +609,9 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
if (pipeline::WoweeBuildingLoader::exists(prefix + wobBase)) {
|
||||
auto wob = pipeline::WoweeBuildingLoader::load(prefix + wobBase);
|
||||
if (wob.isValid()) {
|
||||
pipeline::WMOModel wobAsWmo;
|
||||
if (pipeline::WoweeBuildingLoader::toWMOModel(wob, wobAsWmo)) {
|
||||
if (pipeline::WoweeBuildingLoader::toWMOModel(wob, wmoModel)) {
|
||||
LOG_INFO("Loaded WOB building: ", prefix + wobBase);
|
||||
// TODO: feed wobAsWmo into the WMO render pipeline
|
||||
wobLoaded = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -618,37 +619,39 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
|
||||
if (wmoData.empty()) continue;
|
||||
if (!wobLoaded) {
|
||||
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
|
||||
if (wmoData.empty()) continue;
|
||||
|
||||
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||
if (wmoModel.nGroups > 0) {
|
||||
std::string basePath = wmoPath;
|
||||
std::string extension;
|
||||
if (basePath.size() > 4) {
|
||||
extension = basePath.substr(basePath.size() - 4);
|
||||
std::string extLower = extension;
|
||||
for (char& c : extLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (extLower == ".wmo") {
|
||||
basePath = basePath.substr(0, basePath.size() - 4);
|
||||
wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||
if (wmoModel.nGroups > 0) {
|
||||
std::string basePath = wmoPath;
|
||||
std::string extension;
|
||||
if (basePath.size() > 4) {
|
||||
extension = basePath.substr(basePath.size() - 4);
|
||||
std::string extLower = extension;
|
||||
for (char& c : extLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (extLower == ".wmo") {
|
||||
basePath = basePath.substr(0, basePath.size() - 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
||||
char groupSuffix[16];
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
||||
std::string groupPath = basePath + groupSuffix;
|
||||
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (!groupData.empty()) {
|
||||
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
||||
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
||||
char groupSuffix[16];
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
||||
std::string groupPath = basePath + groupSuffix;
|
||||
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (!groupData.empty()) {
|
||||
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue