mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +00:00
Add MCLQ water, TaxiPathNode transports, and vanilla M2 particles
- Parse MCLQ sub-chunks in vanilla ADTs for water rendering (WotLK uses MH2O) - Load TaxiPathNode.dbc for MO_TRANSPORT world-coordinate paths (vanilla boats) - Parse data[] from SMSG_GAMEOBJECT_QUERY_RESPONSE (taxiPathId for transports) - Support vanilla M2 particle emitters (504-byte struct, different from WotLK 476) - Add character preview texture diagnostic logging - Fix disconnect handling on character screen (show error only when no chars)
This commit is contained in:
parent
cbb3035313
commit
bf31da8c13
14 changed files with 556 additions and 55 deletions
|
|
@ -374,6 +374,17 @@ void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTT
|
|||
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) {
|
||||
|
|
@ -453,6 +464,100 @@ void ADTLoader::parseMCAL(const uint8_t* data, size_t size, MapChunk& chunk) {
|
|||
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<float> 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<uint8_t> 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
|
||||
|
|
|
|||
|
|
@ -1050,26 +1050,34 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup);
|
||||
}
|
||||
|
||||
// Parse particle emitters (WotLK M2ParticleOld: 0x1DC = 476 bytes per emitter)
|
||||
// Skip for vanilla — emitter struct size differs
|
||||
static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1DC;
|
||||
if (header.version >= 264 &&
|
||||
header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
|
||||
header.nParticleEmitters < 256 &&
|
||||
static_cast<size_t>(header.ofsParticleEmitters) +
|
||||
static_cast<size_t>(header.nParticleEmitters) * EMITTER_STRUCT_SIZE <= m2Data.size()) {
|
||||
// Parse particle emitters — struct size differs between versions:
|
||||
// WotLK (version >= 264): M2ParticleOld = 0x1DC (476) bytes, M2TrackDisk (20 bytes), FBlocks
|
||||
// Vanilla (version < 264): 0x1F8 (504) bytes, M2TrackDiskVanilla (28 bytes), static lifecycle arrays
|
||||
if (header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
|
||||
header.nParticleEmitters < 256) {
|
||||
|
||||
// Build sequence flags for parseAnimTrack
|
||||
const bool isVanilla = (header.version < 264);
|
||||
static constexpr uint32_t EMITTER_SIZE_WOTLK = 0x1DC; // 476
|
||||
static constexpr uint32_t EMITTER_SIZE_VANILLA = 0x1F8; // 504
|
||||
const uint32_t emitterSize = isVanilla ? EMITTER_SIZE_VANILLA : EMITTER_SIZE_WOTLK;
|
||||
|
||||
if (static_cast<size_t>(header.ofsParticleEmitters) +
|
||||
static_cast<size_t>(header.nParticleEmitters) * emitterSize <= m2Data.size()) {
|
||||
|
||||
// Build sequence flags for parseAnimTrack (WotLK only)
|
||||
std::vector<uint32_t> emSeqFlags;
|
||||
emSeqFlags.reserve(model.sequences.size());
|
||||
for (const auto& seq : model.sequences) {
|
||||
emSeqFlags.push_back(seq.flags);
|
||||
if (!isVanilla) {
|
||||
emSeqFlags.reserve(model.sequences.size());
|
||||
for (const auto& seq : model.sequences) {
|
||||
emSeqFlags.push_back(seq.flags);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t ei = 0; ei < header.nParticleEmitters; ei++) {
|
||||
uint32_t base = header.ofsParticleEmitters + ei * EMITTER_STRUCT_SIZE;
|
||||
uint32_t base = header.ofsParticleEmitters + ei * emitterSize;
|
||||
|
||||
M2ParticleEmitter em;
|
||||
// Header fields (0x00-0x33) are the same for both versions
|
||||
em.particleId = readValue<int32_t>(m2Data, base + 0x00);
|
||||
em.flags = readValue<uint32_t>(m2Data, base + 0x04);
|
||||
em.position.x = readValue<float>(m2Data, base + 0x08);
|
||||
|
|
@ -1085,32 +1093,97 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
if (em.textureRows == 0) em.textureRows = 1;
|
||||
if (em.textureCols == 0) em.textureCols = 1;
|
||||
|
||||
// Parse animated tracks (M2TrackDisk at known offsets)
|
||||
auto parseTrack = [&](uint32_t off, M2AnimationTrack& track) {
|
||||
if (base + off + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + off);
|
||||
parseAnimTrack(m2Data, disk, track, TrackType::FLOAT, emSeqFlags);
|
||||
}
|
||||
};
|
||||
parseTrack(0x34, em.emissionSpeed);
|
||||
parseTrack(0x48, em.speedVariation);
|
||||
parseTrack(0x5C, em.verticalRange);
|
||||
parseTrack(0x70, em.horizontalRange);
|
||||
parseTrack(0x84, em.gravity);
|
||||
parseTrack(0x98, em.lifespan);
|
||||
parseTrack(0xB0, em.emissionRate);
|
||||
parseTrack(0xC8, em.emissionAreaLength);
|
||||
parseTrack(0xDC, em.emissionAreaWidth);
|
||||
parseTrack(0xF0, em.deceleration);
|
||||
if (isVanilla) {
|
||||
// Vanilla: 10 contiguous M2TrackDiskVanilla tracks (28 bytes each) at 0x34
|
||||
auto parseTrackV = [&](uint32_t off, M2AnimationTrack& track) {
|
||||
if (base + off + sizeof(M2TrackDiskVanilla) <= m2Data.size()) {
|
||||
M2TrackDiskVanilla disk = readValue<M2TrackDiskVanilla>(m2Data, base + off);
|
||||
parseAnimTrackVanilla(m2Data, disk, track, TrackType::FLOAT);
|
||||
}
|
||||
};
|
||||
parseTrackV(0x34, em.emissionSpeed); // +28 = 0x50
|
||||
parseTrackV(0x50, em.speedVariation); // +28 = 0x6C
|
||||
parseTrackV(0x6C, em.verticalRange); // +28 = 0x88
|
||||
parseTrackV(0x88, em.horizontalRange); // +28 = 0xA4
|
||||
parseTrackV(0xA4, em.gravity); // +28 = 0xC0
|
||||
parseTrackV(0xC0, em.lifespan); // +28 = 0xDC
|
||||
parseTrackV(0xDC, em.emissionRate); // +28 = 0xF8
|
||||
parseTrackV(0xF8, em.emissionAreaLength); // +28 = 0x114
|
||||
parseTrackV(0x114, em.emissionAreaWidth); // +28 = 0x130
|
||||
parseTrackV(0x130, em.deceleration); // +28 = 0x14C
|
||||
|
||||
// Parse FBlocks (color, alpha, scale) — FBlocks are 16 bytes each
|
||||
parseFBlock(m2Data, base + 0x104, em.particleColor, 0);
|
||||
parseFBlock(m2Data, base + 0x114, em.particleAlpha, 1);
|
||||
parseFBlock(m2Data, base + 0x124, em.particleScale, 2);
|
||||
// Vanilla: NO FBlocks — color/alpha/scale are static inline values
|
||||
// Layout (empirically confirmed from real vanilla M2 files):
|
||||
// +0x14C: float midpoint (lifecycle split: 0→mid→1)
|
||||
// +0x150: uint32 colorValues[3] (BGRA, A channel = opacity)
|
||||
// +0x15C: float scaleValues[3] (1D particle scale)
|
||||
float midpoint = readValue<float>(m2Data, base + 0x14C);
|
||||
if (midpoint < 0.0f || midpoint > 1.0f) midpoint = 0.5f;
|
||||
|
||||
// Synthesize color FBlock from static BGRA values
|
||||
{
|
||||
em.particleColor.timestamps = {0.0f, midpoint, 1.0f};
|
||||
em.particleColor.vec3Values.resize(3);
|
||||
em.particleAlpha.timestamps = {0.0f, midpoint, 1.0f};
|
||||
em.particleAlpha.floatValues.resize(3);
|
||||
for (int c = 0; c < 3; c++) {
|
||||
uint32_t bgra = readValue<uint32_t>(m2Data, base + 0x150 + c * 4);
|
||||
float b = ((bgra >> 0) & 0xFF) / 255.0f;
|
||||
float g = ((bgra >> 8) & 0xFF) / 255.0f;
|
||||
float r = ((bgra >> 16) & 0xFF) / 255.0f;
|
||||
float a = ((bgra >> 24) & 0xFF) / 255.0f;
|
||||
em.particleColor.vec3Values[c] = glm::vec3(r, g, b);
|
||||
em.particleAlpha.floatValues[c] = a;
|
||||
}
|
||||
// If all alpha zero, use sensible default (fade out)
|
||||
bool allZero = true;
|
||||
for (auto v : em.particleAlpha.floatValues) {
|
||||
if (v > 0.01f) { allZero = false; break; }
|
||||
}
|
||||
if (allZero) {
|
||||
em.particleAlpha.floatValues = {1.0f, 1.0f, 0.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize scale FBlock from static float values
|
||||
{
|
||||
em.particleScale.timestamps = {0.0f, midpoint, 1.0f};
|
||||
em.particleScale.floatValues.resize(3);
|
||||
for (int s = 0; s < 3; s++) {
|
||||
float scale = readValue<float>(m2Data, base + 0x15C + s * 4);
|
||||
if (scale < 0.001f || scale > 100.0f) scale = 1.0f;
|
||||
em.particleScale.floatValues[s] = scale;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// WotLK: M2TrackDisk (20 bytes) at known offsets with vary floats interspersed
|
||||
auto parseTrack = [&](uint32_t off, M2AnimationTrack& track) {
|
||||
if (base + off + sizeof(M2TrackDisk) <= m2Data.size()) {
|
||||
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + off);
|
||||
parseAnimTrack(m2Data, disk, track, TrackType::FLOAT, emSeqFlags);
|
||||
}
|
||||
};
|
||||
parseTrack(0x34, em.emissionSpeed);
|
||||
parseTrack(0x48, em.speedVariation);
|
||||
parseTrack(0x5C, em.verticalRange);
|
||||
parseTrack(0x70, em.horizontalRange);
|
||||
parseTrack(0x84, em.gravity);
|
||||
parseTrack(0x98, em.lifespan);
|
||||
parseTrack(0xB0, em.emissionRate);
|
||||
parseTrack(0xC8, em.emissionAreaLength);
|
||||
parseTrack(0xDC, em.emissionAreaWidth);
|
||||
parseTrack(0xF0, em.deceleration);
|
||||
|
||||
// Parse FBlocks (color, alpha, scale) — FBlocks are 16 bytes each
|
||||
parseFBlock(m2Data, base + 0x104, em.particleColor, 0);
|
||||
parseFBlock(m2Data, base + 0x114, em.particleAlpha, 1);
|
||||
parseFBlock(m2Data, base + 0x124, em.particleScale, 2);
|
||||
}
|
||||
|
||||
model.particleEmitters.push_back(std::move(em));
|
||||
}
|
||||
core::Logger::getInstance().debug(" Particle emitters: ", model.particleEmitters.size());
|
||||
} // end size check
|
||||
}
|
||||
|
||||
// Read collision mesh (bounding triangles/vertices/normals)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue