mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Character body/skin textures live in CharSections-composed paths that don't exist as standalone BLPs. Exporting a zone with many character NPCs would spam hundreds of warnings. Now logs the first 5, suppresses the rest, and reports the total skipped count in the summary line.
138 lines
5.2 KiB
C++
138 lines
5.2 KiB
C++
#include "texture_exporter.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "pipeline/blp_loader.hpp"
|
|
#include "pipeline/m2_loader.hpp"
|
|
#include "pipeline/wmo_loader.hpp"
|
|
#include "core/logger.hpp"
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#include "stb_image_write.h"
|
|
|
|
namespace wowee {
|
|
namespace editor {
|
|
|
|
std::vector<std::string> TextureExporter::collectUsedTextures(const pipeline::ADTTerrain& terrain) {
|
|
std::unordered_set<std::string> unique;
|
|
for (const auto& tex : terrain.textures)
|
|
unique.insert(tex);
|
|
std::vector<std::string> result(unique.begin(), unique.end());
|
|
std::sort(result.begin(), result.end());
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> TextureExporter::collectM2Textures(pipeline::AssetManager* am,
|
|
const std::string& m2Path) {
|
|
std::vector<std::string> out;
|
|
if (!am || m2Path.empty()) return out;
|
|
|
|
auto data = am->readFile(m2Path);
|
|
if (data.empty()) return out;
|
|
auto m2 = pipeline::M2Loader::load(data);
|
|
// Skin file holds geometry but textures live in the M2 header itself.
|
|
// Even if isValid() is false (no skin loaded), the texture list is populated.
|
|
|
|
std::unordered_set<std::string> unique;
|
|
for (const auto& tex : m2.textures) {
|
|
if (tex.filename.empty()) continue;
|
|
std::string p = tex.filename;
|
|
std::transform(p.begin(), p.end(), p.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
unique.insert(p);
|
|
}
|
|
out.assign(unique.begin(), unique.end());
|
|
std::sort(out.begin(), out.end());
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::string> TextureExporter::collectWMOTextures(pipeline::AssetManager* am,
|
|
const std::string& wmoPath) {
|
|
std::vector<std::string> out;
|
|
if (!am || wmoPath.empty()) return out;
|
|
|
|
auto rootData = am->readFile(wmoPath);
|
|
if (rootData.empty()) return out;
|
|
auto wmo = pipeline::WMOLoader::load(rootData);
|
|
|
|
// Load group files so any group-only texture references are populated too.
|
|
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 = am->readFile(base + suffix);
|
|
if (!gd.empty()) pipeline::WMOLoader::loadGroup(gd, wmo, gi);
|
|
}
|
|
|
|
std::unordered_set<std::string> unique;
|
|
for (const auto& tex : wmo.textures) {
|
|
if (tex.empty()) continue;
|
|
std::string p = tex;
|
|
std::transform(p.begin(), p.end(), p.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
unique.insert(p);
|
|
}
|
|
|
|
// WMO doodads (props inside the building) are M2 models — their textures
|
|
// also need to ship with the zone or the building will render with missing
|
|
// chairs/decorations.
|
|
std::unordered_set<std::string> seenDoodadM2;
|
|
for (const auto& [offset, name] : wmo.doodadNames) {
|
|
(void)offset;
|
|
if (name.empty() || !seenDoodadM2.insert(name).second) continue;
|
|
for (auto& t : collectM2Textures(am, name)) unique.insert(std::move(t));
|
|
}
|
|
|
|
out.assign(unique.begin(), unique.end());
|
|
std::sort(out.begin(), out.end());
|
|
return out;
|
|
}
|
|
|
|
int TextureExporter::exportTexturesAsPng(pipeline::AssetManager* am,
|
|
const std::vector<std::string>& texturePaths,
|
|
const std::string& outputDir) {
|
|
namespace fs = std::filesystem;
|
|
int exported = 0;
|
|
|
|
int notFound = 0;
|
|
for (const auto& texPath : texturePaths) {
|
|
auto blpImage = am->loadTexture(texPath);
|
|
if (!blpImage.isValid()) {
|
|
// Many character/dynamic textures legitimately don't exist as files
|
|
// (composed at runtime from CharSections.dbc); don't spam.
|
|
++notFound;
|
|
if (notFound <= 5) LOG_WARNING("Texture not found or invalid: ", texPath);
|
|
continue;
|
|
}
|
|
|
|
// Build output path: replace backslashes, change .blp to .png
|
|
std::string outPath = texPath;
|
|
std::replace(outPath.begin(), outPath.end(), '\\', '/');
|
|
// Lowercase
|
|
std::transform(outPath.begin(), outPath.end(), outPath.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
// Change extension
|
|
auto dotPos = outPath.rfind('.');
|
|
if (dotPos != std::string::npos)
|
|
outPath = outPath.substr(0, dotPos) + ".png";
|
|
|
|
std::string fullPath = outputDir + "/" + outPath;
|
|
fs::create_directories(fs::path(fullPath).parent_path());
|
|
|
|
// Write RGBA data as PNG
|
|
if (stbi_write_png(fullPath.c_str(), blpImage.width, blpImage.height, 4,
|
|
blpImage.data.data(), blpImage.width * 4)) {
|
|
exported++;
|
|
} else {
|
|
LOG_WARNING("Failed to write PNG: ", fullPath);
|
|
}
|
|
}
|
|
|
|
LOG_INFO("Exported ", exported, "/", texturePaths.size(), " textures as PNG to ", outputDir,
|
|
notFound > 5 ? " (" + std::to_string(notFound - 5) + " more not-found warnings suppressed)" : "");
|
|
return exported;
|
|
}
|
|
|
|
} // namespace editor
|
|
} // namespace wowee
|