From 175d2930b798c3a568d3dcbeb10bc051b3f6a85e Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 07:48:56 -0500 Subject: [PATCH] feat(physics): real sizes for the navmesh carvers the server didn't know 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 --- dGame/dComponents/PhysicsComponent.cpp | 76 +++++++++++++++++++------- dGame/dComponents/PhysicsComponent.h | 8 +++ 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp index cbdf4e1f6..65bb25f5a 100644 --- a/dGame/dComponents/PhysicsComponent.cpp +++ b/dGame/dComponents/PhysicsComponent.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "eReplicaComponentType.h" #include "NiPoint3.h" @@ -21,6 +23,30 @@ #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; @@ -91,9 +117,13 @@ void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitia } 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(m_Parent->GetLOT(), type); + auto componentID = compRegistryTable->GetByIDAndType(lot, type); CDPhysicsComponentTable* physComp = CDClientManager::GetTable(); @@ -109,51 +139,55 @@ dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type, bool dpEntity* toReturn; if (info->physicsAsset == "miscellaneous\\misc_phys_10x1x5.hkx") { - toReturn = new dpEntity(m_Parent->GetObjectID(), 10.0f, 5.0f, 1.0f); + 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(m_Parent->GetObjectID(), 1638.4f, 13.521004f * 2.0f, 1638.4f); + 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 - m_Position = m_Position - NiPoint3Constant::UNIT_Y * 13.521004f; + position = position - NiPoint3Constant::UNIT_Y * 13.521004f; } else if (info->physicsAsset == "env\\trigger_wall_tall.hkx") { - toReturn = new dpEntity(m_Parent->GetObjectID(), 10.0f, 25.0f, 1.0f); + toReturn = new dpEntity(id, 10.0f, 25.0f, 1.0f); } else if (info->physicsAsset == "env\\env_gen_placeholderphysics.hkx") { - toReturn = new dpEntity(m_Parent->GetObjectID(), 20.0f, 20.0f, 20.0f); + 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(m_Parent->GetObjectID(), 1.0f, 12.9755f, 20.45f); + 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(m_Parent->GetObjectID(), 8.0f, 8.0f, 4.0f); + 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(m_Parent->GetObjectID(), 18.0f, 5.0f, 15.0f); - m_Position += QuatUtils::Forward(m_Rotation) * 7.5f; + 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(m_Parent->GetObjectID(), 1.0f, 1.0f, 12.0f); - m_Position += QuatUtils::Forward(m_Rotation) * 6.0f; + 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(m_Parent->GetObjectID(), 6.0f, 6.0f, 6.0f); + toReturn = new dpEntity(id, 6.0f, 6.0f, 6.0f); } else if (info->physicsAsset == "env\\vfx_propertyImaginationBall.hkx") { - toReturn = new dpEntity(m_Parent->GetObjectID(), 4.5f); + toReturn = new dpEntity(id, 4.5f); } else if (info->physicsAsset == "env\\env_won_fv_gas-blocking-volume.hkx") { - toReturn = new dpEntity(m_Parent->GetObjectID(), 390.496826f, 111.467964f, 600.821534f, true); - m_Position.y -= (111.467964f * m_Parent->GetDefaultScale()) / 2; + 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(m_Parent->GetObjectID(), 112.416870f, 50.363434f, 87.679268f); + toReturn = new dpEntity(id, 112.416870f, 50.363434f, 87.679268f); } else if (info->physicsAsset == "env\\GFTrack_DeathVolume2_RoadGaps.hkx") { - toReturn = new dpEntity(m_Parent->GetObjectID(), 48.386536f, 50.363434f, 259.361755f); - } */ else { + 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(m_Parent->GetObjectID(), 2.0f, 2.0f, 2.0f); + 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(m_CollisionGroup)); + toReturn->SetCollisionGroup(static_cast(collisionGroup)); return toReturn; } diff --git a/dGame/dComponents/PhysicsComponent.h b/dGame/dComponents/PhysicsComponent.h index 18dd793c9..a8f127403 100644 --- a/dGame/dComponents/PhysicsComponent.h +++ b/dGame/dComponents/PhysicsComponent.h @@ -4,6 +4,7 @@ #include "Component.h" #include "NiPoint3.h" #include "NiQuaternion.h" +#include "dCommonVars.h" namespace GameMessages { struct GetObjectReportInfo; @@ -39,6 +40,13 @@ protected: // isFallback, when given, says whether the asset had no known shape and got a stand in cube dpEntity* CreatePhysicsEntity(eReplicaComponentType type, bool* isFallback = nullptr); + /** + * The shape of the physics asset of a LOT's physics component of the given type, for an object placed with this + * scale and rotation, or nullptr when it has none. Some shapes move position (in and out) to line up with the + * client's. isFallback, when given, says whether the asset had no known shape and got a stand in cube. + */ + static dpEntity* CreateAssetShape(LWOOBJID id, LOT lot, eReplicaComponentType type, int32_t collisionGroup, float scale, const NiQuaternion& rotation, NiPoint3& position, bool* isFallback = nullptr); + /** * Makes this object a wall the server's movers can't walk through when its data says it is one * (dpMovementBlockers::BlockingFilter: a navmesh carver, or a solid object only enemies collide with). Only