#include "content_pack.hpp" #include "core/logger.hpp" #include #include #include #include namespace wowee { namespace editor { // WCP file format (simple concatenated archive): // Header: "WCP1" (4 bytes) + fileCount (4) + infoJsonSize (4) // Info JSON block (infoJsonSize bytes) // File table: for each file: pathLen(2) + path(pathLen) + dataSize(4) // File data: concatenated file contents static constexpr uint32_t WCP_MAGIC = 0x31504357; // "WCP1" bool ContentPacker::packZone(const std::string& outputDir, const std::string& mapName, const std::string& destPath, const ContentPackInfo& info) { namespace fs = std::filesystem; std::string srcDir = outputDir + "/" + mapName; if (!fs::exists(srcDir)) { LOG_ERROR("Source directory not found: ", srcDir); return false; } // Collect all files std::vector> files; // relative path, full path for (auto& entry : fs::recursive_directory_iterator(srcDir)) { if (!entry.is_regular_file()) continue; std::string rel = fs::relative(entry.path(), srcDir).string(); files.push_back({rel, entry.path().string()}); } if (files.empty()) { LOG_ERROR("No files to pack in: ", srcDir); return false; } // Build info JSON nlohmann::json infoObj; infoObj["format"] = info.format; infoObj["name"] = info.name; infoObj["author"] = info.author; infoObj["description"] = info.description; infoObj["version"] = info.version; infoObj["mapId"] = info.mapId; infoObj["fileCount"] = files.size(); nlohmann::json fileArr = nlohmann::json::array(); for (const auto& [rel, full] : files) { fileArr.push_back({{"path", rel}, {"size", fs::file_size(full)}}); } infoObj["files"] = fileArr; std::string infoJson = infoObj.dump(2); // Write WCP file std::ofstream out(destPath, std::ios::binary); if (!out) { LOG_ERROR("Failed to create pack file: ", destPath); return false; } // Header out.write(reinterpret_cast(&WCP_MAGIC), 4); uint32_t fileCount = static_cast(files.size()); out.write(reinterpret_cast(&fileCount), 4); uint32_t infoSize = static_cast(infoJson.size()); out.write(reinterpret_cast(&infoSize), 4); // Info JSON out.write(infoJson.data(), infoJson.size()); // File table + data for (const auto& [rel, full] : files) { uint16_t pathLen = static_cast(rel.size()); out.write(reinterpret_cast(&pathLen), 2); out.write(rel.data(), pathLen); std::ifstream fin(full, std::ios::binary | std::ios::ate); uint32_t dataSize = static_cast(fin.tellg()); fin.seekg(0); out.write(reinterpret_cast(&dataSize), 4); std::vector buf(dataSize); fin.read(buf.data(), dataSize); out.write(buf.data(), dataSize); } LOG_INFO("Content pack created: ", destPath, " (", files.size(), " files, ", out.tellp(), " bytes)"); return true; } bool ContentPacker::unpackZone(const std::string& wcpPath, const std::string& destDir) { std::ifstream in(wcpPath, std::ios::binary); if (!in) return false; uint32_t magic; in.read(reinterpret_cast(&magic), 4); if (magic != WCP_MAGIC) { LOG_ERROR("Not a WCP file: ", wcpPath); return false; } uint32_t fileCount, infoSize; in.read(reinterpret_cast(&fileCount), 4); in.read(reinterpret_cast(&infoSize), 4); // Skip info JSON in.seekg(infoSize, std::ios::cur); namespace fs = std::filesystem; fs::create_directories(destDir); for (uint32_t i = 0; i < fileCount; i++) { uint16_t pathLen; in.read(reinterpret_cast(&pathLen), 2); std::string path(pathLen, '\0'); in.read(path.data(), pathLen); uint32_t dataSize; in.read(reinterpret_cast(&dataSize), 4); std::vector data(dataSize); in.read(data.data(), dataSize); std::string fullPath = destDir + "/" + path; fs::create_directories(fs::path(fullPath).parent_path()); std::ofstream fout(fullPath, std::ios::binary); fout.write(data.data(), dataSize); } LOG_INFO("Content pack extracted to: ", destDir, " (", fileCount, " files)"); return true; } bool ContentPacker::readInfo(const std::string& wcpPath, ContentPackInfo& info) { std::ifstream in(wcpPath, std::ios::binary); if (!in) return false; uint32_t magic; in.read(reinterpret_cast(&magic), 4); if (magic != WCP_MAGIC) return false; uint32_t fileCount, infoSize; in.read(reinterpret_cast(&fileCount), 4); in.read(reinterpret_cast(&infoSize), 4); std::string jsonStr(infoSize, '\0'); in.read(jsonStr.data(), infoSize); try { auto j = nlohmann::json::parse(jsonStr); info.name = j.value("name", ""); info.author = j.value("author", ""); info.description = j.value("description", ""); info.version = j.value("version", ""); info.format = j.value("format", ""); info.mapId = j.value("mapId", 9000u); info.files.clear(); if (j.contains("files") && j["files"].is_array()) { for (const auto& jf : j["files"]) { ContentPackInfo::FileEntry fe; fe.path = jf.value("path", ""); fe.size = jf.value("size", 0ULL); auto dot = fe.path.rfind('.'); if (dot != std::string::npos) { std::string ext = fe.path.substr(dot); if (ext == ".wot" || ext == ".whm") fe.category = "terrain"; else if (ext == ".wom") fe.category = "model"; else if (ext == ".wob") fe.category = "building"; else if (ext == ".png") fe.category = "texture"; else if (ext == ".json") fe.category = "data"; else if (ext == ".adt" || ext == ".wdt") fe.category = "legacy"; else fe.category = "other"; } info.files.push_back(fe); } } } catch (...) { return false; } return true; } static bool checkMagic(const std::string& path, uint32_t expectedMagic) { std::ifstream f(path, std::ios::binary); if (!f) return false; uint32_t magic = 0; f.read(reinterpret_cast(&magic), 4); return magic == expectedMagic; } ContentPacker::ValidationResult ContentPacker::validateZone(const std::string& zoneDir) { namespace fs = std::filesystem; ValidationResult r; if (!fs::exists(zoneDir)) return r; static constexpr uint32_t WHM_MAGIC = 0x314D4857; // "WHM1" static constexpr uint32_t WOM_MAGIC = 0x314D4F57; // "WOM1" static constexpr uint32_t WOB_MAGIC = 0x31424F57; // "WOB1" static constexpr uint32_t WOC_MAGIC = 0x31434F57; // "WOC1" for (auto& entry : fs::recursive_directory_iterator(zoneDir)) { if (!entry.is_regular_file()) continue; std::string ext = entry.path().extension().string(); std::string fname = entry.path().filename().string(); if (ext == ".wot") r.hasWot = true; if (ext == ".whm") { r.hasWhm = true; if (checkMagic(entry.path().string(), WHM_MAGIC)) r.whmValid = true; } if (ext == ".wom") { r.hasWom = true; if (checkMagic(entry.path().string(), WOM_MAGIC)) r.womValid = true; } if (ext == ".wob") { r.hasWob = true; if (checkMagic(entry.path().string(), WOB_MAGIC)) r.wobValid = true; } if (ext == ".woc") { r.hasWoc = true; if (checkMagic(entry.path().string(), WOC_MAGIC)) r.wocValid = true; } if (ext == ".png") r.hasPng = true; if (fname == "zone.json") r.hasZoneJson = true; if (fname == "creatures.json") r.hasCreatures = true; if (fname == "quests.json") r.hasQuests = true; if (fname == "objects.json") r.hasObjects = true; } return r; } int ContentPacker::ValidationResult::openFormatScore() const { int score = 0; if (hasWot) score++; if (hasWhm && whmValid) score++; if (hasZoneJson) score++; if (hasPng) score++; if (hasWom && womValid) score++; if (hasWob && wobValid) score++; if (hasWoc && wocValid) score++; return score; // max 7 for fully open } std::string ContentPacker::ValidationResult::summary() const { std::string s; auto add = [&](bool has, bool valid, const char* name) { if (!has) return; s += name; if (!valid) s += "(!)"; s += " "; }; add(hasWot, true, "WOT"); add(hasWhm, whmValid, "WHM"); add(hasWom, womValid, "WOM"); add(hasWob, wobValid, "WOB"); add(hasWoc, wocValid, "WOC"); if (hasZoneJson) s += "zone.json "; if (hasPng) s += "PNG "; if (hasCreatures) s += "creatures "; if (hasQuests) s += "quests "; if (hasObjects) s += "objects "; return s.empty() ? "(empty)" : s; } } // namespace editor } // namespace wowee