mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The client never loads a level object whose config has carver_only set (its resource manager skips the load; LWOBasePhysComponent::LoadConfigData 0x00c495c9 reads the flag next to navmesh_carver), and live never sent one: none of the 331 carver_only placements (mostly Crux Prime's trigger boxes) shows up among the live constructions, where their zones' other objects do. DLU spawned them as objects. They are skipped now. The ones that carve the navmesh (53, in six zones, among them the Avant Gardens Sentinel camp walls) still stop the server's movers: their shape goes into the world's movement blockers without an object, owned by the physics world (dpWorld::AddOwnedMovementBlocker) and freed when it shuts down. None of them has a script, trigger or group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
398 lines
16 KiB
C++
398 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;
|
|
|
|
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
if (physicsComponentTable) {
|
|
auto* info = physicsComponentTable->GetByID(componentID);
|
|
if (info) {
|
|
m_CollisionGroup = info->collisionGroup;
|
|
}
|
|
}
|
|
|
|
if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar<int32_t>(u"CollisionGroupID");
|
|
|
|
RegisterMsg(&PhysicsComponent::OnGetPosition);
|
|
}
|
|
|
|
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;
|
|
}
|