feat(pipeline): add WOMX (Wowee World Map index) format

Novel open replacement for Blizzard's WDT (top-level world
definition table). The 9th open format added to the editor.

A WOMX file holds the manifest of which terrain tiles exist
within a world plus a tiny bit of map-level metadata. The
runtime consults it before attempting to load any individual
tile (so missing tiles produce a clean "no data" result
instead of a file-not-found error).

Format:
  • magic "WMPX", version 1, little-endian
  • mapName + worldType (continent/instance/battleground/arena)
  • gridSize 1..128 (typically 64 for continents)
  • defaultLightId / defaultWeatherId (atmosphere preset
    refs, 0 if none — wires into the WOL/WOW pair)
  • packed bitmap, 1 bit per tile, row-major
  • A 64x64 manifest is exactly 512 bytes of bitmap

API: WoweeWorldMapLoader::save / load / exists; presets
makeContinent (64x64 full), makeInstance (4x4 full),
makeArena (1x1 full).

CLI added (5 flags, 456 total now):
  --gen-world-map <base> [name]            (continent)
  --gen-world-map-instance <base> [name]   (4x4)
  --gen-world-map-arena <base> [name]      (1x1)
  --info-womx <base> [--json]
  --validate-womx <base> [--json]

Round-trip verified: continent + instance + arena presets
all save / load / re-validate to byte-identical state with
correct tile counts.
This commit is contained in:
Kelsi 2026-05-09 14:38:05 -07:00
parent 6c3f5cb33f
commit db47f00657
8 changed files with 527 additions and 0 deletions

View file

