mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
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
This commit is contained in:
parent
d138269a35
commit
2980ca83e7
42 changed files with 5647 additions and 3 deletions
337
tools/editor/adt_writer.cpp
Normal file
337
tools/editor/adt_writer.cpp
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
#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
|
||||
Loading…
Add table
Add a link
Reference in a new issue