diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp index 65bb25f5a..0e4aa5fec 100644 --- a/dGame/dComponents/PhysicsComponent.cpp +++ b/dGame/dComponents/PhysicsComponent.cpp @@ -97,6 +97,56 @@ void PhysicsComponent::RegisterMovementBlocker(const eReplicaComponentType type, 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; diff --git a/dGame/dComponents/PhysicsComponent.h b/dGame/dComponents/PhysicsComponent.h index a8f127403..ca6c30185 100644 --- a/dGame/dComponents/PhysicsComponent.h +++ b/dGame/dComponents/PhysicsComponent.h @@ -6,6 +6,8 @@ #include "NiQuaternion.h" #include "dCommonVars.h" +struct LwoNameValue; + namespace GameMessages { struct GetObjectReportInfo; struct GetPosition; @@ -34,6 +36,20 @@ public: int32_t GetCollisionGroup() const noexcept { return m_CollisionGroup; } void SetCollisionGroup(int32_t group) noexcept { m_CollisionGroup = group; } + + /** + * Whether a level object is carver_only, which the client never loads (LWOResMgr2Interface::Run skips its load + * when its config has carver_only set; LWOBasePhysComponent::LoadConfigData 0x00c495c9 reads it next to + * navmesh_carver). Live never sent one. + */ + [[nodiscard]] static bool IsCarverOnly(const LwoNameValue& settings); + + /** + * For a level object the server doesn't spawn (carver_only): adds its shape to the world's movement blockers, the + * way its physics component would have (RegisterMovementBlocker), owned by the world. + * @return whether it blocks movement (its data makes it a wall and the server knows its shape) + */ + static bool AddLevelMovementBlocker(LWOOBJID id, LOT lot, const LwoNameValue& settings, const NiPoint3& position, const NiQuaternion& rotation, float scale); protected: bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); diff --git a/dPhysics/dpWorld.cpp b/dPhysics/dpWorld.cpp index 1aad9f0f9..47dc5d7c2 100644 --- a/dPhysics/dpWorld.cpp +++ b/dPhysics/dpWorld.cpp @@ -3,6 +3,7 @@ #include "dpGrid.h" #include "DetourCommon.h" +#include #include #include "Game.h" @@ -24,6 +25,7 @@ namespace { bool phys_spatial_partitioning = true; std::vector m_MovementBlockers; + std::vector> m_OwnedMovementBlockers; }; void dpWorld::Initialize(unsigned int zoneID, bool generateNewNavMesh) { @@ -88,6 +90,7 @@ void dpWorld::Shutdown() { } m_MovementBlockers.clear(); + m_OwnedMovementBlockers.clear(); } bool dpWorld::IsLoaded() { @@ -197,6 +200,12 @@ void dpWorld::AddMovementBlocker(const dpEntity* entity, const uint32_t filter) m_MovementBlockers.push_back({ entity, filter }); } +void dpWorld::AddOwnedMovementBlocker(dpEntity* entity, const uint32_t filter) { + if (!entity) return; + m_OwnedMovementBlockers.emplace_back(entity); + AddMovementBlocker(entity, filter); +} + void dpWorld::RemoveMovementBlocker(const dpEntity* entity) { std::erase_if(m_MovementBlockers, [entity](const dpMovementBlocker& blocker) { return blocker.entity == entity; }); } diff --git a/dPhysics/dpWorld.h b/dPhysics/dpWorld.h index f14931c85..37bdf10d1 100644 --- a/dPhysics/dpWorld.h +++ b/dPhysics/dpWorld.h @@ -34,6 +34,11 @@ namespace dpWorld { * the caller, who removes it before deleting it; it is not stepped with the world. */ void AddMovementBlocker(const dpEntity* entity, uint32_t filter); + /** + * A movement blocker with no object of its own (a wall from the level files the server doesn't spawn), owned by the + * world from now on and freed when it shuts down. + */ + void AddOwnedMovementBlocker(dpEntity* entity, uint32_t filter); void RemoveMovementBlocker(const dpEntity* entity); std::span GetMovementBlockers(); diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index dd870a42c..27e9aa712 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -17,6 +17,7 @@ #include "LevelFile.h" #include "ZoneFileLog.h" #include "ClientVersion.h" +#include "PhysicsComponent.h" #include "dConfig.h" #include @@ -166,6 +167,13 @@ void Level::LoadSceneObjects(const std::vector& objects) { continue; } + // The client never loads a carver_only object (and live never sent one), but it still carves the AI navmesh: + // keep it as a wall the server's movers can't walk through, without an object + if (obj.lot != 176 && PhysicsComponent::IsCarverOnly(obj.settings)) { + PhysicsComponent::AddLevelMovementBlocker(obj.id, obj.lot, obj.settings, obj.position, obj.rotation, obj.scale); + continue; + } + if (obj.lot == 176) { //Spawner MakeSpawner(obj, m_SceneID); } else { //Regular object