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,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;
|
||||
|
||||
Reference in New Issue
Block a user