diff --git a/dCommon/dEnums/dpCollisionGroups.h b/dCommon/dEnums/dpCollisionGroups.h deleted file mode 100644 index ad9807b98..000000000 --- a/dCommon/dEnums/dpCollisionGroups.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -/* - * Collision Groups - */ - -enum eCollisionGroup : uint8_t -{ - COLLISION_GROUP_ALL = 0 << 0, - COLLISION_GROUP_NEUTRAL = 1 << 0, - COLLISION_GROUP_FRIENDLY = 1 << 1, - COLLISION_GROUP_ENEMY = 1 << 2, - COLLISION_GROUP_DYNAMIC = 1 << 3, -}; diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 2a9403954..8707d5285 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -923,10 +923,10 @@ void Entity::Unsubscribe(LWOOBJID scriptObjId, const std::string& notificationNa } } -void Entity::SetProximityRadius(float proxRadius, std::string name) { +void Entity::SetProximityRadius(float proxRadius, std::string name, uint32_t collisionGroup) { auto* proxMon = GetComponent(); if (!proxMon) proxMon = AddComponent(-1); - proxMon->SetProximityRadius(proxRadius, name); + proxMon->SetProximityRadius(proxRadius, name, collisionGroup); } void Entity::SetProximityRadius(dpEntity* entity, std::string name) { diff --git a/dGame/Entity.h b/dGame/Entity.h index 4fc00cbf5..275aea2ce 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -196,7 +196,7 @@ public: void Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd, const std::string& notificationName); void Unsubscribe(LWOOBJID scriptObjId, const std::string& notificationName); - void SetProximityRadius(float proxRadius, std::string name); + void SetProximityRadius(float proxRadius, std::string name, uint32_t collisionGroup = 0); void SetProximityRadius(dpEntity* entity, std::string name); void AddChild(Entity* child); diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index a86fb9b5d..b1719478d 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -30,6 +30,15 @@ #include "CDPhysicsComponentTable.h" #include "dNavMesh.h" #include "Amf3.h" +#include "dpCollisionFilter.h" + +namespace { + // The collision groups of players and enemies in PhysicsComponent + constexpr uint32_t PLAYER_COLLISION_GROUP = 10; + constexpr uint32_t ENEMY_COLLISION_GROUP = 12; + // standard_enemy.hkx's radius + constexpr float DEFAULT_BODY_RADIUS = 1.7f; +} BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { RegisterMsg(&BaseCombatAIComponent::MsgGetObjectReportInfo); @@ -105,7 +114,11 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo * Add physics */ - int32_t collisionGroup = (COLLISION_GROUP_DYNAMIC | COLLISION_GROUP_ENEMY); + // The enemy's own body, which other volumes (triggers, damage volumes, proximity monitors) see: its group and + // radius from its physics component, like its character controller in the client. It used to be a sphere of + // the aggro radius, so volumes caught enemies from far away (e.g. the Cavalry Hill damage volumes, #1127). + uint32_t collisionGroup = ENEMY_COLLISION_GROUP; + float bodyRadius = DEFAULT_BODY_RADIUS; CDComponentsRegistryTable* componentRegistryTable = CDClientManager::GetTable(); const auto controllablePhysicsID = componentRegistryTable->GetByIDAndType(parent->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS); @@ -115,15 +128,16 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo if (physicsComponentTable != nullptr) { auto* info = physicsComponentTable->GetByID(controllablePhysicsID); if (info != nullptr) { - collisionGroup = info->bStatic ? COLLISION_GROUP_NEUTRAL : info->collisionGroup; + collisionGroup = static_cast(info->collisionGroup); + if (info->playerRadius > 0.0f) bodyRadius = info->playerRadius; } } - //Create a phantom physics volume so we can detect when we're aggro'd. + // The aggro sensor only needs to see players, as before; targets are picked by faction afterwards m_dpEntity = new dpEntity(m_Parent->GetObjectID(), m_AggroRadius); - m_dpEntityEnemy = new dpEntity(m_Parent->GetObjectID(), m_AggroRadius, false); + m_dpEntityEnemy = new dpEntity(m_Parent->GetObjectID(), bodyRadius, false); - m_dpEntity->SetCollisionGroup(collisionGroup); + m_dpEntity->SetCollisionGroup(dpCollisionFilter::GROUP_MASK | (1u << (PLAYER_COLLISION_GROUP - 1))); m_dpEntityEnemy->SetCollisionGroup(collisionGroup); m_dpEntity->SetPosition(m_Parent->GetPosition()); diff --git a/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp index 99ae1b663..a4e5fdc12 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/ControllablePhysicsComponent.cpp @@ -58,7 +58,8 @@ ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity, const float radius = 1.5f; m_dpEntity = new dpEntity(m_Parent->GetObjectID(), radius, false); - m_dpEntity->SetCollisionGroup(COLLISION_GROUP_DYNAMIC | COLLISION_GROUP_FRIENDLY); + // The player's group from its physics component (10), which trigger volumes filter on + m_dpEntity->SetCollisionGroup(static_cast(m_CollisionGroup)); dpWorld::AddEntity(m_dpEntity); } } diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp index 0d3a302b2..39d67e4ae 100644 --- a/dGame/dComponents/PhysicsComponent.cpp +++ b/dGame/dComponents/PhysicsComponent.cpp @@ -103,6 +103,8 @@ dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type) { //add fallback cube: toReturn = new dpEntity(m_Parent->GetObjectID(), 2.0f, 2.0f, 2.0f); } + // Only touch what the client lets this group touch (e.g. POI walls ignore enemies, threat clearing walls ignore players) + toReturn->SetCollisionGroup(static_cast(m_CollisionGroup)); return toReturn; } @@ -174,7 +176,10 @@ dpEntity* PhysicsComponent::CreatePhysicsLnv(const float scale, const eReplicaCo } } - if (toReturn) dpWorld::AddEntity(toReturn); + if (toReturn) { + toReturn->SetCollisionGroup(static_cast(m_CollisionGroup)); + dpWorld::AddEntity(toReturn); + } return toReturn; } diff --git a/dGame/dComponents/ProximityMonitorComponent.cpp b/dGame/dComponents/ProximityMonitorComponent.cpp index ca48a429c..91b3ad738 100644 --- a/dGame/dComponents/ProximityMonitorComponent.cpp +++ b/dGame/dComponents/ProximityMonitorComponent.cpp @@ -27,8 +27,9 @@ ProximityMonitorComponent::~ProximityMonitorComponent() { m_ProximitiesData.clear(); } -void ProximityMonitorComponent::SetProximityRadius(float proxRadius, const std::string& name) { +void ProximityMonitorComponent::SetProximityRadius(float proxRadius, const std::string& name, const uint32_t collisionGroup) { dpEntity* en = new dpEntity(m_Parent->GetObjectID(), proxRadius); + en->SetCollisionGroup(collisionGroup); en->SetPosition(m_Parent->GetPosition()); dpWorld::AddEntity(en); diff --git a/dGame/dComponents/ProximityMonitorComponent.h b/dGame/dComponents/ProximityMonitorComponent.h index b4aa1f1a3..e9e7a0745 100644 --- a/dGame/dComponents/ProximityMonitorComponent.h +++ b/dGame/dComponents/ProximityMonitorComponent.h @@ -32,7 +32,11 @@ public: * @param proxRadius the radius to use for the physics entity we use to detect proximity * @param name the name of this check */ - void SetProximityRadius(float proxRadius, const std::string& name); + /** + * Adds a sphere that reports what enters and leaves it + * @param collisionGroup the collision group it filters on like the client's (see dpCollisionFilter), 0 for everything + */ + void SetProximityRadius(float proxRadius, const std::string& name, uint32_t collisionGroup = 0); /** * Creates an entry to check proximity for, given a name diff --git a/dPhysics/CMakeLists.txt b/dPhysics/CMakeLists.txt index 508bcd16d..55f2c22d7 100644 --- a/dPhysics/CMakeLists.txt +++ b/dPhysics/CMakeLists.txt @@ -1,5 +1,6 @@ set(DPHYSICS_SOURCES "dpCollisionChecks.cpp" "dpEntity.cpp" + "dpCollisionFilter.cpp" "dpGrid.cpp" "dpKnockback.cpp" "dpShapeBase.cpp" diff --git a/dPhysics/dpCollisionChecks.cpp b/dPhysics/dpCollisionChecks.cpp index c2efee905..be6c6fb8a 100644 --- a/dPhysics/dpCollisionChecks.cpp +++ b/dPhysics/dpCollisionChecks.cpp @@ -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; diff --git a/dPhysics/dpCollisionFilter.cpp b/dPhysics/dpCollisionFilter.cpp new file mode 100644 index 000000000..d3109b73c --- /dev/null +++ b/dPhysics/dpCollisionFilter.cpp @@ -0,0 +1,89 @@ +#include "dpCollisionFilter.h" + +#include + +namespace { + using Table = std::array; + + // 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); +} diff --git a/dPhysics/dpCollisionFilter.h b/dPhysics/dpCollisionFilter.h new file mode 100644 index 000000000..d25295822 --- /dev/null +++ b/dPhysics/dpCollisionFilter.h @@ -0,0 +1,31 @@ +#ifndef DPCOLLISIONFILTER_H +#define DPCOLLISIONFILTER_H + +#include + +/** + * 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 diff --git a/dPhysics/dpEntity.cpp b/dPhysics/dpEntity.cpp index 6368764f1..44cf92425 100644 --- a/dPhysics/dpEntity.cpp +++ b/dPhysics/dpEntity.cpp @@ -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); diff --git a/dPhysics/dpEntity.h b/dPhysics/dpEntity.h index a43da4c39..1c302bf11 100644 --- a/dPhysics/dpEntity.h +++ b/dPhysics/dpEntity.h @@ -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; diff --git a/dPhysics/dpShapeBox.cpp b/dPhysics/dpShapeBox.cpp index bfd72deac..8c1fcdcea 100644 --- a/dPhysics/dpShapeBox.cpp +++ b/dPhysics/dpShapeBox.cpp @@ -7,6 +7,7 @@ #include "NiPoint3.h" #include "NiQuaternion.h" +#include #include 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; diff --git a/dPhysics/dpShapeBox.h b/dPhysics/dpShapeBox.h index 4af0396e1..3619e75eb 100644 --- a/dPhysics/dpShapeBox.h +++ b/dPhysics/dpShapeBox.h @@ -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; diff --git a/dScripts/02_server/Map/AM/AmShieldGenerator.cpp b/dScripts/02_server/Map/AM/AmShieldGenerator.cpp index 29b09e12e..22de7faa2 100644 --- a/dScripts/02_server/Map/AM/AmShieldGenerator.cpp +++ b/dScripts/02_server/Map/AM/AmShieldGenerator.cpp @@ -9,8 +9,9 @@ #include "SkillComponent.h" void AmShieldGenerator::OnStartup(Entity* self) { - self->SetProximityRadius(20, "shield"); - self->SetProximityRadius(21, "buffer"); + // Enemies (the client group table: 10 sees group 12) and players (1 sees group 10), like live + self->SetProximityRadius(20, "shield", 10); + self->SetProximityRadius(21, "buffer", 1); StartShield(self); } diff --git a/dScripts/02_server/Map/AM/AmShieldGeneratorQuickbuild.cpp b/dScripts/02_server/Map/AM/AmShieldGeneratorQuickbuild.cpp index 68ed26ff0..ebbf2f851 100644 --- a/dScripts/02_server/Map/AM/AmShieldGeneratorQuickbuild.cpp +++ b/dScripts/02_server/Map/AM/AmShieldGeneratorQuickbuild.cpp @@ -11,8 +11,9 @@ #include "MissionComponent.h" void AmShieldGeneratorQuickbuild::OnStartup(Entity* self) { - self->SetProximityRadius(20, "shield"); - self->SetProximityRadius(21, "buffer"); + // Enemies (the client group table: 10 sees group 12) and players (1 sees group 10), like live + self->SetProximityRadius(20, "shield", 10); + self->SetProximityRadius(21, "buffer", 1); } void AmShieldGeneratorQuickbuild::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) { diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index a48269c3a..5f87aaa71 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -8,6 +8,7 @@ set(DGAMETEST_SOURCES "StaleSaveGuardTests.cpp" "ContrabandTests.cpp" "KnockbackTests.cpp" + "CollisionFilterTests.cpp" ) add_subdirectory(dComponentsTests) diff --git a/tests/dGameTests/CollisionFilterTests.cpp b/tests/dGameTests/CollisionFilterTests.cpp new file mode 100644 index 000000000..b53bb288b --- /dev/null +++ b/tests/dGameTests/CollisionFilterTests.cpp @@ -0,0 +1,79 @@ +#include + +#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(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); +}