@ -596,6 +596,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_collision.cpp
src/pipeline/wowee_light.cpp
src/pipeline/wowee_weather.cpp
src/pipeline/wowee_world_map.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1338,6 +1339,7 @@ add_executable(wowee_editor
tools/editor/cli_info_density.cpp
tools/editor/cli_info_audio.cpp
tools/editor/cli_world_info.cpp
tools/editor/cli_world_map.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1412,6 +1414,7 @@ add_executable(wowee_editor
src/pipeline/wowee_collision.cpp
src/pipeline/wowee_light.cpp
src/pipeline/wowee_weather.cpp
src/pipeline/wowee_world_map.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,82 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open World Map index (.womx) — novel replacement for
// Blizzard's WDT (top-level world definition table). A WOMX file
// holds the manifest of which terrain tiles exist within a world,
// plus a tiny bit of map-level metadata. The runtime consults it
// before attempting to load any individual tile (so missing tiles
// produce a clean "no data" instead of a file-not-found error).
//
// The grid is a square of size N×N where N is typically 64 (the
// historical WoW value), but the format permits any N up to 128.
// Tile presence is stored as a packed bitmap (1 bit per tile,
// row-major), so a 64×64 manifest is only 512 bytes.
//
// Binary layout (little-endian):
// magic[4] = "WMPX"
// version (uint32) = current 1
// nameLen (uint32) + name bytes
// worldType (uint8) = 0=continent, 1=instance, 2=battleground, 3=arena
// gridSize (uint8) = N (1..128)
// pad[2]
// defaultLightId (uint32) -- 0 if no atmosphere preset
// defaultWeatherId (uint32) -- 0 if no atmosphere preset
// reserved[3] (uint32 each)
// bitmapBytes (uint32) = ceil(N*N/8)
// bitmap (bitmapBytes)
struct WoweeWorldMap {
enum WorldType : uint8_t {
Continent = 0,
Instance = 1,
Battleground = 2,
Arena = 3,
};
std::string name;
uint8_t worldType = Continent;
uint8_t gridSize = 64;
uint32_t defaultLightId = 0;
uint32_t defaultWeatherId = 0;
// Packed row-major bitmap: bit (y * gridSize + x) is set
// when a tile exists at column x, row y.
std::vector<uint8_t> tileBitmap;
bool isValid() const { return gridSize > 0 && gridSize <= 128; }
bool hasTile(uint32_t x, uint32_t y) const;
void setTile(uint32_t x, uint32_t y, bool present);
// Count of set bits in the bitmap (= number of present tiles).
uint32_t countTiles() const;
static const char* worldTypeName(uint8_t t);
};
class WoweeWorldMapLoader {
public:
static bool save(const WoweeWorldMap& map,
const std::string& basePath);
static WoweeWorldMap load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-world-map* variants.
//
// makeContinent — full 64×64 grid with all tiles present
// (continent-style world map, ~4.3 km²)
// makeInstance — small 4×4 grid for dungeon-scale worlds
// makeArena — 1×1 single-tile arena
static WoweeWorldMap makeContinent(const std::string& mapName);
static WoweeWorldMap makeInstance(const std::string& mapName);
static WoweeWorldMap makeArena(const std::string& mapName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,207 @@
#include "pipeline/wowee_world_map.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'M', 'P', 'X'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
std::string normalizePath(std::string base) {
if (base.size() < 5 || base.substr(base.size() - 5) != ".womx") {
base += ".womx";
}
return base;
}
size_t bitmapBytesFor(uint32_t gridSize) {
return (static_cast<size_t>(gridSize) * gridSize + 7) / 8;
}
} // namespace
bool WoweeWorldMap::hasTile(uint32_t x, uint32_t y) const {
if (x >= gridSize || y >= gridSize) return false;
size_t bit = static_cast<size_t>(y) * gridSize + x;
size_t byte = bit / 8;
if (byte >= tileBitmap.size()) return false;
return (tileBitmap[byte] >> (bit & 7)) & 1;
}
void WoweeWorldMap::setTile(uint32_t x, uint32_t y, bool present) {
if (x >= gridSize || y >= gridSize) return;
size_t bit = static_cast<size_t>(y) * gridSize + x;
size_t byte = bit / 8;
if (tileBitmap.size() <= byte) tileBitmap.resize(byte + 1, 0);
uint8_t mask = static_cast<uint8_t>(1u << (bit & 7));
if (present) tileBitmap[byte] |= mask;
else tileBitmap[byte] &= static_cast<uint8_t>(~mask);
}
uint32_t WoweeWorldMap::countTiles() const {
uint32_t n = 0;
size_t totalBits = static_cast<size_t>(gridSize) * gridSize;
for (size_t bit = 0; bit < totalBits; ++bit) {
size_t byte = bit / 8;
if (byte >= tileBitmap.size()) break;
if ((tileBitmap[byte] >> (bit & 7)) & 1) n++;
}
return n;
}
const char* WoweeWorldMap::worldTypeName(uint8_t t) {
switch (t) {
case Continent: return "continent";
case Instance: return "instance";
case Battleground: return "battleground";
case Arena: return "arena";
default: return "unknown";
}
}
bool WoweeWorldMapLoader::save(const WoweeWorldMap& m,
const std::string& basePath) {
if (m.gridSize == 0 || m.gridSize > 128) return false;
std::ofstream os(normalizePath(basePath), std::ios::binary);
if (!os) return false;
os.write(kMagic, 4);
writePOD(os, kVersion);
uint32_t nameLen = static_cast<uint32_t>(m.name.size());
writePOD(os, nameLen);
if (nameLen > 0) os.write(m.name.data(), nameLen);
writePOD(os, m.worldType);
writePOD(os, m.gridSize);
uint16_t pad = 0;
writePOD(os, pad);
writePOD(os, m.defaultLightId);
writePOD(os, m.defaultWeatherId);
uint32_t reserved = 0;
writePOD(os, reserved);
writePOD(os, reserved);
writePOD(os, reserved);
size_t expectedBytes = bitmapBytesFor(m.gridSize);
uint32_t bitmapBytes = static_cast<uint32_t>(expectedBytes);
writePOD(os, bitmapBytes);
// Pad bitmap up to expectedBytes if caller under-sized it
// (setTile may not have grown it to the full grid coverage).
if (m.tileBitmap.size() >= expectedBytes) {
os.write(reinterpret_cast<const char*>(m.tileBitmap.data()),
expectedBytes);
} else {
if (!m.tileBitmap.empty()) {
os.write(reinterpret_cast<const char*>(m.tileBitmap.data()),
m.tileBitmap.size());
}
std::vector<uint8_t> tail(expectedBytes - m.tileBitmap.size(), 0);
os.write(reinterpret_cast<const char*>(tail.data()), tail.size());
}
return os.good();
}
WoweeWorldMap WoweeWorldMapLoader::load(const std::string& basePath) {
WoweeWorldMap out;
std::ifstream is(normalizePath(basePath), std::ios::binary);
if (!is) return out;
char magic[4];
is.read(magic, 4);
if (std::memcmp(magic, kMagic, 4) != 0) return out;
uint32_t version = 0;
if (!readPOD(is, version) || version != kVersion) return out;
uint32_t nameLen = 0;
if (!readPOD(is, nameLen)) return out;
if (nameLen > 0) {
out.name.resize(nameLen);
is.read(out.name.data(), nameLen);
if (is.gcount() != static_cast<std::streamsize>(nameLen)) {
out.name.clear();
return out;
}
}
if (!readPOD(is, out.worldType)) return out;
if (!readPOD(is, out.gridSize)) return out;
uint16_t pad = 0;
if (!readPOD(is, pad)) return out;
if (!readPOD(is, out.defaultLightId)) return out;
if (!readPOD(is, out.defaultWeatherId)) return out;
uint32_t reserved = 0;
if (!readPOD(is, reserved)) return out;
if (!readPOD(is, reserved)) return out;
if (!readPOD(is, reserved)) return out;
uint32_t bitmapBytes = 0;
if (!readPOD(is, bitmapBytes)) return out;
// Cap to a sane upper bound in case the file is corrupted —
// a 128×128 grid is 2048 bytes, so anything > 4 KiB is a sign
// of trouble.
if (bitmapBytes > 4096) {
out.gridSize = 0;
return out;
}
out.tileBitmap.resize(bitmapBytes);
if (bitmapBytes > 0) {
is.read(reinterpret_cast<char*>(out.tileBitmap.data()), bitmapBytes);
if (is.gcount() != static_cast<std::streamsize>(bitmapBytes)) {
out.tileBitmap.clear();
out.gridSize = 0;
return out;
}
}
return out;
}
bool WoweeWorldMapLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeWorldMap WoweeWorldMapLoader::makeContinent(const std::string& mapName) {
WoweeWorldMap m;
m.name = mapName;
m.worldType = WoweeWorldMap::Continent;
m.gridSize = 64;
m.tileBitmap.assign(bitmapBytesFor(64), 0xFF);
// Last byte may have spare bits past 64*64 — but 64*64 is
// a multiple of 8 (4096), so this is exact and no masking
// is needed.
return m;
}
WoweeWorldMap WoweeWorldMapLoader::makeInstance(const std::string& mapName) {
WoweeWorldMap m;
m.name = mapName;
m.worldType = WoweeWorldMap::Instance;
m.gridSize = 4;
m.tileBitmap.assign(bitmapBytesFor(4), 0);
for (uint32_t y = 0; y < 4; ++y)
for (uint32_t x = 0; x < 4; ++x)
m.setTile(x, y, true);
return m;
}
WoweeWorldMap WoweeWorldMapLoader::makeArena(const std::string& mapName) {
WoweeWorldMap m;
m.name = mapName;
m.worldType = WoweeWorldMap::Arena;
m.gridSize = 1;
m.tileBitmap.assign(bitmapBytesFor(1), 0);
m.setTile(0, 0, true);
return m;
}
} // namespace pipeline
} // namespace wowee

View file

@ -21,6 +21,9 @@ const char* const kArgRequired[] = {
"--export-wow-json", "--import-wow-json",
"--info-wow", "--validate-wow",
"--validate-wom",
"--gen-world-map", "--gen-world-map-instance",
"--gen-world-map-arena",
"--info-womx", "--validate-womx",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -36,6 +36,7 @@
#include "cli_info_density.hpp"
#include "cli_info_audio.hpp"
#include "cli_world_info.hpp"
#include "cli_world_map.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -113,6 +114,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleInfoDensity,
handleInfoAudio,
handleWorldInfo,
handleWorldMap,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -817,6 +817,16 @@ void printUsage(const char* argv0) {
std::printf(" Walk every WOW entry; check typeId / intensity bounds [0,1] / weight > 0 / duration min ≤ max\n");
std::printf(" --validate-wom <wom-base> [--json]\n");
std::printf(" Static sanity checks on .wom: index range, bone refs, bound box, batch coverage, animation track count\n");
std::printf(" --gen-world-map <womx-base> [mapName]\n");
std::printf(" Emit .womx world-tile manifest: 64x64 continent grid with all tiles present (open WDT replacement)\n");
std::printf(" --gen-world-map-instance <womx-base> [mapName]\n");
std::printf(" Emit .womx world-tile manifest: 4x4 instance grid (small-world / dungeon scale)\n");
std::printf(" --gen-world-map-arena <womx-base> [mapName]\n");
std::printf(" Emit .womx world-tile manifest: 1x1 single-tile arena (smallest valid world)\n");
std::printf(" --info-womx <womx-base> [--json]\n");
std::printf(" Print WOMX manifest (worldType / gridSize / tilesPresent / defaultLightId / defaultWeatherId)\n");
std::printf(" --validate-womx <womx-base> [--json]\n");
std::printf(" Static checks on .womx: gridSize 1..128, worldType in range, tileBitmap matches expected size\n");
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -0,0 +1,209 @@
#include "cli_world_map.hpp"
#include "pipeline/wowee_world_map.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
std::string stripWomxExt(std::string base) {
if (base.size() >= 5 && base.substr(base.size() - 5) == ".womx")
base = base.substr(0, base.size() - 5);
return base;
}
bool saveOrError(const wowee::pipeline::WoweeWorldMap& m,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeWorldMapLoader::save(m, base)) {
std::fprintf(stderr, "%s: failed to save %s.womx\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeWorldMap& m,
const std::string& base) {
std::printf("Wrote %s.womx\n", base.c_str());
std::printf(" name : %s\n", m.name.c_str());
std::printf(" worldType : %u (%s)\n", m.worldType,
wowee::pipeline::WoweeWorldMap::worldTypeName(m.worldType));
std::printf(" gridSize : %ux%u tiles\n", m.gridSize, m.gridSize);
std::printf(" tilesPresent: %u / %u\n",
m.countTiles(),
static_cast<uint32_t>(m.gridSize) * m.gridSize);
}
int handleGenContinent(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string mapName = "Continent";
if (i + 1 < argc && argv[i + 1][0] != '-') mapName = argv[++i];
base = stripWomxExt(base);
auto m = wowee::pipeline::WoweeWorldMapLoader::makeContinent(mapName);
if (!saveOrError(m, base, "gen-world-map")) return 1;
printGenSummary(m, base);
return 0;
}
int handleGenInstance(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string mapName = "Instance";
if (i + 1 < argc && argv[i + 1][0] != '-') mapName = argv[++i];
base = stripWomxExt(base);
auto m = wowee::pipeline::WoweeWorldMapLoader::makeInstance(mapName);
if (!saveOrError(m, base, "gen-world-map-instance")) return 1;
printGenSummary(m, base);
return 0;
}
int handleGenArena(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string mapName = "Arena";
if (i + 1 < argc && argv[i + 1][0] != '-') mapName = argv[++i];
base = stripWomxExt(base);
auto m = wowee::pipeline::WoweeWorldMapLoader::makeArena(mapName);
if (!saveOrError(m, base, "gen-world-map-arena")) return 1;
printGenSummary(m, base);
return 0;
}
int handleInfo(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
base = stripWomxExt(base);
if (!wowee::pipeline::WoweeWorldMapLoader::exists(base)) {
std::fprintf(stderr, "WOMX not found: %s.womx\n", base.c_str());
return 1;
}
auto m = wowee::pipeline::WoweeWorldMapLoader::load(base);
uint32_t total = static_cast<uint32_t>(m.gridSize) * m.gridSize;
uint32_t present = m.countTiles();
if (jsonOut) {
nlohmann::json j;
j["womx"] = base + ".womx";
j["name"] = m.name;
j["worldType"] = m.worldType;
j["worldTypeName"] =
wowee::pipeline::WoweeWorldMap::worldTypeName(m.worldType);
j["gridSize"] = m.gridSize;
j["totalTiles"] = total;
j["tilesPresent"] = present;
j["defaultLightId"] = m.defaultLightId;
j["defaultWeatherId"] = m.defaultWeatherId;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WOMX: %s.womx\n", base.c_str());
std::printf(" name : %s\n", m.name.c_str());
std::printf(" worldType : %u (%s)\n", m.worldType,
wowee::pipeline::WoweeWorldMap::worldTypeName(m.worldType));
std::printf(" gridSize : %ux%u (%u total tiles)\n",
m.gridSize, m.gridSize, total);
std::printf(" tilesPresent : %u (%.1f%%)\n",
present,
total ? (100.0f * present / total) : 0.0f);
std::printf(" defaultLightId : %u\n", m.defaultLightId);
std::printf(" defaultWeatherId : %u\n", m.defaultWeatherId);
return 0;
}
int handleValidate(int& i, int argc, char** argv) {
std::string base = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
base = stripWomxExt(base);
if (!wowee::pipeline::WoweeWorldMapLoader::exists(base)) {
std::fprintf(stderr, "validate-womx: WOMX not found: %s.womx\n",
base.c_str());
return 1;
}
auto m = wowee::pipeline::WoweeWorldMapLoader::load(base);
std::vector<std::string> errors;
std::vector<std::string> warnings;
if (m.gridSize == 0 || m.gridSize > 128) {
errors.push_back("gridSize " + std::to_string(m.gridSize) +
" not in range 1..128");
}
if (m.worldType > wowee::pipeline::WoweeWorldMap::Arena) {
errors.push_back("worldType " + std::to_string(m.worldType) +
" not in known range 0..3");
}
size_t expectedBytes =
(static_cast<size_t>(m.gridSize) * m.gridSize + 7) / 8;
if (m.tileBitmap.size() != expectedBytes) {
errors.push_back("tileBitmap size " +
std::to_string(m.tileBitmap.size()) +
" != expected " + std::to_string(expectedBytes) +
" for grid " + std::to_string(m.gridSize) + "x" +
std::to_string(m.gridSize));
}
if (m.countTiles() == 0) {
warnings.push_back("no tiles present in bitmap (empty world)");
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["womx"] = base + ".womx";
j["ok"] = ok;
j["errors"] = errors;
j["warnings"] = warnings;
std::printf("%s\n", j.dump(2).c_str());
return ok ? 0 : 1;
}
std::printf("validate-womx: %s.womx\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %ux%u grid, %u/%u tiles present\n",
m.gridSize, m.gridSize,
m.countTiles(),
static_cast<uint32_t>(m.gridSize) * m.gridSize);
return 0;
}
if (!warnings.empty()) {
std::printf(" warnings (%zu):\n", warnings.size());
for (const auto& w : warnings)
std::printf(" - %s\n", w.c_str());
}
if (!errors.empty()) {
std::printf(" ERRORS (%zu):\n", errors.size());
for (const auto& e : errors)
std::printf(" - %s\n", e.c_str());
}
return ok ? 0 : 1;
}
} // namespace
bool handleWorldMap(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-world-map") == 0 && i + 1 < argc) {
outRc = handleGenContinent(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-world-map-instance") == 0 && i + 1 < argc) {
outRc = handleGenInstance(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-world-map-arena") == 0 && i + 1 < argc) {
outRc = handleGenArena(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-womx") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-womx") == 0 && i + 1 < argc) {
outRc = handleValidate(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,11 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
bool handleWorldMap(int& i, int argc, char** argv, int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee