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 507ebc7fda
commit 9907d7deb8
20 changed files with 277 additions and 64 deletions

View File

@@ -8,6 +8,7 @@ set(DGAMETEST_SOURCES
"StaleSaveGuardTests.cpp"
"ContrabandTests.cpp"
"KnockbackTests.cpp"
"CollisionFilterTests.cpp"
)
add_subdirectory(dComponentsTests)

View File

@@ -0,0 +1,79 @@
#include <gtest/gtest.h>
#include "dpCollisionFilter.h"
#include "dpEntity.h"
#include "dpShapeBox.h"
using namespace dpCollisionFilter;
namespace {
constexpr uint32_t PLAYER = 10;
constexpr uint32_t ENEMY = 12;
}
TEST(CollisionFilterTests, GroupZeroTouchesEverything) {
EXPECT_TRUE(ShouldCollide(0, PLAYER));
EXPECT_TRUE(ShouldCollide(ENEMY, 0));
EXPECT_TRUE(ShouldCollide(0, 0));
}
TEST(CollisionFilterTests, TableIsSymmetric) {
for (uint32_t a = 1; a <= GROUP_COUNT; a++) {
for (uint32_t b = 1; b <= GROUP_COUNT; b++) EXPECT_EQ(GroupsCollide(a, b), GroupsCollide(b, a)) << a << " " << b;
}
}
TEST(CollisionFilterTests, LiveProximityGroups) {
// The AM shield generator: group 10 to find enemies, group 1 to find players
EXPECT_TRUE(ShouldCollide(10, ENEMY));
EXPECT_FALSE(ShouldCollide(10, PLAYER));
EXPECT_TRUE(ShouldCollide(1, PLAYER));
EXPECT_FALSE(ShouldCollide(1, ENEMY));
}
TEST(CollisionFilterTests, TriggerWalls) {
// POI trigger walls (group 1) ignore enemies; "Clear threat list" walls (18) only catch enemies
EXPECT_FALSE(ShouldCollide(1, ENEMY));
EXPECT_TRUE(ShouldCollide(18, ENEMY));
EXPECT_FALSE(ShouldCollide(18, PLAYER));
// Death volumes (4) catch both
EXPECT_TRUE(ShouldCollide(4, PLAYER));
EXPECT_TRUE(ShouldCollide(4, ENEMY));
// Groups 21 and 25 touch nothing
EXPECT_FALSE(ShouldCollide(21, PLAYER));
EXPECT_FALSE(ShouldCollide(25, ENEMY));
}
TEST(CollisionFilterTests, Flags) {
// Two phantom-only objects never touch, one is ignored
EXPECT_FALSE(ShouldCollide(PHANTOM_ONLY | 4, PHANTOM_ONLY | PLAYER));
EXPECT_TRUE(ShouldCollide(PHANTOM_ONLY | 4, PLAYER));
// A group mask touches exactly the groups in it
const auto playersOnly = GROUP_MASK | (1u << (PLAYER - 1));
EXPECT_TRUE(ShouldCollide(playersOnly, PLAYER));
EXPECT_FALSE(ShouldCollide(ENEMY, playersOnly));
EXPECT_FALSE(ShouldCollide(playersOnly, playersOnly));
// Parts of the same system don't touch each other
EXPECT_FALSE(ShouldCollide(0x08000000 | ENEMY, 0x08000000 | ENEMY));
EXPECT_TRUE(ShouldCollide(0x08000000 | ENEMY, 0x10000000 | ENEMY));
}
TEST(CollisionFilterTests, RotatedBoxesKeepTheirShape) {
// A 40 long, 1 thick wall turned 45 degrees: its axis aligned bounds are a ~29x29 square, but only the wall counts
dpEntity wall(1, 40.0f, 10.0f, 1.0f);
wall.SetRotation(QuatUtils::AxisAngle(NiPoint3(0.0f, 1.0f, 0.0f), glm::radians(45.0f)));
wall.SetPosition(NiPoint3(100.0f, 0.0f, 100.0f));
auto* box = static_cast<dpShapeBox*>(wall.GetShape());
// Along the wall
const auto along = QuatUtils::AxisAngle(NiPoint3(0.0f, 1.0f, 0.0f), glm::radians(45.0f));
const auto onWall = NiPoint3(100.0f, 5.0f, 100.0f) + NiPoint3(15.0f, 0.0f, 0.0f).RotateByQuaternion(along);
EXPECT_FLOAT_EQ(box->SquaredDistanceTo(onWall), 0.0f);
// A corner of the old axis aligned box, far from the wall
EXPECT_GT(box->SquaredDistanceTo(NiPoint3(112.0f, 5.0f, 112.0f)), 100.0f);
// Above the top and below the bottom
EXPECT_NEAR(box->SquaredDistanceTo(NiPoint3(100.0f, 12.0f, 100.0f)), 4.0f, 1e-3f);
EXPECT_NEAR(box->SquaredDistanceTo(NiPoint3(100.0f, -3.0f, 100.0f)), 9.0f, 1e-3f);
}