#include "pipeline/wmo_loader.hpp" #include "core/logger.hpp" #include #include namespace wowee { namespace pipeline { namespace { // WMO chunk identifiers constexpr uint32_t MVER = 0x4D564552; // Version constexpr uint32_t MOHD = 0x4D4F4844; // Header constexpr uint32_t MOTX = 0x4D4F5458; // Textures constexpr uint32_t MOMT = 0x4D4F4D54; // Materials constexpr uint32_t MOGN = 0x4D4F474E; // Group names constexpr uint32_t MOGI = 0x4D4F4749; // Group info constexpr uint32_t MOLT = 0x4D4F4C54; // Lights constexpr uint32_t MODN = 0x4D4F444E; // Doodad names constexpr uint32_t MODD = 0x4D4F4444; // Doodad definitions constexpr uint32_t MODS = 0x4D4F4453; // Doodad sets constexpr uint32_t MOPV = 0x4D4F5056; // Portal vertices constexpr uint32_t MOPT = 0x4D4F5054; // Portal info constexpr uint32_t MOPR = 0x4D4F5052; // Portal references constexpr uint32_t MFOG = 0x4D464F47; // Fog // WMO group chunk identifiers constexpr uint32_t MOGP = 0x4D4F4750; // Group header constexpr uint32_t MOVV = 0x4D4F5656; // Vertices constexpr uint32_t MOVI = 0x4D4F5649; // Indices constexpr uint32_t MOBA = 0x4D4F4241; // Batches constexpr uint32_t MOCV = 0x4D4F4356; // Vertex colors constexpr uint32_t MONR = 0x4D4F4E52; // Normals constexpr uint32_t MOTV = 0x4D4F5456; // Texture coords // Read utilities template T read(const std::vector& data, uint32_t& offset) { if (offset + sizeof(T) > data.size()) { return T{}; } T value; std::memcpy(&value, &data[offset], sizeof(T)); offset += sizeof(T); return value; } template std::vector readArray(const std::vector& data, uint32_t offset, uint32_t count) { std::vector result; if (offset + count * sizeof(T) > data.size()) { return result; } result.resize(count); std::memcpy(result.data(), &data[offset], count * sizeof(T)); return result; } std::string readString(const std::vector& data, uint32_t offset) { std::string result; while (offset < data.size() && data[offset] != 0) { result += static_cast(data[offset++]); } return result; } } // anonymous namespace WMOModel WMOLoader::load(const std::vector& wmoData) { WMOModel model; if (wmoData.size() < 8) { core::Logger::getInstance().error("WMO data too small"); return model; } core::Logger::getInstance().info("Loading WMO model..."); uint32_t offset = 0; // Parse chunks while (offset + 8 <= wmoData.size()) { uint32_t chunkId = read(wmoData, offset); uint32_t chunkSize = read(wmoData, offset); if (offset + chunkSize > wmoData.size()) { core::Logger::getInstance().warning("Chunk extends beyond file"); break; } uint32_t chunkStart = offset; uint32_t chunkEnd = offset + chunkSize; switch (chunkId) { case MVER: { model.version = read(wmoData, offset); core::Logger::getInstance().info("WMO version: ", model.version); break; } case MOHD: { // Header model.nGroups = read(wmoData, offset); model.nPortals = read(wmoData, offset); model.nLights = read(wmoData, offset); model.nDoodadNames = read(wmoData, offset); model.nDoodadDefs = read(wmoData, offset); model.nDoodadSets = read(wmoData, offset); [[maybe_unused]] uint32_t ambColor = read(wmoData, offset); // Ambient color [[maybe_unused]] uint32_t wmoID = read(wmoData, offset); model.boundingBoxMin.x = read(wmoData, offset); model.boundingBoxMin.y = read(wmoData, offset); model.boundingBoxMin.z = read(wmoData, offset); model.boundingBoxMax.x = read(wmoData, offset); model.boundingBoxMax.y = read(wmoData, offset); model.boundingBoxMax.z = read(wmoData, offset); core::Logger::getInstance().info("WMO groups: ", model.nGroups); break; } case MOTX: { // Textures - raw block of null-terminated strings // Material texture1/texture2/texture3 are byte offsets into this chunk. // We must map every offset to its texture index. uint32_t texOffset = chunkStart; uint32_t texIndex = 0; core::Logger::getInstance().info("MOTX chunk: ", chunkSize, " bytes"); while (texOffset < chunkEnd) { uint32_t relativeOffset = texOffset - chunkStart; std::string texName = readString(wmoData, texOffset); if (texName.empty()) { // Skip null bytes (empty entries or padding) texOffset++; continue; } // Store mapping from byte offset to texture index model.textureOffsetToIndex[relativeOffset] = texIndex; model.textures.push_back(texName); core::Logger::getInstance().info(" MOTX texture[", texIndex, "] at offset ", relativeOffset, ": ", texName); texOffset += texName.length() + 1; texIndex++; } core::Logger::getInstance().info("WMO textures: ", model.textures.size()); break; } case MOMT: { // Materials - dump raw fields to find correct layout uint32_t nMaterials = chunkSize / 64; // Each material is 64 bytes for (uint32_t i = 0; i < nMaterials; i++) { // Read all 16 uint32 fields (64 bytes) uint32_t fields[16]; for (int j = 0; j < 16; j++) { fields[j] = read(wmoData, offset); } // SMOMaterial layout (wowdev.wiki): // 0: flags, 1: shader, 2: blendMode // 3: texture_1 (MOTX offset) // 4: sidnColor (emissive), 5: frameSidnColor // 6: texture_2 (MOTX offset) // 7: diffColor, 8: ground_type // 9: texture_3 (MOTX offset) // 10: color_2, 11: flags2 // 12-15: runtime WMOMaterial mat; mat.flags = fields[0]; mat.shader = fields[1]; mat.blendMode = fields[2]; mat.texture1 = fields[3]; mat.color1 = fields[4]; mat.texture2 = fields[6]; // Skip frameSidnColor at [5] mat.color2 = fields[7]; mat.texture3 = fields[9]; // Skip ground_type at [8] mat.color3 = fields[10]; model.materials.push_back(mat); } core::Logger::getInstance().info("WMO materials: ", model.materials.size()); break; } case MOGN: { // Group names uint32_t nameOffset = chunkStart; while (nameOffset < chunkEnd) { std::string name = readString(wmoData, nameOffset); if (name.empty()) break; model.groupNames.push_back(name); nameOffset += name.length() + 1; } core::Logger::getInstance().info("WMO group names: ", model.groupNames.size()); break; } case MOGI: { // Group info uint32_t nGroupInfo = chunkSize / 32; // Each group info is 32 bytes for (uint32_t i = 0; i < nGroupInfo; i++) { WMOGroupInfo info; info.flags = read(wmoData, offset); info.boundingBoxMin.x = read(wmoData, offset); info.boundingBoxMin.y = read(wmoData, offset); info.boundingBoxMin.z = read(wmoData, offset); info.boundingBoxMax.x = read(wmoData, offset); info.boundingBoxMax.y = read(wmoData, offset); info.boundingBoxMax.z = read(wmoData, offset); info.nameOffset = read(wmoData, offset); model.groupInfo.push_back(info); } core::Logger::getInstance().info("WMO group info: ", model.groupInfo.size()); break; } case MOLT: { // Lights uint32_t nLights = chunkSize / 48; // Approximate size for (uint32_t i = 0; i < nLights && offset < chunkEnd; i++) { WMOLight light; light.type = read(wmoData, offset); light.useAttenuation = read(wmoData, offset); light.pad[0] = read(wmoData, offset); light.pad[1] = read(wmoData, offset); light.pad[2] = read(wmoData, offset); light.color.r = read(wmoData, offset); light.color.g = read(wmoData, offset); light.color.b = read(wmoData, offset); light.color.a = read(wmoData, offset); light.position.x = read(wmoData, offset); light.position.y = read(wmoData, offset); light.position.z = read(wmoData, offset); light.intensity = read(wmoData, offset); light.attenuationStart = read(wmoData, offset); light.attenuationEnd = read(wmoData, offset); for (int j = 0; j < 4; j++) { light.unknown[j] = read(wmoData, offset); } model.lights.push_back(light); } core::Logger::getInstance().info("WMO lights: ", model.lights.size()); break; } case MODN: { // Doodad names — stored by byte offset into the MODN chunk // (MODD nameIndex is a byte offset, not a vector index) uint32_t nameOffset = 0; // Offset relative to chunk start while (chunkStart + nameOffset < chunkEnd) { std::string name = readString(wmoData, chunkStart + nameOffset); if (!name.empty()) { model.doodadNames[nameOffset] = name; } nameOffset += name.length() + 1; } core::Logger::getInstance().debug("Loaded ", model.doodadNames.size(), " doodad names"); break; } case MODD: { // Doodad definitions uint32_t nDoodads = chunkSize / 40; // Each doodad is 40 bytes for (uint32_t i = 0; i < nDoodads; i++) { WMODoodad doodad; // Name index (3 bytes) + flags (1 byte) uint32_t nameAndFlags = read(wmoData, offset); doodad.nameIndex = nameAndFlags & 0x00FFFFFF; doodad.position.x = read(wmoData, offset); doodad.position.y = read(wmoData, offset); doodad.position.z = read(wmoData, offset); // C4Quaternion in file: x, y, z, w doodad.rotation.x = read(wmoData, offset); doodad.rotation.y = read(wmoData, offset); doodad.rotation.z = read(wmoData, offset); doodad.rotation.w = read(wmoData, offset); doodad.scale = read(wmoData, offset); uint32_t color = read(wmoData, offset); doodad.color.b = ((color >> 0) & 0xFF) / 255.0f; doodad.color.g = ((color >> 8) & 0xFF) / 255.0f; doodad.color.r = ((color >> 16) & 0xFF) / 255.0f; doodad.color.a = ((color >> 24) & 0xFF) / 255.0f; model.doodads.push_back(doodad); } core::Logger::getInstance().info("WMO doodads: ", model.doodads.size()); break; } case MODS: { // Doodad sets uint32_t nSets = chunkSize / 32; // Each set is 32 bytes for (uint32_t i = 0; i < nSets; i++) { WMODoodadSet set; std::memcpy(set.name, &wmoData[offset], 20); offset += 20; set.startIndex = read(wmoData, offset); set.count = read(wmoData, offset); set.padding = read(wmoData, offset); model.doodadSets.push_back(set); } core::Logger::getInstance().info("WMO doodad sets: ", model.doodadSets.size()); break; } case MOPV: { // Portal vertices uint32_t nVerts = chunkSize / 12; // Each vertex is 3 floats for (uint32_t i = 0; i < nVerts; i++) { glm::vec3 vert; vert.x = read(wmoData, offset); vert.y = read(wmoData, offset); vert.z = read(wmoData, offset); model.portalVertices.push_back(vert); } break; } case MOPT: { // Portal info uint32_t nPortals = chunkSize / 20; // Each portal reference is 20 bytes for (uint32_t i = 0; i < nPortals; i++) { WMOPortal portal; portal.startVertex = read(wmoData, offset); portal.vertexCount = read(wmoData, offset); portal.planeIndex = read(wmoData, offset); portal.padding = read(wmoData, offset); // Skip additional data (12 bytes) offset += 12; model.portals.push_back(portal); } core::Logger::getInstance().info("WMO portals: ", model.portals.size()); break; } default: // Unknown chunk, skip it break; } offset = chunkEnd; } // Initialize groups array model.groups.resize(model.nGroups); core::Logger::getInstance().info("WMO model loaded successfully"); return model; } bool WMOLoader::loadGroup(const std::vector& groupData, WMOModel& model, uint32_t groupIndex) { if (groupIndex >= model.groups.size()) { core::Logger::getInstance().error("Invalid group index: ", groupIndex); return false; } if (groupData.size() < 20) { core::Logger::getInstance().error("WMO group file too small"); return false; } auto& group = model.groups[groupIndex]; group.groupId = groupIndex; uint32_t offset = 0; // Parse chunks in group file while (offset + 8 < groupData.size()) { uint32_t chunkId = read(groupData, offset); uint32_t chunkSize = read(groupData, offset); uint32_t chunkEnd = offset + chunkSize; if (chunkEnd > groupData.size()) { break; } if (chunkId == MVER) { // Version - skip } else if (chunkId == MOGP) { // Group header - parse sub-chunks // MOGP header is 68 bytes, followed by sub-chunks if (chunkSize < 68) { offset = chunkEnd; continue; } // Read MOGP header uint32_t mogpOffset = offset; group.flags = read(groupData, mogpOffset); group.boundingBoxMin.x = read(groupData, mogpOffset); group.boundingBoxMin.y = read(groupData, mogpOffset); group.boundingBoxMin.z = read(groupData, mogpOffset); group.boundingBoxMax.x = read(groupData, mogpOffset); group.boundingBoxMax.y = read(groupData, mogpOffset); group.boundingBoxMax.z = read(groupData, mogpOffset); mogpOffset += 4; // nameOffset group.portalStart = read(groupData, mogpOffset); group.portalCount = read(groupData, mogpOffset); mogpOffset += 8; // transBatchCount, intBatchCount, extBatchCount, padding group.fogIndices[0] = read(groupData, mogpOffset); group.fogIndices[1] = read(groupData, mogpOffset); group.fogIndices[2] = read(groupData, mogpOffset); group.fogIndices[3] = read(groupData, mogpOffset); group.liquidType = read(groupData, mogpOffset); // Skip to end of 68-byte header mogpOffset = offset + 68; // Parse sub-chunks within MOGP while (mogpOffset + 8 < chunkEnd) { uint32_t subChunkId = read(groupData, mogpOffset); uint32_t subChunkSize = read(groupData, mogpOffset); uint32_t subChunkEnd = mogpOffset + subChunkSize; if (subChunkEnd > chunkEnd) { break; } // Debug: log chunk magic as string char magic[5] = {0}; magic[0] = (subChunkId >> 0) & 0xFF; magic[1] = (subChunkId >> 8) & 0xFF; magic[2] = (subChunkId >> 16) & 0xFF; magic[3] = (subChunkId >> 24) & 0xFF; static int logCount = 0; if (logCount < 30) { core::Logger::getInstance().debug(" WMO sub-chunk: ", magic, " (0x", std::hex, subChunkId, std::dec, ") size=", subChunkSize); logCount++; } if (subChunkId == 0x4D4F5654) { // MOVT - Vertices uint32_t vertexCount = subChunkSize / 12; // 3 floats per vertex for (uint32_t i = 0; i < vertexCount; i++) { WMOVertex vertex; vertex.position.x = read(groupData, mogpOffset); vertex.position.y = read(groupData, mogpOffset); vertex.position.z = read(groupData, mogpOffset); vertex.normal = glm::vec3(0, 0, 1); vertex.texCoord = glm::vec2(0, 0); vertex.color = glm::vec4(1, 1, 1, 1); group.vertices.push_back(vertex); } } else if (subChunkId == 0x4D4F5649) { // MOVI - Indices uint32_t indexCount = subChunkSize / 2; // uint16_t per index for (uint32_t i = 0; i < indexCount; i++) { group.indices.push_back(read(groupData, mogpOffset)); } } else if (subChunkId == 0x4D4F4E52) { // MONR - Normals uint32_t normalCount = subChunkSize / 12; for (uint32_t i = 0; i < normalCount && i < group.vertices.size(); i++) { group.vertices[i].normal.x = read(groupData, mogpOffset); group.vertices[i].normal.y = read(groupData, mogpOffset); group.vertices[i].normal.z = read(groupData, mogpOffset); } } else if (subChunkId == 0x4D4F5456) { // MOTV - Texture coords // Update texture coords for existing vertices uint32_t texCoordCount = subChunkSize / 8; core::Logger::getInstance().info(" MOTV: ", texCoordCount, " tex coords for ", group.vertices.size(), " vertices"); for (uint32_t i = 0; i < texCoordCount && i < group.vertices.size(); i++) { group.vertices[i].texCoord.x = read(groupData, mogpOffset); group.vertices[i].texCoord.y = read(groupData, mogpOffset); } if (texCoordCount > 0 && !group.vertices.empty()) { core::Logger::getInstance().debug(" First UV: (", group.vertices[0].texCoord.x, ", ", group.vertices[0].texCoord.y, ")"); } } else if (subChunkId == 0x4D4F4356) { // MOCV - Vertex colors // Update vertex colors uint32_t colorCount = subChunkSize / 4; for (uint32_t i = 0; i < colorCount && i < group.vertices.size(); i++) { uint8_t b = read(groupData, mogpOffset); uint8_t g = read(groupData, mogpOffset); uint8_t r = read(groupData, mogpOffset); uint8_t a = read(groupData, mogpOffset); group.vertices[i].color = glm::vec4(r/255.0f, g/255.0f, b/255.0f, a/255.0f); } } else if (subChunkId == 0x4D4F4241) { // MOBA - Batches // SMOBatch structure (24 bytes): // - 6 x int16 bounding box (12 bytes) // - uint32 startIndex (4 bytes) // - uint16 count (2 bytes) // - uint16 minIndex (2 bytes) // - uint16 maxIndex (2 bytes) // - uint8 flags (1 byte) // - uint8 material_id (1 byte) uint32_t batchCount = subChunkSize / 24; for (uint32_t i = 0; i < batchCount; i++) { WMOBatch batch; mogpOffset += 12; // Skip bounding box (6 x int16 = 12 bytes) batch.startIndex = read(groupData, mogpOffset); batch.indexCount = read(groupData, mogpOffset); batch.startVertex = read(groupData, mogpOffset); batch.lastVertex = read(groupData, mogpOffset); batch.flags = read(groupData, mogpOffset); batch.materialId = read(groupData, mogpOffset); group.batches.push_back(batch); static int batchLogCount = 0; if (batchLogCount < 15) { core::Logger::getInstance().info(" Batch[", i, "]: start=", batch.startIndex, " count=", batch.indexCount, " verts=[", batch.startVertex, "-", batch.lastVertex, "] mat=", (int)batch.materialId, " flags=", (int)batch.flags); batchLogCount++; } } } mogpOffset = subChunkEnd; } } offset = chunkEnd; } // Create a default batch if none were loaded if (group.batches.empty() && !group.indices.empty()) { WMOBatch batch; batch.startIndex = 0; batch.indexCount = static_cast(group.indices.size()); batch.materialId = 0; group.batches.push_back(batch); } core::Logger::getInstance().info("WMO group ", groupIndex, " loaded: ", group.vertices.size(), " vertices, ", group.indices.size(), " indices, ", group.batches.size(), " batches"); return !group.vertices.empty() && !group.indices.empty(); } } // namespace pipeline } // namespace wowee