fix: filter physics volumes like the client's collision groups

Volumes on the server touched far more than in the client:

- An enemy's body in the physics world was a sphere the size of its aggro
  radius, so trigger and damage volumes caught enemies from far away
  (Cavalry Hill enemies taking damage on spawn). It is now the enemy's
  own radius and collision group from its physics component.
- Trigger volumes ignored their collision group and caught everything.
  dpEntity now filters with the client's collision filter
  (PeCollisionFilter, 1.10.64 0x00fb6940, group table from 0x00fcf9a0):
  POI walls ignore enemies, threat clearing walls ignore players, and
  so on. The aggro sensor keeps seeing only players.
- Rotated boxes were tested as the axis aligned box around them, which
  for a turned wall covers a big square (the AG survival boundary).
  Sphere and point tests now use the box's own axes.

Proximity monitors take an optional collision group like live's
SetProximityRadius; the AM shield generators use live's (10 finds
enemies, 1 finds players).

Fixes #1127
Refs #1971

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 23:57:50 -05:00
parent 2d76c81bba
commit 5d3c2e6f9a
20 changed files with 277 additions and 64 deletions

View File

@@ -1,5 +1,6 @@
set(DPHYSICS_SOURCES "dpCollisionChecks.cpp"
"dpEntity.cpp"
"dpCollisionFilter.cpp"
"dpGrid.cpp"
"dpKnockback.cpp"
"dpShapeBase.cpp"

View File

@@ -100,16 +100,7 @@ bool dpCollisionChecks::CheckSphereBox(dpEntity* a, dpEntity* b) {
spherePos = a->GetPosition();
}
//Get closest point from the box to the sphere center by clamping
float x = std::max(box->m_MinX, std::min(spherePos.x, box->m_MaxX));
float y = std::max(box->m_MinY, std::min(spherePos.y, box->m_MaxY));
float z = std::max(box->m_MinZ, std::min(spherePos.z, box->m_MaxZ));
//Check the distance between that point & our sphere
float dX = x - spherePos.x;
float dY = y - spherePos.y;
float dZ = z - spherePos.z;
float distanceSquared = (dX * dX) + (dY * dY) + (dZ * dZ);
const float distanceSquared = box->SquaredDistanceTo(spherePos);
const float radius = sphere->GetRadius();
return distanceSquared < radius* radius;

View File

@@ -0,0 +1,89 @@
#include "dpCollisionFilter.h"
#include <array>
namespace {
using Table = std::array<uint32_t, dpCollisionFilter::GROUP_COUNT>;
// Group `group` touches every group whose bit is set in `mask`, and each of those touches it back
constexpr void Enable(Table& table, const uint32_t group, const uint32_t mask) {
table[group - 1] |= mask;
for (uint32_t other = 0; other < dpCollisionFilter::GROUP_COUNT; other++) {
if (mask & (1u << other)) table[other] |= 1u << (group - 1);
}
}
// The client's calls, in its order
constexpr Table BuildTable() {
Table table{};
Enable(table, 1, 0x96d9);
Enable(table, 2, 0x9400);
Enable(table, 10, 0x1cc9);
Enable(table, 3, 0x204d8);
Enable(table, 12, 0x24ed8);
Enable(table, 4, 0x1604);
Enable(table, 13, 0x164b);
Enable(table, 14, 0x1000);
Enable(table, 15, 0x246d9);
Enable(table, 24, 0xc000);
Enable(table, 5, 0x7c5);
Enable(table, 16, 0x4c3);
Enable(table, 7, 0x96d5);
Enable(table, 11, 0x9257);
Enable(table, 8, 0x8235);
Enable(table, 18, 0x4);
Enable(table, 19, 0x40449);
Enable(table, 20, 0x967d);
Enable(table, 22, 0x451);
Enable(table, 23, 0x401cd9);
Enable(table, 17, 0x400280);
Enable(table, 21, 0);
Enable(table, 25, 0);
return table;
}
constexpr Table GROUP_TABLE = BuildTable();
// Not the same system: same group and the same system bits (and some set) never touch (0x00fb5720)
bool DifferentSystems(const uint32_t a, const uint32_t b) {
using namespace dpCollisionFilter;
const bool sameSystem = (a & GROUP_BITS) == (b & GROUP_BITS)
&& ((a & SYSTEM_BITS) != 0 || (b & SYSTEM_BITS) != 0)
&& (a & SYSTEM_BITS) == (b & SYSTEM_BITS);
return !sameSystem;
}
// A group mask touches the other's group (0x00fb5780)
bool MaskHasGroup(const uint32_t mask, const uint32_t other) {
using namespace dpCollisionFilter;
const auto group = other & GROUP_BITS;
return other == 0 || (group > 0 && group <= 32 && (mask & (1u << (group - 1))) != 0);
}
}
bool dpCollisionFilter::GroupsCollide(const uint32_t groupA, const uint32_t groupB) {
if (groupA == 0 || groupB == 0) return true;
if (groupA > GROUP_COUNT || groupB > GROUP_COUNT) return false;
return (GROUP_TABLE[groupA - 1] & (1u << (groupB - 1))) != 0;
}
bool dpCollisionFilter::ShouldCollide(uint32_t filterA, uint32_t filterB) {
if ((filterA & PHANTOM_ONLY) || (filterB & PHANTOM_ONLY)) {
if ((filterA & PHANTOM_ONLY) && (filterB & PHANTOM_ONLY)) return false;
filterA &= ~PHANTOM_ONLY;
filterB &= ~PHANTOM_ONLY;
}
if (filterA == 0 || filterB == 0) return true;
const bool maskA = (filterA & GROUP_MASK) != 0;
const bool maskB = (filterB & GROUP_MASK) != 0;
if (!maskA && !maskB) {
const auto groupA = filterA & GROUP_BITS;
const auto groupB = filterB & GROUP_BITS;
return GroupsCollide(groupA, groupB) && DifferentSystems(filterA, filterB);
}
if (maskA && maskB) return false;
if (maskA) return MaskHasGroup(filterA, filterB) && DifferentSystems(filterA, filterB);
return MaskHasGroup(filterB, filterA) && DifferentSystems(filterB, filterA);
}

View File

@@ -0,0 +1,31 @@
#ifndef DPCOLLISIONFILTER_H
#define DPCOLLISIONFILTER_H
#include <cstdint>
/**
* Which physics objects can touch, the way the client's Havok filter decides it (1.10.64: table built in
* PeCollisionFilter::SetupGroups 0x00fcf9a0, tested in PeCollisionFilter::IsCollisionEnabled 0x00fb6940).
*
* A filter value is a collision group (PhysicsComponent.collisionGroup, or the CollisionGroupID config) with
* optional flags in the high bits:
* - 0 touches everything.
* - PHANTOM_ONLY (0x40000000): two objects that both have it never touch; otherwise it is ignored.
* - GROUP_MASK (0x04000000): the low bits are a mask of the groups it touches (bit n = group n + 1) instead of a group.
* - Two objects with the same group and the same non zero high bits never touch (the same system's parts).
* Otherwise two groups touch when the client's group table says so. Players are group 10, enemies 12.
*/
namespace dpCollisionFilter {
constexpr uint32_t PHANTOM_ONLY = 0x40000000;
constexpr uint32_t GROUP_MASK = 0x04000000;
constexpr uint32_t GROUP_BITS = 0x07ffffff;
constexpr uint32_t SYSTEM_BITS = 0xf8000000;
constexpr uint32_t GROUP_COUNT = 26;
[[nodiscard]] bool ShouldCollide(uint32_t filterA, uint32_t filterB);
// Whether group a touches group b in the client's table (groups start at 1)
[[nodiscard]] bool GroupsCollide(uint32_t groupA, uint32_t groupB);
};
#endif //!DPCOLLISIONFILTER_H

View File

@@ -8,7 +8,7 @@ dpEntity::dpEntity(const LWOOBJID& objectID, dpShapeType shapeType, bool isStati
m_IsStatic = isStatic;
m_CollisionShape = nullptr;
m_Scale = 1.0f;
m_CollisionGroup = COLLISION_GROUP_ALL;
m_CollisionGroup = 0;
switch (shapeType) {
case dpShapeType::Sphere:
@@ -29,7 +29,7 @@ dpEntity::dpEntity(const LWOOBJID& objectID, NiPoint3 boxDimensions, bool isStat
m_IsStatic = isStatic;
m_CollisionShape = nullptr;
m_Scale = 1.0f;
m_CollisionGroup = COLLISION_GROUP_ALL;
m_CollisionGroup = 0;
m_CollisionShape = new dpShapeBox(this, boxDimensions.x, boxDimensions.y, boxDimensions.z);
}
@@ -39,7 +39,7 @@ dpEntity::dpEntity(const LWOOBJID& objectID, float width, float height, float de
m_IsStatic = isStatic;
m_CollisionShape = nullptr;
m_Scale = 1.0f;
m_CollisionGroup = COLLISION_GROUP_ALL;
m_CollisionGroup = 0;
m_CollisionShape = new dpShapeBox(this, width, height, depth);
}
@@ -49,7 +49,7 @@ dpEntity::dpEntity(const LWOOBJID& objectID, float radius, bool isStatic) {
m_IsStatic = isStatic;
m_CollisionShape = nullptr;
m_Scale = 1.0f;
m_CollisionGroup = COLLISION_GROUP_ALL;
m_CollisionGroup = 0;
m_CollisionShape = new dpShapeSphere(this, radius);
}
@@ -70,9 +70,7 @@ void dpEntity::Update(float deltaTime) {
void dpEntity::CheckCollision(dpEntity* other) {
if (!m_CollisionShape) return;
if ((m_CollisionGroup & other->m_CollisionGroup) & (~COLLISION_GROUP_DYNAMIC)) {
return;
}
if (!dpCollisionFilter::ShouldCollide(m_CollisionGroup, other->m_CollisionGroup)) return;
const auto objId = other->GetObjectID();
const auto objItr = m_CurrentlyCollidingObjects.find(objId);

View File

@@ -8,7 +8,7 @@
#include "dCommonVars.h"
#include "dpCommon.h"
#include "dpShapeBase.h"
#include "dpCollisionGroups.h"
#include "dpCollisionFilter.h"
#include "dpGrid.h"
class dpEntity {
@@ -44,8 +44,9 @@ public:
bool GetIsStatic() const { return m_IsStatic; }
uint8_t GetCollisionGroup() const { return m_CollisionGroup; }
void SetCollisionGroup(uint8_t value) { m_CollisionGroup = value; }
// The collision filter value (see dpCollisionFilter): the client's collision group, 0 touches everything
uint32_t GetCollisionGroup() const { return m_CollisionGroup; }
void SetCollisionGroup(uint32_t value) { m_CollisionGroup = value; }
bool GetSleeping() const { return m_Sleeping; }
void SetSleeping(bool value) { m_Sleeping = value; }
@@ -76,7 +77,7 @@ private:
dpGrid* m_Grid = nullptr;
uint8_t m_CollisionGroup;
uint32_t m_CollisionGroup;
bool m_Sleeping = false;
bool m_IsGargantuan = false;

View File

@@ -7,6 +7,7 @@
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include <algorithm>
#include <iostream>
dpShapeBox::dpShapeBox(dpEntity* parentEntity, float width, float height, float depth) :
@@ -78,6 +79,7 @@ void dpShapeBox::SetScale(float scale) {
void dpShapeBox::SetRotation(const NiQuaternion& rotation) {
if (m_HasBeenRotated) return; //Boxes cannot be rotated more than once.
m_HasBeenRotated = true;
m_Orientation = rotation;
m_TopMinLeft = m_TopMinLeft.RotateByQuaternion(rotation);
m_TopMaxLeft = m_TopMaxLeft.RotateByQuaternion(rotation);
@@ -93,19 +95,17 @@ void dpShapeBox::SetRotation(const NiQuaternion& rotation) {
}
bool dpShapeBox::IsVertInBox(const NiPoint3& vert) {
//if we are in the correct height
if (vert.y >= m_MinY && vert.y <= m_MaxY) {
return SquaredDistanceTo(vert) <= 0.0f;
}
//if we're inside the x bounds
if (vert.x >= m_MinX && vert.x <= m_MaxX) {
float dpShapeBox::SquaredDistanceTo(const NiPoint3& point) const {
// Into the box's frame: its origin is the middle of its bottom face
const auto local = (point - m_Origin).RotateByQuaternion(glm::conjugate(m_Orientation));
//if we're inside the z bounds
if (vert.z >= m_MinZ && vert.z <= m_MaxZ)
return true;
}
}
return false;
const float dX = local.x - std::clamp(local.x, -m_Width, m_Width);
const float dY = local.y - std::clamp(local.y, 0.0f, m_Height * 2.0f);
const float dZ = local.z - std::clamp(local.z, -m_Depth, m_Depth);
return dX * dX + dY * dY + dZ * dZ;
}
void dpShapeBox::InitVertices() {
@@ -129,6 +129,7 @@ void dpShapeBox::InitVertices() {
void dpShapeBox::SetPosition(const NiPoint3& position) {
if (isTransformed) return;
isTransformed = true;
m_Origin = position;
for (auto& vert : m_Vertices) {
vert.x += position.x;

View File

@@ -33,6 +33,12 @@ public:
bool IsVertInBox(const NiPoint3& vert);
/**
* The squared distance from a point to the box, measured along the box's own (rotated) axes. The m_Min/m_Max
* bounds are the axis aligned box around the rotated one, which for a rotated wall is far bigger than the wall.
*/
float SquaredDistanceTo(const NiPoint3& point) const;
void InitVertices();
void SetPosition(const NiPoint3& position);
@@ -64,6 +70,10 @@ private:
float m_Scale;
// Where the box was placed and how it was turned, to test against its real shape
NiPoint3 m_Origin{};
NiQuaternion m_Orientation = QuatUtils::IDENTITY;
bool m_HasBeenRotated = false;
bool isScaled = false;
bool isTransformed = false;