#include "PhysicsComponent.h" #include #include #include #include #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 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(); if (physicsComponentTable) { auto* info = physicsComponentTable->GetByID(componentID); if (info) { m_CollisionGroup = info->collisionGroup; } } if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar(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(u"navmesh_carver"), solid, static_cast(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(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(); auto* const physicsTable = CDClientManager::GetTable(); // 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(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(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(); auto componentID = compRegistryTable->GetByIDAndType(lot, type); CDPhysicsComponentTable* physComp = CDClientManager::GetTable(); 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(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(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(u"primitiveModelType"); x = m_Parent->GetVar(u"primitiveModelValueX"); y = m_Parent->GetVar(u"primitiveModelValueY"); z = m_Parent->GetVar(u"primitiveModelValueZ"); } else { CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); auto componentID = compRegistryTable->GetByIDAndType(m_Parent->GetLOT(), type); CDPhysicsComponentTable* physComp = CDClientManager::GetTable(); 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(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(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(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("Collision Group") = m_CollisionGroup; return true; }