Kelsidavis-WoWee/tools/editor/adt_writer.cpp
Kelsi 2980ca83e7 feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.

Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
  Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format

Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)

Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel

NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
  (Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON

Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format

Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed

Known issues:
- M2/WMO rendering may not display on first placement (frame index
  sync between update/render was misaligned — now fixed but untested
  end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00

337 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "adt_writer.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <cstring>
#include <filesystem>
namespace wowee {
namespace editor {
// ADT chunk magics (little-endian as read from file)
static constexpr uint32_t MVER = 0x4D564552;
static constexpr uint32_t MHDR = 0x4D484452;
static constexpr uint32_t MCIN = 0x4D43494E;
static constexpr uint32_t MTEX = 0x4D544558;
static constexpr uint32_t MMDX = 0x4D4D4458;
static constexpr uint32_t MMID = 0x4D4D4944;
static constexpr uint32_t MWMO = 0x4D574D4F;
static constexpr uint32_t MWID = 0x4D574944;
static constexpr uint32_t MDDF = 0x4D444446;
static constexpr uint32_t MODF = 0x4D4F4446;
static constexpr uint32_t MCNK = 0x4D434E4B;
static constexpr uint32_t MCVT = 0x4D435654;
static constexpr uint32_t MCNR = 0x4D434E52;
static constexpr uint32_t MCLY = 0x4D434C59;
static constexpr uint32_t MCAL = 0x4D43414C;
void ADTWriter::writeChunkHeader(std::vector<uint8_t>& buf, uint32_t magic, uint32_t size) {
writeU32(buf, magic);
writeU32(buf, size);
}
void ADTWriter::writeU32(std::vector<uint8_t>& buf, uint32_t val) {
buf.push_back(val & 0xFF);
buf.push_back((val >> 8) & 0xFF);
buf.push_back((val >> 16) & 0xFF);
buf.push_back((val >> 24) & 0xFF);
}
void ADTWriter::writeU16(std::vector<uint8_t>& buf, uint16_t val) {
buf.push_back(val & 0xFF);
buf.push_back((val >> 8) & 0xFF);
}
void ADTWriter::writeFloat(std::vector<uint8_t>& buf, float val) {
uint32_t bits;
std::memcpy(&bits, &val, 4);
writeU32(buf, bits);
}
void ADTWriter::writeBytes(std::vector<uint8_t>& buf, const void* data, size_t size) {
const uint8_t* p = static_cast<const uint8_t*>(data);
buf.insert(buf.end(), p, p + size);
}
void ADTWriter::patchSize(std::vector<uint8_t>& buf, size_t headerOffset) {
uint32_t size = static_cast<uint32_t>(buf.size() - headerOffset - 8);
std::memcpy(buf.data() + headerOffset + 4, &size, 4);
}
void ADTWriter::writeMVER(std::vector<uint8_t>& buf) {
writeChunkHeader(buf, MVER, 4);
writeU32(buf, 18); // ADT version
}
void ADTWriter::writeMHDR(std::vector<uint8_t>& buf, size_t& mhdrOffset) {
mhdrOffset = buf.size();
writeChunkHeader(buf, MHDR, 64);
// 16 uint32 fields — all zeros for now (offsets filled later if needed)
for (int i = 0; i < 16; i++) writeU32(buf, 0);
}
void ADTWriter::writeMTEX(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MTEX, 0);
for (const auto& tex : terrain.textures) {
writeBytes(buf, tex.c_str(), tex.size() + 1); // null-terminated
}
patchSize(buf, start);
}
void ADTWriter::writeMMDX(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MMDX, 0);
for (const auto& name : terrain.doodadNames) {
writeBytes(buf, name.c_str(), name.size() + 1);
}
patchSize(buf, start);
// MMID: offsets into MMDX
size_t mmidStart = buf.size();
writeChunkHeader(buf, MMID, 0);
uint32_t offset = 0;
for (const auto& name : terrain.doodadNames) {
writeU32(buf, offset);
offset += static_cast<uint32_t>(name.size() + 1);
}
patchSize(buf, mmidStart);
}
void ADTWriter::writeMWMO(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MWMO, 0);
for (const auto& name : terrain.wmoNames) {
writeBytes(buf, name.c_str(), name.size() + 1);
}
patchSize(buf, start);
// MWID: offsets into MWMO
size_t mwidStart = buf.size();
writeChunkHeader(buf, MWID, 0);
uint32_t offset = 0;
for (const auto& name : terrain.wmoNames) {
writeU32(buf, offset);
offset += static_cast<uint32_t>(name.size() + 1);
}
patchSize(buf, mwidStart);
}
void ADTWriter::writeMDDF(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MDDF, 0);
for (const auto& p : terrain.doodadPlacements) {
writeU32(buf, p.nameId);
writeU32(buf, p.uniqueId);
writeFloat(buf, p.position[0]);
writeFloat(buf, p.position[1]);
writeFloat(buf, p.position[2]);
writeFloat(buf, p.rotation[0]);
writeFloat(buf, p.rotation[1]);
writeFloat(buf, p.rotation[2]);
writeU16(buf, p.scale);
writeU16(buf, p.flags);
}
patchSize(buf, start);
}
void ADTWriter::writeMODF(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MODF, 0);
for (const auto& p : terrain.wmoPlacements) {
writeU32(buf, p.nameId);
writeU32(buf, p.uniqueId);
writeFloat(buf, p.position[0]);
writeFloat(buf, p.position[1]);
writeFloat(buf, p.position[2]);
writeFloat(buf, p.rotation[0]);
writeFloat(buf, p.rotation[1]);
writeFloat(buf, p.rotation[2]);
writeFloat(buf, p.extentLower[0]);
writeFloat(buf, p.extentLower[1]);
writeFloat(buf, p.extentLower[2]);
writeFloat(buf, p.extentUpper[0]);
writeFloat(buf, p.extentUpper[1]);
writeFloat(buf, p.extentUpper[2]);
writeU16(buf, p.flags);
writeU16(buf, p.doodadSet);
}
patchSize(buf, start);
}
void ADTWriter::writeMCNK(std::vector<uint8_t>& buf, const pipeline::MapChunk& chunk,
int chunkX, int chunkY) {
size_t mcnkStart = buf.size();
writeChunkHeader(buf, MCNK, 0);
// MCNK header (128 bytes)
writeU32(buf, chunk.flags);
writeU32(buf, chunkX);
writeU32(buf, chunkY);
writeU32(buf, static_cast<uint32_t>(chunk.layers.size()));
writeU32(buf, 0); // nDoodadRefs
// Offsets within MCNK — filled with placeholder (parser uses sub-chunk magic scanning)
for (int i = 0; i < 5; i++) writeU32(buf, 0); // ofsHeight, ofsNormal, ofsLayer, ofsRefs, ofsAlpha
writeU32(buf, 0); // sizeAlpha
writeU32(buf, 0); // ofsShadow
writeU32(buf, 0); // sizeShadow
writeU32(buf, 0); // areaid
writeU32(buf, 0); // nMapObjRefs
writeU16(buf, chunk.holes);
writeU16(buf, 0); // padding
// 16 bytes of low-quality texture map (doodadStencil)
for (int i = 0; i < 4; i++) writeU32(buf, 0);
writeU32(buf, 0); // predTex
writeU32(buf, 0); // noEffectDoodad
writeU32(buf, 0); // ofsSndEmitters
writeU32(buf, 0); // nSndEmitters
writeU32(buf, 0); // ofsLiquid
writeU32(buf, 0); // sizeLiquid
writeFloat(buf, chunk.position[0]);
writeFloat(buf, chunk.position[1]);
writeFloat(buf, chunk.position[2]);
writeU32(buf, 0); // ofsMCCV
writeU32(buf, 0); // ofsMCLV
writeU32(buf, 0); // unused
// MCVT sub-chunk (145 floats = 580 bytes)
writeChunkHeader(buf, MCVT, 145 * 4);
for (int i = 0; i < 145; i++) {
writeFloat(buf, chunk.heightMap.heights[i]);
}
// MCNR sub-chunk (145 * 3 = 435 bytes + 13 pad = 448)
writeChunkHeader(buf, MCNR, 435 + 13);
writeBytes(buf, chunk.normals.data(), 435);
for (int i = 0; i < 13; i++) buf.push_back(0); // padding
// MCLY sub-chunk
{
size_t mclyStart = buf.size();
writeChunkHeader(buf, MCLY, 0);
for (const auto& layer : chunk.layers) {
writeU32(buf, layer.textureId);
writeU32(buf, layer.flags);
writeU32(buf, layer.offsetMCAL);
writeU32(buf, layer.effectId);
}
patchSize(buf, mclyStart);
}
// MCAL sub-chunk (alpha maps)
{
size_t mcalStart = buf.size();
writeChunkHeader(buf, MCAL, 0);
if (!chunk.alphaMap.empty()) {
writeBytes(buf, chunk.alphaMap.data(), chunk.alphaMap.size());
}
patchSize(buf, mcalStart);
}
patchSize(buf, mcnkStart);
}
std::vector<uint8_t> ADTWriter::serialize(const pipeline::ADTTerrain& terrain) {
std::vector<uint8_t> buf;
buf.reserve(2 * 1024 * 1024);
writeMVER(buf);
size_t mhdrOffset = 0;
writeMHDR(buf, mhdrOffset);
// MCIN placeholder (256 entries × 16 bytes = 4096 bytes)
size_t mcinStart = buf.size();
writeChunkHeader(buf, MCIN, 4096);
for (int i = 0; i < 256 * 4; i++) writeU32(buf, 0);
writeMTEX(buf, terrain);
writeMMDX(buf, terrain);
writeMWMO(buf, terrain);
writeMDDF(buf, terrain);
writeMODF(buf, terrain);
// Write 256 MCNK chunks and record offsets
std::vector<size_t> mcnkOffsets(256);
std::vector<uint32_t> mcnkSizes(256);
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
int idx = y * 16 + x;
mcnkOffsets[idx] = buf.size();
writeMCNK(buf, terrain.chunks[idx], x, y);
mcnkSizes[idx] = static_cast<uint32_t>(buf.size() - mcnkOffsets[idx]);
}
}
// Patch MCIN with offsets and sizes
for (int i = 0; i < 256; i++) {
size_t entryOffset = mcinStart + 8 + i * 16;
uint32_t offset = static_cast<uint32_t>(mcnkOffsets[i]);
uint32_t size = mcnkSizes[i];
std::memcpy(buf.data() + entryOffset, &offset, 4);
std::memcpy(buf.data() + entryOffset + 4, &size, 4);
// flags and asyncId stay 0
}
return buf;
}
bool ADTWriter::write(const pipeline::ADTTerrain& terrain, const std::string& path) {
auto data = serialize(terrain);
auto dir = std::filesystem::path(path).parent_path();
if (!dir.empty()) {
std::filesystem::create_directories(dir);
}
std::ofstream file(path, std::ios::binary);
if (!file) {
LOG_ERROR("Failed to open file for writing: ", path);
return false;
}
file.write(reinterpret_cast<const char*>(data.data()), data.size());
LOG_INFO("ADT written: ", path, " (", data.size(), " bytes)");
return true;
}
bool ADTWriter::writeWDT(const std::string& mapName, int tileX, int tileY,
const std::string& path) {
std::vector<uint8_t> buf;
buf.reserve(32768);
// MVER
writeChunkHeader(buf, MVER, 4);
writeU32(buf, 18);
// MPHD (map header — 32 bytes, all zeros = no special flags)
writeChunkHeader(buf, 0x4D504844, 32);
for (int i = 0; i < 8; i++) writeU32(buf, 0);
// MAIN (64×64 grid of 8-byte entries: flags + asyncId)
writeChunkHeader(buf, 0x4D41494E, 64 * 64 * 8);
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
if (x == tileX && y == tileY) {
writeU32(buf, 1); // FLAG_EXISTS
} else {
writeU32(buf, 0);
}
writeU32(buf, 0); // asyncId
}
}
auto dir = std::filesystem::path(path).parent_path();
if (!dir.empty()) std::filesystem::create_directories(dir);
std::ofstream file(path, std::ios::binary);
if (!file) {
LOG_ERROR("Failed to write WDT: ", path);
return false;
}
file.write(reinterpret_cast<const char*>(buf.data()), buf.size());
LOG_INFO("WDT written: ", path, " (", buf.size(), " bytes, map=", mapName, ")");
return true;
}
} // namespace editor
} // namespace wowee