From 8b8c3049539b5944c628ce7159807f7752f41198 Mon Sep 17 00:00:00 2001
From: Petr Mikheev <ptmikheev@gmail.com>
Date: Sun, 3 Jul 2022 02:14:32 +0200
Subject: [PATCH] Treat empty `RootCollisionNode` in NIF as NCC flag and
 generate CameraOnly collision shape

---
 .../nifloader/testbulletnifloader.cpp         | 35 ++++++++++++++-
 components/nifbullet/bulletnifloader.cpp      | 44 +++++++++++++++----
 components/nifbullet/bulletnifloader.hpp      |  3 +-
 3 files changed, 71 insertions(+), 11 deletions(-)

diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
index e52e2f34a0..21b9e9df3a 100644
--- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
+++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
@@ -187,6 +187,7 @@ namespace Resource
         return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get())
             && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get())
             && lhs.mCollisionBox == rhs.mCollisionBox
+            && lhs.mCollisionType == rhs.mCollisionType
             && lhs.mAnimatedShapes == rhs.mAnimatedShapes;
     }
 
@@ -197,6 +198,7 @@ namespace Resource
             << value.mAvoidCollisionShape.get() << ", "
             << value.mCollisionBox << ", "
             << value.mAnimatedShapes
+            << ", collisionType=" << value.mCollisionType
             << "}";
     }
 }
@@ -1034,7 +1036,7 @@ namespace
     TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision)
     {
         mNiStringExtraData.next = Nif::ExtraPtr(&mNiStringExtraData2);
-        mNiStringExtraData2.string = "NC___";
+        mNiStringExtraData2.string = "NCC__";
         mNiStringExtraData2.recType = Nif::RC_NiStringExtraData;
         mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData);
         mNiTriShape.parents.push_back(&mNiNode);
@@ -1054,7 +1056,6 @@ namespace
         EXPECT_EQ(*result, expected);
     }
 
-
     TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision)
     {
         mNiStringExtraData.string = "NC___";
@@ -1100,6 +1101,36 @@ namespace
         EXPECT_EQ(*result, expected);
     }
 
+    TEST_F(TestBulletNifLoader, for_empty_root_collision_node_without_nc_should_return_shape_with_cameraonly_collision)
+    {
+        Nif::NiTriShape niTriShape;
+        Nif::NiNode emptyCollisionNode;
+        init(niTriShape);
+        init(emptyCollisionNode);
+
+        niTriShape.data = Nif::NiGeometryDataPtr(&mNiTriShapeData);
+        niTriShape.parents.push_back(&mNiNode);
+
+        emptyCollisionNode.recType = Nif::RC_RootCollisionNode;
+        emptyCollisionNode.parents.push_back(&mNiNode);
+
+        mNiNode.children = Nif::NodeList(std::vector<Nif::NodePtr>(
+            {Nif::NodePtr(&niTriShape), Nif::NodePtr(&emptyCollisionNode)}));
+
+        EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1));
+        EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode));
+        EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif"));
+        const auto result = mLoader.load(mNifFile);
+
+        std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
+        triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
+        Resource::BulletShape expected;
+        expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true));
+        expected.mCollisionType = Resource::BulletShape::CollisionType::Camera;
+
+        EXPECT_EQ(*result, expected);
+    }
+
     TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_mrk_should_return_shape_with_null_collision_shape)
     {
         mNiStringExtraData.string = "MRK";
diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp
index 49f5e8f069..bae5bd4565 100644
--- a/components/nifbullet/bulletnifloader.cpp
+++ b/components/nifbullet/bulletnifloader.cpp
@@ -220,8 +220,12 @@ osg::ref_ptr<Resource::BulletShape> BulletNifLoader::load(const Nif::File& nif)
     // from the collision data present in every root node.
     for (const Nif::Node* node : roots)
     {
-        bool autogenerated = hasAutoGeneratedCollision(*node);
-        handleNode(filename, *node, nullptr, 0, autogenerated, isAnimated, autogenerated, false, mShape->mCollisionType);
+        bool hasCollisionNode = hasRootCollisionNode(*node);
+        bool hasCollisionShape = hasCollisionNode && !collisionShapeIsEmpty(*node);
+        if (hasCollisionNode && !hasCollisionShape)
+            mShape->mCollisionType = Resource::BulletShape::CollisionType::Camera;
+        bool generateCollisionShape = !hasCollisionShape;
+        handleNode(filename, *node, nullptr, 0, generateCollisionShape, isAnimated, generateCollisionShape, false, mShape->mCollisionType);
     }
 
     if (mCompoundShape)
@@ -295,18 +299,37 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node& node, const std::string&
     return false;
 }
 
-bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node& rootNode)
+bool BulletNifLoader::hasRootCollisionNode(const Nif::Node& rootNode) const
 {
     if (const Nif::NiNode* ninode = dynamic_cast<const Nif::NiNode*>(&rootNode))
     {
         const Nif::NodeList &list = ninode->children;
         for(size_t i = 0;i < list.length();i++)
         {
-            if(!list[i].empty())
-            {
-                if(list[i].getPtr()->recType == Nif::RC_RootCollisionNode)
-                    return false;
-            }
+            if(list[i].empty())
+                continue;
+            if (list[i].getPtr()->recType == Nif::RC_RootCollisionNode)
+                return true;
+        }
+    }
+    return false;
+}
+
+bool BulletNifLoader::collisionShapeIsEmpty(const Nif::Node& rootNode) const
+{
+    if (const Nif::NiNode* ninode = dynamic_cast<const Nif::NiNode*>(&rootNode))
+    {
+        const Nif::NodeList &list = ninode->children;
+        for(size_t i = 0;i < list.length();i++)
+        {
+            if(list[i].empty())
+                continue;
+            const Nif::Node* childNode = list[i].getPtr();
+            if (childNode->recType != Nif::RC_RootCollisionNode)
+                continue;
+            const Nif::NiNode* niChildnode = static_cast<const Nif::NiNode*>(childNode);  // RootCollisionNode is always a NiNode
+            if (childNode->hasBounds || niChildnode->children.length() > 0)
+                return false;
         }
     }
     return true;
@@ -319,6 +342,11 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node& n
     if (node.recType == Nif::RC_NiCollisionSwitch && !node.collisionActive())
         return;
 
+    // If RootCollisionNode is empty we treat it as NCC flag and autogenerate collision shape as there was no RootCollisionNode.
+    // So ignoring it here if `autogenerated` is true and collisionType was set to `Camera`.
+    if (node.recType == Nif::RC_RootCollisionNode && autogenerated && collisionType == Resource::BulletShape::CollisionType::Camera)
+        return;
+
     // Accumulate the flags from all the child nodes. This works for all
     // the flags we currently use, at least.
     flags |= node.flags;
diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp
index e90c882bc3..c674bbbd13 100644
--- a/components/nifbullet/bulletnifloader.hpp
+++ b/components/nifbullet/bulletnifloader.hpp
@@ -59,7 +59,8 @@ private:
     void handleNode(const std::string& fileName, const Nif::Node& node,const Nif::Parent* parent, int flags,
         bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid, unsigned int& cameraOnlyCollision);
 
-    bool hasAutoGeneratedCollision(const Nif::Node& rootNode);
+    bool hasRootCollisionNode(const Nif::Node& rootNode) const;
+    bool collisionShapeIsEmpty(const Nif::Node& rootNode) const;
 
     void handleNiTriShape(const Nif::NiGeometry& nifNode, const Nif::Parent* parent, const osg::Matrixf& transform,
         bool isAnimated, bool avoid);