mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
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.
This commit is contained in:
parent
f5d8dcf75a
commit
79ae91a6d5
3 changed files with 216 additions and 0 deletions
175
tools/editor/content_pack.cpp
Normal file
175
tools/editor/content_pack.cpp
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#include "content_pack.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
||||
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<std::pair<std::string, std::string>> 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<const char*>(&WCP_MAGIC), 4);
|
||||
uint32_t fileCount = static_cast<uint32_t>(files.size());
|
||||
out.write(reinterpret_cast<const char*>(&fileCount), 4);
|
||||
uint32_t infoSize = static_cast<uint32_t>(infoJson.size());
|
||||
out.write(reinterpret_cast<const char*>(&infoSize), 4);
|
||||
|
||||
// Info JSON
|
||||
out.write(infoJson.data(), infoJson.size());
|
||||
|
||||
// File table + data
|
||||
for (const auto& [rel, full] : files) {
|
||||
uint16_t pathLen = static_cast<uint16_t>(rel.size());
|
||||
out.write(reinterpret_cast<const char*>(&pathLen), 2);
|
||||
out.write(rel.data(), pathLen);
|
||||
|
||||
std::ifstream fin(full, std::ios::binary | std::ios::ate);
|
||||
uint32_t dataSize = static_cast<uint32_t>(fin.tellg());
|
||||
fin.seekg(0);
|
||||
out.write(reinterpret_cast<const char*>(&dataSize), 4);
|
||||
|
||||
std::vector<char> 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<char*>(&magic), 4);
|
||||
if (magic != WCP_MAGIC) {
|
||||
LOG_ERROR("Not a WCP file: ", wcpPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t fileCount, infoSize;
|
||||
in.read(reinterpret_cast<char*>(&fileCount), 4);
|
||||
in.read(reinterpret_cast<char*>(&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<char*>(&pathLen), 2);
|
||||
std::string path(pathLen, '\0');
|
||||
in.read(path.data(), pathLen);
|
||||
|
||||
uint32_t dataSize;
|
||||
in.read(reinterpret_cast<char*>(&dataSize), 4);
|
||||
|
||||
std::vector<char> 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<char*>(&magic), 4);
|
||||
if (magic != WCP_MAGIC) return false;
|
||||
|
||||
uint32_t fileCount, infoSize;
|
||||
in.read(reinterpret_cast<char*>(&fileCount), 4);
|
||||
in.read(reinterpret_cast<char*>(&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
|
||||
40
tools/editor/content_pack.hpp
Normal file
40
tools/editor/content_pack.hpp
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
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<FileEntry> 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue