mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 15:57:13 +00:00
The physics row's collision group in LoadDataFromTemplate and the level's CollisionGroupID in LoadConfigData; each physics type then reads its own level keys and makes its physics after that, in the same order as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
404 lines
16 KiB
C++
404 lines
16 KiB
C++
#include "PhysicsComponent.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <map>
|
|
#include <string>
|
|
|
|
#include "eReplicaComponentType.h"
|
|
#include "NiPoint3.h"
|
|
#include "NiQuaternion.h"
|
|
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDPhysicsComponentTable.h"
|
|
|
|
#include "dpEntity.h"
|
|
#include "dpWorld.h"
|
|
#include "dpMovementBlockers.h"
|
|
#include "dpShapeBox.h"
|
|
#include "dpShapeSphere.h"
|
|
|
|
#include "EntityInfo.h"
|
|
#include "Amf3.h"
|
|
#include "Entity.h"
|
|
#include "Logger.h"
|
|
|
|
namespace {
|
|
struct KnownBox {
|
|
NiPoint3 size; // width (x), height (y, up from the base) and depth (z)
|
|
NiPoint3 offset; // where the box's base centre is, from the object's position in its own axes, before scaling
|
|
};
|
|
|
|
/**
|
|
* Assets whose collision the server stands in for with a box: the bounds of all the parts of the asset's collision
|
|
* shape in the client's physics files (res/physics), by file name in lower case. They are the navmesh carvers of
|
|
* Robot City (hedges, greebles, robot statues) and the Ninjago Monastery (benches).
|
|
* The hedge corner is an L: its box also covers the inside of the corner.
|
|
*/
|
|
const std::map<std::string, KnownBox> KNOWN_BOXES = {
|
|
{ "env_won_nim_bush_square-section.hkx", { { 4.3949f, 4.9645f, 9.0341f }, { -0.0343f, 0.0004f, -0.0527f } } },
|
|
{ "env_won_nim_bush_square-corner.hkx", { { 9.1824f, 4.7916f, 9.7440f }, { -1.0403f, 0.2150f, 1.0503f } } },
|
|
{ "env_nim_ag_bushinpot.hkx", { { 5.4603f, 5.7094f, 11.4660f }, { 0.1707f, -0.2426f, 0.5737f } } },
|
|
{ "greeble_001w.hkx", { { 3.18f, 1.90f, 3.18f }, { 0.0f, 0.01f, 0.0f } } },
|
|
{ "greeble_002w.hkx", { { 2.38f, 1.907f, 1.5802f }, { -0.4f, 0.01f, 0.0001f } } },
|
|
{ "robot_statue_01.hkx", { { 1.8642f, 3.1844f, 1.67f }, { 0.4f, 0.01f, -0.275f } } },
|
|
{ "robot_statue_02.hkx", { { 3.0994f, 4.1242f, 1.9228f }, { 0.0165f, 0.0117f, 0.0967f } } },
|
|
{ "env_won_gnar_ninja-bench.hkx", { { 6.4f, 0.9796f, 1.6f }, { 0.0f, -0.0052f, 0.0005f } } },
|
|
};
|
|
}
|
|
|
|
PhysicsComponent::PhysicsComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
|
m_Position = NiPoint3Constant::ZERO;
|
|
m_Rotation = QuatUtils::IDENTITY;
|
|
m_DirtyPosition = false;
|
|
|
|
RegisterMsg(&PhysicsComponent::OnGetPosition);
|
|
}
|
|
|
|
// The PhysicsComponent row's collision group
|
|
void PhysicsComponent::LoadDataFromTemplate() {
|
|
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
if (physicsComponentTable) {
|
|
auto* info = physicsComponentTable->GetByID(m_ComponentID);
|
|
if (info) {
|
|
m_CollisionGroup = info->collisionGroup;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The level config's CollisionGroupID, when there, replaces it
|
|
void PhysicsComponent::LoadConfigData() {
|
|
if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar<int32_t>(u"CollisionGroupID");
|
|
}
|
|
|
|
PhysicsComponent::~PhysicsComponent() {
|
|
if (!m_MovementBlocker) return;
|
|
dpWorld::RemoveMovementBlocker(m_MovementBlocker);
|
|
delete m_MovementBlocker;
|
|
}
|
|
|
|
void PhysicsComponent::RegisterMovementBlocker(const eReplicaComponentType type, const bool solid, const float scale) {
|
|
if (m_MovementBlocker) return;
|
|
const auto filter = dpMovementBlockers::BlockingFilter(m_Parent->GetVar<bool>(u"navmesh_carver"), solid, static_cast<uint32_t>(m_CollisionGroup));
|
|
if (!filter) return;
|
|
|
|
// Some shapes move the object's position to line up with the client's; that is for the shape only
|
|
const auto placed = m_Position;
|
|
bool isFallback = false;
|
|
auto* const blocker = CreatePhysicsEntity(type, &isFallback);
|
|
const auto shapePosition = m_Position;
|
|
m_Position = placed;
|
|
if (!blocker) return;
|
|
if (isFallback) {
|
|
LOG_DEBUG("%llu (LOT %i) should block movement, but the server doesn't know its shape", m_Parent->GetObjectID(), m_Parent->GetLOT());
|
|
delete blocker;
|
|
return;
|
|
}
|
|
|
|
blocker->SetScale(scale);
|
|
blocker->SetRotation(m_Rotation);
|
|
blocker->SetPosition(shapePosition);
|
|
m_MovementBlocker = blocker;
|
|
dpWorld::AddMovementBlocker(m_MovementBlocker, *filter);
|
|
}
|
|
|
|
namespace {
|
|
bool SettingIsTrue(const LwoNameValue& settings, const std::u16string& key) {
|
|
const auto it = settings.find(key);
|
|
return it != settings.end() && it->second && GeneralUtils::TryParse<int32_t>(it->second->GetValueAsString()).value_or(0) != 0;
|
|
}
|
|
}
|
|
|
|
bool PhysicsComponent::IsCarverOnly(const LwoNameValue& settings) {
|
|
return SettingIsTrue(settings, u"carver_only");
|
|
}
|
|
|
|
bool PhysicsComponent::AddLevelMovementBlocker(const LWOOBJID id, const LOT lot, const LwoNameValue& settings, const NiPoint3& position, const NiQuaternion& rotation, const float scale) {
|
|
auto* const registry = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
auto* const physicsTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
// The physics component it would have: fixed simple physics is solid (no level object of this kind moves), a
|
|
// phantom volume is not
|
|
for (const auto type : { eReplicaComponentType::SIMPLE_PHYSICS, eReplicaComponentType::PHANTOM_PHYSICS }) {
|
|
const auto componentID = registry->GetByIDAndType(lot, type, -1);
|
|
if (componentID == -1) continue;
|
|
|
|
int32_t collisionGroup = 0;
|
|
if (const auto* const info = physicsTable->GetByID(componentID)) collisionGroup = info->collisionGroup;
|
|
if (const auto it = settings.find(u"CollisionGroupID"); it != settings.end() && it->second) {
|
|
collisionGroup = GeneralUtils::TryParse<int32_t>(it->second->GetValueAsString()).value_or(collisionGroup);
|
|
}
|
|
|
|
const bool solid = type == eReplicaComponentType::SIMPLE_PHYSICS;
|
|
const auto filter = dpMovementBlockers::BlockingFilter(SettingIsTrue(settings, u"navmesh_carver"), solid, static_cast<uint32_t>(collisionGroup));
|
|
if (!filter) return false;
|
|
|
|
auto shapePosition = position;
|
|
bool isFallback = false;
|
|
auto* const blocker = CreateAssetShape(id, lot, type, collisionGroup, scale, rotation, shapePosition, &isFallback);
|
|
if (!blocker) return false;
|
|
if (isFallback) {
|
|
LOG_DEBUG("%llu (LOT %i) should block movement, but the server doesn't know its shape", id, lot);
|
|
delete blocker;
|
|
return false;
|
|
}
|
|
|
|
blocker->SetScale(scale);
|
|
blocker->SetRotation(rotation);
|
|
blocker->SetPosition(shapePosition);
|
|
dpWorld::AddOwnedMovementBlocker(blocker, *filter);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PhysicsComponent::OnGetPosition(GameMessages::GetPosition& msg) {
|
|
msg.pos = GetPosition();
|
|
return true;
|
|
}
|
|
|
|
void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
outBitStream.Write(bIsInitialUpdate || m_DirtyPosition);
|
|
if (bIsInitialUpdate || m_DirtyPosition) {
|
|
outBitStream.Write(m_Position.x);
|
|
outBitStream.Write(m_Position.y);
|
|
outBitStream.Write(m_Position.z);
|
|
outBitStream.Write(m_Rotation.x);
|
|
outBitStream.Write(m_Rotation.y);
|
|
outBitStream.Write(m_Rotation.z);
|
|
outBitStream.Write(m_Rotation.w);
|
|
if (!bIsInitialUpdate) m_DirtyPosition = false;
|
|
}
|
|
}
|
|
|
|
dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type, bool* isFallback) {
|
|
return CreateAssetShape(m_Parent->GetObjectID(), m_Parent->GetLOT(), type, m_CollisionGroup, m_Parent->GetDefaultScale(), m_Rotation, m_Position, isFallback);
|
|
}
|
|
|
|
dpEntity* PhysicsComponent::CreateAssetShape(const LWOOBJID id, const LOT lot, const eReplicaComponentType type, const int32_t collisionGroup, const float scale, const NiQuaternion& rotation, NiPoint3& position, bool* isFallback) {
|
|
if (isFallback) *isFallback = false;
|
|
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
auto componentID = compRegistryTable->GetByIDAndType(lot, type);
|
|
|
|
CDPhysicsComponentTable* physComp = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
if (physComp == nullptr) return nullptr;
|
|
|
|
auto* info = physComp->GetByID(componentID);
|
|
if (info == nullptr || info->physicsAsset == "" || info->physicsAsset == "NO_PHYSICS") return nullptr;
|
|
|
|
// The file name alone, in lower case: some assets are named by another folder, or case, than the file has
|
|
const auto slash = info->physicsAsset.find_last_of("\\/");
|
|
auto assetFile = info->physicsAsset.substr(slash == std::string::npos ? 0 : slash + 1);
|
|
std::ranges::transform(assetFile, assetFile.begin(), [](const unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
dpEntity* toReturn;
|
|
if (info->physicsAsset == "miscellaneous\\misc_phys_10x1x5.hkx") {
|
|
toReturn = new dpEntity(id, 10.0f, 5.0f, 1.0f);
|
|
} else if (info->physicsAsset == "miscellaneous\\misc_phys_640x640.hkx") {
|
|
// TODO Fix physics simulation to do simulation at high velocities due to bullet through paper problem...
|
|
toReturn = new dpEntity(id, 1638.4f, 13.521004f * 2.0f, 1638.4f);
|
|
|
|
// Move this down by 13.521004 units so it is still effectively at the same height as before
|
|
position = position - NiPoint3Constant::UNIT_Y * 13.521004f;
|
|
} else if (info->physicsAsset == "env\\trigger_wall_tall.hkx") {
|
|
toReturn = new dpEntity(id, 10.0f, 25.0f, 1.0f);
|
|
} else if (info->physicsAsset == "env\\env_gen_placeholderphysics.hkx") {
|
|
toReturn = new dpEntity(id, 20.0f, 20.0f, 20.0f);
|
|
} else if (assetFile == "poi_trigger_wall.hkx") {
|
|
// test\POI_trigger_wall.hkx in the CDClient (e.g. "Clear threat list Trigger Wall"), env\ in the client's files:
|
|
// a box 1 wide, 12.98 tall from its base and 20.45 deep
|
|
toReturn = new dpEntity(id, 1.0f, 12.9755f, 20.45f);
|
|
} else if (assetFile == "trigger_rectangle_box.hkx") {
|
|
// "Trigger Rectangle Box": a box 8 wide, 8 tall from its base and 4 deep
|
|
toReturn = new dpEntity(id, 8.0f, 8.0f, 4.0f);
|
|
} else if (info->physicsAsset == "env\\NG_NinjaGo\\env_ng_gen_gate_chamber_puzzle_ceiling_tile_falling_phantom.hkx") {
|
|
toReturn = new dpEntity(id, 18.0f, 5.0f, 15.0f);
|
|
position += QuatUtils::Forward(rotation) * 7.5f;
|
|
} else if (info->physicsAsset == "env\\NG_NinjaGo\\ng_flamejet_brick_phantom.HKX") {
|
|
toReturn = new dpEntity(id, 1.0f, 1.0f, 12.0f);
|
|
position += QuatUtils::Forward(rotation) * 6.0f;
|
|
} else if (info->physicsAsset == "env\\Ring_Trigger.hkx") {
|
|
toReturn = new dpEntity(id, 6.0f, 6.0f, 6.0f);
|
|
} else if (info->physicsAsset == "env\\vfx_propertyImaginationBall.hkx") {
|
|
toReturn = new dpEntity(id, 4.5f);
|
|
} else if (info->physicsAsset == "env\\env_won_fv_gas-blocking-volume.hkx") {
|
|
toReturn = new dpEntity(id, 390.496826f, 111.467964f, 600.821534f, true);
|
|
position.y -= (111.467964f * scale) / 2;
|
|
// Leaving these out for now since they cause more issues than they solve in racing tracks without proper OBB checks.
|
|
} /* else if (info->physicsAsset == "env\\GFTrack_DeathVolume1_CaveExit.hkx") {
|
|
toReturn = new dpEntity(id, 112.416870f, 50.363434f, 87.679268f);
|
|
} else if (info->physicsAsset == "env\\GFTrack_DeathVolume2_RoadGaps.hkx") {
|
|
toReturn = new dpEntity(id, 48.386536f, 50.363434f, 259.361755f);
|
|
} */ else if (const auto known = KNOWN_BOXES.find(assetFile); known != KNOWN_BOXES.end()) {
|
|
const auto& [size, offset] = known->second;
|
|
toReturn = new dpEntity(id, size.x, size.y, size.z);
|
|
position += (QuatUtils::Right(rotation) * offset.x + QuatUtils::Up(rotation) * offset.y + QuatUtils::Forward(rotation) * offset.z) * scale;
|
|
} else {
|
|
// LOG_DEBUG("This one is supposed to have %s", info->physicsAsset.c_str());
|
|
|
|
//add fallback cube:
|
|
toReturn = new dpEntity(id, 2.0f, 2.0f, 2.0f);
|
|
if (isFallback) *isFallback = true;
|
|
}
|
|
// 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>(collisionGroup));
|
|
return toReturn;
|
|
}
|
|
|
|
dpEntity* PhysicsComponent::CreatePhysicsLnv(const float scale, const eReplicaComponentType type) const {
|
|
int pcShapeType = -1;
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float z = 0.0f;
|
|
float width = 0.0f; //aka "radius"
|
|
float height = 0.0f;
|
|
dpEntity* toReturn = nullptr;
|
|
|
|
if (m_Parent->HasVar(u"primitiveModelType")) {
|
|
pcShapeType = m_Parent->GetVar<int32_t>(u"primitiveModelType");
|
|
x = m_Parent->GetVar<float>(u"primitiveModelValueX");
|
|
y = m_Parent->GetVar<float>(u"primitiveModelValueY");
|
|
z = m_Parent->GetVar<float>(u"primitiveModelValueZ");
|
|
} else {
|
|
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
auto componentID = compRegistryTable->GetByIDAndType(m_Parent->GetLOT(), type);
|
|
|
|
CDPhysicsComponentTable* physComp = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
if (physComp == nullptr) return nullptr;
|
|
|
|
auto info = physComp->GetByID(componentID);
|
|
|
|
if (info == nullptr) return nullptr;
|
|
|
|
pcShapeType = info->pcShapeType;
|
|
width = info->playerRadius;
|
|
height = info->playerHeight;
|
|
}
|
|
|
|
switch (pcShapeType) {
|
|
case 0: { // HKX type
|
|
break;
|
|
}
|
|
case 1: { //Make a new box shape
|
|
NiPoint3 boxSize(x, y, z);
|
|
if (x == 0.0f) {
|
|
//LU has some weird values, so I think it's best to scale them down a bit
|
|
if (height < 0.5f) height = 2.0f;
|
|
if (width < 0.5f) width = 2.0f;
|
|
|
|
//Scale them:
|
|
width = width * scale;
|
|
height = height * scale;
|
|
|
|
boxSize = NiPoint3(width, height, width);
|
|
}
|
|
|
|
toReturn = new dpEntity(m_Parent->GetObjectID(), boxSize);
|
|
|
|
toReturn->SetPosition({ m_Position.x, m_Position.y - (height / 2), m_Position.z });
|
|
break;
|
|
}
|
|
case 2: { //Make a new cylinder shape
|
|
break;
|
|
}
|
|
case 3: { //Make a new sphere shape
|
|
auto [x, y, z] = m_Position;
|
|
toReturn = new dpEntity(m_Parent->GetObjectID(), width);
|
|
toReturn->SetPosition({ x, y, z });
|
|
break;
|
|
}
|
|
case 4: { //Make a new capsule shape
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (toReturn) {
|
|
toReturn->SetCollisionGroup(static_cast<uint32_t>(m_CollisionGroup));
|
|
dpWorld::AddEntity(toReturn);
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
void PhysicsComponent::SpawnVertices(dpEntity* entity) const {
|
|
if (!entity) return;
|
|
|
|
LOG("Spawning vertices for %llu", m_Parent->GetObjectID());
|
|
EntityInfo info;
|
|
info.lot = 33;
|
|
info.spawner = nullptr;
|
|
info.spawnerID = m_Parent->GetObjectID();
|
|
info.spawnerNodeID = 0;
|
|
|
|
// These don't use overloaded methods as dPhysics does not link with dGame at the moment.
|
|
auto box = dynamic_cast<dpShapeBox*>(entity->GetShape());
|
|
if (box) {
|
|
for (auto vert : box->GetVertices()) {
|
|
LOG("Vertex at %f, %f, %f", vert.x, vert.y, vert.z);
|
|
|
|
info.pos = vert;
|
|
Entity* newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
}
|
|
}
|
|
auto sphere = dynamic_cast<dpShapeSphere*>(entity->GetShape());
|
|
if (sphere) {
|
|
auto [x, y, z] = entity->GetPosition(); // Use shapes position instead of the parent's position in case it's different
|
|
float plusX = x + sphere->GetRadius();
|
|
float minusX = x - sphere->GetRadius();
|
|
float plusY = y + sphere->GetRadius();
|
|
float minusY = y - sphere->GetRadius();
|
|
float plusZ = z + sphere->GetRadius();
|
|
float minusZ = z - sphere->GetRadius();
|
|
|
|
auto radius = sphere->GetRadius();
|
|
LOG("Radius: %f", radius);
|
|
LOG("Plus Vertices %f %f %f", plusX, plusY, plusZ);
|
|
LOG("Minus Vertices %f %f %f", minusX, minusY, minusZ);
|
|
|
|
info.pos = NiPoint3{ x, plusY, z };
|
|
Entity* newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
|
|
info.pos = NiPoint3{ x, minusY, z };
|
|
newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
|
|
info.pos = NiPoint3{ plusX, y, z };
|
|
newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
|
|
info.pos = NiPoint3{ minusX, y, z };
|
|
newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
|
|
info.pos = NiPoint3{ x, y, plusZ };
|
|
newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
|
|
info.pos = NiPoint3{ x, y, minusZ };
|
|
newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
|
|
info.pos = NiPoint3{ x, y, z };
|
|
newEntity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(newEntity);
|
|
}
|
|
}
|
|
|
|
bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
auto& info = reportInfo.info->PushDebug("Physics");
|
|
reportInfo.subCategory = &info;
|
|
|
|
auto& pos = info.PushDebug("Position").PushDebug(m_Position);
|
|
|
|
auto& rot = info.PushDebug("Rotation").PushDebug(m_Rotation);
|
|
|
|
info.PushDebug<AMFIntValue>("Collision Group") = m_CollisionGroup;
|
|
|
|
return true;
|
|
}
|