From 79ae91a6d527dd12ae6f060b8a6e88da5692c914 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 09:27:00 -0700 Subject: [PATCH] feat(editor): Wowee Content Pack (.wcp) format for zone distribution - WCP format: simple binary archive with magic header, JSON manifest, and concatenated file data. Not tied to any proprietary format. - ContentPacker::packZone(): bundles all zone files from output dir into a single .wcp file (terrain, objects, creatures, quests, manifest) - ContentPacker::unpackZone(): extracts .wcp to a directory - ContentPacker::readInfo(): reads pack metadata without extracting - Format: "WCP1" magic + fileCount + infoJSON + file table + data - Foundation for distributing custom zones to other wowee users and private servers Note: currently bundles ADT/WDT files as-is. Future: convert terrain to open format (heightmap + JSON) for fully open redistribution. --- CMakeLists.txt | 1 + tools/editor/content_pack.cpp | 175 ++++++++++++++++++++++++++++++++++ tools/editor/content_pack.hpp | 40 ++++++++ 3 files changed, 216 insertions(+) create mode 100644 tools/editor/content_pack.cpp create mode 100644 tools/editor/content_pack.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index abec4dc8..2bccc893 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1294,6 +1294,7 @@ add_executable(wowee_editor tools/editor/quest_editor.cpp tools/editor/transform_gizmo.cpp tools/editor/zone_manifest.cpp + tools/editor/content_pack.cpp tools/editor/asset_browser.cpp tools/editor/editor_water.cpp tools/editor/editor_markers.cpp diff --git a/tools/editor/content_pack.cpp b/tools/editor/content_pack.cpp new file mode 100644 index 00000000..24ad9bb8 --- /dev/null +++ b/tools/editor/content_pack.cpp @@ -0,0 +1,175 @@ +#include "content_pack.hpp" +#include "core/logger.hpp" +#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 + std::string infoJson = "{\n"; + infoJson += " \"format\": \"" + info.format + "\",\n"; + infoJson += " \"name\": \"" + info.name + "\",\n"; + infoJson += " \"author\": \"" + info.author + "\",\n"; + infoJson += " \"description\": \"" + info.description + "\",\n"; + infoJson += " \"version\": \"" + info.version + "\",\n"; + infoJson += " \"mapId\": " + std::to_string(info.mapId) + ",\n"; + infoJson += " \"fileCount\": " + std::to_string(files.size()) + ",\n"; + infoJson += " \"files\": [\n"; + for (size_t i = 0; i < files.size(); i++) { + auto fsize = fs::file_size(files[i].second); + infoJson += " {\"path\": \"" + files[i].first + "\", \"size\": " + std::to_string(fsize) + "}"; + if (i + 1 < files.size()) infoJson += ","; + infoJson += "\n"; + } + infoJson += " ]\n}\n"; + + // 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 json(infoSize, '\0'); + in.read(json.data(), infoSize); + + // Parse basic fields + auto findStr = [&](const std::string& key) -> std::string { + auto pos = json.find("\"" + key + "\""); + if (pos == std::string::npos) return ""; + pos = json.find('"', json.find(':', pos) + 1); + if (pos == std::string::npos) return ""; + auto end = json.find('"', pos + 1); + return json.substr(pos + 1, end - pos - 1); + }; + + info.name = findStr("name"); + info.author = findStr("author"); + info.description = findStr("description"); + info.version = findStr("version"); + info.format = findStr("format"); + + return true; +} + +} // namespace editor +} // namespace wowee diff --git a/tools/editor/content_pack.hpp b/tools/editor/content_pack.hpp new file mode 100644 index 00000000..9f7edda7 --- /dev/null +++ b/tools/editor/content_pack.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace editor { + +struct ContentPackInfo { + std::string name; + std::string author; + std::string description; + std::string version = "1.0"; + uint32_t mapId = 9000; + std::string format = "wcp-1.0"; + + struct FileEntry { + std::string path; // path inside pack + std::string category; // terrain, object, creature, quest, texture, model + uint64_t size = 0; + }; + std::vector files; +}; + +class ContentPacker { +public: + // Pack all zone data from output directory into a .wcp file + static bool packZone(const std::string& outputDir, const std::string& mapName, + const std::string& destPath, const ContentPackInfo& info); + + // Unpack a .wcp file to a directory + static bool unpackZone(const std::string& wcpPath, const std::string& destDir); + + // Read pack info without extracting + static bool readInfo(const std::string& wcpPath, ContentPackInfo& info); +}; + +} // namespace editor +} // namespace wowee