From ba8f89d76d27edcb5fe64f49cc04668f67e31c50 Mon Sep 17 00:00:00 2001 From: vperus Date: Fri, 6 Mar 2026 18:37:19 +0200 Subject: [PATCH 1/4] container: added more required dependencies --- container/builder-ubuntu.Dockerfile | 2 ++ 1 file changed, 2 insertions(+) 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/* From 7971e71d1bd13e2b70fdc90e443f112e2378e445 Mon Sep 17 00:00:00 2001 From: vperus Date: Fri, 6 Mar 2026 18:41:41 +0200 Subject: [PATCH 2/4] Replace heap allocation with view --- src/core/logger.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) 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); From ee4e6a31cee87caca02a77203f9021b74099114a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Mar 2026 10:37:32 -0800 Subject: [PATCH 3/4] Filter WMO decorative geometry from collision, fix tram portal trigger IDs Parse MOPY per-triangle flags in WMO groups and exclude detail/decorative triangles (flag 0x04) from collision detection. This prevents invisible walls from objects like gears and railings in WMO interiors. Add WotLK area trigger IDs 2173/2175 to extended-range tram triggers. --- include/pipeline/wmo_loader.hpp | 1 + src/game/game_handler.cpp | 3 ++- src/pipeline/wmo_loader.cpp | 10 ++++++++++ src/rendering/wmo_renderer.cpp | 19 +++++++++++++++++-- 4 files changed, 30 insertions(+), 3 deletions(-) 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/src/game/game_handler.cpp b/src/game/game_handler.cpp index 71a65c92..714d870b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8812,8 +8812,9 @@ void GameHandler::checkAreaTriggers() { // 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 + 712, 713, // Stormwind/Ironforge → Deeprun Tram (classic IDs) 2166, 2171, // Tram interior exit triggers + 2173, 2175, // Stormwind/Ironforge tram entrance (WotLK IDs) }; for (const auto& at : areaTriggers_) { 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/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 7c2558d5..ee050856 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1871,12 +1871,27 @@ 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()) { + // Filter out non-collidable triangles + resources.collisionIndices.reserve(group.indices.size()); + size_t numTris = group.indices.size() / 3; + for (size_t t = 0; t < numTris; t++) { + uint8_t flags = (t < group.triFlags.size()) ? group.triFlags[t] : 0; + if (flags & 0x04) continue; // detail/decorative — skip collision + resources.collisionIndices.push_back(group.indices[t * 3 + 0]); + resources.collisionIndices.push_back(group.indices[t * 3 + 1]); + resources.collisionIndices.push_back(group.indices[t * 3 + 2]); + } + } else { + resources.collisionIndices = group.indices; + } // Compute actual bounding box from vertices (WMO header bboxes can be unreliable) if (!resources.collisionVertices.empty()) { From 4cbceced675703f0f020fb3e755396a08fa2376c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Mar 2026 12:26:17 -0800 Subject: [PATCH 4/4] Fix invisible walls from WMO doodad M2 collision and MOPY filtering WMO interior doodads (gears, decorations) were blocking player movement via M2 collision. Skip collision for all WMO doodad M2 instances since the WMO itself handles wall collision. Also filter WMO wall collision using MOPY per-triangle flags: only rendered+collidable triangles block the player, skipping invisible collision hulls. Revert tram portal extended range (no longer needed with collision fix). --- include/rendering/m2_renderer.hpp | 2 ++ include/rendering/wmo_renderer.hpp | 3 +++ src/core/application.cpp | 4 +++- src/game/game_handler.cpp | 13 ++----------- src/rendering/m2_renderer.cpp | 11 +++++++++++ src/rendering/terrain_manager.cpp | 5 ++++- src/rendering/wmo_renderer.cpp | 26 +++++++++++++++++++------- 7 files changed, 44 insertions(+), 20 deletions(-) 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/game/game_handler.cpp b/src/game/game_handler.cpp index 714d870b..5ff720b3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8809,22 +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 (classic IDs) - 2166, 2171, // Tram interior exit triggers - 2173, 2175, // Stormwind/Ironforge tram entrance (WotLK IDs) - }; - 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; @@ -8832,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/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 ee050856..ce4d4ab1 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1879,15 +1879,12 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes resources.collisionVertices.push_back(v.position); } if (!group.triFlags.empty()) { - // Filter out non-collidable triangles - resources.collisionIndices.reserve(group.indices.size()); + // 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++) { - uint8_t flags = (t < group.triFlags.size()) ? group.triFlags[t] : 0; - if (flags & 0x04) continue; // detail/decorative — skip collision - resources.collisionIndices.push_back(group.indices[t * 3 + 0]); - resources.collisionIndices.push_back(group.indices[t * 3 + 1]); - resources.collisionIndices.push_back(group.indices[t * 3 + 2]); + resources.triMopyFlags[t] = (t < group.triFlags.size()) ? group.triFlags[t] : 0; } } else { resources.collisionIndices = group.indices; @@ -3102,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]];