mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Walls whose asset shape the server doesn't know are left out of the movement blockers (a stand in cube would be a guess), which left out every navmesh carver in Robot City (hedges, hedge corners, potted bushes, greebles, robot statues) and the Ninjago Monastery benches. Their sizes now come from the client's own collision shapes (res/physics): the bounds of all of a shape's parts, with the offset of the box from the object's position. The hedge corner is an L, so its box also covers the inside of the corner. The same bounds match two shapes the server already had (misc_phys_10x1x5: 10 x 5 x 1; Trigger_Rectangle_Box: 8 x 8 x 4). CreatePhysicsEntity's shape lookup becomes CreateAssetShape, which needs no component, for walls without an object. Still unknown: the Ninjago Monastery cave's primitive model carver (a 77.6 cube whose origin, centre or base, isn't known). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
348 lines
14 KiB
C++
348 lines
14 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);
|
|
}
|
|
|
|
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;
|
|
}
|