mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
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:
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/*
|
||||
* 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,
|
||||
};
|
||||
@@ -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<ProximityMonitorComponent>();
|
||||
if (!proxMon) proxMon = AddComponent<ProximityMonitorComponent>(-1);
|
||||
proxMon->SetProximityRadius(proxRadius, name);
|
||||
proxMon->SetProximityRadius(proxRadius, name, collisionGroup);
|
||||
}
|
||||
|
||||
void Entity::SetProximityRadius(dpEntity* entity, std::string name) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<CDComponentsRegistryTable>();
|
||||
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<uint32_t>(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());
|
||||
|
||||
@@ -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<uint32_t>(m_CollisionGroup));
|
||||
dpWorld::AddEntity(m_dpEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<uint32_t>(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<uint32_t>(m_CollisionGroup));
|
||||
dpWorld::AddEntity(toReturn);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
set(DPHYSICS_SOURCES "dpCollisionChecks.cpp"
|
||||
"dpEntity.cpp"
|
||||
"dpCollisionFilter.cpp"
|
||||
"dpGrid.cpp"
|
||||
"dpKnockback.cpp"
|
||||
"dpShapeBase.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;
|
||||
|
||||
89
dPhysics/dpCollisionFilter.cpp
Normal file
89
dPhysics/dpCollisionFilter.cpp
Normal 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);
|
||||
}
|
||||
31
dPhysics/dpCollisionFilter.h
Normal file
31
dPhysics/dpCollisionFilter.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ set(DGAMETEST_SOURCES
|
||||
"StaleSaveGuardTests.cpp"
|
||||
"ContrabandTests.cpp"
|
||||
"KnockbackTests.cpp"
|
||||
"CollisionFilterTests.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(dComponentsTests)
|
||||
|
||||
79
tests/dGameTests/CollisionFilterTests.cpp
Normal file
79
tests/dGameTests/CollisionFilterTests.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user