Compare commits

...

6 commits

Author SHA1 Message Date
Kelsi Rae Davis
d6de60e413
Merge pull request #10 from VPeruS/ranges
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Replace heap allocation with view
2026-03-06 16:49:32 -08:00
Kelsi Rae Davis
8e01c3da3e
Merge pull request #9 from VPeruS/docker-build-update
container: added more required dependencies
2026-03-06 16:48:44 -08:00
Kelsi
4cbceced67 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).
2026-03-06 12:26:17 -08:00
Kelsi
ee4e6a31ce 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.
2026-03-06 10:37:32 -08:00
vperus
7971e71d1b Replace heap allocation with view 2026-03-06 18:41:41 +02:00
vperus
ba8f89d76d container: added more required dependencies 2026-03-06 18:37:19 +02:00
11 changed files with 80 additions and 23 deletions

View file

@ -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/*

View file

@ -157,6 +157,7 @@ struct WMOGroup {
std::vector<WMOVertex> vertices;
std::vector<uint16_t> indices;
std::vector<WMOBatch> batches;
std::vector<uint8_t> triFlags; // Per-triangle MOPY flags (0x04 = detail/no-collide)
// Portals
std::vector<WMOPortal> portals;

View file

@ -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<uint32_t>& instanceIds);
void setSkipCollision(uint32_t instanceId, bool skip);
void clear();
void cleanupUnusedModels();

View file

@ -409,6 +409,9 @@ private:
// Pre-computed per-triangle normals (unit length, indexed by triStart/3)
std::vector<glm::vec3> triNormals;
// Per-collision-triangle MOPY flags (indexed by collision tri index, i.e. triStart/3)
std::vector<uint8_t> triMopyFlags;
// Scratch bitset for deduplicating triangle queries (sized to numTriangles)
mutable std::vector<uint8_t> triVisited;

View file

@ -3719,7 +3719,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(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++;

View file

@ -6,6 +6,9 @@
#include <cstdlib>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <iterator>
#include <ranges>
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<char>(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);

View file

@ -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<uint32_t> 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);

View file

@ -498,6 +498,16 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
group.indices.push_back(read<uint16_t>(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<uint8_t>(groupData, mogpOffset);
read<uint8_t>(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");

View file

@ -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<uint32_t>& instanceIds) {
if (instanceIds.empty() || instances.empty()) {
return;
@ -3649,6 +3658,7 @@ std::optional<float> 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 ---

View file

@ -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;
}

View file

@ -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);
}
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]];