diff --git a/container/builder-ubuntu.Dockerfile b/container/builder-ubuntu.Dockerfile index 13da080f..26f32b50 100644 --- a/container/builder-ubuntu.Dockerfile +++ b/container/builder-ubuntu.Dockerfile @@ -15,6 +15,8 @@ RUN apt-get update && \ libavcodec-dev \ libswscale-dev \ libavutil-dev \ + libvulkan-dev \ + vulkan-tools \ libstorm-dev && \ rm -rf /var/lib/apt/lists/* diff --git a/include/pipeline/wmo_loader.hpp b/include/pipeline/wmo_loader.hpp index fecb287d..ed2cf149 100644 --- a/include/pipeline/wmo_loader.hpp +++ b/include/pipeline/wmo_loader.hpp @@ -157,6 +157,7 @@ struct WMOGroup { std::vector vertices; std::vector indices; std::vector batches; + std::vector triFlags; // Per-triangle MOPY flags (0x04 = detail/no-collide) // Portals std::vector portals; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 83762a5c..6b8ebc15 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -182,6 +182,7 @@ struct M2Instance { bool cachedIsGroundDetail = false; bool cachedIsInvisibleTrap = false; bool cachedIsValid = false; + bool skipCollision = false; // WMO interior doodads — skip player wall collision float cachedBoundRadius = 0.0f; // Frame-skip optimization (update distant animations less frequently) @@ -287,6 +288,7 @@ public: void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen); void removeInstance(uint32_t instanceId); void removeInstances(const std::vector& instanceIds); + void setSkipCollision(uint32_t instanceId, bool skip); void clear(); void cleanupUnusedModels(); diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 87c60a66..4587d0b7 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -409,6 +409,9 @@ private: // Pre-computed per-triangle normals (unit length, indexed by triStart/3) std::vector triNormals; + // Per-collision-triangle MOPY flags (indexed by collision tri index, i.e. triStart/3) + std::vector triMopyFlags; + // Scratch bitset for deduplicating triangle queries (sized to numTriangles) mutable std::vector triVisited; diff --git a/src/core/application.cpp b/src/core/application.cpp index a162dba3..8c94289a 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3719,7 +3719,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); m2Renderer->loadModel(m2Model, doodadModelId); - m2Renderer->createInstance(doodadModelId, worldPos, glm::vec3(0.0f), doodad.scale); + uint32_t doodadInstId = m2Renderer->createInstance(doodadModelId, worldPos, glm::vec3(0.0f), doodad.scale); + if (doodadInstId) m2Renderer->setSkipCollision(doodadInstId, true); loadedDoodads++; } LOG_INFO("Loaded ", loadedDoodads, " instance WMO doodads"); @@ -6735,6 +6736,7 @@ void Application::processPendingTransportDoodads() { m2Renderer->loadModel(m2Model, doodadModelId); uint32_t m2InstanceId = m2Renderer->createInstance(doodadModelId, glm::vec3(0.0f), glm::vec3(0.0f), 1.0f); if (m2InstanceId == 0) continue; + m2Renderer->setSkipCollision(m2InstanceId, true); wmoRenderer->addDoodadToInstance(it->instanceId, m2InstanceId, doodadTemplate.localTransform); it->spawnedDoodads++; diff --git a/src/core/logger.cpp b/src/core/logger.cpp index cdc1afc6..0a85d6df 100644 --- a/src/core/logger.cpp +++ b/src/core/logger.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include namespace wowee { namespace core { @@ -42,15 +45,16 @@ void Logger::ensureFile() { } } if (const char* level = std::getenv("WOWEE_LOG_LEVEL")) { - std::string v(level); - std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - if (v == "debug") setLogLevel(LogLevel::DEBUG); - else if (v == "info") setLogLevel(LogLevel::INFO); - else if (v == "warn" || v == "warning") setLogLevel(LogLevel::WARNING); - else if (v == "error") setLogLevel(kLogLevelError); - else if (v == "fatal") setLogLevel(LogLevel::FATAL); + auto toLower = [] (unsigned char c) { return std::tolower(c); }; + using namespace std::literals; + + auto v = std::string_view{level} | std::views::transform(toLower); + if (std::ranges::equal(v, "debug"sv)) setLogLevel(LogLevel::DEBUG); + else if (std::ranges::equal(v, "info"sv)) setLogLevel(LogLevel::INFO); + else if (std::ranges::equal(v, "warn"sv) || std::ranges::equal(v, "warning"sv)) + setLogLevel(LogLevel::WARNING); + else if (std::ranges::equal(v, "error"sv)) setLogLevel(kLogLevelError); + else if (std::ranges::equal(v, "fatal"sv)) setLogLevel(LogLevel::FATAL); } std::error_code ec; std::filesystem::create_directories("logs", ec); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 71a65c92..5ff720b3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8809,21 +8809,13 @@ void GameHandler::checkAreaTriggers() { areaTriggerSuppressFirst_ = false; } - // Deeprun Tram entrance triggers need extended range because WMO - // collision walls block the player from reaching the trigger center. - static const std::unordered_set extendedRangeTriggers = { - 712, 713, // Stormwind/Ironforge → Deeprun Tram - 2166, 2171, // Tram interior exit triggers - }; - for (const auto& at : areaTriggers_) { if (at.mapId != currentMapId_) continue; - const bool extended = extendedRangeTriggers.count(at.id) > 0; bool inside = false; if (at.radius > 0.0f) { // Sphere trigger — small minimum so player must be near the portal - float effectiveRadius = std::max(at.radius, extended ? 45.0f : 12.0f); + float effectiveRadius = std::max(at.radius, 12.0f); float dx = px - at.x; float dy = py - at.y; float dz = pz - at.z; @@ -8831,7 +8823,7 @@ void GameHandler::checkAreaTriggers() { inside = (distSq <= effectiveRadius * effectiveRadius); } else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) { // Box trigger — small minimum so player must walk into the portal area - float boxMin = extended ? 60.0f : 16.0f; + float boxMin = 16.0f; float effLength = std::max(at.boxLength, boxMin); float effWidth = std::max(at.boxWidth, boxMin); float effHeight = std::max(at.boxHeight, boxMin); diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index e90a79de..22a4df42 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -498,6 +498,16 @@ bool WMOLoader::loadGroup(const std::vector& groupData, group.indices.push_back(read(groupData, mogpOffset)); } } + else if (subChunkId == 0x4D4F5059) { // MOPY - Triangle material info + // 2 bytes per triangle: flags (uint8) + materialId (uint8) + // flag 0x04 = detail/decorative geometry (no collision) + uint32_t triCount = subChunkSize / 2; + group.triFlags.resize(triCount); + for (uint32_t i = 0; i < triCount; i++) { + group.triFlags[i] = read(groupData, mogpOffset); + read(groupData, mogpOffset); // materialId (skip) + } + } else if (subChunkId == 0x4D4F4E52) { // MONR - Normals uint32_t normalCount = subChunkSize / 12; core::Logger::getInstance().debug(" MONR: ", normalCount, " normals for ", group.vertices.size(), " vertices"); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 2ea99420..925c020b 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -3321,6 +3321,15 @@ void M2Renderer::removeInstance(uint32_t instanceId) { } } +void M2Renderer::setSkipCollision(uint32_t instanceId, bool skip) { + for (auto& inst : instances) { + if (inst.id == instanceId) { + inst.skipCollision = skip; + return; + } + } +} + void M2Renderer::removeInstances(const std::vector& instanceIds) { if (instanceIds.empty() || instances.empty()) { return; @@ -3649,6 +3658,7 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ, const M2ModelGPU& model = it->second; if (model.collisionNoBlock || model.isInvisibleTrap || model.isSpellEffect) continue; + if (instance.skipCollision) continue; // --- Mesh-based floor: vertical ray vs collision triangles --- // Does NOT skip the AABB path — both contribute and highest wins. @@ -3803,6 +3813,7 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, const M2ModelGPU& model = it->second; if (model.collisionNoBlock || model.isInvisibleTrap || model.isSpellEffect) continue; + if (instance.skipCollision) continue; if (instance.scale <= 0.001f) continue; // --- Mesh-based wall collision: closest-point push --- diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index ccef1dae..9ecd3df9 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -865,7 +865,10 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { m2Renderer->loadModel(doodad.model, doodad.modelId); uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix( doodad.modelId, doodad.modelMatrix, doodad.worldPosition); - if (wmoDoodadInstId) ft.m2InstanceIds.push_back(wmoDoodadInstId); + if (wmoDoodadInstId) { + m2Renderer->setSkipCollision(wmoDoodadInstId, true); + ft.m2InstanceIds.push_back(wmoDoodadInstId); + } ft.wmoDoodadIndex++; if (ft.wmoDoodadIndex < pending->wmoDoodads.size()) return false; } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 7c2558d5..ce4d4ab1 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1871,12 +1871,24 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes resources.indexBuffer = idxBuf.buffer; resources.indexAlloc = idxBuf.allocation; - // Store collision geometry for floor raycasting + // Store collision geometry for floor raycasting. + // Use MOPY per-triangle flags to exclude detail/decorative geometry (flag 0x04) + // from collision — these are things like gears, railings, etc. resources.collisionVertices.reserve(group.vertices.size()); for (const auto& v : group.vertices) { resources.collisionVertices.push_back(v.position); } - resources.collisionIndices = group.indices; + if (!group.triFlags.empty()) { + // Store all triangles but tag each with MOPY flags for collision filtering + resources.collisionIndices = group.indices; + size_t numTris = group.indices.size() / 3; + resources.triMopyFlags.resize(numTris, 0); + for (size_t t = 0; t < numTris; t++) { + resources.triMopyFlags[t] = (t < group.triFlags.size()) ? group.triFlags[t] : 0; + } + } else { + resources.collisionIndices = group.indices; + } // Compute actual bounding box from vertices (WMO header bboxes can be unreliable) if (!resources.collisionVertices.empty()) { @@ -3087,6 +3099,21 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, float triHeight = tb.maxZ - tb.minZ; if (triHeight < 1.0f && tb.maxZ <= localFeetZ + 1.2f) continue; + // Use MOPY flags to filter wall collision. + // Only RENDERED triangles (flag 0x20) with collision intent (0x01) + // should block the player. Skip invisible collision hulls (0x08/0x48) + // and non-collidable render-only geometry. + uint32_t triIdx = triStart / 3; + if (!group.triMopyFlags.empty() && triIdx < group.triMopyFlags.size()) { + uint8_t mopy = group.triMopyFlags[triIdx]; + // Must be rendered (0x20) AND have base collision flag (0x01) + bool rendered = (mopy & 0x20) != 0; + bool collidable = (mopy & 0x01) != 0; + if (mopy != 0 && !(rendered && collidable)) { + continue; + } + } + const glm::vec3& v0 = verts[indices[triStart]]; const glm::vec3& v1 = verts[indices[triStart + 1]]; const glm::vec3& v2 = verts[indices[triStart + 2]];