#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 #include #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" namespace wowee { namespace editor { std::vector TextureExporter::collectUsedTextures(const pipeline::ADTTerrain& terrain) { std::unordered_set unique; for (const auto& tex : terrain.textures) unique.insert(tex); std::vector result(unique.begin(), unique.end()); std::sort(result.begin(), result.end()); return result; } std::vector TextureExporter::collectM2Textures(pipeline::AssetManager* am, const std::string& m2Path) { std::vector 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 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 TextureExporter::collectWMOTextures(pipeline::AssetManager* am, const std::string& wmoPath) { std::vector 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 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 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& 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