#include "pipeline/adt_loader.hpp" #include "core/logger.hpp" #include #include #include namespace wowee { namespace pipeline { // HeightMap implementation float HeightMap::getHeight(int x, int y) const { if (x < 0 || x > 8 || y < 0 || y > 8) { return 0.0f; } // MCVT heights are stored in interleaved 9x17 row-major layout: // Row 0: 9 outer (indices 0-8), then 8 inner (indices 9-16) // Row 1: 9 outer (indices 17-25), then 8 inner (indices 26-33) // ... // Outer vertex (x, y) is at index: y * 17 + x int index = y * 17 + x; if (index < 0 || index >= 145) return 0.0f; return heights[index]; } // ADTLoader implementation ADTTerrain ADTLoader::load(const std::vector& adtData) { ADTTerrain terrain; if (adtData.empty()) { LOG_ERROR("Empty ADT data"); return terrain; } LOG_DEBUG("Loading ADT terrain (", adtData.size(), " bytes)"); size_t offset = 0; int chunkIndex = 0; // Parse chunks int totalChunks = 0; while (offset < adtData.size()) { ChunkHeader header; if (!readChunkHeader(adtData.data(), offset, adtData.size(), header)) { break; } const uint8_t* chunkData = adtData.data() + offset + 8; size_t chunkSize = header.size; totalChunks++; if (totalChunks <= 5) { // Log first few chunks for debugging char magic[5] = {0}; std::memcpy(magic, &header.magic, 4); } // Parse based on chunk type if (header.magic == MVER) { parseMVER(chunkData, chunkSize, terrain); } else if (header.magic == MTEX) { parseMTEX(chunkData, chunkSize, terrain); } else if (header.magic == MMDX) { parseMMDX(chunkData, chunkSize, terrain); } else if (header.magic == MWMO) { parseMWMO(chunkData, chunkSize, terrain); } else if (header.magic == MDDF) { parseMDDF(chunkData, chunkSize, terrain); } else if (header.magic == MODF) { parseMODF(chunkData, chunkSize, terrain); } else if (header.magic == MH2O) { LOG_DEBUG("Found MH2O chunk (", chunkSize, " bytes)"); parseMH2O(chunkData, chunkSize, terrain); } else if (header.magic == MCNK) { parseMCNK(chunkData, chunkSize, chunkIndex++, terrain); } // Move to next chunk offset += 8 + chunkSize; } terrain.loaded = true; return terrain; } bool ADTLoader::readChunkHeader(const uint8_t* data, size_t offset, size_t dataSize, ChunkHeader& header) { if (offset + 8 > dataSize) { return false; } header.magic = readUInt32(data, offset); header.size = readUInt32(data, offset + 4); // Validate chunk size if (offset + 8 + header.size > dataSize) { LOG_WARNING("Chunk extends beyond file: magic=0x", std::hex, header.magic, ", size=", std::dec, header.size); return false; } return true; } uint32_t ADTLoader::readUInt32(const uint8_t* data, size_t offset) { uint32_t value; std::memcpy(&value, data + offset, sizeof(uint32_t)); return value; } float ADTLoader::readFloat(const uint8_t* data, size_t offset) { float value; std::memcpy(&value, data + offset, sizeof(float)); return value; } uint16_t ADTLoader::readUInt16(const uint8_t* data, size_t offset) { uint16_t value; std::memcpy(&value, data + offset, sizeof(uint16_t)); return value; } void ADTLoader::parseMVER(const uint8_t* data, size_t size, ADTTerrain& terrain) { if (size < 4) { LOG_WARNING("MVER chunk too small"); return; } terrain.version = readUInt32(data, 0); LOG_DEBUG("ADT version: ", terrain.version); } void ADTLoader::parseMTEX(const uint8_t* data, size_t size, ADTTerrain& terrain) { // MTEX contains null-terminated texture filenames size_t offset = 0; while (offset < size) { const char* textureName = reinterpret_cast(data + offset); size_t nameLen = std::strlen(textureName); if (nameLen == 0) { break; } terrain.textures.push_back(std::string(textureName, nameLen)); offset += nameLen + 1; // +1 for null terminator } LOG_DEBUG("Loaded ", terrain.textures.size(), " texture names"); } void ADTLoader::parseMMDX(const uint8_t* data, size_t size, ADTTerrain& terrain) { // MMDX contains null-terminated M2 model filenames size_t offset = 0; while (offset < size) { const char* modelName = reinterpret_cast(data + offset); size_t nameLen = std::strlen(modelName); if (nameLen == 0) { break; } terrain.doodadNames.push_back(std::string(modelName, nameLen)); offset += nameLen + 1; } LOG_DEBUG("Loaded ", terrain.doodadNames.size(), " doodad names"); } void ADTLoader::parseMWMO(const uint8_t* data, size_t size, ADTTerrain& terrain) { // MWMO contains null-terminated WMO filenames size_t offset = 0; while (offset < size) { const char* wmoName = reinterpret_cast(data + offset); size_t nameLen = std::strlen(wmoName); if (nameLen == 0) { break; } terrain.wmoNames.push_back(std::string(wmoName, nameLen)); offset += nameLen + 1; } LOG_DEBUG("Loaded ", terrain.wmoNames.size(), " WMO names from MWMO chunk"); for (size_t i = 0; i < terrain.wmoNames.size(); i++) { LOG_DEBUG(" WMO[", i, "]: ", terrain.wmoNames[i]); // Flag potential duplicate cathedral models if (terrain.wmoNames[i].find("cathedral") != std::string::npos || terrain.wmoNames[i].find("Cathedral") != std::string::npos) { LOG_DEBUG("*** CATHEDRAL WMO FOUND: ", terrain.wmoNames[i]); } } } void ADTLoader::parseMDDF(const uint8_t* data, size_t size, ADTTerrain& terrain) { // MDDF contains doodad placements (36 bytes each) const size_t entrySize = 36; size_t count = size / entrySize; for (size_t i = 0; i < count; i++) { size_t offset = i * entrySize; ADTTerrain::DoodadPlacement placement; placement.nameId = readUInt32(data, offset); placement.uniqueId = readUInt32(data, offset + 4); placement.position[0] = readFloat(data, offset + 8); placement.position[1] = readFloat(data, offset + 12); placement.position[2] = readFloat(data, offset + 16); placement.rotation[0] = readFloat(data, offset + 20); placement.rotation[1] = readFloat(data, offset + 24); placement.rotation[2] = readFloat(data, offset + 28); placement.scale = readUInt16(data, offset + 32); placement.flags = readUInt16(data, offset + 34); terrain.doodadPlacements.push_back(placement); } LOG_DEBUG("Loaded ", terrain.doodadPlacements.size(), " doodad placements"); } void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain) { // MODF contains WMO placements (64 bytes each) const size_t entrySize = 64; size_t count = size / entrySize; for (size_t i = 0; i < count; i++) { size_t offset = i * entrySize; ADTTerrain::WMOPlacement placement; placement.nameId = readUInt32(data, offset); placement.uniqueId = readUInt32(data, offset + 4); placement.position[0] = readFloat(data, offset + 8); placement.position[1] = readFloat(data, offset + 12); placement.position[2] = readFloat(data, offset + 16); placement.rotation[0] = readFloat(data, offset + 20); placement.rotation[1] = readFloat(data, offset + 24); placement.rotation[2] = readFloat(data, offset + 28); placement.extentLower[0] = readFloat(data, offset + 32); placement.extentLower[1] = readFloat(data, offset + 36); placement.extentLower[2] = readFloat(data, offset + 40); placement.extentUpper[0] = readFloat(data, offset + 44); placement.extentUpper[1] = readFloat(data, offset + 48); placement.extentUpper[2] = readFloat(data, offset + 52); placement.flags = readUInt16(data, offset + 56); placement.doodadSet = readUInt16(data, offset + 58); terrain.wmoPlacements.push_back(placement); // Log STORMWIND.WMO placements to detect duplicates at different Z heights if (placement.nameId < terrain.wmoNames.size()) { const std::string& wmoName = terrain.wmoNames[placement.nameId]; std::string upperName = wmoName; std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::toupper); if (upperName.find("STORMWIND.WMO") != std::string::npos) { LOG_DEBUG("*** STORMWIND.WMO PLACEMENT:", " uniqueId=", placement.uniqueId, " pos=(", placement.position[0], ", ", placement.position[1], ", ", placement.position[2], ")", " rot=(", placement.rotation[0], ", ", placement.rotation[1], ", ", placement.rotation[2], ")", " doodadSet=", placement.doodadSet, " flags=0x", std::hex, placement.flags, std::dec); } } } LOG_DEBUG("Loaded ", terrain.wmoPlacements.size(), " WMO placements"); } void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTTerrain& terrain) { if (chunkIndex < 0 || chunkIndex >= 256) { LOG_WARNING("Invalid chunk index: ", chunkIndex); return; } MapChunk& chunk = terrain.chunks[chunkIndex]; // Read MCNK header (128 bytes) if (size < 128) { LOG_WARNING("MCNK chunk too small"); return; } chunk.flags = readUInt32(data, 0); chunk.indexX = readUInt32(data, 4); chunk.indexY = readUInt32(data, 8); // Read holes mask (at offset 0x3C = 60 in MCNK header) // Each bit represents a 2x2 block of the 8x8 quad grid chunk.holes = readUInt16(data, 60); // Read layer count and offsets from MCNK header uint32_t nLayers = readUInt32(data, 12); uint32_t ofsHeight = readUInt32(data, 20); // MCVT offset uint32_t ofsNormal = readUInt32(data, 24); // MCNR offset uint32_t ofsLayer = readUInt32(data, 28); // MCLY offset uint32_t ofsAlpha = readUInt32(data, 36); // MCAL offset uint32_t sizeAlpha = readUInt32(data, 40); // Debug first chunk only if (chunkIndex == 0) { LOG_DEBUG("MCNK[0] offsets: nLayers=", nLayers, " height=", ofsHeight, " normal=", ofsNormal, " layer=", ofsLayer, " alpha=", ofsAlpha, " sizeAlpha=", sizeAlpha, " size=", size, " holes=0x", std::hex, chunk.holes, std::dec); } // MCNK position is in canonical WoW coordinates (NOT ADT placement space): // offset 104: wowY (west axis, horizontal — unused, XY computed from tile indices) // offset 108: wowX (north axis, horizontal — unused, XY computed from tile indices) // offset 112: wowZ = HEIGHT BASE (MCVT heights are relative to this) chunk.position[0] = readFloat(data, 104); // wowY (unused) chunk.position[1] = readFloat(data, 108); // wowX (unused) chunk.position[2] = readFloat(data, 112); // wowZ = height base // Parse sub-chunks using offsets from MCNK header // WoW ADT sub-chunks may have their own 8-byte headers (magic+size) // Check by inspecting the first 4 bytes at the offset // Height map (MCVT) - 145 floats = 580 bytes if (ofsHeight > 0 && ofsHeight + 580 <= size) { // Check if this points to a sub-chunk header (magic "MCVT" = 0x4D435654) uint32_t possibleMagic = readUInt32(data, ofsHeight); uint32_t headerSkip = 0; if (possibleMagic == MCVT) { headerSkip = 8; // Skip magic + size if (chunkIndex == 0) { LOG_DEBUG("MCNK sub-chunks have headers (MCVT magic found at offset ", ofsHeight, ")"); } } parseMCVT(data + ofsHeight + headerSkip, 580, chunk); } // Normals (MCNR) - 145 normals (3 bytes each) + 13 padding = 448 bytes if (ofsNormal > 0 && ofsNormal + 448 <= size) { uint32_t possibleMagic = readUInt32(data, ofsNormal); uint32_t skip = (possibleMagic == MCNR) ? 8 : 0; parseMCNR(data + ofsNormal + skip, 448, chunk); } // Texture layers (MCLY) - 16 bytes per layer if (ofsLayer > 0 && nLayers > 0) { size_t layerSize = nLayers * 16; uint32_t possibleMagic = readUInt32(data, ofsLayer); uint32_t skip = (possibleMagic == MCLY) ? 8 : 0; if (ofsLayer + skip + layerSize <= size) { parseMCLY(data + ofsLayer + skip, layerSize, chunk); } } // Alpha maps (MCAL) - variable size from header if (ofsAlpha > 0 && sizeAlpha > 0 && ofsAlpha + sizeAlpha <= size) { uint32_t possibleMagic = readUInt32(data, ofsAlpha); uint32_t skip = (possibleMagic == MCAL) ? 8 : 0; parseMCAL(data + ofsAlpha + skip, sizeAlpha - skip, chunk); } // Liquid (MCLQ) - vanilla/TBC per-chunk water (no MH2O in these expansions) // ofsLiquid at MCNK header offset 0x60, sizeLiquid at 0x64 uint32_t ofsLiquid = readUInt32(data, 0x60); uint32_t sizeLiquid = readUInt32(data, 0x64); if (ofsLiquid > 0 && sizeLiquid > 8 && ofsLiquid + sizeLiquid <= size) { uint32_t possibleMagic = readUInt32(data, ofsLiquid); uint32_t skip = (possibleMagic == MCLQ) ? 8 : 0; parseMCLQ(data + ofsLiquid + skip, sizeLiquid - skip, chunkIndex, chunk.flags, terrain); } } void ADTLoader::parseMCVT(const uint8_t* data, size_t size, MapChunk& chunk) { // MCVT contains 145 height values (floats) if (size < 145 * sizeof(float)) { LOG_WARNING("MCVT chunk too small: ", size, " bytes"); return; } float minHeight = 999999.0f; float maxHeight = -999999.0f; for (int i = 0; i < 145; i++) { float height = readFloat(data, i * sizeof(float)); chunk.heightMap.heights[i] = height; if (height < minHeight) minHeight = height; if (height > maxHeight) maxHeight = height; } chunk.heightMap.loaded = true; // Log height range for first chunk only static bool logged = false; if (!logged) { LOG_INFO("MCVT height range: [", minHeight, ", ", maxHeight, "]", " (heights[0]=", chunk.heightMap.heights[0], " heights[8]=", chunk.heightMap.heights[8], " heights[136]=", chunk.heightMap.heights[136], " heights[144]=", chunk.heightMap.heights[144], ")"); logged = true; } } void ADTLoader::parseMCNR(const uint8_t* data, size_t size, MapChunk& chunk) { // MCNR contains 145 normals (3 bytes each, signed) if (size < 145 * 3) { LOG_WARNING("MCNR chunk too small: ", size, " bytes"); return; } for (int i = 0; i < 145 * 3; i++) { chunk.normals[i] = static_cast(data[i]); } } void ADTLoader::parseMCLY(const uint8_t* data, size_t size, MapChunk& chunk) { // MCLY contains texture layer definitions (16 bytes each) size_t layerCount = size / 16; if (layerCount > 4) { LOG_WARNING("More than 4 texture layers: ", layerCount); layerCount = 4; } static int layerLogCount = 0; for (size_t i = 0; i < layerCount; i++) { TextureLayer layer; layer.textureId = readUInt32(data, i * 16 + 0); layer.flags = readUInt32(data, i * 16 + 4); layer.offsetMCAL = readUInt32(data, i * 16 + 8); layer.effectId = readUInt32(data, i * 16 + 12); if (layerLogCount < 10) { LOG_DEBUG(" MCLY[", i, "]: texId=", layer.textureId, " flags=0x", std::hex, layer.flags, std::dec, " alphaOfs=", layer.offsetMCAL, " useAlpha=", layer.useAlpha(), " compressed=", layer.compressedAlpha()); layerLogCount++; } chunk.layers.push_back(layer); } } void ADTLoader::parseMCAL(const uint8_t* data, size_t size, MapChunk& chunk) { // MCAL contains alpha maps for texture layers // Store raw data; decompression happens per-layer during mesh generation chunk.alphaMap.resize(size); std::memcpy(chunk.alphaMap.data(), data, size); } void ADTLoader::parseMCLQ(const uint8_t* data, size_t size, int chunkIndex, uint32_t mcnkFlags, ADTTerrain& terrain) { // MCLQ: Vanilla/TBC per-chunk liquid data (inside MCNK) // Layout: // float minHeight, maxHeight (8 bytes) // SLiquidVertex[9*9] (81 * 8 = 648 bytes) // water: uint8 depth, flow0, flow1, filler, float height // magma: uint16 s, uint16 t, float height // uint8 tiles[8*8] (64 bytes) // Total minimum: 720 bytes if (size < 720) { return; // Not enough data for a valid MCLQ } float minHeight = readFloat(data, 0); float maxHeight = readFloat(data, 4); // Determine liquid type from MCNK flags // 0x04 = has liquid (river/lake), 0x08 = ocean, 0x10 = magma, 0x20 = slime uint16_t liquidType = 0; // water if (mcnkFlags & 0x08) liquidType = 1; // ocean else if (mcnkFlags & 0x10) liquidType = 2; // magma else if (mcnkFlags & 0x20) liquidType = 3; // slime // Read 9x9 height values (skip depth/flow bytes, just read the float height) const uint8_t* vertData = data + 8; std::vector heights(81); for (int i = 0; i < 81; i++) { heights[i] = readFloat(vertData, i * 8 + 4); // float at offset 4 within each 8-byte vertex } // Read 8x8 tile flags const uint8_t* tileData = data + 8 + 648; std::vector tileMask(64); bool anyVisible = false; for (int i = 0; i < 64; i++) { uint8_t tileFlag = tileData[i]; // Bit 0x0F = liquid type, bit 0x40 = fatigue, bit 0x80 = hidden // A tile is visible if NOT hidden (0x80 not set) and type is non-zero or has base flag bool hidden = (tileFlag & 0x80) != 0; tileMask[i] = hidden ? 0 : 1; if (!hidden) anyVisible = true; } if (!anyVisible) { return; // All tiles hidden, no visible water } // Validate heights - if all heights are 0 or unreasonable, skip bool validHeights = false; for (float h : heights) { if (h != 0.0f && std::isfinite(h)) { validHeights = true; break; } } // If heights are all zero, use maxHeight as flat water level if (!validHeights) { for (float& h : heights) h = maxHeight; } // Build a WaterLayer matching the MH2O format ADTTerrain::WaterLayer layer; layer.liquidType = liquidType; layer.flags = 0; layer.minHeight = minHeight; layer.maxHeight = maxHeight; layer.x = 0; layer.y = 0; layer.width = 8; // 8 tiles = 9 vertices per axis layer.height = 8; layer.heights = std::move(heights); layer.mask.resize(8); // 8 bytes = 64 bits for 8x8 tiles for (int row = 0; row < 8; row++) { uint8_t rowBits = 0; for (int col = 0; col < 8; col++) { if (tileMask[row * 8 + col]) { rowBits |= (1 << col); } } layer.mask[row] = rowBits; } terrain.waterData[chunkIndex].layers.push_back(std::move(layer)); static int mclqLogCount = 0; if (mclqLogCount < 5) { LOG_INFO("MCLQ[", chunkIndex, "]: type=", liquidType, " height=[", minHeight, ",", maxHeight, "]"); mclqLogCount++; } } void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain) { // MH2O contains water/liquid data for all 256 map chunks // Structure: 256 SMLiquidChunk headers followed by instance data // Each SMLiquidChunk header is 12 bytes (WotLK 3.3.5a): // - uint32_t offsetInstances (offset from MH2O chunk start) // - uint32_t layerCount // - uint32_t offsetAttributes (offset from MH2O chunk start) const size_t headerSize = 12; // SMLiquidChunk size for WotLK const size_t totalHeaderSize = 256 * headerSize; if (size < totalHeaderSize) { LOG_WARNING("MH2O chunk too small for headers: ", size, " bytes"); return; } int totalLayers = 0; for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) { size_t headerOffset = chunkIdx * headerSize; uint32_t offsetInstances = readUInt32(data, headerOffset); uint32_t layerCount = readUInt32(data, headerOffset + 4); // uint32_t offsetAttributes = readUInt32(data, headerOffset + 8); // Not used if (layerCount == 0 || offsetInstances == 0) { continue; // No water in this chunk } // Sanity checks if (offsetInstances >= size) { continue; } if (layerCount > 16) { // Sanity check - max 16 layers per chunk is reasonable LOG_WARNING("MH2O: Invalid layer count ", layerCount, " for chunk ", chunkIdx); continue; } // Parse each liquid layer (SMLiquidInstance - 24 bytes) for (uint32_t layerIdx = 0; layerIdx < layerCount; layerIdx++) { size_t instanceOffset = offsetInstances + layerIdx * 24; if (instanceOffset + 24 > size) { break; } ADTTerrain::WaterLayer layer; layer.liquidType = readUInt16(data, instanceOffset); uint16_t liquidObject = readUInt16(data, instanceOffset + 2); // LVF format flags layer.minHeight = readFloat(data, instanceOffset + 4); layer.maxHeight = readFloat(data, instanceOffset + 8); layer.x = data[instanceOffset + 12]; layer.y = data[instanceOffset + 13]; layer.width = data[instanceOffset + 14]; layer.height = data[instanceOffset + 15]; uint32_t offsetExistsBitmap = readUInt32(data, instanceOffset + 16); uint32_t offsetVertexData = readUInt32(data, instanceOffset + 20); // Skip invalid layers if (layer.width == 0 || layer.height == 0) { continue; } // Clamp dimensions to valid range if (layer.width > 8) layer.width = 8; if (layer.height > 8) layer.height = 8; if (layer.x + layer.width > 8) layer.width = 8 - layer.x; if (layer.y + layer.height > 8) layer.height = 8 - layer.y; // Read exists bitmap (which tiles have water). // In WotLK MH2O this is chunk-wide 8x8 tile flags (64 bits = 8 bytes), // even when the layer covers a sub-rect. constexpr size_t bitmapBytes = 8; // Note: offsets in SMLiquidInstance are relative to MH2O chunk start if (offsetExistsBitmap > 0) { size_t bitmapOffset = offsetExistsBitmap; if (bitmapOffset + bitmapBytes <= size) { layer.mask.resize(bitmapBytes); std::memcpy(layer.mask.data(), data + bitmapOffset, bitmapBytes); } } else { // No bitmap means all tiles in chunk are valid for this layer. layer.mask.resize(bitmapBytes, 0xFF); } // Read vertex heights // Number of vertices is (width+1) * (height+1) size_t numVertices = (layer.width + 1) * (layer.height + 1); // Check liquid object flags (LVF) to determine vertex format bool hasHeightData = (liquidObject != 2); // LVF_height_depth or LVF_height_texcoord if (hasHeightData && offsetVertexData > 0) { size_t vertexOffset = offsetVertexData; size_t vertexDataSize = numVertices * sizeof(float); if (vertexOffset + vertexDataSize <= size) { layer.heights.resize(numVertices); for (size_t i = 0; i < numVertices; i++) { layer.heights[i] = readFloat(data, vertexOffset + i * sizeof(float)); } } else { // Offset out of bounds - use flat water layer.heights.resize(numVertices, layer.minHeight); } } else { // No height data - use flat surface at minHeight layer.heights.resize(numVertices, layer.minHeight); } // Default flags layer.flags = 0; terrain.waterData[chunkIdx].layers.push_back(layer); totalLayers++; } } LOG_DEBUG("Loaded MH2O water data: ", totalLayers, " liquid layers across ", size, " bytes"); } } // namespace pipeline } // namespace wowee