Files
DarkflameServer/dGame/dComponents/PhysicsComponent.cpp
Aaron Kimbrell 43b99b730f refactor(component): physics components load in the client's phases
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>
2026-10-01 08:34:28 -05:00

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;
}